mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +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
|
|
@ -114,13 +114,13 @@ export default function AdminView({ archives }) {
|
|||
<section id="admin-view" className="view admin-view is-active">
|
||||
<h1>Admin</h1>
|
||||
|
||||
<div className="admin-tabs">
|
||||
<button className={`admin-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={`admin-tab${tab === 'archives' ? ' is-active' : ''}`} onClick={() => setTab('archives')}>Archives</button>
|
||||
<div className="view-tabs">
|
||||
<button className={`view-tab${tab === 'users' ? ' is-active' : ''}`} onClick={() => setTab('users')}>Users</button>
|
||||
<button className={`view-tab${tab === 'roles' ? ' is-active' : ''}`} onClick={() => setTab('roles')}>Roles</button>
|
||||
<button className={`view-tab${tab === 'archives' ? ' is-active' : ''}`} onClick={() => setTab('archives')}>Archives</button>
|
||||
</div>
|
||||
|
||||
{error && <div className="capture-error">{error}</div>}
|
||||
{error && <div className="form-msg form-msg--err">{error}</div>}
|
||||
|
||||
{tab === 'users' && (
|
||||
<div className="admin-section">
|
||||
|
|
@ -156,9 +156,9 @@ export default function AdminView({ archives }) {
|
|||
value={newPassword} onChange={e => setNewPassword(e.target.value)} required />
|
||||
<input className="admin-input" type="email" placeholder="Email (optional)"
|
||||
value={newEmail} onChange={e => setNewEmail(e.target.value)} />
|
||||
{createError && <div className="capture-error">{createError}</div>}
|
||||
<button className="capture-submit" type="submit" disabled={creating}>
|
||||
{creating ? 'Creating…' : 'Create User'}
|
||||
{createError && <div className="form-msg form-msg--err">{createError}</div>}
|
||||
<button className="btn-primary" type="submit" disabled={creating}>
|
||||
{creating ? 'Creating\u2026' : 'Create User'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -192,9 +192,9 @@ export default function AdminView({ archives }) {
|
|||
onChange={e => setNewRoleSlug(e.target.value)} required />
|
||||
<input className="admin-input" placeholder="Display Name (e.g. Moderator)"
|
||||
value={newRoleName} onChange={e => setNewRoleName(e.target.value)} required />
|
||||
{roleCreateError && <div className="capture-error">{roleCreateError}</div>}
|
||||
<button className="capture-submit" type="submit" disabled={creatingRole}>
|
||||
{creatingRole ? 'Creating…' : 'Create Role'}
|
||||
{roleCreateError && <div className="form-msg form-msg--err">{roleCreateError}</div>}
|
||||
<button className="btn-primary" type="submit" disabled={creatingRole}>
|
||||
{creatingRole ? 'Creating\u2026' : 'Create Role'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -324,40 +324,43 @@ export default function CollectionsView({ archiveId }) {
|
|||
</div>
|
||||
|
||||
{/* Create form */}
|
||||
<details className="coll-create-details" style={{ marginTop: '1.5rem' }}>
|
||||
<summary style={{ cursor: 'pointer', fontWeight: 600 }}>Create collection</summary>
|
||||
<form onSubmit={handleCreate} style={{ marginTop: '0.75rem', display: 'flex', flexDirection: 'column', gap: '0.5rem', maxWidth: 400 }}>
|
||||
<label>
|
||||
Name
|
||||
<details className="coll-create-details">
|
||||
<summary>+ Create collection</summary>
|
||||
<form className="coll-create-form" onSubmit={handleCreate}>
|
||||
<div className="form-field">
|
||||
<label className="form-label" htmlFor="coll-name">Name</label>
|
||||
<input
|
||||
className="capture-input"
|
||||
id="coll-name"
|
||||
type="text"
|
||||
value={newName}
|
||||
onChange={e => { setNewName(e.target.value); if (!newSlug) setNewSlug(e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')) }}
|
||||
placeholder="My Collection"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Slug
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label className="form-label" htmlFor="coll-slug">Slug</label>
|
||||
<input
|
||||
className="capture-input"
|
||||
id="coll-slug"
|
||||
type="text"
|
||||
value={newSlug}
|
||||
onChange={e => setNewSlug(e.target.value)}
|
||||
placeholder="my-collection"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Default visibility
|
||||
<select className="capture-input" style={{ height: 42 }} value={newVis} onChange={e => setNewVis(Number(e.target.value))}>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label className="form-label" htmlFor="coll-vis">Default visibility</label>
|
||||
<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>)}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{createError && <div className="collections-error">{createError}</div>}
|
||||
<button className="capture-submit" type="submit" disabled={creating} style={{ alignSelf: 'flex-start', padding: '8px 20px' }}>
|
||||
{creating ? 'Creating…' : 'Create'}
|
||||
<button className="btn-primary" type="submit" disabled={creating}>
|
||||
{creating ? 'Creating\u2026' : 'Create collection'}
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet,
|
|||
['Added', formatTimestamp(detail.summary.archived_at)],
|
||||
['Source', detail.summary.source_kind],
|
||||
['Type', detail.summary.entity_kind],
|
||||
['Visibility', detail.summary.visibility],
|
||||
['Visibility', VIS_LABEL[detail.summary.visibility] ?? detail.summary.visibility],
|
||||
['Root', detail.structured_root_relpath],
|
||||
] : []
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet,
|
|||
</div>
|
||||
)}
|
||||
{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">
|
||||
<span className="hash">/</span>
|
||||
|
|
|
|||
|
|
@ -23,32 +23,41 @@ export default function LoginPage({ onLogin }) {
|
|||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<h1>Archivr</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label>
|
||||
Username
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
{error && <p className="error">{error}</p>}
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Logging in\u2026' : 'Log in'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="login-card">
|
||||
<h1 className="login-brand">Archivr</h1>
|
||||
<p className="login-tagline">Sign in to your archive</p>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="login-field">
|
||||
<label className="login-label" htmlFor="login-username">Username</label>
|
||||
<input
|
||||
className="login-input"
|
||||
id="login-username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
autoFocus
|
||||
required
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="login-field">
|
||||
<label className="login-label" htmlFor="login-password">Password</label>
|
||||
<input
|
||||
className="login-input"
|
||||
id="login-password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
required
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="login-error">{error}</p>}
|
||||
<button className="login-submit" type="submit" disabled={loading}>
|
||||
{loading ? 'Signing in\u2026' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
</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 }) {
|
||||
return (
|
||||
<section id="runs-view" className="view is-active">
|
||||
<table className="entry-table">
|
||||
<thead>
|
||||
<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>
|
||||
</thead>
|
||||
<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}>
|
||||
<td>{run.started_at ?? ''}</td>
|
||||
<td>{run.status ?? ''}</td>
|
||||
<td>{run.requested_count ?? ''}</td>
|
||||
<td>{run.completed_count ?? ''}</td>
|
||||
<td>{run.failed_count ?? ''}</td>
|
||||
<td>{fmtDate(run.started_at)}</td>
|
||||
<td><StatusBadge status={run.status} /></td>
|
||||
<td>{run.requested_count ?? '—'}</td>
|
||||
<td>{run.completed_count ?? '—'}</td>
|
||||
<td>{run.failed_count ?? '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,23 +7,24 @@ import {
|
|||
} from '../api.js'
|
||||
|
||||
const ROLE_ADMIN = 4
|
||||
const ROLE_OWNER = 8
|
||||
|
||||
export default function SettingsView() {
|
||||
const { currentUser, setCurrentUser } = useContext(AuthContext) ?? {}
|
||||
const isAdmin = currentUser && ((currentUser.role_bits & ROLE_ADMIN) !== 0)
|
||||
const [tab, setTab] = useState('profile')
|
||||
|
||||
const tabs = ['profile', 'tokens', ...(isAdmin ? ['instance'] : [])]
|
||||
const tabLabels = { profile: 'Profile', tokens: 'API Tokens', instance: 'Instance' }
|
||||
|
||||
return (
|
||||
<section className="admin-view">
|
||||
<h1>Settings</h1>
|
||||
<div className="admin-tabs" style={{ display: 'flex', gap: 12, marginBottom: 20 }}>
|
||||
{['profile', 'tokens', ...(isAdmin ? ['instance'] : [])].map(t => (
|
||||
<div className="view-tabs">
|
||||
{tabs.map(t => (
|
||||
<button key={t}
|
||||
className={`nav-link${tab === t ? ' is-active' : ''}`}
|
||||
style={{ borderBottom: tab === t ? '2px solid var(--accent)' : undefined, color: 'var(--ink)' }}
|
||||
className={`view-tab${tab === t ? ' is-active' : ''}`}
|
||||
onClick={() => setTab(t)}>
|
||||
{t === 'profile' ? 'Profile' : t === 'tokens' ? 'API Tokens' : 'Instance'}
|
||||
{tabLabels[t]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -78,34 +79,43 @@ function ProfileTab({ currentUser, setCurrentUser }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 480 }}>
|
||||
<div className="admin-section">
|
||||
<h2 style={{ fontSize: 15, marginBottom: 12 }}>Display Name</h2>
|
||||
<form className="admin-form" onSubmit={handleSaveProfile}>
|
||||
<input className="admin-input" placeholder={currentUser?.username ?? ''}
|
||||
value={displayName} onChange={e => setDisplayName(e.target.value)} />
|
||||
{saveMsg && (
|
||||
<div className={saveMsg.ok ? 'muted' : 'capture-error'}>{saveMsg.text}</div>
|
||||
)}
|
||||
<button className="capture-submit" type="submit" disabled={saving}>
|
||||
<div style={{ maxWidth: 440 }}>
|
||||
<div className="form-section">
|
||||
<h2>Display Name</h2>
|
||||
<form onSubmit={handleSaveProfile}>
|
||||
<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)} />
|
||||
</div>
|
||||
{saveMsg && <div className={`form-msg form-msg--${saveMsg.ok ? 'ok' : 'err'}`}>{saveMsg.text}</div>}
|
||||
<button className="btn-primary" type="submit" disabled={saving}>
|
||||
{saving ? 'Saving\u2026' : 'Save'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="admin-section" style={{ marginTop: 28 }}>
|
||||
<h2 style={{ fontSize: 15, marginBottom: 12 }}>Change Password</h2>
|
||||
<form className="admin-form" onSubmit={handleChangePassword}>
|
||||
<input className="admin-input" type="password" placeholder="Current password"
|
||||
value={curPw} onChange={e => setCurPw(e.target.value)} required />
|
||||
<input className="admin-input" type="password" placeholder="New password"
|
||||
value={newPw} onChange={e => setNewPw(e.target.value)} required />
|
||||
<input className="admin-input" type="password" placeholder="Confirm new password"
|
||||
value={confirmPw} onChange={e => setConfirmPw(e.target.value)} required />
|
||||
{pwMsg && (
|
||||
<div className={pwMsg.ok ? 'muted' : 'capture-error'}>{pwMsg.text}</div>
|
||||
)}
|
||||
<button className="capture-submit" type="submit" disabled={pwSaving}>
|
||||
<div className="form-section">
|
||||
<h2>Change Password</h2>
|
||||
<form onSubmit={handleChangePassword}>
|
||||
<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 />
|
||||
</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 />
|
||||
</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 />
|
||||
</div>
|
||||
{pwMsg && <div className={`form-msg form-msg--${pwMsg.ok ? 'ok' : 'err'}`}>{pwMsg.text}</div>}
|
||||
<button className="btn-primary" type="submit" disabled={pwSaving}>
|
||||
{pwSaving ? 'Changing\u2026' : 'Change Password'}
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -120,7 +130,7 @@ function TokensTab() {
|
|||
const [error, setError] = useState(null)
|
||||
const [newName, setNewName] = useState('')
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [newToken, setNewToken] = useState(null) // { raw_token, name }
|
||||
const [newToken, setNewToken] = useState(null)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true); setError(null)
|
||||
|
|
@ -157,48 +167,47 @@ function TokensTab() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 640 }}>
|
||||
<h2 style={{ fontSize: 15, marginBottom: 12 }}>API Tokens</h2>
|
||||
{newToken && (
|
||||
<div style={{ background: '#e8f5e9', border: '1px solid #a5d6a7', padding: '12px 14px', marginBottom: 16, fontSize: 13 }}>
|
||||
<strong>Token created.</strong> Copy it now \u2014 it will not be shown again.<br />
|
||||
<code style={{ wordBreak: 'break-all', display: 'block', marginTop: 6, padding: '6px 8px', background: '#f5f5f5', border: '1px solid #ddd' }}>
|
||||
{newToken.raw_token}
|
||||
</code>
|
||||
<button style={{ marginTop: 8, fontSize: 12, border: '1px solid #aaa', background: 'none', cursor: 'pointer' }}
|
||||
onClick={() => setNewToken(null)}>Dismiss</button>
|
||||
</div>
|
||||
)}
|
||||
<form className="admin-form" onSubmit={handleCreate} style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||
<input className="admin-input" placeholder="Token name" value={newName}
|
||||
onChange={e => setNewName(e.target.value)} style={{ flex: 1 }} required />
|
||||
<button className="capture-submit" type="submit" disabled={creating}>
|
||||
{creating ? 'Creating\u2026' : 'Create'}
|
||||
</button>
|
||||
</form>
|
||||
{error && <div className="capture-error">{error}</div>}
|
||||
{loading ? <div className="muted">Loading\u2026</div> : (
|
||||
<div className="admin-list">
|
||||
{tokens.length === 0 && <div className="muted">No tokens yet.</div>}
|
||||
{tokens.map(tok => (
|
||||
<div key={tok.token_uid} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
border: '1px solid var(--line)', background: 'var(--paper-3)', padding: '10px 12px' }}>
|
||||
<div>
|
||||
<strong style={{ fontSize: 14 }}>{tok.name}</strong>
|
||||
<div className="muted" style={{ fontSize: 12, marginTop: 2 }}>
|
||||
Created {tok.created_at.slice(0, 10)}
|
||||
{tok.last_used_at && ` \u00b7 Last used ${tok.last_used_at.slice(0, 10)}`}
|
||||
<div style={{ maxWidth: 600 }}>
|
||||
<div className="form-section">
|
||||
<h2>API Tokens</h2>
|
||||
{newToken && (
|
||||
<div className="token-banner">
|
||||
<strong>Token created.</strong> Copy it now — it won't be shown again.
|
||||
<code>{newToken.raw_token}</code>
|
||||
<button className="token-dismiss" onClick={() => setNewToken(null)}>Dismiss</button>
|
||||
</div>
|
||||
)}
|
||||
<form className="token-create-row" onSubmit={handleCreate}>
|
||||
<input className="field-input field-input--flex" placeholder="Token name"
|
||||
value={newName} onChange={e => setNewName(e.target.value)} required />
|
||||
<button className="btn-primary" type="submit" disabled={creating}>
|
||||
{creating ? 'Creating\u2026' : 'Create token'}
|
||||
</button>
|
||||
</form>
|
||||
{error && <div className="form-msg form-msg--err">{error}</div>}
|
||||
{loading ? (
|
||||
<div className="muted">Loading\u2026</div>
|
||||
) : (
|
||||
<div>
|
||||
{tokens.length === 0 && <div className="muted">No tokens yet.</div>}
|
||||
{tokens.map(tok => (
|
||||
<div key={tok.token_uid} className="token-row">
|
||||
<div className="token-row-info">
|
||||
<strong>{tok.name}</strong>
|
||||
<div className="muted">
|
||||
Created {tok.created_at.slice(0, 10)}
|
||||
{tok.last_used_at && ` \u00b7 Last used ${tok.last_used_at.slice(0, 10)}`}
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn-danger" style={{ fontSize: 12, padding: '4px 10px' }}
|
||||
onClick={() => handleDelete(tok.token_uid)}>
|
||||
Revoke
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={() => handleDelete(tok.token_uid)}
|
||||
style={{ border: '1px solid var(--line)', background: 'none', cursor: 'pointer',
|
||||
color: 'var(--accent)', fontSize: 12, padding: '4px 10px' }}>
|
||||
Revoke
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -232,40 +241,40 @@ function InstanceTab() {
|
|||
}
|
||||
|
||||
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
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 480 }}>
|
||||
<h2 style={{ fontSize: 15, marginBottom: 12 }}>Instance Settings</h2>
|
||||
<form onSubmit={handleSave}>
|
||||
<div className="admin-section">
|
||||
<div style={{ maxWidth: 440 }}>
|
||||
<div className="form-section">
|
||||
<h2>Instance Settings</h2>
|
||||
<form onSubmit={handleSave}>
|
||||
{[
|
||||
['public_index_enabled', 'Public index (unauthenticated browsing)'],
|
||||
['public_entry_content_enabled', 'Public entry content'],
|
||||
['open_registration_enabled', 'Open registration'],
|
||||
].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]}
|
||||
onChange={e => setSettings(s => ({ ...s, [key]: e.target.checked }))} />
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', fontWeight: 600, marginBottom: 4, fontSize: 13 }}>Default entry visibility</label>
|
||||
<select className="admin-input" value={settings.default_entry_visibility}
|
||||
<div className="form-field" style={{ marginTop: 4 }}>
|
||||
<label className="form-label">Default entry visibility</label>
|
||||
<select className="field-input" value={settings.default_entry_visibility}
|
||||
onChange={e => setSettings(s => ({ ...s, default_entry_visibility: Number(e.target.value) }))}>
|
||||
<option value={0}>Private (0)</option>
|
||||
<option value={2}>Unlisted (2)</option>
|
||||
<option value={3}>Public (3)</option>
|
||||
<option value={0}>Private</option>
|
||||
<option value={2}>Unlisted</option>
|
||||
<option value={3}>Public</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{saveMsg && <div className={saveMsg.ok ? 'muted' : 'capture-error'}>{saveMsg.text}</div>}
|
||||
<button className="capture-submit" type="submit" disabled={saving}>
|
||||
{saving ? 'Saving\u2026' : 'Save Settings'}
|
||||
</button>
|
||||
</form>
|
||||
{saveMsg && <div className={`form-msg form-msg--${saveMsg.ok ? 'ok' : 'err'}`}>{saveMsg.text}</div>}
|
||||
<button className="btn-primary" type="submit" disabled={saving}>
|
||||
{saving ? 'Saving\u2026' : 'Save Settings'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,42 +32,53 @@ export default function SetupPage({ onComplete }) {
|
|||
|
||||
return (
|
||||
<div className="setup-page">
|
||||
<h1>Welcome to Archivr</h1>
|
||||
<p>Create your owner account to get started.</p>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label>
|
||||
Username
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Confirm password
|
||||
<input
|
||||
type="password"
|
||||
value={confirm}
|
||||
onChange={e => setConfirm(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
{error && <p className="error">{error}</p>}
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Creating account\u2026' : 'Create account'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="setup-card">
|
||||
<h1 className="setup-brand">Archivr</h1>
|
||||
<p className="setup-tagline">Create your owner account to get started.</p>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="setup-field">
|
||||
<label className="setup-label" htmlFor="setup-username">Username</label>
|
||||
<input
|
||||
className="setup-input"
|
||||
id="setup-username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
autoFocus
|
||||
required
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="setup-field">
|
||||
<label className="setup-label" htmlFor="setup-password">Password</label>
|
||||
<input
|
||||
className="setup-input"
|
||||
id="setup-password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
required
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<div className="setup-field">
|
||||
<label className="setup-label" htmlFor="setup-confirm">Confirm password</label>
|
||||
<input
|
||||
className="setup-input"
|
||||
id="setup-confirm"
|
||||
type="password"
|
||||
value={confirm}
|
||||
onChange={e => setConfirm(e.target.value)}
|
||||
required
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
{error && <p className="setup-error">{error}</p>}
|
||||
<button className="setup-submit" type="submit" disabled={loading}>
|
||||
{loading ? 'Creating account\u2026' : 'Create account'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,14 @@ export default function TagsView({ tagNodes, tagFilter, onTagFilterSet, onViewCh
|
|||
return (
|
||||
<section id="tags-view" className="view is-active">
|
||||
<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 ? (
|
||||
<div>No tags yet.</div>
|
||||
<p className="muted" style={{ padding: '8px 0' }}>No tags yet.</p>
|
||||
) : (
|
||||
<ul className="tag-tree-list">
|
||||
{tagNodes.map(node => (
|
||||
|
|
|
|||
|
|
@ -33,9 +33,8 @@ export default function Topbar({ archives, archiveId, onArchiveChange, view, onV
|
|||
<button className="capture-button" onClick={onCaptureClick}>+ Capture</button>
|
||||
{currentUser && (
|
||||
<div className="user-menu">
|
||||
<span className="username">{currentUser.display_name || currentUser.username}</span>
|
||||
<button className="nav-link" onClick={() => onViewChange('settings')} style={{ color: '#d7cdbf', fontSize: 13 }}>Settings</button>
|
||||
<button onClick={handleLogout} disabled={loggingOut} className="logout-btn">
|
||||
<span className="user-name">{currentUser.display_name || currentUser.username}</span>
|
||||
<button className="logout-btn" onClick={handleLogout} disabled={loggingOut}>
|
||||
{loggingOut ? 'Logging out\u2026' : 'Log out'}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -564,6 +564,7 @@ select {
|
|||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent);
|
||||
}
|
||||
.form-field .capture-input { margin-bottom: 0; }
|
||||
.capture-error {
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
|
|
@ -604,10 +605,6 @@ select {
|
|||
.admin-view {
|
||||
padding: 22px;
|
||||
}
|
||||
.admin-view h1 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.admin-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
|
@ -622,7 +619,28 @@ select {
|
|||
|
||||
/* ── 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 {
|
||||
list-style: none;
|
||||
|
|
@ -877,7 +895,424 @@ select {
|
|||
}
|
||||
.coll-add-btn:hover { opacity: 0.85; }
|
||||
.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 ──────────────────────────────────────────────────────────── */
|
||||
@media (max-width: 900px) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue