Skip to content
File

Blob: DESIGN.md

Markdown567 lines

name: dab description: The private wing of your data library — a sparse control plane for a personal DAV host. colors: canvas: "#221f21" zinc-50: "#fafaf9" zinc-100: "#f5f4f4" zinc-200: "#e5e4e5" zinc-300: "#d6d4d7" zinc-400: "#a3a1a8" zinc-500: "#747178" zinc-600: "#555259" zinc-700: "#423f42" zinc-800: "#2a2729" zinc-900: "#1b181a" zinc-950: "#0c090b" accent-50: "#fdf2f8" accent-100: "#fce7f3" accent-200: "#fbcfe8" accent-300: "#f9a8d4" accent-400: "#f06da6" accent-500: "#db3a8b" accent-600: "#b8226e" accent-700: "#941b58" accent-800: "#76174a" accent-900: "#5b143b" semantic-emerald-400: "#34d399" semantic-red-400: "#f87171" semantic-amber-300: "#fcd34d" typography: display: fontFamily: "Fraunces, ui-serif, Georgia, serif" fontSize: "clamp(1.875rem, 2vw + 1rem, 2.25rem)" fontWeight: 800 lineHeight: "1.05" letterSpacing: "-0.025em" h1: fontFamily: "Hanken Grotesk, ui-sans-serif, system-ui, sans-serif" fontSize: "1.5rem" fontWeight: 700 lineHeight: "1.2" letterSpacing: "-0.02em" h2: fontFamily: "Hanken Grotesk, ui-sans-serif, system-ui, sans-serif" fontSize: "1.25rem" fontWeight: 600 lineHeight: "1.3" h3: fontFamily: "Hanken Grotesk, ui-sans-serif, system-ui, sans-serif" fontSize: "1rem" fontWeight: 600 lineHeight: "1.4" body: fontFamily: "Hanken Grotesk, ui-sans-serif, system-ui, sans-serif" fontSize: "0.875rem" fontWeight: 450 lineHeight: "1.55" caption: fontFamily: "Hanken Grotesk, ui-sans-serif, system-ui, sans-serif" fontSize: "0.75rem" fontWeight: 500 lineHeight: "1.4" letterSpacing: "0.1em" mono: fontFamily: "JetBrains Mono, SF Mono, Fira Code, monospace" fontSize: "0.75rem" fontWeight: 400 lineHeight: "1.5" rounded: sm: "0.5rem" md: "0.75rem" lg: "1rem" xl: "1rem" "2xl": "1rem" full: "9999px" spacing: "2": "0.5rem" "3": "0.75rem" "4": "1rem" "5": "1.25rem" "6": "1.5rem" "8": "2rem" components: button-primary: backgroundColor: "{colors.accent-600}" textColor: "#ffffff" rounded: "{rounded.md}" padding: "0 1rem" height: "2.5rem" typography: "{typography.body}" button-primary-hover: backgroundColor: "{colors.accent-500}" textColor: "#ffffff" rounded: "{rounded.md}" padding: "0 1rem" height: "2.5rem" button-secondary: backgroundColor: "{colors.zinc-800}" textColor: "{colors.zinc-200}" rounded: "{rounded.md}" padding: "0 1rem" height: "2.5rem" typography: "{typography.body}" button-danger: backgroundColor: "{colors.zinc-900}" textColor: "{colors.semantic-red-400}" rounded: "{rounded.md}" padding: "0 1rem" height: "2.5rem" button-ghost: backgroundColor: "transparent" textColor: "{colors.zinc-400}" rounded: "{rounded.md}" padding: "0 1rem" height: "2.5rem" card-default: backgroundColor: "{colors.zinc-900}" textColor: "{colors.zinc-100}" rounded: "{rounded.2xl}" padding: "1.25rem" card-accent: backgroundColor: "{colors.zinc-900}" textColor: "{colors.zinc-100}" rounded: "{rounded.2xl}" padding: "1.25rem" input: backgroundColor: "{colors.zinc-800}" textColor: "{colors.zinc-100}" rounded: "{rounded.md}" padding: "0.625rem 1rem" typography: "{typography.body}" input-focus: backgroundColor: "{colors.zinc-800}" textColor: "{colors.zinc-100}" rounded: "{rounded.md}" padding: "0.625rem 1rem" badge-neutral: backgroundColor: "{colors.zinc-800}" textColor: "{colors.zinc-300}" rounded: "{rounded.full}" padding: "0.125rem 0.5rem" typography: "{typography.caption}" badge-accent: backgroundColor: "{colors.accent-900}" textColor: "{colors.accent-300}" rounded: "{rounded.full}" padding: "0.125rem 0.5rem" typography: "{typography.caption}"


Design System: dab

1. Overview

Creative North Star: "The private wing of the library."

dab is the small reading room behind tessera's front desk. Each subject's opaque five-word host is the placard at the entrance to a wing of stacks; inside, three labeled shelves hold calendars, contacts, and files. The control-plane UI is a place to organize what is on those shelves, hand visitors (DAV clients) a shelfmark, and leave. Nothing on this surface wants your attention longer than necessary.

The aesthetic is institutional-print restraint with a single warm bookmark slip. Fraunces (warm variable serif) does the ceremonial work on page H1s and the brand mark, the way a typeset placard sits above a row of stacks. Hanken Grotesk does the steady labor everywhere else. Coral-magenta (#db3a8b) appears rarely, never as decoration — the primary CTA, the active nav state, the small heart in the footer, the bookmark slip on a hover row. Its power is rarity.

The whole system explicitly rejects the share-link product (Dropbox / iCloud / Drive), the enterprise NAS appliance (Nextcloud / ownCloud / Synology), the AI-template dark-mode default (neon-on-near-black, gradient meshes, glassmorphism), and the cheerful-Apple pastel calendar aesthetic. dab is not a daily driver; the design is sized for the four-times-a-year visit and the sixty-second task, and every surface should read like a labeled shelf rather than a marketing tile.

Key Characteristics:

  • Lifted-canvas dark only (#221f21); no light-mode toggle.
  • One coral-magenta accent, one warm serif, one warm sans, one mono. Nothing more.
  • Catalog-card density: each row carries display name, internal name (mono), one timestamp, the URL in a collapsed disclosure.
  • Personality budget spent on the Layers glyph -rotate-6 on hover, the serif H1, the gold-magenta CTA, and the footer heart. Nowhere else.
  • 75ms default transition, scoped to specific properties. Never transition-all.

2. Colors: The Library Palette

Warm-shifted zinc neutrals on a lifted canvas, with a single coral-magenta accent borrowed from a bookmark slip pressed into a closed book.

Primary

  • Bookmark Coral (#db3a8b, oklch(60.3% 0.219 357)): The primary accent. Appears on the primary CTA fill at accent-600 (#b8226e) with white text, hovering to accent-500. Active nav-state background at accent-500/10 with text at accent-300. The footer heart fills at accent-500. Active row indicator on hover-state borders. A "you saw me twice on this page" frequency; if it appears more often, something is being decorated that should not be.

Neutral: Warm Zinc

Stock zinc shifted +2–3 R, –1–4 B per stop, so the rest cool cast lifts and the palette feels like cream-aged paper under low light. Darker stops get the larger shift; lighter stops barely move so text contrast is unaffected.

  • Canvas (#221f21, ~zinc-850): Body / main content. The brightest reading surface; the place the eye rests.
  • Chrome (#1b181a, zinc-900): Header, recessed wrappers (code blocks, table headers, disclosure containers). Darker than canvas to frame and recede.
  • Elevated (#2a2729, zinc-800): Menus, dialogs, popovers, dropdowns, card fill. Lighter than canvas to float over it.
  • Hover-inside-elevated (#423f42, zinc-700): Hover state for rows inside an elevated surface; also the resting border on inputs and secondary buttons.
  • Body text (zinc-100, #f5f4f4): Primary content. Contrast on canvas 14.3:1.
  • Secondary text (zinc-300, #d6d4d7): Labels, soft headings.
  • Muted text (zinc-400, #a3a1a8): Caption, descriptions, table cells that aren't the primary value. Contrast 5.9:1 on canvas.
  • Placeholder / disabled (zinc-500, #747178): Input placeholders, inert glyphs.

Semantic

Role Background Text Border
Success bg-emerald-500/10 text-emerald-400 border-emerald-500/20
Error bg-red-500/10 text-red-400 border-red-500/20
Warning bg-amber-500/10 text-amber-300 border-amber-500/20
Info bg-accent-500/10 text-accent-300 border-accent-500/20

Named Rules

The One Bookmark Rule. Coral-magenta is the primary CTA, the active nav state, the footer heart, and nothing else. Never a background fill. Never a gradient. Never a decorative shadow. Never as a row stripe. If a third coral-magenta element appears on a page, one of the three is wrong.

The Lifted Canvas Rule. Body background is #221f21, not zinc-950, not #000. Lifting prevents halation for astigmatic readers and gives the surface hierarchy room to express depth without shadows. Don't push canvas darker for "drama"; it makes the wing feel like a basement.

The Warm-Shifted Zinc Rule. Override stock zinc in @theme with the warm-shifted values. Stock zinc leans cool/clinical; the warm shift reads as "neutral" rather than "cool gray" and rhymes with cream paper. The warmth should be felt, not seen.

3. Typography

Display Font: Fraunces (variable serif, opsz 9–144, wght 500–800, SOFT 30–100) with Georgia / serif fallback. Loaded via Google Fonts; CSP allows fonts.googleapis.com because dab is not a credential surface (tessera is).

Body Font: Hanken Grotesk (variable, wght 400–700) with system-ui fallback. The default body { font-weight: 450 } is heavier than 400 to counteract halation on dark surfaces.

Mono Font: JetBrains Mono (variable, wght 400, 500) for IDs, mono input values, DAV URLs, host labels.

Character: Fraunces is warm, slightly opinionated, with the SOFT axis turned up so the strokes feel hand-cut rather than mechanical. Paired with Hanken's quiet workhorse sans, the effect is "typeset placard above a row of stacks" — the H1 carries weight in posture, the body is the steady labor, the mono is the URL on the catalog card.

Hierarchy

  • Display (Fraunces 800, clamp(1.875rem, 2vw + 1rem, 2.25rem), line-height 1.05, tracking -0.025em): Page H1 inside PageHeader, rendered through font-display. One per page. Holds the metaphor.
  • H1 (Hanken 700, 1.5rem, tracking -0.02em): Section titles below a page H1. Rare; most surfaces don't need a second tier.
  • H2 (Hanken 600, 1.25rem): Card title, dialog title, sub-section.
  • H3 (Hanken 600, 1rem): List labels, group headers.
  • Body (Hanken 450, 0.875rem, line-height 1.55): Default body and control text. Max line length 65–75ch for paragraphs.
  • Caption (Hanken 500, 0.75rem, tracking 0.1em, uppercase): Overlines, meta labels (<dt> content in a definition list), table column headers. This is the "catalog card label" tier.
  • Mono (JetBrains Mono 400, 0.75rem): IDs, URLs, host labels, copy-paste-shaped values. Always slightly smaller than body.

Named Rules

The Serif Once Rule. Fraunces appears on the page H1 inside PageHeader and on the brand mark in the header — nowhere else. Section titles, card titles, dialog titles all use Hanken. Spreading the serif across cards turns the wing into a museum gift shop. One serif moment per page.

The 450 Floor Rule. Body text never drops below font-weight: 450. No font-light, no font-thin. Light strokes halate on dark surfaces for ~33% of users. The 450 floor is set on <body> and cascades; only override upward.

The Mono For Mono-Shaped Values Rule. JetBrains Mono is reserved for values that are themselves machine-shaped: UUIDs, DAV URLs, host labels, ETags, sync tokens. Never use mono for "tech feel" — that's the AI-template reflex. A user-facing display name is not mono.

4. Elevation

dab uses tonal layering, not shadows, to express depth. The four-tier warm-zinc hierarchy is the elevation system; box-shadow is reserved for the single context where tonal layering can't carry the load (dialogs).

Surfaces from depth to height:

  • Chrome (zinc-900 / #1b181a): Header, sidebar (none in dab), table header rows, code blocks, disclosure containers. Darker than canvas to frame the content and recede visually.
  • Canvas (#221f21): Body / main content. The brightest reading surface — the eye rests here.
  • Elevated (zinc-800 / #2a2729): Cards, menus, popovers, dropdown panels. Lighter than canvas to float.
  • Hover (zinc-700 / #423f42): Hover state on rows inside Elevated, resting borders, secondary button rest.

Shadow vocabulary:

  • Dialog scrim (backdrop:bg-zinc-950/60 backdrop:backdrop-blur-sm): Native <dialog> element backdrop. The 4px blur is cosmetic, not structural; never larger.
  • Dialog lift (shadow-2xl): Native <dialog> only. The single context where a heavy shadow earns its place — the dialog is genuinely floating over an obscured backdrop.

Named Rules

The Tonal-First Rule. Depth comes from tone, not shadow. If you need something to feel raised, give it a lighter zinc fill — don't reach for box-shadow. Shadows on cards or buttons read as decoration; they earn their place only on <dialog>.

The Sticky Header Blur Rule. Header is bg-zinc-900/95 backdrop-blur-sm (4px). Never backdrop-blur-xl (24px) — the larger radius is ~6× more expensive per frame and barely distinguishable at high background opacity. The 5% transparency lets a hint of canvas scroll through.

5. Components

For each component, the character line states the role before the values.

Header

The chrome at the top of every authed page. Sticky, framed in zinc-900/95 with a 4px backdrop blur, separated from the canvas by a 1px zinc-800/60 border-bottom. The Layers brand glyph (#db3a8b stroked, 24×24, strokeWidth={2}) sits left, with dab and your DAV-native data home stacked beside it. On hover the glyph rotates -rotate-6 — the single warmth moment in the chrome. Nav links are inline pills, accent background at 10% with accent-300 text when active, otherwise muted with hover:bg-zinc-800/70.

  • Background: bg-zinc-900/95 backdrop-blur-sm
  • Border: border-b border-zinc-800/60
  • Position: sticky top-0 z-50
  • Container: mx-auto max-w-7xl px-4 py-3 sm:px-6
  • Brand mark: Layers icon, text-accent-400, strokeWidth={2}, hover -rotate-6 transition-transform duration-200

Footer

Two links on one row, tight padding. Matches tessera's spacing exactly (py-3 mobile, py-2 sm+) so the suite reads consistent.

  • "Made with [heart] on Cloudflare" linking to https://limic.dev; the heart is filled coral-magenta at accent-500.
  • "Source code" linking to https://git.limic.dev/rachel/dab.
  • Text: text-xs text-zinc-500 with underline decoration-zinc-700 underline-offset-2 and hover:text-accent-400.

Buttons

  • Shape: Soft 12px corners (rounded-xl). 10px height (h-10) at md; 8px (h-8) at sm. Internal padding px-4/px-3.
  • Primary: bg-accent-600 text-white → hover:bg-accent-500. Solid fill, no gradient, no accent shadow. active:scale-[0.98] on press; scoped transition-[background-color,transform,border-color].
  • Secondary: border border-zinc-700/60 bg-zinc-800/60 text-zinc-200 → hover:bg-zinc-700/60. Resting on Chrome or Canvas; rests darker inside an Elevated container.
  • Danger: border border-red-500/20 bg-red-500/10 text-red-300 → hover:bg-red-500/20. Confirm-shaped actions only (Revoke, Delete); the row's "delete" affordance is a ghost button with a red icon, not a danger fill.
  • Ghost: text-zinc-400 hover:bg-zinc-800/70 hover:text-zinc-100. The default for icon-only buttons and inline row actions.
  • Loading: Loader2 from lucide spinning on the left; disable interactions and dim opacity. Optimistic where the server is fast (PAT list invalidation); spinner where it isn't (mutation in flight).

Cards

The labeled shelf. One per record (calendar, address book, PAT, file row).

  • Corner Style: rounded-2xl (16px). Same radius as dialogs; consistency across surfaces.
  • Default: border border-zinc-800/60 bg-zinc-900/50 p-5 sm:p-6. The Elevated tone over canvas without a shadow.
  • Accent (rare): border border-accent-500/20 bg-gradient-to-br from-zinc-900/80 to-zinc-900/40. Reserved for the one card per page that's the "this is the new thing you should look at" affordance (currently used by the home page tile that says "create your first calendar"). Do not multiply.
  • Interactive: Add hover:-translate-y-0.5 transition-transform cursor-pointer. Never a border-color hover transition; lighten within the same hue family if at all.

Inputs

  • Style: w-full rounded-xl border border-zinc-700/60 bg-zinc-800/80 px-4 py-2.5 text-zinc-100 placeholder:text-zinc-500.
  • Focus: focus:border-accent-500/50 focus:ring-1 focus:ring-accent-500/30 focus:outline-none. The global *:focus-visible ring (ring-2 ring-accent-500/50 ring-offset-canvas) layers over for keyboard reach.
  • Error / disabled: No special fill; inherit form-level error banner
    • native disabled opacity. The input is not the error site; the inline <ErrorBanner> is.
  • Textarea: Same as input plus resize-y rows={3}.

Badge

Pill-shaped chip, mono uppercase tracking, semantic by tone.

  • Style: inline-flex rounded-full border px-2 py-0.5 text-xs font-medium.
  • Tones: neutral (zinc), accent (coral-magenta 500/10 fill + 300 text), success (emerald), warning (amber), danger (red).
  • Use: PAT status (Active / Revoked / Expired), scope chips on PATs, any short categorical tag. Never use a badge as decoration.

Dialog

Native <dialog> element; the browser handles focus trap and Escape.

  • Container: rounded-2xl border border-zinc-700/60 bg-zinc-800/95 text-zinc-100 shadow-2xl animate-scale-fade.
  • Backdrop: backdrop:bg-zinc-950/60 backdrop:backdrop-blur-sm.
  • Layout: Header (title + close X), body, optional footer with right- aligned actions. Header / footer separated from body by border-y border-zinc-700/60.
  • Size: max-w-sm / max-w-md (default) / max-w-2xl.

Copyable (signature)

A row with a mono input on the left and a "Copy" button on the right, border around the lot. The receipt for any operation that produces a URL, token, or shelfmark.

  • Container: rounded-xl border border-zinc-700/60 bg-zinc-900/60 p-2.
  • Value: Mono, text-sm text-zinc-200, whitespace-nowrap overflow-x-auto.
  • Button: Secondary-shaped, transitions to a Check icon + "Copied" for 1.4s after success, then back. No celebratory animation; the icon swap is the entire feedback.
  • Toast: toast.success("Copied ${label}.") on success; toast.error("Could not copy ${label}.") on clipboard rejection.

Layout containers

  • Page max-width: max-w-7xl (1280px) centered, px-4 sm:px-6.
  • Page-vertical padding: py-8 inside the main wrapper.
  • PageHeader: border-b border-zinc-800/60 pb-6 mb-6. Icon (left, text-accent-400) + display H1 (Fraunces) + body description (Hanken zinc-400). Optional action row right-aligned. Stacks below sm.
  • Card grids: grid gap-3 sm:grid-cols-2 for two-column collections (calendars, addressbooks). Single column for everything else.

Named Rules

The One-H1-Per-Page Rule. Every authed page has exactly one display H1 inside PageHeader. The dashboard's tile titles are H2 / CardTitle, not H1. The serif appears once.

The Borderless Hover Rule. Hover transitions stay within the same hue family. Borders never cross-fade between zinc and accent; doing so produces a muddy intermediate even at 75ms. Use background tint shifts (hover:bg- zinc-700/60) for hover state, not border-color transitions.

The 75ms Default Rule. --default-transition-duration: 75ms overrides Tailwind's 150ms default. Anything that feels sluggish at 75ms is either animating a layout property (don't) or has too many properties in scope (also don't). Scope transitions explicitly: transition-colors, transition-transform, transition-[background-color,transform,border-color]. Never transition-all.

6. Do's and Don'ts

Concrete, forceful guardrails. Quote PRODUCT.md anti-references verbatim where they apply.

Do

  • Do use accent-600 for the primary CTA fill, hovering to accent-500. Solid, never a gradient. bg-accent-600 text-white hover:bg-accent-500.
  • Do keep the page H1 in Fraunces via font-display. One serif H1 per page. Section titles and card titles are Hanken.
  • Do treat each list row as a labeled shelf: display name, internal name (mono), one timestamp, the URL in a collapsed <details>. No charts, no sparklines, no KPI tiles.
  • Do copy tessera's footer spacing (py-3 mobile, py-2 sm+). The suite reads consistent because the footer is.
  • Do make the brand glyph rotate -rotate-6 on hover. That single micro-interaction carries the "a person made this" message; everything else stays still.
  • Do render calendar color as a h-3 w-3 rounded-full dot beside the display name. Solid fill, 1px border in zinc-700/60. No tinted background, no soft-pastel pill.
  • Do return the user to the URL as the receipt. After PAT or collection creation, the page shows the URL inside Copyable. No "Successfully created!" banner.
  • Do respect prefers-reduced-motion via the global rule. No per-element guards.
  • Do use tonal layering for depth (Chrome / Canvas / Elevated / Hover). Reach for shadow-2xl only on <dialog>.

Don't

  • Don't make dab feel like Dropbox, iCloud, or Google Drive. No tile-grid file browser. No "Upgrade to Pro" banner. No share-link modal with social buttons. No "Recents" carousel. No "+ New" floating action button.
  • Don't make dab feel like Nextcloud, ownCloud, or Synology DSM. No Bootstrap-styled "Files / Contacts / Calendar" tab bar. No left-rail accordion. No cyan-on-gray palette. No "App Center" landing page. No settings buried five clicks deep.
  • Don't ship any AI-template dark-mode signal: no neon-on-near-black, no gradient meshes, no glassmorphism (backdrop-blur-xl is forbidden), no gradient text (background-clip: text + gradient is banned), no shield-plus-checkmark hero icon, no "Your data, secure" hero copy, no "Get started in 60 seconds" CTA.
  • Don't drift into the iCloud-style "soft pastel" calendar aesthetic. Calendar color is one 12px dot, not a tinted pill. No pastel event-pill mocks anywhere in the UI. dab does not render a calendar grid; don't let the aesthetic leak in via empty states.
  • Don't use transition-all. Always scope to the properties that change. transition-colors, transition-transform, transition-[border-color,box-shadow].
  • Don't animate border hue shifts (zinc → accent on hover). The intermediate hues read as muddy even at 75ms. Use background tint shifts instead.
  • Don't use shadow-lg or shadow-xl on interactive elements. Reserve shadow-2xl for <dialog>. No colored accent shadows (shadow-accent-500/10) anywhere.
  • Don't use font-light or font-thin. Body weight floor is 450.
  • Don't use side-stripe borders (left/right border-l >1px as a colored accent on cards or rows). Universal ban in the impeccable vocabulary; rewrite with leading icons or full borders instead.
  • Don't add a "Welcome back, {name}!" or "Successfully created!" banner. The user knows their name. The row appearing is the success.
  • Don't put Fraunces on more than one element per page. Cards, dialogs, badges, and buttons all use Hanken. The serif is the signature, not decoration.
  • Don't use mono for "tech feel." JetBrains Mono is for mono-shaped values (URLs, IDs, ETags, host labels) only.