create-zudo-doc 2.5.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,7 +31,7 @@ pnpm create zudo-doc my-docs --yes
31
31
  # Fully specified, non-interactive
32
32
  pnpm create zudo-doc my-docs \
33
33
  --lang ja \
34
- --scheme Dracula \
34
+ --scheme "Default Dark" \
35
35
  --no-i18n \
36
36
  --search \
37
37
  --pm pnpm \
@@ -109,11 +109,11 @@ pnpm create zudo-doc my-docs \
109
109
  --install \
110
110
  --yes
111
111
 
112
- # Light/dark color scheme with Catppuccin pairings
112
+ # Light/dark color scheme with the Default pairing
113
113
  pnpm create zudo-doc my-docs \
114
114
  --color-scheme-mode light-dark \
115
- --light-scheme "Catppuccin Latte" \
116
- --dark-scheme "Catppuccin Mocha" \
115
+ --light-scheme "Default Light" \
116
+ --dark-scheme "Default Dark" \
117
117
  --default-mode dark \
118
118
  --yes
119
119
 
@@ -130,7 +130,7 @@ await createZudoDoc({
130
130
  projectName: "my-docs",
131
131
  defaultLang: "en",
132
132
  colorSchemeMode: "single",
133
- singleScheme: "Dracula",
133
+ singleScheme: "Default Dark",
134
134
  features: ["search", "sidebarFilter", "tagGovernance"],
135
135
  packageManager: "pnpm",
136
136
  install: true,
package/dist/api.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import path from "path";
2
+ import { SINGLE_SCHEMES } from "./constants.js";
2
3
  import { scaffold } from "./scaffold.js";
3
4
  import { initGitRepo, installDependencies, validateProjectName } from "./utils.js";
4
5
  export async function createZudoDoc(options) {
@@ -6,6 +7,21 @@ export async function createZudoDoc(options) {
6
7
  const nameError = validateProjectName(rest.projectName);
7
8
  if (nameError)
8
9
  throw new Error(`Invalid projectName: ${nameError}`);
10
+ // Validate scheme names like the CLI (cli.ts) and preset (preset.ts) paths do.
11
+ // Only `Default Light`/`Default Dark` exist post-catalog-drop (#2619); an
12
+ // unvalidated name here would be written verbatim into settings.ts and throw
13
+ // "Unknown color scheme" at the generated site's build — the exact failure
14
+ // this epic set out to eliminate. This is the last scaffolding entry point
15
+ // that lacked the guard.
16
+ for (const [label, value] of [
17
+ ["color scheme", rest.singleScheme],
18
+ ["light scheme", rest.lightScheme],
19
+ ["dark scheme", rest.darkScheme],
20
+ ]) {
21
+ if (value && !SINGLE_SCHEMES.includes(value)) {
22
+ throw new Error(`Unknown ${label} "${value}"`);
23
+ }
24
+ }
9
25
  const choices = { ...rest, defaultLang: rest.defaultLang ?? "en" };
10
26
  await scaffold(choices);
11
27
  const targetDir = path.resolve(process.cwd(), choices.projectName);
package/dist/cli.js CHANGED
@@ -110,12 +110,7 @@ ${pc.bold("Examples:")}
110
110
  create-zudo-doc my-docs --yes
111
111
 
112
112
  ${pc.dim("# Fully specified")}
113
- create-zudo-doc my-docs --lang ja --scheme Dracula --no-i18n --pm pnpm --install
114
-
115
- ${pc.dim("# Light/dark mode with custom schemes")}
116
- create-zudo-doc my-docs --color-scheme-mode light-dark \\
117
- --light-scheme "GitHub Light" --dark-scheme "GitHub Dark" \\
118
- --default-mode dark --yes
113
+ create-zudo-doc my-docs --lang ja --scheme "Default Dark" --no-i18n --pm pnpm --install
119
114
  `);
120
115
  }
121
116
  export function validateArgs(args) {
package/dist/constants.js CHANGED
@@ -1,73 +1,12 @@
1
1
  export const LIGHT_DARK_PAIRINGS = [
2
2
  { light: "Default Light", dark: "Default Dark", label: "Default" },
3
- { light: "GitHub Light", dark: "GitHub Dark", label: "GitHub" },
4
- { light: "Catppuccin Latte", dark: "Catppuccin Mocha", label: "Catppuccin" },
5
- { light: "Solarized Light", dark: "Solarized Dark", label: "Solarized" },
6
- { light: "Rose Pine Dawn", dark: "Rose Pine", label: "Rosé Pine" },
7
- { light: "Atom One Light", dark: "Atom One Dark", label: "Atom One" },
8
- { light: "Everforest Light", dark: "Everforest Dark", label: "Everforest" },
9
- { light: "Gruvbox Light", dark: "Gruvbox Dark", label: "Gruvbox" },
10
- { light: "Ayu Light", dark: "Ayu Dark", label: "Ayu" },
11
- ];
12
- // All available single schemes (dark ones most popular first)
13
- export const SINGLE_SCHEMES = [
14
- "Default Dark",
15
- "Dracula",
16
- "Catppuccin Mocha",
17
- "GitHub Dark",
18
- "Nord",
19
- "TokyoNight",
20
- "Gruvbox Dark",
21
- "Atom One Dark",
22
- "Rose Pine",
23
- "Solarized Dark",
24
- "Material Ocean",
25
- "Monokai Pro",
26
- "Everforest Dark",
27
- "Kanagawa Wave",
28
- "Night Owl",
29
- "Ayu Dark",
30
- "VS Code Dark+",
31
- "Doom One",
32
- "Challenger Deep",
33
- "Catppuccin Frappe",
34
- "Catppuccin Macchiato",
35
- "Gruvbox Dark Hard",
36
- "Rose Pine Moon",
37
- "GitHub Dark Dimmed",
38
- "Ayu Mirage",
39
- "Material Darker",
40
- "Material Dark",
41
- "Monokai Remastered",
42
- "Monokai Vivid",
43
- "Monokai Soda",
44
- "Solarized Dark Higher Contrast",
45
- "Gruvbox Material Dark",
46
- "Kanagawa Dragon",
47
- // Light schemes
48
- "Default Light",
49
- "GitHub Light",
50
- "Catppuccin Latte",
51
- "Solarized Light",
52
- "Rose Pine Dawn",
53
- "Atom One Light",
54
- "Everforest Light",
55
- "Gruvbox Light",
56
- "Ayu Light",
57
3
  ];
4
+ // All available single schemes (dark-first ordering — asserted by the host
5
+ // sync test).
6
+ export const SINGLE_SCHEMES = ["Default Dark", "Default Light"];
58
7
  // Light-only subset of SINGLE_SCHEMES. Used by the preset generator to populate
59
8
  // the "Light scheme" dropdown (dark schemes are derived as SINGLE_SCHEMES minus these).
60
- export const LIGHT_SCHEMES = [
61
- "Default Light",
62
- "GitHub Light",
63
- "Catppuccin Latte",
64
- "Solarized Light",
65
- "Rose Pine Dawn",
66
- "Atom One Light",
67
- "Everforest Light",
68
- "Gruvbox Light",
69
- "Ayu Light",
70
- ];
9
+ export const LIGHT_SCHEMES = ["Default Light"];
71
10
  export const SUPPORTED_LANGS = [
72
11
  { value: "en", label: "English" },
73
12
  { value: "ja", label: "Japanese" },
package/dist/index.js CHANGED
@@ -89,7 +89,7 @@ async function main() {
89
89
  prefilled.respectPrefersColorScheme ??= true;
90
90
  }
91
91
  else {
92
- prefilled.singleScheme ??= "Dracula";
92
+ prefilled.singleScheme ??= "Default Dark";
93
93
  }
94
94
  prefilled.packageManager ??= "pnpm";
95
95
  prefilled.githubUrl ??= "";
package/dist/prompts.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as p from "@clack/prompts";
2
- import { LIGHT_DARK_PAIRINGS, SINGLE_SCHEMES, FEATURES, SUPPORTED_LANGS } from "./constants.js";
2
+ import { SINGLE_SCHEMES, FEATURES, SUPPORTED_LANGS } from "./constants.js";
3
3
  import { validateProjectName } from "./utils.js";
4
4
  export async function runPrompts(prefilled = {}) {
5
5
  // 1. Project name
@@ -88,48 +88,9 @@ export async function runPrompts(prefilled = {}) {
88
88
  darkScheme = prefilled.darkScheme;
89
89
  }
90
90
  else {
91
- const pairingChoice = await p.select({
92
- message: "Choose a light/dark pairing:",
93
- options: [
94
- ...LIGHT_DARK_PAIRINGS.map((pair) => ({
95
- value: pair.label,
96
- label: `${pair.light} + ${pair.dark}`,
97
- hint: pair.label,
98
- })),
99
- {
100
- value: "custom",
101
- label: "Pick individually",
102
- hint: "Choose light and dark schemes separately",
103
- },
104
- ],
105
- });
106
- if (p.isCancel(pairingChoice))
107
- process.exit(0);
108
- if (pairingChoice === "custom") {
109
- const lightSchemes = SINGLE_SCHEMES.filter((s) => ["Light", "Latte", "Dawn"].some((k) => s.includes(k)));
110
- const darkSchemes = SINGLE_SCHEMES.filter((s) => !["Light", "Latte", "Dawn"].some((k) => s.includes(k)));
111
- const light = await p.select({
112
- message: "Choose light scheme:",
113
- options: lightSchemes.map((s) => ({ value: s, label: s })),
114
- });
115
- if (p.isCancel(light))
116
- process.exit(0);
117
- lightScheme = light;
118
- const dark = await p.select({
119
- message: "Choose dark scheme:",
120
- options: darkSchemes.map((s) => ({ value: s, label: s })),
121
- });
122
- if (p.isCancel(dark))
123
- process.exit(0);
124
- darkScheme = dark;
125
- }
126
- else {
127
- const pairing = LIGHT_DARK_PAIRINGS.find((pair) => pair.label === pairingChoice);
128
- if (pairing) {
129
- lightScheme = pairing.light;
130
- darkScheme = pairing.dark;
131
- }
132
- }
91
+ // Only the Default pairing exists — auto-assign, no prompt.
92
+ lightScheme = "Default Light";
93
+ darkScheme = "Default Dark";
133
94
  }
134
95
  // Default mode
135
96
  if (prefilled.defaultMode === undefined) {
@@ -11,5 +11,5 @@ export { getSecondaryLang };
11
11
  *
12
12
  * Bumped in lockstep by scripts/release-create-zudo-doc.sh.
13
13
  */
14
- export declare const ZUDO_DOC_PIN = "^2.5.1";
14
+ export declare const ZUDO_DOC_PIN = "^3.0.0";
15
15
  export declare function scaffold(choices: UserChoices): Promise<void>;
package/dist/scaffold.js CHANGED
@@ -18,7 +18,7 @@ export { getSecondaryLang };
18
18
  *
19
19
  * Bumped in lockstep by scripts/release-create-zudo-doc.sh.
20
20
  */
21
- export const ZUDO_DOC_PIN = "^2.5.1";
21
+ export const ZUDO_DOC_PIN = "^3.0.0";
22
22
  /**
23
23
  * Files in `templates/base/**` that must never be copied into a generated
24
24
  * project. Each entry is matched against the path relative to `templates/base/`
@@ -582,7 +582,7 @@ function generatePackageJson(choices) {
582
582
  // @takazudo/zudo-doc/integrations/doc-history which in turn imports
583
583
  // @takazudo/zudo-doc-history-server/git-history. Without this dep the
584
584
  // plugin host fails at init with ERR_MODULE_NOT_FOUND — W8A (#1739).
585
- deps["@takazudo/zudo-doc-history-server"] = "^2.5.1";
585
+ deps["@takazudo/zudo-doc-history-server"] = "^3.0.0";
586
586
  // tsx is no longer needed here: the relocated package plugin imports the
587
587
  // runner directly (no `tsx -e` spawn) since the package ships compiled
588
588
  // dist/ — package-first migration #2321 (#2337).
@@ -593,7 +593,7 @@ function generatePackageJson(choices) {
593
593
  if (choices.features.includes("designTokenPanel")) {
594
594
  // @takazudo/zdtp requires preact >= 10.29.1 — see the preact floor comment
595
595
  // above (~line 382) for why the floor is set there and the coupling this creates.
596
- deps["@takazudo/zdtp"] = "0.4.2";
596
+ deps["@takazudo/zdtp"] = "0.4.5";
597
597
  }
598
598
  if (choices.features.includes("tagGovernance")) {
599
599
  // gray-matter is already in `deps` unconditionally (base template uses it),
@@ -35,15 +35,15 @@ export function generateSettingsFile(choices) {
35
35
  lines.push(``);
36
36
  lines.push(`export const settings = {`);
37
37
  if (choices.colorSchemeMode === "single") {
38
- lines.push(` colorScheme: ${JSON.stringify(choices.singleScheme ?? "Dracula")},`);
38
+ lines.push(` colorScheme: ${JSON.stringify(choices.singleScheme ?? "Default Dark")},`);
39
39
  lines.push(` colorMode: false as ColorModeConfig | false,`);
40
40
  }
41
41
  else {
42
- lines.push(` colorScheme: ${JSON.stringify(choices.darkScheme ?? "GitHub Dark")},`);
42
+ lines.push(` colorScheme: ${JSON.stringify(choices.darkScheme ?? "Default Dark")},`);
43
43
  lines.push(` colorMode: {`);
44
44
  lines.push(` defaultMode: ${JSON.stringify(choices.defaultMode ?? "dark")},`);
45
- lines.push(` lightScheme: ${JSON.stringify(choices.lightScheme ?? "GitHub Light")},`);
46
- lines.push(` darkScheme: ${JSON.stringify(choices.darkScheme ?? "GitHub Dark")},`);
45
+ lines.push(` lightScheme: ${JSON.stringify(choices.lightScheme ?? "Default Light")},`);
46
+ lines.push(` darkScheme: ${JSON.stringify(choices.darkScheme ?? "Default Dark")},`);
47
47
  lines.push(` respectPrefersColorScheme: ${choices.respectPrefersColorScheme ?? true},`);
48
48
  lines.push(` } satisfies ColorModeConfig as ColorModeConfig | false,`);
49
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zudo-doc",
3
- "version": "2.5.1",
3
+ "version": "3.0.0",
4
4
  "description": "Create a new zudo-doc documentation site",
5
5
  "license": "MIT",
6
6
  "author": "Takeshi Takatsudo",
@@ -1,21 +1,30 @@
1
1
  /**
2
2
  * Project-side color-scheme helpers.
3
3
  *
4
- * The MECHANISM (palette→CSS injection logic) now lives in the package:
4
+ * The MECHANISM (ramp→CSS injection logic) now lives in the package:
5
5
  * `@takazudo/zudo-doc/color-scheme-utils`. This file re-exports those
6
6
  * mechanism symbols and adds project-specific thin wrappers that read from
7
- * this project's `colorSchemes` map and `settings`. The DATA (palette values,
7
+ * this project's `colorSchemes` map and `settings`. The DATA (ramp values,
8
8
  * scheme names) stays in `./color-schemes` and `./settings`.
9
9
  *
10
10
  * S9a package-first migration — zudolab/zudo-doc#2333.
11
+ * Ramp-native rewrite — zudolab/zudo-doc#2585/#2586.
11
12
  */
12
13
 
13
14
  export {
14
- type ColorRef,
15
+ type OKLCH,
16
+ type StateRole,
17
+ type SemanticKey,
18
+ type RampRef,
19
+ type Ramps,
20
+ type ModeMap,
15
21
  type ColorScheme,
16
- SEMANTIC_DEFAULTS,
22
+ type CssEmitScope,
23
+ STATE_ROLES,
24
+ SEMANTIC_KEYS,
25
+ SEMANTIC_RAMP_DEFAULTS,
17
26
  SEMANTIC_CSS_NAMES,
18
- resolveColor,
27
+ resolveRampRef,
19
28
  resolveSemanticColors,
20
29
  schemeToCssPairs,
21
30
  generateCssCustomProperties as generateCssCustomPropertiesFromScheme,
@@ -1,136 +1,165 @@
1
- /** A color reference: palette index (number) or direct color value (string) */
2
- export type ColorRef = number | string;
1
+ /**
2
+ * Ramp-native color schemes (Color Ramp Restructure — zudolab/zudo-doc#2584;
3
+ * minimize pass #2601 / #2602).
4
+ *
5
+ * A `ColorScheme` is `{ ramps, map }` (the MECHANISM types live in the package
6
+ * and are re-exported by `./color-scheme-utils`):
7
+ * - `ramps` — the shared Tier-1 source of truth: a warm-neutral `base` ramp
8
+ * (5 stops, index 0 = lightest), an `accent` ramp (3 stops), and 4 `state`
9
+ * colors. Light and dark modes SHARE these values.
10
+ * - `map` — the per-mode Tier-2 wiring: which ramp stop (or literal OKLCH)
11
+ * each UI role points at.
12
+ *
13
+ * The palette was minimized from base=12 / accent=7 to **base=5 / accent=3**
14
+ * (#2602): the ramp-native engine is length-agnostic, so this is a DATA change.
15
+ * Semantic roles are aggressively merged onto shared stops to keep the number of
16
+ * distinct tones small — most notably `surface`, `codeBg`(light) and
17
+ * `chatAssistantBg` collapse onto `bg`, so header Version boxes and doc cards
18
+ * render as page-bg + border only (no gray fill). 5 stops leave no subtle
19
+ * near-white, so light-mode elevated fills are border-only by design.
20
+ *
21
+ * `ColorScheme` is re-exported here so the many sites that still
22
+ * `import { ColorScheme } from "./color-schemes"` (contrast tooling,
23
+ * zfb.config.ts, …) keep resolving until their own waves port them.
24
+ */
3
25
 
4
- export interface ColorScheme {
5
- background: ColorRef;
6
- foreground: ColorRef;
7
- cursor: ColorRef;
8
- selectionBg: ColorRef;
9
- selectionFg: ColorRef;
10
- palette: [
11
- string, string, string, string, string, string, string, string,
12
- string, string, string, string, string, string, string, string,
13
- ];
14
- /** Optional, vestigial. Carried only in the optional color-scheme config
15
- * envelope consumed by the design token panel tooling (falls back to
16
- * DEFAULT_SHIKI_THEME when omitted), but has no visible effect: that
17
- * tooling's Shiki integration is a no-op stub, and page code highlighting is
18
- * done by syntect (dual-theme, configured via `codeHighlight` in
19
- * zfb.config.ts), not Shiki. */
20
- shikiTheme?: string;
21
- /** Optional semantic overrides — when omitted, defaults are used:
22
- * surface=p0, muted=p8, accent=p5, accentHover=p14
23
- * codeBg=p10, codeFg=p11, success=p2, danger=p1, warning=p3, info=p4
24
- * Each field accepts a palette index (number) or a direct color value (string). */
25
- semantic?: {
26
- surface?: ColorRef;
27
- muted?: ColorRef;
28
- accent?: ColorRef;
29
- accentHover?: ColorRef;
30
- codeBg?: ColorRef;
31
- codeFg?: ColorRef;
32
- success?: ColorRef;
33
- danger?: ColorRef;
34
- warning?: ColorRef;
35
- info?: ColorRef;
36
- mermaidNodeBg?: ColorRef;
37
- mermaidText?: ColorRef;
38
- mermaidLine?: ColorRef;
39
- mermaidLabelBg?: ColorRef;
40
- mermaidNoteBg?: ColorRef;
41
- chatUserBg?: ColorRef;
42
- chatUserText?: ColorRef;
43
- chatAssistantBg?: ColorRef;
44
- chatAssistantText?: ColorRef;
45
- /** UI chrome over user images — enlarge/close overlay buttons */
46
- imageOverlayBg?: ColorRef;
47
- imageOverlayFg?: ColorRef;
48
- /** <mark> highlight for matched keywords in search results */
49
- matchedKeywordBg?: ColorRef;
50
- matchedKeywordFg?: ColorRef;
51
- };
52
- }
26
+ import type { ColorScheme, Ramps, ModeMap } from "./color-scheme-utils";
27
+
28
+ export type { ColorScheme } from "./color-scheme-utils";
53
29
 
54
30
  /**
55
- * Standard palette index convention (all schemes should follow this):
56
- *
57
- * | Index | Role | Description |
58
- * |-------|-------------------|------------------------------------------|
59
- * | p0 | Dark surface | Deepest surface (code blocks, mermaid) |
60
- * | p1 | Danger | Red family — errors, destructive actions |
61
- * | p2 | Success | Green family — confirmations, tips |
62
- * | p3 | Warning | Yellow/amber — caution messages |
63
- * | p4 | Info | Blue family — informational highlights |
64
- * | p5 | Accent | Primary interactive color (links, CTA) |
65
- * | p6 | Neutral | Slate/cyan — borders, secondary elements |
66
- * | p7 | Secondary neutral | Gray or muted accent |
67
- * | p8 | Muted | Gray — borders, secondary text, comments |
68
- * | p9 | Background | Page background |
69
- * | p10 | Surface | Elevated surface (panels, sidebars) |
70
- * | p11 | Text primary | Main body text |
71
- * | p12 | Accent variant | Brighter or alternate accent |
72
- * | p13 | Decorative | Purple/lavender — non-semantic decoration |
73
- * | p14 | Accent hover | Hover state for interactive elements |
74
- * | p15 | Text secondary | Secondary text or muted foreground |
31
+ * Shared Tier-1 ramps identical across Default Light and Default Dark.
32
+ * Minimized to 5 base / 3 accent (#2602). The warm-neutral spine keeps hue 65;
33
+ * the accent stops are the three tuned orange stops carried over from the 7-stop
34
+ * ramp (old accent2/3/6, re-indexed). Index 0 = lightest.
75
35
  */
76
- export const colorSchemes: Record<string, ColorScheme> = {
77
- "Default Light": {
78
- background: 9,
79
- foreground: 11,
80
- cursor: 6,
81
- selectionBg: 11,
82
- selectionFg: 10,
83
- palette: [
84
- "oklch(0.309 0.000 0.00)" /* #303030 */, "oklch(0.453 0.172 27.68)" /* #a01515 */, "oklch(0.398 0.090 147.43)" /* #1f5429 */, "oklch(0.451 0.130 23.94)" /* #903030 */, // p0-3: dark surface, danger, success, warning — darkened for WCAG AA (#2298)
85
- "oklch(0.441 0.144 258.56)" /* #174fa0 */, "oklch(0.453 0.0997 61.17)" /* #7d470b */, "oklch(0.704 0.040 256.99)" /* #90a1b9 */, "oklch(0.472 0.089 71.81)" /* #7a5218 */, // p4-7: info, accent, neutral, secondary — darkened for WCAG AA (#2298)
86
- "oklch(0.528 0.000 0.00)" /* #6b6b6b */, "oklch(0.901 0.007 53.44)" /* #e2ddda */, "oklch(0.936 0.003 17.22)" /* #ece9e9 */, "oklch(0.309 0.000 0.00)" /* #303030 */, // p8-11: muted, background, surface, text
87
- "oklch(0.670 0.119 251.69)" /* #5b99dc */, "oklch(0.749 0.106 300.21)" /* #b89ee7 */, "oklch(0.650 0.027 257.67)" /* #8590a0 */, "oklch(0.417 0.0755 72.95)" /* #654516 */, // p12-15: accent variant, decorative, hover, muted foreground
88
- ],
89
- semantic: {
90
- surface: 10,
91
- muted: "oklch(0.492 0.000 0.00)" /* upstream #6b6b6b (p8) → L-0.036 for AA (scheme-a11y #2489) */, // muted-vs-bg → 4.61:1
92
- accent: 5,
93
- accentHover: "oklch(0.492 0.027 257.67)" /* upstream #8590a0 (p14) → L-0.158, C-0.000 for AA (scheme-a11y #2489) */, // accent-hover-vs-bg → 4.61:1
94
- codeBg: 10,
95
- codeFg: 11,
96
- success: 2,
97
- danger: 1,
98
- warning: 3,
99
- info: 4,
100
- mermaidNoteBg: "oklch(0.821 0.007 53.44)" /* derived bg-elevated from bg (p9) default (p0) collided 1:1 with mermaidText/p11 (scheme-a11y #2489) */,
101
- imageOverlayBg: 11,
102
- imageOverlayFg: 10,
103
- matchedKeywordBg: "oklch(0.959 0.109 102.63)" /* #fff59d */,
104
- matchedKeywordFg: "oklch(0.000 0.000 0.00)" /* #000000 */,
105
- },
36
+ const ramps: Ramps = {
37
+ base: [
38
+ "oklch(.965 .004 65)", // 0 — lightest (light bg / dark fg)
39
+ "oklch(.705 .008 65)", // 1 — dark muted / light selection & mermaid fill
40
+ "oklch(.480 .008 65)", // 2 — light muted / dark selection & mermaid note
41
+ "oklch(.300 .006 65)", // 3 — dark codeBg / mermaid fill
42
+ "oklch(.185 .005 65)", // 4 — darkest (dark bg / light fg)
43
+ ],
44
+ accent: [
45
+ "oklch(.755 .130 64)", // 0 dark hover (was accent2)
46
+ "oklch(.700 .158 62)", // 1 dark accent (was accent3)
47
+ "oklch(.470 .120 56)", // 2 light accent (was accent6)
48
+ ],
49
+ state: {
50
+ danger: "oklch(.640 .170 25)",
51
+ success: "oklch(.680 .145 145)",
52
+ warning: "oklch(.760 .135 82)",
53
+ info: "oklch(.680 .130 245)",
54
+ },
55
+ };
56
+
57
+ /**
58
+ * Default Dark — the authored reference scheme (epic #2584; minimized #2602).
59
+ * bg = darkest base stop, fg = lightest. Roles merged onto shared stops:
60
+ * `surface`, `chatAssistantBg`, `imageOverlayBg` all = `bg` (b4) so elevated
61
+ * panels are bg + border only. Per-mode AA-tuned literals: `danger`,
62
+ * `matchedKeyword*` (carried from #2593). Full WCAG matrix passes at
63
+ * threshold+0.1.
64
+ */
65
+ const darkMap: ModeMap = {
66
+ bg: { base: 4 },
67
+ fg: { base: 0 },
68
+ selectionBg: { base: 2 },
69
+ selectionFg: { base: 0 },
70
+ semantic: {
71
+ // Merged onto bg (b4) — elevated panels read as page-bg + border, no gray fill.
72
+ surface: { base: 4 },
73
+ muted: { base: 1 },
74
+ accent: { accent: 1 },
75
+ accentHover: { accent: 0 },
76
+ codeBg: { base: 3 },
77
+ codeFg: { base: 0 },
78
+ success: { state: "success" },
79
+ // Per-mode AA-tuned literal — shared state.danger oklch(.640 .170 25) is too
80
+ // dark for the danger-admonition title on its 12%-tint dark bg. L .640→.655
81
+ // (H/C fixed) — admonition-danger at threshold+0.1; carried from #2593.
82
+ danger: "oklch(.655 .170 25)",
83
+ warning: { state: "warning" },
84
+ info: { state: "info" },
85
+ mermaidNodeBg: { base: 3 },
86
+ mermaidText: { base: 0 },
87
+ mermaidLine: { base: 1 },
88
+ mermaidLabelBg: { base: 3 },
89
+ mermaidNoteBg: { base: 2 },
90
+ chatUserBg: { accent: 1 },
91
+ chatUserText: { base: 4 },
92
+ // Merged onto bg (b4) — the chat assistant bubble reads as page-bg + border.
93
+ chatAssistantBg: { base: 4 },
94
+ chatAssistantText: { base: 0 },
95
+ imageOverlayBg: { base: 4 },
96
+ imageOverlayFg: { base: 0 },
97
+ // Search-result <mark> highlight: an amber (accent-hue) fill with dark text —
98
+ // the classic highlighter look, identical in both modes. Kept as literals so
99
+ // the amber fill is scheme-stable; matchedKeywordFg dark so text clears AA on
100
+ // the amber bg — matched-keyword at threshold+0.1; carried from #2593.
101
+ matchedKeywordBg: "oklch(.700 .158 62)",
102
+ matchedKeywordFg: "oklch(.300 .003 65)",
106
103
  },
107
- "Default Dark": {
108
- background: 9,
109
- foreground: 15,
110
- cursor: 6,
111
- selectionBg: 10,
112
- selectionFg: 11,
113
- palette: [
114
- "oklch(0.226 0.000 0.00)" /* #1c1c1c */, "oklch(0.656 0.143 16.99)" /* #da6871 */, "oklch(0.746 0.103 133.16)" /* #93bb77 */, "oklch(0.809 0.096 82.54)" /* #dfbb77 */, // p0-3: dark surface, danger, success, warning
115
- "oklch(0.714 0.120 245.01)" /* #5caae9 */, "oklch(0.677 0.160 318.34)" /* #c074d6 */, "oklch(0.704 0.040 256.99)" /* #90a1b9 */, "oklch(0.706 0.000 0.00)" /* #a0a0a0 */, // p4-7: info, accent, neutral, secondary
116
- "oklch(0.627 0.000 0.00)" /* #888888 */, "oklch(0.209 0.000 0.00)" /* #181818 */, "oklch(0.341 0.000 0.00)" /* #383838 */, "oklch(0.907 0.000 0.00)" /* #e0e0e0 */, // p8-11: muted, background, surface, text
117
- "oklch(0.733 0.099 61.25)" /* #d69a66 */, "oklch(0.677 0.160 318.34)" /* #c074d6 */, "oklch(0.801 0.057 256.82)" /* #a7c0e3 */, "oklch(0.783 0.000 0.00)" /* #b8b8b8 */, // p12-15: accent variant, decorative, hover, text secondary
118
- ],
119
- semantic: {
120
- surface: 0,
121
- muted: "oklch(0.657 0.000 0.00)" /* upstream #888888 (p8) → L+0.030 for AA (scheme-a11y #2489) */, // muted-vs-{bg,codeBg,chatAssistantBg} → 4.62:1
122
- accent: 12,
123
- accentHover: 14,
124
- codeBg: "oklch(0.281 0.000 0.00)" /* upstream #383838 (p10) → L-0.060 toward bg for muted-vs-codeBg AA (rendered-bg #2510) */,
125
- codeFg: 11,
126
- success: 2,
127
- danger: "oklch(0.662 0.143 16.99)" /* upstream #da6871 → L+0.006 for AA (scheme-a11y #2489) */, // admonition-danger → 4.61:1
128
- warning: 3,
129
- info: 4,
130
- imageOverlayBg: 0,
131
- imageOverlayFg: 11,
132
- matchedKeywordBg: "oklch(0.959 0.109 102.63)" /* #fff59d */,
133
- matchedKeywordFg: "oklch(0.000 0.000 0.00)" /* #000000 */,
134
- },
104
+ };
105
+
106
+ /**
107
+ * Default Light — the authored light-mode scheme (epic #2584; minimized #2602).
108
+ * Shares `ramps` with Default Dark; the `map` inverts (light bg = lightest base
109
+ * stop, dark fg = darkest). Roles merged onto shared stops: `surface`, `codeBg`,
110
+ * `chatAssistantBg` all = `bg` (b0) so elevated panels are bg + border only
111
+ * (the intended flat, few-tone look 5 stops leave no subtle near-white fill).
112
+ * The accent/state colors authored for a dark bg need darker per-mode
113
+ * literals to clear AA on a near-white bg. Full WCAG matrix passes at
114
+ * threshold+0.1; ramps untouched so Default Dark is unaffected.
115
+ */
116
+ const lightMap: ModeMap = {
117
+ bg: { base: 0 },
118
+ fg: { base: 4 },
119
+ selectionBg: { base: 1 },
120
+ selectionFg: { base: 4 },
121
+ semantic: {
122
+ // Merged onto bg (b0) — elevated panels read as page-bg + border, no gray fill.
123
+ surface: { base: 0 },
124
+ muted: { base: 2 },
125
+ accent: { accent: 2 },
126
+ // Light: per-mode literal — the accent ramp has no stop darker than accent2,
127
+ // so the link-hover state darkens further (hover-darkens-on-light
128
+ // convention). L .400, C fitted to the sRGB gamut edge at that L; carried
129
+ // from #2595. accent-hover-vs-bg clears threshold+0.1.
130
+ accentHover: "oklch(.400 .096 56)",
131
+ // Merged onto bg (b0) — inline/code-block fill reads as page-bg + border.
132
+ codeBg: { base: 0 },
133
+ codeFg: { base: 4 },
134
+ // Light: per-mode literals — the shared state colors are authored for a dark
135
+ // bg and are too light on the 12%-tint admonition backgrounds over a light
136
+ // page bg. Each darkened (H fixed; C at gamut max, clipped where noted) to
137
+ // clear its admonition pair at threshold+0.1; carried from #2595.
138
+ success: "oklch(.470 .140 145)",
139
+ danger: "oklch(.505 .170 25)",
140
+ warning: "oklch(.490 .100 82)", // C .135→.100 (gamut-clips at this L)
141
+ info: "oklch(.485 .122 245)",
142
+ mermaidNodeBg: { base: 1 },
143
+ mermaidText: { base: 4 },
144
+ mermaidLine: { base: 2 },
145
+ mermaidLabelBg: { base: 1 },
146
+ mermaidNoteBg: { base: 1 },
147
+ chatUserBg: { accent: 1 },
148
+ // Dark text on the amber user bubble (matches Default Dark).
149
+ chatUserText: { base: 4 },
150
+ // Merged onto bg (b0) — the chat assistant bubble reads as page-bg + border.
151
+ chatAssistantBg: { base: 0 },
152
+ chatAssistantText: { base: 4 },
153
+ imageOverlayBg: { base: 4 },
154
+ imageOverlayFg: { base: 0 },
155
+ // Search-result <mark>: amber highlighter fill with dark text — same look as
156
+ // Default Dark (an amber-on-white highlight reads identically in both modes).
157
+ matchedKeywordBg: "oklch(.700 .158 62)",
158
+ matchedKeywordFg: "oklch(.300 .003 65)",
135
159
  },
136
160
  };
161
+
162
+ export const colorSchemes: Record<string, ColorScheme> = {
163
+ "Default Light": { ramps, map: lightMap },
164
+ "Default Dark": { ramps, map: darkMap },
165
+ };
@@ -74,18 +74,10 @@
74
74
  * Tight token strategy: import preflight + utilities only (no default theme).
75
75
  * Only project-specific tokens are available.
76
76
  *
77
- * Raw palette: --zd-0 to --zd-15 + bg/fg/sel (injected by ColorSchemeProvider)
77
+ * Base roles: --zd-bg/--zd-fg/--zd-selection-bg/--zd-selection-fg + ramps
78
+ * (--palette-base-0..4 / --palette-accent-0..2 / --palette-state-*) +
79
+ * 23 --zd-{role} semantic tokens, all injected by ColorSchemeProvider.
78
80
  * Tailwind tokens: registered below via @theme
79
- *
80
- * Palette slots:
81
- * 0/8: black / bright-black (surfaces, muted text, borders)
82
- * 1/9: red / bright-red
83
- * 2/10: green / bright-green
84
- * 3/11: yellow / bright-yellow
85
- * 4/12: blue / bright-blue
86
- * 5/13: magenta / bright-magenta
87
- * 6/14: cyan / bright-cyan
88
- * 7/15: white / bright-white
89
81
  * ======================================== */
90
82
 
91
83
  @theme {
@@ -117,26 +109,8 @@
117
109
  /* ── Base ── */
118
110
  --color-bg: var(--zd-bg);
119
111
  --color-fg: var(--zd-fg);
120
- --color-sel-bg: var(--zd-sel-bg);
121
- --color-sel-fg: var(--zd-sel-fg);
122
-
123
- /* ── Raw palette (p0–p15) ── */
124
- --color-p0: var(--zd-0);
125
- --color-p1: var(--zd-1);
126
- --color-p2: var(--zd-2);
127
- --color-p3: var(--zd-3);
128
- --color-p4: var(--zd-4);
129
- --color-p5: var(--zd-5);
130
- --color-p6: var(--zd-6);
131
- --color-p7: var(--zd-7);
132
- --color-p8: var(--zd-8);
133
- --color-p9: var(--zd-9);
134
- --color-p10: var(--zd-10);
135
- --color-p11: var(--zd-11);
136
- --color-p12: var(--zd-12);
137
- --color-p13: var(--zd-13);
138
- --color-p14: var(--zd-14);
139
- --color-p15: var(--zd-15);
112
+ --color-sel-bg: var(--zd-selection-bg);
113
+ --color-sel-fg: var(--zd-selection-fg);
140
114
 
141
115
  /* ── Semantic aliases ── */
142
116
  --color-surface: var(--zd-surface);
@@ -1,17 +1,61 @@
1
1
  /**
2
2
  * zdtp (zudo-design-token-panel) PanelConfig for this project.
3
3
  *
4
- * This config object is the single source of truth passed to
5
- * `configurePanel(designTokenPanelConfig)` in the bootstrap module.
4
+ * Single source of truth passed to `bootstrapDesignTokenPanel(...)` in
5
+ * `src/lib/design-token-panel-bootstrap.ts`.
6
6
  *
7
- * Type notes:
8
- * - zdtp's `ColorScheme` requires a `shikiTheme: string` field that is not
9
- * present in this project's local `ColorScheme` type or data (zdtp uses it
10
- * only for the panel's client-side code-block preview). Rather than an unsafe
11
- * `as unknown as Record<string, ZdtpColorScheme>` double-cast, every local
12
- * scheme map is run through `toZdtpColorSchemes()` below, which supplies
13
- * `DEFAULT_SHIKI_THEME` as the fallback so the result satisfies zdtp's
14
- * required-field shape with an ordinary type-checked assignment.
7
+ * Ramp-native model (mirrors zudo-doc's Color Ramp Restructure —
8
+ * zudolab/zudo-doc#2584 / #2592; mode-scoped Color tab #2606 / #2610)
9
+ * -------------------------------------------------------------------------
10
+ * The color model is ramp-native (`ColorScheme = { ramps, map }`, see
11
+ * `src/config/color-schemes.ts`). The panel surfaces it through two tabs:
12
+ *
13
+ * - **Palette tab** (reserved id `palette`): three `kind:'color'` OKLCH tiers —
14
+ * `base` (5 stops `--palette-base-0..4`), `accent` (3 stops →
15
+ * `--palette-accent-0..2`), and `state` (4 roles → `--palette-state-{role}`).
16
+ * zdtp's native L/C/H curve editor renders these. Per zdtp's palette-tab
17
+ * contract, a tab carrying MULTIPLE `kind:'color'` tiers MUST omit
18
+ * `colorExtras` (otherwise `resolveColorClusterFromTab` cannot pick a single
19
+ * palette tier). The ramps are shared across light/dark, so token defaults
20
+ * are read from the active scheme's `ramps` and stay in sync with
21
+ * `color-schemes.ts`. The cssVars match the `--palette-*` custom properties
22
+ * the ColorSchemeProvider emits (`schemeToCssPairs`).
23
+ *
24
+ * - **Color tab** (reserved id `color`): a single `semantic` tier holding the
25
+ * 4 base roles (`--zd-bg`/`--zd-fg`/`--zd-selection-{bg,fg}`) + 23 `--zd-*`
26
+ * semantic roles, each rendered as a grouped ramp dropdown. The tier declares
27
+ * `referencesRamps` pointing at the Palette tab's `base`/`accent`/`state`
28
+ * tiers, so each row's `default` is the encoded `tierId:itemId` ramp
29
+ * reference (`buildSemanticTierItems` / `rampRefToPanelDefault`), the picker
30
+ * renders grouped `<optgroup>` dropdowns, and editing emits live
31
+ * `var(--palette-*)`. The tier carries NO `referencesTier` — that resolves
32
+ * intra-tab only; the cross-tab wiring is `referencesRamps` + `semantic:true`.
33
+ *
34
+ * Mode-scoped defaults
35
+ * --------------------
36
+ * The Color tab's semantic defaults are MODE-SCOPED: `buildDesignTokenPanelConfig(mode)`
37
+ * seeds the tier from the active mode's scheme (light map vs dark map). The
38
+ * bootstrap (`@takazudo/zudo-doc/design-token-panel-bootstrap`) destroys +
39
+ * reconfigures the panel on every `color-scheme-changed` toggle so its defaults
40
+ * follow the live light/dark mode.
41
+ *
42
+ * Caveat: a *saved* color OVERRIDE is still mode-AGNOSTIC here — but this is
43
+ * this project's config-shape choice, not a zdtp limitation. zdtp 0.4.5 ships
44
+ * per-scheme/per-mode keyed color persistence (v4 envelope,
45
+ * Takazudo/zudo-design-token-panel#500 / #509): the color slice is keyed by
46
+ * the cluster's resolved scheme identity (`panelSettings.colorScheme` /
47
+ * `colorMode`). This project's color cluster is scheme-less
48
+ * (`colorExtras.colorSchemes = {}`, no `colorMode`) and switches modes
49
+ * externally via the destroy+reconfigure dance above rather than zdtp's own
50
+ * `colorMode` field, so zdtp always resolves the same single (stub) scheme
51
+ * identity and an override repaints both modes until Reset. Only the
52
+ * DEFAULTS are mode-faithful. Do NOT work around this by reaching into zdtp's
53
+ * private storage keys.
54
+ *
55
+ * The color cluster is **scheme-less**: `colorExtras.colorSchemes = {}` (zdtp's
56
+ * documented scheme-less cluster shape) — the ramps ARE the editable source of
57
+ * truth, surfaced by the Palette tab, so a bundled scheme-preset registry no
58
+ * longer applies.
15
59
  */
16
60
 
17
61
  import type {
@@ -20,7 +64,6 @@ import type {
20
64
  TierConfig,
21
65
  TierItem,
22
66
  ColorClusterExtras,
23
- ColorScheme as ZdtpColorScheme,
24
67
  TokenDef,
25
68
  } from "@takazudo/zdtp";
26
69
  import {
@@ -28,66 +71,23 @@ import {
28
71
  FONT_TOKENS,
29
72
  SIZE_TOKENS,
30
73
  } from "./design-tokens-manifest";
74
+ import {
75
+ getActiveScheme,
76
+ STATE_ROLES,
77
+ type ColorScheme,
78
+ } from "./color-scheme-utils";
79
+ import { buildSemanticTierItems } from "@takazudo/zudo-doc/color-scheme-utils";
31
80
  import { colorSchemes } from "./color-schemes";
32
- import type { ColorScheme as LocalColorScheme } from "./color-schemes";
33
- import { SEMANTIC_DEFAULTS, SEMANTIC_CSS_NAMES } from "./color-scheme-utils";
34
81
  import { settings } from "./settings";
35
- import { DESIGN_TOKEN_SCHEMA } from "@takazudo/zudo-doc/theme";
36
82
 
37
83
  /**
38
- * Base-role fallback indices. Background defaults to palette index 0,
39
- * foreground to 15, cursor to 6, selection to 0/15.
40
- */
41
- const BASE_DEFAULTS = {
42
- background: 0,
43
- foreground: 15,
44
- cursor: 6,
45
- selectionBg: 0,
46
- selectionFg: 15,
47
- } as const;
48
-
49
- /**
50
- * Fallback Shiki theme used when a color scheme's `shikiTheme` field is absent.
84
+ * Inert fallback for the still-REQUIRED `ColorClusterExtras.defaultShikiTheme`.
85
+ * zdtp's Shiki integration is a no-op stub, so this value has no visible
86
+ * effect if you highlight code some other way — but the field is typed
87
+ * `string`, so a value is required.
51
88
  */
52
89
  const DEFAULT_SHIKI_THEME = "github-dark";
53
90
 
54
- /**
55
- * Normalize this project's local `ColorScheme` records into zdtp's
56
- * `ColorScheme` shape. zdtp's type requires `shikiTheme: string`; the local
57
- * scheme type makes it optional. This helper fills `DEFAULT_SHIKI_THEME` only
58
- * when a scheme doesn't declare its own, so the result is assignable to
59
- * `Record<string, ZdtpColorScheme>` via an ordinary type-checked assignment —
60
- * replacing the previous `as unknown as` double-cast that bypassed every field
61
- * check. Tracked upstream at Takazudo/zudo-design-token-panel#342 (shikiTheme
62
- * should be optional in zdtp's `ColorScheme` type); drop this helper once that
63
- * lands.
64
- */
65
- function toZdtpColorSchemes(
66
- schemes: Record<string, LocalColorScheme>,
67
- ): Record<string, ZdtpColorScheme> {
68
- const normalized: Record<string, ZdtpColorScheme> = {};
69
- for (const [name, scheme] of Object.entries(schemes)) {
70
- normalized[name] = {
71
- ...scheme,
72
- shikiTheme: scheme.shikiTheme ?? DEFAULT_SHIKI_THEME,
73
- };
74
- }
75
- return normalized;
76
- }
77
-
78
- /**
79
- * Initial palette taken from the configured active scheme.
80
- */
81
- function getInitialPalette(): readonly string[] {
82
- const scheme = colorSchemes[settings.colorScheme];
83
- if (!scheme) {
84
- throw new Error(
85
- `Unknown color scheme: "${settings.colorScheme}". Available: ${Object.keys(colorSchemes).join(", ")}`,
86
- );
87
- }
88
- return scheme.palette;
89
- }
90
-
91
91
  // ---------------------------------------------------------------------------
92
92
  // Helpers — partition flat manifest arrays into TabConfig.tiers by group.
93
93
  // ---------------------------------------------------------------------------
@@ -129,6 +129,9 @@ function toTierItem(t: TokenDef): TierItem {
129
129
 
130
130
  /**
131
131
  * Build a tier from the subset of `tokens` whose `group` matches `groupId`.
132
+ * The tier's items share the same kind by construction (all items in a
133
+ * group share the same slider/select/text shape in this project's manifest).
134
+ * zdtp's `assertValidTabs` validator requires this.
132
135
  */
133
136
  function tierFromGroup(
134
137
  tokens: readonly TokenDef[],
@@ -145,100 +148,203 @@ function tierFromGroup(
145
148
  }
146
149
 
147
150
  // ---------------------------------------------------------------------------
148
- // Color tab — palette and semantic tiers + colorExtras for cluster metadata.
151
+ // Palette tab — three ramp tiers (base / accent / state), OKLCH curve editor.
149
152
  // ---------------------------------------------------------------------------
150
153
 
151
- const PALETTE_TIER_ID = "palette";
154
+ /**
155
+ * Build the three ramp tiers from the active scheme's shared `ramps`. Light
156
+ * and dark schemes share the same Tier-1 ramps, so the active scheme's ramps
157
+ * are the single source of truth — read from here rather than hardcoding a
158
+ * second copy of the values.
159
+ */
160
+ function buildRampTiers(): TierConfig[] {
161
+ const { ramps } = getActiveScheme();
152
162
 
153
- function buildPaletteTier(): TierConfig {
154
- const initial = getInitialPalette();
155
- const items: TierItem[] = [];
156
- for (let i = 0; i < 16; i++) {
157
- items.push({
158
- id: `p${i}`,
159
- cssVar: `--zd-${i}`,
160
- label: `p${i}`,
161
- default: initial[i] ?? "#808080",
162
- type: { kind: "color" },
163
- });
164
- }
165
- return {
166
- id: PALETTE_TIER_ID,
167
- label: "Palette",
168
- items,
163
+ const baseTier: TierConfig = {
164
+ id: "base",
165
+ label: "Base",
166
+ items: ramps.base.map((color, i): TierItem => ({
167
+ id: `base-${i}`,
168
+ cssVar: `--palette-base-${i}`,
169
+ label: String(i),
170
+ default: color,
171
+ type: { kind: "color", format: "oklch" },
172
+ })),
173
+ };
174
+
175
+ const accentTier: TierConfig = {
176
+ id: "accent",
177
+ label: "Accent",
178
+ items: ramps.accent.map((color, i): TierItem => ({
179
+ id: `accent-${i}`,
180
+ cssVar: `--palette-accent-${i}`,
181
+ label: String(i),
182
+ default: color,
183
+ type: { kind: "color", format: "oklch" },
184
+ })),
185
+ };
186
+
187
+ const stateTier: TierConfig = {
188
+ id: "state",
189
+ label: "State",
190
+ items: STATE_ROLES.map((role): TierItem => ({
191
+ id: `state-${role}`,
192
+ cssVar: `--palette-state-${role}`,
193
+ label: role,
194
+ default: ramps.state[role],
195
+ type: { kind: "color", format: "oklch" },
196
+ })),
169
197
  };
198
+
199
+ return [baseTier, accentTier, stateTier];
170
200
  }
171
201
 
172
- function buildSemanticTier(): TierConfig {
173
- const items: TierItem[] = [];
174
- for (const [key, cssVar] of Object.entries(SEMANTIC_CSS_NAMES)) {
175
- const idx = SEMANTIC_DEFAULTS[key];
176
- if (idx === undefined) continue;
177
- items.push({
178
- id: key,
179
- cssVar,
180
- label: key,
181
- default: `p${idx}`,
182
- type: { kind: "color" },
183
- });
184
- }
202
+ const PALETTE_TAB: TabConfig = {
203
+ id: "palette",
204
+ label: "Palette",
205
+ tiers: buildRampTiers(),
206
+ // No colorExtras: a tab with multiple kind:'color' tiers MUST omit it so
207
+ // resolveColorClusterFromTab is not invoked and zdtp renders the ramps with
208
+ // its native curve editor (zdtp palette-tab contract).
209
+ };
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Color tab mode-scoped semantic tier (4 base roles + 23 --zd-* roles) as
213
+ // grouped ramp dropdowns referencing the Palette tab.
214
+ // ---------------------------------------------------------------------------
215
+
216
+ type PanelMode = "light" | "dark";
217
+
218
+ /**
219
+ * Resolve the `ColorScheme` whose `map` seeds the semantic tier for `mode`.
220
+ * With a light/dark pair configured, picks `colorMode.{light,dark}Scheme`; on
221
+ * the single-scheme path (`settings.colorMode === false`) there is no pair, so
222
+ * the active `settings.colorScheme` is used for both modes (no toggle wiring).
223
+ */
224
+ function schemeForMode(mode: PanelMode): ColorScheme {
225
+ const cm = settings.colorMode;
226
+ if (!cm) return getActiveScheme();
227
+ const name = mode === "dark" ? cm.darkScheme : cm.lightScheme;
228
+ return colorSchemes[name] ?? getActiveScheme();
229
+ }
230
+
231
+ /**
232
+ * Semantic tier — 4 base roles + 23 `--zd-*` roles, each a grouped ramp
233
+ * dropdown. `referencesRamps` names the Palette tab's ramp tiers this tier's
234
+ * `{ref}` mappings resolve against (cross-tab, resolved at mount by
235
+ * `resolveColorClusterFromTab`); `semantic: true` marks it so zdtp never
236
+ * mistakes it for the palette tier. Items come from `buildSemanticTierItems`:
237
+ * every `default` is an encoded `tierId:itemId` ramp reference, so a scheme
238
+ * with no override round-trips to the exact ramp stop (no snapping to a
239
+ * resolved color). Seeded from `schemeForMode(mode)` — this is the only
240
+ * mode-varying tier.
241
+ */
242
+ function buildSemanticTier(mode: PanelMode): TierConfig {
185
243
  return {
186
244
  id: "semantic",
187
245
  label: "Semantic",
188
- items,
189
- referencesTier: PALETTE_TIER_ID,
246
+ semantic: true,
247
+ referencesRamps: [
248
+ { tab: "palette", tier: "base" },
249
+ { tab: "palette", tier: "accent" },
250
+ { tab: "palette", tier: "state" },
251
+ ],
252
+ items: buildSemanticTierItems(schemeForMode(mode)),
190
253
  };
191
254
  }
192
255
 
193
- const COLOR_EXTRAS: ColorClusterExtras = {
194
- // Customize these values to match your project name to avoid localStorage
195
- // collisions when multiple zudo-doc projects run in the same browser.
196
- id: "my-doc",
197
- label: "My Doc",
198
- baseRoles: {
199
- background: "--zd-bg",
200
- foreground: "--zd-fg",
201
- cursor: "--zd-cursor",
202
- selectionBg: "--zd-sel-bg",
203
- selectionFg: "--zd-sel-fg",
204
- },
205
- baseDefaults: BASE_DEFAULTS,
206
- defaultShikiTheme: DEFAULT_SHIKI_THEME,
207
- // toZdtpColorSchemes fills the fallback only for schemes without their own
208
- // shikiTheme, so this is a type-checked assignment rather than an unsafe cast.
209
- colorSchemes: toZdtpColorSchemes(colorSchemes),
210
- panelSettings: {
211
- colorScheme: settings.colorScheme,
212
- // colorMode: strip off respectPrefersColorScheme (not in zdtp's shape).
213
- colorMode: settings.colorMode
214
- ? {
215
- defaultMode: settings.colorMode.defaultMode,
216
- lightScheme: settings.colorMode.lightScheme,
217
- darkScheme: settings.colorMode.darkScheme,
218
- }
219
- : false,
220
- },
221
- };
256
+ /**
257
+ * Color cluster extras. Scheme-less (`colorSchemes: {}`) with no base-role
258
+ * editors (`baseRoles`/`baseDefaults` empty) the Palette tab owns the ramps
259
+ * and the semantic tier owns the roles.
260
+ *
261
+ * `panelSettings.colorMode` is an OBJECT (not `false`) with `defaultMode = mode`.
262
+ * On this scheme-less cluster its only live effect is `getClusterDefaultMode()`,
263
+ * which drives the per-mode literal collapse/preview side — pinning it to the
264
+ * ACTIVE mode keeps that side following the live toggle (a bare `false` would
265
+ * pin it to `light` and mis-collapse per-mode literals for a dark-mode user).
266
+ * `colorScheme`/`lightScheme`/`darkScheme` only need to be non-empty strings
267
+ * (the validator requires that even with `colorSchemes: {}`; the names are not
268
+ * checked against the empty registry).
269
+ */
270
+ function buildColorExtras(mode: PanelMode): ColorClusterExtras {
271
+ const cm = settings.colorMode;
272
+ const lightScheme = cm ? cm.lightScheme : settings.colorScheme;
273
+ const darkScheme = cm ? cm.darkScheme : settings.colorScheme;
274
+ return {
275
+ // Customize id/label to match your project name to avoid localStorage
276
+ // collisions when multiple zudo-doc-based projects run in the same browser.
277
+ id: "my-doc",
278
+ label: "My Doc",
279
+ baseRoles: {},
280
+ baseDefaults: {},
281
+ defaultShikiTheme: DEFAULT_SHIKI_THEME,
282
+ colorSchemes: {},
283
+ panelSettings: {
284
+ colorScheme: settings.colorScheme,
285
+ colorMode: { defaultMode: mode, lightScheme, darkScheme },
286
+ },
287
+ };
288
+ }
222
289
 
223
- const COLOR_TAB: TabConfig = {
224
- id: "color",
225
- label: "Color",
226
- tiers: [buildPaletteTier(), buildSemanticTier()],
227
- colorExtras: COLOR_EXTRAS,
228
- };
290
+ function buildColorTab(mode: PanelMode): TabConfig {
291
+ return {
292
+ id: "color",
293
+ label: "Color",
294
+ tiers: [buildSemanticTier(mode)],
295
+ colorExtras: buildColorExtras(mode),
296
+ };
297
+ }
229
298
 
230
299
  // ---------------------------------------------------------------------------
231
- // Font tab
300
+ // Font tab — five tiers grouped by the manifest's `group` field.
232
301
  // ---------------------------------------------------------------------------
233
302
 
234
303
  const FONT_SCALE_TIER_ID = "font-scale";
235
304
 
305
+ /**
306
+ * Tier 2 semantic role → Tier 1 abstract scale item id. Mirrors the `var(--…)`
307
+ * wiring in `global.css` (`--text-body: var(--text-scale-md)` etc.). The role
308
+ * tier is a *reference* tier: each item's stored value is the id of a
309
+ * `font-scale` item, exactly like the Color tab's semantic→palette tier. zdtp
310
+ * renders these as dropdowns and emits `var(--text-scale-*)`, so editing a
311
+ * scale step propagates to every role live.
312
+ */
313
+ const FONT_ROLE_TO_SCALE: Readonly<Record<string, string>> = {
314
+ "text-micro": "text-scale-2xs",
315
+ "text-caption": "text-scale-xs",
316
+ "text-small": "text-scale-sm",
317
+ "text-body": "text-scale-md",
318
+ "text-title": "text-scale-lg",
319
+ "text-heading": "text-scale-xl",
320
+ "text-display": "text-scale-2xl",
321
+ };
322
+
323
+ /**
324
+ * Build the semantic font-size tier as a reference tier pointing at the
325
+ * `font-scale` tier. Defaults are overridden to the referenced scale id (the
326
+ * manifest still records resolved rem values for serde / the flat-manifest
327
+ * generator template, so the override happens here rather than in the
328
+ * manifest to keep both consumers correct).
329
+ */
330
+ function buildFontRoleTier(): TierConfig {
331
+ const base = tierFromGroup(FONT_TOKENS, "font-size", "Font size");
332
+ return {
333
+ ...base,
334
+ items: base.items.map((item) => {
335
+ const scaleId = FONT_ROLE_TO_SCALE[item.id];
336
+ return scaleId ? { ...item, default: scaleId } : item;
337
+ }),
338
+ referencesTier: FONT_SCALE_TIER_ID,
339
+ };
340
+ }
341
+
236
342
  const FONT_TAB: TabConfig = {
237
343
  id: "font",
238
344
  label: "Font",
239
345
  tiers: [
240
346
  tierFromGroup(FONT_TOKENS, FONT_SCALE_TIER_ID, "Scale"),
241
- tierFromGroup(FONT_TOKENS, "font-size", "Font size"),
347
+ buildFontRoleTier(),
242
348
  tierFromGroup(FONT_TOKENS, "line-height", "Line height"),
243
349
  tierFromGroup(FONT_TOKENS, "font-weight", "Font weight"),
244
350
  tierFromGroup(FONT_TOKENS, "font-family", "Font family"),
@@ -246,7 +352,7 @@ const FONT_TAB: TabConfig = {
246
352
  };
247
353
 
248
354
  // ---------------------------------------------------------------------------
249
- // Spacing tab
355
+ // Spacing tab — four tiers grouped by the manifest's `group` field.
250
356
  // ---------------------------------------------------------------------------
251
357
 
252
358
  const SPACING_TAB: TabConfig = {
@@ -261,7 +367,7 @@ const SPACING_TAB: TabConfig = {
261
367
  };
262
368
 
263
369
  // ---------------------------------------------------------------------------
264
- // Size tab
370
+ // Size tab — two tiers grouped by the manifest's `group` field.
265
371
  // ---------------------------------------------------------------------------
266
372
 
267
373
  const SIZE_TAB: TabConfig = {
@@ -273,16 +379,57 @@ const SIZE_TAB: TabConfig = {
273
379
  ],
274
380
  };
275
381
 
276
- export const designTokenPanelConfig: PanelConfig = {
277
- // Customize these values to match your project name to avoid localStorage
278
- // collisions when multiple zudo-doc projects run in the same browser.
279
- storagePrefix: "my-doc-tweak",
280
- consoleNamespace: "myDoc",
281
- modalClassPrefix: "my-doc-design-token-panel-modal",
282
- // Must match DESIGN_TOKEN_SCHEMA in @takazudo/zudo-doc/theme so that
283
- // exported JSON files remain importable across panel versions.
284
- schemaId: DESIGN_TOKEN_SCHEMA,
285
- exportFilenameBase: "my-doc-design-tokens",
286
- tabs: [COLOR_TAB, FONT_TAB, SPACING_TAB, SIZE_TAB],
287
- colorPresets: {},
288
- };
382
+ /**
383
+ * Detect the initial color-scheme mode for the first `configurePanel` call. In
384
+ * the browser, read `<html data-theme>` (set pre-paint by the
385
+ * ColorSchemeProvider bootstrap); otherwise fall back to
386
+ * `settings.colorMode.defaultMode` (or `light` on the single-scheme path).
387
+ * SSR-safe: guards `document`.
388
+ */
389
+ function detectInitialMode(): PanelMode {
390
+ if (typeof document !== "undefined") {
391
+ const attr = document.documentElement.getAttribute("data-theme");
392
+ if (attr === "light" || attr === "dark") return attr;
393
+ }
394
+ const cm = settings.colorMode;
395
+ return cm ? cm.defaultMode : "light";
396
+ }
397
+
398
+ /**
399
+ * Build the full PanelConfig for a given color-scheme `mode`. Only the Color
400
+ * tab's semantic tier and `panelSettings.colorMode.defaultMode` vary by mode;
401
+ * the Palette/Font/Spacing/Size tabs are mode-independent. The bootstrap calls
402
+ * this per mode on every `color-scheme-changed` toggle (destroy + reconfigure).
403
+ */
404
+ export function buildDesignTokenPanelConfig(mode: PanelMode): PanelConfig {
405
+ return {
406
+ // Customize these values to match your project name to avoid localStorage
407
+ // collisions when multiple zudo-doc-based projects run in the same browser.
408
+ storagePrefix: "my-doc-tweak",
409
+ consoleNamespace: "myDoc",
410
+ modalClassPrefix: "my-doc-design-token-panel-modal",
411
+ // DISPLAY-ONLY in zdtp 0.4.5: the panel's export hard-codes
412
+ // `zudo-design-tokens/v2` and auto-upgrades to `.../v3` when object leaves
413
+ // ({ref}/{literal}/per-mode) are present — which the semantic tier's ramp
414
+ // refs always are, so real exports carry v3. `schemaId` does NOT gate
415
+ // import; it only labels the Import-modal hint. zdtp DOES export its own
416
+ // `SCHEMA_V1`/`SCHEMA_V2`/`SCHEMA_V3` constants (#498/#505), but those are
417
+ // zdtp's fixed internal schema strings, unrelated to this field. Set to v3
418
+ // (a literal, not one of those constants) so the hint matches what exports
419
+ // actually carry.
420
+ schemaId: "zudo-design-tokens/v3",
421
+ exportFilenameBase: "my-doc-design-tokens",
422
+ tabs: [PALETTE_TAB, buildColorTab(mode), FONT_TAB, SPACING_TAB, SIZE_TAB],
423
+ };
424
+ }
425
+
426
+ /**
427
+ * Back-compat / initial-call export: the config for the mode detected at module
428
+ * load. Pass the `buildDesignTokenPanelConfig` BUILDER (not this plain object)
429
+ * to the bootstrap for live mode-scoped rebuilds on light/dark toggle — see
430
+ * `src/lib/design-token-panel-bootstrap.ts`. This plain-config export remains
431
+ * for callers that only need a static config.
432
+ */
433
+ export const designTokenPanelConfig: PanelConfig = buildDesignTokenPanelConfig(
434
+ detectInitialMode(),
435
+ );
@@ -5,9 +5,16 @@
5
5
  * settings.designTokenPanel is truthy. The dynamic import is gated there so
6
6
  * this module is only bundled when the feature is enabled.
7
7
  *
8
- * The wiring MECHANISM (configurePanel + setLifecycleAdapter) now lives in the
9
- * package at `@takazudo/zudo-doc/design-token-panel-bootstrap`. This file
10
- * passes the project's PanelConfig DATA to that mechanism.
8
+ * The wiring MECHANISM (configurePanel + setLifecycleAdapter + the mode-scoped
9
+ * reconfigure) lives in the package at
10
+ * `@takazudo/zudo-doc/design-token-panel-bootstrap`. This file passes the
11
+ * project's PanelConfig DATA to that mechanism.
12
+ *
13
+ * We pass the `buildDesignTokenPanelConfig` BUILDER (not the plain
14
+ * `designTokenPanelConfig` object) so the bootstrap rebuilds the panel per
15
+ * light/dark mode on every `color-scheme-changed` toggle — keeping the Color
16
+ * tab's semantic defaults mode-faithful. A plain-config caller also works (see
17
+ * `bootstrapDesignTokenPanel`'s JSDoc) but gets no toggle listener.
11
18
  *
12
19
  * S9a package-first migration — zudolab/zudo-doc#2333.
13
20
  *
@@ -18,6 +25,6 @@
18
25
  */
19
26
 
20
27
  import { bootstrapDesignTokenPanel } from "@takazudo/zudo-doc/design-token-panel-bootstrap";
21
- import { designTokenPanelConfig } from "@/config/design-token-panel-config";
28
+ import { buildDesignTokenPanelConfig } from "@/config/design-token-panel-config";
22
29
 
23
- bootstrapDesignTokenPanel(designTokenPanelConfig);
30
+ bootstrapDesignTokenPanel(buildDesignTokenPanelConfig);