From caee7f667242ca4d473fa3cda72b72297ae1a200 Mon Sep 17 00:00:00 2001 From: jules Date: Tue, 7 Jul 2026 22:56:29 +1000 Subject: [PATCH] lib-theme-phosphor: initial import (calm-pro/dense-utility set, 2026-07) One of four divergent candidates for the operator/console theme family (successor to Skyrise for developer-product screens). Dense/pro type register, keyboard-first focus treatment, tabular numerals, coarse-pointer 44px touch floor, neutral code chips, dark card separation per house sweep. Co-Authored-By: Claude Fable 5 --- README.md | 41 +++++ theme.css | 440 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 481 insertions(+) create mode 100644 README.md create mode 100644 theme.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d4ea53 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# lib-theme-phosphor + +**Terminal heritage, dark-first.** Phosphor's dark mode is its native identity: deep charcoal +with a barely-there green undertone (never blue-gray, never pure black), cards stepping +*lighter* off the background, and hairline light-alpha borders doing all the separation work — +the elevation ladder exists but is whisper-quiet. One phosphor-green accent +(`oklch(0.75 0.16 148)` carrying dark ink in dark mode; deepened to `oklch(0.45 0.12 150)` +under white labels in light mode) is spent on the primary action, focus, and selection only. +Light mode is the "paper terminal": warm-white paper, deep green-charcoal ink. Success is teal +(hue 178) so state-green never collides with action-green. Dense/pro register, radius +0.375rem, instant-feeling motion (100/180/280/400ms, springs aliased — no bounce), +keyboard-first focus (2px offset phosphor outline on buttons, flush 1px ring on inputs), +tabular numerals on tables and via the `.num` class. + +Built as one of four candidates for the calm-pro / dense-utility family (operator, console, +and admin surfaces; Skyrise successor for developer-product screens). + +## Fonts + +The signature is **IBM Plex Mono for headings** (weight 500/600, slightly tight tracking) over +**IBM Plex Sans** body, with Plex Mono again for code. Mono headings run wide, so the display +ramp is deliberately modest. + +Per the Crema token contract, `theme.css` does **not** `@import` fonts. The consuming app must +load them at the very top of its entry CSS (`app/app.css`): + +```css +@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap"); +``` + +Stacks (already wired in `theme.css`): + +- `--font-sans` / `--body-family`: `"IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` +- `--font-heading` / `--heading-family`: `"IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace` +- `--font-mono`: same mono stack as headings + +## Validate + +```bash +node create-crema-app/scripts/validate-theme.mjs lib-theme-phosphor/theme.css +``` diff --git a/theme.css b/theme.css new file mode 100644 index 0000000..0ce5914 --- /dev/null +++ b/theme.css @@ -0,0 +1,440 @@ +/* + * Phosphor — terminal heritage, dark-first. + * + * The dark mode IS the theme: a deep charcoal with a barely-there green + * undertone (hue ~150, chroma 0.006 — never blue-gray, never pure black), + * the way a switched-off CRT still remembers its phosphor coat. Cards step + * LIGHTER off the background by a full 0.06 L, and hairline light-alpha + * borders do all the separation work — the elevation ladder exists but is + * whisper-quiet, because on a terminal, borders beat shadows. The single + * accent is phosphor green (~oklch(0.75 0.16 148)) carrying DARK ink on the + * bright fill (the ivrea dark-ink-on-bright pattern), spent on the primary + * action, focus, and selection only. + * + * Light mode is the "paper terminal": warm-white paper (hue ~120, faint) with + * deep green-charcoal ink, and the same green deepened to ~L0.45 so white + * labels clear the floor. Success is pulled to TEAL (hue 178, 30° from the + * primary's 148) so state-green never collides with action-green. + * + * Type is the signature: IBM Plex Mono for HEADINGS (500/600, slightly tight), + * IBM Plex Sans for body, Plex Mono for code. Dense/pro register — body + * 0.9375rem, modest display sizes (mono runs wide). Radius is a tight + * 0.375rem; motion is instant-feeling (100/180/280/400ms), springs aliased. + * Syntax colors are a muted-precise terminal scheme — no neon soup. + * + * Token contract matches lib-theme-caffe-florian/theme.css (Crema v1). + */ + +/* + * Fonts: consuming apps load IBM Plex Sans + IBM Plex 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 here — + * or smuggling the URL in a comment for a build script to lift — breaks + * PostCSS's ordering rules, so neither is done in this file. + */ + +:root { + --font-sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-heading: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; + --font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", 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: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; + --body-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + + /* ── Colors ── paper terminal: warm-white paper, deep green-charcoal ink, + * one deepened phosphor-green accent carrying white labels. */ + --background: oklch(0.975 0.004 120); + --foreground: oklch(0.22 0.015 150); + + --card: oklch(0.99 0.002 120); + --card-foreground: oklch(0.22 0.015 150); + + --popover: oklch(0.995 0.002 120); + --popover-foreground: oklch(0.22 0.015 150); + + /* Primary — phosphor green deepened for paper (L0.45) so near-white + * labels sit well above the floor. The bright terminal green returns + * in dark mode, where it belongs. */ + --primary: oklch(0.45 0.12 150); + --primary-foreground: oklch(0.98 0.008 140); + + --secondary: oklch(0.93 0.008 140); + --secondary-foreground: oklch(0.3 0.015 150); + + --muted: oklch(0.935 0.007 135); + --muted-foreground: oklch(0.44 0.018 150); + + /* Accent — hover/selection wash: a faint green tint with deep green ink, + * not a second competing fill. */ + --accent: oklch(0.93 0.028 148); + --accent-foreground: oklch(0.33 0.09 150); + + /* ── Status ── success is TEAL (hue 178, 30° off the green primary) so + * "ok" never impersonates "act". Destructive/info carry light labels on + * deep fills; warning carries dark amber ink on a mid amber. */ + --destructive: oklch(0.55 0.21 27); + --destructive-foreground: oklch(0.98 0.01 27); + --success: oklch(0.5 0.11 178); + --success-foreground: oklch(0.98 0.01 178); + --warning: oklch(0.66 0.15 80); + --warning-foreground: oklch(0.21 0.05 80); + --info: oklch(0.5 0.13 235); + --info-foreground: oklch(0.98 0.01 235); + + /* Borders do the separation work — a touch stronger than house norm. */ + --border: oklch(0.22 0.015 150 / 0.14); + --input: oklch(0.22 0.015 150 / 0.16); + --ring: oklch(0.45 0.12 150 / 0.6); + + /* Chart palette — phosphor green lead, then blue / amber / violet / red. + * Hues spaced, adjacent-index lightness varied. */ + --chart-1: oklch(0.52 0.13 150); + --chart-2: oklch(0.58 0.13 235); + --chart-3: oklch(0.68 0.14 80); + --chart-4: oklch(0.55 0.15 310); + --chart-5: oklch(0.6 0.17 25); + + --sidebar: oklch(0.955 0.006 130); + --sidebar-foreground: oklch(0.22 0.015 150); + --sidebar-primary: oklch(0.45 0.12 150); + --sidebar-primary-foreground: oklch(0.98 0.008 140); + --sidebar-accent: oklch(0.92 0.028 148); + --sidebar-accent-foreground: oklch(0.33 0.09 150); + --sidebar-border: oklch(0.22 0.015 150 / 0.1); + --sidebar-ring: oklch(0.45 0.12 150 / 0.6); + + /* Syntax — paper-terminal ink: precise, low-chroma, green-led. */ + --syntax-keyword: oklch(0.45 0.11 150); + --syntax-string: oklch(0.5 0.09 80); + --syntax-number: oklch(0.48 0.09 178); + --syntax-comment: oklch(0.55 0.015 150); + --syntax-function: oklch(0.48 0.11 235); + --syntax-type: oklch(0.5 0.09 310); + + /* ── Chat surfaces ── + * Contract for lib-chat-ui / lib-agent-ui / lib-agent-dock-ui. User bubble + * is the deep green-charcoal ink as a panel with paper text; assistant is + * a clean card behind a hairline border. */ + --chat-user-bg: oklch(0.27 0.018 150); + --chat-user-fg: oklch(0.97 0.005 120); + --chat-assistant-bg: oklch(0.99 0.002 120); + --chat-assistant-fg: oklch(0.22 0.015 150); + --chat-assistant-border: oklch(0.22 0.015 150 / 0.14); + + /* Tight console radius. */ + --radius: 0.375rem; + + /* ── Motion ── instant-feeling. Springs alias to the standard eases — + * a console does not bounce; personality lives in the speed. */ + --duration-fast: 100ms; + --duration-base: 180ms; + --duration-slow: 280ms; + --duration-slower: 400ms; + --duration-spring: 180ms; + + --ease-standard: cubic-bezier(0.2, 0, 0, 1); + --ease-decelerate: cubic-bezier(0, 0, 0, 1); + --ease-accelerate: cubic-bezier(0.3, 0, 1, 1); + --ease-emphasized: cubic-bezier(0.2, 0, 0, 1); + + --ease-spring-gentle: var(--ease-standard); + --ease-spring-snappy: var(--ease-decelerate); + --ease-spring-bouncy: var(--ease-standard); + + /* ── Typography ── dense/pro register, fixed rem. Display sizes run + * modest because mono headings run wide. */ + --text-hero-size: 2.75rem; + --text-hero-lh: 1.1; + --text-display-size: 2rem; + --text-display-lh: 1.15; + --text-headline-size: 1.5rem; + --text-headline-lh: 1.25; + --text-title-size: 1.1875rem; + --text-title-lh: 1.35; + --text-subtitle-size: 1.0625rem; + --text-subtitle-lh: 1.45; + --text-body-size: 0.9375rem; + --text-body-lh: 1.5; + --text-label-size: 0.8125rem; + --text-label-lh: 1.25rem; + --text-caption-size: 0.75rem; + --text-caption-lh: 1.0625rem; + + /* ── Elevation ── whisper-quiet: borders separate, shadows only hint. + * The ladder is present but nearly flat. */ + --elevation-0: none; + --elevation-1: 0 1px 1px oklch(0.22 0.015 150 / 0.04); + --elevation-2: 0 1px 2px oklch(0.22 0.015 150 / 0.05); + --elevation-3: 0 2px 4px -1px oklch(0.22 0.015 150 / 0.06); + --elevation-4: 0 4px 10px -3px oklch(0.22 0.015 150 / 0.08); + --elevation-5: 0 8px 20px -6px oklch(0.22 0.015 150 / 0.1); + + --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 — the native identity. Deep charcoal with the faint phosphor + * undertone (hue 150, chroma 0.006 — not blue-gray, not black). Cards step + * a full 0.06 L LIGHTER; popover sits one step above card, below muted. + * Hairline light-alpha borders carry the structure; the bright phosphor + * green takes dark ink on top. + */ +.dark { + --background: oklch(0.17 0.006 150); + --foreground: oklch(0.93 0.008 150); + + --card: oklch(0.23 0.007 150); + --card-foreground: oklch(0.93 0.008 150); + + --popover: oklch(0.25 0.007 150); + --popover-foreground: oklch(0.93 0.008 150); + + --primary: oklch(0.75 0.16 148); + --primary-foreground: oklch(0.18 0.045 148); + + --secondary: oklch(0.27 0.008 150); + --secondary-foreground: oklch(0.9 0.008 150); + + --muted: oklch(0.27 0.008 150); + --muted-foreground: oklch(0.71 0.012 150); + + --accent: oklch(0.29 0.04 148); + --accent-foreground: oklch(0.86 0.12 148); + + --destructive: oklch(0.68 0.19 25); + --destructive-foreground: oklch(0.16 0.045 25); + --success: oklch(0.78 0.13 178); + --success-foreground: oklch(0.18 0.04 178); + --warning: oklch(0.8 0.15 80); + --warning-foreground: oklch(0.2 0.05 80); + --info: oklch(0.72 0.12 235); + --info-foreground: oklch(0.16 0.04 235); + + /* Light-alpha hairlines — a touch stronger than house norm; borders are + * the separation mechanism here, not shadows. */ + --border: oklch(0.95 0.01 150 / 0.14); + --input: oklch(0.95 0.01 150 / 0.17); + --ring: oklch(0.75 0.16 148 / 0.6); + + --chart-1: oklch(0.75 0.16 148); + --chart-2: oklch(0.66 0.13 235); + --chart-3: oklch(0.79 0.14 80); + --chart-4: oklch(0.64 0.14 310); + --chart-5: oklch(0.68 0.17 25); + + /* Sidebar drops BELOW the background — the darkest rail on screen, a + * terminal gutter. */ + --sidebar: oklch(0.15 0.006 150); + --sidebar-foreground: oklch(0.93 0.008 150); + --sidebar-primary: oklch(0.75 0.16 148); + --sidebar-primary-foreground: oklch(0.18 0.045 148); + --sidebar-accent: oklch(0.27 0.04 148); + --sidebar-accent-foreground: oklch(0.86 0.12 148); + --sidebar-border: oklch(0.95 0.01 150 / 0.1); + --sidebar-ring: oklch(0.75 0.16 148 / 0.6); + + /* Syntax — a great terminal colorscheme: green keywords, warm strings, + * teal numbers, dim comments. Chroma capped ~0.12; muted-precise. */ + --syntax-keyword: oklch(0.78 0.11 148); + --syntax-string: oklch(0.79 0.09 80); + --syntax-number: oklch(0.76 0.09 178); + --syntax-comment: oklch(0.58 0.015 150); + --syntax-function: oklch(0.77 0.08 235); + --syntax-type: oklch(0.74 0.08 310); + + /* ── Chat surfaces (dark) ── user panel steps above muted; assistant sits + * on the card step behind a hairline. */ + --chat-user-bg: oklch(0.31 0.015 150); + --chat-user-fg: oklch(0.96 0.006 150); + --chat-assistant-bg: oklch(0.23 0.007 150); + --chat-assistant-fg: oklch(0.93 0.008 150); + --chat-assistant-border: oklch(0.95 0.01 150 / 0.14); + + --elevation-1: 0 1px 1px oklch(0 0 0 / 0.16); + --elevation-2: 0 1px 2px oklch(0 0 0 / 0.2); + --elevation-3: 0 2px 4px -1px oklch(0 0 0 / 0.24); + --elevation-4: 0 4px 10px -3px oklch(0 0 0 / 0.28); + --elevation-5: 0 8px 20px -6px oklch(0 0 0 / 0.34); +} + +/* + * @layer base — Phosphor's dense, keyboard-first console philosophy. + * + * Mono headings, hairline-separated calm surfaces, tabular numerals in data + * contexts, and the phosphor green reserved for focus, selection, and the + * primary action. Everything else stays quiet. + */ +@layer base { + body { + min-height: 100vh; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.004em; + font-family: var(--font-sans); + font-size: var(--text-body-size); + line-height: var(--text-body-lh); + } + + /* Headings — IBM Plex Mono, the signature. 600, slightly tight; mono + * already runs wide, so no extra 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: 600; + letter-spacing: -0.015em; + color: var(--foreground); + } + + h4, + [data-slot="card-title"] { + font-weight: 500; + } + + code, kbd, samp, pre, + [data-slot="code"], + [data-slot="code-block"] { + font-family: var(--font-mono); + } + + /* Inline code — neutral chip: faint foreground tint, foreground ink. + * Never accent-colored. */ + :not(pre) > code { + padding: 0.1em 0.35em; + border-radius: calc(var(--radius) * 0.5); + background: color-mix(in oklch, var(--foreground) 6%, transparent); + color: var(--foreground); + font-size: 0.9em; + } + + /* Dense utility — data reads in columns. Tabular numerals for tables and + * an opt-in .num class for stat tiles, IDs, and metric rows. */ + [data-slot="table"] { + font-variant-numeric: tabular-nums; + } + .num { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + letter-spacing: -0.01em; + } + + /* Selection — the phosphor spark. */ + ::selection { + background: color-mix(in oklch, var(--primary) 30%, transparent); + color: var(--foreground); + } + + /* Links — green-leaning ink, underline-on-hover. */ + a { + color: color-mix(in oklch, var(--primary) 70%, var(--foreground)); + text-decoration-color: color-mix(in oklch, var(--primary) 40%, transparent); + text-underline-offset: 0.2em; + transition: text-decoration-color var(--duration-fast) var(--ease-standard); + } + a:hover { + text-decoration-color: var(--primary); + } + + [data-slot="sidebar"] { + background: var(--sidebar); + border-right: var(--border-width-base) solid var(--sidebar-border); + } +} + +/* + * Focus-visible — keyboard-first is a signature. Buttons get a crisp 2px + * phosphor outline, offset; inputs get a flush 1px ring plus a border-color + * change (see the input block below). Placed outside @layer base so it beats + * Tailwind's default ring utilities. + */ +[data-slot="button"]:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; + box-shadow: none; +} +[data-slot="input"]:focus-visible, +[data-slot="textarea"]:focus-visible, +[data-slot="select-trigger"]:focus-visible { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 1px var(--primary); +} + +/* Buttons — quiet weight; opacity absorb on press, instant. */ +[data-slot="button"] { + font-weight: 500; + letter-spacing: -0.004em; +} + +/* 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 — desktop keeps the dense console ladder; coarse pointers + * get the 44px floor on every control. */ +@media (pointer: coarse) { + [data-slot="button"] { + min-height: 2.75rem; + min-width: 2.75rem; + } + [data-slot="input"], + [data-slot="select-trigger"] { + min-height: 2.75rem; + } +} +[data-slot="button"]:hover:not(:disabled) { + opacity: 0.9; +} +[data-slot="button"]:active:not(:disabled) { + opacity: 0.82; + transform: scale(0.99); +} + +/* Inputs — card surface, hairline border, phosphor focus as a flush ring. */ +[data-slot="input"], +[data-slot="textarea"], +[data-slot="select-trigger"] { + background: var(--card); + border: var(--border-width-base) solid var(--input); + 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="input"]::placeholder, +[data-slot="textarea"]::placeholder { + color: var(--muted-foreground); + opacity: 0.8; +} +[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) 26%, transparent); +} +[data-slot="input"]:focus, +[data-slot="textarea"]:focus, +[data-slot="select-trigger"]:focus { + border-color: var(--primary); + box-shadow: 0 0 0 1px var(--primary); + outline: none; +}