@astryxdesign/core 0.4.2 → 0.4.3-canary.ac850d9

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 (85) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +79 -45
  3. package/dist/Avatar/Avatar.d.ts.map +1 -1
  4. package/dist/Avatar/Avatar.js +3 -19
  5. package/dist/Banner/Banner.d.ts +6 -2
  6. package/dist/Banner/Banner.d.ts.map +1 -1
  7. package/dist/Banner/Banner.js +71 -30
  8. package/dist/Banner/index.d.ts +14 -12
  9. package/dist/Banner/index.d.ts.map +1 -1
  10. package/dist/Banner/index.js +10 -8
  11. package/dist/Chat/ChatTokenizedText.js +1 -1
  12. package/dist/ComplexSelector/ComplexSelector.d.ts +38 -4
  13. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  14. package/dist/ComplexSelector/ComplexSelector.js +96 -35
  15. package/dist/ComplexSelector/index.d.ts +2 -2
  16. package/dist/ComplexSelector/index.d.ts.map +1 -1
  17. package/dist/ComplexSelector/index.js +1 -1
  18. package/dist/Markdown/parser.d.ts.map +1 -1
  19. package/dist/Markdown/parser.js +55 -12
  20. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  21. package/dist/PowerSearch/PowerSearch.js +4 -1
  22. package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
  23. package/dist/PowerSearch/formatFilterValue.js +2 -4
  24. package/dist/Table/columnUtils.d.ts.map +1 -1
  25. package/dist/Table/columnUtils.js +4 -1
  26. package/dist/TextArea/TextArea.d.ts +6 -3
  27. package/dist/TextArea/TextArea.d.ts.map +1 -1
  28. package/dist/TextArea/TextArea.js +17 -6
  29. package/dist/TreeList/TreeList.js +2 -1
  30. package/dist/astryx.css +6 -4
  31. package/dist/theme/defineTheme.d.ts +30 -7
  32. package/dist/theme/defineTheme.d.ts.map +1 -1
  33. package/dist/theme/defineTheme.js +36 -47
  34. package/dist/theme/expandColorScale.d.ts +17 -2
  35. package/dist/theme/expandColorScale.d.ts.map +1 -1
  36. package/dist/theme/expandColorScale.js +63 -33
  37. package/dist/theme/mergeComponents.d.ts +20 -0
  38. package/dist/theme/mergeComponents.d.ts.map +1 -0
  39. package/dist/theme/mergeComponents.js +56 -0
  40. package/dist/theme/onMediaTokens.d.ts +6 -1
  41. package/dist/theme/onMediaTokens.d.ts.map +1 -1
  42. package/dist/theme/onMediaTokens.js +11 -3
  43. package/dist/utils/characters.d.ts +27 -0
  44. package/dist/utils/characters.d.ts.map +1 -0
  45. package/dist/utils/characters.js +83 -0
  46. package/dist/utils/index.d.ts +1 -0
  47. package/dist/utils/index.d.ts.map +1 -1
  48. package/dist/utils/index.js +1 -0
  49. package/package.json +4 -8
  50. package/src/Avatar/Avatar.test.tsx +6 -1
  51. package/src/Avatar/Avatar.tsx +3 -21
  52. package/src/Banner/Banner.doc.mjs +9 -7
  53. package/src/Banner/Banner.test.tsx +68 -0
  54. package/src/Banner/Banner.tsx +97 -36
  55. package/src/Banner/index.ts +15 -13
  56. package/src/Chat/ChatTokenizedText.tsx +1 -1
  57. package/src/ComplexSelector/ComplexSelector.doc.mjs +55 -6
  58. package/src/ComplexSelector/ComplexSelector.test.tsx +197 -6
  59. package/src/ComplexSelector/ComplexSelector.tsx +153 -32
  60. package/src/ComplexSelector/index.ts +3 -1
  61. package/src/Markdown/parser.test.ts +53 -0
  62. package/src/Markdown/parser.ts +53 -12
  63. package/src/PowerSearch/PowerSearch.test.tsx +48 -3
  64. package/src/PowerSearch/PowerSearch.tsx +4 -1
  65. package/src/PowerSearch/formatFilterValue.test.ts +22 -0
  66. package/src/PowerSearch/formatFilterValue.ts +2 -4
  67. package/src/Table/Table.test.tsx +6 -0
  68. package/src/Table/columnUtils.ts +3 -1
  69. package/src/TextArea/TextArea.doc.mjs +1 -1
  70. package/src/TextArea/TextArea.test.tsx +72 -0
  71. package/src/TextArea/TextArea.tsx +26 -8
  72. package/src/TreeList/TreeList.doc.mjs +2 -2
  73. package/src/TreeList/TreeList.tsx +1 -1
  74. package/src/theme/defineTheme.test.ts +127 -0
  75. package/src/theme/defineTheme.ts +70 -55
  76. package/src/theme/expandColorScale.test.ts +138 -0
  77. package/src/theme/expandColorScale.ts +89 -44
  78. package/src/theme/extensibleAxes.test.ts +365 -0
  79. package/src/theme/mergeComponents.ts +59 -0
  80. package/src/theme/onMediaTokens.ts +9 -2
  81. package/src/utils/characters.test.ts +141 -0
  82. package/src/utils/characters.ts +88 -0
  83. package/src/utils/index.ts +2 -0
  84. package/dist/astryx.umd.js +0 -147
  85. package/dist/astryx.umd.js.map +0 -7
@@ -15,7 +15,7 @@
15
15
  * - /apps/storybook/stories/TextArea.stories.tsx (storybook stories)
16
16
  * - /packages/cli/assets/templates/blocks/components/TextArea/ (showcase blocks)
17
17
  */
18
- import { useId, useOptimistic, useTransition, useRef, useCallback } from 'react';
18
+ import { useId, useOptimistic, useTransition, useRef, useCallback, useMemo } from 'react';
19
19
  import * as stylex from '@stylexjs/stylex';
20
20
  import "../theme/tokens.stylex.js";
21
21
  import { colorVars, spacingVars, typographyVars, typeScaleVars } from "../theme/tokens.stylex.js";
@@ -29,6 +29,7 @@ import { useInputStatusIcon } from "../hooks/useInputStatusIcon.js";
29
29
  import { useAnnounce } from "../hooks/useAnnounce.js";
30
30
  import { useSize } from "../SizeContext/SizeContext.js";
31
31
  import { themeProps } from "../utils/themeProps.js";
32
+ import { characterCount } from "../utils/characters.js";
32
33
  import { useTranslator } from "../i18n/index.js";
33
34
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
34
35
  const COUNTER_WARNING_THRESHOLD = 0.8;
@@ -208,7 +209,11 @@ export function TextArea({
208
209
  return;
209
210
  }
210
211
  const newValue = e.target.value;
211
- announceCounter(newValue.length);
212
+ // Guarded here, not just inside announceCounter, so textareas without a
213
+ // maxLength never pay for segmenting the whole value on each keystroke.
214
+ if (maxLength != null) {
215
+ announceCounter(characterCount(newValue));
216
+ }
212
217
  onChange?.(newValue, e);
213
218
  if (changeAction && !e.defaultPrevented) {
214
219
  startTransition(async () => {
@@ -217,6 +222,12 @@ export function TextArea({
217
222
  });
218
223
  }
219
224
  };
225
+
226
+ // Counter semantics count user-perceived characters, so an emoji or flag
227
+ // sequence counts as one character, not its code units.
228
+ // Only measured when a counter exists — segmentation is O(value length),
229
+ // and memoized so re-renders that keep the same value skip it entirely.
230
+ const valueLength = useMemo(() => maxLength != null ? characterCount(optimisticValue) : 0, [maxLength, optimisticValue]);
220
231
  const effectivelyDisabled = isDisabled || isBusy;
221
232
 
222
233
  // Focus textarea when clicking anywhere on the wrapper (icons, padding, etc.)
@@ -293,7 +304,7 @@ export function TextArea({
293
304
  "data-autofocus": hasAutoFocus || undefined,
294
305
  "aria-describedby": ariaDescribedBy,
295
306
  "aria-required": isRequired && !isOptional ? 'true' : undefined,
296
- "aria-invalid": status?.type === 'error' || maxLength != null && optimisticValue.length > maxLength ? 'true' : undefined,
307
+ "aria-invalid": status?.type === 'error' || maxLength != null && valueLength > maxLength ? 'true' : undefined,
297
308
  "aria-busy": isBusy || undefined,
298
309
  ...stylex.props(styles.textarea, textareaSizeStyles[size], isDisabled && styles.textareaDisabled, Boolean(startIcon) && styles.textareaWithStartIcon,
299
310
  // Reserve trailing space only when the end slot actually renders
@@ -318,8 +329,8 @@ export function TextArea({
318
329
  1: {
319
330
  className: "x10l6tqk x112dmjn x13oyzgk x47corl x78zum5 x6s0dn4 xzye2dw x9ynric x141an7d x1ltkj2j xjt36v0"
320
331
  }
321
- }[!!(optimisticValue.length > maxLength) << 0],
322
- children: [optimisticValue.length > maxLength &&
332
+ }[!!(valueLength > maxLength) << 0],
333
+ children: [valueLength > maxLength &&
323
334
  /*#__PURE__*/
324
335
  // Non-color cue so the over-limit state isn't conveyed by the red
325
336
  // color alone (WCAG 1.4.1). Decorative — the count text and the
@@ -327,7 +338,7 @@ export function TextArea({
327
338
  _jsx(Icon, {
328
339
  icon: "warning",
329
340
  size: "sm"
330
- }), optimisticValue.length, "/", maxLength]
341
+ }), valueLength, "/", maxLength]
331
342
  })]
332
343
  }), showsDisabledMessage && disabledMessageTooltip.renderTooltip(disabledMessage)]
333
344
  });
@@ -226,7 +226,8 @@ export function TreeList({
226
226
  ref: ref,
227
227
  "data-testid": testId,
228
228
  ...mergeProps(themeProps('tree-list', {
229
- density
229
+ density,
230
+ variant
230
231
  }), stylex.props(styles.root, xstyle), className, style),
231
232
  children: [header != null && /*#__PURE__*/_jsx("div", {
232
233
  id: headerId,
package/dist/astryx.css CHANGED
@@ -378,6 +378,7 @@
378
378
  .x1pjcqnp:not(#\#):not(#\#){border-radius:inherit}
379
379
  .x1ws5lxm:not(#\#):not(#\#){border-radius:max(0px,calc(var(--_dropdown-menu-radius,var(--spacing-2)) - var(--_dropdown-menu-padding,var(--spacing-1))))}
380
380
  .xc910v0:not(#\#):not(#\#){border-radius:max(0px,calc(var(--_segmented-control-radius) - var(--_segmented-control-padding)))}
381
+ .x1vf2byn:not(#\#):not(#\#){border-radius:var(--_banner-radius,var(--radius-container))}
381
382
  .x1jxw6zd:not(#\#):not(#\#){border-radius:var(--_button-radius,var(--radius-element))}
382
383
  .xy0xnkn:not(#\#):not(#\#){border-radius:var(--_button-radius,var(--radius-full))}
383
384
  .x153u1i6:not(#\#):not(#\#){border-radius:var(--_card-radius)}
@@ -639,14 +640,14 @@
639
640
  .x18o3ruo:not(#\#):not(#\#):not(#\#){background-image:none}
640
641
  .x1mwwwfo:not(#\#):not(#\#):not(#\#){border-collapse:collapse}
641
642
  .x16uus16:not(#\#):not(#\#):not(#\#){border-end-end-radius:0}
643
+ .x1eprgri:not(#\#):not(#\#):not(#\#){border-end-end-radius:var(--_banner-radius,var(--radius-container))}
642
644
  .x6k0y1c:not(#\#):not(#\#):not(#\#){border-end-end-radius:var(--_chat-composer-radius)}
643
- .xiaxfje:not(#\#):not(#\#):not(#\#){border-end-end-radius:var(--radius-container)}
644
645
  .x14i3lts:not(#\#):not(#\#):not(#\#){border-end-end-radius:var(--radius-element)}
645
646
  .x1dgc8on:not(#\#):not(#\#):not(#\#){border-end-end-radius:var(--radius-full)}
646
647
  .x1whkvtz:not(#\#):not(#\#):not(#\#){border-end-end-radius:var(--radius-inner)}
647
648
  .xbiv7yw:not(#\#):not(#\#):not(#\#){border-end-start-radius:0}
649
+ .x1k5g1gk:not(#\#):not(#\#):not(#\#){border-end-start-radius:var(--_banner-radius,var(--radius-container))}
648
650
  .xkvxy4d:not(#\#):not(#\#):not(#\#){border-end-start-radius:var(--_chat-composer-radius)}
649
- .x14k8p9y:not(#\#):not(#\#):not(#\#){border-end-start-radius:var(--radius-container)}
650
651
  .xquck67:not(#\#):not(#\#):not(#\#){border-end-start-radius:var(--radius-element)}
651
652
  .x1wc3881:not(#\#):not(#\#):not(#\#){border-end-start-radius:var(--radius-full)}
652
653
  .xrfs2vl:not(#\#):not(#\#):not(#\#){border-end-start-radius:var(--radius-inner)}
@@ -664,16 +665,16 @@
664
665
  .xtpqe59:not(#\#):not(#\#):not(#\#){border-inline-start-width:var(--spacing-0-5)}
665
666
  .x1gukg7c:not(#\#):not(#\#):not(#\#){border-spacing:0}
666
667
  .x1ga7v0g:not(#\#):not(#\#):not(#\#){border-start-end-radius:0}
668
+ .x13nour3:not(#\#):not(#\#):not(#\#){border-start-end-radius:var(--_banner-radius,var(--radius-container))}
667
669
  .xzc3kxz:not(#\#):not(#\#):not(#\#){border-start-end-radius:var(--_chat-composer-radius)}
668
670
  .xg0tfu:not(#\#):not(#\#):not(#\#){border-start-end-radius:var(--radius-chat)}
669
- .x7hs6f1:not(#\#):not(#\#):not(#\#){border-start-end-radius:var(--radius-container)}
670
671
  .xxbepff:not(#\#):not(#\#):not(#\#){border-start-end-radius:var(--radius-element)}
671
672
  .x8agd5o:not(#\#):not(#\#):not(#\#){border-start-end-radius:var(--radius-full)}
672
673
  .x126per9:not(#\#):not(#\#):not(#\#){border-start-end-radius:var(--radius-inner)}
673
674
  .x15mokao:not(#\#):not(#\#):not(#\#){border-start-start-radius:0}
675
+ .xwxb7qi:not(#\#):not(#\#):not(#\#){border-start-start-radius:var(--_banner-radius,var(--radius-container))}
674
676
  .xy7p8pm:not(#\#):not(#\#):not(#\#){border-start-start-radius:var(--_chat-composer-radius)}
675
677
  .x1n70msy:not(#\#):not(#\#):not(#\#){border-start-start-radius:var(--radius-chat)}
676
- .x81l70g:not(#\#):not(#\#):not(#\#){border-start-start-radius:var(--radius-container)}
677
678
  .x19415el:not(#\#):not(#\#):not(#\#){border-start-start-radius:var(--radius-full)}
678
679
  .x3qb8m:not(#\#):not(#\#):not(#\#){border-start-start-radius:var(--radius-inner)}
679
680
  .x183tx6i:not(#\#):not(#\#):not(#\#){border-start-start-radius:var(--radius-page)}
@@ -972,6 +973,7 @@
972
973
  .x1t137rt:not(#\#):not(#\#):not(#\#){outline-style:none}
973
974
  .x1k57tk5:not(#\#):not(#\#):not(#\#){outline-width:0}
974
975
  .x9y5y3v:not(#\#):not(#\#):not(#\#){overflow-clip-margin:1px}
976
+ .xj0a0fe:not(#\#):not(#\#):not(#\#){overflow-wrap:anywhere}
975
977
  .x1mzt3pk:not(#\#):not(#\#):not(#\#){overflow-wrap:break-word}
976
978
  .x1h4wwuj:not(#\#):not(#\#):not(#\#){overflow-wrap:normal}
977
979
  .xyri2b:not(#\#):not(#\#):not(#\#){padding-inline-end:0}
@@ -112,9 +112,14 @@ export interface DefineThemeInput {
112
112
  /** Theme name — used for data-astryx-theme attribute and identification */
113
113
  name: string;
114
114
  /**
115
- * Base theme to extend. When provided, the new theme starts with the
116
- * base theme's tokens, components, and fonts, then applies overrides
117
- * from this input on top. The base theme's values have lowest precedence.
115
+ * Base theme to extend. When provided, the new theme starts with everything
116
+ * the base resolved to — tokens, component overrides, icons, indicators, and
117
+ * its `onDark`/`onLight` surfaces — then applies this input on top. The base
118
+ * theme's values have lowest precedence.
119
+ *
120
+ * The result is flat: an extended theme carries its inheritance in its own
121
+ * resolved output, so `astryx theme build` emits one self-contained
122
+ * stylesheet and the base's CSS does not need to be loaded alongside it.
118
123
  *
119
124
  * Use this to create variant themes that customize only a few aspects
120
125
  * (e.g. icons, accent color) without re-specifying the full theme.
@@ -189,20 +194,38 @@ export interface DefineThemeInput {
189
194
  */
190
195
  radius?: RadiusScaleConfig;
191
196
  /**
192
- * Color scale configuration. Generates color token overrides from a
193
- * single accent color using the HCT perceptual color model.
197
+ * Color scale configuration. Generates color token overrides from an
198
+ * accent seed using the HCT perceptual color model.
194
199
  *
195
200
  * Only generates tokens derivable from the accent — status colors,
196
201
  * categorical hues, and fixed tokens (on-dark/on-light) use defaults.
197
- * Explicit `tokens` entries always take precedence.
202
+ *
203
+ * `accent` accepts a single hex (same seed for both color schemes) or a
204
+ * `[light, dark]` tuple, matching `TokenValue`. With a tuple, the light
205
+ * scheme's full palette derives from the light seed and the dark
206
+ * scheme's from the dark seed.
198
207
  *
199
208
  * `accent` is optional — omit it for a neutral-only theme, which keeps
200
209
  * the default accent tokens and only themes the neutrals.
201
210
  *
211
+ * Precedence vs `tokens`: explicit `tokens` entries win over generated
212
+ * values, token by token. Because `--color-accent-muted`,
213
+ * `--color-text-accent` and `--color-icon-accent` are generated as
214
+ * `var(--color-accent)` references, a `tokens['--color-accent']`
215
+ * override re-points them at runtime. `--color-on-accent` does NOT
216
+ * follow: it is baked from the `color.accent` seed (a contrast
217
+ * computation CSS cannot express), so overriding the accent through
218
+ * `tokens` without also overriding `--color-on-accent` leaves the two
219
+ * out of sync. To re-seat the whole palette per scheme, prefer a tuple
220
+ * `color.accent` over the `tokens['--color-accent']` workaround.
221
+ *
202
222
  * @example
203
- * ```tsx
223
+ * ```
204
224
  * color: { accent: '#0064E0', neutralStyle: 'cool', contrast: 'standard' }
205
225
  *
226
+ * // Per-scheme accents — light palette from the first seed, dark from the second
227
+ * color: { accent: ['#0064E0', '#48CAE4'] }
228
+ *
206
229
  * // Neutral-only — accent tokens stay at their defaults
207
230
  * color: { neutralStyle: 'warm' }
208
231
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"defineTheme.d.ts","sourceRoot":"","sources":["../../src/theme/defineTheme.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"defineTheme.d.ts","sourceRoot":"","sources":["../../src/theme/defineTheme.ts"],"names":[],"mappings":"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"}
@@ -44,6 +44,7 @@ import { expandRadiusScale } from "./expandRadiusScale.js";
44
44
  import { expandColorScale } from "./expandColorScale.js";
45
45
  import { domainTokenDefaults } from "./domainTokens/index.js";
46
46
  import { registerTheme } from "./themeRegistry.js";
47
+ import { deepMergeComponents } from "./mergeComponents.js";
47
48
 
48
49
  // =============================================================================
49
50
  // Types
@@ -155,48 +156,6 @@ function resolveTokenValue(value) {
155
156
  return value;
156
157
  }
157
158
 
158
- /**
159
- * Deep-merge two component style maps.
160
- * Properties in `overrides` take precedence over `base`.
161
- * This allows typeScale-generated rules to be overridden by explicit components.
162
- */
163
- function deepMergeComponents(base, overrides) {
164
- if (!base && !overrides) {
165
- return undefined;
166
- }
167
- if (!base) {
168
- return overrides;
169
- }
170
- if (!overrides) {
171
- return base;
172
- }
173
- const result = {};
174
-
175
- // Start with all base entries
176
- for (const [component, rules] of Object.entries(base)) {
177
- result[component] = {
178
- ...rules
179
- };
180
- }
181
-
182
- // Merge overrides on top
183
- for (const [component, rules] of Object.entries(overrides)) {
184
- if (!result[component]) {
185
- result[component] = {
186
- ...rules
187
- };
188
- } else {
189
- for (const [key, styles] of Object.entries(rules)) {
190
- result[component][key] = {
191
- ...result[component][key],
192
- ...styles
193
- };
194
- }
195
- }
196
- }
197
- return result;
198
- }
199
-
200
159
  /**
201
160
  * Resolve a FontWeight name to a var() reference.
202
161
  * Named weights map to var(--font-weight-*); raw values pass through.
@@ -226,6 +185,24 @@ function buildFontFamily(family, fallbacks) {
226
185
  return quoted;
227
186
  }
228
187
 
188
+ /**
189
+ * Describe a rejected `extends` value for the error message — enough to tell a
190
+ * missed import (`undefined`) from a module namespace or a plain object.
191
+ */
192
+ function describeBadBase(value) {
193
+ if (value === undefined) {
194
+ return 'undefined';
195
+ }
196
+ if (value === null) {
197
+ return 'null';
198
+ }
199
+ if (typeof value !== 'object') {
200
+ return typeof value;
201
+ }
202
+ const keys = Object.keys(value);
203
+ return `an object with keys [${keys.slice(0, 4).join(', ')}${keys.length > 4 ? ', …' : ''}]`;
204
+ }
205
+
229
206
  /**
230
207
  * Create an Astryx theme.
231
208
  *
@@ -239,7 +216,15 @@ function buildFontFamily(family, fallbacks) {
239
216
  export function defineTheme(input) {
240
217
  const tokens = {};
241
218
 
242
- // 0. Pre-seed from base theme when `extends` is provided (lowest precedence)
219
+ // 0. Pre-seed from base theme when `extends` is provided (lowest precedence).
220
+ // A base that is not a theme is refused rather than ignored: `extends` used
221
+ // to inherit nothing when its value was undefined, which is what a named
222
+ // import silently resolving to the wrong module hands over, and the theme
223
+ // then built into a plausible-looking stylesheet missing everything it was
224
+ // supposed to inherit.
225
+ if ('extends' in input && !isDefinedTheme(input.extends)) {
226
+ throw new Error(`defineTheme("${input.name}"): \`extends\` must be a theme from defineTheme(), got ${describeBadBase(input.extends)}. ` + `Check that the import naming your base theme resolves to its source and exports that name — ` + `a generated \`<theme>.js\` artifact sitting next to the source exports \`<name>Theme\`, not the source's own export.`);
227
+ }
243
228
  const base = input.extends;
244
229
  if (base) {
245
230
  for (const [key, value] of Object.entries(base.tokens)) {
@@ -370,9 +355,10 @@ export function defineTheme(input) {
370
355
  components = deepMergeComponents(base.components, components);
371
356
  }
372
357
 
373
- // 4. Resolve on-media token overrides (defaults + user overrides)
374
- const __onDark = resolveOnMedia('dark', input.onDark);
375
- const __onLight = resolveOnMedia('light', input.onLight);
358
+ // 4. Resolve on-media token overrides (base's resolved surface, then
359
+ // defaults, then this theme's own overrides)
360
+ const __onDark = resolveOnMedia('dark', input.onDark, base?.__onDark);
361
+ const __onLight = resolveOnMedia('light', input.onLight, base?.__onLight);
376
362
 
377
363
  // 5. Merge icons — input icons override base icons
378
364
  const icons = input.icons && base?.icons ? {
@@ -392,7 +378,10 @@ export function defineTheme(input) {
392
378
  components,
393
379
  icons,
394
380
  indicators,
395
- __inputTokens: input.tokens,
381
+ __inputTokens: base?.__inputTokens || input.tokens ? {
382
+ ...base?.__inputTokens,
383
+ ...input.tokens
384
+ } : undefined,
396
385
  __onDark,
397
386
  __onLight
398
387
  };
@@ -6,6 +6,9 @@
6
6
  * // Minimal — just a seed color
7
7
  * { accent: '#0064E0' }
8
8
  *
9
+ * // Per-scheme seeds: light palettes from '#0064E0', dark from '#48CAE4'
10
+ * { accent: ['#0064E0', '#48CAE4'] }
11
+ *
9
12
  * // With customization
10
13
  * { accent: '#B7410E', neutralStyle: 'warm', contrast: 'high' }
11
14
  *
@@ -15,14 +18,18 @@
15
18
  */
16
19
  export interface ColorScaleConfig {
17
20
  /**
18
- * Seed accent color as hex (#RRGGBB). Everything derives from this.
21
+ * Seed accent color. Everything derives from this.
22
+ *
23
+ * Either a single hex (#RRGGBB) used for both color schemes, or a
24
+ * `[light, dark]` hex tuple: the light scheme's palettes derive from
25
+ * the light seed and the dark scheme's palettes from the dark seed.
19
26
  *
20
27
  * Optional. When omitted, the neutral palettes are seeded from the
21
28
  * default accent's hue and the accent tokens (--color-accent,
22
29
  * --color-accent-muted, --color-on-accent) are not generated — they
23
30
  * fall through to colorDefaults.
24
31
  */
25
- accent?: string;
32
+ accent?: string | [light: string, dark: string];
26
33
  /**
27
34
  * Neutral tone warmth. Controls how much of the seed's hue bleeds
28
35
  * into neutral/background colors.
@@ -56,6 +63,11 @@ export declare function ensureContrastTone(hue: number, chroma: number, startTon
56
63
  * --color-on-dark/on-light) are NOT generated — they fall through
57
64
  * to colorDefaults.
58
65
  *
66
+ * A `[light, dark]` tuple accent seeds each scheme separately: the light
67
+ * half of every generated `light-dark()` pair comes from the light seed's
68
+ * palettes, the dark half from the dark seed's. A string accent seeds both
69
+ * halves from the same palettes, exactly as before tuples were supported.
70
+ *
59
71
  * Without an `accent`, the accent tokens join that fall-through set: the
60
72
  * neutrals are seeded from the default accent's hue, and --color-accent,
61
73
  * --color-accent-muted and --color-on-accent keep their colorDefaults values.
@@ -65,6 +77,9 @@ export declare function ensureContrastTone(hue: number, chroma: number, startTon
65
77
  * const tokens = expandColorScale({ accent: '#0064E0' });
66
78
  * // tokens['--color-accent'] === 'light-dark(#..., #...)'
67
79
  *
80
+ * const perScheme = expandColorScale({ accent: ['#0064E0', '#48CAE4'] });
81
+ * // light half derives from #0064E0, dark half from #48CAE4
82
+ *
68
83
  * const neutralOnly = expandColorScale({ neutralStyle: 'warm' });
69
84
  * // neutralOnly['--color-accent'] === undefined
70
85
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"expandColorScale.d.ts","sourceRoot":"","sources":["../../src/theme/expandColorScale.ts"],"names":[],"mappings":"AAwCA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAE3C;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAyCtD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,EACZ,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,MAAM,CAYR;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAoI3E"}
1
+ {"version":3,"file":"expandColorScale.d.ts","sourceRoot":"","sources":["../../src/theme/expandColorScale.ts"],"names":[],"mappings":"AAgDA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAEhD;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAE3C;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAyCtD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,EACZ,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,MAAM,CAYR;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CA0J3E"}
@@ -6,8 +6,8 @@
6
6
  * @output Token overrides for derivable color tokens
7
7
  * @position Theme utility; consumed by defineTheme.ts
8
8
  *
9
- * Generates color token overrides from a single accent color using the
10
- * HCT perceptual color model. Only produces tokens that meaningfully
9
+ * Generates color token overrides from an accent seed using the HCT
10
+ * perceptual color model. Only produces tokens that meaningfully
11
11
  * derive from the accent — status colors, categorical hues, and fixed
12
12
  * tokens (on-dark/on-light) fall through to colorDefaults.
13
13
  *
@@ -15,11 +15,19 @@
15
15
  * ramp (seeded from the default accent's hue) while the accent tokens
16
16
  * themselves fall through to colorDefaults, same as the tokens above.
17
17
  *
18
+ * `accent` also accepts a `[light, dark]` tuple (matching `TokenValue` in
19
+ * defineTheme). A tuple seeds each color scheme from its own half: the
20
+ * light side of every generated `light-dark()` pair derives from the light
21
+ * accent's palettes, the dark side from the dark accent's. A plain string
22
+ * seeds both sides identically, token for token the same output as before
23
+ * tuples existed.
24
+ *
18
25
  * WCAG contrast guarantees (asserted in expandColorScale.test.ts):
19
26
  * - Text tones are guaranteed >= 4.5:1 against their surfaces by tone
20
27
  * spacing alone — HCT tone is CIE L*, which fixes relative luminance
21
28
  * regardless of hue/chroma, so the fixed tone assignments hold for any
22
- * accent/neutralStyle (WCAG 1.4.3).
29
+ * accent/neutralStyle (WCAG 1.4.3), and each half of a tuple pairs with
30
+ * surfaces derived from that same seed.
23
31
  * - --color-border-emphasized (form-control boundaries) is tone-bumped
24
32
  * until it reaches >= 3:1 against the generated surface (WCAG 1.4.11).
25
33
  * - --color-border, --color-skeleton, and --color-track are intentionally
@@ -46,6 +54,9 @@ import { hexToHct, hctToHex, tonalPalette, hexWithAlpha } from "./hct.js";
46
54
  * // Minimal — just a seed color
47
55
  * { accent: '#0064E0' }
48
56
  *
57
+ * // Per-scheme seeds: light palettes from '#0064E0', dark from '#48CAE4'
58
+ * { accent: ['#0064E0', '#48CAE4'] }
59
+ *
49
60
  * // With customization
50
61
  * { accent: '#B7410E', neutralStyle: 'warm', contrast: 'high' }
51
62
  *
@@ -126,6 +137,11 @@ export function ensureContrastTone(hue, chroma, startTone, step, background, min
126
137
  * --color-on-dark/on-light) are NOT generated — they fall through
127
138
  * to colorDefaults.
128
139
  *
140
+ * A `[light, dark]` tuple accent seeds each scheme separately: the light
141
+ * half of every generated `light-dark()` pair comes from the light seed's
142
+ * palettes, the dark half from the dark seed's. A string accent seeds both
143
+ * halves from the same palettes, exactly as before tuples were supported.
144
+ *
129
145
  * Without an `accent`, the accent tokens join that fall-through set: the
130
146
  * neutrals are seeded from the default accent's hue, and --color-accent,
131
147
  * --color-accent-muted and --color-on-accent keep their colorDefaults values.
@@ -135,6 +151,9 @@ export function ensureContrastTone(hue, chroma, startTone, step, background, min
135
151
  * const tokens = expandColorScale({ accent: '#0064E0' });
136
152
  * // tokens['--color-accent'] === 'light-dark(#..., #...)'
137
153
  *
154
+ * const perScheme = expandColorScale({ accent: ['#0064E0', '#48CAE4'] });
155
+ * // light half derives from #0064E0, dark half from #48CAE4
156
+ *
138
157
  * const neutralOnly = expandColorScale({ neutralStyle: 'warm' });
139
158
  * // neutralOnly['--color-accent'] === undefined
140
159
  * ```
@@ -145,14 +164,26 @@ export function expandColorScale(config) {
145
164
  neutralStyle = 'cool',
146
165
  contrast = 'standard'
147
166
  } = config;
148
- const seed = hexToHct(accent ?? DEFAULT_ACCENT_SEED);
149
- const seedHue = seed.hue;
150
- const primaryChroma = Math.max(seed.chroma, 48);
167
+
168
+ // Normalize to per-scheme seeds. A string accent (or an absent one) uses
169
+ // the same seed for both halves, which keeps single-seed output identical
170
+ // to the pre-tuple implementation.
171
+ const [lightAccent, darkAccent] = Array.isArray(accent) ? accent : [accent, accent];
172
+ const lightSeed = hexToHct(lightAccent ?? DEFAULT_ACCENT_SEED);
173
+ const sameSeed = darkAccent === lightAccent;
174
+ const darkSeed = sameSeed ? lightSeed : hexToHct(darkAccent ?? DEFAULT_ACCENT_SEED);
151
175
  const neutralChroma = NEUTRAL_CHROMA[neutralStyle] ?? 5;
152
176
  const neutralVariantChroma = NEUTRAL_VARIANT_CHROMA[neutralStyle] ?? 8;
153
- const P = tonalPalette(seedHue, primaryChroma);
154
- const N = tonalPalette(seedHue, neutralChroma);
155
- const NV = tonalPalette(seedHue, neutralVariantChroma);
177
+
178
+ // Palette naming: *L palettes feed the light (first) half of each ld()
179
+ // pair below, *D palettes the dark (second) half. With a single seed the
180
+ // D palettes alias the L ones.
181
+ const PL = tonalPalette(lightSeed.hue, Math.max(lightSeed.chroma, 48));
182
+ const NL = tonalPalette(lightSeed.hue, neutralChroma);
183
+ const NVL = tonalPalette(lightSeed.hue, neutralVariantChroma);
184
+ const PD = sameSeed ? PL : tonalPalette(darkSeed.hue, Math.max(darkSeed.chroma, 48));
185
+ const ND = sameSeed ? NL : tonalPalette(darkSeed.hue, neutralChroma);
186
+ const NVD = sameSeed ? NVL : tonalPalette(darkSeed.hue, neutralVariantChroma);
156
187
  const isHigh = contrast === 'high';
157
188
  const textPrimaryLightTone = isHigh ? 0 : 10;
158
189
  const textPrimaryDarkTone = isHigh ? 99 : 90;
@@ -173,7 +204,7 @@ export function expandColorScale(config) {
173
204
  // contrast starts at 70/30 and walks toward mid-scale only as far as needed.
174
205
  const borderEmphasizedStartLight = isHigh ? 50 : 70;
175
206
  const borderEmphasizedStartDark = isHigh ? 50 : 30;
176
- const borderEmphasized = ld(ensureContrastTone(seedHue, neutralVariantChroma, borderEmphasizedStartLight, -1, N[99], NON_TEXT_MIN_CONTRAST), ensureContrastTone(seedHue, neutralVariantChroma, borderEmphasizedStartDark, 1, N[10], NON_TEXT_MIN_CONTRAST));
207
+ const borderEmphasized = ld(ensureContrastTone(lightSeed.hue, neutralVariantChroma, borderEmphasizedStartLight, -1, NL[99], NON_TEXT_MIN_CONTRAST), ensureContrastTone(darkSeed.hue, neutralVariantChroma, borderEmphasizedStartDark, 1, ND[10], NON_TEXT_MIN_CONTRAST));
177
208
  return {
178
209
  // Core semantic — only with a seed accent. Without one these fall through
179
210
  // to colorDefaults, whose --color-accent is NOT what the default seed
@@ -181,47 +212,46 @@ export function expandColorScale(config) {
181
212
  // recolor every neutral-only theme. Nullish and not truthy, matching the
182
213
  // seed above, so a supplied-but-malformed accent keeps its old behavior.
183
214
  ...(accent != null ? {
184
- '--color-accent': ld(P[40], P[80]),
215
+ '--color-accent': ld(PL[40], PD[80]),
185
216
  // Derived accent tokens reference --color-accent instead of baking its
186
217
  // resolved hex, so a scoped override of the base token re-accents the
187
218
  // whole subtree at runtime. --color-on-accent stays baked: it is a
188
219
  // contrast computation against the accent, which CSS cannot express.
189
220
  '--color-accent-muted': ld(accentWithAlpha(0.2), accentWithAlpha(0.25)),
190
- '--color-on-accent': ld(P[100], P[20])
221
+ '--color-on-accent': ld(PL[100], PD[20])
191
222
  } : null),
192
- '--color-neutral': ld(hexWithAlpha(N[10], 0.1), hexWithAlpha(N[90], 0.2)),
193
- '--color-background-surface': ld(N[99], N[10]),
194
- '--color-background-body': ld(N[95], N[5]),
195
- '--color-overlay': ld(hexWithAlpha(N[10], 0.4), hexWithAlpha(N[10], 0.6)),
196
- '--color-overlay-hover': ld(hexWithAlpha(N[10], 0.05), hexWithAlpha(N[100], 0.05)),
197
- '--color-overlay-pressed': ld(hexWithAlpha(N[10], 0.1), hexWithAlpha(N[100], 0.1)),
198
- '--color-background-muted': ld(hexWithAlpha(N[10], 0.05), hexWithAlpha(N[10], 0.5)),
223
+ '--color-neutral': ld(hexWithAlpha(NL[10], 0.1), hexWithAlpha(ND[90], 0.2)),
224
+ '--color-background-surface': ld(NL[99], ND[10]),
225
+ '--color-background-body': ld(NL[95], ND[5]),
226
+ '--color-overlay': ld(hexWithAlpha(NL[10], 0.4), hexWithAlpha(ND[10], 0.6)),
227
+ '--color-overlay-hover': ld(hexWithAlpha(NL[10], 0.05), hexWithAlpha(ND[100], 0.05)),
228
+ '--color-overlay-pressed': ld(hexWithAlpha(NL[10], 0.1), hexWithAlpha(ND[100], 0.1)),
229
+ '--color-background-muted': ld(hexWithAlpha(NL[10], 0.05), hexWithAlpha(ND[10], 0.5)),
199
230
  // Text
200
- '--color-text-primary': ld(N[textPrimaryLightTone], N[textPrimaryDarkTone]),
201
- '--color-text-secondary': ld(NV[textSecondaryLightTone], NV[textSecondaryDarkTone]),
202
- '--color-text-disabled': ld(NV[60], NV[40]),
231
+ '--color-text-primary': ld(NL[textPrimaryLightTone], ND[textPrimaryDarkTone]),
232
+ '--color-text-secondary': ld(NVL[textSecondaryLightTone], NVD[textSecondaryDarkTone]),
233
+ '--color-text-disabled': ld(NVL[60], NVD[40]),
203
234
  '--color-text-accent': 'var(--color-accent)',
204
235
  // Icon
205
236
  '--color-icon-accent': 'var(--color-accent)',
206
- '--color-icon-primary': ld(N[textPrimaryLightTone], N[textPrimaryDarkTone]),
207
- '--color-icon-secondary': ld(NV[textSecondaryLightTone], NV[textSecondaryDarkTone]),
208
- '--color-icon-disabled': ld(NV[60], NV[40]),
237
+ '--color-icon-primary': ld(NL[textPrimaryLightTone], ND[textPrimaryDarkTone]),
238
+ '--color-icon-secondary': ld(NVL[textSecondaryLightTone], NVD[textSecondaryDarkTone]),
239
+ '--color-icon-disabled': ld(NVL[60], NVD[40]),
209
240
  // Surface variants
210
- '--color-background-card': ld(N[99], N[10]),
211
- '--color-background-popover': ld(N[99], N[20]),
212
- '--color-background-inverted': ld(N[10], N[99]),
213
- // Border
241
+ '--color-background-card': ld(NL[99], ND[10]),
242
+ '--color-background-popover': ld(NL[99], ND[20]),
243
+ '--color-background-inverted': ld(NL[10], ND[99]),
214
244
  // Border
215
245
  // Decorative hairline — not a WCAG 1.4.11 boundary. High contrast
216
246
  // doubles the alpha so structural boundaries stay perceivable.
217
- '--color-border': ld(hexWithAlpha(N[10], borderSubtleAlpha), hexWithAlpha(N[95], borderSubtleAlpha)),
247
+ '--color-border': ld(hexWithAlpha(NL[10], borderSubtleAlpha), hexWithAlpha(ND[95], borderSubtleAlpha)),
218
248
  '--color-border-emphasized': borderEmphasized,
219
249
  // Effects
220
- '--color-skeleton': ld(NV[70], NV[30]),
250
+ '--color-skeleton': ld(NVL[70], NVD[30]),
221
251
  // Channel-on-body surface (ProgressBar/Slider tracks, Switch off-state).
222
252
  // Defaults to the same NV[70]/NV[30] ramp stop as --color-skeleton.
223
- '--color-track': ld(NV[70], NV[30]),
224
- '--color-shadow': ld(hexWithAlpha(N[0], 0.1), hexWithAlpha(N[0], 0.3)),
253
+ '--color-track': ld(NVL[70], NVD[30]),
254
+ '--color-shadow': ld(hexWithAlpha(NL[0], 0.1), hexWithAlpha(ND[0], 0.3)),
225
255
  '--color-tint-hover': ld('black', 'white')
226
256
  };
227
257
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @file Component style-map merging
3
+ *
4
+ * One merge rule for component overrides, shared by every layer that composes
5
+ * them: `extends` inheritance, generated type-scale rules, and the on-media
6
+ * (`onDark`/`onLight`) surfaces. Merging is per style key, so a child that
7
+ * restates one property of `button.base` keeps the rest of the base's.
8
+ *
9
+ * @input two ComponentStyleMaps — the base and the overrides that win
10
+ * @output a merged ComponentStyleMap
11
+ * @position packages/core/src/theme/mergeComponents.ts
12
+ */
13
+ import type { ComponentStyleMap } from './defineTheme';
14
+ /**
15
+ * Deep-merge component style maps: `overrides` wins per style key, and every
16
+ * component and key the base declared that the overrides do not mention is
17
+ * carried through untouched.
18
+ */
19
+ export declare function deepMergeComponents(base?: ComponentStyleMap, overrides?: ComponentStyleMap): ComponentStyleMap | undefined;
20
+ //# sourceMappingURL=mergeComponents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mergeComponents.d.ts","sourceRoot":"","sources":["../../src/theme/mergeComponents.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAC;AAErD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,CAAC,EAAE,iBAAiB,EACxB,SAAS,CAAC,EAAE,iBAAiB,GAC5B,iBAAiB,GAAG,SAAS,CAiC/B"}