@hadoku/themes 0.0.0-stage → 5.12.16

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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE-lucide +43 -0
  3. package/README.md +433 -2
  4. package/THEME_USAGE_GUIDE.md +308 -0
  5. package/dist/HadokuThemeRoot.d.ts +53 -0
  6. package/dist/HadokuThemeRoot.js +66 -0
  7. package/dist/ThemeIcons.d.ts +31 -0
  8. package/dist/ThemeIcons.js +91 -0
  9. package/dist/effects.d.ts +25 -0
  10. package/dist/effects.js +25 -0
  11. package/dist/icons/Icon.d.ts +36 -0
  12. package/dist/icons/Icon.js +89 -0
  13. package/dist/icons/index.d.ts +61 -0
  14. package/dist/icons/index.js +87 -0
  15. package/dist/icons/registry.generated.d.ts +158 -0
  16. package/dist/icons/registry.generated.js +446 -0
  17. package/dist/index.d.ts +82 -0
  18. package/dist/index.js +169 -0
  19. package/dist/metadata.d.ts +35 -0
  20. package/dist/metadata.js +108 -0
  21. package/dist/platform/index.d.ts +94 -0
  22. package/dist/platform/index.js +149 -0
  23. package/dist/platform/usePlatform.d.ts +29 -0
  24. package/dist/platform/usePlatform.js +65 -0
  25. package/dist/prefsApiBase.d.ts +37 -0
  26. package/dist/prefsApiBase.js +40 -0
  27. package/dist/theme-types.d.ts +34 -0
  28. package/dist/theme-types.js +1 -0
  29. package/dist/themeContext.d.ts +62 -0
  30. package/dist/themeContext.js +70 -0
  31. package/dist/themePrefs.d.ts +29 -0
  32. package/dist/themePrefs.js +48 -0
  33. package/dist/useRefElement.d.ts +19 -0
  34. package/dist/useRefElement.js +52 -0
  35. package/dist/useTheme.d.ts +46 -0
  36. package/dist/useTheme.js +216 -0
  37. package/package.json +83 -4
  38. package/scripts/audit-tokens.mjs +126 -0
  39. package/scripts/check-contrast.mjs +24 -0
  40. package/scripts/check-docs.mjs +53 -0
  41. package/scripts/check-icons.mjs +473 -0
  42. package/scripts/check-usage.mjs +297 -0
  43. package/scripts/codemod-icons.mjs +396 -0
  44. package/scripts/generate-icons.mjs +199 -0
  45. package/scripts/generate-preview.mjs +374 -0
  46. package/scripts/generate-tailwind-colors.mjs +109 -0
  47. package/scripts/lib/color.mjs +184 -0
  48. package/scripts/lib/contract.d.mts +58 -0
  49. package/scripts/lib/contract.mjs +348 -0
  50. package/scripts/lib/derive.mjs +324 -0
  51. package/scripts/lib/icon-registry.mjs +92 -0
  52. package/scripts/lib/parse-themes.mjs +19 -0
  53. package/scripts/lib/tokens.mjs +188 -0
  54. package/scripts/normalize-tokens.mjs +161 -0
  55. package/src/advanced.css +218 -0
  56. package/src/icons/Icon.tsx +151 -0
  57. package/src/icons/emoji-map.json +202 -0
  58. package/src/icons/index.ts +151 -0
  59. package/src/icons/registry.generated.ts +455 -0
  60. package/src/icons/sources.json +150 -0
  61. package/src/icons.css +145 -0
  62. package/src/style.css +1421 -0
  63. package/src/tailwind-colors.css +88 -0
  64. package/src/tailwind-integration.css +49 -0
@@ -0,0 +1,308 @@
1
+ # Theme Usage Guide — which token do I use?
2
+
3
+ **This is the doc for _using_ colors.** For authoring a new theme's palette, see
4
+ [`THEME_CREATION_GUIDE.md`](./THEME_CREATION_GUIDE.md). For the accessibility
5
+ contracts, see [`docs/THEME_SYSTEM_RULES.md`](../docs/THEME_SYSTEM_RULES.md).
6
+
7
+ ## The mental model, in one line
8
+
9
+ **A token names a semantic role, not a hue. Light/dark is automatic — never
10
+ branch on theme mode.**
11
+
12
+ There are 18 themes. `--color-success` is sky blue in one and gold in another.
13
+ Pick the token that matches what the element _means_; the theme decides the color.
14
+
15
+ ---
16
+
17
+ ## Decision table
18
+
19
+ `<f>` is one of `primary` · `success` · `warning` · `danger` · `neutral`.
20
+
21
+ | You're styling… | Use |
22
+ | -------------------------------- | ---------------------------------------------------------------- |
23
+ | Filled / primary button | `bg-<f>` + `text-on-<f>` |
24
+ | …its gradient or pressed state | `bg-<f>-dark` (still pair with `text-on-<f>`) |
25
+ | Tint badge, chip, banner, alert | `bg-<f>-bg` + `text-on-<f>-bg` |
26
+ | Ghost button / row hover tint | `hover:bg-<f>-hover` |
27
+ | Body text | `text-text` |
28
+ | Secondary / muted text | `text-text-secondary` → `text-text-tertiary` → `text-text-muted` |
29
+ | Page background | `bg-bg` |
30
+ | Card / panel surface | `bg-bg-card` |
31
+ | Inset or alternate surface | `bg-bg-alt` |
32
+ | Neutral row/button hover surface | `bg-bg-hover` |
33
+ | Modal scrim | `bg-bg-overlay` |
34
+ | Border / divider | `border-border` |
35
+ | Subtle divider | `border-border-light` |
36
+
37
+ ### The one rule people get wrong
38
+
39
+ `--color-<f>` and `--color-<f>-bg` are **different surfaces with different text
40
+ colors.** They are not interchangeable.
41
+
42
+ ```
43
+ bg-<f> (solid fill) → text-on-<f>
44
+ bg-<f>-bg (faint tint) → text-on-<f>-bg ← NOT text-<f>
45
+ ```
46
+
47
+ Using `text-<f>` on `bg-<f>-bg` fails WCAG AA in 62 of 90 theme/family
48
+ combinations. That pairing is why badges kept turning up unreadable.
49
+
50
+ ### The same rule, one step further out
51
+
52
+ A fill colour is not a text colour **on any surface**, not just on its own tint.
53
+ `text-<f>` with no background at all lands on the page or a card, and that pair
54
+ was validated by nothing: `check-contrast` checks `on-<f>` over `<f>`, so
55
+ accent-over-surface passed by never being looked at.
56
+
57
+ ```
58
+ text-<f> with no background → lands on bg / bg-card ← unvalidated, usually fails
59
+ ```
60
+
61
+ Measured against `--color-bg`: `--color-primary` clears the 3:1 large-text floor
62
+ in only 12 of 18 themes (izakaya-light 1.92, nature-light 2.15, ocean-light 2.28,
63
+ lavender-light 2.53, pink-light 2.56, strawberry-light 2.61). `--color-danger`
64
+ misses AA on a card in 11 of 18. This shipped in `.app-header__title` and reached
65
+ every consuming app at once.
66
+
67
+ Text on a surface is `text-text` (8.87–17.58 across all 18) or
68
+ `text-text-secondary`. To carry an accent, either pair `bg-<f>-bg` with
69
+ `text-on-<f>-bg`, or move the accent off the glyph entirely — a border, rule, or
70
+ underline is unconstrained by text contrast. `check-usage` gates this, in CSS
71
+ and as Tailwind classes in markup (`text-danger` with no `bg-danger` fill).
72
+
73
+ ---
74
+
75
+ ## Complete token list
76
+
77
+ Every token below is defined in **all 18 themes**. Nothing else exists — if a
78
+ name isn't here, it isn't a token.
79
+
80
+ ### Semantic families
81
+
82
+ | family | fill | gradient/pressed | tint surface | hover overlay | text on fill | text on tint |
83
+ | --------- | ----------------- | ---------------------- | -------------------- | ----------------------- | -------------------- | ----------------------- |
84
+ | `primary` | `--color-primary` | `--color-primary-dark` | `--color-primary-bg` | `--color-primary-hover` | `--color-on-primary` | `--color-on-primary-bg` |
85
+ | `success` | `--color-success` | `--color-success-dark` | `--color-success-bg` | `--color-success-hover` | `--color-on-success` | `--color-on-success-bg` |
86
+ | `warning` | `--color-warning` | `--color-warning-dark` | `--color-warning-bg` | `--color-warning-hover` | `--color-on-warning` | `--color-on-warning-bg` |
87
+ | `danger` | `--color-danger` | `--color-danger-dark` | `--color-danger-bg` | `--color-danger-hover` | `--color-on-danger` | `--color-on-danger-bg` |
88
+ | `neutral` | `--color-neutral` | `--color-neutral-dark` | `--color-neutral-bg` | `--color-neutral-hover` | `--color-on-neutral` | `--color-on-neutral-bg` |
89
+
90
+ The table is a **rectangle on purpose.** Every family has exactly the same six
91
+ tokens, so the symmetric guess is always the right one.
92
+
93
+ ### Structural
94
+
95
+ | role | token | AA-safe for text? |
96
+ | --------------- | ------------------------ | --------------------- |
97
+ | body text | `--color-text` | ✅ all 18 themes |
98
+ | secondary text | `--color-text-secondary` | ✅ all 18 themes |
99
+ | tertiary text | `--color-text-tertiary` | ⚠️ fails in 5 themes |
100
+ | muted text | `--color-text-muted` | ⚠️ fails in 14 themes |
101
+ | border | `--color-border` | — |
102
+ | subtle border | `--color-border-light` | — |
103
+ | page background | `--color-bg` | — |
104
+ | card surface | `--color-bg-card` | — |
105
+ | alt surface | `--color-bg-alt` | — |
106
+ | hover surface | `--color-bg-hover` | — |
107
+ | modal scrim | `--color-bg-overlay` | — |
108
+
109
+ > **`-tertiary` and `-muted` are decorative-only.** Measured against
110
+ > `bg`/`bg-card`/`bg-alt` in all 18 themes, `-tertiary` drops below 4.5:1 in 5
111
+ > and `-muted` in 14. Use them for de-emphasised ornament (placeholder glyphs,
112
+ > disabled states — WCAG exempts inactive controls). **Any text a user must
113
+ > read gets `--color-text` or `--color-text-secondary`**, both of which clear
114
+ > AA on every surface in every theme. `check-contrast.mjs` gates those two and
115
+ > deliberately does not gate the other two.
116
+
117
+ **41 tokens total.** Non-color tokens (spacing, radius, shadows, type) use the
118
+ `--hdk-*` prefix — see [`README.md`](./README.md).
119
+
120
+ ---
121
+
122
+ ## Anti-patterns — hard rules
123
+
124
+ | ❌ Never | ✅ Instead | Why |
125
+ | --------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------- |
126
+ | `text-white`, `color: #fff` on a filled button | `text-on-<f>` | `--color-<f>` is light in many themes; white-on-light is invisible |
127
+ | `var(--color-x, #hex)` | `var(--color-x)` | A fallback renders the hardcoded value, so a wrong token looks right in light mode and breaks in the other 17 |
128
+ | `text-<f>` on `bg-<f>-bg` | `text-on-<f>-bg` | Fails AA in 62/90 theme×family combinations |
129
+ | `[data-theme='dark'] .foo { color: … }` | one token, no branch | Tokens already switch per theme; branching double-applies |
130
+ | Inventing `--color-<f>-light` / `-lighter` / `-darker` / `--color-muted-bg` | the six canonical family tokens | Removed in v3.0.0 — see [`THEME_GRAVEYARD.md`](./THEME_GRAVEYARD.md) |
131
+ | `--color-<f>` as body text | `--color-text` | Solid fills are for backgrounds; §4 of the rules doc |
132
+ | `@import "@wolffm/themes/style.css" layer(base)` | import it **unlayered** | Layering it makes every color resolve to nothing |
133
+
134
+ ---
135
+
136
+ ## Icons
137
+
138
+ Icons are part of the theme contract, not each app's business. Raw emoji were the old
139
+ answer and they are retired: the same codepoint renders differently on every platform,
140
+ and no CSS can fix it. The package ships **78 vendored line icons** (artwork from
141
+ [Lucide](https://lucide.dev), ISC — see `LICENSE-lucide`) behind an enforced allowlist.
142
+
143
+ **Reference icons by name. Never inline your own SVG, and never add an emoji.**
144
+
145
+ ### Three consumption paths
146
+
147
+ ```tsx
148
+ // React, in an app that also uses the theming stack
149
+ import { Icon } from '@wolffm/themes'
150
+ ;<Icon name="popcorn" />
151
+ ;<Icon name="popcorn" variant="tint" family="warning" />
152
+ ```
153
+
154
+ ```tsx
155
+ // React, icons only — same component, none of the theming stack.
156
+ // The root barrel re-exports useTheme, which imports @wolffm/prefs-client
157
+ // and zod. If all you want is glyphs, take them from here instead.
158
+ import { Icon } from '@wolffm/themes/icons/react'
159
+ ```
160
+
161
+ ```astro
162
+ ---
163
+ // Astro / Qwik / plain HTML — no client JS, CSP-safe
164
+ import { getIconSvg, getIconTileClass } from '@wolffm/themes/icons'
165
+ ---
166
+ <span set:html={getIconSvg('popcorn')} />
167
+ <span class={getIconTileClass('warning', 'tint')} set:html={getIconSvg('popcorn')} />
168
+ ```
169
+
170
+ Both paths render byte-identical geometry — that is asserted in `e2e/icons.spec.ts`.
171
+
172
+ Import the stylesheet **unlayered**, next to `style.css`:
173
+
174
+ ```css
175
+ @import '@wolffm/themes/style.css';
176
+ @import '@wolffm/themes/icons.css';
177
+ ```
178
+
179
+ ### Colour: bare glyphs inherit, accents go on the tile
180
+
181
+ A bare icon inherits `currentColor`, so it is exactly as legible as the text beside it
182
+ and carries no contrast obligation of its own. **That is the default, and usually the
183
+ right answer.**
184
+
185
+ Do **not** paint the glyph with an accent token. Measured across all 18 themes on both
186
+ `--color-bg-card` and `--color-bg`, a bare `--color-warning` glyph misses the WCAG
187
+ 1.4.11 non-text minimum of 3:1 in 8 of 36 combinations (light 2.15, ocean-light 1.92,
188
+ pink-light 1.67); every other family fails somewhere too, and `--color-<f>-dark` does
189
+ not rescue it. This is the same accent-as-text anti-pattern listed above, and
190
+ `check:contrast` does **not** cover it — it validates `on-<f>` over `<f>`, never an
191
+ accent over a surface.
192
+
193
+ When you want the accent, put it on the tile. Both treatments pass 3:1 in 18/18 themes
194
+ for all five families, because they reuse pairings the system already guarantees:
195
+
196
+ | Variant | Background | Glyph | Contrast |
197
+ | -------- | ---------------- | ------------------- | ----------------------------- |
198
+ | `bare` | — (inherits) | `currentColor` | matches its text, always safe |
199
+ | `accent` | — (inherits) | `--color-<f>` | **yours to own** — see below |
200
+ | `tint` | `--color-<f>-bg` | `--color-on-<f>-bg` | 3:1 in 18/18 themes |
201
+ | `filled` | `--color-<f>` | `--color-on-<f>` | 3:1 in 18/18 themes |
202
+
203
+ **How you colour an icon is your call; where the colour comes from is not.** Every
204
+ variant resolves through family tokens, and no raw colour can travel through this
205
+ API — so an icon cannot drift from the theme.
206
+
207
+ Two cases, both first-class:
208
+
209
+ ```tsx
210
+ // Inline in a button — inherit, so the icon matches the button's own text in
211
+ // every theme with no extra thought.
212
+ <button className="pill-btn"><Icon name="plus" /> Add</button>
213
+
214
+ // Its own element, deliberately a different colour from the surrounding text.
215
+ <Icon name="warning" variant="accent" family="danger" />
216
+ ```
217
+
218
+ `accent` is opt-in rather than default because contrast moves to you when you use
219
+ it: a bare accent glyph clears 3:1 on a card or page surface in 28 of 36
220
+ theme/surface combinations for `warning`. That is fine when you know the surface —
221
+ an accent glyph on a tinted row, or beside a label you have checked — and wrong as
222
+ a blanket default. When the accent must be safe everywhere, use a tile.
223
+
224
+ Framework-free gets the same control:
225
+
226
+ ```js
227
+ getIconSvg('warning', { family: 'danger' }) // accent glyph
228
+ getIconSvg('plus') // inherits
229
+ ```
230
+
231
+ ### The set
232
+
233
+ | Name | Name | Name | Name | Name | Name |
234
+ | -------------- | ------------- | ------------ | ------------- | ------------ | -------------- |
235
+ | `activity` | `antenna` | `arrow-left` | `arrow-right` | `book` | `briefcase` |
236
+ | `broom` | `calendar` | `camera` | `chat` | `check` | `chevron-down` |
237
+ | `circle` | `clapper` | `clipboard` | `clock` | `close` | `compass` |
238
+ | `crop` | `crown` | `desktop` | `document` | `download` | `edit` |
239
+ | `error` | `eye` | `fire` | `folder` | `gear` | `handshake` |
240
+ | `helping-hand` | `home` | `hourglass` | `image` | `inbox` | `info` |
241
+ | `link` | `lock` | `magnifier` | `mail` | `masks` | `minus` |
242
+ | `music` | `note` | `package` | `palette` | `panel-left` | `panel-right` |
243
+ | `paste` | `play` | `plus` | `popcorn` | `printer` | `redo` |
244
+ | `refresh` | `robot` | `scissors` | `shuffle` | `skull` | `sleep` |
245
+ | `sparkles` | `star` | `stop` | `thumbs-down` | `thumbs-up` | `trash` |
246
+ | `trophy` | `undo` | `upload` | `user` | `videotape` | `volume-high` |
247
+ | `volume-low` | `volume-mute` | `wand` | `warning` | `wave` | `wrench` |
248
+
249
+ ### See them all
250
+
251
+ `pnpm run gallery` (in hadoku-task) serves a live page at
252
+ `http://localhost:5200/dev/gallery/index.html`: every icon, the real
253
+ `<HadokuThemePicker>` to sweep all 18 themes, variant/family/size controls, the
254
+ React-vs-string comparison, and the icons sitting in real app chrome. It is the
255
+ fastest way to check a new icon before committing it.
256
+
257
+ ### Adding an icon
258
+
259
+ 1. Add `"name": "lucide-slug"` to `themes/src/icons/sources.json`.
260
+ 2. `pnpm run generate:icons` — fails loudly if the slug is not in lucide.
261
+ 3. Commit the regenerated `registry.generated.ts`.
262
+
263
+ ### Verify it
264
+
265
+ ```sh
266
+ node node_modules/@wolffm/themes/scripts/check-icons.mjs ./src
267
+ ```
268
+
269
+ Fails on raw emoji in an icon position and on icon names not in the registry. The name
270
+ check matters most outside TypeScript — Astro templates and JSON catalogues never see
271
+ the `IconName` union, so a typo there renders nothing at all, silently.
272
+
273
+ ---
274
+
275
+ ## Setup
276
+
277
+ ```css
278
+ @import '@wolffm/themes/style.css'; /* 1. tokens — MUST be unlayered */
279
+ @import 'tailwindcss'; /* 2. Tailwind */
280
+ @import '@wolffm/themes/tailwind-colors.css'; /* 3. the color mapping */
281
+ ```
282
+
283
+ That's the whole integration. **Do not hand-write an `@theme` color block** —
284
+ `tailwind-colors.css` maps all 41 tokens, and a local copy is exactly the
285
+ drift this package exists to prevent.
286
+
287
+ Not using Tailwind? Import step 1 only and use `var(--color-*)` directly.
288
+
289
+ ### Why step 1 must stay unlayered
290
+
291
+ Tailwind emits each `@theme` entry into `@layer theme` as `:root { --x: var(--x) }`
292
+ — self-referential, and invalid on its own. It resolves only because
293
+ `style.css` declares the same properties **outside** any cascade layer, and
294
+ unlayered declarations win. Put it in a layer and all 41 colors silently
295
+ become `transparent`.
296
+
297
+ ---
298
+
299
+ ## Verify it
300
+
301
+ ```sh
302
+ node node_modules/@wolffm/themes/scripts/check-usage.mjs ./src
303
+ ```
304
+
305
+ Fails the build on: unknown tokens, Tailwind classes with no matching token,
306
+ `var()` fallbacks, and layered imports of `style.css`. Wire it into `lint` and
307
+ pre-commit — an unmapped class produces **no CSS rule at all**, so neither
308
+ typecheck nor build will ever catch it for you.
@@ -0,0 +1,53 @@
1
+ /**
2
+ * HadokuThemeRoot — the single thing a hadoku app mounts to get the platform's
3
+ * theming and the locked header.
4
+ *
5
+ * Wrap the app root once:
6
+ *
7
+ * ```tsx
8
+ * import { HadokuThemeRoot } from '@wolffm/themes'
9
+ *
10
+ * export function mount(el: HTMLElement) {
11
+ * createRoot(el).render(
12
+ * <HadokuThemeRoot>
13
+ * <App />
14
+ * </HadokuThemeRoot>
15
+ * )
16
+ * }
17
+ * ```
18
+ *
19
+ * That is the entire integration. `<AppHeader>` then renders the shared theme
20
+ * picker and the shared settings gear from this context — an app does not pass
21
+ * them, cannot substitute them, and no longer needs a local `useTheme`,
22
+ * `themePrefs` or `themeConfig` module.
23
+ *
24
+ * WHY THE CONTEXT LIVES HERE TOO
25
+ * -------------------------------
26
+ * It did not always. The context was defined in task-ui-components (where
27
+ * AppHeader consumes it) and only FILLED here, because this package used to
28
+ * import the theme icons from there and the reverse import would have closed a
29
+ * cycle.
30
+ *
31
+ * A context defined in one package and provided from another only works while
32
+ * exactly one copy of the defining module exists across every install tree,
33
+ * bundle and import map that touches it. That held until it didn't: on
34
+ * 2026-08-05 three apps threw "No <HadokuThemeRoot> above this component" with
35
+ * the provider plainly mounted, because a second copy of task-ui-components
36
+ * meant a second createContext() — provider filling one, consumer reading the
37
+ * other.
38
+ *
39
+ * Moving the icons here reversed the arrow, so the context now ships beside the
40
+ * provider that fills it, in the package every app already resolves through the
41
+ * parent's import map.
42
+ */
43
+ import React, { type RefObject } from 'react';
44
+ export interface HadokuThemeRootProps {
45
+ children: React.ReactNode;
46
+ /** Theme forced by a parent shell (the portfolio passes its resolved theme
47
+ * into micro-frontend mounts). Omit for a standalone app. */
48
+ theme?: string;
49
+ /** Mirror `data-theme` onto this container as well as <html> — needed when
50
+ * the app mounts into a subtree rather than owning the document. */
51
+ containerRef?: RefObject<HTMLElement | null>;
52
+ }
53
+ export declare function HadokuThemeRoot({ children, theme: propsTheme, containerRef }: HadokuThemeRootProps): React.JSX.Element;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * HadokuThemeRoot — the single thing a hadoku app mounts to get the platform's
3
+ * theming and the locked header.
4
+ *
5
+ * Wrap the app root once:
6
+ *
7
+ * ```tsx
8
+ * import { HadokuThemeRoot } from '@wolffm/themes'
9
+ *
10
+ * export function mount(el: HTMLElement) {
11
+ * createRoot(el).render(
12
+ * <HadokuThemeRoot>
13
+ * <App />
14
+ * </HadokuThemeRoot>
15
+ * )
16
+ * }
17
+ * ```
18
+ *
19
+ * That is the entire integration. `<AppHeader>` then renders the shared theme
20
+ * picker and the shared settings gear from this context — an app does not pass
21
+ * them, cannot substitute them, and no longer needs a local `useTheme`,
22
+ * `themePrefs` or `themeConfig` module.
23
+ *
24
+ * WHY THE CONTEXT LIVES HERE TOO
25
+ * -------------------------------
26
+ * It did not always. The context was defined in task-ui-components (where
27
+ * AppHeader consumes it) and only FILLED here, because this package used to
28
+ * import the theme icons from there and the reverse import would have closed a
29
+ * cycle.
30
+ *
31
+ * A context defined in one package and provided from another only works while
32
+ * exactly one copy of the defining module exists across every install tree,
33
+ * bundle and import map that touches it. That held until it didn't: on
34
+ * 2026-08-05 three apps threw "No <HadokuThemeRoot> above this component" with
35
+ * the provider plainly mounted, because a second copy of task-ui-components
36
+ * meant a second createContext() — provider filling one, consumer reading the
37
+ * other.
38
+ *
39
+ * Moving the icons here reversed the arrow, so the context now ships beside the
40
+ * provider that fills it, in the package every app already resolves through the
41
+ * parent's import map.
42
+ */
43
+ import React, { useMemo } from 'react';
44
+ import { HadokuThemeProvider } from './themeContext.js';
45
+ import { useTheme } from './useTheme.js';
46
+ import { THEME_ICON_MAP } from './metadata.js';
47
+ /** Resolve a theme name to its icon. Central so the picker's toggle button
48
+ * looks identical in every app — this used to be a per-app arrow function,
49
+ * and one app had a debug logger inside it. */
50
+ function getThemeIcon(theme) {
51
+ const Icon = THEME_ICON_MAP[theme];
52
+ return Icon ? React.createElement(Icon, null) : null;
53
+ }
54
+ export function HadokuThemeRoot({ children, theme: propsTheme, containerRef }) {
55
+ const t = useTheme({ propsTheme, containerRef });
56
+ const value = useMemo(() => ({
57
+ theme: t.theme,
58
+ setTheme: t.setTheme,
59
+ themeFamilies: t.themeFamilies,
60
+ getThemeIcon,
61
+ isDarkTheme: t.isDarkTheme,
62
+ isThemeReady: t.isThemeReady,
63
+ isInitialThemeLoad: t.isInitialThemeLoad
64
+ }), [t.theme, t.setTheme, t.themeFamilies, t.isDarkTheme, t.isThemeReady, t.isInitialThemeLoad]);
65
+ return React.createElement(HadokuThemeProvider, { value: value }, children);
66
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Theme-family icons — one per theme pair in THEME_FAMILIES.
3
+ *
4
+ * These lived in @wolffm/task-ui-components, which is why @wolffm/themes had to
5
+ * import from it, which is why the dependency arrow ran themes -> ui-components,
6
+ * which is why HadokuThemeContext could not be defined next to the provider that
7
+ * fills it. That split is what let a duplicated ui-components module produce two
8
+ * distinct React contexts and take three apps down on 2026-08-05.
9
+ *
10
+ * They belong here: they are theme metadata, they are consumed by THEME_FAMILIES
11
+ * a few lines away, and they depend on nothing but React. Moving them reverses
12
+ * the arrow and lets the context live with its provider.
13
+ *
14
+ * UI chrome (SettingsIcon, TagIcon) and the fallback shapes stay in
15
+ * task-ui-components — those belong to the components that draw them.
16
+ */
17
+ import React from 'react';
18
+ export declare const SunIcon: () => React.JSX.Element;
19
+ export declare const MoonIcon: () => React.JSX.Element;
20
+ export declare const StrawberryIcon: () => React.JSX.Element;
21
+ export declare const WaveIcon: () => React.JSX.Element;
22
+ export declare const ZapIcon: () => React.JSX.Element;
23
+ export declare const CoffeeIcon: () => React.JSX.Element;
24
+ export declare const FlowerIcon: () => React.JSX.Element;
25
+ export declare const HeartIcon: () => React.JSX.Element;
26
+ export declare const LeafIcon: () => React.JSX.Element;
27
+ export declare const LanternIcon: () => React.JSX.Element;
28
+ /**
29
+ * Generic/Fallback icons for custom themes without specific icons
30
+ * These provide visual differentiation when using the ThemePicker with custom themes
31
+ */
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Theme-family icons — one per theme pair in THEME_FAMILIES.
3
+ *
4
+ * These lived in @wolffm/task-ui-components, which is why @wolffm/themes had to
5
+ * import from it, which is why the dependency arrow ran themes -> ui-components,
6
+ * which is why HadokuThemeContext could not be defined next to the provider that
7
+ * fills it. That split is what let a duplicated ui-components module produce two
8
+ * distinct React contexts and take three apps down on 2026-08-05.
9
+ *
10
+ * They belong here: they are theme metadata, they are consumed by THEME_FAMILIES
11
+ * a few lines away, and they depend on nothing but React. Moving them reverses
12
+ * the arrow and lets the context live with its provider.
13
+ *
14
+ * UI chrome (SettingsIcon, TagIcon) and the fallback shapes stay in
15
+ * task-ui-components — those belong to the components that draw them.
16
+ */
17
+ import React from 'react';
18
+ /** Shared SVG attributes. Duplicated from task-ui-components' icon file rather
19
+ * than imported: this package must not depend on that one — that dependency is
20
+ * the whole reason the icons moved. Seventeen lines of constants is the right
21
+ * price for a one-way graph. */
22
+ const iconProps = {
23
+ width: 20,
24
+ height: 20,
25
+ viewBox: '0 0 24 24',
26
+ fill: 'none',
27
+ stroke: 'currentColor',
28
+ strokeWidth: 2,
29
+ strokeLinecap: 'round',
30
+ strokeLinejoin: 'round'
31
+ };
32
+ export const SunIcon = () => (React.createElement("svg", { ...iconProps },
33
+ React.createElement("circle", { cx: "12", cy: "12", r: "5" }),
34
+ React.createElement("line", { x1: "12", y1: "1", x2: "12", y2: "3" }),
35
+ React.createElement("line", { x1: "12", y1: "21", x2: "12", y2: "23" }),
36
+ React.createElement("line", { x1: "4.22", y1: "4.22", x2: "5.64", y2: "5.64" }),
37
+ React.createElement("line", { x1: "18.36", y1: "18.36", x2: "19.78", y2: "19.78" }),
38
+ React.createElement("line", { x1: "1", y1: "12", x2: "3", y2: "12" }),
39
+ React.createElement("line", { x1: "21", y1: "12", x2: "23", y2: "12" }),
40
+ React.createElement("line", { x1: "4.22", y1: "19.78", x2: "5.64", y2: "18.36" }),
41
+ React.createElement("line", { x1: "18.36", y1: "5.64", x2: "19.78", y2: "4.22" })));
42
+ export const MoonIcon = () => (React.createElement("svg", { ...iconProps },
43
+ React.createElement("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" })));
44
+ export const StrawberryIcon = () => (React.createElement("svg", { ...iconProps },
45
+ React.createElement("path", { d: "M12 21 C12 21 6.5 15 6.5 11 C6.5 8.5 8 7 10 7 C11 7 12 7.5 12 7.5 C12 7.5 13 7 14 7 C16 7 17.5 8.5 17.5 11 C17.5 15 12 21 12 21 Z", fill: "currentColor" }),
46
+ React.createElement("path", { d: "M9.5 7.5 L9 5 L11 5.5 Z", fill: "currentColor" }),
47
+ React.createElement("path", { d: "M14.5 7.5 L15 5 L13 5.5 Z", fill: "currentColor" }),
48
+ React.createElement("path", { d: "M12 7.5 L12 4 L12 5", stroke: "currentColor", strokeWidth: "1.5", fill: "none" }),
49
+ React.createElement("line", { x1: "10", y1: "10", x2: "10", y2: "11", stroke: "currentColor", strokeWidth: "1", opacity: "0.4" }),
50
+ React.createElement("line", { x1: "14", y1: "10", x2: "14", y2: "11", stroke: "currentColor", strokeWidth: "1", opacity: "0.4" }),
51
+ React.createElement("line", { x1: "9", y1: "13", x2: "9", y2: "14", stroke: "currentColor", strokeWidth: "1", opacity: "0.4" }),
52
+ React.createElement("line", { x1: "15", y1: "13", x2: "15", y2: "14", stroke: "currentColor", strokeWidth: "1", opacity: "0.4" }),
53
+ React.createElement("line", { x1: "11", y1: "16", x2: "11", y2: "17", stroke: "currentColor", strokeWidth: "1", opacity: "0.4" }),
54
+ React.createElement("line", { x1: "13", y1: "16", x2: "13", y2: "17", stroke: "currentColor", strokeWidth: "1", opacity: "0.4" })));
55
+ export const WaveIcon = () => (React.createElement("svg", { ...iconProps },
56
+ React.createElement("path", { d: "M2 12c2-2 4-2 6 0s4 2 6 0 4-2 6 0" }),
57
+ React.createElement("path", { d: "M2 17c2-2 4-2 6 0s4 2 6 0 4-2 6 0" }),
58
+ React.createElement("path", { d: "M2 7c2-2 4-2 6 0s4 2 6 0 4-2 6 0" })));
59
+ export const ZapIcon = () => (React.createElement("svg", { ...iconProps },
60
+ React.createElement("polygon", { points: "13 2 3 14 12 14 11 22 21 10 12 10 13 2" })));
61
+ export const CoffeeIcon = () => (React.createElement("svg", { ...iconProps },
62
+ React.createElement("path", { d: "M18 8h1a4 4 0 0 1 0 8h-1" }),
63
+ React.createElement("path", { d: "M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z" }),
64
+ React.createElement("line", { x1: "6", y1: "1", x2: "6", y2: "4" }),
65
+ React.createElement("line", { x1: "10", y1: "1", x2: "10", y2: "4" }),
66
+ React.createElement("line", { x1: "14", y1: "1", x2: "14", y2: "4" })));
67
+ export const FlowerIcon = () => (React.createElement("svg", { ...iconProps },
68
+ React.createElement("circle", { cx: "12", cy: "12", r: "2", fill: "currentColor" }),
69
+ React.createElement("circle", { cx: "12", cy: "6", r: "2.5", fill: "none", stroke: "currentColor", strokeWidth: "2" }),
70
+ React.createElement("circle", { cx: "18", cy: "10", r: "2.5", fill: "none", stroke: "currentColor", strokeWidth: "2" }),
71
+ React.createElement("circle", { cx: "16", cy: "16", r: "2.5", fill: "none", stroke: "currentColor", strokeWidth: "2" }),
72
+ React.createElement("circle", { cx: "8", cy: "16", r: "2.5", fill: "none", stroke: "currentColor", strokeWidth: "2" }),
73
+ React.createElement("circle", { cx: "6", cy: "10", r: "2.5", fill: "none", stroke: "currentColor", strokeWidth: "2" })));
74
+ export const HeartIcon = () => (React.createElement("svg", { ...iconProps },
75
+ React.createElement("path", { d: "M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z", fill: "currentColor" })));
76
+ export const LeafIcon = () => (React.createElement("svg", { ...iconProps },
77
+ React.createElement("path", { d: "M11 20A7 7 0 0 1 9.8 6.1C15.5 5 17 4.48 19 2c1 2 2 4.18 2 8 0 5.5-4.78 10-10 10Z", fill: "currentColor" }),
78
+ React.createElement("path", { d: "M2 21c0-3 1.85-5.36 5.08-6C9 14.5 11 14 11 20", stroke: "currentColor", strokeWidth: "2", fill: "none" }),
79
+ React.createElement("path", { d: "M11 8c3 2 5 4 7 7", stroke: "white", strokeWidth: "1.5", opacity: "0.4" })));
80
+ export const LanternIcon = () => (React.createElement("svg", { ...iconProps },
81
+ React.createElement("path", { d: "M12 1.5v2" }),
82
+ React.createElement("path", { d: "M9 3.5h6" }),
83
+ React.createElement("path", { d: "M8.5 5.5h7c3 1.8 4.5 4 4.5 6.5s-1.5 4.7-4.5 6.5h-7C5.5 16.7 4 14.5 4 12s1.5-4.7 4.5-6.5z" }),
84
+ React.createElement("path", { d: "M5 9.5q7 1.6 14 0", strokeWidth: "1.5" }),
85
+ React.createElement("path", { d: "M5 14.5q7 -1.6 14 0", strokeWidth: "1.5" }),
86
+ React.createElement("path", { d: "M9 20.5h6" }),
87
+ React.createElement("path", { d: "M12 20.5v2" })));
88
+ /**
89
+ * Generic/Fallback icons for custom themes without specific icons
90
+ * These provide visual differentiation when using the ThemePicker with custom themes
91
+ */
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Per-theme advanced effect map
3
+ *
4
+ * Declares which effect modifier each surface type uses for a given
5
+ * theme. Components that want to apply effects automatically (Path B
6
+ * — themed primitive components) read this map. Until then, consumers
7
+ * apply hdk-advanced-surface[--modifier] classes directly in JSX.
8
+ *
9
+ * To add an advanced contract to a theme:
10
+ * 1. Define --advanced-stop-* and --advanced-gradient in the theme's
11
+ * [data-theme='X'] block in style.css
12
+ * 2. Add an entry to THEME_EFFECTS below
13
+ */
14
+ import type { Theme } from './index';
15
+ export type AdvancedEffect = 'shift' | 'cascade-rich' | 'sat-rich' | 'none';
16
+ export interface ThemeEffectMap {
17
+ card: AdvancedEffect;
18
+ modal: AdvancedEffect;
19
+ button: AdvancedEffect;
20
+ }
21
+ export declare const THEME_EFFECTS: Partial<Record<Theme, ThemeEffectMap>>;
22
+ /** True when the theme ships an advanced visual contract. */
23
+ export declare function hasAdvanced(theme: string): boolean;
24
+ /** Returns the effect map for a theme, or null if it has no advanced contract. */
25
+ export declare function getThemeEffects(theme: string): ThemeEffectMap | null;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Per-theme advanced effect map
3
+ *
4
+ * Declares which effect modifier each surface type uses for a given
5
+ * theme. Components that want to apply effects automatically (Path B
6
+ * — themed primitive components) read this map. Until then, consumers
7
+ * apply hdk-advanced-surface[--modifier] classes directly in JSX.
8
+ *
9
+ * To add an advanced contract to a theme:
10
+ * 1. Define --advanced-stop-* and --advanced-gradient in the theme's
11
+ * [data-theme='X'] block in style.css
12
+ * 2. Add an entry to THEME_EFFECTS below
13
+ */
14
+ export const THEME_EFFECTS = {
15
+ light: { card: 'shift', modal: 'none', button: 'none' },
16
+ 'cyberpunk-dark': { card: 'cascade-rich', modal: 'none', button: 'none' }
17
+ };
18
+ /** True when the theme ships an advanced visual contract. */
19
+ export function hasAdvanced(theme) {
20
+ return theme in THEME_EFFECTS;
21
+ }
22
+ /** Returns the effect map for a theme, or null if it has no advanced contract. */
23
+ export function getThemeEffects(theme) {
24
+ return THEME_EFFECTS[theme] ?? null;
25
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * React `<Icon>` — a thin wrapper over the string registry.
3
+ *
4
+ * The glyph is emitted as real JSX children rather than dangerouslySetInnerHTML: the
5
+ * markup is ours and build-time-generated, but React apps run under the same strict
6
+ * CSP as the frontpage, and parsing 78 vendored strings at runtime buys nothing when
7
+ * the shapes can be elements. So the paths are parsed once, lazily, per icon actually
8
+ * rendered — an app that uses six icons never touches the other seventy-two.
9
+ */
10
+ import React from 'react';
11
+ import type { IconName } from './registry.generated';
12
+ import type { IconFamily, IconVariant } from './index';
13
+ export interface IconProps {
14
+ name: IconName;
15
+ /** CSS length. Defaults to `1em` so the glyph scales with surrounding text. */
16
+ size?: string | number;
17
+ /**
18
+ * Which family token colours this icon. Applies to `accent` (the glyph itself)
19
+ * and to `tint`/`filled` (the tile). Ignored for `bare`, which inherits
20
+ * `currentColor` by design.
21
+ */
22
+ family?: IconFamily;
23
+ /** `bare` (default), `accent`, `tint`, or `filled` — see IconVariant. */
24
+ variant?: IconVariant;
25
+ /** Round the accent tile. No effect when `variant` is `bare`. */
26
+ round?: boolean;
27
+ /**
28
+ * Accessible name. Omit for decorative icons — those render `aria-hidden` so a
29
+ * screen reader does not announce a glyph that repeats an adjacent text label.
30
+ */
31
+ title?: string;
32
+ className?: string;
33
+ /** Stroke width in the 24x24 viewBox. Lucide's default is 2. */
34
+ strokeWidth?: number;
35
+ }
36
+ export declare const Icon: React.FC<IconProps>;