@ttoss/fsl-theme 2.0.0 → 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 (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-C8JrZf-b.d.mts → react-BwsjWKmo.d.mts} +46 -15
  25. package/dist/{react-DLB8nrXQ.cjs → react-DQErMGJz.cjs} +90 -18
  26. package/dist/{react-Dt7vGAVG.d.cts → react-Drr-QSuX.d.cts} +46 -15
  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 +1 -1
@@ -18,7 +18,7 @@ var resolveTheme = cfg => {
18
18
  const mode = stored != null && stored.mode != null && cfg.validModes.indexOf(stored.mode) !== -1 ? stored.mode : cfg.defaultMode;
19
19
  return {
20
20
  mode,
21
- resolvedMode: mode === "system" ? typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : mode
21
+ resolvedMode: mode === "system" ? typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : mode
22
22
  };
23
23
  };
24
24
 
@@ -34,6 +34,20 @@ var writeStorage = (key, data) => {
34
34
  localStorage.setItem(key, JSON.stringify(data));
35
35
  } catch {}
36
36
  };
37
+ var createMediaQuery = () => {
38
+ if (typeof window.matchMedia === "function") return window.matchMedia("(prefers-color-scheme: dark)");
39
+ return {
40
+ matches: false,
41
+ addEventListener: () => {},
42
+ removeEventListener: () => {}
43
+ };
44
+ };
45
+ /**
46
+ * DEV-only registry of live runtimes per root element. Two runtimes writing
47
+ * `data-tt-*` to the same root overwrite each other (last writer wins), so
48
+ * the second attach warns. Entries are decremented on `destroy()`.
49
+ */
50
+ var activeRuntimesPerRoot = /* @__PURE__ */new WeakMap();
37
51
  /**
38
52
  * Creates a framework-agnostic runtime that manages theme switching.
39
53
  *
@@ -60,6 +74,11 @@ var createThemeRuntime = (config = {}) => {
60
74
  } = config;
61
75
  const listeners = /* @__PURE__ */new Set();
62
76
  let destroyed = false;
77
+ if (process.env.NODE_ENV !== "production") {
78
+ const count = (activeRuntimesPerRoot.get(root) ?? 0) + 1;
79
+ activeRuntimesPerRoot.set(root, count);
80
+ if (count > 1) console.warn("[fsl-theme] Multiple theme runtimes are attached to the same root element. They overwrite each other's data-tt-* attributes (last writer wins) and their mode states do not sync. Render a single <ThemeProvider> per root, or pass a distinct `root` to each.");
81
+ }
63
82
  const init = resolveTheme({
64
83
  storageKey,
65
84
  defaultMode,
@@ -94,7 +113,7 @@ var createThemeRuntime = (config = {}) => {
94
113
  const state = getState();
95
114
  for (const listener of listeners) listener(state);
96
115
  };
97
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
116
+ const mediaQuery = createMediaQuery();
98
117
  const onSystemChange = () => {
99
118
  if (destroyed) return;
100
119
  resolvedMode = mediaQuery.matches ? "dark" : "light";
@@ -107,6 +126,30 @@ var createThemeRuntime = (config = {}) => {
107
126
  if (active) mediaQuery.addEventListener("change", onSystemChange);
108
127
  };
109
128
  syncMediaListener(mode === "system");
129
+ /** Parse + validate a persisted `{ mode }` payload; null when unusable. */
130
+ const parseStoredMode = raw => {
131
+ let stored = null;
132
+ try {
133
+ stored = JSON.parse(raw);
134
+ } catch {
135
+ return null;
136
+ }
137
+ const nextMode = stored?.mode;
138
+ return nextMode && VALID_MODES.includes(nextMode) ? nextMode : null;
139
+ };
140
+ const onStorage = event => {
141
+ if (destroyed || event.key !== storageKey || event.newValue === null) return;
142
+ if (event.storageArea != null && event.storageArea !== localStorage) return;
143
+ const nextMode = parseStoredMode(event.newValue);
144
+ if (!nextMode) return;
145
+ mode = nextMode;
146
+ resolvedMode = mode === "system" ? mediaQuery.matches ? "dark" : "light" : mode;
147
+ syncMediaListener(mode === "system");
148
+ applyState({
149
+ persist: false
150
+ });
151
+ };
152
+ window.addEventListener("storage", onStorage);
110
153
  const setMode = newMode => {
111
154
  if (destroyed || !VALID_MODES.includes(newMode)) return;
112
155
  mode = newMode;
@@ -126,7 +169,12 @@ var createThemeRuntime = (config = {}) => {
126
169
  const destroy = () => {
127
170
  destroyed = true;
128
171
  syncMediaListener(false);
172
+ window.removeEventListener("storage", onStorage);
129
173
  listeners.clear();
174
+ if (process.env.NODE_ENV !== "production") {
175
+ const count = activeRuntimesPerRoot.get(root) ?? 0;
176
+ activeRuntimesPerRoot.set(root, Math.max(0, count - 1));
177
+ }
130
178
  };
131
179
  return {
132
180
  getState,
@@ -173,7 +221,7 @@ var getThemeScriptContent = (config = {}) => {
173
221
  defaultTheme,
174
222
  defaultMode,
175
223
  validModes: [...VALID_MODES]
176
- })};var d=document.documentElement;var s=null;try{var r=localStorage.getItem(cfg.storageKey);if(r)s=JSON.parse(r);}catch(e){}var m=(s!=null&&s.mode!=null&&cfg.validModes.indexOf(s.mode)!==-1)?s.mode:cfg.defaultMode;var rm=m==="system"?(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"):m;if(cfg.defaultTheme){d.setAttribute("data-tt-theme",cfg.defaultTheme);}else{d.removeAttribute("data-tt-theme");}d.setAttribute("data-tt-mode",rm);d.style.colorScheme=rm;})()`;
224
+ }).replace(/</g, "\\u003C")};var d=document.documentElement;var s=null;try{var r=localStorage.getItem(cfg.storageKey);if(r)s=JSON.parse(r);}catch(e){}var m=(s!=null&&s.mode!=null&&cfg.validModes.indexOf(s.mode)!==-1)?s.mode:cfg.defaultMode;var rm=m==="system"?(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"):m;if(cfg.defaultTheme){d.setAttribute("data-tt-theme",cfg.defaultTheme);}else{d.removeAttribute("data-tt-theme");}d.setAttribute("data-tt-mode",rm);d.style.colorScheme=rm;})()`;
177
225
  };
178
226
 
179
227
  //#endregion
@@ -18,7 +18,7 @@ var resolveTheme = cfg => {
18
18
  const mode = stored != null && stored.mode != null && cfg.validModes.indexOf(stored.mode) !== -1 ? stored.mode : cfg.defaultMode;
19
19
  return {
20
20
  mode,
21
- resolvedMode: mode === "system" ? typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : mode
21
+ resolvedMode: mode === "system" ? typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : mode
22
22
  };
23
23
  };
24
24
 
@@ -34,6 +34,20 @@ var writeStorage = (key, data) => {
34
34
  localStorage.setItem(key, JSON.stringify(data));
35
35
  } catch {}
36
36
  };
37
+ var createMediaQuery = () => {
38
+ if (typeof window.matchMedia === "function") return window.matchMedia("(prefers-color-scheme: dark)");
39
+ return {
40
+ matches: false,
41
+ addEventListener: () => {},
42
+ removeEventListener: () => {}
43
+ };
44
+ };
45
+ /**
46
+ * DEV-only registry of live runtimes per root element. Two runtimes writing
47
+ * `data-tt-*` to the same root overwrite each other (last writer wins), so
48
+ * the second attach warns. Entries are decremented on `destroy()`.
49
+ */
50
+ var activeRuntimesPerRoot = /* @__PURE__ */new WeakMap();
37
51
  /**
38
52
  * Creates a framework-agnostic runtime that manages theme switching.
39
53
  *
@@ -60,6 +74,11 @@ var createThemeRuntime = (config = {}) => {
60
74
  } = config;
61
75
  const listeners = /* @__PURE__ */new Set();
62
76
  let destroyed = false;
77
+ if (process.env.NODE_ENV !== "production") {
78
+ const count = (activeRuntimesPerRoot.get(root) ?? 0) + 1;
79
+ activeRuntimesPerRoot.set(root, count);
80
+ if (count > 1) console.warn("[fsl-theme] Multiple theme runtimes are attached to the same root element. They overwrite each other's data-tt-* attributes (last writer wins) and their mode states do not sync. Render a single <ThemeProvider> per root, or pass a distinct `root` to each.");
81
+ }
63
82
  const init = resolveTheme({
64
83
  storageKey,
65
84
  defaultMode,
@@ -94,7 +113,7 @@ var createThemeRuntime = (config = {}) => {
94
113
  const state = getState();
95
114
  for (const listener of listeners) listener(state);
96
115
  };
97
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
116
+ const mediaQuery = createMediaQuery();
98
117
  const onSystemChange = () => {
99
118
  if (destroyed) return;
100
119
  resolvedMode = mediaQuery.matches ? "dark" : "light";
@@ -107,6 +126,30 @@ var createThemeRuntime = (config = {}) => {
107
126
  if (active) mediaQuery.addEventListener("change", onSystemChange);
108
127
  };
109
128
  syncMediaListener(mode === "system");
129
+ /** Parse + validate a persisted `{ mode }` payload; null when unusable. */
130
+ const parseStoredMode = raw => {
131
+ let stored = null;
132
+ try {
133
+ stored = JSON.parse(raw);
134
+ } catch {
135
+ return null;
136
+ }
137
+ const nextMode = stored?.mode;
138
+ return nextMode && VALID_MODES.includes(nextMode) ? nextMode : null;
139
+ };
140
+ const onStorage = event => {
141
+ if (destroyed || event.key !== storageKey || event.newValue === null) return;
142
+ if (event.storageArea != null && event.storageArea !== localStorage) return;
143
+ const nextMode = parseStoredMode(event.newValue);
144
+ if (!nextMode) return;
145
+ mode = nextMode;
146
+ resolvedMode = mode === "system" ? mediaQuery.matches ? "dark" : "light" : mode;
147
+ syncMediaListener(mode === "system");
148
+ applyState({
149
+ persist: false
150
+ });
151
+ };
152
+ window.addEventListener("storage", onStorage);
110
153
  const setMode = newMode => {
111
154
  if (destroyed || !VALID_MODES.includes(newMode)) return;
112
155
  mode = newMode;
@@ -126,7 +169,12 @@ var createThemeRuntime = (config = {}) => {
126
169
  const destroy = () => {
127
170
  destroyed = true;
128
171
  syncMediaListener(false);
172
+ window.removeEventListener("storage", onStorage);
129
173
  listeners.clear();
174
+ if (process.env.NODE_ENV !== "production") {
175
+ const count = activeRuntimesPerRoot.get(root) ?? 0;
176
+ activeRuntimesPerRoot.set(root, Math.max(0, count - 1));
177
+ }
130
178
  };
131
179
  return {
132
180
  getState,
@@ -173,7 +221,7 @@ var getThemeScriptContent = (config = {}) => {
173
221
  defaultTheme,
174
222
  defaultMode,
175
223
  validModes: [...VALID_MODES]
176
- })};var d=document.documentElement;var s=null;try{var r=localStorage.getItem(cfg.storageKey);if(r)s=JSON.parse(r);}catch(e){}var m=(s!=null&&s.mode!=null&&cfg.validModes.indexOf(s.mode)!==-1)?s.mode:cfg.defaultMode;var rm=m==="system"?(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"):m;if(cfg.defaultTheme){d.setAttribute("data-tt-theme",cfg.defaultTheme);}else{d.removeAttribute("data-tt-theme");}d.setAttribute("data-tt-mode",rm);d.style.colorScheme=rm;})()`;
224
+ }).replace(/</g, "\\u003C")};var d=document.documentElement;var s=null;try{var r=localStorage.getItem(cfg.storageKey);if(r)s=JSON.parse(r);}catch(e){}var m=(s!=null&&s.mode!=null&&cfg.validModes.indexOf(s.mode)!==-1)?s.mode:cfg.defaultMode;var rm=m==="system"?(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"):m;if(cfg.defaultTheme){d.setAttribute("data-tt-theme",cfg.defaultTheme);}else{d.removeAttribute("data-tt-theme");}d.setAttribute("data-tt-mode",rm);d.style.colorScheme=rm;})()`;
177
225
  };
178
226
 
179
227
  //#endregion
@@ -2,9 +2,9 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, {
3
3
  value: 'Module'
4
4
  });
5
- const require_createTheme = require('../createTheme--UFgVlEn.cjs');
5
+ const require_createTheme = require('../createTheme-B7H0xSVp.cjs');
6
6
  const require_helpers = require('../helpers-BU40JvFA.cjs');
7
- const require_withDataviz = require('../withDataviz-CYRf4exT.cjs');
7
+ const require_withDataviz = require('../withDataviz-DrUtw8cc.cjs');
8
8
 
9
9
  //#region src/themes/bruttal.ts
10
10
  /**
@@ -1,3 +1,3 @@
1
1
 
2
- import { t as bruttal } from "../index-7YL90Xqs.cjs";
2
+ import { t as bruttal } from "../index-BmZQVd3G.cjs";
3
3
  export { bruttal };
@@ -1,3 +1,3 @@
1
1
 
2
- import { t as bruttal } from "../index-BmCTIOzh.mjs";
2
+ import { t as bruttal } from "../index-XZsRrMTF.mjs";
3
3
  export { bruttal };
@@ -1,7 +1,7 @@
1
1
  /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
- import { a as darkAlternate, n as createTheme } from "../createTheme-B6tVIikC.mjs";
2
+ import { a as darkAlternate, n as createTheme } from "../createTheme-Bm9LVQ4w.mjs";
3
3
  import { r as deepMerge } from "../helpers-BXaKe2s3.mjs";
4
- import { t as withDataviz } from "../withDataviz-BvpZJnFx.mjs";
4
+ import { t as withDataviz } from "../withDataviz-Ln3sEwb7.mjs";
5
5
 
6
6
  //#region src/themes/bruttal.ts
7
7
  /**
@@ -11,7 +11,7 @@ import { t as CSS_PATH_PREFIXES } from "./tokenRegistry-DviyqDeE.mjs";
11
11
  */
12
12
  var toCssVarName = tokenPath => {
13
13
  for (const [prefix, cssPrefix] of CSS_PATH_PREFIXES) if (tokenPath.startsWith(prefix)) return `${cssPrefix}${tokenPath.slice(prefix.length).replace(/\./g, "-")}`;
14
- return `--tt-${tokenPath.replace(/\./g, "-")}`;
14
+ return `--tt-${(tokenPath.startsWith("semantic.") ? tokenPath.slice(9) : tokenPath).replace(/\./g, "-")}`;
15
15
  };
16
16
  /**
17
17
  * Replace all embedded `{token.path}` refs inside a raw string with `var(--tt-...)` references.
@@ -212,6 +212,34 @@ var isThemeBundle = input => {
212
212
  return "baseMode" in input && "base" in input;
213
213
  };
214
214
  /**
215
+ * Build the no-JS / pre-hydration system-mode fallback block.
216
+ *
217
+ * Canonical 1-theme model only (no `themeId`): before the runtime (or the SSR
218
+ * bootstrap script) stamps `data-tt-mode`, and for users with JavaScript
219
+ * disabled, the alternate mode would otherwise never apply. This block scopes
220
+ * the alternate diff vars to `@media (prefers-color-scheme: <mode>)` behind
221
+ * `:root:not([data-tt-mode])`, so it applies exactly while no explicit mode
222
+ * attribute exists and gets out of the way the moment the runtime writes one
223
+ * (persisted user choice always wins over the OS preference).
224
+ */
225
+ var buildSystemModeFallbackBlock = ({
226
+ mode,
227
+ vars
228
+ }) => {
229
+ const entries = Object.entries(vars);
230
+ const selector = ":root:not([data-tt-mode])";
231
+ const lines = entries.map(([name, value]) => {
232
+ return ` ${name}: ${hasCqUnits(value) ? toViewportFallback(String(value)) : value};`;
233
+ });
234
+ const cqEntries = entries.filter(([, value]) => {
235
+ return hasCqUnits(value);
236
+ });
237
+ const cqBlock = cqEntries.length > 0 ? `\n\n @supports (width: 1cqi) {\n ${selector} {\n${cqEntries.map(([name, value]) => {
238
+ return ` ${name}: ${value};`;
239
+ }).join("\n")}\n }\n }` : "";
240
+ return `@media (prefers-color-scheme: ${mode}) {\n ${selector} {\n color-scheme: ${mode};\n${lines.join("\n")}\n }${cqBlock}\n}`;
241
+ };
242
+ /**
215
243
  * Compute a diff record: only entries in `full` whose values differ from `base`.
216
244
  */
217
245
  var diffCssVars = ({
@@ -238,7 +266,8 @@ var diffCssVars = ({
238
266
  */
239
267
  var bundleToCssVars = (bundle, options) => {
240
268
  const {
241
- themeId
269
+ themeId,
270
+ systemModeFallback = true
242
271
  } = options;
243
272
  const {
244
273
  baseMode,
@@ -297,7 +326,13 @@ var bundleToCssVars = (bundle, options) => {
297
326
  alternate: alternateResult,
298
327
  toCssString: () => {
299
328
  const parts = [baseResult.toCssString()];
300
- if (Object.keys(diffVars).length > 0) parts.push(alternateResult.toCssString());
329
+ if (Object.keys(diffVars).length > 0) {
330
+ parts.push(alternateResult.toCssString());
331
+ if (!themeId && systemModeFallback) parts.push(buildSystemModeFallbackBlock({
332
+ mode: alternateMode,
333
+ vars: diffVars
334
+ }));
335
+ }
301
336
  return parts.join("\n\n");
302
337
  }
303
338
  };
@@ -11,7 +11,7 @@ const require_tokenRegistry = require('./tokenRegistry-f_Bx_BAN.cjs');
11
11
  */
12
12
  var toCssVarName = tokenPath => {
13
13
  for (const [prefix, cssPrefix] of require_tokenRegistry.CSS_PATH_PREFIXES) if (tokenPath.startsWith(prefix)) return `${cssPrefix}${tokenPath.slice(prefix.length).replace(/\./g, "-")}`;
14
- return `--tt-${tokenPath.replace(/\./g, "-")}`;
14
+ return `--tt-${(tokenPath.startsWith("semantic.") ? tokenPath.slice(9) : tokenPath).replace(/\./g, "-")}`;
15
15
  };
16
16
  /**
17
17
  * Replace all embedded `{token.path}` refs inside a raw string with `var(--tt-...)` references.
@@ -212,6 +212,34 @@ var isThemeBundle = input => {
212
212
  return "baseMode" in input && "base" in input;
213
213
  };
214
214
  /**
215
+ * Build the no-JS / pre-hydration system-mode fallback block.
216
+ *
217
+ * Canonical 1-theme model only (no `themeId`): before the runtime (or the SSR
218
+ * bootstrap script) stamps `data-tt-mode`, and for users with JavaScript
219
+ * disabled, the alternate mode would otherwise never apply. This block scopes
220
+ * the alternate diff vars to `@media (prefers-color-scheme: <mode>)` behind
221
+ * `:root:not([data-tt-mode])`, so it applies exactly while no explicit mode
222
+ * attribute exists and gets out of the way the moment the runtime writes one
223
+ * (persisted user choice always wins over the OS preference).
224
+ */
225
+ var buildSystemModeFallbackBlock = ({
226
+ mode,
227
+ vars
228
+ }) => {
229
+ const entries = Object.entries(vars);
230
+ const selector = ":root:not([data-tt-mode])";
231
+ const lines = entries.map(([name, value]) => {
232
+ return ` ${name}: ${hasCqUnits(value) ? toViewportFallback(String(value)) : value};`;
233
+ });
234
+ const cqEntries = entries.filter(([, value]) => {
235
+ return hasCqUnits(value);
236
+ });
237
+ const cqBlock = cqEntries.length > 0 ? `\n\n @supports (width: 1cqi) {\n ${selector} {\n${cqEntries.map(([name, value]) => {
238
+ return ` ${name}: ${value};`;
239
+ }).join("\n")}\n }\n }` : "";
240
+ return `@media (prefers-color-scheme: ${mode}) {\n ${selector} {\n color-scheme: ${mode};\n${lines.join("\n")}\n }${cqBlock}\n}`;
241
+ };
242
+ /**
215
243
  * Compute a diff record: only entries in `full` whose values differ from `base`.
216
244
  */
217
245
  var diffCssVars = ({
@@ -238,7 +266,8 @@ var diffCssVars = ({
238
266
  */
239
267
  var bundleToCssVars = (bundle, options) => {
240
268
  const {
241
- themeId
269
+ themeId,
270
+ systemModeFallback = true
242
271
  } = options;
243
272
  const {
244
273
  baseMode,
@@ -297,7 +326,13 @@ var bundleToCssVars = (bundle, options) => {
297
326
  alternate: alternateResult,
298
327
  toCssString: () => {
299
328
  const parts = [baseResult.toCssString()];
300
- if (Object.keys(diffVars).length > 0) parts.push(alternateResult.toCssString());
329
+ if (Object.keys(diffVars).length > 0) {
330
+ parts.push(alternateResult.toCssString());
331
+ if (!themeId && systemModeFallback) parts.push(buildSystemModeFallbackBlock({
332
+ mode: alternateMode,
333
+ vars: diffVars
334
+ }));
335
+ }
301
336
  return parts.join("\n\n");
302
337
  }
303
338
  };
package/dist/vars.cjs CHANGED
@@ -3,8 +3,8 @@ Object.defineProperty(exports, Symbol.toStringTag, {
3
3
  value: 'Module'
4
4
  });
5
5
  const require_helpers = require('./helpers-BU40JvFA.cjs');
6
- const require_baseBundle = require('./baseBundle-DdRSa5mf.cjs');
7
- const require_toCssVars = require('./toCssVars-BseqmMcT.cjs');
6
+ const require_baseBundle = require('./baseBundle-a9LDsqYy.cjs');
7
+ const require_toCssVars = require('./toCssVars-D0BR23cP.cjs');
8
8
 
9
9
  //#region src/roots/toVars.ts
10
10
  /**
@@ -23,7 +23,7 @@ const require_toCssVars = require('./toCssVars-BseqmMcT.cjs');
23
23
  *
24
24
  * const vars = buildVarsMap(baseTheme);
25
25
  * vars.colors.action.primary.background.default
26
- * // → 'var(--tt-action-primary-background-default)'
26
+ * // → 'var(--tt-colors-action-primary-background-default)'
27
27
  * ```
28
28
  */
29
29
  var buildVarsMap = theme => {
@@ -57,15 +57,15 @@ var buildVarsMap = theme => {
57
57
  * import { vars } from '@ttoss/fsl-theme/vars';
58
58
  *
59
59
  * <Button style={{ background: vars.colors.action.primary.background.default }} />
60
- * // → background: 'var(--tt-action-primary-background-default)'
60
+ * // → background: 'var(--tt-colors-action-primary-background-default)'
61
61
  * ```
62
62
  *
63
63
  * ## Usage in CSS / CSS Modules
64
64
  *
65
65
  * ```css
66
66
  * .button {
67
- * background: var(--tt-action-primary-background-default);
68
- * color: var(--tt-action-primary-text-default);
67
+ * background: var(--tt-colors-action-primary-background-default);
68
+ * color: var(--tt-colors-action-primary-text-default);
69
69
  * }
70
70
  * ```
71
71
  *
@@ -110,7 +110,7 @@ var buildVarsMap = theme => {
110
110
  * buildVarsMap(myTheme.base) as CssVarsMap<MySemanticTokens>;
111
111
  *
112
112
  * myVars.colors.brandX.primary.default;
113
- * // → 'var(--tt-colors-brandx-primary-default)'
113
+ * // → 'var(--tt-colors-brandX-primary-default)' (camelCase is preserved)
114
114
  * ```
115
115
  *
116
116
  * For one-off custom keys where the extra structure is trivial, use
package/dist/vars.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- import { a as ThemeTokens, n as SemanticTokens } from "./Types-BkaxtsMv.cjs";
2
+ import { a as ThemeTokens, n as SemanticTokens } from "./Types-a_sfpbha.cjs";
3
3
 
4
4
  //#region src/roots/toVars.d.ts
5
5
  /**
@@ -38,7 +38,7 @@ type CssVarsMap<T> = { [K in keyof T as K extends `$${string}` ? never : K]: Non
38
38
  *
39
39
  * const vars = buildVarsMap(baseTheme);
40
40
  * vars.colors.action.primary.background.default
41
- * // → 'var(--tt-action-primary-background-default)'
41
+ * // → 'var(--tt-colors-action-primary-background-default)'
42
42
  * ```
43
43
  */
44
44
  declare const buildVarsMap: (theme: ThemeTokens) => CssVarsMap<ThemeTokens["semantic"]>;
@@ -60,15 +60,15 @@ declare const buildVarsMap: (theme: ThemeTokens) => CssVarsMap<ThemeTokens["sema
60
60
  * import { vars } from '@ttoss/fsl-theme/vars';
61
61
  *
62
62
  * <Button style={{ background: vars.colors.action.primary.background.default }} />
63
- * // → background: 'var(--tt-action-primary-background-default)'
63
+ * // → background: 'var(--tt-colors-action-primary-background-default)'
64
64
  * ```
65
65
  *
66
66
  * ## Usage in CSS / CSS Modules
67
67
  *
68
68
  * ```css
69
69
  * .button {
70
- * background: var(--tt-action-primary-background-default);
71
- * color: var(--tt-action-primary-text-default);
70
+ * background: var(--tt-colors-action-primary-background-default);
71
+ * color: var(--tt-colors-action-primary-text-default);
72
72
  * }
73
73
  * ```
74
74
  *
@@ -113,7 +113,7 @@ declare const buildVarsMap: (theme: ThemeTokens) => CssVarsMap<ThemeTokens["sema
113
113
  * buildVarsMap(myTheme.base) as CssVarsMap<MySemanticTokens>;
114
114
  *
115
115
  * myVars.colors.brandX.primary.default;
116
- * // → 'var(--tt-colors-brandx-primary-default)'
116
+ * // → 'var(--tt-colors-brandX-primary-default)' (camelCase is preserved)
117
117
  * ```
118
118
  *
119
119
  * For one-off custom keys where the extra structure is trivial, use
package/dist/vars.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- import { a as ThemeTokens, n as SemanticTokens } from "./Types-BkaxtsMv.mjs";
2
+ import { a as ThemeTokens, n as SemanticTokens } from "./Types-a_sfpbha.mjs";
3
3
 
4
4
  //#region src/roots/toVars.d.ts
5
5
  /**
@@ -38,7 +38,7 @@ type CssVarsMap<T> = { [K in keyof T as K extends `$${string}` ? never : K]: Non
38
38
  *
39
39
  * const vars = buildVarsMap(baseTheme);
40
40
  * vars.colors.action.primary.background.default
41
- * // → 'var(--tt-action-primary-background-default)'
41
+ * // → 'var(--tt-colors-action-primary-background-default)'
42
42
  * ```
43
43
  */
44
44
  declare const buildVarsMap: (theme: ThemeTokens) => CssVarsMap<ThemeTokens["semantic"]>;
@@ -60,15 +60,15 @@ declare const buildVarsMap: (theme: ThemeTokens) => CssVarsMap<ThemeTokens["sema
60
60
  * import { vars } from '@ttoss/fsl-theme/vars';
61
61
  *
62
62
  * <Button style={{ background: vars.colors.action.primary.background.default }} />
63
- * // → background: 'var(--tt-action-primary-background-default)'
63
+ * // → background: 'var(--tt-colors-action-primary-background-default)'
64
64
  * ```
65
65
  *
66
66
  * ## Usage in CSS / CSS Modules
67
67
  *
68
68
  * ```css
69
69
  * .button {
70
- * background: var(--tt-action-primary-background-default);
71
- * color: var(--tt-action-primary-text-default);
70
+ * background: var(--tt-colors-action-primary-background-default);
71
+ * color: var(--tt-colors-action-primary-text-default);
72
72
  * }
73
73
  * ```
74
74
  *
@@ -113,7 +113,7 @@ declare const buildVarsMap: (theme: ThemeTokens) => CssVarsMap<ThemeTokens["sema
113
113
  * buildVarsMap(myTheme.base) as CssVarsMap<MySemanticTokens>;
114
114
  *
115
115
  * myVars.colors.brandX.primary.default;
116
- * // → 'var(--tt-colors-brandx-primary-default)'
116
+ * // → 'var(--tt-colors-brandX-primary-default)' (camelCase is preserved)
117
117
  * ```
118
118
  *
119
119
  * For one-off custom keys where the extra structure is trivial, use
package/dist/vars.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
2
  import { o as isPlainObject } from "./helpers-BXaKe2s3.mjs";
3
- import { t as baseBundle } from "./baseBundle-R7WyjyTw.mjs";
4
- import { t as toCssVarName } from "./toCssVars-C-9yIZDR.mjs";
3
+ import { t as baseBundle } from "./baseBundle-CXdxj_re.mjs";
4
+ import { t as toCssVarName } from "./toCssVars-CWTlp9St.mjs";
5
5
 
6
6
  //#region src/roots/toVars.ts
7
7
  /**
@@ -20,7 +20,7 @@ import { t as toCssVarName } from "./toCssVars-C-9yIZDR.mjs";
20
20
  *
21
21
  * const vars = buildVarsMap(baseTheme);
22
22
  * vars.colors.action.primary.background.default
23
- * // → 'var(--tt-action-primary-background-default)'
23
+ * // → 'var(--tt-colors-action-primary-background-default)'
24
24
  * ```
25
25
  */
26
26
  var buildVarsMap = theme => {
@@ -54,15 +54,15 @@ var buildVarsMap = theme => {
54
54
  * import { vars } from '@ttoss/fsl-theme/vars';
55
55
  *
56
56
  * <Button style={{ background: vars.colors.action.primary.background.default }} />
57
- * // → background: 'var(--tt-action-primary-background-default)'
57
+ * // → background: 'var(--tt-colors-action-primary-background-default)'
58
58
  * ```
59
59
  *
60
60
  * ## Usage in CSS / CSS Modules
61
61
  *
62
62
  * ```css
63
63
  * .button {
64
- * background: var(--tt-action-primary-background-default);
65
- * color: var(--tt-action-primary-text-default);
64
+ * background: var(--tt-colors-action-primary-background-default);
65
+ * color: var(--tt-colors-action-primary-text-default);
66
66
  * }
67
67
  * ```
68
68
  *
@@ -107,7 +107,7 @@ var buildVarsMap = theme => {
107
107
  * buildVarsMap(myTheme.base) as CssVarsMap<MySemanticTokens>;
108
108
  *
109
109
  * myVars.colors.brandX.primary.default;
110
- * // → 'var(--tt-colors-brandx-primary-default)'
110
+ * // → 'var(--tt-colors-brandX-primary-default)' (camelCase is preserved)
111
111
  * ```
112
112
  *
113
113
  * For one-off custom keys where the extra structure is trivial, use
@@ -1,5 +1,5 @@
1
1
  /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
- const require_createTheme = require('./createTheme--UFgVlEn.cjs');
2
+ const require_createTheme = require('./createTheme-B7H0xSVp.cjs');
3
3
 
4
4
  //#region src/dataviz/baseTheme.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
- import { t as buildTheme } from "./createTheme-B6tVIikC.mjs";
2
+ import { t as buildTheme } from "./createTheme-Bm9LVQ4w.mjs";
3
3
 
4
4
  //#region src/dataviz/baseTheme.ts
5
5
  /**
package/llms.txt CHANGED
@@ -289,7 +289,8 @@ Not every state is legal in every `ux` context. Illegal combinations are TypeScr
289
289
  | `indeterminate` | — | ✅ | — | — | — |
290
290
  | `current` | — | — | ✅ (active route) | — | — |
291
291
  | `visited` | — | — | ✅ | — | ✅ (link in content) |
292
- | `droptarget` | | ✅ (file input) | | — | ✅ |
292
+ | `droptarget` | | ✅ (file input) | | — | ✅ |
293
+ | `invalid` | — | ✅ (runtime validation outcome — `isInvalid`, never authorial; pairs with `input.negative.*` on the validationMessage) | — | — | — |
293
294
 
294
295
  State disambiguation rules:
295
296
  - **`active` vs `pressed`**: `active` = the brief moment of clicking (transient). `pressed` = persistent toggle engaged ("Bold" button). Never use `active` for toggle state.
@@ -575,7 +576,7 @@ const DarkModeToggle = () => {
575
576
  ```tsx
576
577
  import { useResolvedTokens } from '@ttoss/fsl-theme/react';
577
578
 
578
- // Keys are full semantic dot-paths; values are resolved raw CSS values (hex, px, etc.)
579
+ // Keys are full semantic dot-paths; values are resolved raw CSS values (hex, numbers, etc.)
579
580
  const NativeButton = ({ disabled }: { disabled?: boolean }) => {
580
581
  const resolved = useResolvedTokens();
581
582
  return (
@@ -584,8 +585,7 @@ const NativeButton = ({ disabled }: { disabled?: boolean }) => {
584
585
  backgroundColor: disabled
585
586
  ? resolved['semantic.colors.action.primary.background.disabled']
586
587
  : resolved['semantic.colors.action.primary.background.default'],
587
- minHeight: resolved['semantic.sizing.hit.base'],
588
- paddingHorizontal: resolved['semantic.spacing.inset.control.md'],
588
+ opacity: disabled ? Number(resolved['semantic.opacity.disabled']) : 1,
589
589
  }}
590
590
  />
591
591
  );
@@ -594,6 +594,8 @@ const NativeButton = ({ disabled }: { disabled?: boolean }) => {
594
594
 
595
595
  `useResolvedTokens()` automatically applies coarse-pointer hit target overrides — mirrors the `@media (any-pointer: coarse)` block that `toCssVars` injects for CSS consumers.
596
596
 
597
+ ⚠ **CSS-coupled tokens stay unresolved** (model.md §8): spacing steps (`calc(N * var(--tt-core-spacing-engine-unit))`), fluid `text.*.fontSize` / `sizing.hit.*` (`clamp()` + `cqi`), `sizing.viewport.*` (`dvh`/`dvw`), `sizing.measure.reading` (`ch`), and `spacing.gutter.*` are CSS expressions and are **not usable** outside a CSS engine. In non-CSS environments, consume colors, opacity, z-index, font weights/leading, and other scalar tokens — derive dimensional values from your platform's layout system.
598
+
597
599
  ---
598
600
 
599
601
  # JOB 2 — Customizing a theme
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/fsl-theme",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "ttoss theme provider for UI.",
5
5
  "keywords": [
6
6
  "css-variables",