Storage dashboard design system
The taste-design doc for the storage dashboard pair (org Overview + admin Dashboard) - tokens and components from the Vela UI system, layout from the app shell, and a real-data contract from the storage data model.
The full spec
Design System: Storage Dashboard (Vela UI)
Skill: stitch-design-taste, adapted to Vela UI’s ratified system — velaui/catalog/DESIGN.md
is the ground truth for tokens/typography/shapes; storage/ui/src is ground truth for the
dashboard screen anatomy; storage/api/src/models/schema.ts is ground truth for every data
value. Where this document and the taste template disagree, the shipped system wins.
Configuration — Style Dials
| Dial | Level | Why |
|---|---|---|
| Creativity | 6 |
An operations dashboard, not a marketing site. The identity is typographic precision, halftone meters and census-grade numbers — not expressive editorial layout. |
| Density | 7 |
Cockpit-leaning: metric strips, dense tables, quota gauges. Breathing room comes from panel padding (24px) and section gaps, not from empty columns. |
| Variance | 6 |
Structured asymmetry: a 1-column by-app breakdown beside a 2-column widget+usage stack, panels of unequal width — but always a regular grid underneath. |
| Motion | 5 |
Restrained by system rule: hover tints, sheet/dialog transitions, skeleton-first loading. Spring physics where motion exists; no theatrical choreography. |
1. Visual Theme & Atmosphere
A restrained, near-monochrome dashboard that reads like a well-typeset census report:
near-black ink on paper-white, one teal accent used sparingly, flat 1px borders instead of
shadows on every resting surface, and dotted halftone meters as the only decorative voice —
usage rendered as fields of 3px dots rather than smooth bars. The atmosphere is quiet,
precise, infrastructure-grade: every figure is a real counter from usage_counters
(organizations, objects, stored bytes), never a marketing number. The two surfaces are the
organization’s Overview (one bucket at a glance) and the platform administration Dashboard
(totals across every organization); both share the app shell — an icon rail, an 80px header
with breadcrumb, and panels of 24px padding, 12px radius, 1px border, no shadow.
2. Color Palette & Roles
Only the semantic layer is public. Light values below; dark mode is mechanical, never
hand-authored (.dark swaps white→neutral-950 #0A0A0A and derives everything else via
color-mix() — no bespoke dark palette).
| Descriptive name | Token | Hex (light) | Role |
|---|---|---|---|
| Paper | bg-main |
#FFFFFF |
Page and every panel surface |
| Surface Tint | bg-surface-1/2 |
#F2F2F2 / #E6E6E6 |
Hover fills, chip tags, meter tracks (5% / 10% ink mixes) |
| Ink | primary-default / bg-foreground / text-strong |
#0A0A0A |
Primary buttons, table headers, headings, active nav, focus borders. Off-black — never #000000 |
| Slate Text | text-muted |
#525252 |
Descriptions, subtitles, secondary copy |
| Note Text | text-note |
#A3A3A3 |
Placeholders, disabled, “as of” stamps, empty-state hints |
| Hairline | border-default / input-default |
#E5E5E5 / #F5F5F5 |
1px structural lines; input fills |
| Signal Teal | accent-default |
#14B8A6 |
THE one chromatic accent (H 176°, S 78% — under the 80% cap). Accent badges, accent focus rings on choice controls. Sparing use |
| Cyan Info | info-middle / info-strong / info |
#06B6D4 / #0891B2 / #ECFEFF |
“public” and informational badges |
| Green Success | success-middle / success-strong |
#22C55E / #16A34A |
Status “active”, copied confirmations |
| Amber Warning | warning-middle / warning-strong |
#F59E0B / #D97706 |
Near-quota: meter + “near limit · 84%” badge |
| Red Negative | negative-middle / negative-strong / negative |
#DC2626 / #991B1B / #FEF2F2 |
Over-quota, revoke, destructive actions, 507 refusals |
| Chart 1–5 | chart-1..5 |
#3B82F6 #14B8A6 #F97316 #A855F7 #EC4899 |
Per-app halftone share tones only — never for text |
Status tints are used as subtle badge surfaces with their -strong text step. Known gap,
do not repeat: variant="subtle" color="info" (info-strong on info) measures 3.54:1 — below
AA; check any new tint/text pairing against 4.5:1 before shipping.
3. Typography Rules
- Family: Inter Display (variable, 100–900, the loaded static WOFF2 set). This is the system’s identity — do NOT substitute Geist/Satoshi/Outfit. The taste-template “no Inter” rule is overridden by the ratified system (see catalog/DESIGN.md §Typography).
- Display/Headings: 32px / 600 / 1.2 page titles; panel titles 16px / 600 / 1.4;
metric figures 24px / 600 / 1.3 with
tabular-nums. Hierarchy through weight and color (text-strongvstext-muted), not through size escalation. - Body: 14px / 400 / 1.4 — the only reading size. Descriptions may drop to 12px / 1.3.
- Labels: interactive labels semibold (600) — buttons 14/600; static data labels medium (500) — tags 14/500, badges 12/500. Metric captions: uppercase 10px / 500.
- Mono (system monospace stack —
font-mono): every identifier — bucketobj-8f3ac2d4, access keysSTG…, sha256/etag, mono prefix filter values, timestamps in dense tables. Metric figures stay in Inter withtabular-nums; identifiers go mono. Density is 7, so mono wins for all keys and opaque tokens. - Banned: serifs anywhere (dashboard), pure-black body text, weight overloading in body copy (use color contrast instead), display sizes above 32px on dashboard surfaces.
4. Component Stylings
- Buttons: six variants —
primary(ink fill#0A0A0A, white label, hover lightens to#171717),secondary(#F5F5F5),outline(transparent, 1pxborder-default),ghost,destructive(10% negative fill +negative-middlelabel from rest — the ONLY resting-state-colored variant),link(no chrome at rest; a padded radiused hit box only on focus-visible). Radii by size tier (8px up tomd40px, 12px atxl56px). Flat surfaces, no glow, no gradient; active gives tactile feedback (state scrim/1px press — never neon). Sizes 24/32/40/48/56. - Inputs: three variants —
outlined(bordered, default),fill(input-defaultsurface, borderless with a transparent 1px reserve so hover/focus never reflow),underline(bottom border only). Sizessm32px default /md/lg. Error state viaaria-invalid: negative border + 25%-negative focus ring. Label above, helper optional, error below — no floating labels. - Badges & Tags: badge = pill (rounded-full), four variants (solid/subtle/outline/ghost)
× seven colors; status vocabulary:
active=success,revoked=neutral/negative,public=info,near limit=warning. Tag =bg-surface-1chip, 8px radius, withselectedas a 1px inset outline — background and outline never coexist. - Panels (the dashboard’s card): 24px padding, 12px radius, 1px
border-default,bg-main, no shadow, 16px internal gap; header separated by a 16px-under-divider (border-b, 16px padding below). Panel title 16/600 + description 12 muted; floated actions (RefreshBar “As of HH:MM:SS · refresh”, Export CSV, quota actions) pin to the header’s right. Footer links: border-t divider + link button with a 16px arrow-right. - Metric (resource card block): uppercase 10px muted label, 24px semibold
tabular-numsvalue 6px below, optional 3-row halftone meter, 12px muted hint below — the exact composition of the Platform usage card (Organizations · Objects · Stored). - Halftone meters: the signature viz — dots 3px on a 5px pitch (7×6px rows), filled share in a tone, rest in a 20%-ink tint. Tones: neutral (blue-600) at rest, warning ≥80%, danger ≥100%. Used for by-app shares and quota; never smooth gradient bars.
- Quota gauges: thin
role="progressbar"bar (1.5px track,bg-surface-2), fill tone by used fraction:primary-default< 80%,warning-middle80–99%,negative-middle≥ 100%; percentage label beside;near limit · N%warning badge orat limit — uploads are refusednegative badge per the thresholds below. - Tables: 44px header row (12px medium muted labels), 56px data rows, 1px
border-tdividers, no row striping; identifiers mono with a hover copy control that really copies and flips to “Copied”; actions as ghost icon buttons with tooltips; foreign-scope rows readread-only. On phones the table becomes a stacked card list (md:hiddenduality). - Overlays: dialog = 12px radius,
bg-main, border,shadow-md, 24px padding, icon slot (status fills, rounded-full 48px); sheet = edge-docked panel, 24px radius,shadow-xs; dropdown/popover/select = 12px surface +shadow-md, 6px-radius items with 5%-ink hover. Elevation is binary: resting surfaces have none, overlays carrymd. - Loading: skeletons matching the final layout 1:1 (Tiles/List/Rows), not spinners.
- Empty states: composed — 32px
icon-noteglyph, title, description, onesmaction (e.g. over-quota’s all-clear “No organization is over its quota” check state). - Copy buttons: ghost xs, min 52px,
navigator.clipboard.writeText+ 1.6s green “Copied” flip; degrade silently where the clipboard rejects.
5. Layout Principles
- App shell: 80px icon rail (folded) expanding to a grouped nav sidebar; the trigger
sits on the rail’s edge. Rail ↔ expanded folds below 1024px. Header: 80px, breadcrumb
(muted parent / strong page), notification bell with dot, light/dark segmented, plan card
(hidden < lg). Main:
p-6,px-8≥1200,gap-6between stacked surfaces. - Grid: 12/10/4 columns at desktop/tablet/mobile; 80/48/20px margins; flat 20px gutter.
Page content contained to ~1400px; the widest panel is 1372px. Use CSS Grid for
structural layout; never flexbox
calc()percentage math. - Dashboard anatomy (both modes follows the same grammar): page head (32px title +
muted subtitle + primary CTA + refresh); then the data surfaces. Org Overview: 1-column
By-app panel left, right stack of the bucket widget (128px, mono path chip + copy +
primary Upload CTA) over the Usage panel — grid
minmax(0,1fr) / minmax(0,1.25fr), stacking single-column below 1200px. Admin Dashboard: full-width Platform usage panel (three metrics) over the full-width By-organization table. - The metrics triad is data, not marketing — the banned “3 equal cards” rule targets marketing feature rows; a three-metric resource card (Organizations · Objects · Stored) is the app’s own composition and stays. Never build a second decorative triplet.
- No overlapping elements; every value, chip and action occupies its own cell.
- Full-height surfaces use
min-h-[100dvh], neverh-screen.
6. Motion & Interaction
The system animates rarely — encode restraint first, then physics where motion exists:
- Defaults: hover = 5%-ink surface tint only (no glow, no scale). Buttons give a subtle
press (scrim/1px). Focus rings are 3px
ring-neutral-shadowwith outline-colored border. - Overlays: sheet slides from the right (transform), backdrop fades (opacity); dialogs fade + slight scale on entry, ESC/backdrop/close all wired. Fold/unfold of the rail animates width via the system’s collapsible keyframes (ease-out), never layout props by hand.
- Spring physics where a physical transition exists (toggle switches, sheet):
stiffness: 100, damping: 20. No linear easing. - Loading: skeleton-first (layout-matching shimmer), never circular spinners. Lists mount immediately — no cascade delay on tables (operators need the data now); if a reveal is added, cap it at ≤ 12 rows, 60ms steps, transform/opacity only.
- Perpetual loops: none shipped today, by design. If a live cue is ever needed (e.g. a syncing state), use a 1s opacity pulse of an existing status dot — no spinning, no floating icons.
- Hardware rules: animate only
transformandopacity; nevertop/left/width/height; keep any heavy animation in isolated leaf components; target 60fps.
7. Dashboards Have No Marketing Hero
The first impression is the page head + metric strip — treat it as the hero and own its discipline:
- Head: 32px title (“Storage administration” / “Overview”), one muted subtitle line, one primary CTA at most (New access key / Upload / Raise quota), the RefreshBar stamp on the same row. No secondary “learn more” links, no scroll hints, no stacked illustration.
- Asymmetric by construction: left-aligned title block, right-aligned actions, then the data grid — never a centered hero.
- First data impression: the metric strip must be real counters (see the data contract) — “Organizations · Objects · Stored” for admin; “Stored / Objects / Free” + by-app shares for the org — never invented statistics.
8. Responsive Rules
- < 768px: every multi-column layout collapses to one column; the objects table becomes the stacked card list; panels keep 16px padding; buttons may go full-width for the one primary CTA.
- < 1024px: sidebar folds to the 80px rail; the plan card and wide header chrome hide.
- < 640px: the rail becomes a slide-in drawer behind “Open menu” with a 44px trigger.
- No horizontal overflow on mobile — tables fold to cards, identifiers truncate, chips wrap.
- Typography: page titles scale (32px stays legible at 390px; never below 14px body).
- Touch targets: primary actions ≤ 40px minimum (system
mddefault); row icon actions stay 32px with adequate hit spacing per the system’s density — do not inflate table rows; mobile drawer targets 44px. - Viewports to verify: 375 / 390 / 768 / 1024 / 1440.
9. Real Data Contract (the anti-fabrication anchor)
Every figure a generated screen shows must come from this model — never invented:
| Concept | Real shape | Notes |
|---|---|---|
| Organization | uuid id (Shell’s), name string (synced) |
No slug, no created_at in the model |
| Bucket | one per org, derived obj-<organizationId> |
Created lazily on first upload; is_public affects signed-link lifetime |
| Quota | bytes_limit bigint bytes, nullable; note (e.g. Plan: Team (10 GiB)) |
NULL or missing row = UNLIMITED — not a plan-derived number; editor works in GiB (round(gib × 1024³)) |
| Objects | object_key (app-relative), app_slug (namespace), visibility ∈ {org, app, owner}, size_bytes (bytes), content_type, uploaded_via ∈ {rest,s3}, public_token (NULL = not public), created_at |
“Published” = public_token not null, not a boolean |
| Usage (org) | OrgUsage { organizationId, organizationName, objectCount, bytesTotal, bytesLimit, usedFraction, apps: { appSlug, objectCount, bytesTotal }[] } |
usedFraction null when unlimited; apps sorted by bytesTotal desc |
| Usage (admin totals) | Totals { organizations, objects, bytes } |
organizations counts only orgs with ≥1 usage row |
| Thresholds | near = usedFraction ≥ 0.8 AND < 1; over = bytesTotal ≥ bytesLimit |
The “near limit · N%” and “at limit” badges are these predicates |
| Sizes | formatBytes: binary units B/KiB/MiB/GiB/TiB/PiB, 1024-based; toFixed(2) < 10 else toFixed(1) |
Never decimal-GB marketing figures |
| Access keys | access_key_id = STG + 20 of [A-Z0-9]; status ∈ {active,revoked}; description; last_used_at; secretAccessKey returned exactly once at creation |
Max 50 keys per org; listing never shows secrets |
| Failures | 507 QUOTA_EXCEEDED (org full — detail bytesLimit/bytesUsed/bytesRequested), 413 PAYLOAD_TOO_LARGE (single-request ceiling, default 50 MiB) |
507 copy is caller-facing by contract; multipart sidesteps 413, quota still enforced at complete |
| Signed links | default 900s, clamp 60s–7d; public objects use the 7-day expiry | Every link expires — show expiresAt, never a permanent URL |
10. Anti-Patterns (Banned)
- No fabricated data — any metric outside §9 (uptime %, response times, invented quotas)
renders as
[metric]placeholder, never a number. - No invented KPI sections (“SYSTEM PERFORMANCE”, “BY THE NUMBERS” cards) beyond the two sanctioned metric surfaces.
- No generic placeholder identities — keep the blocks’ established Acme / Hiếu Võ sample
personae, and every ID follows the real shapes (uuid orgs,
obj-<uuid>,STG…keys). - No
LABEL // YEARformatting. - No marketing clichés (“Elevate”, “Seamless”, “Next-Gen”).
- No emojis; no custom cursors; no scroll-hint copy (“Scroll to explore”) or bouncing chevrons.
- No pure black
#000000— ink is#0A0A0A; no neon/outer glows; no gradients on surfaces or headline text; no oversaturated accents (teal at 78% S is the ceiling); no purple/blue neons. - No shadows on resting surfaces (cards, inputs, tables) — only overlays (
md) and the sheet (xs). - No arbitrary radii — 6 / 8 / 12 / 24px + pill only (the 10px
lgbutton corner is a verified one-off, not a tier). - No 3-equal-card rows except the data metrics triad; no centered heroes; no overlapping
text/visuals; no
h-screen; no flexbox percentage math; no primitive token usage (neutral-950etc.) in app code — semantic tokens only. - No serif typography anywhere in the dashboard.