@ttoss/fsl-theme 2.0.0 → 2.1.1

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 (48) hide show
  1. package/README.md +41 -30
  2. package/dist/{Types-BkaxtsMv.d.cts → Types-a_sfpbha.d.cts} +23 -11
  3. package/dist/{Types-BkaxtsMv.d.mts → Types-a_sfpbha.d.mts} +23 -11
  4. package/dist/{baseBundle-R7WyjyTw.mjs → baseBundle-CXdxj_re.mjs} +1 -1
  5. package/dist/{baseBundle-DdRSa5mf.cjs → baseBundle-a9LDsqYy.cjs} +1 -1
  6. package/dist/{createTheme--UFgVlEn.cjs → createTheme-B7H0xSVp.cjs} +95 -17
  7. package/dist/{createTheme-B6tVIikC.mjs → createTheme-Bm9LVQ4w.mjs} +96 -18
  8. package/dist/css.cjs +4 -3
  9. package/dist/css.d.cts +21 -2
  10. package/dist/css.d.mts +21 -2
  11. package/dist/css.mjs +4 -3
  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.d.cts +1 -1
  17. package/dist/dtcg.d.mts +1 -1
  18. package/dist/{index-7YL90Xqs.d.cts → index-BmZQVd3G.d.cts} +1 -1
  19. package/dist/{index-BmCTIOzh.d.mts → index-XZsRrMTF.d.mts} +1 -1
  20. package/dist/index.cjs +1 -1
  21. package/dist/index.d.cts +3 -3
  22. package/dist/index.d.mts +3 -3
  23. package/dist/index.mjs +1 -1
  24. package/dist/{react-Dt7vGAVG.d.cts → react-2D_CNVF2.d.cts} +50 -19
  25. package/dist/{react-C8JrZf-b.d.mts → react-BwnHHvJj.d.mts} +50 -19
  26. package/dist/{react-DLB8nrXQ.cjs → react-DQErMGJz.cjs} +90 -18
  27. package/dist/react.cjs +3 -1
  28. package/dist/react.d.cts +1 -1
  29. package/dist/react.d.mts +1 -1
  30. package/dist/react.mjs +92 -18
  31. package/dist/runtime-entry.cjs +1 -1
  32. package/dist/runtime-entry.mjs +1 -1
  33. package/dist/{ssrScript-MF5SIONH.cjs → ssrScript-D73WcrCM.cjs} +51 -3
  34. package/dist/{ssrScript-DNLlLDps.mjs → ssrScript-zDK1dqXr.mjs} +51 -3
  35. package/dist/themes/bruttal.cjs +2 -2
  36. package/dist/themes/bruttal.d.cts +1 -1
  37. package/dist/themes/bruttal.d.mts +1 -1
  38. package/dist/themes/bruttal.mjs +2 -2
  39. package/dist/{toCssVars-C-9yIZDR.mjs → toCssVars-CWTlp9St.mjs} +38 -3
  40. package/dist/{toCssVars-BseqmMcT.cjs → toCssVars-D0BR23cP.cjs} +38 -3
  41. package/dist/vars.cjs +7 -7
  42. package/dist/vars.d.cts +6 -6
  43. package/dist/vars.d.mts +6 -6
  44. package/dist/vars.mjs +7 -7
  45. package/dist/{withDataviz-CYRf4exT.cjs → withDataviz-DrUtw8cc.cjs} +1 -1
  46. package/dist/{withDataviz-BvpZJnFx.mjs → withDataviz-Ln3sEwb7.mjs} +1 -1
  47. package/llms.txt +6 -4
  48. package/package.json +8 -4
@@ -1,9 +1,17 @@
1
1
 
2
- import { i as ThemeBundle, n as SemanticTokens } from "./Types-BkaxtsMv.mjs";
2
+ import { i as ThemeBundle, n as SemanticTokens } from "./Types-a_sfpbha.mjs";
3
3
  import { a as ResolvedMode, o as ThemeMode, t as ThemeScriptConfig } from "./ssrScript-BVysxDws.mjs";
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
  */
@@ -83,25 +91,32 @@ interface ThemeProviderProps {
83
91
  onModeChange?: (mode: ThemeMode, resolvedMode: ResolvedMode) => void;
84
92
  /**
85
93
  * Root element to anchor `data-tt-theme` / `data-tt-mode` attributes.
86
- * Defaults to `document.documentElement`. Pass a container element for
87
- * Storybook isolation or micro-frontend use cases.
94
+ * Defaults to `document.documentElement`. Pass a container for Storybook
95
+ * isolation or micro-frontend use cases.
88
96
  *
89
- * Because the element is often `null` on the first render when passed via
90
- * `ref.current`, `root` is reactive: the runtime is recreated once when
91
- * 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.
92
107
  *
93
108
  * @example
94
109
  * ```tsx
95
110
  * // Storybook decorator — isolates each story from <html>
96
111
  * const rootRef = React.useRef<HTMLDivElement>(null);
97
112
  * <div ref={rootRef}>
98
- * <ThemeProvider theme={myTheme} root={rootRef.current ?? undefined}>
113
+ * <ThemeProvider theme={myTheme} themeId="story" root={rootRef}>
99
114
  * <Story />
100
115
  * </ThemeProvider>
101
116
  * </div>
102
117
  * ```
103
118
  */
104
- root?: HTMLElement;
119
+ root?: ThemeRootInput;
105
120
  children: React.ReactNode;
106
121
  }
107
122
  /**
@@ -134,7 +149,7 @@ declare const ThemeProvider: ({
134
149
  onModeChange,
135
150
  root,
136
151
  children
137
- }: ThemeProviderProps) => any;
152
+ }: ThemeProviderProps) => import("react/jsx-runtime").JSX.Element;
138
153
  /**
139
154
  * Access mode state and the mode setter — without subscribing to theme changes.
140
155
  *
@@ -178,11 +193,11 @@ declare const useColorMode: () => UseColorModeResult;
178
193
  * inline styles produces silently broken rendering:
179
194
  *
180
195
  * ```tsx
181
- * // ✗ WRONG — tokens.colors.brand.main is '{core.colors.brand.main}', not '#FF0000'
182
- * <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 }} />
183
198
  *
184
199
  * // ✓ CSS consumers — use vars:
185
- * <div style={{ color: 'var(--tt-color-brand-main)' }} />
200
+ * <div style={{ color: 'var(--tt-colors-action-primary-text-default)' }} />
186
201
  *
187
202
  * // ✓ Non-CSS consumers (React Native, canvas) — use useResolvedTokens():
188
203
  * const resolved = useResolvedTokens();
@@ -197,8 +212,8 @@ declare const useColorMode: () => UseColorModeResult;
197
212
  *
198
213
  * const Button = () => {
199
214
  * const tokens = useTokens(); // introspection only
200
- * // tokens.colors.action.primary.background.default → '{core.colors.brand.500}'
201
- * 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)' }} />;
202
217
  * };
203
218
  * ```
204
219
  */
@@ -218,13 +233,21 @@ declare const useTokens: () => SemanticTokens;
218
233
  *
219
234
  * ```tsx
220
235
  * // ✓ CSS (browser)
221
- * <div style={{ color: 'var(--tt-color-informational-primary-default)' }} />
236
+ * <div style={{ color: 'var(--tt-colors-informational-primary-text-default)' }} />
222
237
  *
223
238
  * // ✓ Non-CSS (React Native, canvas)
224
239
  * const resolved = useResolvedTokens();
225
240
  * <View style={{ backgroundColor: resolved['semantic.colors.action.primary.background.default'] }} />
226
241
  * ```
227
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
+ *
228
251
  * Requires `<ThemeProvider theme={...}>`.
229
252
  *
230
253
  * @example
@@ -276,7 +299,7 @@ declare const ThemeScript: ({
276
299
  defaultMode,
277
300
  storageKey,
278
301
  nonce
279
- }?: ThemeScriptProps) => any;
302
+ }?: ThemeScriptProps) => import("react/jsx-runtime").JSX.Element;
280
303
  /**
281
304
  * Props for the `ThemeStyles` component.
282
305
  */
@@ -296,6 +319,13 @@ interface ThemeStylesProps {
296
319
  themeId?: string;
297
320
  /** CSP nonce for the inline style tag. */
298
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;
299
329
  }
300
330
  /**
301
331
  * Renders an inline `<style>` tag with all CSS Custom Properties for a theme
@@ -346,8 +376,9 @@ interface ThemeStylesProps {
346
376
  declare const ThemeStyles: ({
347
377
  theme,
348
378
  themeId,
349
- nonce
350
- }: ThemeStylesProps) => any;
379
+ nonce,
380
+ systemModeFallback
381
+ }: ThemeStylesProps) => import("react/jsx-runtime").JSX.Element;
351
382
  /**
352
383
  * Props for the `ThemeHead` component.
353
384
  */
@@ -406,6 +437,6 @@ declare const ThemeHead: ({
406
437
  nonce,
407
438
  defaultMode,
408
439
  storageKey
409
- }: ThemeHeadProps) => any;
440
+ }: ThemeHeadProps) => import("react/jsx-runtime").JSX.Element;
410
441
  //#endregion
411
442
  export { ThemeScript as a, ThemeStylesProps as c, useResolvedTokens as d, useTokens as f, ThemeProviderProps as i, UseColorModeResult as l, ThemeHeadProps as n, ThemeScriptProps as o, ThemeProvider as r, ThemeStyles as s, ThemeHead as t, useColorMode as u };
@@ -28,7 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  //#endregion
29
29
  const require_helpers = require('./helpers-BU40JvFA.cjs');
30
30
  const require_css = require('./css.cjs');
31
- const require_ssrScript = require('./ssrScript-MF5SIONH.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");
@@ -45,6 +45,57 @@ let react_jsx_runtime = require("react/jsx-runtime");
45
45
  var themeStyleHref = themeId => {
46
46
  return `tt-theme-${themeId ?? "root"}`;
47
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
+ };
48
99
  var COARSE_QUERY = "(any-pointer: coarse)";
49
100
  /**
50
101
  * Subscribe to coarse-pointer media query changes.
@@ -127,20 +178,26 @@ var ThemeProvider = ({
127
178
  children
128
179
  }) => {
129
180
  const runtimeRef = react.useRef(null);
130
- const initDefaultMode = react.useRef(defaultMode);
131
- const initStorageKey = react.useRef(storageKey);
181
+ const [initialDefaultMode] = react.useState(defaultMode);
182
+ const [initialStorageKey] = react.useState(storageKey);
132
183
  const [state, setState] = react.useState(() => {
133
184
  return {
134
185
  mode: defaultMode ?? "system",
135
186
  resolvedMode: (defaultMode ?? "system") === "system" ? "light" : defaultMode
136
187
  };
137
188
  });
138
- 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.");
139
196
  const runtime = require_ssrScript.createThemeRuntime({
140
197
  defaultTheme: themeId,
141
- defaultMode: initDefaultMode.current,
142
- storageKey: initStorageKey.current,
143
- root
198
+ defaultMode: initialDefaultMode,
199
+ storageKey: initialStorageKey,
200
+ root: resolveRootElement(root)
144
201
  });
145
202
  runtimeRef.current = runtime;
146
203
  setState(runtime.getState());
@@ -150,7 +207,7 @@ var ThemeProvider = ({
150
207
  runtime.destroy();
151
208
  runtimeRef.current = null;
152
209
  };
153
- }, [root, themeId]);
210
+ }, [root, themeId, rootRetry]);
154
211
  const setMode = react.useCallback(mode => {
155
212
  runtimeRef.current?.setMode(mode);
156
213
  }, []);
@@ -169,8 +226,11 @@ var ThemeProvider = ({
169
226
  return applyCoarseHitOverrides(result, theme.base, isCoarse);
170
227
  }, [theme, semanticTokens, isCoarse]);
171
228
  const cssContent = react.useMemo(() => {
172
- return theme ? require_css.getThemeStylesContent(theme, themeId) : null;
229
+ return theme ? require_css.getThemeStylesContent(theme, themeId, {
230
+ systemModeFallback: shouldEmitSystemFallback(initialDefaultMode)
231
+ }) : null;
173
232
  }, [theme, themeId]);
233
+ useDedupMismatchWarning(cssContent, themeId);
174
234
  const onModeChangeRef = react.useRef(onModeChange);
175
235
  react.useEffect(() => {
176
236
  onModeChangeRef.current = onModeChange;
@@ -231,11 +291,11 @@ var useColorMode = () => {
231
291
  * inline styles produces silently broken rendering:
232
292
  *
233
293
  * ```tsx
234
- * // ✗ WRONG — tokens.colors.brand.main is '{core.colors.brand.main}', not '#FF0000'
235
- * <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 }} />
236
296
  *
237
297
  * // ✓ CSS consumers — use vars:
238
- * <div style={{ color: 'var(--tt-color-brand-main)' }} />
298
+ * <div style={{ color: 'var(--tt-colors-action-primary-text-default)' }} />
239
299
  *
240
300
  * // ✓ Non-CSS consumers (React Native, canvas) — use useResolvedTokens():
241
301
  * const resolved = useResolvedTokens();
@@ -250,8 +310,8 @@ var useColorMode = () => {
250
310
  *
251
311
  * const Button = () => {
252
312
  * const tokens = useTokens(); // introspection only
253
- * // tokens.colors.action.primary.background.default → '{core.colors.brand.500}'
254
- * 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)' }} />;
255
315
  * };
256
316
  * ```
257
317
  */
@@ -276,13 +336,21 @@ var useTokens = () => {
276
336
  *
277
337
  * ```tsx
278
338
  * // ✓ CSS (browser)
279
- * <div style={{ color: 'var(--tt-color-informational-primary-default)' }} />
339
+ * <div style={{ color: 'var(--tt-colors-informational-primary-text-default)' }} />
280
340
  *
281
341
  * // ✓ Non-CSS (React Native, canvas)
282
342
  * const resolved = useResolvedTokens();
283
343
  * <View style={{ backgroundColor: resolved['semantic.colors.action.primary.background.default'] }} />
284
344
  * ```
285
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
+ *
286
354
  * Requires `<ThemeProvider theme={...}>`.
287
355
  *
288
356
  * @example
@@ -393,12 +461,15 @@ var ThemeScript = ({
393
461
  var ThemeStyles = ({
394
462
  theme,
395
463
  themeId,
396
- nonce
464
+ nonce,
465
+ systemModeFallback
397
466
  }) => {
398
467
  return /* @__PURE__ */(0, react_jsx_runtime.jsx)("style", {
399
468
  nonce,
400
469
  dangerouslySetInnerHTML: {
401
- __html: require_css.getThemeStylesContent(theme, themeId)
470
+ __html: require_css.getThemeStylesContent(theme, themeId, {
471
+ systemModeFallback
472
+ })
402
473
  }
403
474
  });
404
475
  };
@@ -461,7 +532,8 @@ var ThemeHead = ({
461
532
  }), /* @__PURE__ */(0, react_jsx_runtime.jsx)(ThemeStyles, {
462
533
  theme,
463
534
  themeId,
464
- nonce
535
+ nonce,
536
+ systemModeFallback: shouldEmitSystemFallback(defaultMode)
465
537
  })]
466
538
  });
467
539
  };
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-DLB8nrXQ.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-Dt7vGAVG.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-2D_CNVF2.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-C8JrZf-b.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-BwnHHvJj.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,7 +1,9 @@
1
1
  /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+ 'use client';
3
+
2
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-DNLlLDps.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
 
@@ -17,6 +19,57 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
17
19
  var themeStyleHref = themeId => {
18
20
  return `tt-theme-${themeId ?? "root"}`;
19
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
+ };
20
73
  var COARSE_QUERY = "(any-pointer: coarse)";
21
74
  /**
22
75
  * Subscribe to coarse-pointer media query changes.
@@ -99,20 +152,26 @@ var ThemeProvider = ({
99
152
  children
100
153
  }) => {
101
154
  const runtimeRef = React.useRef(null);
102
- const initDefaultMode = React.useRef(defaultMode);
103
- const initStorageKey = React.useRef(storageKey);
155
+ const [initialDefaultMode] = React.useState(defaultMode);
156
+ const [initialStorageKey] = React.useState(storageKey);
104
157
  const [state, setState] = React.useState(() => {
105
158
  return {
106
159
  mode: defaultMode ?? "system",
107
160
  resolvedMode: (defaultMode ?? "system") === "system" ? "light" : defaultMode
108
161
  };
109
162
  });
110
- 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.");
111
170
  const runtime = createThemeRuntime({
112
171
  defaultTheme: themeId,
113
- defaultMode: initDefaultMode.current,
114
- storageKey: initStorageKey.current,
115
- root
172
+ defaultMode: initialDefaultMode,
173
+ storageKey: initialStorageKey,
174
+ root: resolveRootElement(root)
116
175
  });
117
176
  runtimeRef.current = runtime;
118
177
  setState(runtime.getState());
@@ -122,7 +181,7 @@ var ThemeProvider = ({
122
181
  runtime.destroy();
123
182
  runtimeRef.current = null;
124
183
  };
125
- }, [root, themeId]);
184
+ }, [root, themeId, rootRetry]);
126
185
  const setMode = React.useCallback(mode => {
127
186
  runtimeRef.current?.setMode(mode);
128
187
  }, []);
@@ -141,8 +200,11 @@ var ThemeProvider = ({
141
200
  return applyCoarseHitOverrides(result, theme.base, isCoarse);
142
201
  }, [theme, semanticTokens, isCoarse]);
143
202
  const cssContent = React.useMemo(() => {
144
- return theme ? getThemeStylesContent(theme, themeId) : null;
203
+ return theme ? getThemeStylesContent(theme, themeId, {
204
+ systemModeFallback: shouldEmitSystemFallback(initialDefaultMode)
205
+ }) : null;
145
206
  }, [theme, themeId]);
207
+ useDedupMismatchWarning(cssContent, themeId);
146
208
  const onModeChangeRef = React.useRef(onModeChange);
147
209
  React.useEffect(() => {
148
210
  onModeChangeRef.current = onModeChange;
@@ -203,11 +265,11 @@ var useColorMode = () => {
203
265
  * inline styles produces silently broken rendering:
204
266
  *
205
267
  * ```tsx
206
- * // ✗ WRONG — tokens.colors.brand.main is '{core.colors.brand.main}', not '#FF0000'
207
- * <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 }} />
208
270
  *
209
271
  * // ✓ CSS consumers — use vars:
210
- * <div style={{ color: 'var(--tt-color-brand-main)' }} />
272
+ * <div style={{ color: 'var(--tt-colors-action-primary-text-default)' }} />
211
273
  *
212
274
  * // ✓ Non-CSS consumers (React Native, canvas) — use useResolvedTokens():
213
275
  * const resolved = useResolvedTokens();
@@ -222,8 +284,8 @@ var useColorMode = () => {
222
284
  *
223
285
  * const Button = () => {
224
286
  * const tokens = useTokens(); // introspection only
225
- * // tokens.colors.action.primary.background.default → '{core.colors.brand.500}'
226
- * 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)' }} />;
227
289
  * };
228
290
  * ```
229
291
  */
@@ -248,13 +310,21 @@ var useTokens = () => {
248
310
  *
249
311
  * ```tsx
250
312
  * // ✓ CSS (browser)
251
- * <div style={{ color: 'var(--tt-color-informational-primary-default)' }} />
313
+ * <div style={{ color: 'var(--tt-colors-informational-primary-text-default)' }} />
252
314
  *
253
315
  * // ✓ Non-CSS (React Native, canvas)
254
316
  * const resolved = useResolvedTokens();
255
317
  * <View style={{ backgroundColor: resolved['semantic.colors.action.primary.background.default'] }} />
256
318
  * ```
257
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
+ *
258
328
  * Requires `<ThemeProvider theme={...}>`.
259
329
  *
260
330
  * @example
@@ -365,12 +435,15 @@ var ThemeScript = ({
365
435
  var ThemeStyles = ({
366
436
  theme,
367
437
  themeId,
368
- nonce
438
+ nonce,
439
+ systemModeFallback
369
440
  }) => {
370
441
  return /* @__PURE__ */jsx("style", {
371
442
  nonce,
372
443
  dangerouslySetInnerHTML: {
373
- __html: getThemeStylesContent(theme, themeId)
444
+ __html: getThemeStylesContent(theme, themeId, {
445
+ systemModeFallback
446
+ })
374
447
  }
375
448
  });
376
449
  };
@@ -433,7 +506,8 @@ var ThemeHead = ({
433
506
  }), /* @__PURE__ */jsx(ThemeStyles, {
434
507
  theme,
435
508
  themeId,
436
- nonce
509
+ nonce,
510
+ systemModeFallback: shouldEmitSystemFallback(defaultMode)
437
511
  })]
438
512
  });
439
513
  };
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, {
3
3
  value: 'Module'
4
4
  });
5
- const require_ssrScript = require('./ssrScript-MF5SIONH.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-DNLlLDps.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 };