commit 3a6bca9f2b813dce7c5c52aad412ad33e3a8bfb1 Author: jules Date: Wed May 20 16:25:46 2026 +1000 Initial commit: Modena theme + --chat-* surface tokens Co-Authored-By: Claude Opus 4.7 (1M context) diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a97eb1 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# lib-theme-modena + +Editorial design system theme for Crema. Ports the Modena content-presentation +patterns (centered article hero, serif body, sibling-margin prose rhythm, +primary-rail blockquotes, dark code blocks) into a single `theme.css` you +import like any other Crema theme. + +## Use + +```css +/* app/app.css */ +@import "../../lib-theme-modena/theme.css"; +@import "tailwindcss"; +``` + +That's it. All `@crema/content-ui` consumers (Article, ArticleHeader, +ArticleMeta, Prose, Pullquote, etc.) automatically pick up the editorial +look because Modena targets `[data-slot="article"]` and `[data-slot="prose"]`. + +## What it provides + +- **Standard Crema tokens** — `--background`, `--foreground`, `--primary`, + `--card`, `--border`, etc. — so shadcn/lib-* components keep working. +- **Modena-specific tokens** — `--mod-prose-*`, `--mod-text-*`, + `--mod-accent`, `--mod-gray-*`, `--mod-radius-*`, `--mod-space-*`, + `--mod-tracking-*` — consumed by `lib-modena-ui` components. +- **Newsreader** editorial serif (Google Fonts, optical-size enabled) + loaded at theme-import time. +- **Article surface styling** — applies to anything inside + `[data-slot="article"]`. Comment bodies and other Prose surfaces outside + the article stay neutral. + +## Override + +Override any token at the app level to brand: + +```css +:root { + --mod-accent: oklch(0.62 0.20 30); /* venetian red */ +} +``` + +## Dark mode + +Add `class="dark"` to ``. All tokens are remapped. + +## Pairs with + +- `@crema/content-ui` — article + prose primitives. +- `lib-modena-ui` — Modena-specific extra components (Callout, Tag, Bookmark, + Comparison, Changelog, etc.). diff --git a/theme.css b/theme.css new file mode 100644 index 0000000..77d1240 --- /dev/null +++ b/theme.css @@ -0,0 +1,447 @@ +/* + * Modena — Editorial design system theme. + * + * Content-presentation tokens + base typography. Designed for blogs, + * articles, documentation, and long-form essays. Editorial minimalism: + * the UI disappears so the content takes center stage. + * + * Ports the relevant subset of the Modena reference into a Crema-style + * theme.css that: + * • Defines the standard Crema design tokens (--background, --primary, + * --foreground, etc.) so all shadcn/lib-* components keep working. + * • Adds Modena-specific tokens (--mod-prose-*, --mod-text-*, etc.) + * consumed by lib-modena-ui components and the article surface. + * • Applies base typography to h1-h4, [data-slot="article"], and + * [data-slot="prose"] so any consumer of @crema/content-ui + * automatically picks up the editorial look. + * + * Pairs with: @crema/content-ui (article primitives), lib-modena-ui + * (Modena-specific extra components). + */ + +/* Newsreader for the editorial serif. Pulled at theme-import time so + * consumers don't have to remember to import the font separately. */ +@import url("https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,700;1,6..72,400&family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap"); + +:root { + /* ── Crema-shaped tokens (apps and shadcn components consume these) ── */ + --background: oklch(1 0 0); + --foreground: oklch(0.16 0.02 255); + --card: oklch(1 0 0); + --card-foreground: oklch(0.16 0.02 255); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.16 0.02 255); + --primary: oklch(0.55 0.22 260); + --primary-foreground: oklch(0.99 0.005 80); + --secondary: oklch(0.96 0.005 250); + --secondary-foreground: oklch(0.20 0.025 255); + --muted: oklch(0.97 0.003 250); + --muted-foreground: oklch(0.50 0.015 250); + --accent: oklch(0.95 0.01 260); + --accent-foreground: oklch(0.18 0.02 255); + --destructive: oklch(0.55 0.22 25); + --destructive-foreground: oklch(0.99 0.005 80); + --success: oklch(0.65 0.18 145); + --success-foreground: oklch(0.99 0.005 80); + --warning: oklch(0.72 0.16 70); + --warning-foreground: oklch(0.15 0.02 70); + --border: oklch(0.92 0.005 250); + --input: oklch(0.92 0.005 250); + --ring: oklch(0.55 0.22 260); + + /* ── Chat surfaces ── + * Contract for lib-agent-chat-ui / lib-agent-dock-ui. User bubble is + * a filled, deep, high-contrast panel with light text; assistant + * bubble is a card-like surface with a hairline border. */ + --chat-user-bg: oklch(0.48 0.2 260); + --chat-user-fg: oklch(0.99 0.005 80); + --chat-assistant-bg: oklch(0.98 0.003 250); + --chat-assistant-fg: oklch(0.16 0.02 255); + --chat-assistant-border: oklch(0.92 0.005 250); + + --radius: 0.5rem; + --sidebar: oklch(0.985 0.003 250); + --sidebar-foreground: oklch(0.16 0.02 255); + --sidebar-accent: oklch(0.94 0.005 250); + --sidebar-accent-foreground: oklch(0.20 0.025 255); + --sidebar-border: oklch(0.92 0.005 250); + --sidebar-ring: oklch(0.55 0.22 260); + --sidebar-primary: oklch(0.55 0.22 260); + --sidebar-primary-foreground: oklch(0.99 0.005 80); + + /* ── Modena-specific tokens ─────────────────────────────────────── */ + + /* Accent — duplicate of --primary so Modena components can read + * either. Override --mod-accent at the app/site level to brand. */ + --mod-accent: var(--primary); + + /* Gray scale (11 steps, matches Modena's reference). */ + --mod-gray-50: oklch(0.985 0.002 250); + --mod-gray-100: oklch(0.965 0.003 250); + --mod-gray-200: oklch(0.92 0.004 250); + --mod-gray-300: oklch(0.86 0.006 250); + --mod-gray-400: oklch(0.70 0.012 250); + --mod-gray-500: oklch(0.55 0.018 250); + --mod-gray-600: oklch(0.45 0.020 250); + --mod-gray-700: oklch(0.36 0.022 250); + --mod-gray-800: oklch(0.24 0.024 250); + --mod-gray-900: oklch(0.17 0.025 250); + --mod-gray-950: oklch(0.10 0.025 250); + + /* Text */ + --mod-text: var(--foreground); + --mod-text-secondary: oklch(0.42 0.020 250); + --mod-text-muted: var(--muted-foreground); + --mod-text-faint: oklch(0.68 0.012 250); + + /* Prose */ + --mod-prose-color: oklch(0.22 0.022 250); + --mod-prose-size: clamp(1.1875rem, 1.0625rem + 0.4vw, 1.3125rem); + --mod-prose-line-height: 1.7; + --mod-prose-max-width: 720px; + --mod-prose-tracking: -0.011em; + + /* Inline code */ + --mod-code-color: oklch(0.55 0.22 350); + --mod-code-bg: var(--muted); + --mod-code-border: var(--border); + + /* Code block (dark always) */ + --mod-bg-code: oklch(0.16 0.020 250); + --mod-bg-code-fg: oklch(0.92 0.010 80); + --mod-bg-code-header: oklch(0.13 0.020 250); + + /* Status colors */ + --mod-info: oklch(0.55 0.22 260); + --mod-info-bg: color-mix(in oklch, oklch(0.55 0.22 260) 6%, transparent); + --mod-info-border: color-mix(in oklch, oklch(0.55 0.22 260) 20%, transparent); + --mod-success: oklch(0.65 0.18 145); + --mod-success-bg: color-mix(in oklch, oklch(0.65 0.18 145) 6%, transparent); + --mod-success-border: color-mix(in oklch, oklch(0.65 0.18 145) 20%, transparent); + --mod-warning: oklch(0.72 0.16 70); + --mod-warning-bg: color-mix(in oklch, oklch(0.72 0.16 70) 6%, transparent); + --mod-warning-border: color-mix(in oklch, oklch(0.72 0.16 70) 20%, transparent); + --mod-danger: oklch(0.55 0.22 25); + --mod-danger-bg: color-mix(in oklch, oklch(0.55 0.22 25) 6%, transparent); + --mod-danger-border: color-mix(in oklch, oklch(0.55 0.22 25) 20%, transparent); + + /* Fonts */ + --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-heading: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace; + --mod-font-body: "Newsreader", "Source Serif 4", Georgia, "Times New Roman", serif; + --font-ai-prose: var(--mod-font-body); + + /* Letter-spacing scale */ + --mod-tracking: -0.011em; + --mod-tracking-tight: -0.02em; + --mod-tracking-heading: -0.025em; + --mod-tracking-wide: 0.04em; + + /* Spacing (Modena's 4px grid) */ + --mod-space-1: 4px; + --mod-space-2: 8px; + --mod-space-3: 12px; + --mod-space-4: 16px; + --mod-space-5: 20px; + --mod-space-6: 24px; + --mod-space-8: 32px; + --mod-space-10: 40px; + --mod-space-12: 48px; + --mod-space-16: 64px; + + /* Radius scale */ + --mod-radius-sm: 4px; + --mod-radius: 6px; + --mod-radius-md: 8px; + --mod-radius-lg: 12px; + --mod-radius-xl: 16px; + --mod-radius-pill: 9999px; + + /* Type scale (Crema-shaped) */ + --text-display-size: clamp(2.5rem, 2rem + 2vw, 3.25rem); + --text-display-lh: 1.1; + --text-headline-size: clamp(2rem, 1.7rem + 1.2vw, 2.5rem); + --text-headline-lh: 1.15; + --text-title-size: 1.5rem; + --text-title-lh: 1.3; + --text-body-size: 1rem; + --text-body-lh: 1.6; + --text-label-size: 0.875rem; + --text-label-lh: 1.4; + --text-caption-size: 0.75rem; + --text-caption-lh: 1.4; + + /* Motion */ + --duration-fast: 120ms; + --duration-base: 200ms; + --duration-slow: 300ms; + --ease-standard: cubic-bezier(0.25, 0.1, 0.25, 1); + --ease-decelerate: cubic-bezier(0, 0, 0.25, 1); + + /* Elevation */ + --elevation-0: none; + --elevation-1: 0 1px 2px rgba(0, 0, 0, 0.04); + --elevation-2: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.03); + --elevation-3: 0 4px 8px -2px rgba(0, 0, 0, 0.06), 0 2px 4px -2px rgba(0, 0, 0, 0.03); + --elevation-4: 0 12px 24px -4px rgba(0, 0, 0, 0.08), 0 4px 8px -4px rgba(0, 0, 0, 0.03); + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* ── DARK MODE ──────────────────────────────────────────────────── */ +.dark { + --background: oklch(0.13 0.018 250); + --foreground: oklch(0.95 0.010 250); + --card: oklch(0.17 0.020 250); + --card-foreground: oklch(0.95 0.010 250); + --popover: oklch(0.17 0.020 250); + --popover-foreground: oklch(0.95 0.010 250); + --primary: oklch(0.65 0.22 260); + --primary-foreground: oklch(0.99 0.005 80); + --secondary: oklch(0.22 0.020 250); + --secondary-foreground: oklch(0.92 0.010 250); + --muted: oklch(0.20 0.020 250); + --muted-foreground: oklch(0.65 0.020 250); + --accent: oklch(0.24 0.022 250); + --accent-foreground: oklch(0.92 0.010 250); + --border: oklch(0.27 0.020 250); + --input: oklch(0.27 0.020 250); + --ring: oklch(0.65 0.22 260); + + /* ── Chat surfaces (dark) ── */ + --chat-user-bg: oklch(0.55 0.2 260); + --chat-user-fg: oklch(0.99 0.005 80); + --chat-assistant-bg: oklch(0.20 0.020 250); + --chat-assistant-fg: oklch(0.95 0.010 250); + --chat-assistant-border: oklch(0.27 0.020 250); + + --sidebar: oklch(0.11 0.018 250); + --sidebar-foreground: oklch(0.92 0.010 250); + --sidebar-accent: oklch(0.22 0.022 250); + --sidebar-accent-foreground: oklch(0.92 0.010 250); + --sidebar-border: oklch(0.25 0.020 250); + --sidebar-primary: oklch(0.65 0.22 260); + --sidebar-primary-foreground: oklch(0.99 0.005 80); + + --mod-text: var(--foreground); + --mod-text-secondary: oklch(0.65 0.020 250); + --mod-text-muted: oklch(0.55 0.020 250); + --mod-text-faint: oklch(0.40 0.018 250); + --mod-prose-color: oklch(0.86 0.012 250); + --mod-code-color: oklch(0.78 0.22 350); + --mod-code-bg: oklch(0.22 0.020 250); + --mod-code-border: oklch(0.30 0.020 250); +} + +/* ── BASE TYPOGRAPHY ───────────────────────────────────────────── + * Caffe Florian's theme applies serif font-heading to h1-h4 globally; + * Modena keeps headings in Inter sans. Apply explicitly so it sticks + * regardless of cascade order with other themes' base layers. */ +html { + font-family: var(--font-sans); +} + +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-sans); + font-weight: 700; + letter-spacing: var(--mod-tracking-heading); +} + +/* ── ARTICLE SURFACE ───────────────────────────────────────────── + * Targets @crema/content-ui's data-slot attributes. Anyone using + *
...
+ * automatically picks up the editorial look. Outside [data-slot="article"] + * Prose stays neutral (so comment bodies don't get drop-cap/serif). */ + +[data-slot="article"] { + max-width: var(--mod-prose-max-width); + margin-inline: auto; + padding-inline: var(--mod-space-6); + color: var(--mod-text); +} + +[data-slot="article"] [data-slot="article-header"] { + text-align: center; + padding-top: var(--mod-space-16); + margin-bottom: 3vmin; + border: none; +} +[data-slot="article"] [data-slot="article-header"] h1 { + font-family: var(--font-sans); + font-size: var(--text-display-size); + line-height: var(--text-display-lh); + letter-spacing: var(--mod-tracking-heading); + font-weight: 800; + margin-top: 0.5rem; + color: var(--mod-text); +} +[data-slot="article"] [data-slot="article-header"] p { + font-family: var(--mod-font-body); + font-optical-sizing: auto; + font-size: clamp(1.125rem, 1rem + 0.4vw, 1.375rem); + line-height: 1.5; + color: var(--mod-text-secondary); + margin: 0.75em auto 0; + max-width: 36rem; +} +[data-slot="article"] [data-slot="article-header"] > div:first-child { + font-size: 0.875rem; + font-weight: 600; + letter-spacing: var(--mod-tracking-wide); + text-transform: uppercase; + color: var(--mod-accent); +} +[data-slot="article"] [data-slot="article-meta"] { + justify-content: center; + padding-top: var(--mod-space-6); + margin-top: var(--mod-space-6); + font-size: 0.875rem; + color: var(--mod-text-muted); +} + +[data-slot="article"] [data-slot="prose"] { + font-family: var(--mod-font-body); + font-optical-sizing: auto; + font-size: var(--mod-prose-size); + line-height: var(--mod-prose-line-height); + letter-spacing: var(--mod-prose-tracking); + color: var(--mod-prose-color); +} +[data-slot="article"] [data-slot="prose"] > * + * { + margin-top: 1.5em; +} +[data-slot="article"] [data-slot="prose"] :where(p) { margin: 0; } + +[data-slot="article"] [data-slot="prose"] :where(h1, h2, h3, h4, h5, h6) { + font-family: var(--font-sans); + font-weight: 700; + line-height: 1.2; + letter-spacing: var(--mod-tracking-heading); + color: var(--mod-text); +} +[data-slot="article"] [data-slot="prose"] :where(h2) { + font-size: 1.875rem; + margin-top: 2.4em; + margin-bottom: 0.6em; +} +[data-slot="article"] [data-slot="prose"] :where(h3) { + font-size: 1.5rem; + margin-top: 2em; + margin-bottom: 0.5em; +} +[data-slot="article"] [data-slot="prose"] :where(h4) { + font-size: 1.25rem; + margin-top: 1.8em; + margin-bottom: 0.4em; +} + +[data-slot="article"] [data-slot="prose"] :where(strong) { + font-weight: 700; + color: var(--mod-text); +} + +[data-slot="article"] [data-slot="prose"] :where(ul, ol) { + padding-left: 1.5em; + margin: 0; +} +[data-slot="article"] [data-slot="prose"] :where(li + li) { + margin-top: 0.4em; +} +[data-slot="article"] [data-slot="prose"] :where(li::marker) { + color: var(--mod-text-faint); +} + +[data-slot="article"] [data-slot="prose"] :where(blockquote) { + position: relative; + font-size: 1.25em; + font-weight: 500; + line-height: 1.5; + color: var(--mod-text); + padding: 0 0 0 1.5em; + margin: 2em 0; + border: none; + font-style: normal; +} +[data-slot="article"] [data-slot="prose"] :where(blockquote)::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: var(--mod-accent); + border-radius: 2px; +} +[data-slot="article"] [data-slot="prose"] :where(blockquote p) { margin: 0; } +[data-slot="article"] [data-slot="prose"] :where(blockquote p + p) { margin-top: 0.6em; } + +[data-slot="article"] [data-slot="prose"] :where([data-slot="pullquote"]) { + font-family: var(--mod-font-body); + font-size: 1.625em; + font-style: italic; + font-weight: 400; + line-height: 1.5; + text-align: center; + padding: 0; + color: var(--mod-text); + margin: 2.5em 0; +} +[data-slot="article"] [data-slot="prose"] :where([data-slot="pullquote"])::before { + display: none; +} + +[data-slot="article"] [data-slot="prose"] :where(hr) { + border: none; + border-top: 1px solid var(--border); + opacity: 0.6; + margin: 3em 0; + width: auto; + height: auto; + background: transparent; +} + +[data-slot="article"] [data-slot="prose"] :where(:not(pre) > code) { + font-family: var(--font-mono); + font-size: 0.85em; + color: var(--mod-code-color); + background: var(--mod-code-bg); + border: 1px solid var(--mod-code-border); + border-radius: var(--mod-radius-sm); + padding: 0.1em 0.35em; +} +[data-slot="article"] [data-slot="prose"] :where(pre) { + font-family: var(--font-mono); + font-size: 0.875rem; + line-height: 1.65; + background: var(--mod-bg-code); + color: var(--mod-bg-code-fg); + border-radius: var(--mod-radius-md); + padding: var(--mod-space-5); + overflow-x: auto; + margin: 2em 0; +} +[data-slot="article"] [data-slot="prose"] :where(pre code) { + background: transparent; + color: inherit; + border: none; + padding: 0; + border-radius: 0; + font-size: inherit; +} + +[data-slot="article"] [data-slot="prose"] :where(a) { + color: var(--mod-accent); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 2px; + transition: color var(--duration-fast) var(--ease-standard); +} +[data-slot="article"] [data-slot="prose"] :where(a:hover) { + color: color-mix(in oklch, var(--mod-accent) 75%, var(--foreground)); +}