From 15f8748be1c36a5739b5ae862887625d1cb018d9 Mon Sep 17 00:00:00 2001 From: rednakse Date: Wed, 1 Apr 2026 23:23:03 +0300 Subject: [PATCH] style: rebuild ui shell with stable minimalist layout --- docs/PLAN.md | 1 + docs/PROJECT_INDEX.md | 2 +- src/App.test.tsx | 2 +- src/App.tsx | 485 +++++++++++++++--------------- src/app.css | 682 ++++++++++++++++++++---------------------- 5 files changed, 573 insertions(+), 599 deletions(-) diff --git a/docs/PLAN.md b/docs/PLAN.md index a89f422..04fb45b 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -20,3 +20,4 @@ Updated: 2026-04-01 4. Added paranoia-oriented tests for login gating, proxy link encoding, quota edge cases, and traffic share formatting. 5. Simplified the UI into a calmer minimalist layout with reduced visual noise and denser operational presentation. 6. Moved user creation into a modal flow and tightened the operator UX with quieter navigation and a denser users table. +7. Rebuilt the UI shell from scratch around a stable topbar/tab layout with fixed typography and lower visual noise across window sizes. diff --git a/docs/PROJECT_INDEX.md b/docs/PROJECT_INDEX.md index d1dd40c..63d1b22 100644 --- a/docs/PROJECT_INDEX.md +++ b/docs/PROJECT_INDEX.md @@ -18,7 +18,7 @@ Updated: 2026-04-01 ## Frontend - `src/main.tsx`: application bootstrap -- `src/App.tsx`: authenticated panel shell, modal user creation flow, and tab composition +- `src/App.tsx`: authenticated panel shell rebuilt around a topbar/tab layout, plus modal user creation flow - `src/App.test.tsx`: login-gate and modal interaction tests - `src/app.css`: full panel styling - `src/data/mockDashboard.ts`: realistic mock state shaped for future API responses diff --git a/src/App.test.tsx b/src/App.test.tsx index 7b59374..7102d2e 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -29,7 +29,7 @@ describe('App login gate', () => { await loginIntoPanel(user); expect(screen.getByRole('navigation', { name: /primary/i })).toBeInTheDocument(); - expect(screen.getByText(/control panel/i)).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: /3proxy ui/i })).toBeInTheDocument(); }); it('opens add-user flow in a modal and closes it on escape', async () => { diff --git a/src/App.tsx b/src/App.tsx index ca8213f..7d53dbd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { FormEvent, KeyboardEvent, useEffect, useMemo, useState } from 'react'; +import { FormEvent, KeyboardEvent, useEffect, useState } from 'react'; import './app.css'; import { dashboardSnapshot, panelAuth } from './data/mockDashboard'; import { @@ -38,9 +38,10 @@ function LoginGate({ onUnlock }: { onUnlock: () => void }) { return (
-

3proxy UI

-

Panel access

-

Hardcoded credentials for the current prototype build.

+
+

3proxy UI

+

Sign in to the control panel.

+
); } -function DashboardTab() { - const serviceTone = getServiceTone(dashboardSnapshot.service.status); - - return ( -
-
-
-

Service

-

3proxy status

-

Health, process metadata, and restart actions.

-
-
- {dashboardSnapshot.service.status} - {dashboardSnapshot.service.pidLabel} - {dashboardSnapshot.service.versionLabel} -
-
- - -
-
- -
-

Traffic

- {formatBytes(dashboardSnapshot.traffic.totalBytes)} -

- {dashboardSnapshot.traffic.liveConnections} live connections across{' '} - {dashboardSnapshot.traffic.activeUsers} active users. -

-
- {dashboardSnapshot.traffic.daily.map((bucket) => ( -
- {bucket.day} -
-
-
- {formatBytes(bucket.bytes)} -
- ))} -
-
- -
-

User pressure

- {dashboardSnapshot.users.total} -

Configured users in the active profile.

-
-
- Live now - {dashboardSnapshot.users.live} -
-
- Near quota - {dashboardSnapshot.users.nearQuota} -
-
- Exceeded - {dashboardSnapshot.users.exceeded} -
-
-
- -
-

Runtime attention

-
- {dashboardSnapshot.attention.map((item) => ( -
- -
- {item.title} -

{item.message}

-
-
- ))} -
-
-
- ); -} - -function AddUserModal({ - onClose, -}: { - onClose: () => void; -}) { +function AddUserModal({ onClose }: { onClose: () => void }) { useEffect(() => { const handleKeyDown = (event: globalThis.KeyboardEvent) => { if (event.key === 'Escape') { @@ -186,11 +97,8 @@ function AddUserModal({ onClick={stopPropagation} >
-
-

Users

-

Add user

-
-
@@ -212,7 +120,7 @@ function AddUserModal({
- @@ -223,6 +131,99 @@ function AddUserModal({ ); } +function DashboardTab() { + const serviceTone = getServiceTone(dashboardSnapshot.service.status); + + return ( +
+
+
+

Service

+ {dashboardSnapshot.service.status} +
+
+
+
Process
+
{dashboardSnapshot.service.pidLabel}
+
+
+
Version
+
{dashboardSnapshot.service.versionLabel}
+
+
+
Uptime
+
{dashboardSnapshot.service.uptimeLabel}
+
+
+
Last event
+
{dashboardSnapshot.service.lastEvent}
+
+
+
+ + +
+
+ +
+
+

Traffic

+
+
+
+ Total + {formatBytes(dashboardSnapshot.traffic.totalBytes)} +
+
+ Connections + {dashboardSnapshot.traffic.liveConnections} +
+
+ Active users + {dashboardSnapshot.traffic.activeUsers} +
+
+
+ +
+
+

Daily usage

+
+
+ {dashboardSnapshot.traffic.daily.map((bucket) => ( +
+ {bucket.day} +
+
+
+ {formatBytes(bucket.bytes)} +
+ ))} +
+
+ +
+
+

Attention

+
+
+ {dashboardSnapshot.attention.map((item) => ( +
+ +
+ {item.title} +

{item.message}

+
+
+ ))} +
+
+
+ ); +} + function UsersTab() { const [copiedId, setCopiedId] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); @@ -234,132 +235,128 @@ function UsersTab() { }; return ( -
-
-
-
-

Users

-

Accounts and usage

+ <> +
+
+
+
+

Users

+

{dashboardSnapshot.userRecords.length} accounts in current profile

+
+
+
+ {dashboardSnapshot.users.live} live + {dashboardSnapshot.users.nearQuota} near quota + {dashboardSnapshot.users.exceeded} exceeded +
+ +
-
- {dashboardSnapshot.userRecords.length} rows - -
-
-
- - - - - - - - - - - - - {dashboardSnapshot.userRecords.map((user) => { - const proxyLink = buildProxyLink(user.username, user.password, user.host, user.port); - const exhausted = isQuotaExceeded(user.usedBytes, user.quotaBytes); +
+
UserEndpointStatusUsedRemainingProxy
+ + + + + + + + + + + + + {dashboardSnapshot.userRecords.map((user) => { + const proxyLink = buildProxyLink(user.username, user.password, user.host, user.port); + const exhausted = isQuotaExceeded(user.usedBytes, user.quotaBytes); - return ( - - - - - - - - - ); - })} - -
UserEndpointStatusUsedRemainingShareProxy
-
- {user.username} - {user.port} -
-
{`${user.host}:${user.port}`} - {user.status} - {formatBytes(user.usedBytes)}{formatQuotaState(user.usedBytes, user.quotaBytes)} - -
-
-
- Total traffic share is derived from current snapshot: {formatTrafficShare( - dashboardSnapshot.userRecords.reduce((sum, user) => sum + user.usedBytes, 0), - dashboardSnapshot.traffic.totalBytes, - )} -
-
+ return ( + + +
+ {user.username} +
+ + {`${user.host}:${user.port}`} + + {user.status} + + {formatBytes(user.usedBytes)} + {formatQuotaState(user.usedBytes, user.quotaBytes)} + {formatTrafficShare(user.usedBytes, dashboardSnapshot.traffic.totalBytes)} + + + + + ); + })} + + +
+ + {isModalOpen ? setIsModalOpen(false)} /> : null} - + ); } function SystemTab() { - const serviceToggles = useMemo( - () => - dashboardSnapshot.system.services.map((service) => ( -
-
- {service.name} - {service.description} -
-
- :{service.port} - - {service.enabled ? 'enabled' : 'disabled'} - -
-
- )), - [], - ); - return ( -
+
-

Runtime model

-

Ports and reload mode

-
+
+

Runtime

+
+
- Config mode - {dashboardSnapshot.system.configMode} +
Config mode
+
{dashboardSnapshot.system.configMode}
- Reload - {dashboardSnapshot.system.reloadMode} +
Reload
+
{dashboardSnapshot.system.reloadMode}
- Storage - {dashboardSnapshot.system.storageMode} +
Storage
+
{dashboardSnapshot.system.storageMode}
+
+
+ +
+
+

Services

+
+
+ {dashboardSnapshot.system.services.map((service) => ( +
+
+ {service.name} +

{service.description}

+
+
+ {service.port} + + {service.enabled ? 'enabled' : 'disabled'} + +
+
+ ))}
-
-

Services

-
{serviceToggles}
-
- -
-
-
-

Generated config preview

-

Generated config

-
- View-only for now +
+
+

Generated config

{dashboardSnapshot.system.previewConfig}
@@ -376,50 +373,44 @@ export default function App() { } return ( -
- - -
-
+
-

Current host

-

{dashboardSnapshot.system.publicHost}

+ Status + {dashboardSnapshot.service.status}
-
- {dashboardSnapshot.service.status} - {dashboardSnapshot.service.versionLabel} +
+ Version + {dashboardSnapshot.service.versionLabel}
-
+
+ Users + {dashboardSnapshot.users.total} +
+
+ - {activeTab === 'dashboard' ? : null} - {activeTab === 'users' ? : null} - {activeTab === 'system' ? : null} -
+ + + {activeTab === 'dashboard' ? : null} + {activeTab === 'users' ? : null} + {activeTab === 'system' ? : null} ); } diff --git a/src/app.css b/src/app.css index dcfc563..5f4609b 100644 --- a/src/app.css +++ b/src/app.css @@ -1,40 +1,44 @@ :root { - font-family: "Segoe UI", Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - color: #18212b; - background: #f4f6f8; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; + color: #111827; + background: #f3f4f6; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - --bg: #f4f6f8; + --page-bg: #f3f4f6; --surface: #ffffff; - --surface-soft: #f8fafb; - --border: #dde4ea; - --border-strong: #cbd5dd; - --text: #18212b; - --muted: #667380; - --accent: #1f6feb; - --accent-soft: #edf4ff; - --success: #16794c; - --warn: #9a6700; - --danger: #b42318; - --idle: #667380; - --shadow: 0 1px 2px rgba(16, 24, 40, 0.04); + --surface-muted: #f9fafb; + --border: #e5e7eb; + --border-strong: #d1d5db; + --text: #111827; + --muted: #6b7280; + --accent: #2563eb; + --accent-muted: #eff6ff; + --success: #15803d; + --warning: #a16207; + --danger: #b91c1c; + --shadow: 0 1px 2px rgba(17, 24, 39, 0.04); } * { box-sizing: border-box; } +html, +body, +#app { + min-height: 100%; +} + body { margin: 0; min-width: 320px; - min-height: 100vh; - background: var(--bg); + background: var(--page-bg); color: var(--text); + font-size: 14px; + line-height: 1.5; } button, @@ -46,13 +50,8 @@ button { cursor: pointer; } -#app, -.login-shell, -.app-shell { - min-height: 100vh; -} - .login-shell { + min-height: 100vh; display: grid; place-items: center; padding: 24px; @@ -60,49 +59,41 @@ button { .login-card, .panel-card, -.sidebar, -.topbar { +.shell-header, +.tabbar { background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow); } .login-card { - width: min(100%, 420px); - padding: 28px; - border-radius: 16px; + width: min(100%, 360px); + padding: 24px; + border-radius: 12px; display: grid; - gap: 18px; + gap: 20px; } -.eyebrow, -.section-label { - margin: 0; - font-size: 0.75rem; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--muted); +.login-copy { + display: grid; + gap: 4px; } -.login-card h1, -.brand-block h1, -.panel-card h2, -.topbar h2 { +.login-copy h1, +.shell-title h1, +.card-header h2, +.modal-header h2 { margin: 0; - color: var(--text); - font-size: 1.375rem; - line-height: 1.25; + font-size: 20px; + line-height: 1.2; font-weight: 600; + color: var(--text); } -.lede, -.muted, -.attention-item p, -.sidebar-foot p, -.service-row span, -.user-cell span, -.table-foot { +.login-copy p, +.toolbar-title p, +.service-row p, +.event-row p { margin: 0; color: var(--muted); } @@ -110,351 +101,367 @@ button { .login-form, .modal-form { display: grid; - gap: 14px; + gap: 12px; } .login-form label, .modal-form label { display: grid; gap: 6px; - color: var(--text); - font-size: 0.94rem; + font-weight: 500; } .login-form input, .modal-form input { width: 100%; - padding: 10px 12px; - border-radius: 10px; + height: 40px; + padding: 0 12px; border: 1px solid var(--border-strong); + border-radius: 8px; background: var(--surface); color: var(--text); } .login-form input:focus, .modal-form input:focus { - outline: 2px solid rgba(31, 111, 235, 0.18); + outline: 2px solid rgba(37, 99, 235, 0.12); border-color: var(--accent); } -.login-form button, -.action-row button, -.modal-form button, -.copy-link, -.danger-link, -.nav-item, -.ghost-button { - transition: - background-color 0.15s ease, - border-color 0.15s ease, - color 0.15s ease; -} - -.login-form button, -.action-row button, -.modal-form button, -.header-actions button { - padding: 10px 14px; - border-radius: 10px; +button, +.button-secondary, +.copy-button { + height: 36px; + padding: 0 12px; + border-radius: 8px; border: 1px solid var(--accent); background: var(--accent); color: #ffffff; font-weight: 600; } -.secondary { - border-color: var(--border-strong) !important; - background: var(--surface) !important; - color: var(--text) !important; +.button-secondary, +.copy-button { + border-color: var(--border-strong); + background: var(--surface); + color: var(--text); } -.login-form button:hover, -.action-row button:hover, -.modal-form button:hover, -.header-actions button:hover, -.ghost-button:hover, -.copy-link:hover, -.danger-link:hover { - filter: brightness(0.98); +.copy-button.danger { + color: var(--danger); } .form-error { margin: 0; color: var(--danger); - font-size: 0.92rem; } -.login-note { - display: grid; - gap: 4px; - padding-top: 6px; - border-top: 1px solid var(--border); - color: var(--muted); - font-size: 0.9rem; -} - -.app-shell { - display: grid; - grid-template-columns: 220px minmax(0, 1fr); - gap: 16px; - padding: 16px; -} - -.sidebar { - border-radius: 14px; - padding: 20px; - display: grid; - gap: 20px; - align-self: start; - position: sticky; - top: 16px; -} - -.brand-block { - display: grid; - gap: 4px; -} - -.brand-block h1 { - font-size: 1.12rem; -} - -.nav-list { - display: grid; - gap: 8px; -} - -.nav-item { - padding: 10px 12px; - border-radius: 8px; - text-align: left; - border: 1px solid transparent; - background: transparent; - color: var(--text); - display: block; - font-weight: 500; -} - -.nav-item.active { - background: var(--accent-soft); - border-color: #cfe0ff; -} - -.sidebar-foot { - display: grid; - gap: 8px; - padding-top: 16px; - border-top: 1px solid var(--border); -} - -.content-shell { +.shell { + width: min(1280px, calc(100vw - 32px)); + margin: 16px auto; display: grid; gap: 16px; } -.topbar { - padding: 14px 18px; - border-radius: 14px; +.shell-header { + border-radius: 12px; + padding: 18px 20px; display: flex; justify-content: space-between; + align-items: center; gap: 16px; +} + +.shell-title { + display: grid; + gap: 4px; +} + +.shell-title p { + margin: 0; + color: var(--muted); +} + +.header-meta { + display: grid; + grid-auto-flow: column; + gap: 24px; align-items: center; } -.topbar-meta { - display: flex; - gap: 10px; - flex-wrap: wrap; - color: var(--muted); - font-size: 0.88rem; +.header-meta div { + display: grid; + gap: 2px; } -.tab-grid { +.header-meta span { + color: var(--muted); + font-size: 12px; +} + +.header-meta strong { + font-size: 14px; + font-weight: 600; +} + +.tabbar { + display: inline-flex; + width: fit-content; + gap: 4px; + padding: 4px; + border-radius: 10px; +} + +.tab-button { + height: 34px; + padding: 0 14px; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--muted); + font-weight: 600; +} + +.tab-button.active { + background: var(--accent-muted); + color: var(--accent); +} + +.page-grid { display: grid; gap: 16px; grid-template-columns: repeat(2, minmax(0, 1fr)); } -.users-grid { +.page-grid.single-column { grid-template-columns: 1fr; } -.system-grid { - grid-template-columns: minmax(280px, 1fr) minmax(0, 2fr); +.system-grid .wide-card { + grid-column: 1 / -1; } .panel-card { - border-radius: 14px; + border-radius: 12px; padding: 18px; display: grid; gap: 14px; min-width: 0; } -.hero-card { - background: var(--surface); -} - -.hero-status, -.action-row, -.stat-stack, -.attention-item, +.card-header, +.table-toolbar, +.toolbar-actions, +.actions-row, +.modal-header, +.modal-actions, .service-row, -.system-stats { +.service-meta { display: flex; - gap: 10px; - flex-wrap: wrap; align-items: center; + justify-content: space-between; + gap: 12px; } -.metric { - font-size: 2rem; - line-height: 1; - color: var(--text); - font-weight: 600; +.card-header { + align-items: baseline; } -.sparkline-list, -.attention-list, +.stats-strip { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.stats-strip div { + display: grid; + gap: 4px; + padding: 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--surface-muted); +} + +.stats-strip span, +.kv-list dt { + color: var(--muted); +} + +.stats-strip strong { + font-size: 18px; + line-height: 1.2; +} + +.kv-list { + margin: 0; + display: grid; + gap: 10px; +} + +.kv-list div { + display: grid; + grid-template-columns: 120px minmax(0, 1fr); + gap: 12px; +} + +.kv-list dt, +.kv-list dd { + margin: 0; +} + +.usage-list, +.event-list, .service-list { display: grid; gap: 10px; } -.sparkline-row { +.usage-row { display: grid; - grid-template-columns: 56px minmax(0, 1fr) 88px; - gap: 10px; + grid-template-columns: 40px minmax(0, 1fr) 96px; + gap: 12px; align-items: center; - font-size: 0.92rem; +} + +.usage-row span { color: var(--muted); } -.sparkline-track { +.usage-row strong { + font-size: 13px; + text-align: right; +} + +.usage-bar { height: 8px; border-radius: 999px; - background: #edf1f4; + background: #eceff3; overflow: hidden; } -.sparkline-track div { +.usage-bar div { height: 100%; - border-radius: inherit; - background: #7aa7e8; + background: #94a3b8; } -.stat-stack { - justify-content: space-between; -} - -.stat-stack div, -.system-stats div { - flex: 1 1 120px; - display: grid; - gap: 4px; +.event-row, +.service-row { padding: 12px; border: 1px solid var(--border); - border-radius: 12px; - background: var(--surface-soft); + border-radius: 10px; + background: var(--surface-muted); } -.attention-item, -.service-row { - padding: 12px 14px; - border-radius: 12px; - border: 1px solid var(--border); - background: var(--surface-soft); -} - -.dot { - width: 8px; - height: 8px; - border-radius: 999px; - flex: none; -} - -.dot.warn { - background: #d4a72c; -} - -.dot.live { - background: #2da66a; -} - -.dot.fail { - background: #d95040; -} - -.table-card, -.config-card { - min-width: 0; -} - -.table-header { - display: flex; - justify-content: space-between; +.event-row { + display: grid; + grid-template-columns: 10px minmax(0, 1fr); gap: 12px; align-items: start; } -.header-actions { +.event-marker { + width: 8px; + height: 8px; + border-radius: 999px; + margin-top: 6px; +} + +.event-marker.live { + background: var(--success); +} + +.event-marker.warn { + background: var(--warning); +} + +.event-marker.fail { + background: var(--danger); +} + +.toolbar-title { + display: grid; + gap: 4px; +} + +.toolbar-title h2 { + margin: 0; + font-size: 18px; + line-height: 1.2; +} + +.toolbar-actions { + flex-wrap: wrap; +} + +.summary-pills { display: flex; - gap: 10px; + gap: 8px; + flex-wrap: wrap; +} + +.summary-pills span { + display: inline-flex; align-items: center; + height: 30px; + padding: 0 10px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--surface-muted); + color: var(--muted); } .table-wrap { overflow: auto; border: 1px solid var(--border); - border-radius: 12px; + border-radius: 10px; } table { width: 100%; + min-width: 760px; border-collapse: collapse; - min-width: 700px; background: var(--surface); } th, td { - padding: 13px 14px; - text-align: left; + padding: 12px 14px; border-bottom: 1px solid var(--border); - font-size: 0.94rem; + text-align: left; + white-space: nowrap; } th { + background: var(--surface-muted); color: var(--muted); - font-size: 0.78rem; - font-weight: 700; + font-size: 12px; + font-weight: 600; text-transform: uppercase; - letter-spacing: 0.05em; - background: var(--surface-soft); + letter-spacing: 0.04em; } tbody tr:last-child td { - border-bottom: none; + border-bottom: 0; } -.user-cell { - display: grid; - gap: 2px; +.user-cell strong { + font-size: 14px; } .status-pill { display: inline-flex; align-items: center; justify-content: center; - min-height: 28px; - padding: 4px 10px; + min-width: 72px; + height: 28px; + padding: 0 10px; + border: 1px solid currentColor; border-radius: 999px; - font-size: 0.72rem; + font-size: 11px; font-weight: 700; text-transform: uppercase; - letter-spacing: 0.06em; - border: 1px solid currentColor; - background: #ffffff; + letter-spacing: 0.04em; + background: var(--surface); } .status-pill.live { @@ -462,7 +469,7 @@ tbody tr:last-child td { } .status-pill.warn { - color: var(--warn); + color: var(--warning); } .status-pill.fail { @@ -470,140 +477,115 @@ tbody tr:last-child td { } .status-pill.idle { - color: var(--idle); -} - -.copy-link, -.danger-link { - padding: 8px 12px; - border-radius: 10px; - border: 1px solid var(--border-strong); - background: var(--surface); - color: var(--text); -} - -.danger-link { - color: var(--danger); -} - -.service-row, -.service-meta { - justify-content: space-between; -} - -.config-card { - grid-column: 1 / -1; -} - -.table-foot { - font-size: 0.88rem; + color: var(--muted); } pre { margin: 0; padding: 14px; - border-radius: 12px; overflow: auto; border: 1px solid var(--border); - background: #fbfcfd; - color: #24303c; - font-family: Consolas, "Courier New", monospace; - font-size: 0.88rem; - line-height: 1.6; + border-radius: 10px; + background: #fbfbfc; + color: #1f2937; + font: 13px/1.55 Consolas, "Courier New", monospace; } .modal-backdrop { position: fixed; inset: 0; - background: rgba(15, 23, 42, 0.18); display: grid; place-items: center; - padding: 16px; + padding: 24px; + background: rgba(17, 24, 39, 0.2); } .modal-card { - width: min(100%, 480px); + width: min(100%, 440px); background: var(--surface); border: 1px solid var(--border); - border-radius: 14px; - box-shadow: 0 12px 30px rgba(15, 23, 42, 0.12); + border-radius: 12px; + box-shadow: 0 12px 32px rgba(17, 24, 39, 0.12); padding: 18px; display: grid; gap: 16px; } -.modal-header, -.modal-actions { - display: flex; - justify-content: space-between; - align-items: center; - gap: 10px; -} - .modal-form { grid-template-columns: repeat(2, minmax(0, 1fr)); } .modal-actions { grid-column: 1 / -1; - justify-content: end; + justify-content: flex-end; } -.ghost-button { - padding: 8px 12px; - border-radius: 10px; - border: 1px solid var(--border-strong); - background: var(--surface); - color: var(--text); -} +@media (max-width: 960px) { + .shell { + width: calc(100vw - 24px); + margin: 12px auto; + gap: 12px; + } -@media (max-width: 1120px) { - .app-shell { + .shell-header, + .table-toolbar { + flex-direction: column; + align-items: flex-start; + } + + .header-meta { + grid-auto-flow: row; + grid-template-columns: repeat(3, minmax(0, 1fr)); + width: 100%; + gap: 12px; + } + + .page-grid, + .stats-strip, + .modal-form { grid-template-columns: 1fr; } - .sidebar { - position: static; - } - - .tab-grid, - .users-grid, - .system-grid { - grid-template-columns: 1fr; + .modal-actions { + justify-content: stretch; } } -@media (max-width: 720px) { - .login-shell, - .app-shell { - padding: 12px; - } - - .login-card, - .sidebar, - .panel-card, - .topbar { +@media (max-width: 640px) { + .login-shell { padding: 16px; } - .topbar { - flex-direction: column; - align-items: start; + .shell { + width: calc(100vw - 16px); + margin: 8px auto; } - .header-actions, - .modal-form, - .modal-actions { + .shell-header, + .panel-card, + .login-card, + .modal-card { + padding: 16px; + } + + .tabbar { + width: 100%; + } + + .tab-button { + flex: 1 1 0; + } + + .header-meta { grid-template-columns: 1fr; - flex-direction: column; - align-items: stretch; } - .sparkline-row { - grid-template-columns: 52px minmax(0, 1fr); + .usage-row { + grid-template-columns: 40px minmax(0, 1fr); } - .sparkline-row span:last-child { + .usage-row strong { grid-column: 2; + text-align: left; } }