feat(web): Designsystem v3 "KeyControl" + gruppierte Navigation
Löst das bisherige "Calm Cloud"-Design nach Nutzerfeedback ab: dunkle Navy-Sidebar (#173568) als dauerhaft sichtbare, vertrauenswürdige Navigation, klares Blau (#3B6FD8) als Primärfarbe, hellgrauer Arbeitsbereich (#F4F6FA). Sidebar breiter mit mehr Abstand, größere Schriftgrößen für kleine/gedämpfte Texte, größere max. Inhaltsbreite. Navigation in aufklappbare Gruppen sortiert (Produkte / Abrechnung / Support / Verwaltung) statt einer langen flachen Liste. Kopfbereich zeigt bei Kunden zusätzlich den Organisationsnamen. Ressourcenübersicht zeigt Kunden ihre Produkte jetzt als Kartenraster statt als spärliche Tabelle (Personal behält die tabellarische Ansicht für die höhere Datendichte). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
2f8566307b
commit
b112647ff1
3 changed files with 63 additions and 35 deletions
|
|
@ -15,32 +15,30 @@ export default function AppLayout({ children }: { children: ReactNode }) {
|
|||
useEffect(() => setOpen(false), [path]);
|
||||
if (loading || !me || me.pendingMfa) return <div className="center"><p className="muted" role="status">Wird geladen …</p></div>;
|
||||
const link = (href: string, label: string) => <Link href={href} aria-current={path === href || path.startsWith(href + '/') ? 'page' : undefined}>{label}</Link>;
|
||||
const inGroup = (hrefs: string[]) => hrefs.some((h) => path === h || path.startsWith(h + '/'));
|
||||
const group = (title: string, hrefs: string[], items: ReactNode) => (
|
||||
<details className="group" open={inGroup(hrefs)}><summary>{title}</summary><div className="items">{items}</div></details>
|
||||
);
|
||||
async function logout() { await api('POST', '/auth/logout'); await reload(); r.replace('/login'); }
|
||||
const produkte = [(me.kind === 'customer' || can('resources.read')) && ['/ressourcen', 'Ressourcen'], (me.kind === 'customer' || can('contracts.read')) && ['/vertraege', 'Verträge'], (me.kind === 'customer' || can('orders.read')) && ['/bestellungen', 'Bestellungen']].filter(Boolean) as [string, string][];
|
||||
const verwaltung = [can('customers.read') && ['/admin/kunden', 'Kunden'], can('products.read') && ['/admin/produkte', 'Produkte'], can('domains.read') && ['/admin/domains', 'Domain-Aufstellung'], can('users.read') && ['/admin/benutzer', 'Benutzer'], can('jobs.read') && ['/admin/auftraege', 'Aufträge'], can('audit.read') && ['/admin/audit', 'Audit-Protokoll'], (can('connectors.read') || can('backup.read')) && ['/einstellungen', 'Einstellungen']].filter(Boolean) as [string, string][];
|
||||
const nav = (
|
||||
<nav className={`nav ${open ? 'open' : ''}`} aria-label="Hauptnavigation">
|
||||
<div className="brand">Kundencenter</div>
|
||||
{!enrollNeeded && link('/dashboard', 'Übersicht')}
|
||||
{!enrollNeeded && (me.kind === 'customer' || can('resources.read')) && link('/ressourcen', 'Ressourcen')}
|
||||
{!enrollNeeded && (me.kind === 'customer' || can('contracts.read')) && link('/vertraege', 'Verträge')}
|
||||
{!enrollNeeded && (me.kind === 'customer' || can('orders.read')) && link('/bestellungen', 'Bestellungen')}
|
||||
{!enrollNeeded && (me.kind === 'customer' || can('tickets.read')) && link('/tickets', 'Tickets')}
|
||||
{!enrollNeeded && (me.kind === 'customer' || can('invoices.read')) && link('/rechnungen', 'Rechnungen')}
|
||||
{!enrollNeeded && produkte.length > 0 && group('Produkte', produkte.map(([h]) => h), produkte.map(([h, l]) => <span key={h}>{link(h, l)}</span>))}
|
||||
{!enrollNeeded && (me.kind === 'customer' || can('invoices.read')) && group('Abrechnung', ['/rechnungen'], link('/rechnungen', 'Rechnungen'))}
|
||||
{!enrollNeeded && (me.kind === 'customer' || can('tickets.read')) && group('Support', ['/tickets'], link('/tickets', 'Tickets'))}
|
||||
{!enrollNeeded && me.organizations.length > 0 && link('/organisation', 'Organisation')}
|
||||
{!enrollNeeded && link('/domains', 'Domain prüfen')}
|
||||
{!enrollNeeded && me.kind === 'staff' && <>
|
||||
<div className="group">Verwaltung</div>
|
||||
{can('customers.read') && link('/admin/kunden', 'Kunden')}
|
||||
{can('products.read') && link('/admin/produkte', 'Produkte')}
|
||||
{can('domains.read') && link('/admin/domains', 'Domain-Aufstellung')}
|
||||
{can('users.read') && link('/admin/benutzer', 'Benutzer')}
|
||||
{can('jobs.read') && link('/admin/auftraege', 'Aufträge')}
|
||||
{can('audit.read') && link('/admin/audit', 'Audit-Protokoll')}
|
||||
{(can('connectors.read') || can('backup.read')) && link('/einstellungen', 'Einstellungen')}
|
||||
</>}
|
||||
{!enrollNeeded && me.kind === 'staff' && verwaltung.length > 0 && group('Verwaltung', verwaltung.map(([h]) => h), verwaltung.map(([h, l]) => <span key={h}>{link(h, l)}</span>))}
|
||||
<div className="spacer" />
|
||||
{link('/konto', 'Mein Konto')}
|
||||
<div className="small muted" style={{ padding: '8px 16px' }}>{me.user.name}<br />{me.staffRole ? ROLE_LABEL[me.staffRole] : 'Kunde'}</div>
|
||||
<div className="row" style={{ padding: '0 16px' }}><ThemeToggle /></div>
|
||||
<div className="small who">
|
||||
{me.kind === 'customer' && me.organizations[0] && <div className="org">{me.organizations[0].name}</div>}
|
||||
{me.user.name}<br />{me.staffRole ? ROLE_LABEL[me.staffRole] : 'Kunde'}
|
||||
</div>
|
||||
<div className="row" style={{ padding: '0 8px' }}><ThemeToggle /></div>
|
||||
<button className="btn small" onClick={logout}>Abmelden</button>
|
||||
</nav>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export default function Ressourcen() {
|
|||
useEffect(() => { if (canAssign) api<Cust[]>('GET', '/admin/customers').then(setCusts).catch(() => undefined); }, [canAssign]);
|
||||
const stale = list?.find((r) => r.stale);
|
||||
return (<>
|
||||
<h1>Ressourcen</h1>
|
||||
<div className="pagehead"><h1>Ressourcen</h1></div>
|
||||
{err && <Alert kind="err">{err}</Alert>}
|
||||
{ok && <Alert kind="ok">{ok}</Alert>}
|
||||
{stale && <Alert kind="warn">Einige Angaben sind möglicherweise nicht aktuell: {stale.staleReason} Angezeigt wird der zuletzt bekannte Stand.</Alert>}
|
||||
|
|
@ -55,9 +55,22 @@ export default function Ressourcen() {
|
|||
GROUPS.map((g) => { const rows = list.filter((r) => g.types.includes(r.type)); if (rows.length === 0) return null; const lic = g.key === 'license'; return (
|
||||
<section key={g.key} aria-labelledby={`h-${g.key}`} style={{ marginBottom: 24 }}>
|
||||
<h2 id={`h-${g.key}`}>{g.title} <span className="muted small">({rows.length})</span></h2>
|
||||
{staff ? (
|
||||
<div className="card"><div className="tablewrap"><table><thead><tr><th>Name</th>{!lic && <th>Typ</th>}<th>Status</th><th>Gültig bis</th><th>Stand</th>{lic && <th>Lizenzschlüssel</th>}</tr></thead><tbody>
|
||||
{rows.map((r) => <tr key={r.id}><td><Link href={`/ressourcen/${r.id}`}>{r.name}</Link>{staff && !r.orgId && <> <span className="badge warn"><span aria-hidden="true">▲</span>Nicht zugewiesen</span></>}{canAssign && !r.orgId && <div style={{ marginTop: 8 }}><AssignCell r={r} custs={custs} onError={setErr} onCreated={(c, cr) => { setCusts((l) => [...l, c]); setErr(''); setOk(`Kunde ${cr.customerNumber} angelegt. ` + (cr.mail === 'sent' ? 'Die Einladung wurde per E-Mail versendet.' : cr.inviteLink ? `E-Mail-Versand ist nicht eingerichtet. Einladungslink (nur jetzt sichtbar): ${cr.inviteLink}` : '') + ' Jetzt noch „Zuweisen“ klicken.'); }} onDone={() => { setErr(''); setOk(`„${r.name}“ wurde zugewiesen. Kunden dürfen daran zunächst keine Aktionen ausführen (im Produkt bzw. an der Ressource freigebbar).`); void reload(); }} /></div>}</td>{!lic && <td>{TYPE[r.type] ?? r.type}</td>}<td><ResState value={r.state} /></td>
|
||||
{rows.map((r) => <tr key={r.id}><td><Link href={`/ressourcen/${r.id}`}>{r.name}</Link>{!r.orgId && <> <span className="badge warn"><span aria-hidden="true">▲</span>Nicht zugewiesen</span></>}{canAssign && !r.orgId && <div style={{ marginTop: 8 }}><AssignCell r={r} custs={custs} onError={setErr} onCreated={(c, cr) => { setCusts((l) => [...l, c]); setErr(''); setOk(`Kunde ${cr.customerNumber} angelegt. ` + (cr.mail === 'sent' ? 'Die Einladung wurde per E-Mail versendet.' : cr.inviteLink ? `E-Mail-Versand ist nicht eingerichtet. Einladungslink (nur jetzt sichtbar): ${cr.inviteLink}` : '') + ' Jetzt noch „Zuweisen“ klicken.'); }} onDone={() => { setErr(''); setOk(`„${r.name}“ wurde zugewiesen. Kunden dürfen daran zunächst keine Aktionen ausführen (im Produkt bzw. an der Ressource freigebbar).`); void reload(); }} /></div>}</td>{!lic && <td>{TYPE[r.type] ?? r.type}</td>}<td><ResState value={r.state} /></td>
|
||||
<td>{r.validUntil ? fmt(r.validUntil) : 'unbefristet'}</td><td className="small">{fmt(r.syncedAt)}{r.stale && <> <span className="badge warn"><span aria-hidden="true">▲</span>Veraltet</span></>}</td>{lic && <td>{r.canReveal ? <SecretField resourceId={r.id} compact /> : <span className="muted small">–</span>}</td>}</tr>)}</tbody></table></div></div>
|
||||
) : (
|
||||
<div className="productgrid">{rows.map((r) => (
|
||||
<div className="productcard" key={r.id}>
|
||||
<div className="row between"><h3><Link href={`/ressourcen/${r.id}`}>{r.name}</Link></h3><ResState value={r.state} /></div>
|
||||
{!lic && <div className="id">{TYPE[r.type] ?? r.type}</div>}
|
||||
<div className="small muted">Gültig bis: {r.validUntil ? fmt(r.validUntil) : 'unbefristet'}</div>
|
||||
<div className="small muted">Stand: {fmt(r.syncedAt)}{r.stale && <> <span className="badge warn"><span aria-hidden="true">▲</span>Veraltet</span></>}</div>
|
||||
{lic && r.canReveal && <div style={{ marginTop: 4 }}><SecretField resourceId={r.id} compact /></div>}
|
||||
<div style={{ marginTop: 8 }}><Link className="btn small" href={`/ressourcen/${r.id}`}>Details ansehen</Link></div>
|
||||
</div>
|
||||
))}</div>
|
||||
)}
|
||||
</section>); })}
|
||||
</>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
/* Designsystem v2 "Calm Cloud": warmgraue/GitHub-dunkle Flächen, Dunkelblau als vertrauenswürdige Hauptfarbe,
|
||||
Türkis nur für aktive Navigation/technische Marker. Hell/Dunkel automatisch über prefers-color-scheme,
|
||||
zusätzlich manuell über [data-theme] umschaltbar (siehe ThemeToggle). */
|
||||
/* Designsystem v3 "KeyControl": dunkles Navy als vertrauenswürdige, dauerhaft sichtbare Seitenleiste,
|
||||
klares Blau als Primärfarbe, helle Arbeitsfläche mit klar getrennten Bereichen. Hell/Dunkel automatisch
|
||||
über prefers-color-scheme, zusätzlich manuell über [data-theme] umschaltbar (siehe ThemeToggle). */
|
||||
:root {
|
||||
--bg: #f6f8fa; --surface: #ffffff; --surface-2: #eef1f5; --border: #e1e5eb; --text: #172033; --muted: #5b6472;
|
||||
--accent: #315efb; --accent2: #14b8a6; --accent-contrast: #ffffff; --focus: #315efb;
|
||||
--bg: #f4f6fa; --surface: #ffffff; --surface-2: #eef1f6; --border: #e3e8f0; --text: #172033; --muted: #5b6472;
|
||||
--accent: #3b6fd8; --accent2: #14b8a6; --accent-contrast: #ffffff; --focus: #3b6fd8;
|
||||
--ok: #22a06b; --ok-bg: #e4f5ec; --info: #2e6fe0; --info-bg: #e7f1fc; --warn: #e59a17; --warn-bg: #fdf1dc; --err: #dc4c64; --err-bg: #fbe7ea; --lock: #4b5563; --lock-bg: #eceef2;
|
||||
--nav-bg: #173568; --nav-text: #dbe4f7; --nav-muted: #8fa4d4; --nav-border: #24417f; --nav-hover: #1f4084; --nav-active-bg: #2a4d93; --nav-active-text: #ffffff;
|
||||
--shadow-sm: 0 1px 2px rgba(23, 32, 51, .04), 0 1px 1px rgba(23, 32, 51, .04);
|
||||
--shadow-md: 0 8px 20px rgba(23, 32, 51, .06), 0 2px 6px rgba(23, 32, 51, .04);
|
||||
--radius: 12px; --radius-sm: 8px; --s1: 4px; --s2: 8px; --s3: 16px; --s4: 24px; --s5: 40px;
|
||||
|
|
@ -13,11 +14,13 @@
|
|||
@media (prefers-color-scheme: dark) { :root:not([data-theme='light']) {
|
||||
--bg: #0d1117; --surface: #161b22; --surface-2: #1c2129; --border: #262c36; --text: #e6edf3; --muted: #8b98a5; --accent: #6e8dff; --accent2: #2dd4bf; --accent-contrast: #0d1117; --focus: #6e8dff;
|
||||
--ok: #34d399; --ok-bg: #10281d; --info: #8ab4f8; --info-bg: #16283d; --warn: #f5b84d; --warn-bg: #3a2b0c; --err: #f1798d; --err-bg: #3b1620; --lock: #b7c0cc; --lock-bg: #232932;
|
||||
--nav-bg: #0a1220; --nav-text: #cdd9f0; --nav-muted: #7c8bb0; --nav-border: #1c2740; --nav-hover: #131f36; --nav-active-bg: #1d3358; --nav-active-text: #ffffff;
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, .45); --shadow-md: 0 10px 24px rgba(0, 0, 0, .5), 0 2px 8px rgba(0, 0, 0, .35);
|
||||
} }
|
||||
:root[data-theme='dark'] {
|
||||
--bg: #0d1117; --surface: #161b22; --surface-2: #1c2129; --border: #262c36; --text: #e6edf3; --muted: #8b98a5; --accent: #6e8dff; --accent2: #2dd4bf; --accent-contrast: #0d1117; --focus: #6e8dff;
|
||||
--ok: #34d399; --ok-bg: #10281d; --info: #8ab4f8; --info-bg: #16283d; --warn: #f5b84d; --warn-bg: #3a2b0c; --err: #f1798d; --err-bg: #3b1620; --lock: #b7c0cc; --lock-bg: #232932;
|
||||
--nav-bg: #0a1220; --nav-text: #cdd9f0; --nav-muted: #7c8bb0; --nav-border: #1c2740; --nav-hover: #131f36; --nav-active-bg: #1d3358; --nav-active-text: #ffffff;
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, .45); --shadow-md: 0 10px 24px rgba(0, 0, 0, .5), 0 2px 8px rgba(0, 0, 0, .35);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
|
@ -25,22 +28,30 @@ html { color-scheme: light dark; }
|
|||
body { margin: 0; font: 16px/1.55 var(--font); background: var(--bg); color: var(--text); }
|
||||
body, .btn, .badge, .card, input, select, a, .alert { transition: background-color .15s ease, border-color .15s ease, color .15s ease, box-shadow .15s ease; }
|
||||
h1 { font-size: 1.7rem; font-weight: 700; margin: 0 0 var(--s3); letter-spacing: -0.015em; } h2 { font-size: 1.15rem; font-weight: 600; margin: 0 0 var(--s3); } h3 { font-size: 1rem; font-weight: 600; margin: 0 0 var(--s2); }
|
||||
a { color: var(--accent); } p { margin: 0 0 var(--s3); } .muted { color: var(--muted); } .small { font-size: .875rem; }
|
||||
a { color: var(--accent); } p { margin: 0 0 var(--s3); } .muted { color: var(--muted); } .small { font-size: .9rem; }
|
||||
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 6px; }
|
||||
.skip { position: absolute; left: -999px; } .skip:focus { left: var(--s3); top: var(--s3); background: var(--surface); padding: var(--s2) var(--s3); z-index: 10; }
|
||||
|
||||
.shell { display: grid; grid-template-columns: 260px 1fr; min-height: 100vh; }
|
||||
.nav { background: var(--surface); border-right: 1px solid var(--border); padding: var(--s4) var(--s3); display: flex; flex-direction: column; gap: 2px; }
|
||||
.nav .brand { font-weight: 700; font-size: 1.1rem; letter-spacing: -0.01em; padding: 0 var(--s3) var(--s4); }
|
||||
.nav a { display: block; padding: 10px var(--s3); border-radius: var(--radius-sm); color: var(--text); text-decoration: none; }
|
||||
.nav a:hover { background: var(--surface-2); }
|
||||
.nav a[aria-current='page'] { background: color-mix(in srgb, var(--accent2) 14%, transparent); color: var(--accent2); font-weight: 600; }
|
||||
.nav .group { margin: var(--s3) var(--s3) var(--s1); font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--muted); }
|
||||
.shell { display: grid; grid-template-columns: 288px 1fr; min-height: 100vh; }
|
||||
.nav { background: var(--nav-bg); border-right: 1px solid var(--nav-border); padding: var(--s4) var(--s3) var(--s3); display: flex; flex-direction: column; gap: 2px; }
|
||||
.nav .brand { font-weight: 700; font-size: 1.2rem; letter-spacing: -0.01em; color: #fff; padding: 0 var(--s2) var(--s4); }
|
||||
.nav a { display: block; padding: 11px var(--s3); border-radius: var(--radius-sm); color: var(--nav-text); text-decoration: none; font-size: .96rem; }
|
||||
.nav a:hover { background: var(--nav-hover); color: #fff; }
|
||||
.nav a[aria-current='page'] { background: var(--nav-active-bg); color: var(--nav-active-text); font-weight: 600; }
|
||||
.nav .group { margin: 0; }
|
||||
.nav .group > summary { list-style: none; cursor: pointer; margin: var(--s2) var(--s1) 2px; padding: 8px var(--s2); font-size: .74rem; font-weight: 700; text-transform: uppercase; letter-spacing: .07em; color: var(--nav-muted); display: flex; align-items: center; justify-content: space-between; border-radius: var(--radius-sm); }
|
||||
.nav .group > summary::-webkit-details-marker { display: none; }
|
||||
.nav .group > summary:hover { color: #fff; background: var(--nav-hover); }
|
||||
.nav .group > summary::after { content: '▸'; font-size: .7rem; transition: transform .15s ease; }
|
||||
.nav .group[open] > summary::after { transform: rotate(90deg); }
|
||||
.nav .group .items { display: flex; flex-direction: column; gap: 2px; padding-bottom: var(--s2); }
|
||||
.nav .spacer { flex: 1; }
|
||||
.main { padding: var(--s5) var(--s4); max-width: 1100px; width: 100%; }
|
||||
.nav .who { padding: var(--s2); color: var(--nav-text); }
|
||||
.nav .who .org { color: #fff; font-weight: 600; }
|
||||
.main { padding: var(--s5) var(--s4); max-width: 1240px; width: 100%; }
|
||||
.topbar { display: none; } .bottomnav { display: none; }
|
||||
@media (max-width: 800px) {
|
||||
.shell { grid-template-columns: 1fr; } .nav { display: none; } .nav.open { display: flex; position: fixed; inset: 0 30% 56px 0; z-index: 5; box-shadow: 0 0 0 100vmax rgba(0,0,0,.4); overflow: auto; }
|
||||
.shell { grid-template-columns: 1fr; } .nav { display: none; } .nav.open { display: flex; position: fixed; inset: 0 20% 56px 0; z-index: 5; box-shadow: 0 0 0 100vmax rgba(0,0,0,.4); overflow: auto; }
|
||||
.topbar { display: flex; align-items: center; justify-content: space-between; gap: var(--s2); padding: var(--s3); background: var(--surface); border-bottom: 1px solid var(--border); }
|
||||
.main { padding: var(--s4) var(--s3) calc(var(--s4) + 56px); }
|
||||
.bottomnav { display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 6; background: var(--surface); border-top: 1px solid var(--border); box-shadow: var(--shadow-md); padding-bottom: env(safe-area-inset-bottom, 0); }
|
||||
|
|
@ -61,7 +72,7 @@ a { color: var(--accent); } p { margin: 0 0 var(--s3); } .muted { color: var(--m
|
|||
input, select { font: inherit; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); min-height: 44px; width: 100%; }
|
||||
input:focus-visible, select:focus-visible { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); outline: none; }
|
||||
.cols { display: grid; gap: var(--s3); grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
|
||||
table { width: 100%; border-collapse: collapse; } th, td { text-align: left; padding: 12px var(--s3); border-bottom: 1px solid var(--border); vertical-align: top; } th { font-size: .74rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); background: var(--surface-2); }
|
||||
table { width: 100%; border-collapse: collapse; } th, td { text-align: left; padding: 12px var(--s3); border-bottom: 1px solid var(--border); vertical-align: top; } th { font-size: .78rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); background: var(--surface-2); }
|
||||
tbody tr:hover { background: var(--surface-2); }
|
||||
.tablewrap { overflow-x: auto; border-radius: var(--radius-sm); border: 1px solid var(--border); }
|
||||
.tablewrap table { border: none; } .tablewrap th:first-child, .tablewrap td:first-child { padding-left: var(--s3); }
|
||||
|
|
@ -69,6 +80,12 @@ tbody tr:hover { background: var(--surface-2); }
|
|||
.badge.ok { background: var(--ok-bg); color: var(--ok); } .badge.info { background: var(--info-bg); color: var(--info); } .badge.warn { background: var(--warn-bg); color: var(--warn); } .badge.err { background: var(--err-bg); color: var(--err); } .badge.lock { background: var(--lock-bg); color: var(--lock); }
|
||||
.alert { border-radius: var(--radius-sm); padding: var(--s3); margin-bottom: var(--s3); border: 1px solid transparent; } .alert.err { background: var(--err-bg); color: var(--err); } .alert.ok { background: var(--ok-bg); color: var(--ok); } .alert.warn { background: var(--warn-bg); color: var(--warn); } .alert.info { background: var(--info-bg); color: var(--info); }
|
||||
.empty { text-align: center; padding: var(--s5) var(--s3); color: var(--muted); }
|
||||
.pagehead { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s3); flex-wrap: wrap; margin-bottom: var(--s4); }
|
||||
.pagehead h1 { margin: 0; }
|
||||
.productgrid { display: grid; gap: var(--s3); grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
|
||||
.productcard { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--s4); box-shadow: var(--shadow-sm); display: flex; flex-direction: column; gap: 6px; }
|
||||
.productcard h3 { margin: 0; font-size: 1.05rem; }
|
||||
.productcard .id { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .82rem; color: var(--muted); }
|
||||
.center { min-height: 100vh; display: grid; place-items: center; padding: var(--s3); } .auth { width: 100%; max-width: 420px; }
|
||||
code, .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em; word-break: break-all; } .codes { columns: 2; background: var(--surface-2); padding: var(--s3); border-radius: var(--radius-sm); }
|
||||
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue