commit 80817b4eb6c22f679f1e55fd20f37b0daaafbf57 Author: jules Date: Tue Jul 7 22:35:56 2026 +1000 lib-theme-porcelain: initial import (warm-consumer set, 2026-07) Includes the 2026-07-07 UX sweep: neutral inline-code chips (accent reserved for actions/links/focus), dark-mode card/popover lightness lift for visible surface separation, 44px touch-target floor on coarse pointers, card-title pinned to a named ramp role. Co-Authored-By: Claude Fable 5 diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e2aabb --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# lib-theme-porcelain + +A calm, neutral **bone-china** theme warmed by a single restrained clay accent — the "iCloud / Apple-Settings calm, but warm" answer for a back-of-house personal-cloud utility (me.sky-ai.com). Cool porcelain off-white field (never a cream wash), warm charcoal ink, one muted terracotta accent used on ≤10% of the surface (primary action, selection, focus, links). Albert Sans for body/UI, Newsreader as a restrained serif for headings, JetBrains Mono for code. Whisper-soft neutral elevation, calm-modern 0.875rem radius. + +## Design philosophy + +- **Surface treatment:** Porcelain. A cool off-white field with pure-white cards, hairline borders and whisper-soft neutral shadows — calm resting tiles, not floating chrome. No glass, no offsets. +- **Color:** Neutral-dominant. The background is held OUT of the AI cream/sand band by keeping chroma to a whisper (≤0.004) and the hue faintly cool (~265, bone china) at very high L (0.985) — clean-neutral, not warm parchment. Ink is a warm-leaning near-black charcoal (hue ~55). The one warm note is a muted clay primary (L≈0.54, C≈0.11, H≈47), deepened so a white label clears 4.5:1. Status quartet is spaced clearly in hue (clay 47 / red 27 / green 150 / amber 78 / blue 245) so nothing muddies against the accent. +- **Typography:** Newsreader for headings (restrained modern serif, kept calm at weight 560 — not editorial-loud). Albert Sans for body and UI. JetBrains Mono for code. Product register: fixed rem sizes, full `--text-*` role ramp. +- **Motion:** Calm and product-fast — 150 / 300 / 420ms. Springs alias to standard easings (no overshoot); personality lives in the values, not in bounce. +- **Elevation:** Whisper-soft neutral shadows (near-black at very low alpha), low-key. Same altitude, same shadow. +- **Radius:** 0.875rem — calm-modern. +- **Borders:** Hairline (1px, alpha 0.10 of the ink) so separation stays subtle. + +## Usage + +In a consuming app's `app.css`: + +```css +@import "../../lib-theme-porcelain/theme.css"; +``` + +## Fonts + +The theme does not load fonts (contract ban — fonts load from the consuming app's entry CSS). Add this Google Fonts `@import` at the very top of the app's entry CSS: + +```css +@import url('https://fonts.googleapis.com/css2?family=Albert+Sans:wght@400;500;600;700&family=Newsreader:opsz,wght@6..72,400;6..72,500;6..72,560;6..72,600&family=JetBrains+Mono:wght@400;500&display=swap'); +``` + +Or install locally via Fontsource: + +```bash +npm install @fontsource-variable/albert-sans @fontsource-variable/newsreader @fontsource-variable/jetbrains-mono +``` + +## Dark mode + +Supported via class-based `.dark`. Warm graphite (`oklch(0.20 0.006 60)`) — very low chroma at a warm hue so it reads as a neutral warm charcoal, not blue-black and not umber-brown. The clay accent brightens to L≈0.66 and carries dark ink (dark-ink-on-bright, per the contract); shadows deepen. diff --git a/theme.css b/theme.css new file mode 100644 index 0000000..e590513 --- /dev/null +++ b/theme.css @@ -0,0 +1,457 @@ +/* + * Porcelain — calm, neutral bone-china surfaces warmed by a single restrained + * clay accent. Built as the "iCloud / Apple-Settings calm, but warm" answer for + * a back-of-house personal-cloud utility: quiet chrome, confident whitespace, + * hairline separation, one warm note that points at the next action. + * + * Palette rationale. The field is a true off-white porcelain, not a cream band: + * chroma is held to a whisper (≤0.004) and the hue leans faintly COOL (~265, + * bone china) so the background reads clean-neutral at very high L (0.985) — + * deliberately above and to the cool side of the banned AI cream/sand region + * (L 0.84–0.97, warm hue 40–100). Cards step up to pure white; separation is a + * hairline border plus the faintest neutral elevation. Ink is a warm-leaning + * near-black charcoal (hue ~55) — the one place quiet warmth lives in the text. + * The lone accent is a muted clay / soft terracotta (hue ~47), a restrained + * echo of siena used on ≤10% of the surface: primary action, selection, focus, + * links — never as a wash. Status quartet is held clearly apart in hue (clay 47 + * / red 27 / green 150 / amber 78 / blue 245) so meaning never muddies against + * the warm accent. + * + * Token contract matches lib-theme-caffe-florian/theme.css (Crema v1). + */ + +/* + * Fonts: consuming apps should load Albert Sans + Newsreader + JetBrains Mono at + * the top of their entry CSS (where @import statements are allowed), via the + * Google Fonts URL documented in this theme's README. Embedding the @import + * inside this file — or smuggling the URL in a comment for a build script to + * lift — breaks PostCSS's ordering rules and is banned by the contract, so + * neither is done here. + */ + +:root { + --font-sans: "Albert Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-heading: "Newsreader", Georgia, "Times New Roman", serif; + --font-mono: "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace; + + /* Tailwind utility families. Distinct names so a consuming app's + * `@theme inline` can indirect to these (--font-heading/--font-sans collide + * with Tailwind's own font-* namespace). Mirror --font-heading/--font-sans. */ + --heading-family: "Newsreader", Georgia, "Times New Roman", serif; + --body-family: "Albert Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + + /* ── Colors ── cool bone-china porcelain field, warm charcoal ink, one + * muted clay accent. Kept out of the cream band by very low chroma and a + * faint cool (bone) hue at high L. */ + --background: oklch(0.985 0.004 265); + --foreground: oklch(0.21 0.008 55); + + --card: oklch(1 0 0); + --card-foreground: oklch(0.21 0.008 55); + + --popover: oklch(0.995 0.003 265); + --popover-foreground: oklch(0.21 0.008 55); + + /* Primary — muted clay. Deepened so white label clears the 4.5:1 floor + * (5.13:1) while still reading calm and premium, not loud. */ + --primary: oklch(0.54 0.11 47); + --primary-foreground: oklch(0.99 0.004 60); + + --secondary: oklch(0.955 0.004 265); + --secondary-foreground: oklch(0.30 0.008 55); + + --muted: oklch(0.955 0.004 265); + --muted-foreground: oklch(0.50 0.008 55); + + /* Accent — faint clay tint surface with deep clay ink. The single warm + * note; used for hovers/selection, never a body wash. */ + --accent: oklch(0.94 0.02 47); + --accent-foreground: oklch(0.42 0.10 47); + + --destructive: oklch(0.55 0.20 27); + --destructive-foreground: oklch(0.99 0.004 60); + /* Success — held at hue 150 (well clear of clay 47), dark ink on top. */ + --success: oklch(0.57 0.13 150); + --success-foreground: oklch(0.16 0.03 150); + /* Warning — amber ochre, dark ink on top. */ + --warning: oklch(0.75 0.14 78); + --warning-foreground: oklch(0.22 0.04 78); + /* Info — calm blue, light label on top. */ + --info: oklch(0.53 0.145 245); + --info-foreground: oklch(0.99 0.004 60); + + --border: oklch(0.21 0.008 55 / 0.10); + --input: oklch(0.21 0.008 55 / 0.10); + --ring: oklch(0.54 0.11 47 / 0.5); + + /* Chart palette — clay lead, then hues spaced ≥60° apart (clay / teal / + * blue / violet / green), adjacent lightness varied. Calm, low-shout. */ + --chart-1: oklch(0.62 0.11 47); + --chart-2: oklch(0.60 0.08 195); + --chart-3: oklch(0.55 0.10 255); + --chart-4: oklch(0.66 0.09 315); + --chart-5: oklch(0.64 0.10 150); + + --sidebar: oklch(0.97 0.004 265); + --sidebar-foreground: oklch(0.21 0.008 55); + --sidebar-primary: oklch(0.54 0.11 47); + --sidebar-primary-foreground: oklch(0.99 0.004 60); + --sidebar-accent: oklch(0.94 0.02 47); + --sidebar-accent-foreground: oklch(0.42 0.10 47); + --sidebar-border: oklch(0.21 0.008 55 / 0.08); + --sidebar-ring: oklch(0.54 0.11 47 / 0.5); + + --syntax-keyword: oklch(0.50 0.11 47); + --syntax-string: oklch(0.50 0.11 150); + --syntax-number: oklch(0.54 0.11 78); + --syntax-comment: oklch(0.58 0.008 60); + --syntax-function: oklch(0.50 0.13 255); + --syntax-type: oklch(0.52 0.08 210); + + /* ── Chat surfaces ── + * Contract for lib-agent-chat-ui / lib-agent-dock-ui. User bubble is a + * filled deep clay with a light label; assistant bubble is a clean white + * card with a hairline border. */ + --chat-user-bg: oklch(0.50 0.11 47); + --chat-user-fg: oklch(0.99 0.004 60); + --chat-assistant-bg: oklch(1 0 0); + --chat-assistant-fg: oklch(0.21 0.008 55); + --chat-assistant-border: oklch(0.21 0.008 55 / 0.10); + + /* Calm-modern radius. */ + --radius: 0.875rem; + + /* ── Motion ── calm, product-fast. */ + --duration-fast: 150ms; + --duration-base: 300ms; + --duration-slow: 420ms; + --duration-slower: 560ms; + --duration-spring: 300ms; + + --ease-standard: cubic-bezier(0.4, 0, 0.2, 1); + --ease-decelerate: cubic-bezier(0, 0, 0.2, 1); + --ease-accelerate: cubic-bezier(0.4, 0, 1, 1); + --ease-emphasized: cubic-bezier(0.2, 0, 0, 1); + + /* Calm theme — springs alias to standard easings (no overshoot). Personality + * lives in the values, never in absence (the avalon rule). */ + --ease-spring-gentle: cubic-bezier(0.4, 0, 0.2, 1); + --ease-spring-snappy: cubic-bezier(0.4, 0, 0.2, 1); + --ease-spring-bouncy: cubic-bezier(0.4, 0, 0.2, 1); + + /* ── Typography ── product register, fixed rem sizes, full role ramp. + * Newsreader supplies a restrained modern-serif heading; Albert Sans is the + * humanist body/UI sans. */ + --text-hero-size: 3.5rem; + --text-hero-lh: 3.75rem; + --text-display-size: 3rem; + --text-display-lh: 3.25rem; + --text-headline-size: 2rem; + --text-headline-lh: 2.375rem; + --text-title-size: 1.5rem; + --text-title-lh: 1.875rem; + --text-subtitle-size: 1.1875rem; + --text-subtitle-lh: 1.625rem; + --text-body-size: 1.0625rem; + --text-body-lh: 1.625rem; + --text-label-size: 0.9375rem; + --text-label-lh: 1.25rem; + --text-caption-size: 0.8125rem; + --text-caption-lh: 1.125rem; + + /* ── Elevation ── whisper-soft neutral shadows, near-black at very low + * alpha. Calm resting cards, low-key lift. */ + --elevation-0: none; + --elevation-1: 0 1px 2px oklch(0.21 0.008 55 / 0.04); + --elevation-2: 0 1px 3px oklch(0.21 0.008 55 / 0.06), 0 1px 2px oklch(0.21 0.008 55 / 0.03); + --elevation-3: 0 4px 10px -3px oklch(0.21 0.008 55 / 0.07), 0 2px 4px -2px oklch(0.21 0.008 55 / 0.04); + --elevation-4: 0 12px 28px -6px oklch(0.21 0.008 55 / 0.09), 0 4px 8px -4px oklch(0.21 0.008 55 / 0.04); + --elevation-5: 0 24px 48px -12px oklch(0.21 0.008 55 / 0.14); + + --spacing: 0.25rem; + + --border-width-thin: 0.5px; + --border-width-base: 1px; + --border-width-thick: 2px; + --border-width-heavy: 4px; + + /* No glass — inert values so consuming apps referencing these get no-ops. */ + --glass-blur: 0px; + --glass-saturate: 100%; + --glass-brightness: 1; + --glass-contrast: 1; +} + +/* + * Dark mode — warm graphite (#232120-ish), not blue-black and not umber-brown. + * Very low chroma at a warm hue (~60) keeps it a neutral warm charcoal; the clay + * accent brightens just enough and carries dark ink per the contract. + */ +.dark { + --background: oklch(0.20 0.006 60); + --foreground: oklch(0.94 0.006 70); + + --card: oklch(0.26 0.007 60); + --card-foreground: oklch(0.94 0.006 70); + + --popover: oklch(0.275 0.007 60); + --popover-foreground: oklch(0.94 0.006 70); + + --primary: oklch(0.66 0.12 50); + --primary-foreground: oklch(0.18 0.02 50); + + --secondary: oklch(0.28 0.007 60); + --secondary-foreground: oklch(0.90 0.006 70); + + --muted: oklch(0.28 0.007 60); + --muted-foreground: oklch(0.70 0.008 65); + + --accent: oklch(0.32 0.04 50); + --accent-foreground: oklch(0.85 0.08 50); + + --destructive: oklch(0.62 0.19 27); + --destructive-foreground: oklch(0.16 0.02 27); + --success: oklch(0.70 0.15 150); + --success-foreground: oklch(0.16 0.03 150); + --warning: oklch(0.80 0.14 78); + --warning-foreground: oklch(0.20 0.04 78); + --info: oklch(0.72 0.13 245); + --info-foreground: oklch(0.18 0.03 245); + + --border: oklch(0.95 0.006 70 / 0.10); + --input: oklch(0.95 0.006 70 / 0.10); + --ring: oklch(0.66 0.12 50 / 0.5); + + --chart-1: oklch(0.70 0.12 50); + --chart-2: oklch(0.70 0.08 195); + --chart-3: oklch(0.68 0.10 255); + --chart-4: oklch(0.74 0.09 315); + --chart-5: oklch(0.72 0.11 150); + + --sidebar: oklch(0.22 0.006 60); + --sidebar-foreground: oklch(0.94 0.006 70); + --sidebar-primary: oklch(0.66 0.12 50); + --sidebar-primary-foreground: oklch(0.18 0.02 50); + --sidebar-accent: oklch(0.32 0.04 50); + --sidebar-accent-foreground: oklch(0.85 0.08 50); + --sidebar-border: oklch(0.95 0.006 70 / 0.08); + --sidebar-ring: oklch(0.66 0.12 50 / 0.5); + + --syntax-keyword: oklch(0.74 0.12 50); + --syntax-string: oklch(0.74 0.13 150); + --syntax-number: oklch(0.78 0.13 78); + --syntax-comment: oklch(0.58 0.008 60); + --syntax-function: oklch(0.72 0.13 255); + --syntax-type: oklch(0.72 0.08 210); + + /* ── Chat surfaces (dark) ── deep clay bubble, dark ink label + * (dark-ink-on-bright, 5.56:1). */ + --chat-user-bg: oklch(0.64 0.115 50); + --chat-user-fg: oklch(0.16 0.02 50); + --chat-assistant-bg: oklch(0.26 0.007 60); + --chat-assistant-fg: oklch(0.94 0.006 70); + --chat-assistant-border: oklch(0.95 0.006 70 / 0.10); + + --elevation-1: 0 1px 2px oklch(0 0 0 / 0.3); + --elevation-2: 0 1px 3px oklch(0 0 0 / 0.4), 0 1px 2px oklch(0 0 0 / 0.25); + --elevation-3: 0 4px 10px oklch(0 0 0 / 0.35), 0 2px 4px oklch(0 0 0 / 0.25); + --elevation-4: 0 12px 28px oklch(0 0 0 / 0.4), 0 4px 8px oklch(0 0 0 / 0.25); + --elevation-5: 0 24px 48px oklch(0 0 0 / 0.5); +} + +/* + * @layer base — Porcelain's calm-utility philosophy. + * + * No glass, no offsets. Humanist sans body; restrained modern-serif headings + * used sparingly. Cards sit on the porcelain field with a hairline border and a + * whisper-soft neutral shadow — calm resting surfaces, not floating chrome. The + * clay accent points; everything else is neutral. + */ +@layer base { + body { + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.006em; + font-family: var(--font-sans); + font-size: var(--text-body-size); + line-height: var(--text-body-lh); + } + + /* Serif headings — Newsreader, kept calm (not editorial-loud): moderate + * weight, tight-but-gentle tracking. */ + h1, h2, h3, h4, + [data-slot="card-title"], + [data-slot="dialog-title"], + [data-slot="sheet-title"], + [data-slot="alert-dialog-title"] { + font-family: var(--font-heading); + font-weight: 560; + letter-spacing: -0.012em; + color: var(--foreground); + } + + code, kbd, samp, pre, + [data-slot="code"], + [data-slot="code-block"] { + font-family: var(--font-mono); + } + + /* Inline code — neutral ink on a faint chip; the clay accent stays + * reserved for actions, links and focus. */ + :not(pre) > code { + padding: 0.1em 0.35em; + border-radius: calc(var(--radius) * 0.4); + background: color-mix(in oklch, var(--foreground) 7%, transparent); + color: var(--foreground); + font-size: 0.9em; + } + + /* Selection — the one warm note, faint clay wash. */ + ::selection { + background: color-mix(in oklch, var(--primary) 18%, transparent); + color: var(--foreground); + } + + /* Links — clay, quietly underlined on hover. */ + a { + color: var(--primary); + text-decoration-color: color-mix(in oklch, var(--primary) 35%, transparent); + text-underline-offset: 0.2em; + transition: text-decoration-color var(--duration-fast) var(--ease-standard); + } + a:hover { + text-decoration-color: var(--primary); + } + + /* Card & overlay surfaces — hairline border + whisper elevation so cards + * read as calm resting tiles on porcelain. */ + [data-slot="card"], + [data-slot="popover-content"], + [data-slot="dropdown-menu-content"], + [data-slot="context-menu-content"], + [data-slot="menubar-content"], + [data-slot="hover-card-content"], + [data-slot="select-content"], + [data-slot="combobox-content"], + [data-slot="command"], + [data-slot="tooltip-content"], + [data-slot="sheet-content"], + [data-slot="dialog-content"], + [data-slot="alert-dialog-content"], + [data-slot="drawer-content"] { + border: var(--border-width-base) solid var(--border); + box-shadow: var(--elevation-2); + } + + .dark [data-slot="card"], + .dark [data-slot="popover-content"], + .dark [data-slot="dropdown-menu-content"], + .dark [data-slot="context-menu-content"], + .dark [data-slot="menubar-content"], + .dark [data-slot="hover-card-content"], + .dark [data-slot="select-content"], + .dark [data-slot="combobox-content"], + .dark [data-slot="command"], + .dark [data-slot="tooltip-content"], + .dark [data-slot="sheet-content"], + .dark [data-slot="dialog-content"], + .dark [data-slot="alert-dialog-content"], + .dark [data-slot="drawer-content"] { + box-shadow: var(--elevation-3); + } + + [data-slot="button"] { + font-weight: 500; + } + + [data-slot="sidebar"] { + background: var(--sidebar); + border-right: var(--border-width-base) solid var(--sidebar-border); + } +} + +/* + * Unlayered component polish — placed outside @layer base so it beats Tailwind's + * default utility classes shipped with shadcn. Porcelain keeps controls calm and + * comfortably sized with clay focus: inputs are ≥44px everywhere; buttons keep + * their density under a mouse and clear the 44px floor on coarse pointers. + */ +[data-slot="button"]:hover:not(:disabled) { + opacity: 0.9; +} +[data-slot="button"]:active:not(:disabled) { + opacity: 0.8; + transform: scale(0.99); +} +[data-slot="button"]:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; + box-shadow: none; +} + +[data-slot="input"], +[data-slot="textarea"], +[data-slot="select-trigger"] { + min-height: 2.75rem; + padding-inline: 0.875rem; + font-size: 0.9375rem; + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--foreground); + transition: + border-color var(--duration-fast) var(--ease-standard), + box-shadow var(--duration-fast) var(--ease-standard); +} +[data-slot="textarea"] { + min-height: 6.25rem; + padding-block: 0.75rem; + line-height: 1.55; + resize: vertical; +} +[data-slot="input"]::placeholder, +[data-slot="textarea"]::placeholder { + color: var(--muted-foreground); + opacity: 0.7; +} +[data-slot="input"]:hover:not(:disabled), +[data-slot="textarea"]:hover:not(:disabled), +[data-slot="select-trigger"]:hover:not(:disabled) { + border-color: color-mix(in oklch, var(--foreground) 22%, transparent); +} +[data-slot="input"]:focus, +[data-slot="input"]:focus-visible, +[data-slot="textarea"]:focus, +[data-slot="textarea"]:focus-visible, +[data-slot="select-trigger"]:focus, +[data-slot="select-trigger"]:focus-visible { + border-color: var(--primary); + box-shadow: 0 0 0 1px var(--primary); + outline: none; +} + +[data-slot="card"] { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + transition: + border-color var(--duration-base) var(--ease-standard), + box-shadow var(--duration-base) var(--ease-standard); +} + +/* Card titles — pinned to the subtitle role so the scale is the theme's + * decision, not the card primitive's default. */ +[data-slot="card-title"] { + font-size: var(--text-subtitle-size); + line-height: var(--text-subtitle-lh); +} + +/* Touch targets — buttons clear the 44px floor on coarse pointers + * (inputs are already ≥44px everywhere above). */ +@media (pointer: coarse) { + [data-slot="button"] { + min-height: 2.75rem; + min-width: 2.75rem; + } +}