mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-22 03:05:32 +02:00
ui: comprehensive UI polish pass
- LoginPage/SetupPage: centered card layout with display font, styled fields and submit button - Topbar: fix duplicate Settings nav item; add styled user-menu with username + logout button - RunsView: format ISO timestamps to readable dates, add colored status badges (completed/failed/running) - AdminView: view-tabs system, styled admin-table, admin-input, status-badge (active/disabled), btn-primary - SettingsView: replace all inline styles with form-section/form-field/field-input/btn-primary/btn-danger - CollectionsView: restructure create form with proper field labels and btn-primary - TagsView: add Tags section heading with separator - ContextRail: show visibility as human-readable label not raw number; fix assign-error class - styles.css: add auth-loading, view-tabs, form utilities, btn variants, status badges, run-status pills, token-banner/row, checkbox-row, coll-create-form, tag-tree-header CSS
This commit is contained in:
parent
3ccfcce87b
commit
4311e85f95
21 changed files with 737 additions and 440 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
40
crates/archivr-server/static/assets/index-CWb0sQjJ.js
Normal file
40
crates/archivr-server/static/assets/index-CWb0sQjJ.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
crates/archivr-server/static/assets/index-MrdP6h9x.css
Normal file
1
crates/archivr-server/static/assets/index-MrdP6h9x.css
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -4,8 +4,8 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Archivr</title>
|
<title>Archivr</title>
|
||||||
<script type="module" crossorigin src="/assets/index-C6h0jcH3.js"></script>
|
<script type="module" crossorigin src="/assets/index-CWb0sQjJ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D40QFUPh.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-MrdP6h9x.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
|
|
@ -114,13 +114,13 @@ export default function AdminView({ archives }) {
|
||||||
<section id="admin-view" className="view admin-view is-active">
|
<section id="admin-view" className="view admin-view is-active">
|
||||||
<h1>Admin</h1>
|
<h1>Admin</h1>
|
||||||
|
|
||||||
<div className="admin-tabs">
|
<div className="view-tabs">
|
||||||
<button className={`admin-tab${tab === 'users' ? ' is-active' : ''}`} onClick={() => setTab('users')}>Users</button>
|
<button className={`view-tab${tab === 'users' ? ' is-active' : ''}`} onClick={() => setTab('users')}>Users</button>
|
||||||
<button className={`admin-tab${tab === 'roles' ? ' is-active' : ''}`} onClick={() => setTab('roles')}>Roles</button>
|
<button className={`view-tab${tab === 'roles' ? ' is-active' : ''}`} onClick={() => setTab('roles')}>Roles</button>
|
||||||
<button className={`admin-tab${tab === 'archives' ? ' is-active' : ''}`} onClick={() => setTab('archives')}>Archives</button>
|
<button className={`view-tab${tab === 'archives' ? ' is-active' : ''}`} onClick={() => setTab('archives')}>Archives</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <div className="capture-error">{error}</div>}
|
{error && <div className="form-msg form-msg--err">{error}</div>}
|
||||||
|
|
||||||
{tab === 'users' && (
|
{tab === 'users' && (
|
||||||
<div className="admin-section">
|
<div className="admin-section">
|
||||||
|
|
@ -156,9 +156,9 @@ export default function AdminView({ archives }) {
|
||||||
value={newPassword} onChange={e => setNewPassword(e.target.value)} required />
|
value={newPassword} onChange={e => setNewPassword(e.target.value)} required />
|
||||||
<input className="admin-input" type="email" placeholder="Email (optional)"
|
<input className="admin-input" type="email" placeholder="Email (optional)"
|
||||||
value={newEmail} onChange={e => setNewEmail(e.target.value)} />
|
value={newEmail} onChange={e => setNewEmail(e.target.value)} />
|
||||||
{createError && <div className="capture-error">{createError}</div>}
|
{createError && <div className="form-msg form-msg--err">{createError}</div>}
|
||||||
<button className="capture-submit" type="submit" disabled={creating}>
|
<button className="btn-primary" type="submit" disabled={creating}>
|
||||||
{creating ? 'Creating…' : 'Create User'}
|
{creating ? 'Creating\u2026' : 'Create User'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -192,9 +192,9 @@ export default function AdminView({ archives }) {
|
||||||
onChange={e => setNewRoleSlug(e.target.value)} required />
|
onChange={e => setNewRoleSlug(e.target.value)} required />
|
||||||
<input className="admin-input" placeholder="Display Name (e.g. Moderator)"
|
<input className="admin-input" placeholder="Display Name (e.g. Moderator)"
|
||||||
value={newRoleName} onChange={e => setNewRoleName(e.target.value)} required />
|
value={newRoleName} onChange={e => setNewRoleName(e.target.value)} required />
|
||||||
{roleCreateError && <div className="capture-error">{roleCreateError}</div>}
|
{roleCreateError && <div className="form-msg form-msg--err">{roleCreateError}</div>}
|
||||||
<button className="capture-submit" type="submit" disabled={creatingRole}>
|
<button className="btn-primary" type="submit" disabled={creatingRole}>
|
||||||
{creatingRole ? 'Creating…' : 'Create Role'}
|
{creatingRole ? 'Creating\u2026' : 'Create Role'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -324,40 +324,43 @@ export default function CollectionsView({ archiveId }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Create form */}
|
{/* Create form */}
|
||||||
<details className="coll-create-details" style={{ marginTop: '1.5rem' }}>
|
<details className="coll-create-details">
|
||||||
<summary style={{ cursor: 'pointer', fontWeight: 600 }}>Create collection</summary>
|
<summary>+ Create collection</summary>
|
||||||
<form onSubmit={handleCreate} style={{ marginTop: '0.75rem', display: 'flex', flexDirection: 'column', gap: '0.5rem', maxWidth: 400 }}>
|
<form className="coll-create-form" onSubmit={handleCreate}>
|
||||||
<label>
|
<div className="form-field">
|
||||||
Name
|
<label className="form-label" htmlFor="coll-name">Name</label>
|
||||||
<input
|
<input
|
||||||
className="capture-input"
|
className="capture-input"
|
||||||
|
id="coll-name"
|
||||||
type="text"
|
type="text"
|
||||||
value={newName}
|
value={newName}
|
||||||
onChange={e => { setNewName(e.target.value); if (!newSlug) setNewSlug(e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')) }}
|
onChange={e => { setNewName(e.target.value); if (!newSlug) setNewSlug(e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')) }}
|
||||||
placeholder="My Collection"
|
placeholder="My Collection"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
<label>
|
<div className="form-field">
|
||||||
Slug
|
<label className="form-label" htmlFor="coll-slug">Slug</label>
|
||||||
<input
|
<input
|
||||||
className="capture-input"
|
className="capture-input"
|
||||||
|
id="coll-slug"
|
||||||
type="text"
|
type="text"
|
||||||
value={newSlug}
|
value={newSlug}
|
||||||
onChange={e => setNewSlug(e.target.value)}
|
onChange={e => setNewSlug(e.target.value)}
|
||||||
placeholder="my-collection"
|
placeholder="my-collection"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
<label>
|
<div className="form-field">
|
||||||
Default visibility
|
<label className="form-label" htmlFor="coll-vis">Default visibility</label>
|
||||||
<select className="capture-input" style={{ height: 42 }} value={newVis} onChange={e => setNewVis(Number(e.target.value))}>
|
<select className="capture-input" id="coll-vis" style={{ height: 42 }}
|
||||||
|
value={newVis} onChange={e => setNewVis(Number(e.target.value))}>
|
||||||
{VIS_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
{VIS_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</div>
|
||||||
{createError && <div className="collections-error">{createError}</div>}
|
{createError && <div className="collections-error">{createError}</div>}
|
||||||
<button className="capture-submit" type="submit" disabled={creating} style={{ alignSelf: 'flex-start', padding: '8px 20px' }}>
|
<button className="btn-primary" type="submit" disabled={creating}>
|
||||||
{creating ? 'Creating…' : 'Create'}
|
{creating ? 'Creating\u2026' : 'Create collection'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet,
|
||||||
['Added', formatTimestamp(detail.summary.archived_at)],
|
['Added', formatTimestamp(detail.summary.archived_at)],
|
||||||
['Source', detail.summary.source_kind],
|
['Source', detail.summary.source_kind],
|
||||||
['Type', detail.summary.entity_kind],
|
['Type', detail.summary.entity_kind],
|
||||||
['Visibility', detail.summary.visibility],
|
['Visibility', VIS_LABEL[detail.summary.visibility] ?? detail.summary.visibility],
|
||||||
['Root', detail.structured_root_relpath],
|
['Root', detail.structured_root_relpath],
|
||||||
] : []
|
] : []
|
||||||
|
|
||||||
|
|
@ -158,7 +158,7 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet,
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{assignError && (
|
{assignError && (
|
||||||
<p style={{ color: 'var(--accent)', fontSize: 13, margin: '0 0 8px' }}>{assignError}</p>
|
<p className="form-msg form-msg--err" style={{ margin: '0 0 8px' }}>{assignError}</p>
|
||||||
)}
|
)}
|
||||||
<div className="tag-input-wrap">
|
<div className="tag-input-wrap">
|
||||||
<span className="hash">/</span>
|
<span className="hash">/</span>
|
||||||
|
|
|
||||||
|
|
@ -23,32 +23,41 @@ export default function LoginPage({ onLogin }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-page">
|
<div className="login-page">
|
||||||
<h1>Archivr</h1>
|
<div className="login-card">
|
||||||
|
<h1 className="login-brand">Archivr</h1>
|
||||||
|
<p className="login-tagline">Sign in to your archive</p>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<label>
|
<div className="login-field">
|
||||||
Username
|
<label className="login-label" htmlFor="login-username">Username</label>
|
||||||
<input
|
<input
|
||||||
|
className="login-input"
|
||||||
|
id="login-username"
|
||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={e => setUsername(e.target.value)}
|
onChange={e => setUsername(e.target.value)}
|
||||||
autoFocus
|
autoFocus
|
||||||
required
|
required
|
||||||
|
autoComplete="username"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
<label>
|
<div className="login-field">
|
||||||
Password
|
<label className="login-label" htmlFor="login-password">Password</label>
|
||||||
<input
|
<input
|
||||||
|
className="login-input"
|
||||||
|
id="login-password"
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={e => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
|
autoComplete="current-password"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="login-error">{error}</p>}
|
||||||
<button type="submit" disabled={loading}>
|
<button className="login-submit" type="submit" disabled={loading}>
|
||||||
{loading ? 'Logging in\u2026' : 'Log in'}
|
{loading ? 'Signing in\u2026' : 'Sign in'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,50 @@
|
||||||
|
function fmtDate(iso) {
|
||||||
|
if (!iso) return '—';
|
||||||
|
try {
|
||||||
|
return new Date(iso).toLocaleString(undefined, {
|
||||||
|
year: 'numeric', month: 'short', day: 'numeric',
|
||||||
|
hour: '2-digit', minute: '2-digit',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return iso;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge({ status }) {
|
||||||
|
const cls = status === 'completed' ? 'run-status--completed'
|
||||||
|
: status === 'failed' ? 'run-status--failed'
|
||||||
|
: status === 'running' ? 'run-status--running'
|
||||||
|
: '';
|
||||||
|
return <span className={`run-status ${cls}`}>{status || '—'}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
export default function RunsView({ runs }) {
|
export default function RunsView({ runs }) {
|
||||||
return (
|
return (
|
||||||
<section id="runs-view" className="view is-active">
|
<section id="runs-view" className="view is-active">
|
||||||
<table className="entry-table">
|
<table className="entry-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Started</th><th>Status</th><th>Requested</th><th>Completed</th><th>Failed</th>
|
<th>Started</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Requested</th>
|
||||||
|
<th>Completed</th>
|
||||||
|
<th>Failed</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{runs.map((run, i) => (
|
{runs.length === 0 ? (
|
||||||
|
<tr><td colSpan={5} style={{ color: 'var(--muted)', padding: '24px 16px', textAlign: 'center' }}>No runs yet.</td></tr>
|
||||||
|
) : runs.map((run, i) => (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
<td>{run.started_at ?? ''}</td>
|
<td>{fmtDate(run.started_at)}</td>
|
||||||
<td>{run.status ?? ''}</td>
|
<td><StatusBadge status={run.status} /></td>
|
||||||
<td>{run.requested_count ?? ''}</td>
|
<td>{run.requested_count ?? '—'}</td>
|
||||||
<td>{run.completed_count ?? ''}</td>
|
<td>{run.completed_count ?? '—'}</td>
|
||||||
<td>{run.failed_count ?? ''}</td>
|
<td>{run.failed_count ?? '—'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,23 +7,24 @@ import {
|
||||||
} from '../api.js'
|
} from '../api.js'
|
||||||
|
|
||||||
const ROLE_ADMIN = 4
|
const ROLE_ADMIN = 4
|
||||||
const ROLE_OWNER = 8
|
|
||||||
|
|
||||||
export default function SettingsView() {
|
export default function SettingsView() {
|
||||||
const { currentUser, setCurrentUser } = useContext(AuthContext) ?? {}
|
const { currentUser, setCurrentUser } = useContext(AuthContext) ?? {}
|
||||||
const isAdmin = currentUser && ((currentUser.role_bits & ROLE_ADMIN) !== 0)
|
const isAdmin = currentUser && ((currentUser.role_bits & ROLE_ADMIN) !== 0)
|
||||||
const [tab, setTab] = useState('profile')
|
const [tab, setTab] = useState('profile')
|
||||||
|
|
||||||
|
const tabs = ['profile', 'tokens', ...(isAdmin ? ['instance'] : [])]
|
||||||
|
const tabLabels = { profile: 'Profile', tokens: 'API Tokens', instance: 'Instance' }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="admin-view">
|
<section className="admin-view">
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
<div className="admin-tabs" style={{ display: 'flex', gap: 12, marginBottom: 20 }}>
|
<div className="view-tabs">
|
||||||
{['profile', 'tokens', ...(isAdmin ? ['instance'] : [])].map(t => (
|
{tabs.map(t => (
|
||||||
<button key={t}
|
<button key={t}
|
||||||
className={`nav-link${tab === t ? ' is-active' : ''}`}
|
className={`view-tab${tab === t ? ' is-active' : ''}`}
|
||||||
style={{ borderBottom: tab === t ? '2px solid var(--accent)' : undefined, color: 'var(--ink)' }}
|
|
||||||
onClick={() => setTab(t)}>
|
onClick={() => setTab(t)}>
|
||||||
{t === 'profile' ? 'Profile' : t === 'tokens' ? 'API Tokens' : 'Instance'}
|
{tabLabels[t]}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -78,34 +79,43 @@ function ProfileTab({ currentUser, setCurrentUser }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: 480 }}>
|
<div style={{ maxWidth: 440 }}>
|
||||||
<div className="admin-section">
|
<div className="form-section">
|
||||||
<h2 style={{ fontSize: 15, marginBottom: 12 }}>Display Name</h2>
|
<h2>Display Name</h2>
|
||||||
<form className="admin-form" onSubmit={handleSaveProfile}>
|
<form onSubmit={handleSaveProfile}>
|
||||||
<input className="admin-input" placeholder={currentUser?.username ?? ''}
|
<div className="form-field">
|
||||||
|
<label className="form-label" htmlFor="display-name">Name shown in the UI</label>
|
||||||
|
<input className="field-input" id="display-name"
|
||||||
|
placeholder={currentUser?.username ?? ''}
|
||||||
value={displayName} onChange={e => setDisplayName(e.target.value)} />
|
value={displayName} onChange={e => setDisplayName(e.target.value)} />
|
||||||
{saveMsg && (
|
</div>
|
||||||
<div className={saveMsg.ok ? 'muted' : 'capture-error'}>{saveMsg.text}</div>
|
{saveMsg && <div className={`form-msg form-msg--${saveMsg.ok ? 'ok' : 'err'}`}>{saveMsg.text}</div>}
|
||||||
)}
|
<button className="btn-primary" type="submit" disabled={saving}>
|
||||||
<button className="capture-submit" type="submit" disabled={saving}>
|
|
||||||
{saving ? 'Saving\u2026' : 'Save'}
|
{saving ? 'Saving\u2026' : 'Save'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="admin-section" style={{ marginTop: 28 }}>
|
<div className="form-section">
|
||||||
<h2 style={{ fontSize: 15, marginBottom: 12 }}>Change Password</h2>
|
<h2>Change Password</h2>
|
||||||
<form className="admin-form" onSubmit={handleChangePassword}>
|
<form onSubmit={handleChangePassword}>
|
||||||
<input className="admin-input" type="password" placeholder="Current password"
|
<div className="form-field">
|
||||||
|
<label className="form-label" htmlFor="cur-pw">Current password</label>
|
||||||
|
<input className="field-input" id="cur-pw" type="password"
|
||||||
value={curPw} onChange={e => setCurPw(e.target.value)} required />
|
value={curPw} onChange={e => setCurPw(e.target.value)} required />
|
||||||
<input className="admin-input" type="password" placeholder="New password"
|
</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label className="form-label" htmlFor="new-pw">New password</label>
|
||||||
|
<input className="field-input" id="new-pw" type="password"
|
||||||
value={newPw} onChange={e => setNewPw(e.target.value)} required />
|
value={newPw} onChange={e => setNewPw(e.target.value)} required />
|
||||||
<input className="admin-input" type="password" placeholder="Confirm new password"
|
</div>
|
||||||
|
<div className="form-field">
|
||||||
|
<label className="form-label" htmlFor="confirm-pw">Confirm new password</label>
|
||||||
|
<input className="field-input" id="confirm-pw" type="password"
|
||||||
value={confirmPw} onChange={e => setConfirmPw(e.target.value)} required />
|
value={confirmPw} onChange={e => setConfirmPw(e.target.value)} required />
|
||||||
{pwMsg && (
|
</div>
|
||||||
<div className={pwMsg.ok ? 'muted' : 'capture-error'}>{pwMsg.text}</div>
|
{pwMsg && <div className={`form-msg form-msg--${pwMsg.ok ? 'ok' : 'err'}`}>{pwMsg.text}</div>}
|
||||||
)}
|
<button className="btn-primary" type="submit" disabled={pwSaving}>
|
||||||
<button className="capture-submit" type="submit" disabled={pwSaving}>
|
|
||||||
{pwSaving ? 'Changing\u2026' : 'Change Password'}
|
{pwSaving ? 'Changing\u2026' : 'Change Password'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -120,7 +130,7 @@ function TokensTab() {
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [newName, setNewName] = useState('')
|
const [newName, setNewName] = useState('')
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
const [newToken, setNewToken] = useState(null) // { raw_token, name }
|
const [newToken, setNewToken] = useState(null)
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
setLoading(true); setError(null)
|
setLoading(true); setError(null)
|
||||||
|
|
@ -157,42 +167,40 @@ function TokensTab() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: 640 }}>
|
<div style={{ maxWidth: 600 }}>
|
||||||
<h2 style={{ fontSize: 15, marginBottom: 12 }}>API Tokens</h2>
|
<div className="form-section">
|
||||||
|
<h2>API Tokens</h2>
|
||||||
{newToken && (
|
{newToken && (
|
||||||
<div style={{ background: '#e8f5e9', border: '1px solid #a5d6a7', padding: '12px 14px', marginBottom: 16, fontSize: 13 }}>
|
<div className="token-banner">
|
||||||
<strong>Token created.</strong> Copy it now \u2014 it will not be shown again.<br />
|
<strong>Token created.</strong> Copy it now — it won't be shown again.
|
||||||
<code style={{ wordBreak: 'break-all', display: 'block', marginTop: 6, padding: '6px 8px', background: '#f5f5f5', border: '1px solid #ddd' }}>
|
<code>{newToken.raw_token}</code>
|
||||||
{newToken.raw_token}
|
<button className="token-dismiss" onClick={() => setNewToken(null)}>Dismiss</button>
|
||||||
</code>
|
|
||||||
<button style={{ marginTop: 8, fontSize: 12, border: '1px solid #aaa', background: 'none', cursor: 'pointer' }}
|
|
||||||
onClick={() => setNewToken(null)}>Dismiss</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<form className="admin-form" onSubmit={handleCreate} style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
<form className="token-create-row" onSubmit={handleCreate}>
|
||||||
<input className="admin-input" placeholder="Token name" value={newName}
|
<input className="field-input field-input--flex" placeholder="Token name"
|
||||||
onChange={e => setNewName(e.target.value)} style={{ flex: 1 }} required />
|
value={newName} onChange={e => setNewName(e.target.value)} required />
|
||||||
<button className="capture-submit" type="submit" disabled={creating}>
|
<button className="btn-primary" type="submit" disabled={creating}>
|
||||||
{creating ? 'Creating\u2026' : 'Create'}
|
{creating ? 'Creating\u2026' : 'Create token'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{error && <div className="capture-error">{error}</div>}
|
{error && <div className="form-msg form-msg--err">{error}</div>}
|
||||||
{loading ? <div className="muted">Loading\u2026</div> : (
|
{loading ? (
|
||||||
<div className="admin-list">
|
<div className="muted">Loading\u2026</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
{tokens.length === 0 && <div className="muted">No tokens yet.</div>}
|
{tokens.length === 0 && <div className="muted">No tokens yet.</div>}
|
||||||
{tokens.map(tok => (
|
{tokens.map(tok => (
|
||||||
<div key={tok.token_uid} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
<div key={tok.token_uid} className="token-row">
|
||||||
border: '1px solid var(--line)', background: 'var(--paper-3)', padding: '10px 12px' }}>
|
<div className="token-row-info">
|
||||||
<div>
|
<strong>{tok.name}</strong>
|
||||||
<strong style={{ fontSize: 14 }}>{tok.name}</strong>
|
<div className="muted">
|
||||||
<div className="muted" style={{ fontSize: 12, marginTop: 2 }}>
|
|
||||||
Created {tok.created_at.slice(0, 10)}
|
Created {tok.created_at.slice(0, 10)}
|
||||||
{tok.last_used_at && ` \u00b7 Last used ${tok.last_used_at.slice(0, 10)}`}
|
{tok.last_used_at && ` \u00b7 Last used ${tok.last_used_at.slice(0, 10)}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => handleDelete(tok.token_uid)}
|
<button className="btn-danger" style={{ fontSize: 12, padding: '4px 10px' }}
|
||||||
style={{ border: '1px solid var(--line)', background: 'none', cursor: 'pointer',
|
onClick={() => handleDelete(tok.token_uid)}>
|
||||||
color: 'var(--accent)', fontSize: 12, padding: '4px 10px' }}>
|
|
||||||
Revoke
|
Revoke
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -200,6 +208,7 @@ function TokensTab() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,40 +241,40 @@ function InstanceTab() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) return <div className="muted">Loading\u2026</div>
|
if (loading) return <div className="muted">Loading\u2026</div>
|
||||||
if (error) return <div className="capture-error">{error}</div>
|
if (error) return <div className="form-msg form-msg--err">{error}</div>
|
||||||
if (!settings) return null
|
if (!settings) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: 480 }}>
|
<div style={{ maxWidth: 440 }}>
|
||||||
<h2 style={{ fontSize: 15, marginBottom: 12 }}>Instance Settings</h2>
|
<div className="form-section">
|
||||||
|
<h2>Instance Settings</h2>
|
||||||
<form onSubmit={handleSave}>
|
<form onSubmit={handleSave}>
|
||||||
<div className="admin-section">
|
|
||||||
{[
|
{[
|
||||||
['public_index_enabled', 'Public index (unauthenticated browsing)'],
|
['public_index_enabled', 'Public index (unauthenticated browsing)'],
|
||||||
['public_entry_content_enabled', 'Public entry content'],
|
['public_entry_content_enabled', 'Public entry content'],
|
||||||
['open_registration_enabled', 'Open registration'],
|
['open_registration_enabled', 'Open registration'],
|
||||||
].map(([key, label]) => (
|
].map(([key, label]) => (
|
||||||
<label key={key} style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12, cursor: 'pointer' }}>
|
<label key={key} className="checkbox-row">
|
||||||
<input type="checkbox" checked={!!settings[key]}
|
<input type="checkbox" checked={!!settings[key]}
|
||||||
onChange={e => setSettings(s => ({ ...s, [key]: e.target.checked }))} />
|
onChange={e => setSettings(s => ({ ...s, [key]: e.target.checked }))} />
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div className="form-field" style={{ marginTop: 4 }}>
|
||||||
<label style={{ display: 'block', fontWeight: 600, marginBottom: 4, fontSize: 13 }}>Default entry visibility</label>
|
<label className="form-label">Default entry visibility</label>
|
||||||
<select className="admin-input" value={settings.default_entry_visibility}
|
<select className="field-input" value={settings.default_entry_visibility}
|
||||||
onChange={e => setSettings(s => ({ ...s, default_entry_visibility: Number(e.target.value) }))}>
|
onChange={e => setSettings(s => ({ ...s, default_entry_visibility: Number(e.target.value) }))}>
|
||||||
<option value={0}>Private (0)</option>
|
<option value={0}>Private</option>
|
||||||
<option value={2}>Unlisted (2)</option>
|
<option value={2}>Unlisted</option>
|
||||||
<option value={3}>Public (3)</option>
|
<option value={3}>Public</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{saveMsg && <div className={`form-msg form-msg--${saveMsg.ok ? 'ok' : 'err'}`}>{saveMsg.text}</div>}
|
||||||
{saveMsg && <div className={saveMsg.ok ? 'muted' : 'capture-error'}>{saveMsg.text}</div>}
|
<button className="btn-primary" type="submit" disabled={saving}>
|
||||||
<button className="capture-submit" type="submit" disabled={saving}>
|
|
||||||
{saving ? 'Saving\u2026' : 'Save Settings'}
|
{saving ? 'Saving\u2026' : 'Save Settings'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,42 +32,53 @@ export default function SetupPage({ onComplete }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="setup-page">
|
<div className="setup-page">
|
||||||
<h1>Welcome to Archivr</h1>
|
<div className="setup-card">
|
||||||
<p>Create your owner account to get started.</p>
|
<h1 className="setup-brand">Archivr</h1>
|
||||||
|
<p className="setup-tagline">Create your owner account to get started.</p>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<label>
|
<div className="setup-field">
|
||||||
Username
|
<label className="setup-label" htmlFor="setup-username">Username</label>
|
||||||
<input
|
<input
|
||||||
|
className="setup-input"
|
||||||
|
id="setup-username"
|
||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={e => setUsername(e.target.value)}
|
onChange={e => setUsername(e.target.value)}
|
||||||
autoFocus
|
autoFocus
|
||||||
required
|
required
|
||||||
|
autoComplete="username"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
<label>
|
<div className="setup-field">
|
||||||
Password
|
<label className="setup-label" htmlFor="setup-password">Password</label>
|
||||||
<input
|
<input
|
||||||
|
className="setup-input"
|
||||||
|
id="setup-password"
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={e => setPassword(e.target.value)}
|
onChange={e => setPassword(e.target.value)}
|
||||||
required
|
required
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
<label>
|
<div className="setup-field">
|
||||||
Confirm password
|
<label className="setup-label" htmlFor="setup-confirm">Confirm password</label>
|
||||||
<input
|
<input
|
||||||
|
className="setup-input"
|
||||||
|
id="setup-confirm"
|
||||||
type="password"
|
type="password"
|
||||||
value={confirm}
|
value={confirm}
|
||||||
onChange={e => setConfirm(e.target.value)}
|
onChange={e => setConfirm(e.target.value)}
|
||||||
required
|
required
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
{error && <p className="error">{error}</p>}
|
{error && <p className="setup-error">{error}</p>}
|
||||||
<button type="submit" disabled={loading}>
|
<button className="setup-submit" type="submit" disabled={loading}>
|
||||||
{loading ? 'Creating account\u2026' : 'Create account'}
|
{loading ? 'Creating account\u2026' : 'Create account'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,14 @@ export default function TagsView({ tagNodes, tagFilter, onTagFilterSet, onViewCh
|
||||||
return (
|
return (
|
||||||
<section id="tags-view" className="view is-active">
|
<section id="tags-view" className="view is-active">
|
||||||
<div className="tag-tree">
|
<div className="tag-tree">
|
||||||
|
<div className="tag-tree-header">
|
||||||
|
<span className="tag-tree-title">Tags</span>
|
||||||
|
{tagFilter && (
|
||||||
|
<span className="tag-tree-active">Filtering: {tagFilter}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{tagNodes.length === 0 ? (
|
{tagNodes.length === 0 ? (
|
||||||
<div>No tags yet.</div>
|
<p className="muted" style={{ padding: '8px 0' }}>No tags yet.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="tag-tree-list">
|
<ul className="tag-tree-list">
|
||||||
{tagNodes.map(node => (
|
{tagNodes.map(node => (
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,8 @@ export default function Topbar({ archives, archiveId, onArchiveChange, view, onV
|
||||||
<button className="capture-button" onClick={onCaptureClick}>+ Capture</button>
|
<button className="capture-button" onClick={onCaptureClick}>+ Capture</button>
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
<div className="user-menu">
|
<div className="user-menu">
|
||||||
<span className="username">{currentUser.display_name || currentUser.username}</span>
|
<span className="user-name">{currentUser.display_name || currentUser.username}</span>
|
||||||
<button className="nav-link" onClick={() => onViewChange('settings')} style={{ color: '#d7cdbf', fontSize: 13 }}>Settings</button>
|
<button className="logout-btn" onClick={handleLogout} disabled={loggingOut}>
|
||||||
<button onClick={handleLogout} disabled={loggingOut} className="logout-btn">
|
|
||||||
{loggingOut ? 'Logging out\u2026' : 'Log out'}
|
{loggingOut ? 'Logging out\u2026' : 'Log out'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -564,6 +564,7 @@ select {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);
|
||||||
}
|
}
|
||||||
|
.form-field .capture-input { margin-bottom: 0; }
|
||||||
.capture-error {
|
.capture-error {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|
@ -604,10 +605,6 @@ select {
|
||||||
.admin-view {
|
.admin-view {
|
||||||
padding: 22px;
|
padding: 22px;
|
||||||
}
|
}
|
||||||
.admin-view h1 {
|
|
||||||
margin: 0 0 16px;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
.admin-list {
|
.admin-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
@ -622,7 +619,28 @@ select {
|
||||||
|
|
||||||
/* ── Tag tree ────────────────────────────────────────────────────────────── */
|
/* ── Tag tree ────────────────────────────────────────────────────────────── */
|
||||||
.tag-tree {
|
.tag-tree {
|
||||||
padding: 12px;
|
padding: 20px 22px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
.tag-tree-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
border-bottom: 1px solid var(--line-soft);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.tag-tree-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
}
|
||||||
|
.tag-tree-active {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted-2);
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
.tag-tree-list {
|
.tag-tree-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
@ -877,7 +895,424 @@ select {
|
||||||
}
|
}
|
||||||
.coll-add-btn:hover { opacity: 0.85; }
|
.coll-add-btn:hover { opacity: 0.85; }
|
||||||
.coll-add-btn:disabled { opacity: 0.45; cursor: default; }
|
.coll-add-btn:disabled { opacity: 0.45; cursor: default; }
|
||||||
.coll-create-details summary { font-weight: 600; cursor: pointer; }
|
/* old override — superseded by coll-create-details section below */
|
||||||
|
|
||||||
|
/* ── Auth loading state ─────────────────────────────────────────────────── */
|
||||||
|
.auth-loading {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 15px;
|
||||||
|
background: var(--paper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── User menu (topbar right) ───────────────────────────────────────────── */
|
||||||
|
.user-menu {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding-left: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.user-name {
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--muted-2);
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
.logout-btn {
|
||||||
|
border: 1px solid rgba(255,255,255,0.18);
|
||||||
|
background: transparent;
|
||||||
|
color: #c8bfb0;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 11px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.logout-btn:hover { background: rgba(255,255,255,0.1); color: var(--paper); }
|
||||||
|
.logout-btn:disabled { opacity: 0.5; cursor: default; }
|
||||||
|
|
||||||
|
/* ── Login / Setup pages ─────────────────────────────────────────────────── */
|
||||||
|
.login-page,
|
||||||
|
.setup-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--paper);
|
||||||
|
}
|
||||||
|
.login-card,
|
||||||
|
.setup-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 360px;
|
||||||
|
padding: 40px 36px 44px;
|
||||||
|
background: var(--paper-3);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r3);
|
||||||
|
box-shadow: 0 2px 20px rgba(20,29,24,0.07);
|
||||||
|
}
|
||||||
|
.login-brand,
|
||||||
|
.setup-brand {
|
||||||
|
font-family: var(--display);
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
.login-tagline,
|
||||||
|
.setup-tagline {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0 0 28px;
|
||||||
|
}
|
||||||
|
.login-field,
|
||||||
|
.setup-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.login-label,
|
||||||
|
.setup-label {
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
.login-input,
|
||||||
|
.setup-input {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--field);
|
||||||
|
color: var(--ink);
|
||||||
|
padding: 9px 11px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: var(--r2);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.login-input:focus,
|
||||||
|
.setup-input:focus { border-color: var(--accent); }
|
||||||
|
.login-error,
|
||||||
|
.setup-error {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--accent);
|
||||||
|
margin: 4px 0 8px;
|
||||||
|
}
|
||||||
|
.login-submit,
|
||||||
|
.setup-submit {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
border: none;
|
||||||
|
background: var(--top);
|
||||||
|
color: var(--paper);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 11px 16px;
|
||||||
|
border-radius: var(--r2);
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
.login-submit:hover,
|
||||||
|
.setup-submit:hover { opacity: 0.85; }
|
||||||
|
.login-submit:disabled,
|
||||||
|
.setup-submit:disabled { opacity: 0.45; cursor: default; }
|
||||||
|
|
||||||
|
/* ── Shared sub-view tabs ────────────────────────────────────────────────── */
|
||||||
|
.view-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
border-bottom: 1px solid var(--line-soft);
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
.view-tab {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 7px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
transition: color 0.12s;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
font-size: 11.5px;
|
||||||
|
}
|
||||||
|
.view-tab:hover { color: var(--ink); }
|
||||||
|
.view-tab.is-active { color: var(--ink); border-bottom-color: var(--accent); }
|
||||||
|
|
||||||
|
/* ── Shared form elements ────────────────────────────────────────────────── */
|
||||||
|
.form-section { margin-bottom: 32px; }
|
||||||
|
.form-section h2 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
}
|
||||||
|
.form-row { display: flex; gap: 8px; margin-bottom: 10px; }
|
||||||
|
.form-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.field-input {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--field);
|
||||||
|
color: var(--ink);
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 13.5px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.field-input:focus { border-color: var(--accent); }
|
||||||
|
.field-input--flex { flex: 1; }
|
||||||
|
.form-msg { font-size: 13px; margin: 6px 0; }
|
||||||
|
.form-msg--ok { color: var(--link); }
|
||||||
|
.form-msg--err { color: var(--accent); }
|
||||||
|
.btn-primary {
|
||||||
|
border: none;
|
||||||
|
background: var(--top);
|
||||||
|
color: var(--paper);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.btn-primary:hover { opacity: 0.85; }
|
||||||
|
.btn-primary:disabled { opacity: 0.45; cursor: default; }
|
||||||
|
.btn-ghost {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: none;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-ghost:hover { background: var(--paper-2); }
|
||||||
|
.btn-danger {
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
background: none;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-danger:hover { background: var(--accent); color: var(--paper); }
|
||||||
|
|
||||||
|
/* ── Admin view overrides ────────────────────────────────────────────────── */
|
||||||
|
.admin-view h1 { font-family: var(--display); font-weight: 600; font-size: 26px; margin: 0 0 20px; }
|
||||||
|
.admin-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
.admin-table th {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: var(--paper-2);
|
||||||
|
}
|
||||||
|
.admin-table th:first-child { padding-left: 16px; }
|
||||||
|
.admin-table td {
|
||||||
|
padding: 9px 10px;
|
||||||
|
border-bottom: 1px solid var(--line-soft);
|
||||||
|
color: var(--ink);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.admin-table td:first-child { padding-left: 16px; }
|
||||||
|
.admin-table tr:hover td { background: var(--paper-2); }
|
||||||
|
.admin-row-disabled td { opacity: 0.45; }
|
||||||
|
.admin-section { margin-bottom: 36px; max-width: 860px; }
|
||||||
|
.admin-section h2 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
}
|
||||||
|
.admin-section h3 {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin: 22px 0 10px;
|
||||||
|
}
|
||||||
|
.admin-form { display: flex; flex-direction: column; gap: 8px; max-width: 480px; }
|
||||||
|
.admin-input {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--field);
|
||||||
|
color: var(--ink);
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 13.5px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.admin-input:focus { border-color: var(--accent); }
|
||||||
|
.admin-action-btn {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: none;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.admin-action-btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.status-active { background: #d8eddf; color: #235c35; border: 1px solid #b4d9be; }
|
||||||
|
.status-disabled { background: #ede8e0; color: var(--muted); border: 1px solid var(--line); }
|
||||||
|
|
||||||
|
/* ── Runs view improvements ──────────────────────────────────────────────── */
|
||||||
|
.run-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.run-status--completed { background: #d8eddf; color: #235c35; border: 1px solid #b4d9be; }
|
||||||
|
.run-status--failed { background: #f5ddd8; color: #8d3f30; border: 1px solid #e0b8b0; }
|
||||||
|
.run-status--running { background: #dde8f5; color: #245f72; border: 1px solid #b8cde0; }
|
||||||
|
|
||||||
|
/* ── Token created banner ────────────────────────────────────────────────── */
|
||||||
|
.token-banner {
|
||||||
|
background: #d8eddf;
|
||||||
|
border: 1px solid #b4d9be;
|
||||||
|
padding: 12px 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: var(--r2);
|
||||||
|
}
|
||||||
|
.token-banner code {
|
||||||
|
word-break: break-all;
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--field);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r);
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||||
|
}
|
||||||
|
.token-dismiss {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.token-dismiss:hover { background: var(--paper-2); }
|
||||||
|
.token-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--paper-3);
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: var(--r);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.token-row-info strong { font-size: 14px; display: block; }
|
||||||
|
.token-row-info .muted { font-size: 12px; margin-top: 2px; }
|
||||||
|
.token-create-row { display: flex; gap: 8px; margin-bottom: 16px; }
|
||||||
|
|
||||||
|
/* ── Instance settings checkboxes ───────────────────────────────────────── */
|
||||||
|
.checkbox-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.checkbox-row input[type=checkbox] { width: 15px; height: 15px; accent-color: var(--accent); }
|
||||||
|
|
||||||
|
/* ── Collections create details ─────────────────────────────────────────── */
|
||||||
|
.coll-create-details {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.coll-create-details summary {
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--link);
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: var(--r2);
|
||||||
|
background: var(--paper-3);
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
.coll-create-details summary:hover { background: var(--paper-2); }
|
||||||
|
.coll-create-details[open] summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
||||||
|
.coll-create-form {
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-top: none;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 0 0 var(--r2) var(--r2);
|
||||||
|
background: var(--paper-3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
max-width: 440px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Responsive ──────────────────────────────────────────────────────────── */
|
/* ── Responsive ──────────────────────────────────────────────────────────── */
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue