diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3b6544..a140683 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,9 @@ A theme is a directory under `packages/jaamd/src/themes//` plus one entry ### 1. Write `index.css` -One `:root` block of `--jaamd-*` declarations. These **seeds are required**: +A `:root` block of `--jaamd-*` declarations. A theme that has both a light and a +dark palette adds a second block on `html.dark`, and must repeat the seeds there — +otherwise one mode borrows the other's background. These **seeds are required**: | Token | What it is | |---|---| @@ -38,9 +40,13 @@ Do not restate the font tokens: they are the defaults already. ### 2. Declare it -Add an entry to `src/themes/index.ts` with the directory name as `slug`, a readable -`name`, the `mode` the palette expects (`"light"` or `"dark"` — themes are not -assumed to be dark), and the `shiki` theme that pairs with it. +Add an entry to `src/themes/index.ts` with the directory name as `slug` and a +readable `name`, plus: + +| `mode` | The palette is | `shiki` | +|---|---|---| +| `"light"` / `"dark"` | one palette, applying in both modes | one theme name | +| `"dual"` | two palettes, `:root` and `html.dark` | `{ light, dark }` | ### 3. Generate the dark variant @@ -48,8 +54,9 @@ assumed to be dark), and the `shiki` theme that pairs with it. pnpm build:themes ``` -`dark.css` is `index.css` under `html.dark`, generated and committed. Never edit it -by hand. +`dark.css` is `index.css` under `html.dark` — the variant for using a single-palette +theme only in dark mode. It is generated and committed; never edit it by hand. A +`dual` theme covers dark mode itself and gets no `dark.css`. ### 4. Check it diff --git a/packages/jaamd/README.md b/packages/jaamd/README.md index 353dd0b..1fb8b3f 100644 --- a/packages/jaamd/README.md +++ b/packages/jaamd/README.md @@ -375,14 +375,20 @@ The default set includes dark-mode overrides activated by the `dark` class on ### Theme presets -Three presets restyle all `--jaamd-*` variables to match popular editor colour -schemes: - -| Preset | Import | Recommended Shiki theme | -|--------|--------|------------------------| -| Dracula | `@lancher-dev/jaamd/themes/dracula` | `dracula` | -| Nord | `@lancher-dev/jaamd/themes/nord` | `nord` | -| One Dark | `@lancher-dev/jaamd/themes/one-dark` | `one-dark-pro` | +Eight presets restyle the `--jaamd-*` palette to match popular editor colour +schemes. A **dual** preset carries a light and a dark palette and follows +`html.dark`; the others are one palette that applies in both modes. + +| Preset | Import | Kind | Recommended Shiki theme | +|--------|--------|------|------------------------| +| Catppuccin | `@lancher-dev/jaamd/themes/catppuccin` | dual | `catppuccin-latte` / `catppuccin-mocha` | +| Dracula | `@lancher-dev/jaamd/themes/dracula` | dark | `dracula` | +| Gruvbox | `@lancher-dev/jaamd/themes/gruvbox` | dual | `gruvbox-light-medium` / `gruvbox-dark-medium` | +| Nord | `@lancher-dev/jaamd/themes/nord` | dark | `nord` | +| One Dark | `@lancher-dev/jaamd/themes/one-dark` | dark | `one-dark-pro` | +| Rosé Pine | `@lancher-dev/jaamd/themes/rose-pine` | dual | `rose-pine-dawn` / `rose-pine` | +| Rosé Pine Moon | `@lancher-dev/jaamd/themes/rose-pine-moon` | dual | `rose-pine-dawn` / `rose-pine-moon` | +| Tokyo Night | `@lancher-dev/jaamd/themes/tokyo-night` | dark | `tokyo-night` | As a standalone theme, replacing the default light theme: @@ -396,7 +402,9 @@ jaamd({ theme: "dracula" }) @import "@lancher-dev/jaamd/styles.css"; ``` -Scoped to `html.dark` via the `/dark` variant: +A single-palette preset can also be scoped to `html.dark` via the `/dark` variant, +to use it as the dark half of your own light theme. Dual presets already cover both +modes and have no `/dark`: ```css @import "@lancher-dev/jaamd/themes/dracula/dark.css"; diff --git a/packages/jaamd/src/themes/catppuccin/index.css b/packages/jaamd/src/themes/catppuccin/index.css new file mode 100644 index 0000000..55511c9 --- /dev/null +++ b/packages/jaamd/src/themes/catppuccin/index.css @@ -0,0 +1,31 @@ +/* JAAMD theme — Catppuccin. Latte in light, Mocha in dark. */ + +:root { + --jaamd-bg: #eff1f5; + --jaamd-color-fg: #4c4f69; + --jaamd-color-fg-bright: #4c4f69; + --jaamd-color-primary: #1e66f5; + --jaamd-color-success: #40a02b; + --jaamd-alert-note-color: #1e66f5; + --jaamd-alert-tip-color: #40a02b; + --jaamd-alert-important-color: #1e66f5; + --jaamd-alert-warning-color: #df8e1d; + --jaamd-alert-caution-color: #d20f39; + + --jaamd-em-fg: #8839ef; +} + +html.dark { + --jaamd-bg: #1e1e2e; + --jaamd-color-fg: #cdd6f4; + --jaamd-color-fg-bright: #cdd6f4; + --jaamd-color-primary: #89b4fa; + --jaamd-color-success: #a6e3a1; + --jaamd-alert-note-color: #89b4fa; + --jaamd-alert-tip-color: #a6e3a1; + --jaamd-alert-important-color: #89b4fa; + --jaamd-alert-warning-color: #f9e2af; + --jaamd-alert-caution-color: #f38ba8; + + --jaamd-em-fg: #cba6f7; +} diff --git a/packages/jaamd/src/themes/gruvbox/index.css b/packages/jaamd/src/themes/gruvbox/index.css new file mode 100644 index 0000000..537ae0e --- /dev/null +++ b/packages/jaamd/src/themes/gruvbox/index.css @@ -0,0 +1,33 @@ +/* JAAMD theme — Gruvbox. Light and dark, following the toggle. */ + +:root { + --jaamd-bg: #fbf1c7; + --jaamd-color-fg: #3c3836; + --jaamd-color-fg-bright: #282828; + --jaamd-color-primary: #458588; + --jaamd-color-success: #98971a; + --jaamd-alert-note-color: #458588; + --jaamd-alert-tip-color: #98971a; + --jaamd-alert-important-color: #458588; + --jaamd-alert-warning-color: #d79921; + --jaamd-alert-caution-color: #cc241d; + + --jaamd-border-strong: #d5c4a1; + --jaamd-em-fg: #b16286; +} + +html.dark { + --jaamd-bg: #282828; + --jaamd-color-fg: #ebdbb2; + --jaamd-color-fg-bright: #fbf1c7; + --jaamd-color-primary: #83a598; + --jaamd-color-success: #b8bb26; + --jaamd-alert-note-color: #83a598; + --jaamd-alert-tip-color: #b8bb26; + --jaamd-alert-important-color: #83a598; + --jaamd-alert-warning-color: #fabd2f; + --jaamd-alert-caution-color: #fb4934; + + --jaamd-border-strong: #665c54; + --jaamd-em-fg: #d3869b; +} diff --git a/packages/jaamd/src/themes/index.ts b/packages/jaamd/src/themes/index.ts index d27ad94..c9ae459 100644 --- a/packages/jaamd/src/themes/index.ts +++ b/packages/jaamd/src/themes/index.ts @@ -3,21 +3,52 @@ * this instead of hardcoding a list, and `tests/unit/themes.mjs` checks that * each entry matches a directory that honours the token contract. */ +/** + * `light` and `dark` are single palettes that apply in both modes; `dual` carries + * both, a `:root` block and an `html.dark` one, and follows whatever decides the + * mode. A `dual` theme has no `/dark` variant — its own file already does that. + */ +export type JaamdThemeMode = "light" | "dark" | "dual"; + export interface JaamdTheme { /** Directory name, and the value a `data-jaamd-theme` attribute would carry. */ slug: string; /** Human-readable name, for pickers. */ name: string; - /** Whether the palette expects `html.dark`. Not every theme is dark. */ - mode: "light" | "dark"; - /** Shiki theme that pairs with this palette. */ - shiki: string; + mode: JaamdThemeMode; + /** Shiki theme that pairs with this palette, or a pair for a `dual` one. */ + shiki: string | { light: string; dark: string }; } export const themes: JaamdTheme[] = [ + { + slug: "catppuccin", + name: "Catppuccin", + mode: "dual", + shiki: { light: "catppuccin-latte", dark: "catppuccin-mocha" }, + }, { slug: "dracula", name: "Dracula", mode: "dark", shiki: "dracula" }, + { + slug: "gruvbox", + name: "Gruvbox", + mode: "dual", + shiki: { light: "gruvbox-light-medium", dark: "gruvbox-dark-medium" }, + }, { slug: "nord", name: "Nord", mode: "dark", shiki: "nord" }, { slug: "one-dark", name: "One Dark", mode: "dark", shiki: "one-dark-pro" }, + { + slug: "rose-pine", + name: "Rosé Pine", + mode: "dual", + shiki: { light: "rose-pine-dawn", dark: "rose-pine" }, + }, + { + slug: "rose-pine-moon", + name: "Rosé Pine Moon", + mode: "dual", + shiki: { light: "rose-pine-dawn", dark: "rose-pine-moon" }, + }, + { slug: "tokyo-night", name: "Tokyo Night", mode: "dark", shiki: "tokyo-night" }, ]; /** Seeds a theme must declare; everything else derives from them in variables.css. */ diff --git a/packages/jaamd/src/themes/rose-pine-moon/index.css b/packages/jaamd/src/themes/rose-pine-moon/index.css new file mode 100644 index 0000000..5837196 --- /dev/null +++ b/packages/jaamd/src/themes/rose-pine-moon/index.css @@ -0,0 +1,33 @@ +/* JAAMD theme — Rosé Pine Moon. Dawn in light, moon in dark. */ + +:root { + --jaamd-bg: #faf4ed; + --jaamd-color-fg: #575279; + --jaamd-color-fg-bright: #575279; + --jaamd-color-primary: #286983; + --jaamd-color-success: #286983; + --jaamd-alert-note-color: #56949f; + --jaamd-alert-tip-color: #286983; + --jaamd-alert-important-color: #286983; + --jaamd-alert-warning-color: #ea9d34; + --jaamd-alert-caution-color: #b4637a; + + --jaamd-border-strong: #9893a5; + --jaamd-em-fg: #907aa9; +} + +html.dark { + --jaamd-bg: #232136; + --jaamd-color-fg: #e0def4; + --jaamd-color-fg-bright: #e0def4; + --jaamd-color-primary: #9ccfd8; + --jaamd-color-success: #9ccfd8; + --jaamd-alert-note-color: #3e8fb0; + --jaamd-alert-tip-color: #9ccfd8; + --jaamd-alert-important-color: #9ccfd8; + --jaamd-alert-warning-color: #f6c177; + --jaamd-alert-caution-color: #eb6f92; + + --jaamd-border-strong: #6e6a86; + --jaamd-em-fg: #c4a7e7; +} diff --git a/packages/jaamd/src/themes/rose-pine/index.css b/packages/jaamd/src/themes/rose-pine/index.css new file mode 100644 index 0000000..6d4d8a5 --- /dev/null +++ b/packages/jaamd/src/themes/rose-pine/index.css @@ -0,0 +1,33 @@ +/* JAAMD theme — Rosé Pine. Dawn in light, main in dark. */ + +:root { + --jaamd-bg: #faf4ed; + --jaamd-color-fg: #575279; + --jaamd-color-fg-bright: #575279; + --jaamd-color-primary: #286983; + --jaamd-color-success: #286983; + --jaamd-alert-note-color: #56949f; + --jaamd-alert-tip-color: #286983; + --jaamd-alert-important-color: #286983; + --jaamd-alert-warning-color: #ea9d34; + --jaamd-alert-caution-color: #b4637a; + + --jaamd-border-strong: #9893a5; + --jaamd-em-fg: #907aa9; +} + +html.dark { + --jaamd-bg: #191724; + --jaamd-color-fg: #e0def4; + --jaamd-color-fg-bright: #e0def4; + --jaamd-color-primary: #9ccfd8; + --jaamd-color-success: #9ccfd8; + --jaamd-alert-note-color: #31748f; + --jaamd-alert-tip-color: #9ccfd8; + --jaamd-alert-important-color: #9ccfd8; + --jaamd-alert-warning-color: #f6c177; + --jaamd-alert-caution-color: #eb6f92; + + --jaamd-border-strong: #6e6a86; + --jaamd-em-fg: #c4a7e7; +} diff --git a/packages/jaamd/src/themes/tokyo-night/dark.css b/packages/jaamd/src/themes/tokyo-night/dark.css new file mode 100644 index 0000000..8b1c41c --- /dev/null +++ b/packages/jaamd/src/themes/tokyo-night/dark.css @@ -0,0 +1,18 @@ +/* JAAMD theme — Tokyo Night (dark mode only). A dark theme: it applies in both modes. */ + +html.dark { + --jaamd-bg: #1a1b26; + --jaamd-color-fg: #a9b1d6; + --jaamd-color-fg-bright: #c0caf5; + --jaamd-color-primary: #7aa2f7; + --jaamd-color-success: #9ece6a; + --jaamd-alert-note-color: #7dcfff; + --jaamd-alert-tip-color: #9ece6a; + --jaamd-alert-important-color: #7aa2f7; + --jaamd-alert-warning-color: #e0af68; + --jaamd-alert-caution-color: #f7768e; + + --jaamd-border-strong: #565f89; + --jaamd-color-primary-light: #7dcfff; + --jaamd-em-fg: #bb9af7; +} diff --git a/packages/jaamd/src/themes/tokyo-night/index.css b/packages/jaamd/src/themes/tokyo-night/index.css new file mode 100644 index 0000000..f8f6bd8 --- /dev/null +++ b/packages/jaamd/src/themes/tokyo-night/index.css @@ -0,0 +1,18 @@ +/* JAAMD theme — Tokyo Night. A dark theme: it applies in both modes. */ + +:root { + --jaamd-bg: #1a1b26; + --jaamd-color-fg: #a9b1d6; + --jaamd-color-fg-bright: #c0caf5; + --jaamd-color-primary: #7aa2f7; + --jaamd-color-success: #9ece6a; + --jaamd-alert-note-color: #7dcfff; + --jaamd-alert-tip-color: #9ece6a; + --jaamd-alert-important-color: #7aa2f7; + --jaamd-alert-warning-color: #e0af68; + --jaamd-alert-caution-color: #f7768e; + + --jaamd-border-strong: #565f89; + --jaamd-color-primary-light: #7dcfff; + --jaamd-em-fg: #bb9af7; +} diff --git a/scripts/build-themes.mjs b/scripts/build-themes.mjs index c7fdcfe..cf4dbfb 100644 --- a/scripts/build-themes.mjs +++ b/scripts/build-themes.mjs @@ -7,12 +7,17 @@ * `--check` verifies the committed files instead of writing them. */ -import { readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { readdirSync, readFileSync, writeFileSync, rmSync, existsSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; +import { themes } from "../packages/jaamd/src/themes/index.ts"; + const THEMES = join(process.cwd(), "packages", "jaamd", "src", "themes"); +/** A dual theme carries both palettes already; a dark-only variant is meaningless. */ +const isDual = (slug) => themes.find((t) => t.slug === slug)?.mode === "dual"; + /** The dark variant of a theme's source. */ export function darkVariant(source) { return source @@ -29,12 +34,23 @@ function run(check) { const stale = []; for (const slug of slugs) { + const path = join(THEMES, slug, "dark.css"); + + if (isDual(slug)) { + if (check) { + if (existsSync(path)) stale.push(`${slug} (dual, should have no dark.css)`); + } else if (existsSync(path)) { + rmSync(path); + console.log(`✓ ${slug}/dark.css removed (dual)`); + } + continue; + } + const source = readFileSync(join(THEMES, slug, "index.css"), "utf8"); const expected = darkVariant(source); - const path = join(THEMES, slug, "dark.css"); if (check) { - if (readFileSync(path, "utf8") !== expected) stale.push(slug); + if (!existsSync(path) || readFileSync(path, "utf8") !== expected) stale.push(slug); continue; } diff --git a/tests/build/smoke.mjs b/tests/build/smoke.mjs index 4b14f0b..c8ccc6d 100644 --- a/tests/build/smoke.mjs +++ b/tests/build/smoke.mjs @@ -7,6 +7,8 @@ import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; +import { themes } from "../../packages/jaamd/src/themes/index.ts"; + const DIST = join(process.cwd(), "www", "dist"); const PAGE = join(DIST, "index.html"); @@ -138,6 +140,51 @@ check( "the anchor icon stays invisible on the deeper levels", ); +// ─── themes ────────────────────────────────────────────────────────────────── + +const themeSlugs = [ + ...new Set([...html.matchAll(/html\[data-jaamd-theme="([a-z0-9-]+)"\]/g)].map((m) => m[1])), +]; + +check( + `themes: ${themeSlugs.length} scoped in the page`, + themeSlugs.length > 0, + "the layout did not inject the re-scoped theme CSS", +); + +// Authored on :root, so without re-scoping only the last import would ever win. +check( + "themes: none left on bare :root", + !/(^|\})\s*:root\s*\{[^}]*--jaamd-color-primary/.test(html), + "a theme reached the page unscoped and will override every other one", +); + +check( + "themes: the site bridge is scoped too", + /\[data-jaamd-theme=("?)default\1\]/.test(css), + "an unscoped bridge outranks the themes, so only secondary colours would change", +); + +// One Shiki variable per key is what lets code follow the switch; a dual theme +// needs two, or one of its two modes keeps the previous theme's syntax colours. +for (const theme of themes) { + const keys = + theme.mode === "dual" ? [theme.slug, `${theme.slug}-dark`] : [theme.slug]; + const absent = keys.filter((key) => !html.includes(`--shiki-${key}:`)); + + check( + `themes: ${theme.slug} has its Shiki colours baked`, + absent.length === 0, + `missing --shiki-${absent.join(", --shiki-")} in the rendered code`, + ); +} + +check( + "themes: the picker lists every theme plus the site's own", + countOf(/