diff --git a/.changeset/precompiled-utilities-and-token-export.md b/.changeset/precompiled-utilities-and-token-export.md new file mode 100644 index 0000000..080c74b --- /dev/null +++ b/.changeset/precompiled-utilities-and-token-export.md @@ -0,0 +1,26 @@ +--- +'@e412/rnui-themes': minor +'@e412/rnui-react': patch +--- + +Add a precompiled utility layer and a declared token export. + +**Why:** rnui is normally consumed with Tailwind running in the consumer's build, so any class you write compiles on demand. But it is also consumed *precompiled* — as a single stylesheet embedded in a design tool or preview. In that mode a class Tailwind never emitted silently does nothing: no error, no warning, the element just renders unstyled. Ordinary layout markup (`gap-10`, `grid-cols-4`, `max-w-4xl`, `text-3xl`) hit this constantly. + +**`@e412/rnui-themes/utilities`** — a precompiled stylesheet (~353 KB, ~40 KB gzip) covering complete scales rather than only the values rnui's own components happen to use: + +- spacing `0`–`24` across `p m gap space-x space-y w h min-w min-h` and every axis/side variant +- `text-xs`–`text-9xl`, `font-thin`–`font-black`, `tracking-*`, `leading-*` +- `grid-cols-1..12`, `col-span-*`, `row-span-*` +- `max-w-xs`–`max-w-7xl`, `rounded-*`, `border-*`, `opacity-0..100` +- `sm: md: lg: xl:` responsive and `hover: focus: active: disabled: dark:` states on the interactive subset + +Import it only if you cannot run Tailwind yourself; if you can, keep importing `@e412/rnui-themes` and let your build compile what you use. + +**`@e412/rnui-themes/safelist`** — the safelist source, for adding these scales to your own Tailwind build. + +**`dist/utilities.json`** — the classes that actually shipped, derived from the compiled output rather than the safelist source, so tooling can tell a consumer "`gap-9` is not in this bundle" instead of letting it fail silently. + +**`dist/tokens.json`** — the declared public token surface: 399 tokens across every theme, each with its name, theme, `kind` (`color` | `dimension` | `font` | `radius`), value, and light/dark pair. Built from the hand-authored theme sources, never from compiled CSS, so Tailwind's internal custom properties are structurally excluded. This is now the source of truth for token extraction — do not scan the generated stylesheets. Tokens a theme's dark ramp omits are flagged `darkInherited` rather than silently presented as declared. + +**`@e412/rnui-react`:** renamed the private custom properties `--cell-size` / `--cell-radius` (Calendar) and `--data-grid-fill-size` (DataGrid) to a `--rnui-_` prefix, so they are unambiguously internal and not mistaken for design tokens. These were never documented as customization points. `--sidebar-width` and `--sidebar-width-icon` are deliberately unchanged: they are set via a `style` prop on `SidebarProvider`, which is a supported override. diff --git a/.changeset/three-deliberate-themes.md b/.changeset/three-deliberate-themes.md new file mode 100644 index 0000000..5121760 --- /dev/null +++ b/.changeset/three-deliberate-themes.md @@ -0,0 +1,50 @@ +--- +'@e412/rnui-themes': major +--- + +**Breaking:** replaces the eight theme presets with three. + +`amber`, `crimson`, `forest`, `ocean`, `oxide`, `rose`, `slate` and `violet` are removed, along with their subpath exports. They were one neutral shell with the accent hue swapped — over-saturated, muddy in dark mode, and named for color rather than for use. Eight mediocre options are worse than three deliberate ones. + +| New slug | Character | Body / Headings | Radius | +| --- | --- | --- | --- | +| `press` | Editorial — warm paper, near-black ink, high contrast | Source Sans 3 / Source Serif 4 | `0.5rem` | +| `console` | Technical — dense, low-chroma, terminal-adjacent | Geist / Geist Mono | `0.125rem` | +| `signal` | Brand-forward — warm neutral shell, one confident accent | Instrument Sans / Space Grotesk | `0.75rem` | + +### Migration + +Every `[data-theme="…"]` value changes. Map by the intent behind the old choice, not by hue: + +| Was | Use | Why | +| --- | --- | --- | +| `slate`, `oxide` | `press` | Editorial/professional, serif-or-substantial headings | +| `ocean`, `violet` | `console` | Cool, technical, restrained | +| `rose`, `amber`, `crimson` | `signal` | Warm shell carried by one confident accent | +| `forest` | `press` or `signal` | Depending on whether the serif pairing mattered | + +```diff +- ++ +``` + +```diff +- @import '@e412/rnui-themes/ocean'; ++ @import '@e412/rnui-themes/console'; +``` + +If you were extending a preset, re-point the selector — the token names are unchanged, so overrides carry over as-is: + +```diff +- [data-theme='ocean'] { --primary: oklch(0.48 0.15 220); } ++ [data-theme='console'] { --primary: oklch(0.48 0.15 220); } +``` + +### What else changed + +- **Every theme now defines `--font-heading` and `--font-mono`.** Previously only two of the eight set a heading face; the rest silently fell back to the system stack, which made them look unfinished next to the ones that didn't. +- **Complete dark ramps.** Dark surfaces step monotonically by at least 4% lightness, so elevation reads as elevation instead of noise, and dark chroma never exceeds the light value for the same token — raising chroma to compensate for darkness is what produced the muddiness. +- **Accessibility is a floor, not a preset.** Every `*`/`*-foreground` pair clears WCAG AA in both modes (4.5:1 text, 3:1 borders and rings), verified in the build by `scripts/check-contrast.mjs`, which fails on a regression. There is no separate "high contrast" theme: `press` is the high-contrast option by character, and the floor applies to all three equally. +- **The default theme (no `data-theme`) also now meets that floor.** Its look is otherwise unchanged, but 17 tokens moved to clear AA — `text-warning` was 1.89:1 and `text-success` 2.45:1 against the page background. +- **One accent per theme.** `--primary`, `--focus`, `--ring` and `--sidebar-primary` derive from a single hue; `--accent` is a tint of it rather than a second color. Status colors (`--success`, `--info`, `--warning`, `--destructive`) keep the same hues across all three so they stay recognizable. +- Theme sources are generated from an anchor spec by `scripts/generate-themes.mjs`; edit the anchors there rather than the emitted CSS. diff --git a/.design-sync/NOTES.md b/.design-sync/NOTES.md new file mode 100644 index 0000000..7233637 --- /dev/null +++ b/.design-sync/NOTES.md @@ -0,0 +1,249 @@ +# design-sync notes + +## The repo + +- User: this is basically a shadcn-style system — Base UI primitives + shadcn/ui patterns + Tailwind CSS v4. Styling idiom is Tailwind utility classes over CSS custom properties from `@e412/rnui-themes` (OKLch, `data-theme`/`.dark` switchable). +- pnpm monorepo. `packages/react` (`@e412/rnui-react`, the DS), `packages/themes` (`@e412/rnui-themes`, CSS-only theme presets), Storybook at `apps/storybook`, docs at `apps/docs` (fumadocs MDX). +- Build both packages with root `pnpm run build` (themes first, then react — the root script already orders them). + +## Config decisions ([GENERAL]) + +- `cfg` paths resolve relative to **PKG_DIR** (`packages/react`), not the repo root. That's why `docsDir` is `../../apps/docs/content/docs` and `entry` is `dist/index.mjs`. A repo-root-relative path silently prints `not found — skipped`. +- `--node-modules packages/react/node_modules` — it contains react/react-dom, so no need for the repo-root one. +- **No CSS ships in `packages/react/dist`** (Tailwind v4 compiles at app level, shadcn-style). `[CSS_FROM_STORYBOOK]` scrapes the compiled stylesheet out of `.design-sync/sb-reference` instead. This is the intended path here — do NOT set `cfg.cssEntry`. +- `titleMap` covers 4 storybook titles that aren't export names: `Resizable`→`ResizablePanelGroup`, `Sonner`→`Toaster`, `Advanced`(Charts/Advanced)→`EChart`, and `useFileUpload`→`null` (a hook, deliberately excluded). +- `docsMap` pins the 3 components whose doc filename doesn't slug-match: `ResizablePanelGroup`→resizable.mdx, `Sortable`→sortable-list.mdx, `Toaster`→sonner.mdx. Docs are 72/72 with these. +- `overrides.cardMode: "column"` on the wide components (charts, Card, Tabs, DateSelector, AspectRatio, CopyButton, ScrollArea, Stepper, EChart) and `"single"` on `Progress` (its stories flag GRID_OVERFLOW `escape`, which column can't fix). +- `overrides.Separator.skip: ["components-separator--horizontal"]` — that story is args-only (`` with no sized container), so a bare 1px rule collapses to nothing in storybook's centered canvas. It is `sb-error` in **storybook itself**, not a preview defect; `Default` and `Vertical` both match. +- `readmeHeader` resolves from the **config home** = the dir `.design-sync/` hangs off (the repo root), NOT PKG_DIR. So the value is `.design-sync/conventions.md`. (Different base from `docsDir`/`entry` — easy to get wrong; the build says `not found at the config home` when you do.) + +## Known-benign warnings (triaged — do not re-chase) + +- `! preview decorator bundle failed: Could not resolve "tailwindcss"` — `.storybook/preview.tsx` imports `../src/styles.css`, which `@import`s tailwindcss; esbuild can't resolve that. **Harmless here**: the decorators only toggle `.light`/`.dark` on `` and set body colors. Light-theme tokens live on `:root` in `@e412/rnui-themes`, so unwrapped previews render exactly like the default (light) storybook. No React context is lost. `cfg.provider` is deliberately NOT set. +- `[TOKENS_MISSING]` for `--accordion-panel-height`, `--tw`, `--shiki-light`, `--shiki-dark`, `--shiki-light-bg`, `--shiki-dark-bg` — all injected at runtime (Base UI accordion measurement, a Tailwind internal, shiki's highlighter). Verified visually: CodeBlock's syntax highlighting renders correctly in previews. + +## [GENERAL] Harness bug: `noscript` first-child causes a false `sb-error` + +`compare.mjs`'s content wait is `SB_CONTENT = ':is(#storybook-root,#root) > :not(style,script,link,meta,template)'`. +Its own comment says the exclusion list exists so `waitForSelector` doesn't lock +onto a non-content first child — but `noscript` was omitted. `input-otp` renders +`createElement("noscript", …)` (its NoScriptCSSFallback) as the **first** child, +so the wait locked onto a 0×0 node, timed out after 8s, and both InputOTP stories +were falsely reported `sb-error: no storybook root content`. + +Probe evidence: `#storybook-root` children are `[noscript 0×0, div 208×32]`; the +old selector's first match is `noscript`, the fixed one's is `div`. + +**Fix applied:** added `noscript` to that exclusion list in +`.ds-sync/storybook/compare.mjs`. After it, both InputOTP stories capture and match. + +⚠️ **`.ds-sync/` is gitignored and re-copied from the skill on every sync, so this +patch does NOT survive.** A future sync must re-apply it (or the skill upstream +must fix it) or InputOTP will falsely `sb-error` again. Do **not** "fix" it with +`cfg.overrides.InputOTP.skip` — the story is healthy. This can hit any story whose +root's first child is a `noscript`/hidden element. + +## [GENERAL] echarts subpath imports must ride the bundle's own copy + +`charts/advanced.stories.tsx` does `import { GaugeChart } from 'echarts/charts'` + +`echarts.use([GaugeChart])` at module scope. By default an unknown package subpath +is **bundled fresh** into `_preview/EChart.js`, so `echarts.use()` registered a +*second copy's* GaugeChart — a class extending a different echarts base — over the +working registration from `packages/react/src/components/charts/core.ts`. The gauge +then painted a blank canvas (with a `gauge exists.` double-registration warning) +while every other chart type was unaffected. + +**Fix:** `"extraEntries": ["echarts/charts"]`. That bundles the subpath once into +`_ds_bundle.js` (esbuild dedupes it against the DS's own echarts, so it's the SAME +class) and, because the specifier is now an entry name, the story's import shims to +`window.RnuiReact` instead of bundling a copy. Verified: the CPU gauge renders +identically to storybook. + +Note this defect was **preview-only** — the shipped `_ds_bundle.js` always had +GaugeChart registered correctly, so designs were never affected. + +⚠️ `extraEntries` is part of the **global** grade slice (`configSlicesFor` in +`lib/sync-hashes.mjs`), so changing it clears **every** component's grade. If you +ever need to touch `extraEntries`/`storyImports`/`provider`, do it as early in a +campaign as possible. `overrides..skip` is per-component; `cardMode`/ +`primaryStory` are excluded from keying entirely. + +## Precompiled utility layer (`@e412/rnui-themes/utilities`) + +rnui is normally consumed with Tailwind in the consumer's build, so any class +they write compiles on demand. The design-sync bundle is the opposite: it's +**precompiled**, so a class Tailwind never emitted silently does nothing. + +`packages/themes/src/safelist.css` safelists complete scales (spacing 0–24 on +every box-model utility, `text-xs`–`9xl`, `grid-cols-1..12`, `col-span-*`, +`max-w-xs..7xl`, radius/border/opacity, `sm: md: lg: xl:` responsive and +`hover: focus: active: disabled: dark:` states on the interactive subset). +`utilities.css` compiles it to `dist/utilities.css` (~353 KB, 40 KB gzip) and +`scripts/emit-utilities.mjs` reports what actually shipped to +`dist/utilities.json` (4964 classes / 592 families). + +**`apps/storybook/src/styles.css` imports the safelist**, which is what puts +those classes in the synced `_ds_bundle.css` — the sync scrapes Storybook's +compiled CSS (`[CSS_FROM_STORYBOOK]`), so the safelist has to be in *that* +build, not only in the themes package. + +`utilities.json` is derived from the **compiled output**, not the safelist +source: the safelist says what was asked for, `utilities.json` says what +Tailwind emitted. Downstream tooling should read the latter. + +**Why the safelist lives in `packages/themes` and not `packages/react`:** +`packages/react` already depends on `@e412/rnui-themes`, so a themes → react +dependency would be a workspace cycle. Themes therefore safelists only what it +can compile alone (Tailwind + its own tokens); the component classes still come +from the Storybook scrape, which scans the react dist. + +### Accepted warning: `[FONT_MISSING] "Cambria"` + +Introduced by safelisting `font-serif`, which pulls in Tailwind's default serif +stack: `ui-serif, Georgia, Cambria, "Times New Roman", Times, serif`. Cambria is +a proprietary Microsoft system font — not redistributable, and not a brand font +here. The stack degrades to Georgia (near-universal) and then Times, so this is +a deliberate accept, not an unresolved brand-font gap. + +Distinguish it from a real `[FONT_MISSING]`: all 10 preset brand faces still +ship (20 woff2 in `fonts/`, `@import`ed from `styles.css`). If a *preset* family +ever appears in this warning, that IS a regression — chase it. + +## Declared token export (`dist/tokens.json`) + +Built by `scripts/emit-tokens.mjs` from the **hand-authored theme sources**, +never from compiled CSS — so Tailwind's internal custom properties cannot appear +in it by construction. 399 tokens across 9 themes, each with name, theme, kind +(`color` | `dimension` | `font` | `radius`), value, and its light/dark pair. + +Tokens a preset's dark ramp omits are flagged `darkInherited: true` (21 today) +rather than silently presented as declared — that's the audit trail for the +incomplete dark ramps. + +**This replaces CSS scanning as the token source of truth.** The reason the +bundle kept getting hand-patched downstream was that the token surface was only +discoverable by scanning generated CSS; `tokens.json` removes that reason. + +### Not done as specified, and why + +- **`--sidebar-width` / `--sidebar-width-icon` were NOT prefixed.** They're set + via a `style` prop on `SidebarProvider`, which is the standard shadcn + consumer-override idiom (``). + Renaming them would silently break that for anyone already using it. They're + public API in practice, not private plumbing. +- **`--accordion-panel-height` was NOT prefixed.** rnui only *reads* it; Base UI + sets it at runtime. It isn't ours to rename. +- Prefixed the genuinely private ones: `--cell-size` / `--cell-radius` + (calendar-scoped) and `--data-grid-fill-size` → `--rnui-_*`. +- Tailwind still emits its own internals into `:root`/`:host` in the compiled + CSS; per the request this is left alone, and `tokens.json` is what makes it + not matter. + +## Brand fonts are self-hosted for the sync + +The theme presets name 10 web fonts but `@e412/rnui-themes` deliberately does not +load them (the repo's own theming docs say to load them yourself). That meant +Claude Design flagged *"Missing brand fonts … rendering with substitutes"* and +every preset rendered with a fallback stack. + +Fixed by self-hosting them: `.design-sync/fetch-fonts.mjs` downloads the +variable woff2 from Google Fonts (all 10 families are SIL OFL 1.1, so +redistribution is fine) into `.design-sync/fonts/`, and +`cfg.extraFonts: ["../../.design-sync/fonts/fonts.css"]` makes the build copy +them to `ds-bundle/fonts/` and `@import` the sheet from `styles.css` — the +closure designs actually receive. 20 faces (latin + latin-ext), 0.55 MB. + +Verified in chromium: 20 faces registered, 0 failed requests, all 10 families +report `document.fonts.check()` true, and each preset resolves to its real body +face (oxide→DM Sans, ocean→Plus Jakarta Sans, violet→Outfit, forest→Source Sans +3, rose→Nunito, amber→Sora, slate→Geist, crimson→Instrument Sans). + +Notes for the next run: +- **`extraFonts` is NOT in the grade key** (styling trust class, see + `configSlicesFor`), so adding fonts did not clear any grades. +- **Previews are unaffected** — they render the default theme, whose + `--font-sans` is a system stack, so the compare oracle never sees these fonts. + That is also why `[FONT_MISSING]` never fired: nothing in the *default* theme + needs a webfont. +- Re-run `node .design-sync/fetch-fonts.mjs` to refresh; it rewrites both the + woff2 files and `fonts.css`. +- Only `oxide` and `forest` define `--font-heading`; the other six presets + inherit the `:root` system stack for headings, so their headings are NOT + branded. That is the themes package's own behavior — if it's unintended, the + fix belongs in `packages/themes`, not here. + +## FIXED: percentage-width story roots were invisible in Storybook + +`apps/storybook/.storybook/preview.tsx` sets a **global `layout: 'centered'`**. A centered +storybook root is shrink-to-fit, so a story whose ROOT element has a percentage width +(`className="w-[60%]"`) resolves that width against a 0px parent — the element mounts with +the full Base UI DOM but measures **0px wide** and renders nothing visible. Base UI's +indicator even computes `--start-position: NaN%` / `Infinity%`. + +Exactly 5 stories in 2 files hit this (`grep -rn 'w-\[[0-9]*%\]' apps/storybook/src/stories`): + +- `slider.stories.tsx` → `Default`, `Range` +- `progress.stories.tsx` → `Default`, `Empty`, `Full` + +It was a defect in the repo, not in the sync — the components always rendered fine; only the +storybook oracle was blind to them. + +**Fixed at the source**: both metas now carry `parameters: { layout: 'padded' }` (the same thing +`date-selector.stories.tsx` already did, which is why its `w-full max-w-xl` stories always +rendered). A padded layout gives the story root the full canvas width, so the percentage +resolves normally. + +Consequently the following workarounds were **removed** and must not come back: + +- `cfg.overrides.Slider.skip` / `cfg.overrides.Progress.skip` — both stories sets now capture + and grade normally. +- `.design-sync/previews/Slider.tsx` — the owned stopgap that restored Slider's card while its + stories were unphotographable. Deleted; the generated preview is authoritative again. +- `Progress` went back from `cardMode: "single"` to `"column"` — with all 4 stories capturable + there is no reason to show only one. + +If `[GRID_OVERFLOW]`/`sb-error` ever reappears on these two, check first whether someone +removed the `layout: 'padded'` parameter. + +## Flaky `✗ [RENDER] … root empty` on chart components + +Seen once on `AreaChart` during a driver run, clean on an immediate re-run. The +`.render-check.json` record is the tell: `rootEmpty: true` but `errs: 0`, +`blank: false`, `pngBytes: 75037` — a 75 KB screenshot is not an empty root. The +DOM probe ran before echarts finished mounting while the screenshot was taken +after. + +**Re-run before investigating.** Only treat it as real if it reproduces, or if +`pngBytes` is genuinely tiny (<5 KB) — that's the `[RENDER_BLANK]` signature. + +## `[REFERENCE_STALE?]` after a converter-level config change is a false positive + +That warn fires when the bundle hash moves but `.design-sync/sb-reference` doesn't. +Adding `extraEntries` changed the bundle **without any DS source change**, so it +fired spuriously. Verified: `packages/react/dist/index.mjs` predates +`sb-reference/iframe.html`, and `git status packages/react/src packages/themes +apps/storybook/src` was clean. Before rebuilding the reference (minutes), check +those two things — if the DS source really did move, rebuild both together. + +## [GENERAL] Framing: `max-w-*` stories look wider in previews + +Storybook's `layout: 'centered'` shrink-wraps an element to its intrinsic width +(a bare `` ≈156px) while the preview page renders it at its full +`max-w-*` (e.g. `max-w-sm` = 384px). This affects most form components and is +**container width, not a component defect** — grade it as framing per the §4 +rubric. Do not author an owned preview to "fix" it; that would permanently +shadow the generated one. + +## [GENERAL] Grading gotcha: remote images need a warm CDN + +Avatar stories load `https://github.com/shadcn.png`. On a cold connection that URL took ~15s here (redirect to avatars.githubusercontent.com); the preview panel hit its `networkidle` timeout and captured the **fallback initials** while the storybook panel got the real image — an asymmetric false mismatch that `[ASSETS_BLOCKED]` does NOT catch (that warn only fires when BOTH panels fail). + +**Before grading any image-bearing story a mismatch**, warm the URL (`curl -sL -o /dev/null ` a few times) and recapture with `--force --components `. After warming, Avatar matched on all 5 stories. + +## Re-sync risks + +- **`.design-sync/sb-reference` and the package `dist/` must be rebuilt together.** Any change to `packages/react/src` or the stories requires both, or every grade compares against a stale reference (`[REFERENCE_STALE?]`). +- **Story caps.** Button was captured at `--max-stories 14` (full); BarChart (7) and CodeBlock (10) were graded on their first 6 stories only, as were other components with >6 stories. Their tail stories are trusted, never individually image-judged. If a tail story carries a distinct variant, raise `--max-stories` for it. +- **Component grouping is flat.** The picker gets 2 groups (`components`, `charts`) from the storybook titles. `apps/docs/content/docs/*/meta.json` has a much richer grouping (Layout / Forms / Data Display / Feedback / Navigation / Utilities), but the converter only reads a `category:` **frontmatter** key, and doc categories are only applied when the component's group is generic — storybook already supplies a named group, so it wins. Wiring the meta.json grouping would need forks of both `docs.mjs` and `source-storybook.mjs`; judged not worth the permanent fork surface. Revisit if the converter ever gains a `groupMap` config key. +- **`navigation-menu.mdx` is truncated** at the 8000-char doc cap (8077 → 8049). Harmless today; if that doc grows, the tail is what's lost from `NavigationMenu.prompt.md`. +- Dialog/overlay stories render only their trigger button on both panels (they need a click to open). That's faithful to the repo's own storybook, but the product card for those components shows only buttons. diff --git a/.design-sync/config.json b/.design-sync/config.json new file mode 100644 index 0000000..f0dec37 --- /dev/null +++ b/.design-sync/config.json @@ -0,0 +1,42 @@ +{ + "projectId": "06c8634f-1fde-4d6f-9598-7445be82a439", + "shape": "storybook", + "pkg": "@e412/rnui-react", + "globalName": "RnuiReact", + "entry": "dist/index.mjs", + "storybookConfigDir": "apps/storybook/.storybook", + "storybookStatic": ".design-sync/sb-reference", + "buildCmd": "pnpm run build", + "readmeHeader": ".design-sync/conventions.md", + "docsDir": "../../apps/docs/content/docs", + "extraFonts": ["../../.design-sync/fonts/fonts.css"], + "titleMap": { + "Resizable": "ResizablePanelGroup", + "Sonner": "Toaster", + "Advanced": "EChart", + "useFileUpload": null + }, + "extraEntries": ["echarts/charts"], + "docsMap": { + "ResizablePanelGroup": "../../apps/docs/content/docs/components/resizable.mdx", + "Sortable": "../../apps/docs/content/docs/utilities/sortable-list.mdx", + "Toaster": "../../apps/docs/content/docs/components/sonner.mdx" + }, + "overrides": { + "AreaChart": { "cardMode": "column" }, + "BarChart": { "cardMode": "column" }, + "LineChart": { "cardMode": "column" }, + "PieChart": { "cardMode": "column" }, + "AspectRatio": { "cardMode": "column" }, + "Card": { "cardMode": "column" }, + "CopyButton": { "cardMode": "column" }, + "DateSelector": { "cardMode": "column" }, + "Progress": { "cardMode": "single", "primaryStory": "WithLabelAndValue" }, + "Separator": { "skip": ["components-separator--horizontal"] }, + "Slider": { "cardMode": "column" }, + "EChart": { "cardMode": "column" }, + "ScrollArea": { "cardMode": "column" }, + "Stepper": { "cardMode": "column" }, + "Tabs": { "cardMode": "column" } + } +} diff --git a/.design-sync/conventions.md b/.design-sync/conventions.md new file mode 100644 index 0000000..17d872a --- /dev/null +++ b/.design-sync/conventions.md @@ -0,0 +1,100 @@ +# Building with rnui + +rnui is a shadcn/ui-style system: Base UI primitives, styled with Tailwind CSS v4 +utilities that resolve to OKLch CSS custom properties from `@e412/rnui-themes`. + +## Setup — no theme provider + +Theming is **pure CSS**. There is no `ThemeProvider` and nothing to wrap for +colors, fonts, or radius. Set attributes on a root element instead: + +```jsx +const { Button, Card, CardHeader, CardTitle, CardContent } = window.RnuiReact; + +// data-theme picks a preset; add `dark` for dark mode. Both are optional — +// omitting them gives the default warm-neutral light theme. +
+ + Monthly revenue + + + + + +
+``` + +Presets: `press`, `console`, `signal`. Each changes colors, radius, and +typography. **The brand fonts ship with this design system** (`fonts/`, reached +via `styles.css`) — set `data-theme` and the real typeface renders, nothing to +load. Every theme defines both a body and a heading face: + +| Theme | Character | Body (`--font-sans`) | Headings (`--font-heading`) | Radius | +|---|---|---|---|---| +| `press` | Editorial — warm paper, high contrast | Source Sans 3 | Source Serif 4 | `0.5rem` | +| `console` | Technical — dense, cool, terminal-adjacent | Geist | Geist Mono | `0.125rem` | +| `signal` | Brand-forward — one confident accent | Instrument Sans | Space Grotesk | `0.75rem` | + +All three also set `--font-mono` (Geist Mono). The default (no `data-theme`) is a +warm-neutral system stack. + +Every theme meets WCAG AA in both light and dark — 4.5:1 for text, 3:1 for +borders and rings — so any documented token pairing is safe to use as-is. + +Three components need a wrapper, and only these three: + +- `Tooltip*` — wrap the subtree in `TooltipProvider`, or tooltips never open. +- `Sidebar*` — wrap in `SidebarProvider` (`SidebarTrigger`/`useSidebar` need it). +- Toasts — render `` once near the root, then call `toast(...)`. + +## The styling idiom — Tailwind utilities, precompiled + +Tailwind v4 compiles at **build time** and there is no compiler here, so only +utilities already present in `_ds_bundle.css` resolve. The stylesheet ships +complete scales, so ordinary layout markup works: + +| Concern | Available | +|---|---| +| Spacing | `p m gap space-x space-y` and every axis/side variant (`px- py- pt- pr- pb- pl- mx- my- …`) at `0 0.5 1 1.5 2 2.5 3 3.5 4 5 6 7 8 9 10 11 12 14 16 20 24` | +| Sizing | `w h min-w min-h max-h` on the same scale, plus `auto full screen fit min max`, fractions (`w-1/2` `w-1/3` `w-2/3` `w-3/4` …), and `max-w-xs`…`max-w-7xl` | +| Type | `text-xs`…`text-9xl`, `font-thin`…`font-black`, `tracking-tighter`…`tracking-widest`, `leading-none`…`leading-loose`, `text-left/center/right`, `truncate` | +| Layout | `flex grid block inline-flex inline-block hidden`, `flex-row/col/wrap`, `items-*`, `justify-*`, `grid-cols-1..12`, `grid-rows-1..6`, `col-span-1..12`, `row-span-1..6` | +| Position | `relative absolute fixed sticky`, `inset/top/right/bottom/left`, `z-0..50`, `overflow-*` | +| Borders | `rounded-none`…`rounded-4xl` + `rounded-full` (and per-corner/side), `border-0/2/4/8`, `shadow-xs`…`shadow-2xl` | +| Color | `bg- text- border-` + every design token (below), with `/5`…`/90` alpha on surfaces | +| Variants | `sm: md: lg: xl:` responsive, and `hover: focus: focus-visible: active: disabled: dark:` on colors, opacity and rings | + +Design tokens usable as colors: `background foreground card popover primary +secondary muted accent destructive success info warning invert focus border +input ring sidebar` (each with its `-foreground` pair where one exists), plus +`chart-1`…`chart-5` and the `sidebar-*` family. + +Never invent a color pairing: `bg-primary` goes with `text-primary-foreground`, +and the same holds for `card`, `popover`, `muted`, `accent`, `destructive`, +`success`, `info`, `warning`, `sidebar`. + +**If a class isn't in the list above, don't guess** — an unemitted class fails +silently. Use an inline style against a token instead; tokens always resolve: + +```jsx +
+``` + +The authoritative list of what shipped is `utilities.json` in the +`@e412/rnui-themes` package (~4,900 classes); the tables above summarize it. + +## Where the truth lives + +- `styles.css` — the single entry; its `@import` closure is `tokens/*.css` + (every theme preset) plus `_ds_bundle.css` (all component styles). Read + `tokens/light.css` for the full custom-property list. +- `components///.prompt.md` — real usage examples and variant + tables for that component. Read it before composing anything non-obvious. +- `components///.d.ts` — the exact prop contract. + +Charts are echarts-based: `BarChart`, `LineChart`, `AreaChart`, `PieChart`, +`RadarChart`, `ScatterChart` for the common cases, and `EChart` with `echarts` + +`chartConfig` when you need a custom option object. They read `--chart-1`… +`--chart-5` from the active theme, so they restyle with `data-theme` for free. + +`cn(...)` is exported for conditional class merging. diff --git a/.design-sync/fetch-fonts.mjs b/.design-sync/fetch-fonts.mjs new file mode 100644 index 0000000..d4022a6 --- /dev/null +++ b/.design-sync/fetch-fonts.mjs @@ -0,0 +1,60 @@ +// Fetch the theme presets' brand fonts from Google Fonts as local woff2 + +// a rewritten @font-face sheet, so designs render real brand typography. +import { mkdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const OUT = '/home/aloks98/projects/rnui/.design-sync/fonts'; +mkdirSync(OUT, { recursive: true }); + +// families -> weight range actually reachable via Tailwind utilities in this DS +// (normal 400, medium 500, semibold 600, bold 700) +// Exactly the families the three themes reference — press (Source Sans 3 / +// Source Serif 4), console (Geist / Geist Mono), signal (Instrument Sans / +// Space Grotesk), with Geist Mono serving --font-mono everywhere. Families no +// surviving theme uses were dropped so consumers stop downloading them. +const FAMILIES = [ + 'Geist', 'Geist Mono', 'Instrument Sans', + 'Source Sans 3', 'Source Serif 4', 'Space Grotesk', +]; +// Keep the byte budget sane: latin covers the DS's own copy; latin-ext is a +// cheap safety net for accented content in designs. +const KEEP_SUBSETS = new Set(['latin', 'latin-ext']); +const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; + +const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); +const out = []; +let files = 0, bytes = 0; + +for (const fam of FAMILIES) { + const url = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fam)}:wght@400..700&display=swap`; + const res = await fetch(url, { headers: { 'User-Agent': UA } }); + if (!res.ok) { console.error(`! ${fam}: css ${res.status}`); continue; } + let css = await res.text(); + + // Google emits one @font-face per subset, each preceded by a /* subset */ comment. + const blocks = [...css.matchAll(/\/\*\s*([\w-\[\]]+)\s*\*\/\s*(@font-face\s*\{[^}]*\})/g)]; + let kept = 0; + for (const [, subset, block] of blocks) { + if (!KEEP_SUBSETS.has(subset)) continue; + const m = /url\((https:\/\/fonts\.gstatic\.com\/[^)]+\.woff2)\)/.exec(block); + if (!m) continue; + const fontRes = await fetch(m[1], { headers: { 'User-Agent': UA } }); + if (!fontRes.ok) { console.error(`! ${fam}/${subset}: font ${fontRes.status}`); continue; } + const buf = Buffer.from(await fontRes.arrayBuffer()); + const name = `${slug(fam)}-${subset}.woff2`; + writeFileSync(join(OUT, name), buf); + files++; bytes += buf.length; kept++; + out.push(block.replace(m[0], `url(./${name})`).trim()); + } + console.error(` ${fam}: ${kept} face(s)`); +} + +writeFileSync( + join(OUT, 'fonts.css'), + `/* Brand fonts for the @e412/rnui-themes presets, self-hosted so designs\n` + + ` render real typography instead of a fallback stack.\n` + + ` Generated from Google Fonts (all families SIL Open Font License 1.1).\n` + + ` Regenerate: node .design-sync/fetch-fonts.mjs */\n\n` + + out.join('\n\n') + '\n', +); +console.error(`\n${files} font file(s), ${(bytes / 1024 / 1024).toFixed(2)} MB total`); diff --git a/.design-sync/fonts/fonts.css b/.design-sync/fonts/fonts.css new file mode 100644 index 0000000..9784191 --- /dev/null +++ b/.design-sync/fonts/fonts.css @@ -0,0 +1,114 @@ +/* Brand fonts for the @e412/rnui-themes presets, self-hosted so designs + render real typography instead of a fallback stack. + Generated from Google Fonts (all families SIL Open Font License 1.1). + Regenerate: node .design-sync/fetch-fonts.mjs */ + +@font-face { + font-family: 'Geist'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(./geist-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Geist'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(./geist-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Geist Mono'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(./geist-mono-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Geist Mono'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(./geist-mono-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Instrument Sans'; + font-style: normal; + font-weight: 400 700; + font-stretch: 100%; + font-display: swap; + src: url(./instrument-sans-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Instrument Sans'; + font-style: normal; + font-weight: 400 700; + font-stretch: 100%; + font-display: swap; + src: url(./instrument-sans-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Source Sans 3'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(./source-sans-3-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Source Sans 3'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(./source-sans-3-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Source Serif 4'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(./source-serif-4-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Source Serif 4'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(./source-serif-4-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(./space-grotesk-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Space Grotesk'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(./space-grotesk-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/.design-sync/fonts/geist-latin-ext.woff2 b/.design-sync/fonts/geist-latin-ext.woff2 new file mode 100644 index 0000000..ba90e20 Binary files /dev/null and b/.design-sync/fonts/geist-latin-ext.woff2 differ diff --git a/.design-sync/fonts/geist-latin.woff2 b/.design-sync/fonts/geist-latin.woff2 new file mode 100644 index 0000000..991445d Binary files /dev/null and b/.design-sync/fonts/geist-latin.woff2 differ diff --git a/.design-sync/fonts/geist-mono-latin-ext.woff2 b/.design-sync/fonts/geist-mono-latin-ext.woff2 new file mode 100644 index 0000000..ea22fd9 Binary files /dev/null and b/.design-sync/fonts/geist-mono-latin-ext.woff2 differ diff --git a/.design-sync/fonts/geist-mono-latin.woff2 b/.design-sync/fonts/geist-mono-latin.woff2 new file mode 100644 index 0000000..0750682 Binary files /dev/null and b/.design-sync/fonts/geist-mono-latin.woff2 differ diff --git a/.design-sync/fonts/instrument-sans-latin-ext.woff2 b/.design-sync/fonts/instrument-sans-latin-ext.woff2 new file mode 100644 index 0000000..0ea0da0 Binary files /dev/null and b/.design-sync/fonts/instrument-sans-latin-ext.woff2 differ diff --git a/.design-sync/fonts/instrument-sans-latin.woff2 b/.design-sync/fonts/instrument-sans-latin.woff2 new file mode 100644 index 0000000..8611e41 Binary files /dev/null and b/.design-sync/fonts/instrument-sans-latin.woff2 differ diff --git a/.design-sync/fonts/source-sans-3-latin-ext.woff2 b/.design-sync/fonts/source-sans-3-latin-ext.woff2 new file mode 100644 index 0000000..7f78ca9 Binary files /dev/null and b/.design-sync/fonts/source-sans-3-latin-ext.woff2 differ diff --git a/.design-sync/fonts/source-sans-3-latin.woff2 b/.design-sync/fonts/source-sans-3-latin.woff2 new file mode 100644 index 0000000..864cc41 Binary files /dev/null and b/.design-sync/fonts/source-sans-3-latin.woff2 differ diff --git a/.design-sync/fonts/source-serif-4-latin-ext.woff2 b/.design-sync/fonts/source-serif-4-latin-ext.woff2 new file mode 100644 index 0000000..f40a64e Binary files /dev/null and b/.design-sync/fonts/source-serif-4-latin-ext.woff2 differ diff --git a/.design-sync/fonts/source-serif-4-latin.woff2 b/.design-sync/fonts/source-serif-4-latin.woff2 new file mode 100644 index 0000000..30dbe2d Binary files /dev/null and b/.design-sync/fonts/source-serif-4-latin.woff2 differ diff --git a/.design-sync/fonts/space-grotesk-latin-ext.woff2 b/.design-sync/fonts/space-grotesk-latin-ext.woff2 new file mode 100644 index 0000000..db732c2 Binary files /dev/null and b/.design-sync/fonts/space-grotesk-latin-ext.woff2 differ diff --git a/.design-sync/fonts/space-grotesk-latin.woff2 b/.design-sync/fonts/space-grotesk-latin.woff2 new file mode 100644 index 0000000..0f3474e Binary files /dev/null and b/.design-sync/fonts/space-grotesk-latin.woff2 differ diff --git a/.gitignore b/.gitignore index 7444245..d294a88 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,9 @@ storybook-static/ .next/ .source/ .superpowers/ +.design-sync/sb-reference/ +.design-sync/learnings/ +.design-sync/.cache/ +.design-sync/node_modules +.ds-sync/ +ds-bundle/ diff --git a/README.md b/README.md index 454f37f..9884d36 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Most component libraries trade flexibility for consistency or the other way arou | Package | Description | | --- | --- | | [`@e412/rnui-react`](./packages/react) | 70+ React components — buttons, forms, data grids, charts, drawers, command palettes | -| [`@e412/rnui-themes`](./packages/themes) | CSS themes — 8 built-in presets, OKLch color space, Tailwind v4 integration | +| [`@e412/rnui-themes`](./packages/themes) | CSS themes — 3 built-in presets, OKLch color space, Tailwind v4 integration, plus a precompiled utility layer and a JSON token export for tooling | ## Quick start @@ -33,10 +33,10 @@ Most component libraries trade flexibility for consistency or the other way arou ```html - + - + ``` ```tsx diff --git a/apps/docs/content/docs/theming.mdx b/apps/docs/content/docs/theming.mdx index 683a3e7..e7c97c3 100644 --- a/apps/docs/content/docs/theming.mdx +++ b/apps/docs/content/docs/theming.mdx @@ -21,18 +21,22 @@ This gives you the default warm neutral theme with light and dark mode support. ## Theme Presets -The library ships with **8 built-in theme presets**, each with its own fonts, border radius, and color personality. [Try them live →](/themes) +The library ships with **3 built-in theme presets**, each with its own fonts, border radius, and color personality. [Try them live →](/themes) -| Theme | Personality | Font | Radius | +| Theme | Character | Body / Headings | Radius | | --- | --- | --- | --- | -| **Oxide** | Editorial Craft | Bitter + DM Sans | `0.375rem` | -| **Ocean** | Smooth Aquatic | Plus Jakarta Sans | `0.875rem` | -| **Violet** | Luxury Creative | Outfit | `1rem` | -| **Forest** | Organic Natural | Fraunces + Source Sans 3 | `0.5rem` | -| **Rose** | Soft & Bubbly | Nunito | `1.25rem` | -| **Amber** | Sharp & Energetic | Sora | `0.25rem` | -| **Slate** | Professional Tech | Geist | `0.5rem` | -| **Crimson** | Brutalist Bold | Instrument Sans | `0` | +| **Press** | Editorial — warm paper, near-black ink, high contrast | Source Sans 3 / Source Serif 4 | `0.5rem` | +| **Console** | Technical — dense, low-chroma, terminal-adjacent | Geist / Geist Mono | `0.125rem` | +| **Signal** | Brand-forward — warm neutral shell, one confident accent | Instrument Sans / Space Grotesk | `0.75rem` | + +Three deliberate options rather than eight variations on one shell. Each has its +own neutral temperature, radius and type pairing — the differences are +structural, not just a swapped accent hue. + +Every preset ships a complete light **and** dark ramp, defines `--font-sans`, +`--font-heading` and `--font-mono`, and meets WCAG AA in both modes (4.5:1 for +text, 3:1 for borders and rings). That floor is enforced in the build, so a +regression fails CI rather than shipping. ### Using a Preset @@ -45,13 +49,13 @@ Import `presets` and set the `data-theme` attribute: ``` ```html - + ``` Dark mode works with the `.dark` class as usual: ```html - + ``` ### Loading Fonts @@ -69,7 +73,7 @@ For example, for the Ocean theme: Since themes are `data-theme` attributes, switching is one line of JS: ```ts -document.documentElement.dataset.theme = 'violet'; +document.documentElement.dataset.theme = 'console'; ``` ### Scoping Themes to a Container @@ -77,10 +81,10 @@ document.documentElement.dataset.theme = 'violet'; You can apply a theme to any element, not just ``: ```html -
+
-
+
``` @@ -199,8 +203,8 @@ Start from a preset and tweak specific values: @import '@e412/rnui-themes'; @import '@e412/rnui-themes/presets'; -/* Start from Ocean, customize primary */ -[data-theme="ocean"] { +/* Start from Signal, customize primary */ +[data-theme="signal"] { --primary: oklch(0.48 0.15 220); } ``` @@ -226,3 +230,53 @@ Or access CSS variables directly: font-family: var(--font-heading, var(--font-sans)); } ``` + +## Reading Tokens Programmatically + +For tooling — a token browser, a Figma sync, a theme editor — read +`tokens.json` rather than scanning the compiled CSS. The generated stylesheet +mixes rnui's tokens with Tailwind's internal custom properties, so a CSS scan +reports plumbing as if it were part of the palette. + +```ts +import tokens from '@e412/rnui-themes/tokens.json' + +const signalColors = tokens.tokens.filter( + (t) => t.theme === 'signal' && t.kind === 'color', +) +// → [{ name: '--primary', theme: 'signal', kind: 'color', +// value: 'oklch(…)', light: 'oklch(…)', dark: 'oklch(…)', +// darkInherited: false }, …] +``` + +`kind` is one of `color`, `dimension`, `font`, `radius`. `darkInherited: true` +means the theme's dark ramp doesn't declare that token and it falls through to +the light value — useful for auditing a theme for completeness. + +## Using rnui Without a Tailwind Build + +rnui normally expects Tailwind in your build, which compiles whatever classes +you write. If you're embedding the design system somewhere that can't run +Tailwind — a static stylesheet, a preview iframe, a design tool — any class that +wasn't compiled ahead of time silently does nothing. + +Import the precompiled utility layer instead: + +```css +@import '@e412/rnui-themes/utilities'; +``` + +It ships complete scales (spacing `0`–`24`, `text-xs`–`text-9xl`, +`grid-cols-1..12`, `max-w-xs`–`max-w-7xl`, radius/border/opacity, plus +`sm: md: lg: xl:` and `hover: focus: active: disabled: dark:` variants) at +roughly 353 KB, 40 KB gzipped. `utilities.json` lists exactly what shipped: + +```ts +import utilities from '@e412/rnui-themes/utilities.json' + +utilities.classes.includes('gap-10') // true +``` + +If you *do* run Tailwind, don't import this — keep importing +`@e412/rnui-themes` and let your build emit only what you use. To add the same +scales to your own build, import `@e412/rnui-themes/safelist`. diff --git a/apps/storybook/src/stories/progress.stories.tsx b/apps/storybook/src/stories/progress.stories.tsx index 2969d55..1dcadcb 100644 --- a/apps/storybook/src/stories/progress.stories.tsx +++ b/apps/storybook/src/stories/progress.stories.tsx @@ -5,6 +5,10 @@ const meta = { title: 'Components/Progress', component: Progress, tags: ['autodocs'], + // The stories size the track with a percentage width. The global + // `layout: 'centered'` makes the story root shrink-to-fit, so a percentage + // resolves against a 0px parent and the bar renders invisible. + parameters: { layout: 'padded' }, argTypes: { value: { control: { type: 'range', min: 0, max: 100, step: 1 }, diff --git a/apps/storybook/src/stories/slider.stories.tsx b/apps/storybook/src/stories/slider.stories.tsx index e8230f1..0286850 100644 --- a/apps/storybook/src/stories/slider.stories.tsx +++ b/apps/storybook/src/stories/slider.stories.tsx @@ -5,6 +5,10 @@ const meta = { title: 'Components/Slider', component: Slider, tags: ['autodocs'], + // The stories size the track with a percentage width. The global + // `layout: 'centered'` makes the story root shrink-to-fit, so a percentage + // resolves against a 0px parent and the slider renders invisible. + parameters: { layout: 'padded' }, argTypes: { defaultValue: { control: 'object', diff --git a/apps/storybook/src/styles.css b/apps/storybook/src/styles.css index c88d9e0..f86f7f9 100644 --- a/apps/storybook/src/styles.css +++ b/apps/storybook/src/styles.css @@ -2,3 +2,7 @@ @import "@e412/rnui-themes"; @import "tw-animate-css"; @source "../node_modules/@e412/rnui-react/dist"; +/* Emit the complete utility scales, not just the values these stories happen + * to use. The design-sync bundle is scraped from this build, and it is consumed + * precompiled — a class missing there fails silently for every consumer. */ +@import "@e412/rnui-themes/safelist"; diff --git a/packages/react/src/components/calendar.tsx b/packages/react/src/components/calendar.tsx index 6963fd1..42afaee 100644 --- a/packages/react/src/components/calendar.tsx +++ b/packages/react/src/components/calendar.tsx @@ -33,7 +33,7 @@ function Calendar({ svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, className, @@ -58,24 +58,24 @@ function Calendar({ ), button_previous: cn( buttonVariants({ variant: buttonVariant }), - 'size-(--cell-size) p-0 select-none aria-disabled:opacity-50', + 'size-(--rnui-_cell-size) p-0 select-none aria-disabled:opacity-50', defaultClassNames.button_previous, ), button_next: cn( buttonVariants({ variant: buttonVariant }), - 'size-(--cell-size) p-0 select-none aria-disabled:opacity-50', + 'size-(--rnui-_cell-size) p-0 select-none aria-disabled:opacity-50', defaultClassNames.button_next, ), month_caption: cn( - 'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)', + 'flex h-(--rnui-_cell-size) w-full items-center justify-center px-(--rnui-_cell-size)', defaultClassNames.month_caption, ), dropdowns: cn( - 'flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium', + 'flex h-(--rnui-_cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium', defaultClassNames.dropdowns, ), dropdown_root: cn( - 'relative rounded-(--cell-radius)', + 'relative rounded-(--rnui-_cell-radius)', defaultClassNames.dropdown_root, ), dropdown: cn( @@ -86,18 +86,18 @@ function Calendar({ 'font-medium select-none', captionLayout === 'label' ? 'text-sm' - : 'flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground', + : 'flex items-center gap-1 rounded-(--rnui-_cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground', defaultClassNames.caption_label, ), table: 'w-full border-collapse', weekdays: cn('flex', defaultClassNames.weekdays), weekday: cn( - 'flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none', + 'flex-1 rounded-(--rnui-_cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none', defaultClassNames.weekday, ), week: cn('mt-2 flex w-full', defaultClassNames.week), week_number_header: cn( - 'w-(--cell-size) select-none', + 'w-(--rnui-_cell-size) select-none', defaultClassNames.week_number_header, ), week_number: cn( @@ -105,23 +105,23 @@ function Calendar({ defaultClassNames.week_number, ), day: cn( - 'group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)', + 'group/day relative aspect-square h-full w-full rounded-(--rnui-_cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--rnui-_cell-radius)', props.showWeekNumber - ? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)' - : '[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)', + ? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--rnui-_cell-radius)' + : '[&:first-child[data-selected=true]_button]:rounded-l-(--rnui-_cell-radius)', defaultClassNames.day, ), range_start: cn( - 'relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted', + 'relative isolate z-0 rounded-l-(--rnui-_cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted', defaultClassNames.range_start, ), range_middle: cn('rounded-none', defaultClassNames.range_middle), range_end: cn( - 'relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted', + 'relative isolate z-0 rounded-r-(--rnui-_cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted', defaultClassNames.range_end, ), today: cn( - 'rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none', + 'rounded-(--rnui-_cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none', defaultClassNames.today, ), outside: cn( @@ -172,7 +172,7 @@ function Calendar({ WeekNumber: ({ children, ...props }) => { return ( -
+
{children}
@@ -214,7 +214,7 @@ function CalendarDayButton({ data-range-end={modifiers.range_end} data-range-middle={modifiers.range_middle} className={cn( - 'relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70', + 'relative isolate z-10 flex aspect-square size-auto w-full min-w-(--rnui-_cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-(--rnui-_cell-radius) data-[range-end=true]:rounded-r-(--rnui-_cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--rnui-_cell-radius) data-[range-start=true]:rounded-l-(--rnui-_cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70', defaultClassNames.day, className, )} diff --git a/packages/react/src/components/data-grid/data-grid-table.tsx b/packages/react/src/components/data-grid/data-grid-table.tsx index 3daecaf..d094021 100644 --- a/packages/react/src/components/data-grid/data-grid-table.tsx +++ b/packages/react/src/components/data-grid/data-grid-table.tsx @@ -337,7 +337,7 @@ function DataGridTableFillCol() { return ( ) } @@ -351,7 +351,7 @@ function DataGridTableFillHeadCell() { ) @@ -366,7 +366,7 @@ function DataGridTableFillBodyCell() { ) @@ -381,7 +381,7 @@ function DataGridTableFillFootCell() { ) @@ -430,7 +430,7 @@ function DataGridTableBase({ children }: { children: ReactNode }) { props.tableLayout?.columnsResizable ? { ...columnSizeVars, - width: `calc(${table.getTotalSize()}px + var(--data-grid-fill-size, 0px))`, + width: `calc(${table.getTotalSize()}px + var(--rnui-_data-grid-fill-size, 0px))`, } : undefined } @@ -519,8 +519,8 @@ function DataGridTableViewport({ style={{ ...(props.tableLayout?.columnsResizable ? { - width: `calc(${table.getTotalSize()}px + var(--data-grid-fill-size, 0px))`, - ['--data-grid-fill-size' as string]: `${fillWidth}px`, + width: `calc(${table.getTotalSize()}px + var(--rnui-_data-grid-fill-size, 0px))`, + ['--rnui-_data-grid-fill-size' as string]: `${fillWidth}px`, } : undefined), ...style, diff --git a/packages/themes/README.md b/packages/themes/README.md index 31a247c..764509e 100644 --- a/packages/themes/README.md +++ b/packages/themes/README.md @@ -1,6 +1,6 @@ # @e412/rnui-themes -CSS themes for [`@e412/rnui-react`](../react) — 8 built-in presets, a `data-theme` attribute system, full Tailwind CSS v4 integration, and the OKLch color space for perceptually uniform palettes. +CSS themes for [`@e412/rnui-react`](../react) — 3 built-in presets, a `data-theme` attribute system, full Tailwind CSS v4 integration, and the OKLch color space for perceptually uniform palettes. ```bash pnpm add @e412/rnui-themes @@ -21,36 +21,33 @@ That gives you the warm-neutral default theme. Add presets to unlock the built-i ``` ```html - + ``` ## Built-in presets -| Theme | Personality | Font | Radius | -| --------- | ----------------------------- | ------------------------ | ---------- | -| `oxide` | Editorial craft, warm copper | Bitter + DM Sans | `0.375rem` | -| `ocean` | Smooth aquatic, deep teal | Plus Jakarta Sans | `0.875rem` | -| `violet` | Luxury creative, rich purple | Outfit | `1rem` | -| `forest` | Organic natural, deep emerald | Fraunces + Source Sans 3 | `0.5rem` | -| `rose` | Soft & bubbly, warm pink | Nunito | `1.25rem` | -| `amber` | Sharp & energetic, golden | Sora | `0.25rem` | -| `slate` | Professional tech, blue-gray | Geist | `0.5rem` | -| `crimson` | Brutalist bold, scarlet | Instrument Sans | `0` | +| Theme | Character | Body / Headings | Radius | +| --------- | ------------------------------------------ | ------------------------------- | ---------- | +| `press` | Editorial — warm paper, high contrast | Source Sans 3 / Source Serif 4 | `0.5rem` | +| `console` | Technical — dense, cool, terminal-adjacent | Geist / Geist Mono | `0.125rem` | +| `signal` | Brand-forward — one confident accent | Instrument Sans / Space Grotesk | `0.75rem` | -Themes declare `--font-sans` and sometimes `--font-heading`, but **don't load the fonts** — that's your choice (Google Fonts, `next/font`, self-hosted). Each preset's CSS file documents the Google Fonts URL in its header comment. +Every theme sets `--font-sans`, `--font-heading` and `--font-mono`, ships a complete light _and_ dark ramp, and meets WCAG AA in both modes (4.5:1 text, 3:1 borders and rings) — enforced by `scripts/check-contrast.mjs`, which fails the build on a regression. + +Themes declare their font families but **don't load the fonts** — that's your choice (Google Fonts, `next/font`, self-hosted). Each preset's CSS file documents the Google Fonts URL in its header comment. ## Dark mode The docs and most apps expect a `.dark` class on ``: ```html - + ``` You can also scope themes to containers: ```html -
...
+
...
``` ## Custom themes @@ -83,8 +80,8 @@ Since themes are plain CSS, your overrides cascade naturally: ```css @import '@e412/rnui-themes/presets'; -/* Start from Ocean, customize the primary */ -[data-theme='ocean'] { +/* Start from Signal, customize the primary */ +[data-theme='signal'] { --primary: oklch(0.48 0.15 220); } ``` @@ -92,7 +89,7 @@ Since themes are plain CSS, your overrides cascade naturally: ## Runtime switching ```ts -document.documentElement.dataset.theme = 'violet' +document.documentElement.dataset.theme = 'console' ``` ## Available tokens @@ -107,6 +104,65 @@ document.documentElement.dataset.theme = 'violet' **Typography**: `--font-sans`, `--font-heading` (falls back to `--font-sans`). +### Machine-readable token export + +`dist/tokens.json` is the declared token surface — the source of truth for +tooling. Don't scan the generated CSS: it mixes these tokens with Tailwind's own +internal custom properties, which are not part of the rnui palette. + +```ts +import tokens from '@e412/rnui-themes/tokens.json' + +tokens.tokens + .filter((t) => t.theme === 'signal' && t.kind === 'color') + .map((t) => [t.name, t.light, t.dark]) +``` + +Each entry carries `name`, `theme`, `kind` (`color` | `dimension` | `font` | +`radius`), `value`, and its `light` / `dark` pair. A token a theme's dark ramp +doesn't declare is flagged `darkInherited: true` rather than presented as if the +theme set it deliberately. + +## Precompiled consumers + +If you run Tailwind yourself, ignore this section — importing +`@e412/rnui-themes` and letting your build compile what you use is smaller and +better. + +If you **can't** run Tailwind (a static stylesheet, an embedded preview, a design +tool), a class that was never emitted at build time silently does nothing: no +error, no warning, the element just renders unstyled. Two exports address that: + +```css +/* Precompiled utilities: complete scales, ~353 KB (~40 KB gzip) */ +@import '@e412/rnui-themes/utilities'; +``` + +```css +/* Or add the same scales to your own Tailwind build */ +@import 'tailwindcss'; +@import '@e412/rnui-themes'; +@import '@e412/rnui-themes/safelist'; +``` + +Covered: spacing `0`–`24` on every box-model utility, `text-xs`–`text-9xl`, +`grid-cols-1..12`, `col-span-*`, `max-w-xs`–`max-w-7xl`, radius, border, opacity, +`sm: md: lg: xl:` responsive, and `hover: focus: active: disabled: dark:` on the +interactive subset. + +`dist/utilities.json` lists what actually shipped, so tooling can tell a user +"`gap-9` isn't in this bundle" instead of letting it fail silently: + +```ts +import utilities from '@e412/rnui-themes/utilities.json' + +utilities.classes.includes('gap-10') // true +utilities.families['gap'] // every gap step that shipped +``` + +It's generated from the compiled output rather than the safelist source — the +safelist is what was requested, `utilities.json` is what Tailwind emitted. + ## Imports ```css @@ -114,17 +170,14 @@ document.documentElement.dataset.theme = 'violet' @import '@e412/rnui-themes/base'; /* reset + body font + heading */ @import '@e412/rnui-themes/light'; /* light variables on :root */ @import '@e412/rnui-themes/dark'; /* dark variables on .dark */ -@import '@e412/rnui-themes/presets'; /* all 8 presets as [data-theme] */ +@import '@e412/rnui-themes/presets'; /* all 3 presets as [data-theme] */ +@import '@e412/rnui-themes/utilities'; /* precompiled utility layer (see above) */ +@import '@e412/rnui-themes/safelist'; /* safelist source for your own build */ /* Individual presets (sets :root directly — use for single-theme apps) */ -@import '@e412/rnui-themes/oxide'; -@import '@e412/rnui-themes/ocean'; -@import '@e412/rnui-themes/violet'; -@import '@e412/rnui-themes/forest'; -@import '@e412/rnui-themes/rose'; -@import '@e412/rnui-themes/amber'; -@import '@e412/rnui-themes/slate'; -@import '@e412/rnui-themes/crimson'; +@import '@e412/rnui-themes/press'; +@import '@e412/rnui-themes/console'; +@import '@e412/rnui-themes/signal'; ``` ## License diff --git a/packages/themes/package.json b/packages/themes/package.json index a6b3342..7cbc880 100644 --- a/packages/themes/package.json +++ b/packages/themes/package.json @@ -16,24 +16,26 @@ "./base": "./dist/base.css", "./light": "./dist/light.css", "./dark": "./dist/dark.css", - "./oxide": "./dist/oxide.css", - "./ocean": "./dist/ocean.css", - "./violet": "./dist/violet.css", - "./forest": "./dist/forest.css", - "./rose": "./dist/rose.css", - "./amber": "./dist/amber.css", - "./slate": "./dist/slate.css", - "./crimson": "./dist/crimson.css", - "./presets": "./dist/presets.css" + "./presets": "./dist/presets.css", + "./utilities": "./dist/utilities.css", + "./safelist": "./dist/safelist.css", + "./tokens.json": "./dist/tokens.json", + "./utilities.json": "./dist/utilities.json", + "./press": "./dist/press.css", + "./console": "./dist/console.css", + "./signal": "./dist/signal.css" }, "publishConfig": { "access": "public" }, "scripts": { - "generate": "node scripts/generate-presets.mjs", - "build": "node scripts/generate-presets.mjs && vite build" + "generate": "node scripts/generate-themes.mjs && node scripts/generate-presets.mjs", + "build": "node scripts/generate-themes.mjs && node scripts/generate-presets.mjs && vite build && node scripts/emit-utilities.mjs && node scripts/emit-tokens.mjs && node scripts/check-contrast.mjs" }, "devDependencies": { + "@tailwindcss/vite": "^4.2.2", + "culori": "^4.0.2", + "tailwindcss": "^4.2.2", "vite": "^8.0.0" } } diff --git a/packages/themes/scripts/check-contrast.mjs b/packages/themes/scripts/check-contrast.mjs new file mode 100644 index 0000000..86d782d --- /dev/null +++ b/packages/themes/scripts/check-contrast.mjs @@ -0,0 +1,293 @@ +#!/usr/bin/env node +/** + * Accessibility and ramp-structure gate for every theme. + * + * Accessibility is a floor on all themes, not a separate "high contrast" + * preset — so this runs in the build and fails it on a regression rather than + * leaving contrast to be eyeballed after the fact. + * + * Checks, per theme and per mode: + * + * AA text 4.5:1 for text-on-surface pairs + * AA UI 3:1 for borders, rings and other non-text boundaries + * surfaces dark ramps must step monotonically by >= 4% lightness, so + * elevation reads as elevation instead of noise + * chroma a dark token may not be more saturated than its light + * counterpart (raising chroma to compensate for darkness is + * what makes a dark mode look muddy) + * neutrality no literally-gray neutrals; this system's greys are warm + * + * Pair semantics are taken from how the components actually use the tokens, + * not from a generic convention: + * - `primary`/`primary-foreground` etc. are true text-on-solid pairs. + * - status colors (`destructive`, `success`, `info`, `warning`) are used as + * TEXT on ordinary surfaces (`text-destructive`) and their `-foreground` + * variants as text on a 10% tint of themselves (`bg-destructive/10`), so + * that is what gets measured. + * + * Run: node scripts/check-contrast.mjs [--quiet] (wired into `pnpm build`) + */ + +import { readFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { converter, parse } from 'culori' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') +const toRgb = converter('rgb') +const QUIET = process.argv.includes('--quiet') + +const AA_TEXT = 4.5 +const AA_UI = 3 + +/** + * Themes produced by generate-themes.mjs. They must additionally satisfy the + * ramp-structure rules (surface separation, dark-chroma ceiling); every theme, + * generated or not, must satisfy the accessibility floor. + */ +const GENERATED = new Set(['press', 'console', 'signal']) +const MIN_SURFACE_STEP = 0.04 // 4% lightness between dark elevation steps + +/** WCAG relative luminance from an sRGB triple in 0..1. */ +function luminance({ r, g, b }) { + const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4) + return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b) +} + +function contrast(a, b) { + const [x, y] = [luminance(a), luminance(b)].sort((m, n) => n - m) + return (x + 0.05) / (y + 0.05) +} + +/** Composite `fg` at `alpha` over `bg` (sRGB, as a browser would). */ +function over(fg, bg, alpha) { + return { + r: fg.r * alpha + bg.r * (1 - alpha), + g: fg.g * alpha + bg.g * (1 - alpha), + b: fg.b * alpha + bg.b * (1 - alpha), + } +} + +const failures = [] +const notes = [] + +function need(theme, mode, ratio, min, label) { + if (ratio == null) return + if (ratio + 1e-9 < min) { + failures.push( + `${theme}/${mode}: ${label} is ${ratio.toFixed(2)}:1, needs ${min}:1`, + ) + } +} + +// Text-on-solid pairs: `` is a surface, `-foreground` is text on it. +const SOLID_PAIRS = [ + ['background', 'foreground'], + ['card', 'card-foreground'], + ['popover', 'popover-foreground'], + ['primary', 'primary-foreground'], + ['secondary', 'secondary-foreground'], + ['muted', 'muted-foreground'], + ['accent', 'accent-foreground'], + ['invert', 'invert-foreground'], + ['focus', 'focus-foreground'], + ['sidebar', 'sidebar-foreground'], + ['sidebar-primary', 'sidebar-primary-foreground'], + ['sidebar-accent', 'sidebar-accent-foreground'], +] + +// Used as text directly on ordinary surfaces, and as `-foreground` on a tint. +const STATUS = ['destructive', 'success', 'info', 'warning'] + +// Non-text boundaries. +const UI_ON_SURFACE = [ + 'border', + 'input', + 'ring', + 'sidebar-border', + 'sidebar-ring', +] + +// Dark elevation order. Equal steps are allowed where the spec allows them +// (card <= popover); everything else must strictly increase. +const SURFACE_ORDER = [ + 'background', + 'card', + 'popover', + 'muted', + 'accent', + 'border', +] + +function checkTheme(theme, mode, vars, lightVars) { + /** + * Resolve a token to an opaque sRGB colour. Several tokens are authored with + * alpha (`oklch(100% .004 75/.1)` for borders); measuring those as opaque + * reports a near-white boundary at ~20:1 when the rendered line is faint, so + * composite over the surface it sits on before measuring. + */ + const rgb = (name, backdropName = 'background') => { + const raw = vars[`--${name}`] + if (!raw) return null + const p = parse(raw) + if (!p) return null + const c = toRgb(p) + const alpha = p.alpha ?? 1 + if (alpha >= 1) return c + const backRaw = vars[`--${backdropName}`] + const back = backRaw ? parse(backRaw) : null + if (!back) return c + return over(c, toRgb(back), alpha) + } + const oklchOf = (name) => { + const raw = vars[`--${name}`] + if (!raw) return null + return parse(raw) + } + + for (const [base, fg] of SOLID_PAIRS) { + const a = rgb(base) + const b = rgb(fg) + if (!a || !b) continue + need(theme, mode, contrast(a, b), AA_TEXT, `${fg} on ${base}`) + } + + const bg = rgb('background') + const card = rgb('card') + + // muted-foreground must be readable on BOTH background and card — passing + // against one and failing the other is the usual defect. + const mutedFg = rgb('muted-foreground') + if (mutedFg && bg) { + need( + theme, + mode, + contrast(mutedFg, bg), + AA_TEXT, + 'muted-foreground on background', + ) + } + if (mutedFg && card) { + need( + theme, + mode, + contrast(mutedFg, card), + AA_TEXT, + 'muted-foreground on card', + ) + } + + for (const s of STATUS) { + const solid = rgb(s) + const fg = rgb(`${s}-foreground`) + if (solid && bg) { + need(theme, mode, contrast(solid, bg), AA_TEXT, `text-${s} on background`) + } + if (solid && card) { + need(theme, mode, contrast(solid, card), AA_TEXT, `text-${s} on card`) + } + // `-foreground` sits on a 10% tint of its own colour over the background. + if (fg && solid && bg) { + need( + theme, + mode, + contrast(fg, over(solid, bg, 0.1)), + AA_TEXT, + `${s}-foreground on ${s}/10 tint`, + ) + } + } + + for (const ui of UI_ON_SURFACE) { + const backdrop = ui.startsWith('sidebar') ? 'sidebar' : 'background' + const c = rgb(ui, backdrop) + const surface = ui.startsWith('sidebar') ? (rgb('sidebar') ?? bg) : bg + if (!c || !surface) continue + const ratio = contrast(c, surface) + need(theme, mode, ratio, AA_UI, `${ui} against its surface`) + // "Visible without glowing" — flag borders that read as a bright line. + if (ui === 'border' && ratio > 7) { + notes.push( + `${theme}/${mode}: border is ${ratio.toFixed(2)}:1 against background — very high for a boundary`, + ) + } + } + + // Neutrals must not be literally gray. + for (const name of ['background', 'foreground', 'card', 'muted']) { + const c = oklchOf(name) + if (c && typeof c.c === 'number' && c.c === 0) { + failures.push( + `${theme}/${mode}: --${name} is a pure gray (chroma 0); neutrals must be warm`, + ) + } + } + + // Structural rules below are the ramp discipline the generated presets are + // authored to (see generate-themes.mjs). The default theme predates them and + // is deliberately held only to the accessibility floor above — restyling it + // would change the look every consumer gets out of the box. + if (mode === 'dark' && GENERATED.has(theme)) { + // Elevation must be monotonic and separated. + let prev = null + for (const name of SURFACE_ORDER) { + const c = oklchOf(name) + if (!c || typeof c.l !== 'number') continue + if (prev) { + const step = c.l - prev.l + const equalAllowed = prev.name === 'card' && name === 'popover' + if (step < 0) { + failures.push( + `${theme}/dark: --${name} (L ${c.l.toFixed(3)}) is darker than --${prev.name} (L ${prev.l.toFixed(3)}); elevation must increase`, + ) + } else if (step < MIN_SURFACE_STEP && !(equalAllowed && step >= 0)) { + failures.push( + `${theme}/dark: --${prev.name} → --${name} steps only ${(step * 100).toFixed(1)}% lightness, needs ${MIN_SURFACE_STEP * 100}%`, + ) + } + } + prev = { ...c, name } + } + + // Dark chroma may not exceed the light value for the same token. + for (const key of Object.keys(vars)) { + const d = parse(vars[key]) + const l = lightVars[key] ? parse(lightVars[key]) : null + if (!d || !l || typeof d.c !== 'number' || typeof l.c !== 'number') + continue + if (d.c > l.c + 1e-6) { + failures.push( + `${theme}/dark: ${key} chroma ${d.c.toFixed(3)} exceeds light ${l.c.toFixed(3)}`, + ) + } + } + } +} + +const { tokens } = JSON.parse( + readFileSync(join(root, 'dist', 'tokens.json'), 'utf8'), +) +const themes = {} +for (const t of tokens) { + const entry = (themes[t.theme] ??= { light: {}, dark: {} }) + entry.light[t.name] = t.light ?? t.dark + entry.dark[t.name] = t.dark +} + +for (const [theme, { light, dark }] of Object.entries(themes)) { + checkTheme(theme, 'light', light, light) + checkTheme(theme, 'dark', dark, light) +} + +if (!QUIET) for (const n of notes) console.error(` ~ ${n}`) + +if (failures.length) { + console.error(`\n✗ contrast gate: ${failures.length} failure(s)\n`) + for (const f of failures) console.error(` ${f}`) + console.error('') + process.exit(1) +} + +console.log( + ` contrast: ${Object.keys(themes).length} themes pass AA (${AA_TEXT}:1 text, ${AA_UI}:1 UI) in both modes`, +) diff --git a/packages/themes/scripts/emit-tokens.mjs b/packages/themes/scripts/emit-tokens.mjs new file mode 100644 index 0000000..2620c22 --- /dev/null +++ b/packages/themes/scripts/emit-tokens.mjs @@ -0,0 +1,151 @@ +#!/usr/bin/env node +/** + * Emits dist/tokens.json — the declared public token surface. + * + * Why this exists: the token surface used to be discoverable only by scanning + * generated CSS, which mixes rnui's tokens with Tailwind's `--tw-*` plumbing + * (`--tw-translate-x`, `--tw-ease`, the `--tw-space-*-reverse` pair, …). + * Downstream extraction then presents Tailwind internals as if they were part + * of the rnui palette, and the only way to correct it is to hand-patch the + * generated stylesheet — patches that every recompile wipes. + * + * So the export is built from the hand-authored theme sources instead, never + * from compiled output. `--tw-*` cannot appear here by construction: those + * variables exist only in Tailwind's output, which this script never reads. + * + * Run: node scripts/emit-tokens.mjs (wired into `pnpm build`) + */ + +import { readFileSync, readdirSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') +const srcDir = join(root, 'src') +const distDir = join(root, 'dist') + +// Files that are not themes: infrastructure, or the generated aggregate. +const NOT_A_THEME = new Set([ + 'base.css', + 'index.css', + 'presets.css', + 'safelist.css', + 'utilities.css', + 'light.css', + 'dark.css', +]) + +/** Pull `--name: value` pairs out of the first block matching `selector`. */ +function readBlock(css, selector) { + const re = new RegExp( + `${selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{([\\s\\S]*?)\\n\\}`, + 'm', + ) + const m = re.exec(css) + if (!m) return {} + const out = {} + for (const decl of m[1].matchAll(/--([\w-]+)\s*:\s*([^;]+);/g)) { + out[`--${decl[1]}`] = decl[2].trim() + } + return out +} + +/** + * `kind` is derived from the token's name first (names are the stable contract) + * and only falls back to sniffing the value. + */ +function kindOf(name, value) { + if (name.startsWith('--font-')) return 'font' + if (name === '--radius' || name.startsWith('--radius-')) return 'radius' + if (/^(oklch|rgb|hsl|color|#)/i.test(value)) return 'color' + if (/^-?[\d.]+(rem|px|em|%|vh|vw)$/.test(value)) return 'dimension' + if (/^(calc|var)\(/.test(value)) return 'dimension' + return 'color' +} + +const themes = [] + +// The default theme is split across light.css (:root) and dark.css (.dark). +themes.push({ + slug: 'default', + light: readBlock(readFileSync(join(srcDir, 'light.css'), 'utf8'), ':root'), + dark: readBlock(readFileSync(join(srcDir, 'dark.css'), 'utf8'), '.dark'), +}) + +// Each preset carries both ramps in one file. +for (const file of readdirSync(srcDir).sort()) { + if (!file.endsWith('.css') || NOT_A_THEME.has(file)) continue + const css = readFileSync(join(srcDir, file), 'utf8') + themes.push({ + slug: file.replace(/\.css$/, ''), + light: readBlock(css, ':root'), + dark: readBlock(css, '.dark'), + }) +} + +const tokens = [] +for (const { slug, light, dark } of themes) { + // A preset's dark block may omit tokens it inherits from its own light ramp. + // Record what the theme actually declares, and flag the inherited ones so an + // incomplete dark ramp is visible rather than silently papered over. + for (const name of Object.keys(light)) { + const darkValue = dark[name] ?? null + tokens.push({ + name, + theme: slug, + kind: kindOf(name, light[name]), + value: light[name], + light: light[name], + dark: darkValue ?? light[name], + darkInherited: darkValue === null, + }) + } + // Tokens that appear only in the dark ramp (rare, but don't drop them). + for (const name of Object.keys(dark)) { + if (name in light) continue + tokens.push({ + name, + theme: slug, + kind: kindOf(name, dark[name]), + value: dark[name], + light: null, + dark: dark[name], + darkInherited: false, + }) + } +} + +const offenders = tokens.filter((t) => t.name.startsWith('--tw-')) +if (offenders.length) { + console.error( + `✗ tokens.json would contain ${offenders.length} --tw-* entries: ${offenders + .map((t) => t.name) + .join(', ')}`, + ) + process.exit(1) +} + +const byTheme = {} +for (const t of tokens) (byTheme[t.theme] ??= []).push(t) + +writeFileSync( + join(distDir, 'tokens.json'), + JSON.stringify( + { + $comment: + 'Declared public design tokens for @e412/rnui-themes. Built from the hand-authored theme sources, never from compiled CSS, so Tailwind-internal custom properties are structurally excluded. This file is the source of truth for token extraction; do not scan the generated stylesheets.', + generator: '@e412/rnui-themes', + themes: Object.keys(byTheme), + count: tokens.length, + tokens, + }, + null, + 2, + ) + '\n', +) + +const incomplete = tokens.filter((t) => t.darkInherited).length +console.log( + ` tokens.json: ${tokens.length} tokens across ${Object.keys(byTheme).length} themes` + + (incomplete ? ` (${incomplete} inherit their dark value)` : ''), +) diff --git a/packages/themes/scripts/emit-utilities.mjs b/packages/themes/scripts/emit-utilities.mjs new file mode 100644 index 0000000..fa9c547 --- /dev/null +++ b/packages/themes/scripts/emit-utilities.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node +/** + * Post-build step for the precompiled utility layer. + * + * 1. Copies src/safelist.css to dist/ so `@e412/rnui-themes/safelist` is + * importable from the published package (only `dist` is published). + * 2. Writes dist/utilities.json — the list of utility classes that actually + * shipped in dist/utilities.css. + * + * (2) is deliberately derived from the COMPILED output, not from the safelist + * source. The safelist says what we asked for; utilities.json says what Tailwind + * emitted. Downstream tooling can then tell a consumer "`gap-9` is not in this + * bundle" instead of letting the class fail silently. + * + * Run: node scripts/emit-utilities.mjs (wired into `pnpm build`) + */ + +import { copyFileSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') +const srcDir = join(root, 'src') +const distDir = join(root, 'dist') + +copyFileSync(join(srcDir, 'safelist.css'), join(distDir, 'safelist.css')) + +const css = readFileSync(join(distDir, 'utilities.css'), 'utf8') + +// Tailwind escapes the characters that aren't bare-identifier safe: `sm\:p-4`, +// `w-1\/2`, `p-0\.5`, `bg-primary\/50`. Unescape to get the class a consumer +// would actually type. Selectors are collected from rule preludes only, so +// at-rule bodies (@media, @supports) contribute their inner selectors and +// nothing else. +const classes = new Set() +for (const m of css.matchAll(/\.((?:[\w-]|\\.)+)/g)) { + const cls = m[1].replace(/\\(.)/g, '$1') + // Skip Tailwind's internal marker classes and anything that isn't a utility + // a consumer would write. + if (cls.startsWith('tw-') || cls.startsWith('_')) continue + classes.add(cls) +} + +const sorted = [...classes].sort((a, b) => a.localeCompare(b)) + +// Group by utility family (the part before the first numeric/fractional value) +// so consumers can answer "which gap steps exist?" without scanning the flat list. +const families = {} +for (const cls of sorted) { + const bare = cls.includes(':') ? cls.slice(cls.lastIndexOf(':') + 1) : cls + const family = + bare.replace(/-?\[.*$/, '').replace(/-(\d.*|full|auto|none)$/, '') || bare + ;(families[family] ??= []).push(cls) +} + +writeFileSync( + join(distDir, 'utilities.json'), + JSON.stringify( + { + $comment: + 'Utility classes present in dist/utilities.css. Generated from the compiled output by scripts/emit-utilities.mjs — a class absent here will not apply in a precompiled consumer.', + generator: '@e412/rnui-themes', + count: sorted.length, + familyCount: Object.keys(families).length, + classes: sorted, + families: Object.fromEntries( + Object.entries(families).sort(([a], [b]) => a.localeCompare(b)), + ), + }, + null, + 2, + ) + '\n', +) + +console.log( + ` utilities.json: ${sorted.length} classes across ${Object.keys(families).length} families`, +) diff --git a/packages/themes/scripts/generate-presets.mjs b/packages/themes/scripts/generate-presets.mjs index e9cb8a5..7815870 100644 --- a/packages/themes/scripts/generate-presets.mjs +++ b/packages/themes/scripts/generate-presets.mjs @@ -14,16 +14,7 @@ import { fileURLToPath } from 'node:url' const __dirname = dirname(fileURLToPath(import.meta.url)) const srcDir = join(__dirname, '..', 'src') -const themeNames = [ - 'oxide', - 'ocean', - 'violet', - 'forest', - 'rose', - 'amber', - 'slate', - 'crimson', -] +const themeNames = ['press', 'console', 'signal'] let output = `/* Auto-generated from theme source files — do not edit directly. * Run: node scripts/generate-presets.mjs diff --git a/packages/themes/scripts/generate-themes.mjs b/packages/themes/scripts/generate-themes.mjs new file mode 100644 index 0000000..846c5a2 --- /dev/null +++ b/packages/themes/scripts/generate-themes.mjs @@ -0,0 +1,405 @@ +#!/usr/bin/env node +/** + * Generates the theme source files from a small anchor spec. + * + * Each theme is defined by a handful of anchors (background, foreground, + * primary, in both modes) plus its hues, radius and fonts. Everything else — + * surfaces, borders, rings, muted text, status foregrounds — is DERIVED here so + * the numeric constraints hold by construction rather than by hand-tuning ~270 + * values and hoping: + * + * - dark surfaces step monotonically by >= 4% lightness (elevation reads as + * elevation, not noise) + * - dark chroma never exceeds the light value for the same token + * - neutrals are never literally gray + * - one accent hue per theme drives primary / focus / ring / sidebar-primary, + * and `accent` is a tint of it rather than a second colour + * - status hues are shared across all themes so they stay recognizable + * - contrast floors are solved for, not guessed + * + * scripts/check-contrast.mjs is the gate that proves the output; this script is + * what makes passing it reproducible. + * + * Run: node scripts/generate-themes.mjs (wired into `pnpm build`) + */ + +import { writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { converter, parse } from 'culori' + +const srcDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'src') +const toRgb = converter('rgb') + +const ok = (l, c, h, a) => + a == null ? { mode: 'oklch', l, c, h } : { mode: 'oklch', l, c, h, alpha: a } +const fmt = ({ l, c, h, alpha }) => { + const base = `oklch(${+(l * 100).toFixed(1)}% ${+c.toFixed(4)} ${+h.toFixed(1)}` + return alpha == null || alpha >= 1 + ? `${base})` + : `${base} / ${+alpha.toFixed(2)})` +} + +const lum = (rgb) => { + const lin = (x) => (x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4) + return 0.2126 * lin(rgb.r) + 0.7152 * lin(rgb.g) + 0.0722 * lin(rgb.b) +} +const ratio = (a, b) => { + const [x, y] = [lum(toRgb(a)), lum(toRgb(b))].sort((m, n) => n - m) + return (x + 0.05) / (y + 0.05) +} +const over = (fg, bg, alpha) => { + const f = toRgb(fg) + const b = toRgb(bg) + return { + mode: 'rgb', + r: f.r * alpha + b.r * (1 - alpha), + g: f.g * alpha + b.g * (1 - alpha), + b: f.b * alpha + b.b * (1 - alpha), + } +} + +/** + * Find the lightness at (chroma, hue) that just clears `target` contrast + * against `backdrop`, searching in `dir` (+1 lighter, -1 darker). Binary search + * on L; returns the least-extreme value that passes so colours stay as close to + * the surface as accessibility allows. + */ +function solveL(backdrop, target, { c, h, dir, from }) { + let lo = from + let hi = dir > 0 ? 1 : 0 + if (ratio(ok(hi, c, h), backdrop) < target) return hi // unreachable; clamp + for (let i = 0; i < 40; i++) { + const mid = (lo + hi) / 2 + if (ratio(ok(mid, c, h), backdrop) >= target) hi = mid + else lo = mid + } + return +hi.toFixed(4) +} + +// Status hues are shared across every theme so success/info/warning/destructive +// stay recognizable; only their lightness adapts to the mode. +const STATUS = { + destructive: { h: 27, c: 0.16 }, + success: { h: 152, c: 0.11 }, + info: { h: 250, c: 0.12 }, + warning: { h: 75, c: 0.13 }, +} + +const THEMES = [ + { + slug: 'press', + title: 'Press — Editorial', + blurb: 'Warm paper, near-black ink, high contrast. Serif headings.', + fonts: { + sans: "'Source Sans 3', ui-sans-serif, system-ui, sans-serif", + heading: "'Source Serif 4', ui-serif, Georgia, serif", + mono: "'Geist Mono', ui-monospace, SFMono-Regular, monospace", + }, + fontUrl: + 'https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400..700&family=Source+Serif+4:wght@400..700&display=swap', + radius: '0.5rem', + neutralHue: 70, + accentHue: 25, + light: { + bg: ok(0.975, 0.012, 85), + fg: ok(0.16, 0.015, 60), + primary: ok(0.45, 0.12, 25), + }, + dark: { + bg: ok(0.15, 0.012, 60), + fg: ok(0.93, 0.012, 80), + primary: ok(0.72, 0.11, 30), + }, + }, + { + slug: 'console', + title: 'Console — Technical', + blurb: 'Dense, low-chroma, cool-leaning but never cold. Mono headings.', + fonts: { + sans: "'Geist', ui-sans-serif, system-ui, sans-serif", + heading: "'Geist Mono', ui-monospace, SFMono-Regular, monospace", + mono: "'Geist Mono', ui-monospace, SFMono-Regular, monospace", + }, + fontUrl: + 'https://fonts.googleapis.com/css2?family=Geist:wght@400..700&family=Geist+Mono:wght@400..700&display=swap', + radius: '0.125rem', + neutralHue: 243, + accentHue: 235, + dense: true, + light: { + bg: ok(0.965, 0.005, 240), + fg: ok(0.2, 0.01, 245), + primary: ok(0.52, 0.11, 235), + }, + dark: { + bg: ok(0.16, 0.008, 245), + fg: ok(0.91, 0.008, 240), + primary: ok(0.7, 0.1, 230), + }, + }, + { + slug: 'signal', + title: 'Signal — Brand-forward', + blurb: 'Warm neutral shell, one confident accent doing all the work.', + fonts: { + sans: "'Instrument Sans', ui-sans-serif, system-ui, sans-serif", + heading: "'Space Grotesk', ui-sans-serif, system-ui, sans-serif", + mono: "'Geist Mono', ui-monospace, SFMono-Regular, monospace", + }, + fontUrl: + 'https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400..700&family=Space+Grotesk:wght@400..700&display=swap', + radius: '0.75rem', + neutralHue: 72, + accentHue: 265, + light: { + bg: ok(0.98, 0.008, 75), + fg: ok(0.18, 0.014, 60), + primary: ok(0.55, 0.14, 265), + }, + dark: { + bg: ok(0.17, 0.012, 65), + fg: ok(0.94, 0.01, 75), + primary: ok(0.72, 0.13, 268), + }, + }, +] + +/** Build one mode's complete ramp. */ +function ramp(theme, mode) { + const dark = mode === 'dark' + const { bg, fg, primary } = theme[mode] + const nh = theme.neutralHue + const ah = theme.accentHue + // Neutral chroma: small but never zero. Console runs lower by design. + // Identical in both modes so the dark-chroma clamp is a no-op for neutrals; + // clamping AFTER solving would shift a colour off the contrast floor it was + // solved for. + const nc = theme.slug === 'console' ? 0.006 : 0.01 + const dir = dark ? 1 : -1 // in dark, surfaces get lighter as they elevate + + // Surface ladder. Dark steps 4.5% so the >=4% gate has headroom; light stays + // tight because elevation there reads via border and shadow, not lightness. + // Dark elevates by getting lighter; light elevates by getting closer to + // white, with recessed surfaces stepping down instead. + const step = dark ? 0.045 : 0.02 + const card = ok( + dark ? bg.l + step : Math.min(0.995, bg.l + step * 0.8), + nc * 0.8, + nh, + ) + const popover = ok(card.l, nc * 0.8, nh) + const secondary = ok(dark ? card.l + step : bg.l - step, nc, nh) + const muted = ok(secondary.l, nc, nh) + // `accent` is a tint of the theme's one accent hue, not a second colour. + const accent = ok(dark ? muted.l + step : bg.l - step * 1.4, 0.018, ah) + + // Boundaries: solve for exactly the floor the gate requires. + const borderL = solveL(bg, 3.05, { + c: nc, + h: nh, + dir, + from: dark ? accent.l : bg.l, + }) + const border = ok(borderL, nc, nh) + const input = ok(borderL, nc, nh) + const ring = ok( + solveL(bg, 3.05, { c: 0.06, h: ah, dir, from: bg.l }), + 0.06, + ah, + ) + + /** + * Body text has to clear AA on EVERY surface it can land on, so solve against + * the worst one: in light mode that's the darkest surface (least contrast for + * dark text), in dark mode the lightest. Solving against `card` alone is what + * produces text that passes on cards and fails on the page. + */ + const textSurfaces = [bg, card, muted] + const hardest = dark + ? textSurfaces.reduce((a, b) => (b.l > a.l ? b : a)) + : textSurfaces.reduce((a, b) => (b.l < a.l ? b : a)) + // Small margin above the 4.5 floor absorbs the rounding applied on output. + const AA = 4.65 + + const mutedFgL = solveL(hardest, AA, { + c: nc, + h: nh, + dir: dark ? 1 : -1, + from: dark ? accent.l : muted.l, + }) + const mutedFg = ok(mutedFgL, nc, nh) + + /** + * Text on a solid fill wants a clean, decisive colour — near-white on a dark + * fill, near-black on a light one — not the least-extreme value that merely + * clears the floor (that reads as a washed-out tint). Take the decisive end + * and only fall back to solving if the fill is so mid-toned that neither end + * passes. + */ + const onSolid = (fill) => { + for (const cand of fill.l > 0.55 + ? [ok(0.16, 0.02, ah)] + : [ok(0.98, 0.015, ah)]) { + if (ratio(cand, fill) >= AA) return cand + } + return ok( + solveL(fill, AA, { + c: 0.02, + h: ah, + dir: fill.l > 0.55 ? -1 : 1, + from: fill.l, + }), + 0.02, + ah, + ) + } + const onPrimary = onSolid(primary) + + const status = {} + for (const [name, { h, c }] of Object.entries(STATUS)) { + // The solid colour is used as TEXT on ordinary surfaces (`text-destructive`). + const solidL = solveL(hardest, AA, { c, h, dir: dark ? 1 : -1, from: 0.6 }) + const solid = ok(solidL, c, h) + // The `-foreground` variant sits on a 10% tint of itself. + const tint = over(solid, bg, 0.1) + const fgL = solveL(tint, AA, { + c: c * 0.9, + h, + dir: dark ? 1 : -1, + from: solidL, + }) + status[name] = solid + status[`${name}-foreground`] = ok(fgL, c * 0.9, h) + } + + const invert = ok(dark ? 0.37 : 0.21, nc, nh) + const invertFg = ok(dark ? 0.98 : 0.98, nc * 0.4, nh) + const focus = ok(dark ? 0.62 : 0.55, 0.14, ah) + const focusFg = onSolid(focus) + + // Charts: spread around the accent hue, moderate chroma, dark never hotter. + const chartC = dark ? 0.11 : 0.12 + const chart = [0, 62, 128, 196, 284].map((offset, i) => + ok(dark ? 0.7 - i * 0.015 : 0.55 + i * 0.01, chartC, (ah + offset) % 360), + ) + + // Sidebar is a slightly recessed surface sharing the theme's accent. + const sidebar = ok(dark ? bg.l + step * 0.6 : bg.l - step * 0.8, nc, nh) + const sidebarBorderL = solveL(sidebar, 3.05, { + c: nc, + h: nh, + dir, + from: sidebar.l, + }) + const sidebarAccent = ok( + dark ? sidebar.l + step : sidebar.l - step * 1.4, + 0.018, + ah, + ) + // Measured against the sidebar, so solve against it too. + const sidebarRing = ok( + solveL(sidebar, 3.05, { c: 0.06, h: ah, dir, from: sidebar.l }), + 0.06, + ah, + ) + + return { + '--background': bg, + '--foreground': fg, + '--card': card, + '--card-foreground': fg, + '--popover': popover, + '--popover-foreground': fg, + '--primary': primary, + '--primary-foreground': onPrimary, + '--secondary': secondary, + '--secondary-foreground': fg, + '--muted': muted, + '--muted-foreground': mutedFg, + '--accent': accent, + '--accent-foreground': fg, + '--destructive': status.destructive, + '--destructive-foreground': status['destructive-foreground'], + '--success': status.success, + '--success-foreground': status['success-foreground'], + '--info': status.info, + '--info-foreground': status['info-foreground'], + '--warning': status.warning, + '--warning-foreground': status['warning-foreground'], + '--invert': invert, + '--invert-foreground': invertFg, + '--focus': focus, + '--focus-foreground': focusFg, + '--border': border, + '--input': input, + '--ring': ring, + '--chart-1': chart[0], + '--chart-2': chart[1], + '--chart-3': chart[2], + '--chart-4': chart[3], + '--chart-5': chart[4], + '--sidebar': sidebar, + '--sidebar-foreground': fg, + '--sidebar-primary': primary, + '--sidebar-primary-foreground': onPrimary, + '--sidebar-accent': sidebarAccent, + '--sidebar-accent-foreground': fg, + '--sidebar-border': ok(sidebarBorderL, nc, nh), + '--sidebar-ring': sidebarRing, + } +} + +/** Clamp every dark chroma to its light counterpart. */ +function clampChroma(light, dark) { + for (const key of Object.keys(dark)) { + const l = light[key] + if (l && dark[key].c > l.c) dark[key] = { ...dark[key], c: l.c } + } + return dark +} + +for (const theme of THEMES) { + const light = ramp(theme, 'light') + const dark = clampChroma(light, ramp(theme, 'dark')) + + const head = [ + '/*', + ` * ${theme.title}`, + ` * ${theme.blurb}`, + ' *', + ` * Fonts: ${theme.fonts.heading.split(',')[0]} (headings) + ${theme.fonts.sans.split(',')[0]} (body)`, + ' * Load via Google Fonts, next/font, or self-host:', + ` * ${theme.fontUrl}`, + ' *', + ` * Radius: ${theme.radius}`, + ` * Accent hue: ~${theme.accentHue} · neutral hue: ~${theme.neutralHue}`, + ' *', + ' * GENERATED by scripts/generate-themes.mjs — edit the anchors there, not', + ' * this file. Values are solved against scripts/check-contrast.mjs.', + ' */', + ].join('\n') + + const block = (sel, vars, extra = '') => + `${sel} {\n` + + ` --font-sans: ${theme.fonts.sans};\n` + + ` --font-heading: ${theme.fonts.heading};\n` + + ` --font-mono: ${theme.fonts.mono};\n` + + ` --radius: ${theme.radius};\n` + + extra + + Object.entries(vars) + .map(([k, v]) => ` ${k}: ${fmt(v)};`) + .join('\n') + + '\n}\n' + + // Console runs denser type than the other two. + const dense = theme.dense + ? ' --leading-base: 1.4;\n --text-base-size: 0.9375rem;\n' + : '' + + writeFileSync( + join(srcDir, `${theme.slug}.css`), + `${head}\n\n${block(':root', light, dense)}\n${block('.dark', dark, dense)}`, + ) + console.log(` ${theme.slug}.css`) +} diff --git a/packages/themes/src/amber.css b/packages/themes/src/amber.css deleted file mode 100644 index 51a88cb..0000000 --- a/packages/themes/src/amber.css +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Amber — Sharp & Energetic - * Angular, tight radius, golden tones with high contrast. - * Feels like a fintech dashboard or developer tool. - * - * Font: Sora (geometric, energetic, precise) - * Load via Google Fonts, next/font, or self-host: - * https://fonts.googleapis.com/css2?family=Sora:wght@300;400;500;600;700;800&display=swap - * - * Radius: 0.25rem (sharp, angular, decisive) - * Hue: ~75 (golden/amber) - */ - -:root { - --font-sans: 'Sora', ui-sans-serif, system-ui, sans-serif; - --radius: 0.25rem; - --background: oklch(0.98 0.015 82); - --foreground: oklch(0.18 0.025 70); - --card: oklch(0.974 0.018 80); - --card-foreground: oklch(0.18 0.025 70); - --popover: oklch(0.98 0.015 82); - --popover-foreground: oklch(0.18 0.025 70); - --primary: oklch(0.62 0.17 75); - --primary-foreground: oklch(0.2 0.035 70); - --secondary: oklch(0.94 0.022 80); - --secondary-foreground: oklch(0.28 0.04 70); - --muted: oklch(0.935 0.02 80); - --muted-foreground: oklch(0.5 0.028 75); - --accent: oklch(0.92 0.026 78); - --accent-foreground: oklch(0.28 0.04 70); - --destructive: oklch(0.52 0.2 10); - --destructive-foreground: oklch(0.4 0.15 10); - --success: oklch(0.62 0.14 160); - --success-foreground: oklch(0.38 0.09 160); - --info: oklch(0.52 0.13 260); - --info-foreground: oklch(0.38 0.1 260); - --warning: oklch(0.72 0.17 70); - --warning-foreground: oklch(0.4 0.12 60); - --invert: oklch(0.21 0.025 70); - --invert-foreground: oklch(0.97 0.008 80); - --focus: oklch(0.6 0.17 75); - --focus-foreground: oklch(0.2 0.035 70); - --border: oklch(0.89 0.02 80); - --input: oklch(0.89 0.02 80); - --ring: oklch(0.62 0.08 75); - --chart-1: oklch(0.65 0.18 75); - --chart-2: oklch(0.55 0.17 45); - --chart-3: oklch(0.72 0.15 100); - --chart-4: oklch(0.5 0.14 20); - --chart-5: oklch(0.58 0.11 130); - --sidebar: oklch(0.968 0.018 80); - --sidebar-foreground: oklch(0.18 0.025 70); - --sidebar-primary: oklch(0.62 0.17 75); - --sidebar-primary-foreground: oklch(0.2 0.035 70); - --sidebar-accent: oklch(0.92 0.026 78); - --sidebar-accent-foreground: oklch(0.28 0.04 70); - --sidebar-border: oklch(0.89 0.02 80); - --sidebar-ring: oklch(0.62 0.08 75); -} - -.dark { - /* Dark gold — deep, warm sepia, golden highlights pop with high contrast */ - --background: oklch(0.13 0.02 72); - --foreground: oklch(0.93 0.012 80); - --card: oklch(0.18 0.025 72); - --card-foreground: oklch(0.93 0.012 80); - --popover: oklch(0.18 0.025 72); - --popover-foreground: oklch(0.93 0.012 80); - --primary: oklch(0.78 0.16 75); - --primary-foreground: oklch(0.13 0.035 70); - --secondary: oklch(0.24 0.022 72); - --secondary-foreground: oklch(0.93 0.012 80); - --muted: oklch(0.24 0.022 72); - --muted-foreground: oklch(0.62 0.028 75); - --accent: oklch(0.25 0.026 72); - --accent-foreground: oklch(0.93 0.012 80); - --destructive: oklch(0.65 0.17 10); - --destructive-foreground: oklch(0.52 0.2 10); - --success: oklch(0.68 0.14 160); - --success-foreground: oklch(0.56 0.12 160); - --info: oklch(0.6 0.13 260); - --info-foreground: oklch(0.5 0.15 260); - --warning: oklch(0.78 0.17 70); - --warning-foreground: oklch(0.65 0.15 65); - --invert: oklch(0.34 0.025 70); - --invert-foreground: oklch(0.97 0.008 80); - --focus: oklch(0.72 0.17 75); - --focus-foreground: oklch(0.2 0.035 70); - --border: oklch(1 0.01 80 / 11%); - --input: oklch(1 0.01 80 / 16%); - --ring: oklch(0.55 0.08 75); - --chart-1: oklch(0.75 0.18 75); - --chart-2: oklch(0.66 0.17 45); - --chart-3: oklch(0.8 0.15 100); - --chart-4: oklch(0.62 0.14 20); - --chart-5: oklch(0.7 0.11 130); - --sidebar: oklch(0.16 0.023 72); - --sidebar-foreground: oklch(0.93 0.012 80); - --sidebar-primary: oklch(0.78 0.16 75); - --sidebar-primary-foreground: oklch(0.2 0.035 70); - --sidebar-accent: oklch(0.25 0.026 72); - --sidebar-accent-foreground: oklch(0.93 0.012 80); - --sidebar-border: oklch(1 0.01 80 / 11%); - --sidebar-ring: oklch(0.55 0.08 75); -} diff --git a/packages/themes/src/console.css b/packages/themes/src/console.css new file mode 100644 index 0000000..b516eb2 --- /dev/null +++ b/packages/themes/src/console.css @@ -0,0 +1,116 @@ +/* + * Console — Technical + * Dense, low-chroma, cool-leaning but never cold. Mono headings. + * + * Fonts: 'Geist Mono' (headings) + 'Geist' (body) + * Load via Google Fonts, next/font, or self-host: + * https://fonts.googleapis.com/css2?family=Geist:wght@400..700&family=Geist+Mono:wght@400..700&display=swap + * + * Radius: 0.125rem + * Accent hue: ~235 · neutral hue: ~243 + * + * GENERATED by scripts/generate-themes.mjs — edit the anchors there, not + * this file. Values are solved against scripts/check-contrast.mjs. + */ + +:root { + --font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif; + --font-heading: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --radius: 0.125rem; + --leading-base: 1.4; + --text-base-size: 0.9375rem; + --background: oklch(96.5% 0.005 240); + --foreground: oklch(20% 0.01 245); + --card: oklch(98.1% 0.0048 243); + --card-foreground: oklch(20% 0.01 245); + --popover: oklch(98.1% 0.0048 243); + --popover-foreground: oklch(20% 0.01 245); + --primary: oklch(52% 0.11 235); + --primary-foreground: oklch(98% 0.015 235); + --secondary: oklch(94.5% 0.006 243); + --secondary-foreground: oklch(20% 0.01 245); + --muted: oklch(94.5% 0.006 243); + --muted-foreground: oklch(52.2% 0.006 243); + --accent: oklch(93.7% 0.018 235); + --accent-foreground: oklch(20% 0.01 245); + --destructive: oklch(54.1% 0.16 27); + --destructive-foreground: oklch(52% 0.144 27); + --success: oklch(51% 0.11 152); + --success-foreground: oklch(49.5% 0.099 152); + --info: oklch(52.2% 0.12 250); + --info-foreground: oklch(50.5% 0.108 250); + --warning: oklch(52.8% 0.13 75); + --warning-foreground: oklch(51% 0.117 75); + --invert: oklch(21% 0.006 243); + --invert-foreground: oklch(98% 0.0024 243); + --focus: oklch(55% 0.14 235); + --focus-foreground: oklch(99.7% 0.02 235); + --border: oklch(63.9% 0.006 243); + --input: oklch(63.9% 0.006 243); + --ring: oklch(63.6% 0.06 235); + --chart-1: oklch(55% 0.12 235); + --chart-2: oklch(56% 0.12 297); + --chart-3: oklch(57% 0.12 3); + --chart-4: oklch(58% 0.12 71); + --chart-5: oklch(59% 0.12 159); + --sidebar: oklch(94.9% 0.006 243); + --sidebar-foreground: oklch(20% 0.01 245); + --sidebar-primary: oklch(52% 0.11 235); + --sidebar-primary-foreground: oklch(98% 0.015 235); + --sidebar-accent: oklch(92.1% 0.018 235); + --sidebar-accent-foreground: oklch(20% 0.01 245); + --sidebar-border: oklch(62.7% 0.006 243); + --sidebar-ring: oklch(62.4% 0.06 235); +} + +.dark { + --font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif; + --font-heading: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --radius: 0.125rem; + --leading-base: 1.4; + --text-base-size: 0.9375rem; + --background: oklch(16% 0.005 245); + --foreground: oklch(91% 0.008 240); + --card: oklch(20.5% 0.0048 243); + --card-foreground: oklch(91% 0.008 240); + --popover: oklch(20.5% 0.0048 243); + --popover-foreground: oklch(91% 0.008 240); + --primary: oklch(70% 0.1 230); + --primary-foreground: oklch(16% 0.015 235); + --secondary: oklch(25% 0.006 243); + --secondary-foreground: oklch(91% 0.008 240); + --muted: oklch(25% 0.006 243); + --muted-foreground: oklch(63.4% 0.006 243); + --accent: oklch(29.5% 0.018 235); + --accent-foreground: oklch(91% 0.008 240); + --destructive: oklch(65.3% 0.16 27); + --destructive-foreground: oklch(65.3% 0.144 27); + --success: oklch(62.2% 0.11 152); + --success-foreground: oklch(62.2% 0.099 152); + --info: oklch(63.3% 0.12 250); + --info-foreground: oklch(63.3% 0.108 250); + --warning: oklch(64% 0.13 75); + --warning-foreground: oklch(64% 0.117 75); + --invert: oklch(37% 0.006 243); + --invert-foreground: oklch(98% 0.0024 243); + --focus: oklch(62% 0.14 235); + --focus-foreground: oklch(16% 0.02 235); + --border: oklch(48.6% 0.006 243); + --input: oklch(48.6% 0.006 243); + --ring: oklch(48.3% 0.06 235); + --chart-1: oklch(70% 0.11 235); + --chart-2: oklch(68.5% 0.11 297); + --chart-3: oklch(67% 0.11 3); + --chart-4: oklch(65.5% 0.11 71); + --chart-5: oklch(64% 0.11 159); + --sidebar: oklch(18.7% 0.006 243); + --sidebar-foreground: oklch(91% 0.008 240); + --sidebar-primary: oklch(70% 0.1 230); + --sidebar-primary-foreground: oklch(16% 0.015 235); + --sidebar-accent: oklch(23.2% 0.018 235); + --sidebar-accent-foreground: oklch(91% 0.008 240); + --sidebar-border: oklch(49.6% 0.006 243); + --sidebar-ring: oklch(49.4% 0.06 235); +} diff --git a/packages/themes/src/crimson.css b/packages/themes/src/crimson.css deleted file mode 100644 index 2fe4992..0000000 --- a/packages/themes/src/crimson.css +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Crimson — Brutalist Bold - * Zero radius, stark, high contrast, bold red. - * Feels like a striking editorial site or brutalist web design. - * - * Font: Instrument Sans (sharp, contemporary, confident) - * Load via Google Fonts, next/font, or self-host: - * https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&display=swap - * - * Radius: 0 (completely square, brutalist) - * Hue: ~18 (scarlet/crimson) - */ - -:root { - --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif; - --radius: 0; - --background: oklch(0.982 0.012 15); - --foreground: oklch(0.14 0.025 15); - --card: oklch(0.976 0.015 14); - --card-foreground: oklch(0.14 0.025 15); - --popover: oklch(0.982 0.012 15); - --popover-foreground: oklch(0.14 0.025 15); - --primary: oklch(0.5 0.2 18); - --primary-foreground: oklch(0.97 0.008 15); - --secondary: oklch(0.94 0.018 15); - --secondary-foreground: oklch(0.22 0.04 15); - --muted: oklch(0.935 0.016 15); - --muted-foreground: oklch(0.48 0.028 15); - --accent: oklch(0.92 0.022 12); - --accent-foreground: oklch(0.22 0.04 15); - --destructive: oklch(0.48 0.22 5); - --destructive-foreground: oklch(0.36 0.16 5); - --success: oklch(0.6 0.14 155); - --success-foreground: oklch(0.38 0.09 155); - --info: oklch(0.52 0.13 245); - --info-foreground: oklch(0.38 0.1 245); - --warning: oklch(0.74 0.16 55); - --warning-foreground: oklch(0.4 0.1 45); - --invert: oklch(0.18 0.025 15); - --invert-foreground: oklch(0.97 0.008 15); - --focus: oklch(0.5 0.2 18); - --focus-foreground: oklch(0.97 0.008 15); - --border: oklch(0.88 0.018 15); - --input: oklch(0.88 0.018 15); - --ring: oklch(0.58 0.1 18); - --chart-1: oklch(0.52 0.2 18); - --chart-2: oklch(0.45 0.16 350); - --chart-3: oklch(0.65 0.17 45); - --chart-4: oklch(0.4 0.1 30); - --chart-5: oklch(0.58 0.14 75); - --sidebar: oklch(0.968 0.015 15); - --sidebar-foreground: oklch(0.14 0.025 15); - --sidebar-primary: oklch(0.5 0.2 18); - --sidebar-primary-foreground: oklch(0.97 0.008 15); - --sidebar-accent: oklch(0.92 0.022 12); - --sidebar-accent-foreground: oklch(0.22 0.04 15); - --sidebar-border: oklch(0.88 0.018 15); - --sidebar-ring: oklch(0.58 0.1 18); -} - -.dark { - /* True black — deepest dark mode, stark contrast, bold red cuts through */ - --background: oklch(0.08 0.015 15); - --foreground: oklch(0.94 0.01 15); - --card: oklch(0.13 0.02 15); - --card-foreground: oklch(0.94 0.01 15); - --popover: oklch(0.13 0.02 15); - --popover-foreground: oklch(0.94 0.01 15); - --primary: oklch(0.68 0.2 18); - --primary-foreground: oklch(0.97 0.008 15); - --secondary: oklch(0.18 0.02 15); - --secondary-foreground: oklch(0.94 0.01 15); - --muted: oklch(0.18 0.02 15); - --muted-foreground: oklch(0.58 0.025 15); - --accent: oklch(0.19 0.025 12); - --accent-foreground: oklch(0.94 0.01 15); - --destructive: oklch(0.6 0.2 5); - --destructive-foreground: oklch(0.48 0.22 5); - --success: oklch(0.66 0.14 155); - --success-foreground: oklch(0.54 0.12 155); - --info: oklch(0.6 0.13 245); - --info-foreground: oklch(0.5 0.15 245); - --warning: oklch(0.78 0.16 55); - --warning-foreground: oklch(0.66 0.14 50); - --invert: oklch(0.28 0.025 15); - --invert-foreground: oklch(0.97 0.008 15); - --focus: oklch(0.6 0.2 18); - --focus-foreground: oklch(0.97 0.008 15); - --border: oklch(1 0.008 15 / 14%); - --input: oklch(1 0.008 15 / 18%); - --ring: oklch(0.52 0.1 18); - --chart-1: oklch(0.65 0.2 18); - --chart-2: oklch(0.6 0.16 350); - --chart-3: oklch(0.75 0.17 45); - --chart-4: oklch(0.55 0.1 30); - --chart-5: oklch(0.7 0.14 75); - --sidebar: oklch(0.11 0.018 15); - --sidebar-foreground: oklch(0.94 0.01 15); - --sidebar-primary: oklch(0.68 0.2 18); - --sidebar-primary-foreground: oklch(0.97 0.008 15); - --sidebar-accent: oklch(0.19 0.025 12); - --sidebar-accent-foreground: oklch(0.94 0.01 15); - --sidebar-border: oklch(1 0.008 15 / 14%); - --sidebar-ring: oklch(0.52 0.1 18); -} diff --git a/packages/themes/src/dark.css b/packages/themes/src/dark.css index e02ba62..c7c885a 100644 --- a/packages/themes/src/dark.css +++ b/packages/themes/src/dark.css @@ -14,19 +14,19 @@ --accent: oklch(0.27 0.008 55); --accent-foreground: oklch(0.96 0.005 75); --destructive: oklch(0.704 0.191 22.216); - --destructive-foreground: oklch(57.7% 0.245 27.325); + --destructive-foreground: oklch(63.9% 0.245 27.3); --success: oklch(69.6% 0.17 162.48); --success-foreground: oklch(59.6% 0.145 163.225); - --info: oklch(60.6% 0.25 292.717); - --info-foreground: oklch(54.1% 0.281 293.009); + --info: oklch(67.4% 0.25 292.7); + --info-foreground: oklch(64% 0.281 293); --warning: oklch(79.5% 0.184 86.047); --warning-foreground: oklch(68.1% 0.162 75.834); --invert: oklch(0.37 0.008 55); --invert-foreground: oklch(0.98 0.004 75); --focus: oklch(54.6% 0.245 262.881); --focus-foreground: oklch(98.5% 0 0); - --border: oklch(1 0.004 75 / 10%); - --input: oklch(1 0.004 75 / 15%); + --border: oklch(48.5% 0.004 75); + --input: oklch(48.5% 0.004 75); --ring: oklch(0.55 0.008 55); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); @@ -39,6 +39,6 @@ --sidebar-primary-foreground: oklch(0.98 0.004 75); --sidebar-accent: oklch(0.27 0.008 55); --sidebar-accent-foreground: oklch(0.96 0.005 75); - --sidebar-border: oklch(1 0.004 75 / 10%); + --sidebar-border: oklch(50.7% 0.004 75); --sidebar-ring: oklch(0.55 0.008 55); } diff --git a/packages/themes/src/forest.css b/packages/themes/src/forest.css deleted file mode 100644 index 791aef8..0000000 --- a/packages/themes/src/forest.css +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Forest — Organic Natural - * Serif display, balanced radius, deep emerald tones. - * Feels like a sustainable brand or editorial nature publication. - * - * Fonts: Fraunces (headings, optical-size serif) + Source Sans 3 (body, clean humanist) - * Load via Google Fonts, next/font, or self-host: - * https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700&family=Source+Sans+3:wght@400;500;600;700&display=swap - * - * Radius: 0.5rem (balanced, grounded) - * Hue: ~160 (emerald/jade) - */ - -:root { - --font-sans: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif; - --font-heading: 'Fraunces', ui-serif, Georgia, serif; - --radius: 0.5rem; - --background: oklch(0.98 0.015 152); - --foreground: oklch(0.17 0.025 160); - --card: oklch(0.974 0.018 150); - --card-foreground: oklch(0.17 0.025 160); - --popover: oklch(0.98 0.015 152); - --popover-foreground: oklch(0.17 0.025 160); - --primary: oklch(0.48 0.14 160); - --primary-foreground: oklch(0.97 0.008 155); - --secondary: oklch(0.94 0.02 155); - --secondary-foreground: oklch(0.25 0.04 160); - --muted: oklch(0.935 0.018 155); - --muted-foreground: oklch(0.5 0.028 158); - --accent: oklch(0.92 0.025 150); - --accent-foreground: oklch(0.25 0.04 160); - --destructive: oklch(0.52 0.18 20); - --destructive-foreground: oklch(0.4 0.14 20); - --success: oklch(0.55 0.16 145); - --success-foreground: oklch(0.37 0.1 145); - --info: oklch(0.55 0.11 240); - --info-foreground: oklch(0.38 0.08 240); - --warning: oklch(0.74 0.15 90); - --warning-foreground: oklch(0.42 0.1 80); - --invert: oklch(0.21 0.025 160); - --invert-foreground: oklch(0.97 0.008 155); - --focus: oklch(0.52 0.14 165); - --focus-foreground: oklch(0.97 0.008 155); - --border: oklch(0.89 0.018 155); - --input: oklch(0.89 0.018 155); - --ring: oklch(0.58 0.06 158); - --chart-1: oklch(0.52 0.15 160); - --chart-2: oklch(0.45 0.12 130); - --chart-3: oklch(0.68 0.14 120); - --chart-4: oklch(0.55 0.1 190); - --chart-5: oklch(0.62 0.13 90); - --sidebar: oklch(0.968 0.017 152); - --sidebar-foreground: oklch(0.17 0.025 160); - --sidebar-primary: oklch(0.48 0.14 160); - --sidebar-primary-foreground: oklch(0.97 0.008 155); - --sidebar-accent: oklch(0.92 0.025 150); - --sidebar-accent-foreground: oklch(0.25 0.04 160); - --sidebar-border: oklch(0.89 0.018 155); - --sidebar-ring: oklch(0.58 0.06 158); -} - -.dark { - /* Dense canopy — medium-dark, visible green tint, moonlit feel */ - --background: oklch(0.14 0.025 162); - --foreground: oklch(0.93 0.012 152); - --card: oklch(0.19 0.03 162); - --card-foreground: oklch(0.93 0.012 152); - --popover: oklch(0.19 0.03 162); - --popover-foreground: oklch(0.93 0.012 152); - --primary: oklch(0.7 0.13 158); - --primary-foreground: oklch(0.14 0.03 160); - --secondary: oklch(0.25 0.025 162); - --secondary-foreground: oklch(0.93 0.012 152); - --muted: oklch(0.25 0.025 162); - --muted-foreground: oklch(0.62 0.028 157); - --accent: oklch(0.26 0.03 158); - --accent-foreground: oklch(0.93 0.012 152); - --destructive: oklch(0.65 0.16 20); - --destructive-foreground: oklch(0.52 0.18 20); - --success: oklch(0.62 0.15 145); - --success-foreground: oklch(0.52 0.13 145); - --info: oklch(0.62 0.11 240); - --info-foreground: oklch(0.5 0.13 240); - --warning: oklch(0.78 0.15 90); - --warning-foreground: oklch(0.66 0.13 85); - --invert: oklch(0.34 0.028 160); - --invert-foreground: oklch(0.97 0.008 155); - --focus: oklch(0.58 0.14 165); - --focus-foreground: oklch(0.97 0.008 155); - --border: oklch(1 0.01 155 / 10%); - --input: oklch(1 0.01 155 / 15%); - --ring: oklch(0.52 0.06 158); - --chart-1: oklch(0.65 0.15 160); - --chart-2: oklch(0.58 0.12 130); - --chart-3: oklch(0.76 0.14 120); - --chart-4: oklch(0.68 0.1 190); - --chart-5: oklch(0.72 0.13 90); - --sidebar: oklch(0.17 0.028 162); - --sidebar-foreground: oklch(0.93 0.012 152); - --sidebar-primary: oklch(0.7 0.13 158); - --sidebar-primary-foreground: oklch(0.97 0.008 155); - --sidebar-accent: oklch(0.26 0.03 158); - --sidebar-accent-foreground: oklch(0.93 0.012 152); - --sidebar-border: oklch(1 0.01 155 / 10%); - --sidebar-ring: oklch(0.52 0.06 158); -} diff --git a/packages/themes/src/light.css b/packages/themes/src/light.css index 19739aa..1772b5a 100644 --- a/packages/themes/src/light.css +++ b/packages/themes/src/light.css @@ -13,24 +13,24 @@ --secondary: oklch(0.96 0.007 75); --secondary-foreground: oklch(0.22 0.008 55); --muted: oklch(0.955 0.006 75); - --muted-foreground: oklch(0.55 0.01 55); + --muted-foreground: oklch(53.1% 0.01 55); --accent: oklch(0.96 0.007 75); --accent-foreground: oklch(0.22 0.008 55); - --destructive: oklch(0.577 0.245 27.325); + --destructive: oklch(55.5% 0.245 27.3); --destructive-foreground: oklch(44.4% 0.177 26.899); - --success: oklch(69.6% 0.17 162.48); + --success: oklch(49.9% 0.17 162.5); --success-foreground: oklch(37.8% 0.077 168.94); - --info: oklch(60.6% 0.25 292.717); + --info: oklch(56% 0.25 292.7); --info-foreground: oklch(38% 0.189 293.745); - --warning: oklch(79.5% 0.184 86.047); + --warning: oklch(53.3% 0.184 86); --warning-foreground: oklch(42.1% 0.095 57.708); --invert: oklch(0.21 0.006 55); --invert-foreground: oklch(0.98 0.004 75); --focus: oklch(62.3% 0.214 259.815); - --focus-foreground: oklch(98.5% 0 0); - --border: oklch(0.912 0.006 75); - --input: oklch(0.912 0.006 75); - --ring: oklch(0.7 0.008 55); + --focus-foreground: oklch(22.4% 0 0); + --border: oklch(66.1% 0.006 75); + --input: oklch(66.1% 0.006 75); + --ring: oklch(66.2% 0.008 55); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); @@ -42,6 +42,6 @@ --sidebar-primary-foreground: oklch(0.98 0.004 75); --sidebar-accent: oklch(0.96 0.007 75); --sidebar-accent-foreground: oklch(0.22 0.008 55); - --sidebar-border: oklch(0.912 0.006 75); - --sidebar-ring: oklch(0.7 0.008 55); + --sidebar-border: oklch(65% 0.006 75); + --sidebar-ring: oklch(65.1% 0.008 55); } diff --git a/packages/themes/src/ocean.css b/packages/themes/src/ocean.css deleted file mode 100644 index dc0c671..0000000 --- a/packages/themes/src/ocean.css +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Ocean — Smooth Aquatic - * Rounded, generous spacing, deep teal tones. - * Feels like a modern wellness or travel app. - * - * Font: Plus Jakarta Sans (geometric, friendly) - * Load via Google Fonts, next/font, or self-host: - * https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap - * - * Radius: 0.875rem (generous, smooth) - * Hue: ~200 (deep teal/cyan) - */ - -:root { - --font-sans: 'Plus Jakarta Sans', ui-sans-serif, system-ui, sans-serif; - --radius: 0.875rem; - --background: oklch(0.982 0.015 210); - --foreground: oklch(0.17 0.025 220); - --card: oklch(0.976 0.018 208); - --card-foreground: oklch(0.17 0.025 220); - --popover: oklch(0.982 0.015 210); - --popover-foreground: oklch(0.17 0.025 220); - --primary: oklch(0.52 0.115 198); - --primary-foreground: oklch(0.97 0.008 200); - --secondary: oklch(0.94 0.02 210); - --secondary-foreground: oklch(0.25 0.04 210); - --muted: oklch(0.935 0.018 210); - --muted-foreground: oklch(0.5 0.03 215); - --accent: oklch(0.92 0.025 205); - --accent-foreground: oklch(0.25 0.04 210); - --destructive: oklch(0.55 0.2 15); - --destructive-foreground: oklch(0.42 0.15 15); - --success: oklch(0.62 0.14 170); - --success-foreground: oklch(0.38 0.09 170); - --info: oklch(0.52 0.13 230); - --info-foreground: oklch(0.38 0.1 230); - --warning: oklch(0.76 0.15 80); - --warning-foreground: oklch(0.42 0.1 65); - --invert: oklch(0.21 0.025 220); - --invert-foreground: oklch(0.97 0.008 200); - --focus: oklch(0.55 0.12 220); - --focus-foreground: oklch(0.97 0.008 200); - --border: oklch(0.89 0.02 210); - --input: oklch(0.89 0.02 210); - --ring: oklch(0.6 0.06 205); - --chart-1: oklch(0.55 0.13 200); - --chart-2: oklch(0.45 0.14 230); - --chart-3: oklch(0.68 0.13 175); - --chart-4: oklch(0.55 0.14 260); - --chart-5: oklch(0.72 0.1 170); - --sidebar: oklch(0.97 0.018 210); - --sidebar-foreground: oklch(0.17 0.025 220); - --sidebar-primary: oklch(0.52 0.115 198); - --sidebar-primary-foreground: oklch(0.97 0.008 200); - --sidebar-accent: oklch(0.92 0.025 205); - --sidebar-accent-foreground: oklch(0.25 0.04 210); - --sidebar-border: oklch(0.89 0.02 210); - --sidebar-ring: oklch(0.6 0.06 205); -} - -.dark { - /* Deep abyss — very dark, high chroma surfaces, bioluminescent teal accents */ - --background: oklch(0.13 0.03 218); - --foreground: oklch(0.93 0.012 205); - --card: oklch(0.18 0.035 218); - --card-foreground: oklch(0.93 0.012 205); - --popover: oklch(0.18 0.035 218); - --popover-foreground: oklch(0.93 0.012 205); - --primary: oklch(0.72 0.12 195); - --primary-foreground: oklch(0.13 0.03 210); - --secondary: oklch(0.24 0.03 218); - --secondary-foreground: oklch(0.93 0.012 205); - --muted: oklch(0.24 0.03 218); - --muted-foreground: oklch(0.62 0.03 210); - --accent: oklch(0.25 0.035 212); - --accent-foreground: oklch(0.93 0.012 205); - --destructive: oklch(0.68 0.17 15); - --destructive-foreground: oklch(0.55 0.2 15); - --success: oklch(0.68 0.14 170); - --success-foreground: oklch(0.56 0.12 170); - --info: oklch(0.6 0.13 230); - --info-foreground: oklch(0.5 0.15 230); - --warning: oklch(0.78 0.15 80); - --warning-foreground: oklch(0.66 0.13 72); - --invert: oklch(0.34 0.03 218); - --invert-foreground: oklch(0.97 0.008 200); - --focus: oklch(0.58 0.14 220); - --focus-foreground: oklch(0.97 0.008 200); - --border: oklch(1 0.01 210 / 10%); - --input: oklch(1 0.01 210 / 15%); - --ring: oklch(0.55 0.06 205); - --chart-1: oklch(0.68 0.13 200); - --chart-2: oklch(0.58 0.14 230); - --chart-3: oklch(0.75 0.13 175); - --chart-4: oklch(0.68 0.14 260); - --chart-5: oklch(0.8 0.1 170); - --sidebar: oklch(0.16 0.032 218); - --sidebar-foreground: oklch(0.93 0.012 205); - --sidebar-primary: oklch(0.72 0.12 195); - --sidebar-primary-foreground: oklch(0.97 0.008 200); - --sidebar-accent: oklch(0.25 0.035 212); - --sidebar-accent-foreground: oklch(0.93 0.012 205); - --sidebar-border: oklch(1 0.01 210 / 10%); - --sidebar-ring: oklch(0.55 0.06 205); -} diff --git a/packages/themes/src/oxide.css b/packages/themes/src/oxide.css deleted file mode 100644 index bc967a6..0000000 --- a/packages/themes/src/oxide.css +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Oxide — Editorial Craft - * Serif-forward, squared-off, warm copper tones. - * Feels like a well-made magazine or pottery studio. - * - * Fonts: Bitter (headings) + DM Sans (body) - * Load via Google Fonts, next/font, or self-host: - * https://fonts.googleapis.com/css2?family=Bitter:wght@400;500;600;700&family=DM+Sans:wght@400;500;600;700&display=swap - * - * Radius: 0.375rem (modest, angular) - * Hue: ~35 (copper/terracotta) - */ - -:root { - --font-sans: 'DM Sans', ui-sans-serif, system-ui, sans-serif; - --font-heading: 'Bitter', ui-serif, Georgia, serif; - --radius: 0.375rem; - --background: oklch(0.978 0.016 58); - --foreground: oklch(0.18 0.03 35); - --card: oklch(0.972 0.018 56); - --card-foreground: oklch(0.18 0.03 35); - --popover: oklch(0.978 0.016 58); - --popover-foreground: oklch(0.18 0.03 35); - --primary: oklch(0.48 0.145 35); - --primary-foreground: oklch(0.97 0.01 40); - --secondary: oklch(0.94 0.022 55); - --secondary-foreground: oklch(0.28 0.04 35); - --muted: oklch(0.935 0.02 55); - --muted-foreground: oklch(0.5 0.035 40); - --accent: oklch(0.92 0.028 50); - --accent-foreground: oklch(0.28 0.04 35); - --destructive: oklch(0.52 0.2 22); - --destructive-foreground: oklch(0.4 0.15 22); - --success: oklch(0.6 0.14 155); - --success-foreground: oklch(0.38 0.09 155); - --info: oklch(0.55 0.11 250); - --info-foreground: oklch(0.38 0.08 250); - --warning: oklch(0.74 0.15 65); - --warning-foreground: oklch(0.42 0.1 55); - --invert: oklch(0.21 0.025 35); - --invert-foreground: oklch(0.97 0.01 40); - --focus: oklch(0.55 0.14 35); - --focus-foreground: oklch(0.97 0.01 40); - --border: oklch(0.88 0.022 55); - --input: oklch(0.88 0.022 55); - --ring: oklch(0.58 0.08 38); - --chart-1: oklch(0.55 0.16 35); - --chart-2: oklch(0.48 0.14 15); - --chart-3: oklch(0.72 0.16 75); - --chart-4: oklch(0.45 0.08 55); - --chart-5: oklch(0.6 0.14 100); - --sidebar: oklch(0.965 0.02 55); - --sidebar-foreground: oklch(0.18 0.03 35); - --sidebar-primary: oklch(0.48 0.145 35); - --sidebar-primary-foreground: oklch(0.97 0.01 40); - --sidebar-accent: oklch(0.92 0.028 50); - --sidebar-accent-foreground: oklch(0.28 0.04 35); - --sidebar-border: oklch(0.88 0.022 55); - --sidebar-ring: oklch(0.58 0.08 38); -} - -.dark { - /* Warm cave — highest surface lightness, amber glow, visible terracotta tint */ - --background: oklch(0.17 0.025 35); - --foreground: oklch(0.92 0.016 50); - --card: oklch(0.22 0.03 35); - --card-foreground: oklch(0.92 0.016 50); - --popover: oklch(0.22 0.03 35); - --popover-foreground: oklch(0.92 0.016 50); - --primary: oklch(0.74 0.13 35); - --primary-foreground: oklch(0.18 0.03 35); - --secondary: oklch(0.28 0.03 35); - --secondary-foreground: oklch(0.92 0.016 50); - --muted: oklch(0.28 0.03 35); - --muted-foreground: oklch(0.62 0.04 42); - --accent: oklch(0.29 0.035 35); - --accent-foreground: oklch(0.92 0.016 50); - --destructive: oklch(0.65 0.17 22); - --destructive-foreground: oklch(0.52 0.2 22); - --success: oklch(0.66 0.14 155); - --success-foreground: oklch(0.55 0.12 155); - --info: oklch(0.6 0.11 250); - --info-foreground: oklch(0.5 0.13 250); - --warning: oklch(0.76 0.15 65); - --warning-foreground: oklch(0.64 0.13 60); - --invert: oklch(0.36 0.03 35); - --invert-foreground: oklch(0.97 0.01 40); - --focus: oklch(0.65 0.13 35); - --focus-foreground: oklch(0.97 0.01 40); - --border: oklch(1 0.012 50 / 11%); - --input: oklch(1 0.012 50 / 16%); - --ring: oklch(0.55 0.08 38); - --chart-1: oklch(0.68 0.16 35); - --chart-2: oklch(0.6 0.14 15); - --chart-3: oklch(0.8 0.16 75); - --chart-4: oklch(0.58 0.08 55); - --chart-5: oklch(0.7 0.14 100); - --sidebar: oklch(0.2 0.028 35); - --sidebar-foreground: oklch(0.92 0.016 50); - --sidebar-primary: oklch(0.74 0.13 35); - --sidebar-primary-foreground: oklch(0.97 0.01 40); - --sidebar-accent: oklch(0.29 0.035 35); - --sidebar-accent-foreground: oklch(0.92 0.016 50); - --sidebar-border: oklch(1 0.012 50 / 11%); - --sidebar-ring: oklch(0.55 0.08 38); -} diff --git a/packages/themes/src/presets.css b/packages/themes/src/presets.css index 0a72dec..f99ecce 100644 --- a/packages/themes/src/presets.css +++ b/packages/themes/src/presets.css @@ -13,765 +13,307 @@ * [data-theme="brand"].dark, .dark [data-theme="brand"] { ... } */ -/* Oxide */ -[data-theme="oxide"] { - --font-sans: 'DM Sans', ui-sans-serif, system-ui, sans-serif; - --font-heading: 'Bitter', ui-serif, Georgia, serif; - --radius: 0.375rem; - --background: oklch(0.978 0.016 58); - --foreground: oklch(0.18 0.03 35); - --card: oklch(0.972 0.018 56); - --card-foreground: oklch(0.18 0.03 35); - --popover: oklch(0.978 0.016 58); - --popover-foreground: oklch(0.18 0.03 35); - --primary: oklch(0.48 0.145 35); - --primary-foreground: oklch(0.97 0.01 40); - --secondary: oklch(0.94 0.022 55); - --secondary-foreground: oklch(0.28 0.04 35); - --muted: oklch(0.935 0.02 55); - --muted-foreground: oklch(0.5 0.035 40); - --accent: oklch(0.92 0.028 50); - --accent-foreground: oklch(0.28 0.04 35); - --destructive: oklch(0.52 0.2 22); - --destructive-foreground: oklch(0.4 0.15 22); - --success: oklch(0.6 0.14 155); - --success-foreground: oklch(0.38 0.09 155); - --info: oklch(0.55 0.11 250); - --info-foreground: oklch(0.38 0.08 250); - --warning: oklch(0.74 0.15 65); - --warning-foreground: oklch(0.42 0.1 55); - --invert: oklch(0.21 0.025 35); - --invert-foreground: oklch(0.97 0.01 40); - --focus: oklch(0.55 0.14 35); - --focus-foreground: oklch(0.97 0.01 40); - --border: oklch(0.88 0.022 55); - --input: oklch(0.88 0.022 55); - --ring: oklch(0.58 0.08 38); - --chart-1: oklch(0.55 0.16 35); - --chart-2: oklch(0.48 0.14 15); - --chart-3: oklch(0.72 0.16 75); - --chart-4: oklch(0.45 0.08 55); - --chart-5: oklch(0.6 0.14 100); - --sidebar: oklch(0.965 0.02 55); - --sidebar-foreground: oklch(0.18 0.03 35); - --sidebar-primary: oklch(0.48 0.145 35); - --sidebar-primary-foreground: oklch(0.97 0.01 40); - --sidebar-accent: oklch(0.92 0.028 50); - --sidebar-accent-foreground: oklch(0.28 0.04 35); - --sidebar-border: oklch(0.88 0.022 55); - --sidebar-ring: oklch(0.58 0.08 38); -} - -[data-theme="oxide"].dark, -.dark [data-theme="oxide"] { - /* Warm cave — highest surface lightness, amber glow, visible terracotta tint */ - --background: oklch(0.17 0.025 35); - --foreground: oklch(0.92 0.016 50); - --card: oklch(0.22 0.03 35); - --card-foreground: oklch(0.92 0.016 50); - --popover: oklch(0.22 0.03 35); - --popover-foreground: oklch(0.92 0.016 50); - --primary: oklch(0.74 0.13 35); - --primary-foreground: oklch(0.18 0.03 35); - --secondary: oklch(0.28 0.03 35); - --secondary-foreground: oklch(0.92 0.016 50); - --muted: oklch(0.28 0.03 35); - --muted-foreground: oklch(0.62 0.04 42); - --accent: oklch(0.29 0.035 35); - --accent-foreground: oklch(0.92 0.016 50); - --destructive: oklch(0.65 0.17 22); - --destructive-foreground: oklch(0.52 0.2 22); - --success: oklch(0.66 0.14 155); - --success-foreground: oklch(0.55 0.12 155); - --info: oklch(0.6 0.11 250); - --info-foreground: oklch(0.5 0.13 250); - --warning: oklch(0.76 0.15 65); - --warning-foreground: oklch(0.64 0.13 60); - --invert: oklch(0.36 0.03 35); - --invert-foreground: oklch(0.97 0.01 40); - --focus: oklch(0.65 0.13 35); - --focus-foreground: oklch(0.97 0.01 40); - --border: oklch(1 0.012 50 / 11%); - --input: oklch(1 0.012 50 / 16%); - --ring: oklch(0.55 0.08 38); - --chart-1: oklch(0.68 0.16 35); - --chart-2: oklch(0.6 0.14 15); - --chart-3: oklch(0.8 0.16 75); - --chart-4: oklch(0.58 0.08 55); - --chart-5: oklch(0.7 0.14 100); - --sidebar: oklch(0.2 0.028 35); - --sidebar-foreground: oklch(0.92 0.016 50); - --sidebar-primary: oklch(0.74 0.13 35); - --sidebar-primary-foreground: oklch(0.97 0.01 40); - --sidebar-accent: oklch(0.29 0.035 35); - --sidebar-accent-foreground: oklch(0.92 0.016 50); - --sidebar-border: oklch(1 0.012 50 / 11%); - --sidebar-ring: oklch(0.55 0.08 38); -} - -/* Ocean */ -[data-theme="ocean"] { - --font-sans: 'Plus Jakarta Sans', ui-sans-serif, system-ui, sans-serif; - --radius: 0.875rem; - --background: oklch(0.982 0.015 210); - --foreground: oklch(0.17 0.025 220); - --card: oklch(0.976 0.018 208); - --card-foreground: oklch(0.17 0.025 220); - --popover: oklch(0.982 0.015 210); - --popover-foreground: oklch(0.17 0.025 220); - --primary: oklch(0.52 0.115 198); - --primary-foreground: oklch(0.97 0.008 200); - --secondary: oklch(0.94 0.02 210); - --secondary-foreground: oklch(0.25 0.04 210); - --muted: oklch(0.935 0.018 210); - --muted-foreground: oklch(0.5 0.03 215); - --accent: oklch(0.92 0.025 205); - --accent-foreground: oklch(0.25 0.04 210); - --destructive: oklch(0.55 0.2 15); - --destructive-foreground: oklch(0.42 0.15 15); - --success: oklch(0.62 0.14 170); - --success-foreground: oklch(0.38 0.09 170); - --info: oklch(0.52 0.13 230); - --info-foreground: oklch(0.38 0.1 230); - --warning: oklch(0.76 0.15 80); - --warning-foreground: oklch(0.42 0.1 65); - --invert: oklch(0.21 0.025 220); - --invert-foreground: oklch(0.97 0.008 200); - --focus: oklch(0.55 0.12 220); - --focus-foreground: oklch(0.97 0.008 200); - --border: oklch(0.89 0.02 210); - --input: oklch(0.89 0.02 210); - --ring: oklch(0.6 0.06 205); - --chart-1: oklch(0.55 0.13 200); - --chart-2: oklch(0.45 0.14 230); - --chart-3: oklch(0.68 0.13 175); - --chart-4: oklch(0.55 0.14 260); - --chart-5: oklch(0.72 0.1 170); - --sidebar: oklch(0.97 0.018 210); - --sidebar-foreground: oklch(0.17 0.025 220); - --sidebar-primary: oklch(0.52 0.115 198); - --sidebar-primary-foreground: oklch(0.97 0.008 200); - --sidebar-accent: oklch(0.92 0.025 205); - --sidebar-accent-foreground: oklch(0.25 0.04 210); - --sidebar-border: oklch(0.89 0.02 210); - --sidebar-ring: oklch(0.6 0.06 205); -} - -[data-theme="ocean"].dark, -.dark [data-theme="ocean"] { - /* Deep abyss — very dark, high chroma surfaces, bioluminescent teal accents */ - --background: oklch(0.13 0.03 218); - --foreground: oklch(0.93 0.012 205); - --card: oklch(0.18 0.035 218); - --card-foreground: oklch(0.93 0.012 205); - --popover: oklch(0.18 0.035 218); - --popover-foreground: oklch(0.93 0.012 205); - --primary: oklch(0.72 0.12 195); - --primary-foreground: oklch(0.13 0.03 210); - --secondary: oklch(0.24 0.03 218); - --secondary-foreground: oklch(0.93 0.012 205); - --muted: oklch(0.24 0.03 218); - --muted-foreground: oklch(0.62 0.03 210); - --accent: oklch(0.25 0.035 212); - --accent-foreground: oklch(0.93 0.012 205); - --destructive: oklch(0.68 0.17 15); - --destructive-foreground: oklch(0.55 0.2 15); - --success: oklch(0.68 0.14 170); - --success-foreground: oklch(0.56 0.12 170); - --info: oklch(0.6 0.13 230); - --info-foreground: oklch(0.5 0.15 230); - --warning: oklch(0.78 0.15 80); - --warning-foreground: oklch(0.66 0.13 72); - --invert: oklch(0.34 0.03 218); - --invert-foreground: oklch(0.97 0.008 200); - --focus: oklch(0.58 0.14 220); - --focus-foreground: oklch(0.97 0.008 200); - --border: oklch(1 0.01 210 / 10%); - --input: oklch(1 0.01 210 / 15%); - --ring: oklch(0.55 0.06 205); - --chart-1: oklch(0.68 0.13 200); - --chart-2: oklch(0.58 0.14 230); - --chart-3: oklch(0.75 0.13 175); - --chart-4: oklch(0.68 0.14 260); - --chart-5: oklch(0.8 0.1 170); - --sidebar: oklch(0.16 0.032 218); - --sidebar-foreground: oklch(0.93 0.012 205); - --sidebar-primary: oklch(0.72 0.12 195); - --sidebar-primary-foreground: oklch(0.97 0.008 200); - --sidebar-accent: oklch(0.25 0.035 212); - --sidebar-accent-foreground: oklch(0.93 0.012 205); - --sidebar-border: oklch(1 0.01 210 / 10%); - --sidebar-ring: oklch(0.55 0.06 205); -} - -/* Violet */ -[data-theme="violet"] { - --font-sans: 'Outfit', ui-sans-serif, system-ui, sans-serif; - --radius: 1rem; - --background: oklch(0.98 0.016 292); - --foreground: oklch(0.18 0.03 285); - --card: oklch(0.974 0.02 290); - --card-foreground: oklch(0.18 0.03 285); - --popover: oklch(0.98 0.016 292); - --popover-foreground: oklch(0.18 0.03 285); - --primary: oklch(0.52 0.2 285); - --primary-foreground: oklch(0.97 0.01 285); - --secondary: oklch(0.94 0.025 290); - --secondary-foreground: oklch(0.28 0.05 285); - --muted: oklch(0.935 0.022 290); - --muted-foreground: oklch(0.5 0.035 288); - --accent: oklch(0.92 0.03 288); - --accent-foreground: oklch(0.28 0.05 285); - --destructive: oklch(0.55 0.2 355); - --destructive-foreground: oklch(0.4 0.15 355); - --success: oklch(0.62 0.14 155); - --success-foreground: oklch(0.38 0.09 155); - --info: oklch(0.52 0.18 275); - --info-foreground: oklch(0.38 0.14 275); - --warning: oklch(0.76 0.15 70); - --warning-foreground: oklch(0.42 0.1 60); - --invert: oklch(0.22 0.03 285); - --invert-foreground: oklch(0.97 0.01 285); - --focus: oklch(0.55 0.2 285); - --focus-foreground: oklch(0.97 0.01 285); - --border: oklch(0.89 0.022 290); - --input: oklch(0.89 0.022 290); - --ring: oklch(0.6 0.1 285); - --chart-1: oklch(0.55 0.2 285); - --chart-2: oklch(0.58 0.18 320); - --chart-3: oklch(0.48 0.16 260); - --chart-4: oklch(0.68 0.16 350); - --chart-5: oklch(0.65 0.12 240); - --sidebar: oklch(0.968 0.02 290); - --sidebar-foreground: oklch(0.18 0.03 285); - --sidebar-primary: oklch(0.52 0.2 285); - --sidebar-primary-foreground: oklch(0.97 0.01 285); - --sidebar-accent: oklch(0.92 0.03 288); - --sidebar-accent-foreground: oklch(0.28 0.05 285); - --sidebar-border: oklch(0.89 0.022 290); - --sidebar-ring: oklch(0.6 0.1 285); -} - -[data-theme="violet"].dark, -.dark [data-theme="violet"] { - /* Midnight purple — deep, high chroma, vivid neon accents */ - --background: oklch(0.12 0.035 285); - --foreground: oklch(0.93 0.014 290); - --card: oklch(0.17 0.04 285); - --card-foreground: oklch(0.93 0.014 290); - --popover: oklch(0.17 0.04 285); - --popover-foreground: oklch(0.93 0.014 290); - --primary: oklch(0.72 0.18 285); - --primary-foreground: oklch(0.12 0.04 285); - --secondary: oklch(0.24 0.035 285); - --secondary-foreground: oklch(0.93 0.014 290); - --muted: oklch(0.24 0.035 285); - --muted-foreground: oklch(0.62 0.04 288); - --accent: oklch(0.25 0.04 285); - --accent-foreground: oklch(0.93 0.014 290); - --destructive: oklch(0.68 0.17 355); - --destructive-foreground: oklch(0.55 0.2 355); - --success: oklch(0.68 0.14 155); - --success-foreground: oklch(0.56 0.12 155); - --info: oklch(0.6 0.18 275); - --info-foreground: oklch(0.5 0.2 275); - --warning: oklch(0.78 0.15 70); - --warning-foreground: oklch(0.66 0.13 65); - --invert: oklch(0.34 0.04 285); - --invert-foreground: oklch(0.97 0.01 285); - --focus: oklch(0.6 0.2 285); - --focus-foreground: oklch(0.97 0.01 285); - --border: oklch(1 0.012 290 / 10%); - --input: oklch(1 0.012 290 / 15%); - --ring: oklch(0.55 0.1 285); - --chart-1: oklch(0.68 0.2 285); - --chart-2: oklch(0.7 0.18 320); - --chart-3: oklch(0.6 0.18 260); - --chart-4: oklch(0.76 0.16 350); - --chart-5: oklch(0.75 0.12 240); - --sidebar: oklch(0.15 0.038 285); - --sidebar-foreground: oklch(0.93 0.014 290); - --sidebar-primary: oklch(0.72 0.18 285); - --sidebar-primary-foreground: oklch(0.97 0.01 285); - --sidebar-accent: oklch(0.25 0.04 285); - --sidebar-accent-foreground: oklch(0.93 0.014 290); - --sidebar-border: oklch(1 0.012 290 / 10%); - --sidebar-ring: oklch(0.55 0.1 285); -} - -/* Forest */ -[data-theme="forest"] { +/* Press */ +[data-theme="press"] { --font-sans: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif; - --font-heading: 'Fraunces', ui-serif, Georgia, serif; + --font-heading: 'Source Serif 4', ui-serif, Georgia, serif; + --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; --radius: 0.5rem; - --background: oklch(0.98 0.015 152); - --foreground: oklch(0.17 0.025 160); - --card: oklch(0.974 0.018 150); - --card-foreground: oklch(0.17 0.025 160); - --popover: oklch(0.98 0.015 152); - --popover-foreground: oklch(0.17 0.025 160); - --primary: oklch(0.48 0.14 160); - --primary-foreground: oklch(0.97 0.008 155); - --secondary: oklch(0.94 0.02 155); - --secondary-foreground: oklch(0.25 0.04 160); - --muted: oklch(0.935 0.018 155); - --muted-foreground: oklch(0.5 0.028 158); - --accent: oklch(0.92 0.025 150); - --accent-foreground: oklch(0.25 0.04 160); - --destructive: oklch(0.52 0.18 20); - --destructive-foreground: oklch(0.4 0.14 20); - --success: oklch(0.55 0.16 145); - --success-foreground: oklch(0.37 0.1 145); - --info: oklch(0.55 0.11 240); - --info-foreground: oklch(0.38 0.08 240); - --warning: oklch(0.74 0.15 90); - --warning-foreground: oklch(0.42 0.1 80); - --invert: oklch(0.21 0.025 160); - --invert-foreground: oklch(0.97 0.008 155); - --focus: oklch(0.52 0.14 165); - --focus-foreground: oklch(0.97 0.008 155); - --border: oklch(0.89 0.018 155); - --input: oklch(0.89 0.018 155); - --ring: oklch(0.58 0.06 158); - --chart-1: oklch(0.52 0.15 160); - --chart-2: oklch(0.45 0.12 130); - --chart-3: oklch(0.68 0.14 120); - --chart-4: oklch(0.55 0.1 190); - --chart-5: oklch(0.62 0.13 90); - --sidebar: oklch(0.968 0.017 152); - --sidebar-foreground: oklch(0.17 0.025 160); - --sidebar-primary: oklch(0.48 0.14 160); - --sidebar-primary-foreground: oklch(0.97 0.008 155); - --sidebar-accent: oklch(0.92 0.025 150); - --sidebar-accent-foreground: oklch(0.25 0.04 160); - --sidebar-border: oklch(0.89 0.018 155); - --sidebar-ring: oklch(0.58 0.06 158); + --background: oklch(97.5% 0.012 85); + --foreground: oklch(16% 0.015 60); + --card: oklch(99.1% 0.008 70); + --card-foreground: oklch(16% 0.015 60); + --popover: oklch(99.1% 0.008 70); + --popover-foreground: oklch(16% 0.015 60); + --primary: oklch(45% 0.12 25); + --primary-foreground: oklch(98% 0.015 25); + --secondary: oklch(95.5% 0.01 70); + --secondary-foreground: oklch(16% 0.015 60); + --muted: oklch(95.5% 0.01 70); + --muted-foreground: oklch(52.9% 0.01 70); + --accent: oklch(94.7% 0.018 25); + --accent-foreground: oklch(16% 0.015 60); + --destructive: oklch(54.8% 0.16 27); + --destructive-foreground: oklch(52.7% 0.144 27); + --success: oklch(51.6% 0.11 152); + --success-foreground: oklch(50.1% 0.099 152); + --info: oklch(52.8% 0.12 250); + --info-foreground: oklch(51.1% 0.108 250); + --warning: oklch(53.4% 0.13 75); + --warning-foreground: oklch(51.7% 0.117 75); + --invert: oklch(21% 0.01 70); + --invert-foreground: oklch(98% 0.004 70); + --focus: oklch(55% 0.14 25); + --focus-foreground: oklch(98% 0.015 25); + --border: oklch(64.7% 0.01 70); + --input: oklch(64.7% 0.01 70); + --ring: oklch(65.3% 0.06 25); + --chart-1: oklch(55% 0.12 25); + --chart-2: oklch(56% 0.12 87); + --chart-3: oklch(57% 0.12 153); + --chart-4: oklch(58% 0.12 221); + --chart-5: oklch(59% 0.12 309); + --sidebar: oklch(95.9% 0.01 70); + --sidebar-foreground: oklch(16% 0.015 60); + --sidebar-primary: oklch(45% 0.12 25); + --sidebar-primary-foreground: oklch(98% 0.015 25); + --sidebar-accent: oklch(93.1% 0.018 25); + --sidebar-accent-foreground: oklch(16% 0.015 60); + --sidebar-border: oklch(63.5% 0.01 70); + --sidebar-ring: oklch(64.1% 0.06 25); } -[data-theme="forest"].dark, -.dark [data-theme="forest"] { - /* Dense canopy — medium-dark, visible green tint, moonlit feel */ - --background: oklch(0.14 0.025 162); - --foreground: oklch(0.93 0.012 152); - --card: oklch(0.19 0.03 162); - --card-foreground: oklch(0.93 0.012 152); - --popover: oklch(0.19 0.03 162); - --popover-foreground: oklch(0.93 0.012 152); - --primary: oklch(0.7 0.13 158); - --primary-foreground: oklch(0.14 0.03 160); - --secondary: oklch(0.25 0.025 162); - --secondary-foreground: oklch(0.93 0.012 152); - --muted: oklch(0.25 0.025 162); - --muted-foreground: oklch(0.62 0.028 157); - --accent: oklch(0.26 0.03 158); - --accent-foreground: oklch(0.93 0.012 152); - --destructive: oklch(0.65 0.16 20); - --destructive-foreground: oklch(0.52 0.18 20); - --success: oklch(0.62 0.15 145); - --success-foreground: oklch(0.52 0.13 145); - --info: oklch(0.62 0.11 240); - --info-foreground: oklch(0.5 0.13 240); - --warning: oklch(0.78 0.15 90); - --warning-foreground: oklch(0.66 0.13 85); - --invert: oklch(0.34 0.028 160); - --invert-foreground: oklch(0.97 0.008 155); - --focus: oklch(0.58 0.14 165); - --focus-foreground: oklch(0.97 0.008 155); - --border: oklch(1 0.01 155 / 10%); - --input: oklch(1 0.01 155 / 15%); - --ring: oklch(0.52 0.06 158); - --chart-1: oklch(0.65 0.15 160); - --chart-2: oklch(0.58 0.12 130); - --chart-3: oklch(0.76 0.14 120); - --chart-4: oklch(0.68 0.1 190); - --chart-5: oklch(0.72 0.13 90); - --sidebar: oklch(0.17 0.028 162); - --sidebar-foreground: oklch(0.93 0.012 152); - --sidebar-primary: oklch(0.7 0.13 158); - --sidebar-primary-foreground: oklch(0.97 0.008 155); - --sidebar-accent: oklch(0.26 0.03 158); - --sidebar-accent-foreground: oklch(0.93 0.012 152); - --sidebar-border: oklch(1 0.01 155 / 10%); - --sidebar-ring: oklch(0.52 0.06 158); -} - -/* Rose */ -[data-theme="rose"] { - --font-sans: 'Nunito', ui-sans-serif, system-ui, sans-serif; - --radius: 1.25rem; - --background: oklch(0.98 0.016 352); - --foreground: oklch(0.18 0.025 355); - --card: oklch(0.974 0.02 350); - --card-foreground: oklch(0.18 0.025 355); - --popover: oklch(0.98 0.016 352); - --popover-foreground: oklch(0.18 0.025 355); - --primary: oklch(0.6 0.18 350); - --primary-foreground: oklch(0.97 0.008 350); - --secondary: oklch(0.94 0.022 355); - --secondary-foreground: oklch(0.28 0.04 350); - --muted: oklch(0.935 0.02 355); - --muted-foreground: oklch(0.5 0.028 352); - --accent: oklch(0.92 0.026 348); - --accent-foreground: oklch(0.28 0.04 350); - --destructive: oklch(0.52 0.2 15); - --destructive-foreground: oklch(0.4 0.15 15); - --success: oklch(0.62 0.14 155); - --success-foreground: oklch(0.38 0.09 155); - --info: oklch(0.55 0.15 280); - --info-foreground: oklch(0.38 0.12 280); - --warning: oklch(0.76 0.15 75); - --warning-foreground: oklch(0.42 0.1 65); - --invert: oklch(0.21 0.025 355); - --invert-foreground: oklch(0.97 0.008 350); - --focus: oklch(0.58 0.18 350); - --focus-foreground: oklch(0.97 0.008 350); - --border: oklch(0.89 0.02 355); - --input: oklch(0.89 0.02 355); - --ring: oklch(0.62 0.08 350); - --chart-1: oklch(0.6 0.18 350); - --chart-2: oklch(0.55 0.16 20); - --chart-3: oklch(0.65 0.16 330); - --chart-4: oklch(0.72 0.14 50); - --chart-5: oklch(0.58 0.14 300); - --sidebar: oklch(0.968 0.018 355); - --sidebar-foreground: oklch(0.18 0.025 355); - --sidebar-primary: oklch(0.6 0.18 350); - --sidebar-primary-foreground: oklch(0.97 0.008 350); - --sidebar-accent: oklch(0.92 0.026 348); - --sidebar-accent-foreground: oklch(0.28 0.04 350); - --sidebar-border: oklch(0.89 0.02 355); - --sidebar-ring: oklch(0.62 0.08 350); -} - -[data-theme="rose"].dark, -.dark [data-theme="rose"] { - /* Warm dusk — warmest dark mode, pink-warm, cozy and inviting */ - --background: oklch(0.16 0.022 358); - --foreground: oklch(0.93 0.012 350); - --card: oklch(0.21 0.026 358); - --card-foreground: oklch(0.93 0.012 350); - --popover: oklch(0.21 0.026 358); - --popover-foreground: oklch(0.93 0.012 350); - --primary: oklch(0.74 0.16 350); - --primary-foreground: oklch(0.16 0.03 355); - --secondary: oklch(0.27 0.025 358); - --secondary-foreground: oklch(0.93 0.012 350); - --muted: oklch(0.27 0.025 358); - --muted-foreground: oklch(0.62 0.03 352); - --accent: oklch(0.28 0.03 355); - --accent-foreground: oklch(0.93 0.012 350); - --destructive: oklch(0.65 0.17 15); - --destructive-foreground: oklch(0.52 0.2 15); - --success: oklch(0.68 0.14 155); - --success-foreground: oklch(0.56 0.12 155); - --info: oklch(0.62 0.15 280); - --info-foreground: oklch(0.5 0.17 280); - --warning: oklch(0.78 0.15 75); - --warning-foreground: oklch(0.66 0.13 68); - --invert: oklch(0.36 0.028 355); - --invert-foreground: oklch(0.97 0.008 350); - --focus: oklch(0.65 0.18 350); - --focus-foreground: oklch(0.97 0.008 350); - --border: oklch(1 0.01 350 / 10%); - --input: oklch(1 0.01 350 / 15%); - --ring: oklch(0.55 0.08 350); - --chart-1: oklch(0.7 0.18 350); - --chart-2: oklch(0.65 0.16 20); - --chart-3: oklch(0.75 0.16 330); - --chart-4: oklch(0.8 0.14 50); - --chart-5: oklch(0.68 0.14 300); - --sidebar: oklch(0.19 0.024 358); - --sidebar-foreground: oklch(0.93 0.012 350); - --sidebar-primary: oklch(0.74 0.16 350); - --sidebar-primary-foreground: oklch(0.97 0.008 350); - --sidebar-accent: oklch(0.28 0.03 355); - --sidebar-accent-foreground: oklch(0.93 0.012 350); - --sidebar-border: oklch(1 0.01 350 / 10%); - --sidebar-ring: oklch(0.55 0.08 350); -} - -/* Amber */ -[data-theme="amber"] { - --font-sans: 'Sora', ui-sans-serif, system-ui, sans-serif; - --radius: 0.25rem; - --background: oklch(0.98 0.015 82); - --foreground: oklch(0.18 0.025 70); - --card: oklch(0.974 0.018 80); - --card-foreground: oklch(0.18 0.025 70); - --popover: oklch(0.98 0.015 82); - --popover-foreground: oklch(0.18 0.025 70); - --primary: oklch(0.62 0.17 75); - --primary-foreground: oklch(0.2 0.035 70); - --secondary: oklch(0.94 0.022 80); - --secondary-foreground: oklch(0.28 0.04 70); - --muted: oklch(0.935 0.02 80); - --muted-foreground: oklch(0.5 0.028 75); - --accent: oklch(0.92 0.026 78); - --accent-foreground: oklch(0.28 0.04 70); - --destructive: oklch(0.52 0.2 10); - --destructive-foreground: oklch(0.4 0.15 10); - --success: oklch(0.62 0.14 160); - --success-foreground: oklch(0.38 0.09 160); - --info: oklch(0.52 0.13 260); - --info-foreground: oklch(0.38 0.1 260); - --warning: oklch(0.72 0.17 70); - --warning-foreground: oklch(0.4 0.12 60); - --invert: oklch(0.21 0.025 70); - --invert-foreground: oklch(0.97 0.008 80); - --focus: oklch(0.6 0.17 75); - --focus-foreground: oklch(0.2 0.035 70); - --border: oklch(0.89 0.02 80); - --input: oklch(0.89 0.02 80); - --ring: oklch(0.62 0.08 75); - --chart-1: oklch(0.65 0.18 75); - --chart-2: oklch(0.55 0.17 45); - --chart-3: oklch(0.72 0.15 100); - --chart-4: oklch(0.5 0.14 20); - --chart-5: oklch(0.58 0.11 130); - --sidebar: oklch(0.968 0.018 80); - --sidebar-foreground: oklch(0.18 0.025 70); - --sidebar-primary: oklch(0.62 0.17 75); - --sidebar-primary-foreground: oklch(0.2 0.035 70); - --sidebar-accent: oklch(0.92 0.026 78); - --sidebar-accent-foreground: oklch(0.28 0.04 70); - --sidebar-border: oklch(0.89 0.02 80); - --sidebar-ring: oklch(0.62 0.08 75); -} - -[data-theme="amber"].dark, -.dark [data-theme="amber"] { - /* Dark gold — deep, warm sepia, golden highlights pop with high contrast */ - --background: oklch(0.13 0.02 72); - --foreground: oklch(0.93 0.012 80); - --card: oklch(0.18 0.025 72); - --card-foreground: oklch(0.93 0.012 80); - --popover: oklch(0.18 0.025 72); - --popover-foreground: oklch(0.93 0.012 80); - --primary: oklch(0.78 0.16 75); - --primary-foreground: oklch(0.13 0.035 70); - --secondary: oklch(0.24 0.022 72); - --secondary-foreground: oklch(0.93 0.012 80); - --muted: oklch(0.24 0.022 72); - --muted-foreground: oklch(0.62 0.028 75); - --accent: oklch(0.25 0.026 72); - --accent-foreground: oklch(0.93 0.012 80); - --destructive: oklch(0.65 0.17 10); - --destructive-foreground: oklch(0.52 0.2 10); - --success: oklch(0.68 0.14 160); - --success-foreground: oklch(0.56 0.12 160); - --info: oklch(0.6 0.13 260); - --info-foreground: oklch(0.5 0.15 260); - --warning: oklch(0.78 0.17 70); - --warning-foreground: oklch(0.65 0.15 65); - --invert: oklch(0.34 0.025 70); - --invert-foreground: oklch(0.97 0.008 80); - --focus: oklch(0.72 0.17 75); - --focus-foreground: oklch(0.2 0.035 70); - --border: oklch(1 0.01 80 / 11%); - --input: oklch(1 0.01 80 / 16%); - --ring: oklch(0.55 0.08 75); - --chart-1: oklch(0.75 0.18 75); - --chart-2: oklch(0.66 0.17 45); - --chart-3: oklch(0.8 0.15 100); - --chart-4: oklch(0.62 0.14 20); - --chart-5: oklch(0.7 0.11 130); - --sidebar: oklch(0.16 0.023 72); - --sidebar-foreground: oklch(0.93 0.012 80); - --sidebar-primary: oklch(0.78 0.16 75); - --sidebar-primary-foreground: oklch(0.2 0.035 70); - --sidebar-accent: oklch(0.25 0.026 72); - --sidebar-accent-foreground: oklch(0.93 0.012 80); - --sidebar-border: oklch(1 0.01 80 / 11%); - --sidebar-ring: oklch(0.55 0.08 75); +[data-theme="press"].dark, +.dark [data-theme="press"] { + --font-sans: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif; + --font-heading: 'Source Serif 4', ui-serif, Georgia, serif; + --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --radius: 0.5rem; + --background: oklch(15% 0.012 60); + --foreground: oklch(93% 0.012 80); + --card: oklch(19.5% 0.008 70); + --card-foreground: oklch(93% 0.012 80); + --popover: oklch(19.5% 0.008 70); + --popover-foreground: oklch(93% 0.012 80); + --primary: oklch(72% 0.11 30); + --primary-foreground: oklch(16% 0.015 25); + --secondary: oklch(24% 0.01 70); + --secondary-foreground: oklch(93% 0.012 80); + --muted: oklch(24% 0.01 70); + --muted-foreground: oklch(62.7% 0.01 70); + --accent: oklch(28.5% 0.018 25); + --accent-foreground: oklch(93% 0.012 80); + --destructive: oklch(64.6% 0.16 27); + --destructive-foreground: oklch(64.6% 0.144 27); + --success: oklch(61.5% 0.11 152); + --success-foreground: oklch(61.5% 0.099 152); + --info: oklch(62.6% 0.12 250); + --info-foreground: oklch(62.6% 0.108 250); + --warning: oklch(63.2% 0.13 75); + --warning-foreground: oklch(63.2% 0.117 75); + --invert: oklch(37% 0.01 70); + --invert-foreground: oklch(98% 0.004 70); + --focus: oklch(62% 0.14 25); + --focus-foreground: oklch(16% 0.015 25); + --border: oklch(48.3% 0.01 70); + --input: oklch(48.3% 0.01 70); + --ring: oklch(49% 0.06 25); + --chart-1: oklch(70% 0.11 25); + --chart-2: oklch(68.5% 0.11 87); + --chart-3: oklch(67% 0.11 153); + --chart-4: oklch(65.5% 0.11 221); + --chart-5: oklch(64% 0.11 309); + --sidebar: oklch(17.7% 0.01 70); + --sidebar-foreground: oklch(93% 0.012 80); + --sidebar-primary: oklch(72% 0.11 30); + --sidebar-primary-foreground: oklch(16% 0.015 25); + --sidebar-accent: oklch(22.2% 0.018 25); + --sidebar-accent-foreground: oklch(93% 0.012 80); + --sidebar-border: oklch(49.3% 0.01 70); + --sidebar-ring: oklch(49.9% 0.06 25); } -/* Slate */ -[data-theme="slate"] { +/* Console */ +[data-theme="console"] { --font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif; - --radius: 0.5rem; - --background: oklch(0.982 0.012 250); - --foreground: oklch(0.17 0.018 250); - --card: oklch(0.976 0.015 248); - --card-foreground: oklch(0.17 0.018 250); - --popover: oklch(0.982 0.012 250); - --popover-foreground: oklch(0.17 0.018 250); - --primary: oklch(0.48 0.1 250); - --primary-foreground: oklch(0.97 0.006 250); - --secondary: oklch(0.945 0.016 250); - --secondary-foreground: oklch(0.25 0.03 250); - --muted: oklch(0.94 0.014 250); - --muted-foreground: oklch(0.5 0.02 250); - --accent: oklch(0.93 0.018 248); - --accent-foreground: oklch(0.25 0.03 250); - --destructive: oklch(0.52 0.18 20); - --destructive-foreground: oklch(0.4 0.14 20); - --success: oklch(0.6 0.14 165); - --success-foreground: oklch(0.38 0.09 165); - --info: oklch(0.52 0.11 250); - --info-foreground: oklch(0.38 0.08 250); - --warning: oklch(0.76 0.14 75); - --warning-foreground: oklch(0.42 0.1 60); - --invert: oklch(0.21 0.018 250); - --invert-foreground: oklch(0.97 0.006 250); - --focus: oklch(0.52 0.12 255); - --focus-foreground: oklch(0.97 0.006 250); - --border: oklch(0.9 0.014 250); - --input: oklch(0.9 0.014 250); - --ring: oklch(0.58 0.05 250); - --chart-1: oklch(0.52 0.12 250); - --chart-2: oklch(0.58 0.13 220); - --chart-3: oklch(0.5 0.14 290); - --chart-4: oklch(0.62 0.1 200); - --chart-5: oklch(0.6 0.11 330); - --sidebar: oklch(0.97 0.014 250); - --sidebar-foreground: oklch(0.17 0.018 250); - --sidebar-primary: oklch(0.48 0.1 250); - --sidebar-primary-foreground: oklch(0.97 0.006 250); - --sidebar-accent: oklch(0.93 0.018 248); - --sidebar-accent-foreground: oklch(0.25 0.03 250); - --sidebar-border: oklch(0.9 0.014 250); - --sidebar-ring: oklch(0.58 0.05 250); + --font-heading: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --radius: 0.125rem; + --leading-base: 1.4; + --text-base-size: 0.9375rem; + --background: oklch(96.5% 0.005 240); + --foreground: oklch(20% 0.01 245); + --card: oklch(98.1% 0.0048 243); + --card-foreground: oklch(20% 0.01 245); + --popover: oklch(98.1% 0.0048 243); + --popover-foreground: oklch(20% 0.01 245); + --primary: oklch(52% 0.11 235); + --primary-foreground: oklch(98% 0.015 235); + --secondary: oklch(94.5% 0.006 243); + --secondary-foreground: oklch(20% 0.01 245); + --muted: oklch(94.5% 0.006 243); + --muted-foreground: oklch(52.2% 0.006 243); + --accent: oklch(93.7% 0.018 235); + --accent-foreground: oklch(20% 0.01 245); + --destructive: oklch(54.1% 0.16 27); + --destructive-foreground: oklch(52% 0.144 27); + --success: oklch(51% 0.11 152); + --success-foreground: oklch(49.5% 0.099 152); + --info: oklch(52.2% 0.12 250); + --info-foreground: oklch(50.5% 0.108 250); + --warning: oklch(52.8% 0.13 75); + --warning-foreground: oklch(51% 0.117 75); + --invert: oklch(21% 0.006 243); + --invert-foreground: oklch(98% 0.0024 243); + --focus: oklch(55% 0.14 235); + --focus-foreground: oklch(99.7% 0.02 235); + --border: oklch(63.9% 0.006 243); + --input: oklch(63.9% 0.006 243); + --ring: oklch(63.6% 0.06 235); + --chart-1: oklch(55% 0.12 235); + --chart-2: oklch(56% 0.12 297); + --chart-3: oklch(57% 0.12 3); + --chart-4: oklch(58% 0.12 71); + --chart-5: oklch(59% 0.12 159); + --sidebar: oklch(94.9% 0.006 243); + --sidebar-foreground: oklch(20% 0.01 245); + --sidebar-primary: oklch(52% 0.11 235); + --sidebar-primary-foreground: oklch(98% 0.015 235); + --sidebar-accent: oklch(92.1% 0.018 235); + --sidebar-accent-foreground: oklch(20% 0.01 245); + --sidebar-border: oklch(62.7% 0.006 243); + --sidebar-ring: oklch(62.4% 0.06 235); } -[data-theme="slate"].dark, -.dark [data-theme="slate"] { - /* Ice steel — very dark, desaturated, clinical precision, icy blue accents */ - --background: oklch(0.11 0.018 252); - --foreground: oklch(0.93 0.008 250); - --card: oklch(0.16 0.02 252); - --card-foreground: oklch(0.93 0.008 250); - --popover: oklch(0.16 0.02 252); - --popover-foreground: oklch(0.93 0.008 250); - --primary: oklch(0.72 0.1 248); - --primary-foreground: oklch(0.11 0.025 250); - --secondary: oklch(0.22 0.02 252); - --secondary-foreground: oklch(0.93 0.008 250); - --muted: oklch(0.22 0.02 252); - --muted-foreground: oklch(0.62 0.022 250); - --accent: oklch(0.23 0.022 250); - --accent-foreground: oklch(0.93 0.008 250); - --destructive: oklch(0.65 0.16 20); - --destructive-foreground: oklch(0.52 0.18 20); - --success: oklch(0.66 0.14 165); - --success-foreground: oklch(0.54 0.12 165); - --info: oklch(0.6 0.11 250); - --info-foreground: oklch(0.5 0.13 250); - --warning: oklch(0.78 0.14 75); - --warning-foreground: oklch(0.66 0.12 68); - --invert: oklch(0.32 0.02 250); - --invert-foreground: oklch(0.97 0.006 250); - --focus: oklch(0.58 0.14 255); - --focus-foreground: oklch(0.97 0.006 250); - --border: oklch(1 0.008 250 / 9%); - --input: oklch(1 0.008 250 / 14%); - --ring: oklch(0.52 0.05 250); - --chart-1: oklch(0.68 0.12 250); - --chart-2: oklch(0.7 0.13 220); - --chart-3: oklch(0.65 0.14 290); - --chart-4: oklch(0.72 0.1 200); - --chart-5: oklch(0.7 0.11 330); - --sidebar: oklch(0.14 0.019 252); - --sidebar-foreground: oklch(0.93 0.008 250); - --sidebar-primary: oklch(0.72 0.1 248); - --sidebar-primary-foreground: oklch(0.97 0.006 250); - --sidebar-accent: oklch(0.23 0.022 250); - --sidebar-accent-foreground: oklch(0.93 0.008 250); - --sidebar-border: oklch(1 0.008 250 / 9%); - --sidebar-ring: oklch(0.52 0.05 250); +[data-theme="console"].dark, +.dark [data-theme="console"] { + --font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif; + --font-heading: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --radius: 0.125rem; + --leading-base: 1.4; + --text-base-size: 0.9375rem; + --background: oklch(16% 0.005 245); + --foreground: oklch(91% 0.008 240); + --card: oklch(20.5% 0.0048 243); + --card-foreground: oklch(91% 0.008 240); + --popover: oklch(20.5% 0.0048 243); + --popover-foreground: oklch(91% 0.008 240); + --primary: oklch(70% 0.1 230); + --primary-foreground: oklch(16% 0.015 235); + --secondary: oklch(25% 0.006 243); + --secondary-foreground: oklch(91% 0.008 240); + --muted: oklch(25% 0.006 243); + --muted-foreground: oklch(63.4% 0.006 243); + --accent: oklch(29.5% 0.018 235); + --accent-foreground: oklch(91% 0.008 240); + --destructive: oklch(65.3% 0.16 27); + --destructive-foreground: oklch(65.3% 0.144 27); + --success: oklch(62.2% 0.11 152); + --success-foreground: oklch(62.2% 0.099 152); + --info: oklch(63.3% 0.12 250); + --info-foreground: oklch(63.3% 0.108 250); + --warning: oklch(64% 0.13 75); + --warning-foreground: oklch(64% 0.117 75); + --invert: oklch(37% 0.006 243); + --invert-foreground: oklch(98% 0.0024 243); + --focus: oklch(62% 0.14 235); + --focus-foreground: oklch(16% 0.02 235); + --border: oklch(48.6% 0.006 243); + --input: oklch(48.6% 0.006 243); + --ring: oklch(48.3% 0.06 235); + --chart-1: oklch(70% 0.11 235); + --chart-2: oklch(68.5% 0.11 297); + --chart-3: oklch(67% 0.11 3); + --chart-4: oklch(65.5% 0.11 71); + --chart-5: oklch(64% 0.11 159); + --sidebar: oklch(18.7% 0.006 243); + --sidebar-foreground: oklch(91% 0.008 240); + --sidebar-primary: oklch(70% 0.1 230); + --sidebar-primary-foreground: oklch(16% 0.015 235); + --sidebar-accent: oklch(23.2% 0.018 235); + --sidebar-accent-foreground: oklch(91% 0.008 240); + --sidebar-border: oklch(49.6% 0.006 243); + --sidebar-ring: oklch(49.4% 0.06 235); } -/* Crimson */ -[data-theme="crimson"] { +/* Signal */ +[data-theme="signal"] { --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif; - --radius: 0; - --background: oklch(0.982 0.012 15); - --foreground: oklch(0.14 0.025 15); - --card: oklch(0.976 0.015 14); - --card-foreground: oklch(0.14 0.025 15); - --popover: oklch(0.982 0.012 15); - --popover-foreground: oklch(0.14 0.025 15); - --primary: oklch(0.5 0.2 18); - --primary-foreground: oklch(0.97 0.008 15); - --secondary: oklch(0.94 0.018 15); - --secondary-foreground: oklch(0.22 0.04 15); - --muted: oklch(0.935 0.016 15); - --muted-foreground: oklch(0.48 0.028 15); - --accent: oklch(0.92 0.022 12); - --accent-foreground: oklch(0.22 0.04 15); - --destructive: oklch(0.48 0.22 5); - --destructive-foreground: oklch(0.36 0.16 5); - --success: oklch(0.6 0.14 155); - --success-foreground: oklch(0.38 0.09 155); - --info: oklch(0.52 0.13 245); - --info-foreground: oklch(0.38 0.1 245); - --warning: oklch(0.74 0.16 55); - --warning-foreground: oklch(0.4 0.1 45); - --invert: oklch(0.18 0.025 15); - --invert-foreground: oklch(0.97 0.008 15); - --focus: oklch(0.5 0.2 18); - --focus-foreground: oklch(0.97 0.008 15); - --border: oklch(0.88 0.018 15); - --input: oklch(0.88 0.018 15); - --ring: oklch(0.58 0.1 18); - --chart-1: oklch(0.52 0.2 18); - --chart-2: oklch(0.45 0.16 350); - --chart-3: oklch(0.65 0.17 45); - --chart-4: oklch(0.4 0.1 30); - --chart-5: oklch(0.58 0.14 75); - --sidebar: oklch(0.968 0.015 15); - --sidebar-foreground: oklch(0.14 0.025 15); - --sidebar-primary: oklch(0.5 0.2 18); - --sidebar-primary-foreground: oklch(0.97 0.008 15); - --sidebar-accent: oklch(0.92 0.022 12); - --sidebar-accent-foreground: oklch(0.22 0.04 15); - --sidebar-border: oklch(0.88 0.018 15); - --sidebar-ring: oklch(0.58 0.1 18); + --font-heading: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif; + --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --radius: 0.75rem; + --background: oklch(98% 0.008 75); + --foreground: oklch(18% 0.014 60); + --card: oklch(99.5% 0.008 72); + --card-foreground: oklch(18% 0.014 60); + --popover: oklch(99.5% 0.008 72); + --popover-foreground: oklch(18% 0.014 60); + --primary: oklch(55% 0.14 265); + --primary-foreground: oklch(98% 0.015 265); + --secondary: oklch(96% 0.01 72); + --secondary-foreground: oklch(18% 0.014 60); + --muted: oklch(96% 0.01 72); + --muted-foreground: oklch(53.3% 0.01 72); + --accent: oklch(95.2% 0.018 265); + --accent-foreground: oklch(18% 0.014 60); + --destructive: oklch(55.1% 0.16 27); + --destructive-foreground: oklch(53% 0.144 27); + --success: oklch(52% 0.11 152); + --success-foreground: oklch(50.4% 0.099 152); + --info: oklch(53.2% 0.12 250); + --info-foreground: oklch(51.5% 0.108 250); + --warning: oklch(53.8% 0.13 75); + --warning-foreground: oklch(52% 0.117 75); + --invert: oklch(21% 0.01 72); + --invert-foreground: oklch(98% 0.004 72); + --focus: oklch(55% 0.14 265); + --focus-foreground: oklch(98% 0.015 265); + --border: oklch(65% 0.01 72); + --input: oklch(65% 0.01 72); + --ring: oklch(65.1% 0.06 265); + --chart-1: oklch(55% 0.12 265); + --chart-2: oklch(56% 0.12 327); + --chart-3: oklch(57% 0.12 33); + --chart-4: oklch(58% 0.12 101); + --chart-5: oklch(59% 0.12 189); + --sidebar: oklch(96.4% 0.01 72); + --sidebar-foreground: oklch(18% 0.014 60); + --sidebar-primary: oklch(55% 0.14 265); + --sidebar-primary-foreground: oklch(98% 0.015 265); + --sidebar-accent: oklch(93.6% 0.018 265); + --sidebar-accent-foreground: oklch(18% 0.014 60); + --sidebar-border: oklch(63.8% 0.01 72); + --sidebar-ring: oklch(63.9% 0.06 265); } -[data-theme="crimson"].dark, -.dark [data-theme="crimson"] { - /* True black — deepest dark mode, stark contrast, bold red cuts through */ - --background: oklch(0.08 0.015 15); - --foreground: oklch(0.94 0.01 15); - --card: oklch(0.13 0.02 15); - --card-foreground: oklch(0.94 0.01 15); - --popover: oklch(0.13 0.02 15); - --popover-foreground: oklch(0.94 0.01 15); - --primary: oklch(0.68 0.2 18); - --primary-foreground: oklch(0.97 0.008 15); - --secondary: oklch(0.18 0.02 15); - --secondary-foreground: oklch(0.94 0.01 15); - --muted: oklch(0.18 0.02 15); - --muted-foreground: oklch(0.58 0.025 15); - --accent: oklch(0.19 0.025 12); - --accent-foreground: oklch(0.94 0.01 15); - --destructive: oklch(0.6 0.2 5); - --destructive-foreground: oklch(0.48 0.22 5); - --success: oklch(0.66 0.14 155); - --success-foreground: oklch(0.54 0.12 155); - --info: oklch(0.6 0.13 245); - --info-foreground: oklch(0.5 0.15 245); - --warning: oklch(0.78 0.16 55); - --warning-foreground: oklch(0.66 0.14 50); - --invert: oklch(0.28 0.025 15); - --invert-foreground: oklch(0.97 0.008 15); - --focus: oklch(0.6 0.2 18); - --focus-foreground: oklch(0.97 0.008 15); - --border: oklch(1 0.008 15 / 14%); - --input: oklch(1 0.008 15 / 18%); - --ring: oklch(0.52 0.1 18); - --chart-1: oklch(0.65 0.2 18); - --chart-2: oklch(0.6 0.16 350); - --chart-3: oklch(0.75 0.17 45); - --chart-4: oklch(0.55 0.1 30); - --chart-5: oklch(0.7 0.14 75); - --sidebar: oklch(0.11 0.018 15); - --sidebar-foreground: oklch(0.94 0.01 15); - --sidebar-primary: oklch(0.68 0.2 18); - --sidebar-primary-foreground: oklch(0.97 0.008 15); - --sidebar-accent: oklch(0.19 0.025 12); - --sidebar-accent-foreground: oklch(0.94 0.01 15); - --sidebar-border: oklch(1 0.008 15 / 14%); - --sidebar-ring: oklch(0.52 0.1 18); +[data-theme="signal"].dark, +.dark [data-theme="signal"] { + --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif; + --font-heading: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif; + --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --radius: 0.75rem; + --background: oklch(17% 0.008 65); + --foreground: oklch(94% 0.01 75); + --card: oklch(21.5% 0.008 72); + --card-foreground: oklch(94% 0.01 75); + --popover: oklch(21.5% 0.008 72); + --popover-foreground: oklch(94% 0.01 75); + --primary: oklch(72% 0.13 268); + --primary-foreground: oklch(16% 0.015 265); + --secondary: oklch(26% 0.01 72); + --secondary-foreground: oklch(94% 0.01 75); + --muted: oklch(26% 0.01 72); + --muted-foreground: oklch(64.2% 0.01 72); + --accent: oklch(30.5% 0.018 265); + --accent-foreground: oklch(94% 0.01 75); + --destructive: oklch(66% 0.16 27); + --destructive-foreground: oklch(66% 0.144 27); + --success: oklch(62.9% 0.11 152); + --success-foreground: oklch(62.9% 0.099 152); + --info: oklch(64% 0.12 250); + --info-foreground: oklch(64% 0.108 250); + --warning: oklch(64.7% 0.13 75); + --warning-foreground: oklch(64.7% 0.117 75); + --invert: oklch(37% 0.01 72); + --invert-foreground: oklch(98% 0.004 72); + --focus: oklch(62% 0.14 265); + --focus-foreground: oklch(16% 0.015 265); + --border: oklch(49% 0.01 72); + --input: oklch(49% 0.01 72); + --ring: oklch(49.1% 0.06 265); + --chart-1: oklch(70% 0.11 265); + --chart-2: oklch(68.5% 0.11 327); + --chart-3: oklch(67% 0.11 33); + --chart-4: oklch(65.5% 0.11 101); + --chart-5: oklch(64% 0.11 189); + --sidebar: oklch(19.7% 0.01 72); + --sidebar-foreground: oklch(94% 0.01 75); + --sidebar-primary: oklch(72% 0.13 268); + --sidebar-primary-foreground: oklch(16% 0.015 265); + --sidebar-accent: oklch(24.2% 0.018 265); + --sidebar-accent-foreground: oklch(94% 0.01 75); + --sidebar-border: oklch(50.1% 0.01 72); + --sidebar-ring: oklch(50.2% 0.06 265); } diff --git a/packages/themes/src/press.css b/packages/themes/src/press.css new file mode 100644 index 0000000..962623e --- /dev/null +++ b/packages/themes/src/press.css @@ -0,0 +1,112 @@ +/* + * Press — Editorial + * Warm paper, near-black ink, high contrast. Serif headings. + * + * Fonts: 'Source Serif 4' (headings) + 'Source Sans 3' (body) + * Load via Google Fonts, next/font, or self-host: + * https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400..700&family=Source+Serif+4:wght@400..700&display=swap + * + * Radius: 0.5rem + * Accent hue: ~25 · neutral hue: ~70 + * + * GENERATED by scripts/generate-themes.mjs — edit the anchors there, not + * this file. Values are solved against scripts/check-contrast.mjs. + */ + +:root { + --font-sans: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif; + --font-heading: 'Source Serif 4', ui-serif, Georgia, serif; + --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --radius: 0.5rem; + --background: oklch(97.5% 0.012 85); + --foreground: oklch(16% 0.015 60); + --card: oklch(99.1% 0.008 70); + --card-foreground: oklch(16% 0.015 60); + --popover: oklch(99.1% 0.008 70); + --popover-foreground: oklch(16% 0.015 60); + --primary: oklch(45% 0.12 25); + --primary-foreground: oklch(98% 0.015 25); + --secondary: oklch(95.5% 0.01 70); + --secondary-foreground: oklch(16% 0.015 60); + --muted: oklch(95.5% 0.01 70); + --muted-foreground: oklch(52.9% 0.01 70); + --accent: oklch(94.7% 0.018 25); + --accent-foreground: oklch(16% 0.015 60); + --destructive: oklch(54.8% 0.16 27); + --destructive-foreground: oklch(52.7% 0.144 27); + --success: oklch(51.6% 0.11 152); + --success-foreground: oklch(50.1% 0.099 152); + --info: oklch(52.8% 0.12 250); + --info-foreground: oklch(51.1% 0.108 250); + --warning: oklch(53.4% 0.13 75); + --warning-foreground: oklch(51.7% 0.117 75); + --invert: oklch(21% 0.01 70); + --invert-foreground: oklch(98% 0.004 70); + --focus: oklch(55% 0.14 25); + --focus-foreground: oklch(98% 0.015 25); + --border: oklch(64.7% 0.01 70); + --input: oklch(64.7% 0.01 70); + --ring: oklch(65.3% 0.06 25); + --chart-1: oklch(55% 0.12 25); + --chart-2: oklch(56% 0.12 87); + --chart-3: oklch(57% 0.12 153); + --chart-4: oklch(58% 0.12 221); + --chart-5: oklch(59% 0.12 309); + --sidebar: oklch(95.9% 0.01 70); + --sidebar-foreground: oklch(16% 0.015 60); + --sidebar-primary: oklch(45% 0.12 25); + --sidebar-primary-foreground: oklch(98% 0.015 25); + --sidebar-accent: oklch(93.1% 0.018 25); + --sidebar-accent-foreground: oklch(16% 0.015 60); + --sidebar-border: oklch(63.5% 0.01 70); + --sidebar-ring: oklch(64.1% 0.06 25); +} + +.dark { + --font-sans: 'Source Sans 3', ui-sans-serif, system-ui, sans-serif; + --font-heading: 'Source Serif 4', ui-serif, Georgia, serif; + --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --radius: 0.5rem; + --background: oklch(15% 0.012 60); + --foreground: oklch(93% 0.012 80); + --card: oklch(19.5% 0.008 70); + --card-foreground: oklch(93% 0.012 80); + --popover: oklch(19.5% 0.008 70); + --popover-foreground: oklch(93% 0.012 80); + --primary: oklch(72% 0.11 30); + --primary-foreground: oklch(16% 0.015 25); + --secondary: oklch(24% 0.01 70); + --secondary-foreground: oklch(93% 0.012 80); + --muted: oklch(24% 0.01 70); + --muted-foreground: oklch(62.7% 0.01 70); + --accent: oklch(28.5% 0.018 25); + --accent-foreground: oklch(93% 0.012 80); + --destructive: oklch(64.6% 0.16 27); + --destructive-foreground: oklch(64.6% 0.144 27); + --success: oklch(61.5% 0.11 152); + --success-foreground: oklch(61.5% 0.099 152); + --info: oklch(62.6% 0.12 250); + --info-foreground: oklch(62.6% 0.108 250); + --warning: oklch(63.2% 0.13 75); + --warning-foreground: oklch(63.2% 0.117 75); + --invert: oklch(37% 0.01 70); + --invert-foreground: oklch(98% 0.004 70); + --focus: oklch(62% 0.14 25); + --focus-foreground: oklch(16% 0.015 25); + --border: oklch(48.3% 0.01 70); + --input: oklch(48.3% 0.01 70); + --ring: oklch(49% 0.06 25); + --chart-1: oklch(70% 0.11 25); + --chart-2: oklch(68.5% 0.11 87); + --chart-3: oklch(67% 0.11 153); + --chart-4: oklch(65.5% 0.11 221); + --chart-5: oklch(64% 0.11 309); + --sidebar: oklch(17.7% 0.01 70); + --sidebar-foreground: oklch(93% 0.012 80); + --sidebar-primary: oklch(72% 0.11 30); + --sidebar-primary-foreground: oklch(16% 0.015 25); + --sidebar-accent: oklch(22.2% 0.018 25); + --sidebar-accent-foreground: oklch(93% 0.012 80); + --sidebar-border: oklch(49.3% 0.01 70); + --sidebar-ring: oklch(49.9% 0.06 25); +} diff --git a/packages/themes/src/rose.css b/packages/themes/src/rose.css deleted file mode 100644 index c223d01..0000000 --- a/packages/themes/src/rose.css +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Rose — Soft & Bubbly - * Very rounded, bouncy, warm pink tones. - * Feels like a friendly consumer app or social platform. - * - * Font: Nunito (rounded, approachable, bubbly) - * Load via Google Fonts, next/font, or self-host: - * https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800&display=swap - * - * Radius: 1.25rem (pill-like, very soft) - * Hue: ~350 (blush/pink) - */ - -:root { - --font-sans: 'Nunito', ui-sans-serif, system-ui, sans-serif; - --radius: 1.25rem; - --background: oklch(0.98 0.016 352); - --foreground: oklch(0.18 0.025 355); - --card: oklch(0.974 0.02 350); - --card-foreground: oklch(0.18 0.025 355); - --popover: oklch(0.98 0.016 352); - --popover-foreground: oklch(0.18 0.025 355); - --primary: oklch(0.6 0.18 350); - --primary-foreground: oklch(0.97 0.008 350); - --secondary: oklch(0.94 0.022 355); - --secondary-foreground: oklch(0.28 0.04 350); - --muted: oklch(0.935 0.02 355); - --muted-foreground: oklch(0.5 0.028 352); - --accent: oklch(0.92 0.026 348); - --accent-foreground: oklch(0.28 0.04 350); - --destructive: oklch(0.52 0.2 15); - --destructive-foreground: oklch(0.4 0.15 15); - --success: oklch(0.62 0.14 155); - --success-foreground: oklch(0.38 0.09 155); - --info: oklch(0.55 0.15 280); - --info-foreground: oklch(0.38 0.12 280); - --warning: oklch(0.76 0.15 75); - --warning-foreground: oklch(0.42 0.1 65); - --invert: oklch(0.21 0.025 355); - --invert-foreground: oklch(0.97 0.008 350); - --focus: oklch(0.58 0.18 350); - --focus-foreground: oklch(0.97 0.008 350); - --border: oklch(0.89 0.02 355); - --input: oklch(0.89 0.02 355); - --ring: oklch(0.62 0.08 350); - --chart-1: oklch(0.6 0.18 350); - --chart-2: oklch(0.55 0.16 20); - --chart-3: oklch(0.65 0.16 330); - --chart-4: oklch(0.72 0.14 50); - --chart-5: oklch(0.58 0.14 300); - --sidebar: oklch(0.968 0.018 355); - --sidebar-foreground: oklch(0.18 0.025 355); - --sidebar-primary: oklch(0.6 0.18 350); - --sidebar-primary-foreground: oklch(0.97 0.008 350); - --sidebar-accent: oklch(0.92 0.026 348); - --sidebar-accent-foreground: oklch(0.28 0.04 350); - --sidebar-border: oklch(0.89 0.02 355); - --sidebar-ring: oklch(0.62 0.08 350); -} - -.dark { - /* Warm dusk — warmest dark mode, pink-warm, cozy and inviting */ - --background: oklch(0.16 0.022 358); - --foreground: oklch(0.93 0.012 350); - --card: oklch(0.21 0.026 358); - --card-foreground: oklch(0.93 0.012 350); - --popover: oklch(0.21 0.026 358); - --popover-foreground: oklch(0.93 0.012 350); - --primary: oklch(0.74 0.16 350); - --primary-foreground: oklch(0.16 0.03 355); - --secondary: oklch(0.27 0.025 358); - --secondary-foreground: oklch(0.93 0.012 350); - --muted: oklch(0.27 0.025 358); - --muted-foreground: oklch(0.62 0.03 352); - --accent: oklch(0.28 0.03 355); - --accent-foreground: oklch(0.93 0.012 350); - --destructive: oklch(0.65 0.17 15); - --destructive-foreground: oklch(0.52 0.2 15); - --success: oklch(0.68 0.14 155); - --success-foreground: oklch(0.56 0.12 155); - --info: oklch(0.62 0.15 280); - --info-foreground: oklch(0.5 0.17 280); - --warning: oklch(0.78 0.15 75); - --warning-foreground: oklch(0.66 0.13 68); - --invert: oklch(0.36 0.028 355); - --invert-foreground: oklch(0.97 0.008 350); - --focus: oklch(0.65 0.18 350); - --focus-foreground: oklch(0.97 0.008 350); - --border: oklch(1 0.01 350 / 10%); - --input: oklch(1 0.01 350 / 15%); - --ring: oklch(0.55 0.08 350); - --chart-1: oklch(0.7 0.18 350); - --chart-2: oklch(0.65 0.16 20); - --chart-3: oklch(0.75 0.16 330); - --chart-4: oklch(0.8 0.14 50); - --chart-5: oklch(0.68 0.14 300); - --sidebar: oklch(0.19 0.024 358); - --sidebar-foreground: oklch(0.93 0.012 350); - --sidebar-primary: oklch(0.74 0.16 350); - --sidebar-primary-foreground: oklch(0.97 0.008 350); - --sidebar-accent: oklch(0.28 0.03 355); - --sidebar-accent-foreground: oklch(0.93 0.012 350); - --sidebar-border: oklch(1 0.01 350 / 10%); - --sidebar-ring: oklch(0.55 0.08 350); -} diff --git a/packages/themes/src/safelist.css b/packages/themes/src/safelist.css new file mode 100644 index 0000000..29bb229 --- /dev/null +++ b/packages/themes/src/safelist.css @@ -0,0 +1,96 @@ +/* Utility safelist for precompiled consumers. + * + * rnui is normally consumed with Tailwind running in the consumer's build, so + * any class they write is compiled on demand. But the design system is ALSO + * shipped as a precompiled stylesheet (see utilities.css, and the bundle + * published to Claude Design). In that mode a class that wasn't emitted at + * build time silently does nothing — no error, no warning, the element just + * renders unstyled. + * + * This file safelists complete scales rather than only the values rnui's own + * components happen to use. Import it into any Tailwind build that must + * produce a self-contained stylesheet. Bundle size is the deliberate trade: + * silent failure costs far more than kilobytes. + * + * Keep in sync with scripts/emit-utilities.mjs, which reports what actually + * shipped by reading the compiled output. + */ + +/* ── Box model: the full spacing scale on every side/axis variant ───────── */ +@source inline( + "{,sm:,md:,lg:,xl:}{p,px,py,pt,pr,pb,pl,m,mx,my,mt,mr,mb,ml,gap,gap-x,gap-y,space-x,space-y}-{0,0.5,1,1.5,2,2.5,3,3.5,4,5,6,7,8,9,10,11,12,14,16,20,24}" +); + +/* ── Sizing ─────────────────────────────────────────────────────────────── */ +@source inline( + "{,sm:,md:,lg:,xl:}{w,h,min-w,min-h,max-h}-{0,0.5,1,1.5,2,2.5,3,3.5,4,5,6,7,8,9,10,11,12,14,16,20,24}" +); +@source inline("{,sm:,md:,lg:,xl:}{w,h}-{auto,full,screen,fit,min,max}"); +@source inline("{,sm:,md:,lg:,xl:}{min-w,min-h,max-h}-{0,full,fit,min,max,screen}"); +@source inline( + "{,sm:,md:,lg:,xl:}w-{1/2,1/3,2/3,1/4,3/4,1/5,2/5,3/5,4/5,1/6,5/6,1/12,5/12,7/12,11/12}" +); +@source inline( + "{,sm:,md:,lg:,xl:}max-w-{xs,sm,md,lg,xl,2xl,3xl,4xl,5xl,6xl,7xl,full,none,prose,screen}" +); + +/* ── Typography ─────────────────────────────────────────────────────────── */ +@source inline("{,sm:,md:,lg:,xl:}text-{xs,sm,base,lg,xl,2xl,3xl,4xl,5xl,6xl,7xl,8xl,9xl}"); +@source inline("font-{thin,extralight,light,normal,medium,semibold,bold,extrabold,black}"); +@source inline("tracking-{tighter,tight,normal,wide,wider,widest}"); +@source inline("leading-{none,tight,snug,normal,relaxed,loose}"); +@source inline("{,sm:,md:,lg:,xl:}text-{left,center,right,justify}"); +@source inline("{truncate,text-ellipsis,text-clip,break-words,break-all,whitespace-nowrap,whitespace-pre-wrap}"); +@source inline("font-{sans,serif,mono}"); + +/* ── Layout ─────────────────────────────────────────────────────────────── */ +@source inline("{,sm:,md:,lg:,xl:}grid-cols-{1,2,3,4,5,6,7,8,9,10,11,12}"); +@source inline("{,sm:,md:,lg:,xl:}grid-rows-{1,2,3,4,5,6}"); +@source inline("{,sm:,md:,lg:,xl:}col-span-{1,2,3,4,5,6,7,8,9,10,11,12,full}"); +@source inline("{,sm:,md:,lg:,xl:}row-span-{1,2,3,4,5,6,full}"); +@source inline("{,sm:,md:,lg:,xl:}{flex,inline-flex,grid,inline-grid,block,inline-block,inline,hidden,contents}"); +@source inline("{,sm:,md:,lg:,xl:}flex-{row,row-reverse,col,col-reverse,wrap,nowrap,1,auto,initial,none}"); +@source inline("{,sm:,md:,lg:,xl:}items-{start,end,center,baseline,stretch}"); +@source inline("{,sm:,md:,lg:,xl:}justify-{start,end,center,between,around,evenly}"); +@source inline("{,sm:,md:,lg:,xl:}self-{auto,start,end,center,stretch}"); +@source inline("{,sm:,md:,lg:,xl:}{shrink,grow}-{0,1}"); +@source inline("{relative,absolute,fixed,sticky,static}"); +@source inline("{inset,top,right,bottom,left}-{0,0.5,1,2,3,4,auto,full}"); +@source inline("z-{0,10,20,30,40,50,auto}"); +@source inline("{overflow,overflow-x,overflow-y}-{auto,hidden,visible,scroll,clip}"); +@source inline("{,sm:,md:,lg:,xl:}mx-auto"); + +/* ── Borders, radius, shadow, opacity ───────────────────────────────────── */ +@source inline("rounded{,-t,-r,-b,-l,-tl,-tr,-br,-bl}-{none,xs,sm,md,lg,xl,2xl,3xl,4xl,full}"); +@source inline("border{,-t,-r,-b,-l,-x,-y}-{0,2,4,8}"); +@source inline("{shadow,drop-shadow}-{xs,sm,md,lg,xl,2xl,none}"); +@source inline( + "{,hover:,focus:,active:,disabled:,group-hover:,dark:}opacity-{0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100}" +); + +/* ── Design-token colors, plain and stateful ──────────────────────────────── + * Every color the theme layer defines, on bg/text/border, plus the states an + * interactive element actually needs. Token names mirror @theme inline in + * index.css — if a token is added there, add it here too. + */ +@source inline( + "{,hover:,focus:,focus-visible:,active:,disabled:,dark:}{bg,text,border}-{background,foreground,card,card-foreground,popover,popover-foreground,primary,primary-foreground,secondary,secondary-foreground,muted,muted-foreground,accent,accent-foreground,destructive,destructive-foreground,info,info-foreground,success,success-foreground,warning,warning-foreground,invert,invert-foreground,focus,focus-foreground,border,input,ring,sidebar,sidebar-foreground,sidebar-primary,sidebar-primary-foreground,sidebar-accent,sidebar-accent-foreground,sidebar-border,sidebar-ring}" +); +@source inline("{bg,text,border}-{chart-1,chart-2,chart-3,chart-4,chart-5}"); +/* Common alpha steps on the surfaces that get overlaid. */ +@source inline( + "{,hover:,dark:}bg-{primary,secondary,muted,accent,destructive,success,info,warning,foreground,background}/{5,10,20,30,40,50,60,70,80,90}" +); +@source inline("{,hover:}border-{border,input,ring,primary,destructive}/{20,30,40,50,60}"); + +/* ── Rings and outlines ─────────────────────────────────────────────────── */ +@source inline("{,focus:,focus-visible:}ring-{0,1,2,4,8}"); +@source inline("{,focus:,focus-visible:}ring-{ring,primary,destructive,focus,border}"); +@source inline("{,focus:,focus-visible:}outline-{none,0,1,2}"); + +/* ── Interaction, transitions ───────────────────────────────────────────── */ +@source inline("cursor-{pointer,default,not-allowed,wait,text,move}"); +@source inline("{pointer-events-none,pointer-events-auto,select-none,select-text}"); +@source inline("transition{,-all,-colors,-opacity,-transform,-shadow}"); +@source inline("duration-{75,100,150,200,300,500,700,1000}"); +@source inline("ease-{linear,in,out,in-out}"); diff --git a/packages/themes/src/signal.css b/packages/themes/src/signal.css new file mode 100644 index 0000000..30a21bb --- /dev/null +++ b/packages/themes/src/signal.css @@ -0,0 +1,112 @@ +/* + * Signal — Brand-forward + * Warm neutral shell, one confident accent doing all the work. + * + * Fonts: 'Space Grotesk' (headings) + 'Instrument Sans' (body) + * Load via Google Fonts, next/font, or self-host: + * https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400..700&family=Space+Grotesk:wght@400..700&display=swap + * + * Radius: 0.75rem + * Accent hue: ~265 · neutral hue: ~72 + * + * GENERATED by scripts/generate-themes.mjs — edit the anchors there, not + * this file. Values are solved against scripts/check-contrast.mjs. + */ + +:root { + --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif; + --font-heading: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif; + --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --radius: 0.75rem; + --background: oklch(98% 0.008 75); + --foreground: oklch(18% 0.014 60); + --card: oklch(99.5% 0.008 72); + --card-foreground: oklch(18% 0.014 60); + --popover: oklch(99.5% 0.008 72); + --popover-foreground: oklch(18% 0.014 60); + --primary: oklch(55% 0.14 265); + --primary-foreground: oklch(98% 0.015 265); + --secondary: oklch(96% 0.01 72); + --secondary-foreground: oklch(18% 0.014 60); + --muted: oklch(96% 0.01 72); + --muted-foreground: oklch(53.3% 0.01 72); + --accent: oklch(95.2% 0.018 265); + --accent-foreground: oklch(18% 0.014 60); + --destructive: oklch(55.1% 0.16 27); + --destructive-foreground: oklch(53% 0.144 27); + --success: oklch(52% 0.11 152); + --success-foreground: oklch(50.4% 0.099 152); + --info: oklch(53.2% 0.12 250); + --info-foreground: oklch(51.5% 0.108 250); + --warning: oklch(53.8% 0.13 75); + --warning-foreground: oklch(52% 0.117 75); + --invert: oklch(21% 0.01 72); + --invert-foreground: oklch(98% 0.004 72); + --focus: oklch(55% 0.14 265); + --focus-foreground: oklch(98% 0.015 265); + --border: oklch(65% 0.01 72); + --input: oklch(65% 0.01 72); + --ring: oklch(65.1% 0.06 265); + --chart-1: oklch(55% 0.12 265); + --chart-2: oklch(56% 0.12 327); + --chart-3: oklch(57% 0.12 33); + --chart-4: oklch(58% 0.12 101); + --chart-5: oklch(59% 0.12 189); + --sidebar: oklch(96.4% 0.01 72); + --sidebar-foreground: oklch(18% 0.014 60); + --sidebar-primary: oklch(55% 0.14 265); + --sidebar-primary-foreground: oklch(98% 0.015 265); + --sidebar-accent: oklch(93.6% 0.018 265); + --sidebar-accent-foreground: oklch(18% 0.014 60); + --sidebar-border: oklch(63.8% 0.01 72); + --sidebar-ring: oklch(63.9% 0.06 265); +} + +.dark { + --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif; + --font-heading: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif; + --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; + --radius: 0.75rem; + --background: oklch(17% 0.008 65); + --foreground: oklch(94% 0.01 75); + --card: oklch(21.5% 0.008 72); + --card-foreground: oklch(94% 0.01 75); + --popover: oklch(21.5% 0.008 72); + --popover-foreground: oklch(94% 0.01 75); + --primary: oklch(72% 0.13 268); + --primary-foreground: oklch(16% 0.015 265); + --secondary: oklch(26% 0.01 72); + --secondary-foreground: oklch(94% 0.01 75); + --muted: oklch(26% 0.01 72); + --muted-foreground: oklch(64.2% 0.01 72); + --accent: oklch(30.5% 0.018 265); + --accent-foreground: oklch(94% 0.01 75); + --destructive: oklch(66% 0.16 27); + --destructive-foreground: oklch(66% 0.144 27); + --success: oklch(62.9% 0.11 152); + --success-foreground: oklch(62.9% 0.099 152); + --info: oklch(64% 0.12 250); + --info-foreground: oklch(64% 0.108 250); + --warning: oklch(64.7% 0.13 75); + --warning-foreground: oklch(64.7% 0.117 75); + --invert: oklch(37% 0.01 72); + --invert-foreground: oklch(98% 0.004 72); + --focus: oklch(62% 0.14 265); + --focus-foreground: oklch(16% 0.015 265); + --border: oklch(49% 0.01 72); + --input: oklch(49% 0.01 72); + --ring: oklch(49.1% 0.06 265); + --chart-1: oklch(70% 0.11 265); + --chart-2: oklch(68.5% 0.11 327); + --chart-3: oklch(67% 0.11 33); + --chart-4: oklch(65.5% 0.11 101); + --chart-5: oklch(64% 0.11 189); + --sidebar: oklch(19.7% 0.01 72); + --sidebar-foreground: oklch(94% 0.01 75); + --sidebar-primary: oklch(72% 0.13 268); + --sidebar-primary-foreground: oklch(16% 0.015 265); + --sidebar-accent: oklch(24.2% 0.018 265); + --sidebar-accent-foreground: oklch(94% 0.01 75); + --sidebar-border: oklch(50.1% 0.01 72); + --sidebar-ring: oklch(50.2% 0.06 265); +} diff --git a/packages/themes/src/slate.css b/packages/themes/src/slate.css deleted file mode 100644 index ab9ad7f..0000000 --- a/packages/themes/src/slate.css +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Slate — Professional Tech - * Clean, functional, moderate radius, cool blue-gray. - * Feels like a developer tool, IDE, or SaaS dashboard. - * - * Font: Geist (clean, monospaced-inspired, techy) - * Load via Google Fonts, next/font, or self-host: - * https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700;800&display=swap - * - * Radius: 0.5rem (moderate, precise) - * Hue: ~250 (cool blue-steel) - */ - -:root { - --font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif; - --radius: 0.5rem; - --background: oklch(0.982 0.012 250); - --foreground: oklch(0.17 0.018 250); - --card: oklch(0.976 0.015 248); - --card-foreground: oklch(0.17 0.018 250); - --popover: oklch(0.982 0.012 250); - --popover-foreground: oklch(0.17 0.018 250); - --primary: oklch(0.48 0.1 250); - --primary-foreground: oklch(0.97 0.006 250); - --secondary: oklch(0.945 0.016 250); - --secondary-foreground: oklch(0.25 0.03 250); - --muted: oklch(0.94 0.014 250); - --muted-foreground: oklch(0.5 0.02 250); - --accent: oklch(0.93 0.018 248); - --accent-foreground: oklch(0.25 0.03 250); - --destructive: oklch(0.52 0.18 20); - --destructive-foreground: oklch(0.4 0.14 20); - --success: oklch(0.6 0.14 165); - --success-foreground: oklch(0.38 0.09 165); - --info: oklch(0.52 0.11 250); - --info-foreground: oklch(0.38 0.08 250); - --warning: oklch(0.76 0.14 75); - --warning-foreground: oklch(0.42 0.1 60); - --invert: oklch(0.21 0.018 250); - --invert-foreground: oklch(0.97 0.006 250); - --focus: oklch(0.52 0.12 255); - --focus-foreground: oklch(0.97 0.006 250); - --border: oklch(0.9 0.014 250); - --input: oklch(0.9 0.014 250); - --ring: oklch(0.58 0.05 250); - --chart-1: oklch(0.52 0.12 250); - --chart-2: oklch(0.58 0.13 220); - --chart-3: oklch(0.5 0.14 290); - --chart-4: oklch(0.62 0.1 200); - --chart-5: oklch(0.6 0.11 330); - --sidebar: oklch(0.97 0.014 250); - --sidebar-foreground: oklch(0.17 0.018 250); - --sidebar-primary: oklch(0.48 0.1 250); - --sidebar-primary-foreground: oklch(0.97 0.006 250); - --sidebar-accent: oklch(0.93 0.018 248); - --sidebar-accent-foreground: oklch(0.25 0.03 250); - --sidebar-border: oklch(0.9 0.014 250); - --sidebar-ring: oklch(0.58 0.05 250); -} - -.dark { - /* Ice steel — very dark, desaturated, clinical precision, icy blue accents */ - --background: oklch(0.11 0.018 252); - --foreground: oklch(0.93 0.008 250); - --card: oklch(0.16 0.02 252); - --card-foreground: oklch(0.93 0.008 250); - --popover: oklch(0.16 0.02 252); - --popover-foreground: oklch(0.93 0.008 250); - --primary: oklch(0.72 0.1 248); - --primary-foreground: oklch(0.11 0.025 250); - --secondary: oklch(0.22 0.02 252); - --secondary-foreground: oklch(0.93 0.008 250); - --muted: oklch(0.22 0.02 252); - --muted-foreground: oklch(0.62 0.022 250); - --accent: oklch(0.23 0.022 250); - --accent-foreground: oklch(0.93 0.008 250); - --destructive: oklch(0.65 0.16 20); - --destructive-foreground: oklch(0.52 0.18 20); - --success: oklch(0.66 0.14 165); - --success-foreground: oklch(0.54 0.12 165); - --info: oklch(0.6 0.11 250); - --info-foreground: oklch(0.5 0.13 250); - --warning: oklch(0.78 0.14 75); - --warning-foreground: oklch(0.66 0.12 68); - --invert: oklch(0.32 0.02 250); - --invert-foreground: oklch(0.97 0.006 250); - --focus: oklch(0.58 0.14 255); - --focus-foreground: oklch(0.97 0.006 250); - --border: oklch(1 0.008 250 / 9%); - --input: oklch(1 0.008 250 / 14%); - --ring: oklch(0.52 0.05 250); - --chart-1: oklch(0.68 0.12 250); - --chart-2: oklch(0.7 0.13 220); - --chart-3: oklch(0.65 0.14 290); - --chart-4: oklch(0.72 0.1 200); - --chart-5: oklch(0.7 0.11 330); - --sidebar: oklch(0.14 0.019 252); - --sidebar-foreground: oklch(0.93 0.008 250); - --sidebar-primary: oklch(0.72 0.1 248); - --sidebar-primary-foreground: oklch(0.97 0.006 250); - --sidebar-accent: oklch(0.23 0.022 250); - --sidebar-accent-foreground: oklch(0.93 0.008 250); - --sidebar-border: oklch(1 0.008 250 / 9%); - --sidebar-ring: oklch(0.52 0.05 250); -} diff --git a/packages/themes/src/utilities.css b/packages/themes/src/utilities.css new file mode 100644 index 0000000..8df782f --- /dev/null +++ b/packages/themes/src/utilities.css @@ -0,0 +1,12 @@ +/* Precompiled utility layer + design tokens. + * + * For consumers who cannot run Tailwind themselves (a precompiled stylesheet, + * an embedded preview, a design tool). Everything in ./safelist.css is emitted + * eagerly, so ordinary layout markup works without a build step. + * + * If you DO run Tailwind, import '@e412/rnui-themes' instead and let your own + * build compile only what you use — this file is deliberately much larger. + */ +@import 'tailwindcss'; +@import './index.css'; +@import './safelist.css'; diff --git a/packages/themes/src/violet.css b/packages/themes/src/violet.css deleted file mode 100644 index 82397b6..0000000 --- a/packages/themes/src/violet.css +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Violet — Luxury Creative - * Geometric, very rounded, rich purple tones. - * Feels like a premium design tool or creative platform. - * - * Font: Outfit (geometric, modern, clean) - * Load via Google Fonts, next/font, or self-host: - * https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&display=swap - * - * Radius: 1rem (very round, soft) - * Hue: ~285 (amethyst/purple) - */ - -:root { - --font-sans: 'Outfit', ui-sans-serif, system-ui, sans-serif; - --radius: 1rem; - --background: oklch(0.98 0.016 292); - --foreground: oklch(0.18 0.03 285); - --card: oklch(0.974 0.02 290); - --card-foreground: oklch(0.18 0.03 285); - --popover: oklch(0.98 0.016 292); - --popover-foreground: oklch(0.18 0.03 285); - --primary: oklch(0.52 0.2 285); - --primary-foreground: oklch(0.97 0.01 285); - --secondary: oklch(0.94 0.025 290); - --secondary-foreground: oklch(0.28 0.05 285); - --muted: oklch(0.935 0.022 290); - --muted-foreground: oklch(0.5 0.035 288); - --accent: oklch(0.92 0.03 288); - --accent-foreground: oklch(0.28 0.05 285); - --destructive: oklch(0.55 0.2 355); - --destructive-foreground: oklch(0.4 0.15 355); - --success: oklch(0.62 0.14 155); - --success-foreground: oklch(0.38 0.09 155); - --info: oklch(0.52 0.18 275); - --info-foreground: oklch(0.38 0.14 275); - --warning: oklch(0.76 0.15 70); - --warning-foreground: oklch(0.42 0.1 60); - --invert: oklch(0.22 0.03 285); - --invert-foreground: oklch(0.97 0.01 285); - --focus: oklch(0.55 0.2 285); - --focus-foreground: oklch(0.97 0.01 285); - --border: oklch(0.89 0.022 290); - --input: oklch(0.89 0.022 290); - --ring: oklch(0.6 0.1 285); - --chart-1: oklch(0.55 0.2 285); - --chart-2: oklch(0.58 0.18 320); - --chart-3: oklch(0.48 0.16 260); - --chart-4: oklch(0.68 0.16 350); - --chart-5: oklch(0.65 0.12 240); - --sidebar: oklch(0.968 0.02 290); - --sidebar-foreground: oklch(0.18 0.03 285); - --sidebar-primary: oklch(0.52 0.2 285); - --sidebar-primary-foreground: oklch(0.97 0.01 285); - --sidebar-accent: oklch(0.92 0.03 288); - --sidebar-accent-foreground: oklch(0.28 0.05 285); - --sidebar-border: oklch(0.89 0.022 290); - --sidebar-ring: oklch(0.6 0.1 285); -} - -.dark { - /* Midnight purple — deep, high chroma, vivid neon accents */ - --background: oklch(0.12 0.035 285); - --foreground: oklch(0.93 0.014 290); - --card: oklch(0.17 0.04 285); - --card-foreground: oklch(0.93 0.014 290); - --popover: oklch(0.17 0.04 285); - --popover-foreground: oklch(0.93 0.014 290); - --primary: oklch(0.72 0.18 285); - --primary-foreground: oklch(0.12 0.04 285); - --secondary: oklch(0.24 0.035 285); - --secondary-foreground: oklch(0.93 0.014 290); - --muted: oklch(0.24 0.035 285); - --muted-foreground: oklch(0.62 0.04 288); - --accent: oklch(0.25 0.04 285); - --accent-foreground: oklch(0.93 0.014 290); - --destructive: oklch(0.68 0.17 355); - --destructive-foreground: oklch(0.55 0.2 355); - --success: oklch(0.68 0.14 155); - --success-foreground: oklch(0.56 0.12 155); - --info: oklch(0.6 0.18 275); - --info-foreground: oklch(0.5 0.2 275); - --warning: oklch(0.78 0.15 70); - --warning-foreground: oklch(0.66 0.13 65); - --invert: oklch(0.34 0.04 285); - --invert-foreground: oklch(0.97 0.01 285); - --focus: oklch(0.6 0.2 285); - --focus-foreground: oklch(0.97 0.01 285); - --border: oklch(1 0.012 290 / 10%); - --input: oklch(1 0.012 290 / 15%); - --ring: oklch(0.55 0.1 285); - --chart-1: oklch(0.68 0.2 285); - --chart-2: oklch(0.7 0.18 320); - --chart-3: oklch(0.6 0.18 260); - --chart-4: oklch(0.76 0.16 350); - --chart-5: oklch(0.75 0.12 240); - --sidebar: oklch(0.15 0.038 285); - --sidebar-foreground: oklch(0.93 0.014 290); - --sidebar-primary: oklch(0.72 0.18 285); - --sidebar-primary-foreground: oklch(0.97 0.01 285); - --sidebar-accent: oklch(0.25 0.04 285); - --sidebar-accent-foreground: oklch(0.93 0.014 290); - --sidebar-border: oklch(1 0.012 290 / 10%); - --sidebar-ring: oklch(0.55 0.1 285); -} diff --git a/packages/themes/vite.config.ts b/packages/themes/vite.config.ts index 340fdcd..7aef865 100644 --- a/packages/themes/vite.config.ts +++ b/packages/themes/vite.config.ts @@ -1,7 +1,11 @@ import { defineConfig } from 'vite' +import tailwindcss from '@tailwindcss/vite' import { resolve } from 'node:path' export default defineConfig({ + // utilities.css is the only entry that runs Tailwind; the theme files are + // plain token declarations and pass through untouched. + plugins: [tailwindcss()], build: { rollupOptions: { input: { @@ -9,15 +13,11 @@ export default defineConfig({ base: resolve(import.meta.dirname, 'src/base.css'), light: resolve(import.meta.dirname, 'src/light.css'), dark: resolve(import.meta.dirname, 'src/dark.css'), - oxide: resolve(import.meta.dirname, 'src/oxide.css'), - ocean: resolve(import.meta.dirname, 'src/ocean.css'), - violet: resolve(import.meta.dirname, 'src/violet.css'), - forest: resolve(import.meta.dirname, 'src/forest.css'), - rose: resolve(import.meta.dirname, 'src/rose.css'), - amber: resolve(import.meta.dirname, 'src/amber.css'), - slate: resolve(import.meta.dirname, 'src/slate.css'), - crimson: resolve(import.meta.dirname, 'src/crimson.css'), + press: resolve(import.meta.dirname, 'src/press.css'), + console: resolve(import.meta.dirname, 'src/console.css'), + signal: resolve(import.meta.dirname, 'src/signal.css'), presets: resolve(import.meta.dirname, 'src/presets.css'), + utilities: resolve(import.meta.dirname, 'src/utilities.css'), }, output: { assetFileNames: '[name][extname]', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89ff9c9..4028832 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -396,6 +396,15 @@ importers: packages/themes: devDependencies: + '@tailwindcss/vite': + specifier: ^4.2.2 + version: 4.2.2(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + culori: + specifier: ^4.0.2 + version: 4.0.2 + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 vite: specifier: ^8.0.0 version: 8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)