@ttoss/fsl-theme 1.1.22 → 2.1.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.
Files changed (66) hide show
  1. package/README.md +54 -41
  2. package/dist/{Types-BiBa17RL.d.cts → Types-a_sfpbha.d.cts} +84 -24
  3. package/dist/{Types-BiBa17RL.d.mts → Types-a_sfpbha.d.mts} +84 -24
  4. package/dist/{baseBundle-DiYlHNG5.mjs → baseBundle-CXdxj_re.mjs} +3 -2
  5. package/dist/{baseBundle-BWbICg4p.cjs → baseBundle-a9LDsqYy.cjs} +3 -2
  6. package/dist/{createTheme-BHGc1IFI.cjs → createTheme-B7H0xSVp.cjs} +128 -19
  7. package/dist/{createTheme-C1r5nJco.mjs → createTheme-Bm9LVQ4w.mjs} +123 -20
  8. package/dist/css.cjs +5 -4
  9. package/dist/css.d.cts +21 -2
  10. package/dist/css.d.mts +21 -2
  11. package/dist/css.mjs +5 -4
  12. package/dist/dataviz/index.cjs +2 -2
  13. package/dist/dataviz/index.d.cts +1 -1
  14. package/dist/dataviz/index.d.mts +1 -1
  15. package/dist/dataviz/index.mjs +1 -1
  16. package/dist/dtcg.cjs +26 -9
  17. package/dist/dtcg.d.cts +17 -4
  18. package/dist/dtcg.d.mts +17 -4
  19. package/dist/dtcg.mjs +26 -9
  20. package/dist/{helpers-CIqeJ8yd.cjs → helpers-BU40JvFA.cjs} +27 -5
  21. package/dist/{helpers-Dz7NfNK6.mjs → helpers-BXaKe2s3.mjs} +27 -5
  22. package/dist/{index-BuszkWMy.d.mts → index-BmZQVd3G.d.cts} +9 -12
  23. package/dist/{index-Djro6G4e.d.cts → index-XZsRrMTF.d.mts} +9 -12
  24. package/dist/index.cjs +2 -8
  25. package/dist/index.d.cts +4 -4
  26. package/dist/index.d.mts +4 -4
  27. package/dist/index.mjs +2 -5
  28. package/dist/{react-D2J482kx.d.mts → react-BwsjWKmo.d.mts} +59 -16
  29. package/dist/{react-BKLEzIN8.cjs → react-DQErMGJz.cjs} +107 -19
  30. package/dist/{react-BXqSYnmk.d.cts → react-Drr-QSuX.d.cts} +59 -16
  31. package/dist/react.cjs +3 -1
  32. package/dist/react.d.cts +1 -1
  33. package/dist/react.d.mts +1 -1
  34. package/dist/react.mjs +109 -19
  35. package/dist/runtime-entry.cjs +1 -1
  36. package/dist/runtime-entry.mjs +1 -1
  37. package/dist/{ssrScript-D3mYscp1.cjs → ssrScript-D73WcrCM.cjs} +52 -4
  38. package/dist/{ssrScript-DfmopH-w.mjs → ssrScript-zDK1dqXr.mjs} +52 -4
  39. package/dist/themes/bruttal.cjs +19 -4
  40. package/dist/themes/bruttal.d.cts +1 -1
  41. package/dist/themes/bruttal.d.mts +1 -1
  42. package/dist/themes/bruttal.mjs +19 -4
  43. package/dist/{toCssVars-CeckLjW1.mjs → toCssVars-CWTlp9St.mjs} +65 -5
  44. package/dist/{toCssVars-DDowz4wU.cjs → toCssVars-D0BR23cP.cjs} +65 -5
  45. package/dist/{tokenRegistry-DolhB5Vf.mjs → tokenRegistry-DviyqDeE.mjs} +19 -28
  46. package/dist/{tokenRegistry-B9zMRjpL.cjs → tokenRegistry-f_Bx_BAN.cjs} +19 -28
  47. package/dist/vars.cjs +8 -8
  48. package/dist/vars.d.cts +6 -6
  49. package/dist/vars.d.mts +6 -6
  50. package/dist/vars.mjs +8 -8
  51. package/dist/{withDataviz-BzZwFV2e.cjs → withDataviz-DrUtw8cc.cjs} +2 -2
  52. package/dist/{withDataviz-CrgXLR5A.mjs → withDataviz-Ln3sEwb7.mjs} +2 -2
  53. package/llms.txt +11 -9
  54. package/package.json +1 -1
  55. package/dist/themes/corporate.cjs +0 -34
  56. package/dist/themes/corporate.d.cts +0 -3
  57. package/dist/themes/corporate.d.mts +0 -3
  58. package/dist/themes/corporate.mjs +0 -31
  59. package/dist/themes/oca.cjs +0 -34
  60. package/dist/themes/oca.d.cts +0 -3
  61. package/dist/themes/oca.d.mts +0 -3
  62. package/dist/themes/oca.mjs +0 -31
  63. package/dist/themes/ventures.cjs +0 -34
  64. package/dist/themes/ventures.d.cts +0 -3
  65. package/dist/themes/ventures.d.mts +0 -3
  66. package/dist/themes/ventures.mjs +0 -31
@@ -26,14 +26,76 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  }) : target, mod));
27
27
 
28
28
  //#endregion
29
- const require_helpers = require('./helpers-CIqeJ8yd.cjs');
29
+ const require_helpers = require('./helpers-BU40JvFA.cjs');
30
30
  const require_css = require('./css.cjs');
31
- const require_ssrScript = require('./ssrScript-D3mYscp1.cjs');
31
+ const require_ssrScript = require('./ssrScript-D73WcrCM.cjs');
32
32
  let react = require("react");
33
33
  react = __toESM(react);
34
34
  let react_jsx_runtime = require("react/jsx-runtime");
35
35
 
36
36
  //#region src/react.tsx
37
+ /**
38
+ * Stable `href` for the theme's hoistable `<style>` tag. React 19 keys style
39
+ * hoisting **and dedup** on `href` + `precedence`, so re-renders and multiple
40
+ * `<ThemeProvider>`s sharing a `themeId` collapse to a single tag, while
41
+ * distinct `themeId`s coexist (micro-frontends). Note: `ThemeStyles` /
42
+ * `ThemeHead` render an href-less inline `<style>`, so they do **not** dedup
43
+ * against this hoisted tag — don't combine them with a themed `ThemeProvider`.
44
+ */
45
+ var themeStyleHref = themeId => {
46
+ return `tt-theme-${themeId ?? "root"}`;
47
+ };
48
+ /**
49
+ * DEV-only registry of injected theme CSS per hoisted-style `href`.
50
+ * React dedups hoisted `<style>` tags by `href`, so two providers with
51
+ * *different* themes but the same `href` silently drop the second theme's
52
+ * CSS. This registry detects that mismatch and warns.
53
+ */
54
+ var injectedThemeCss = /* @__PURE__ */new Map();
55
+ /**
56
+ * Runs on the client before paint; falls back to `useEffect` on the server
57
+ * so SSR renders never warn. Used for the runtime-creation effect so
58
+ * `data-tt-*` attributes land before first paint in CSR apps (less mode flash).
59
+ */
60
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? react.useLayoutEffect : react.useEffect;
61
+ var resolveRootElement = root => {
62
+ if (!root) return void 0;
63
+ if (root instanceof HTMLElement) return root;
64
+ return root.current ?? void 0;
65
+ };
66
+ /**
67
+ * The OS-preference CSS fallback only makes sense when the app follows the
68
+ * OS. A fixed `'light'`/`'dark'` default must not let `prefers-color-scheme`
69
+ * override it on first paint.
70
+ */
71
+ var shouldEmitSystemFallback = defaultMode => {
72
+ return (defaultMode ?? "system") === "system";
73
+ };
74
+ /**
75
+ * DEV-only: warn when two providers with different themes share the same
76
+ * hoisted-style `href` — React dedups by href, so the second theme's CSS is
77
+ * silently dropped. Distinct `themeId`s give distinct hrefs and coexist.
78
+ */
79
+ var useDedupMismatchWarning = (cssContent, themeId) => {
80
+ react.useEffect(() => {
81
+ if (process.env.NODE_ENV === "production" || !cssContent) return;
82
+ const href = themeStyleHref(themeId);
83
+ const entry = injectedThemeCss.get(href);
84
+ if (entry && entry.css !== cssContent) console.warn(`[fsl-theme] Two <ThemeProvider>s with different themes share the same style key "${href}". React dedups hoisted <style> tags by href, so only the first theme's CSS is injected. Give each theme a distinct themeId: <ThemeProvider theme={...} themeId="my-theme">.`);
85
+ const next = entry ?? {
86
+ css: cssContent,
87
+ count: 0
88
+ };
89
+ next.count += 1;
90
+ injectedThemeCss.set(href, next);
91
+ return () => {
92
+ const current = injectedThemeCss.get(href);
93
+ if (!current) return;
94
+ current.count -= 1;
95
+ if (current.count <= 0) injectedThemeCss.delete(href);
96
+ };
97
+ }, [cssContent, themeId]);
98
+ };
37
99
  var COARSE_QUERY = "(any-pointer: coarse)";
38
100
  /**
39
101
  * Subscribe to coarse-pointer media query changes.
@@ -116,20 +178,26 @@ var ThemeProvider = ({
116
178
  children
117
179
  }) => {
118
180
  const runtimeRef = react.useRef(null);
119
- const initDefaultMode = react.useRef(defaultMode);
120
- const initStorageKey = react.useRef(storageKey);
181
+ const [initialDefaultMode] = react.useState(defaultMode);
182
+ const [initialStorageKey] = react.useState(storageKey);
121
183
  const [state, setState] = react.useState(() => {
122
184
  return {
123
185
  mode: defaultMode ?? "system",
124
186
  resolvedMode: (defaultMode ?? "system") === "system" ? "light" : defaultMode
125
187
  };
126
188
  });
127
- react.useEffect(() => {
189
+ const [rootRetry, setRootRetry] = react.useState(0);
190
+ useIsomorphicLayoutEffect(() => {
191
+ if (root !== void 0 && !(root instanceof HTMLElement) && root.current === null && rootRetry === 0) {
192
+ setRootRetry(1);
193
+ return;
194
+ }
195
+ if (process.env.NODE_ENV !== "production" && root && !themeId) console.warn("[fsl-theme] `root` was passed without `themeId`. Without a themeId, the generated CSS targets `:root`/`:root[data-tt-mode=\"dark\"]` on <html>, but mode attributes are written to the `root` element — the alternate mode CSS will never match. Pass a `themeId` so CSS is scoped to `[data-tt-theme=\"<id>\"]` selectors that match the `root` element.");
128
196
  const runtime = require_ssrScript.createThemeRuntime({
129
197
  defaultTheme: themeId,
130
- defaultMode: initDefaultMode.current,
131
- storageKey: initStorageKey.current,
132
- root
198
+ defaultMode: initialDefaultMode,
199
+ storageKey: initialStorageKey,
200
+ root: resolveRootElement(root)
133
201
  });
134
202
  runtimeRef.current = runtime;
135
203
  setState(runtime.getState());
@@ -139,7 +207,7 @@ var ThemeProvider = ({
139
207
  runtime.destroy();
140
208
  runtimeRef.current = null;
141
209
  };
142
- }, [root, themeId]);
210
+ }, [root, themeId, rootRetry]);
143
211
  const setMode = react.useCallback(mode => {
144
212
  runtimeRef.current?.setMode(mode);
145
213
  }, []);
@@ -158,8 +226,11 @@ var ThemeProvider = ({
158
226
  return applyCoarseHitOverrides(result, theme.base, isCoarse);
159
227
  }, [theme, semanticTokens, isCoarse]);
160
228
  const cssContent = react.useMemo(() => {
161
- return theme ? require_css.getThemeStylesContent(theme, themeId) : null;
229
+ return theme ? require_css.getThemeStylesContent(theme, themeId, {
230
+ systemModeFallback: shouldEmitSystemFallback(initialDefaultMode)
231
+ }) : null;
162
232
  }, [theme, themeId]);
233
+ useDedupMismatchWarning(cssContent, themeId);
163
234
  const onModeChangeRef = react.useRef(onModeChange);
164
235
  react.useEffect(() => {
165
236
  onModeChangeRef.current = onModeChange;
@@ -188,6 +259,7 @@ var ThemeProvider = ({
188
259
  });
189
260
  if (theme) return /* @__PURE__ */(0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
190
261
  children: [/* @__PURE__ */(0, react_jsx_runtime.jsx)("style", {
262
+ href: themeStyleHref(themeId),
191
263
  precedence: "default",
192
264
  children: cssContent
193
265
  }), /* @__PURE__ */(0, react_jsx_runtime.jsx)(ResolvedTokensCtx.Provider, {
@@ -219,11 +291,11 @@ var useColorMode = () => {
219
291
  * inline styles produces silently broken rendering:
220
292
  *
221
293
  * ```tsx
222
- * // ✗ WRONG — tokens.colors.brand.main is '{core.colors.brand.main}', not '#FF0000'
223
- * <div style={{ color: tokens.colors.brand.main }} />
294
+ * // ✗ WRONG — the leaf is '{core.colors.neutral.1000}' (a ref string), not a CSS color
295
+ * <div style={{ color: tokens.colors.action.primary.text.default }} />
224
296
  *
225
297
  * // ✓ CSS consumers — use vars:
226
- * <div style={{ color: 'var(--tt-color-brand-main)' }} />
298
+ * <div style={{ color: 'var(--tt-colors-action-primary-text-default)' }} />
227
299
  *
228
300
  * // ✓ Non-CSS consumers (React Native, canvas) — use useResolvedTokens():
229
301
  * const resolved = useResolvedTokens();
@@ -238,8 +310,8 @@ var useColorMode = () => {
238
310
  *
239
311
  * const Button = () => {
240
312
  * const tokens = useTokens(); // introspection only
241
- * // tokens.colors.action.primary.background.default → '{core.colors.brand.500}'
242
- * return <button style={{ background: 'var(--tt-action-primary-background-default)' }} />;
313
+ * // tokens.colors.action.primary.background.default → '{core.colors.neutral.1000}'
314
+ * return <button style={{ background: 'var(--tt-colors-action-primary-background-default)' }} />;
243
315
  * };
244
316
  * ```
245
317
  */
@@ -264,13 +336,21 @@ var useTokens = () => {
264
336
  *
265
337
  * ```tsx
266
338
  * // ✓ CSS (browser)
267
- * <div style={{ color: 'var(--tt-color-informational-primary-default)' }} />
339
+ * <div style={{ color: 'var(--tt-colors-informational-primary-text-default)' }} />
268
340
  *
269
341
  * // ✓ Non-CSS (React Native, canvas)
270
342
  * const resolved = useResolvedTokens();
271
343
  * <View style={{ backgroundColor: resolved['semantic.colors.action.primary.background.default'] }} />
272
344
  * ```
273
345
  *
346
+ * ### ⚠ CSS-coupled tokens stay unresolved
347
+ * A registered set of dimensional tokens (model.md §8 — spacing steps, fluid
348
+ * `text.*.fontSize`, `sizing.hit.*`, `sizing.viewport.*`, `sizing.measure.reading`,
349
+ * `spacing.gutter.*`) carry CSS-only constructs (`var()`, `calc()`, `clamp()`,
350
+ * `cqi`, `dvh`, `ch`). This hook returns those **as-is** — they are not usable
351
+ * outside a CSS engine. Colors, opacity, z-index, font weights/leading and
352
+ * other scalar tokens resolve to plain raw values and are safe everywhere.
353
+ *
274
354
  * Requires `<ThemeProvider theme={...}>`.
275
355
  *
276
356
  * @example
@@ -344,6 +424,10 @@ var ThemeScript = ({
344
424
  * `dangerouslySetInnerHTML` is safe: content comes exclusively from
345
425
  * `toCssVars()` (a pure internal function) — no user input is interpolated.
346
426
  *
427
+ * Renders a plain inline `<style>` where you place it (put it in `<head>`).
428
+ * Do **not** also pass `theme` to `<ThemeProvider>` — that injects a second
429
+ * copy; pass `theme` to the head component only (see README SSR section).
430
+ *
347
431
  * @example
348
432
  * ```tsx
349
433
  * // SSR escape hatch — no themeId needed for canonical 1-theme model
@@ -377,12 +461,15 @@ var ThemeScript = ({
377
461
  var ThemeStyles = ({
378
462
  theme,
379
463
  themeId,
380
- nonce
464
+ nonce,
465
+ systemModeFallback
381
466
  }) => {
382
467
  return /* @__PURE__ */(0, react_jsx_runtime.jsx)("style", {
383
468
  nonce,
384
469
  dangerouslySetInnerHTML: {
385
- __html: require_css.getThemeStylesContent(theme, themeId)
470
+ __html: require_css.getThemeStylesContent(theme, themeId, {
471
+ systemModeFallback
472
+ })
386
473
  }
387
474
  });
388
475
  };
@@ -445,7 +532,8 @@ var ThemeHead = ({
445
532
  }), /* @__PURE__ */(0, react_jsx_runtime.jsx)(ThemeStyles, {
446
533
  theme,
447
534
  themeId,
448
- nonce
535
+ nonce,
536
+ systemModeFallback: shouldEmitSystemFallback(defaultMode)
449
537
  })]
450
538
  });
451
539
  };
@@ -1,9 +1,17 @@
1
1
 
2
- import { n as SemanticTokens, r as ThemeBundle } from "./Types-BiBa17RL.cjs";
2
+ import { i as ThemeBundle, n as SemanticTokens } from "./Types-a_sfpbha.cjs";
3
3
  import { a as ResolvedMode, o as ThemeMode, t as ThemeScriptConfig } from "./ssrScript-BVysxDws.cjs";
4
4
  import * as React from "react";
5
5
 
6
6
  //#region src/react.d.ts
7
+ /**
8
+ * Accepted shapes for the `root` prop: a concrete element, or a ref object.
9
+ * Prefer the ref form — refs are populated before layout effects run, so the
10
+ * runtime attaches directly to the target element with no transient attach
11
+ * to `<html>` on the first render (the element form is `null` during the
12
+ * first render when read from `ref.current`).
13
+ */
14
+ type ThemeRootInput = HTMLElement | React.RefObject<HTMLElement | null>;
7
15
  /**
8
16
  * Props for the `ThemeProvider` component.
9
17
  */
@@ -24,7 +32,15 @@ interface ThemeProviderProps {
24
32
  *
25
33
  * Passing `theme` enables `useTokens()` and `useResolvedTokens()` for all
26
34
  * descendants, and automatically injects the CSS Custom Properties `<style>`
27
- * tag into the document head (React 19 hoisting).
35
+ * tag into the document head (React 19 hoisting, deduped by `href`).
36
+ *
37
+ * **React version:** auto-injection into `<head>` requires **React 19** style
38
+ * hoisting. On React 18 the `<style>` renders inline where the provider sits
39
+ * (not hoisted); use `<ThemeHead>` / `<ThemeStyles>` in your `<head>` for
40
+ * explicit injection there — but do not also pass `theme` here, since the
41
+ * href-less `<ThemeHead>`/`<ThemeStyles>` tag does not dedup against this
42
+ * provider's href-keyed one. The stable `href` only collapses multiple themed
43
+ * `<ThemeProvider>`s sharing a `themeId` into one tag on 19.
28
44
  *
29
45
  * @example
30
46
  * ```tsx
@@ -75,25 +91,32 @@ interface ThemeProviderProps {
75
91
  onModeChange?: (mode: ThemeMode, resolvedMode: ResolvedMode) => void;
76
92
  /**
77
93
  * Root element to anchor `data-tt-theme` / `data-tt-mode` attributes.
78
- * Defaults to `document.documentElement`. Pass a container element for
79
- * Storybook isolation or micro-frontend use cases.
94
+ * Defaults to `document.documentElement`. Pass a container for Storybook
95
+ * isolation or micro-frontend use cases.
80
96
  *
81
- * Because the element is often `null` on the first render when passed via
82
- * `ref.current`, `root` is reactive: the runtime is recreated once when
83
- * it transitions from `undefined` to the actual element.
97
+ * **Must be paired with `themeId`.** Without a `themeId`, the generated CSS
98
+ * targets `:root` / `:root[data-tt-mode="dark"]` (the `<html>` element),
99
+ * while the attributes are written to this element — the alternate mode CSS
100
+ * would never match. A dev-mode warning fires on this combination.
101
+ *
102
+ * **Prefer passing the ref object itself** (`root={rootRef}`): refs are
103
+ * populated before layout effects, so the runtime attaches directly to the
104
+ * element. Passing `rootRef.current ?? undefined` also works, but the value
105
+ * is `null` on the first render, causing one transient attach to `<html>`
106
+ * before the runtime is recreated on the element.
84
107
  *
85
108
  * @example
86
109
  * ```tsx
87
110
  * // Storybook decorator — isolates each story from <html>
88
111
  * const rootRef = React.useRef<HTMLDivElement>(null);
89
112
  * <div ref={rootRef}>
90
- * <ThemeProvider theme={myTheme} root={rootRef.current ?? undefined}>
113
+ * <ThemeProvider theme={myTheme} themeId="story" root={rootRef}>
91
114
  * <Story />
92
115
  * </ThemeProvider>
93
116
  * </div>
94
117
  * ```
95
118
  */
96
- root?: HTMLElement;
119
+ root?: ThemeRootInput;
97
120
  children: React.ReactNode;
98
121
  }
99
122
  /**
@@ -170,11 +193,11 @@ declare const useColorMode: () => UseColorModeResult;
170
193
  * inline styles produces silently broken rendering:
171
194
  *
172
195
  * ```tsx
173
- * // ✗ WRONG — tokens.colors.brand.main is '{core.colors.brand.main}', not '#FF0000'
174
- * <div style={{ color: tokens.colors.brand.main }} />
196
+ * // ✗ WRONG — the leaf is '{core.colors.neutral.1000}' (a ref string), not a CSS color
197
+ * <div style={{ color: tokens.colors.action.primary.text.default }} />
175
198
  *
176
199
  * // ✓ CSS consumers — use vars:
177
- * <div style={{ color: 'var(--tt-color-brand-main)' }} />
200
+ * <div style={{ color: 'var(--tt-colors-action-primary-text-default)' }} />
178
201
  *
179
202
  * // ✓ Non-CSS consumers (React Native, canvas) — use useResolvedTokens():
180
203
  * const resolved = useResolvedTokens();
@@ -189,8 +212,8 @@ declare const useColorMode: () => UseColorModeResult;
189
212
  *
190
213
  * const Button = () => {
191
214
  * const tokens = useTokens(); // introspection only
192
- * // tokens.colors.action.primary.background.default → '{core.colors.brand.500}'
193
- * return <button style={{ background: 'var(--tt-action-primary-background-default)' }} />;
215
+ * // tokens.colors.action.primary.background.default → '{core.colors.neutral.1000}'
216
+ * return <button style={{ background: 'var(--tt-colors-action-primary-background-default)' }} />;
194
217
  * };
195
218
  * ```
196
219
  */
@@ -210,13 +233,21 @@ declare const useTokens: () => SemanticTokens;
210
233
  *
211
234
  * ```tsx
212
235
  * // ✓ CSS (browser)
213
- * <div style={{ color: 'var(--tt-color-informational-primary-default)' }} />
236
+ * <div style={{ color: 'var(--tt-colors-informational-primary-text-default)' }} />
214
237
  *
215
238
  * // ✓ Non-CSS (React Native, canvas)
216
239
  * const resolved = useResolvedTokens();
217
240
  * <View style={{ backgroundColor: resolved['semantic.colors.action.primary.background.default'] }} />
218
241
  * ```
219
242
  *
243
+ * ### ⚠ CSS-coupled tokens stay unresolved
244
+ * A registered set of dimensional tokens (model.md §8 — spacing steps, fluid
245
+ * `text.*.fontSize`, `sizing.hit.*`, `sizing.viewport.*`, `sizing.measure.reading`,
246
+ * `spacing.gutter.*`) carry CSS-only constructs (`var()`, `calc()`, `clamp()`,
247
+ * `cqi`, `dvh`, `ch`). This hook returns those **as-is** — they are not usable
248
+ * outside a CSS engine. Colors, opacity, z-index, font weights/leading and
249
+ * other scalar tokens resolve to plain raw values and are safe everywhere.
250
+ *
220
251
  * Requires `<ThemeProvider theme={...}>`.
221
252
  *
222
253
  * @example
@@ -288,6 +319,13 @@ interface ThemeStylesProps {
288
319
  themeId?: string;
289
320
  /** CSP nonce for the inline style tag. */
290
321
  nonce?: string;
322
+ /**
323
+ * Emit the `@media (prefers-color-scheme)` fallback block (themeId-less
324
+ * bundles only). Default `true`. Set `false` when the app's `defaultMode`
325
+ * is a fixed `'light'`/`'dark'` rather than `'system'` — `<ThemeHead>`
326
+ * derives this automatically from its `defaultMode`.
327
+ */
328
+ systemModeFallback?: boolean;
291
329
  }
292
330
  /**
293
331
  * Renders an inline `<style>` tag with all CSS Custom Properties for a theme
@@ -301,6 +339,10 @@ interface ThemeStylesProps {
301
339
  * `dangerouslySetInnerHTML` is safe: content comes exclusively from
302
340
  * `toCssVars()` (a pure internal function) — no user input is interpolated.
303
341
  *
342
+ * Renders a plain inline `<style>` where you place it (put it in `<head>`).
343
+ * Do **not** also pass `theme` to `<ThemeProvider>` — that injects a second
344
+ * copy; pass `theme` to the head component only (see README SSR section).
345
+ *
304
346
  * @example
305
347
  * ```tsx
306
348
  * // SSR escape hatch — no themeId needed for canonical 1-theme model
@@ -334,7 +376,8 @@ interface ThemeStylesProps {
334
376
  declare const ThemeStyles: ({
335
377
  theme,
336
378
  themeId,
337
- nonce
379
+ nonce,
380
+ systemModeFallback
338
381
  }: ThemeStylesProps) => any;
339
382
  /**
340
383
  * Props for the `ThemeHead` component.
package/dist/react.cjs CHANGED
@@ -1,8 +1,10 @@
1
1
  /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+ 'use client';
3
+
2
4
  Object.defineProperty(exports, Symbol.toStringTag, {
3
5
  value: 'Module'
4
6
  });
5
- const require_react = require('./react-BKLEzIN8.cjs');
7
+ const require_react = require('./react-DQErMGJz.cjs');
6
8
  exports.ThemeHead = require_react.ThemeHead;
7
9
  exports.ThemeProvider = require_react.ThemeProvider;
8
10
  exports.ThemeScript = require_react.ThemeScript;
package/dist/react.d.cts CHANGED
@@ -1,4 +1,4 @@
1
1
 
2
2
  import { a as ResolvedMode, o as ThemeMode } from "./ssrScript-BVysxDws.cjs";
3
- import { a as ThemeScript, c as ThemeStylesProps, d as useResolvedTokens, f as useTokens, i as ThemeProviderProps, l as UseColorModeResult, n as ThemeHeadProps, o as ThemeScriptProps, r as ThemeProvider, s as ThemeStyles, t as ThemeHead, u as useColorMode } from "./react-BXqSYnmk.cjs";
3
+ import { a as ThemeScript, c as ThemeStylesProps, d as useResolvedTokens, f as useTokens, i as ThemeProviderProps, l as UseColorModeResult, n as ThemeHeadProps, o as ThemeScriptProps, r as ThemeProvider, s as ThemeStyles, t as ThemeHead, u as useColorMode } from "./react-Drr-QSuX.cjs";
4
4
  export { type ResolvedMode, ThemeHead, ThemeHeadProps, type ThemeMode, ThemeProvider, ThemeProviderProps, ThemeScript, ThemeScriptProps, ThemeStyles, ThemeStylesProps, UseColorModeResult, useColorMode, useResolvedTokens, useTokens };
package/dist/react.d.mts CHANGED
@@ -1,4 +1,4 @@
1
1
 
2
2
  import { a as ResolvedMode, o as ThemeMode } from "./ssrScript-BVysxDws.mjs";
3
- import { a as ThemeScript, c as ThemeStylesProps, d as useResolvedTokens, f as useTokens, i as ThemeProviderProps, l as UseColorModeResult, n as ThemeHeadProps, o as ThemeScriptProps, r as ThemeProvider, s as ThemeStyles, t as ThemeHead, u as useColorMode } from "./react-D2J482kx.mjs";
3
+ import { a as ThemeScript, c as ThemeStylesProps, d as useResolvedTokens, f as useTokens, i as ThemeProviderProps, l as UseColorModeResult, n as ThemeHeadProps, o as ThemeScriptProps, r as ThemeProvider, s as ThemeStyles, t as ThemeHead, u as useColorMode } from "./react-BwsjWKmo.mjs";
4
4
  export { type ResolvedMode, ThemeHead, ThemeHeadProps, type ThemeMode, ThemeProvider, ThemeProviderProps, ThemeScript, ThemeScriptProps, ThemeStyles, ThemeStylesProps, UseColorModeResult, useColorMode, useResolvedTokens, useTokens };
package/dist/react.mjs CHANGED
@@ -1,11 +1,75 @@
1
1
  /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
- import { r as deepMerge, s as toFlatTokens } from "./helpers-Dz7NfNK6.mjs";
2
+ 'use client';
3
+
4
+ import { r as deepMerge, s as toFlatTokens } from "./helpers-BXaKe2s3.mjs";
3
5
  import { getThemeStylesContent } from "./css.mjs";
4
- import { i as createThemeRuntime, t as getThemeScriptContent } from "./ssrScript-DfmopH-w.mjs";
6
+ import { i as createThemeRuntime, t as getThemeScriptContent } from "./ssrScript-zDK1dqXr.mjs";
5
7
  import * as React from "react";
6
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
9
 
8
10
  //#region src/react.tsx
11
+ /**
12
+ * Stable `href` for the theme's hoistable `<style>` tag. React 19 keys style
13
+ * hoisting **and dedup** on `href` + `precedence`, so re-renders and multiple
14
+ * `<ThemeProvider>`s sharing a `themeId` collapse to a single tag, while
15
+ * distinct `themeId`s coexist (micro-frontends). Note: `ThemeStyles` /
16
+ * `ThemeHead` render an href-less inline `<style>`, so they do **not** dedup
17
+ * against this hoisted tag — don't combine them with a themed `ThemeProvider`.
18
+ */
19
+ var themeStyleHref = themeId => {
20
+ return `tt-theme-${themeId ?? "root"}`;
21
+ };
22
+ /**
23
+ * DEV-only registry of injected theme CSS per hoisted-style `href`.
24
+ * React dedups hoisted `<style>` tags by `href`, so two providers with
25
+ * *different* themes but the same `href` silently drop the second theme's
26
+ * CSS. This registry detects that mismatch and warns.
27
+ */
28
+ var injectedThemeCss = /* @__PURE__ */new Map();
29
+ /**
30
+ * Runs on the client before paint; falls back to `useEffect` on the server
31
+ * so SSR renders never warn. Used for the runtime-creation effect so
32
+ * `data-tt-*` attributes land before first paint in CSR apps (less mode flash).
33
+ */
34
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect;
35
+ var resolveRootElement = root => {
36
+ if (!root) return void 0;
37
+ if (root instanceof HTMLElement) return root;
38
+ return root.current ?? void 0;
39
+ };
40
+ /**
41
+ * The OS-preference CSS fallback only makes sense when the app follows the
42
+ * OS. A fixed `'light'`/`'dark'` default must not let `prefers-color-scheme`
43
+ * override it on first paint.
44
+ */
45
+ var shouldEmitSystemFallback = defaultMode => {
46
+ return (defaultMode ?? "system") === "system";
47
+ };
48
+ /**
49
+ * DEV-only: warn when two providers with different themes share the same
50
+ * hoisted-style `href` — React dedups by href, so the second theme's CSS is
51
+ * silently dropped. Distinct `themeId`s give distinct hrefs and coexist.
52
+ */
53
+ var useDedupMismatchWarning = (cssContent, themeId) => {
54
+ React.useEffect(() => {
55
+ if (process.env.NODE_ENV === "production" || !cssContent) return;
56
+ const href = themeStyleHref(themeId);
57
+ const entry = injectedThemeCss.get(href);
58
+ if (entry && entry.css !== cssContent) console.warn(`[fsl-theme] Two <ThemeProvider>s with different themes share the same style key "${href}". React dedups hoisted <style> tags by href, so only the first theme's CSS is injected. Give each theme a distinct themeId: <ThemeProvider theme={...} themeId="my-theme">.`);
59
+ const next = entry ?? {
60
+ css: cssContent,
61
+ count: 0
62
+ };
63
+ next.count += 1;
64
+ injectedThemeCss.set(href, next);
65
+ return () => {
66
+ const current = injectedThemeCss.get(href);
67
+ if (!current) return;
68
+ current.count -= 1;
69
+ if (current.count <= 0) injectedThemeCss.delete(href);
70
+ };
71
+ }, [cssContent, themeId]);
72
+ };
9
73
  var COARSE_QUERY = "(any-pointer: coarse)";
10
74
  /**
11
75
  * Subscribe to coarse-pointer media query changes.
@@ -88,20 +152,26 @@ var ThemeProvider = ({
88
152
  children
89
153
  }) => {
90
154
  const runtimeRef = React.useRef(null);
91
- const initDefaultMode = React.useRef(defaultMode);
92
- const initStorageKey = React.useRef(storageKey);
155
+ const [initialDefaultMode] = React.useState(defaultMode);
156
+ const [initialStorageKey] = React.useState(storageKey);
93
157
  const [state, setState] = React.useState(() => {
94
158
  return {
95
159
  mode: defaultMode ?? "system",
96
160
  resolvedMode: (defaultMode ?? "system") === "system" ? "light" : defaultMode
97
161
  };
98
162
  });
99
- React.useEffect(() => {
163
+ const [rootRetry, setRootRetry] = React.useState(0);
164
+ useIsomorphicLayoutEffect(() => {
165
+ if (root !== void 0 && !(root instanceof HTMLElement) && root.current === null && rootRetry === 0) {
166
+ setRootRetry(1);
167
+ return;
168
+ }
169
+ if (process.env.NODE_ENV !== "production" && root && !themeId) console.warn("[fsl-theme] `root` was passed without `themeId`. Without a themeId, the generated CSS targets `:root`/`:root[data-tt-mode=\"dark\"]` on <html>, but mode attributes are written to the `root` element — the alternate mode CSS will never match. Pass a `themeId` so CSS is scoped to `[data-tt-theme=\"<id>\"]` selectors that match the `root` element.");
100
170
  const runtime = createThemeRuntime({
101
171
  defaultTheme: themeId,
102
- defaultMode: initDefaultMode.current,
103
- storageKey: initStorageKey.current,
104
- root
172
+ defaultMode: initialDefaultMode,
173
+ storageKey: initialStorageKey,
174
+ root: resolveRootElement(root)
105
175
  });
106
176
  runtimeRef.current = runtime;
107
177
  setState(runtime.getState());
@@ -111,7 +181,7 @@ var ThemeProvider = ({
111
181
  runtime.destroy();
112
182
  runtimeRef.current = null;
113
183
  };
114
- }, [root, themeId]);
184
+ }, [root, themeId, rootRetry]);
115
185
  const setMode = React.useCallback(mode => {
116
186
  runtimeRef.current?.setMode(mode);
117
187
  }, []);
@@ -130,8 +200,11 @@ var ThemeProvider = ({
130
200
  return applyCoarseHitOverrides(result, theme.base, isCoarse);
131
201
  }, [theme, semanticTokens, isCoarse]);
132
202
  const cssContent = React.useMemo(() => {
133
- return theme ? getThemeStylesContent(theme, themeId) : null;
203
+ return theme ? getThemeStylesContent(theme, themeId, {
204
+ systemModeFallback: shouldEmitSystemFallback(initialDefaultMode)
205
+ }) : null;
134
206
  }, [theme, themeId]);
207
+ useDedupMismatchWarning(cssContent, themeId);
135
208
  const onModeChangeRef = React.useRef(onModeChange);
136
209
  React.useEffect(() => {
137
210
  onModeChangeRef.current = onModeChange;
@@ -160,6 +233,7 @@ var ThemeProvider = ({
160
233
  });
161
234
  if (theme) return /* @__PURE__ */jsxs(Fragment, {
162
235
  children: [/* @__PURE__ */jsx("style", {
236
+ href: themeStyleHref(themeId),
163
237
  precedence: "default",
164
238
  children: cssContent
165
239
  }), /* @__PURE__ */jsx(ResolvedTokensCtx.Provider, {
@@ -191,11 +265,11 @@ var useColorMode = () => {
191
265
  * inline styles produces silently broken rendering:
192
266
  *
193
267
  * ```tsx
194
- * // ✗ WRONG — tokens.colors.brand.main is '{core.colors.brand.main}', not '#FF0000'
195
- * <div style={{ color: tokens.colors.brand.main }} />
268
+ * // ✗ WRONG — the leaf is '{core.colors.neutral.1000}' (a ref string), not a CSS color
269
+ * <div style={{ color: tokens.colors.action.primary.text.default }} />
196
270
  *
197
271
  * // ✓ CSS consumers — use vars:
198
- * <div style={{ color: 'var(--tt-color-brand-main)' }} />
272
+ * <div style={{ color: 'var(--tt-colors-action-primary-text-default)' }} />
199
273
  *
200
274
  * // ✓ Non-CSS consumers (React Native, canvas) — use useResolvedTokens():
201
275
  * const resolved = useResolvedTokens();
@@ -210,8 +284,8 @@ var useColorMode = () => {
210
284
  *
211
285
  * const Button = () => {
212
286
  * const tokens = useTokens(); // introspection only
213
- * // tokens.colors.action.primary.background.default → '{core.colors.brand.500}'
214
- * return <button style={{ background: 'var(--tt-action-primary-background-default)' }} />;
287
+ * // tokens.colors.action.primary.background.default → '{core.colors.neutral.1000}'
288
+ * return <button style={{ background: 'var(--tt-colors-action-primary-background-default)' }} />;
215
289
  * };
216
290
  * ```
217
291
  */
@@ -236,13 +310,21 @@ var useTokens = () => {
236
310
  *
237
311
  * ```tsx
238
312
  * // ✓ CSS (browser)
239
- * <div style={{ color: 'var(--tt-color-informational-primary-default)' }} />
313
+ * <div style={{ color: 'var(--tt-colors-informational-primary-text-default)' }} />
240
314
  *
241
315
  * // ✓ Non-CSS (React Native, canvas)
242
316
  * const resolved = useResolvedTokens();
243
317
  * <View style={{ backgroundColor: resolved['semantic.colors.action.primary.background.default'] }} />
244
318
  * ```
245
319
  *
320
+ * ### ⚠ CSS-coupled tokens stay unresolved
321
+ * A registered set of dimensional tokens (model.md §8 — spacing steps, fluid
322
+ * `text.*.fontSize`, `sizing.hit.*`, `sizing.viewport.*`, `sizing.measure.reading`,
323
+ * `spacing.gutter.*`) carry CSS-only constructs (`var()`, `calc()`, `clamp()`,
324
+ * `cqi`, `dvh`, `ch`). This hook returns those **as-is** — they are not usable
325
+ * outside a CSS engine. Colors, opacity, z-index, font weights/leading and
326
+ * other scalar tokens resolve to plain raw values and are safe everywhere.
327
+ *
246
328
  * Requires `<ThemeProvider theme={...}>`.
247
329
  *
248
330
  * @example
@@ -316,6 +398,10 @@ var ThemeScript = ({
316
398
  * `dangerouslySetInnerHTML` is safe: content comes exclusively from
317
399
  * `toCssVars()` (a pure internal function) — no user input is interpolated.
318
400
  *
401
+ * Renders a plain inline `<style>` where you place it (put it in `<head>`).
402
+ * Do **not** also pass `theme` to `<ThemeProvider>` — that injects a second
403
+ * copy; pass `theme` to the head component only (see README SSR section).
404
+ *
319
405
  * @example
320
406
  * ```tsx
321
407
  * // SSR escape hatch — no themeId needed for canonical 1-theme model
@@ -349,12 +435,15 @@ var ThemeScript = ({
349
435
  var ThemeStyles = ({
350
436
  theme,
351
437
  themeId,
352
- nonce
438
+ nonce,
439
+ systemModeFallback
353
440
  }) => {
354
441
  return /* @__PURE__ */jsx("style", {
355
442
  nonce,
356
443
  dangerouslySetInnerHTML: {
357
- __html: getThemeStylesContent(theme, themeId)
444
+ __html: getThemeStylesContent(theme, themeId, {
445
+ systemModeFallback
446
+ })
358
447
  }
359
448
  });
360
449
  };
@@ -417,7 +506,8 @@ var ThemeHead = ({
417
506
  }), /* @__PURE__ */jsx(ThemeStyles, {
418
507
  theme,
419
508
  themeId,
420
- nonce
509
+ nonce,
510
+ systemModeFallback: shouldEmitSystemFallback(defaultMode)
421
511
  })]
422
512
  });
423
513
  };
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, {
3
3
  value: 'Module'
4
4
  });
5
- const require_ssrScript = require('./ssrScript-D3mYscp1.cjs');
5
+ const require_ssrScript = require('./ssrScript-D73WcrCM.cjs');
6
6
  exports.DATA_MODE_ATTR = require_ssrScript.DATA_MODE_ATTR;
7
7
  exports.DATA_THEME_ATTR = require_ssrScript.DATA_THEME_ATTR;
8
8
  exports.createThemeRuntime = require_ssrScript.createThemeRuntime;
@@ -1,3 +1,3 @@
1
1
  /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
- import { i as createThemeRuntime, n as DATA_MODE_ATTR, r as DATA_THEME_ATTR, t as getThemeScriptContent } from "./ssrScript-DfmopH-w.mjs";
2
+ import { i as createThemeRuntime, n as DATA_MODE_ATTR, r as DATA_THEME_ATTR, t as getThemeScriptContent } from "./ssrScript-zDK1dqXr.mjs";
3
3
  export { DATA_MODE_ATTR, DATA_THEME_ATTR, createThemeRuntime, getThemeScriptContent };