@astryxdesign/core 0.6.0 → 0.6.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 (203) hide show
  1. package/CHANGELOG.md +41 -3
  2. package/dist/AppShell/AppShell.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +12 -1
  4. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  5. package/dist/BottomSheet/BottomSheetSwitcher.js +44 -15
  6. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +3 -2
  7. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  8. package/dist/Breadcrumbs/BreadcrumbItem.js +3 -7
  9. package/dist/Center/Center.d.ts +23 -16
  10. package/dist/Center/Center.d.ts.map +1 -1
  11. package/dist/Center/Center.js +7 -5
  12. package/dist/CodeBlock/CodeBlock.js +2 -2
  13. package/dist/DateInput/DateInput.d.ts.map +1 -1
  14. package/dist/DateInput/DateInput.js +12 -2
  15. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  16. package/dist/DateTimeInput/DateTimeInput.js +12 -2
  17. package/dist/Field/Field.d.ts.map +1 -1
  18. package/dist/Field/Field.js +1 -0
  19. package/dist/Field/InputClearButton.d.ts +2 -2
  20. package/dist/Field/InputClearButton.d.ts.map +1 -1
  21. package/dist/Field/InputClearButton.js +5 -1
  22. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  23. package/dist/Field/PanelSearchInput.js +16 -4
  24. package/dist/FileInput/FileInput.d.ts.map +1 -1
  25. package/dist/FileInput/FileInput.js +12 -1
  26. package/dist/HoverCard/useHoverCard.js +2 -2
  27. package/dist/Indicator/CheckboxIndicator.js +2 -2
  28. package/dist/Indicator/RadioIndicator.js +2 -2
  29. package/dist/Layer/layerStack.d.ts +10 -0
  30. package/dist/Layer/layerStack.d.ts.map +1 -1
  31. package/dist/Layer/layerStack.js +21 -9
  32. package/dist/Layer/useLayerDismissal.d.ts +2 -3
  33. package/dist/Layer/useLayerDismissal.d.ts.map +1 -1
  34. package/dist/Layer/useLayerDismissal.js +2 -3
  35. package/dist/NavIcon/NavIcon.js +2 -2
  36. package/dist/NumberInput/NumberInput.d.ts.map +1 -1
  37. package/dist/NumberInput/NumberInput.js +12 -2
  38. package/dist/Popover/usePopover.d.ts +3 -2
  39. package/dist/Popover/usePopover.d.ts.map +1 -1
  40. package/dist/Popover/usePopover.js +4 -2
  41. package/dist/ProgressBar/ProgressBar.js +2 -2
  42. package/dist/ScrollableArea/ScrollableArea.d.ts +79 -0
  43. package/dist/ScrollableArea/ScrollableArea.d.ts.map +1 -0
  44. package/dist/ScrollableArea/ScrollableArea.js +144 -0
  45. package/dist/ScrollableArea/index.d.ts +11 -0
  46. package/dist/ScrollableArea/index.d.ts.map +1 -0
  47. package/dist/ScrollableArea/index.js +11 -0
  48. package/dist/StatusDot/StatusDot.js +2 -2
  49. package/dist/TextArea/TextArea.js +2 -2
  50. package/dist/TextInput/TextInput.d.ts.map +1 -1
  51. package/dist/TextInput/TextInput.js +19 -4
  52. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  53. package/dist/TimeInput/TimeInput.js +12 -2
  54. package/dist/Typeahead/BaseTypeahead.d.ts +21 -14
  55. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  56. package/dist/Typeahead/BaseTypeahead.js +56 -20
  57. package/dist/astryx.css +15 -0
  58. package/dist/hooks/index.d.ts +2 -0
  59. package/dist/hooks/index.d.ts.map +1 -1
  60. package/dist/hooks/index.js +1 -0
  61. package/dist/hooks/scrollGeometry.d.ts +24 -0
  62. package/dist/hooks/scrollGeometry.d.ts.map +1 -0
  63. package/dist/hooks/scrollGeometry.js +86 -0
  64. package/dist/hooks/scrollOwnerRegistry.d.ts +15 -0
  65. package/dist/hooks/scrollOwnerRegistry.d.ts.map +1 -0
  66. package/dist/hooks/scrollOwnerRegistry.js +24 -0
  67. package/dist/hooks/useFocusTrap.d.ts +8 -0
  68. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  69. package/dist/hooks/useFocusTrap.js +22 -11
  70. package/dist/hooks/useScrollableArea.d.ts +51 -0
  71. package/dist/hooks/useScrollableArea.d.ts.map +1 -0
  72. package/dist/hooks/useScrollableArea.js +287 -0
  73. package/dist/index.d.ts +1 -0
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +1 -0
  76. package/dist/theme/defineTheme.d.ts +2 -6
  77. package/dist/theme/defineTheme.d.ts.map +1 -1
  78. package/dist/theme/defineTheme.js +1 -1
  79. package/dist/theme/derivedVarRegistry.js +1 -1
  80. package/dist/theme/localTokens.d.ts +8 -11
  81. package/dist/theme/localTokens.d.ts.map +1 -1
  82. package/dist/theme/localTokens.js +17 -71
  83. package/dist/theme/themeAdaptations.d.ts.map +1 -1
  84. package/dist/theme/themeAdaptations.js +4 -4
  85. package/dist/utils/themeProps.d.ts +10 -10
  86. package/dist/utils/themeProps.d.ts.map +1 -1
  87. package/dist/utils/themeProps.js +27 -10
  88. package/locales/en.json +16 -0
  89. package/locales/pseudo.json +12 -0
  90. package/package.json +7 -2
  91. package/src/AppShell/AppShell.test.tsx +36 -0
  92. package/src/AppShell/AppShell.tsx +4 -1
  93. package/src/AspectRatio/AspectRatio.doc.mjs +3 -3
  94. package/src/Banner/Banner.test.tsx +3 -1
  95. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +56 -1
  96. package/src/BottomSheet/BottomSheetSwitcher.spec.md +211 -0
  97. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +134 -2
  98. package/src/BottomSheet/BottomSheetSwitcher.tsx +43 -20
  99. package/src/Breadcrumbs/BreadcrumbItem.doc.mjs +10 -5
  100. package/src/Breadcrumbs/BreadcrumbItem.spec.md +225 -0
  101. package/src/Breadcrumbs/BreadcrumbItem.tsx +8 -13
  102. package/src/Breadcrumbs/Breadcrumbs.doc.mjs +2 -2
  103. package/src/Breadcrumbs/Breadcrumbs.test.tsx +49 -2
  104. package/src/Center/Center.doc.mjs +32 -28
  105. package/src/Center/Center.spec.md +225 -0
  106. package/src/Center/Center.test.tsx +42 -4
  107. package/src/Center/Center.tsx +24 -17
  108. package/src/Chat/ChatSystemMessage.test.tsx +2 -9
  109. package/src/CodeBlock/CodeBlock.doc.mjs +2 -2
  110. package/src/CodeBlock/CodeBlock.tsx +2 -2
  111. package/src/DateInput/DateInput.test.tsx +4 -4
  112. package/src/DateInput/DateInput.tsx +15 -4
  113. package/src/DateRangeInput/DateRangeInput.test.tsx +2 -2
  114. package/src/DateTimeInput/DateTimeInput.test.tsx +6 -4
  115. package/src/DateTimeInput/DateTimeInput.tsx +18 -7
  116. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +4 -77
  117. package/src/Field/Field.test.tsx +42 -0
  118. package/src/Field/Field.tsx +6 -0
  119. package/src/Field/InputClearButton.test.tsx +35 -1
  120. package/src/Field/InputClearButton.tsx +7 -3
  121. package/src/Field/PanelSearchInput.tsx +21 -8
  122. package/src/FieldStatus/FieldStatus.spec.md +27 -17
  123. package/src/FieldStatus/FieldStatus.test.tsx +7 -5
  124. package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +198 -0
  125. package/src/FieldStatus/__tests__/StatusMessage.a11y.known-failures.ts +13 -0
  126. package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +305 -0
  127. package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +317 -0
  128. package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +155 -0
  129. package/src/FileInput/FileInput.tsx +10 -1
  130. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +3 -3
  131. package/src/HoverCard/HoverCard.doc.mjs +4 -4
  132. package/src/HoverCard/useHoverCard.tsx +2 -2
  133. package/src/Indicator/CheckboxIndicator.tsx +2 -2
  134. package/src/Indicator/Indicator.doc.mjs +2 -2
  135. package/src/Indicator/Indicator.test.tsx +1 -1
  136. package/src/Indicator/RadioIndicator.tsx +2 -2
  137. package/src/Layer/layerStack.ts +20 -9
  138. package/src/Layer/useLayerDismissal.ts +2 -3
  139. package/src/MultiSelector/MultiSelector.test.tsx +4 -4
  140. package/src/NavIcon/NavIcon.doc.mjs +4 -4
  141. package/src/NavIcon/NavIcon.tsx +2 -2
  142. package/src/NumberInput/NumberInput.tsx +18 -7
  143. package/src/Popover/Popover.doc.mjs +10 -10
  144. package/src/Popover/Popover.spec.md +55 -65
  145. package/src/Popover/Popover.test.tsx +29 -0
  146. package/src/Popover/usePopover.doc.mjs +4 -4
  147. package/src/Popover/usePopover.tsx +7 -4
  148. package/src/ProgressBar/ProgressBar.doc.mjs +4 -4
  149. package/src/ProgressBar/ProgressBar.test.tsx +1 -31
  150. package/src/ProgressBar/ProgressBar.tsx +2 -2
  151. package/src/RadioList/RadioList.test.tsx +5 -144
  152. package/src/RadioList/__tests__/RadioGroup.a11y.chromium.spec.ts +255 -0
  153. package/src/RadioList/__tests__/RadioGroup.a11y.known-failures.ts +12 -0
  154. package/src/RadioList/__tests__/RadioGroup.a11y.renders.tsx +232 -0
  155. package/src/RadioList/__tests__/RadioGroup.a11y.states.ts +503 -0
  156. package/src/RadioList/__tests__/RadioGroup.a11y.test.tsx +217 -0
  157. package/src/ScrollableArea/ScrollableArea.doc.mjs +100 -0
  158. package/src/ScrollableArea/ScrollableArea.spec.md +189 -0
  159. package/src/ScrollableArea/ScrollableArea.test.tsx +299 -0
  160. package/src/ScrollableArea/ScrollableArea.tsx +259 -0
  161. package/src/ScrollableArea/index.ts +26 -0
  162. package/src/ScrollableArea/modules/useScrollableArea.spec.md +121 -0
  163. package/src/SegmentedControl/SegmentedControl.test.tsx +5 -172
  164. package/src/Selector/Selector.test.tsx +4 -4
  165. package/src/Spinner/Spinner.test.tsx +0 -18
  166. package/src/StatusDot/StatusDot.doc.mjs +4 -4
  167. package/src/StatusDot/StatusDot.tsx +2 -2
  168. package/src/TabList/TabList.test.tsx +5 -9
  169. package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +191 -0
  170. package/src/TabList/__tests__/Tabs.a11y.known-failures.ts +45 -0
  171. package/src/TabList/__tests__/Tabs.a11y.renders.tsx +92 -0
  172. package/src/TabList/__tests__/Tabs.a11y.states.ts +247 -0
  173. package/src/TabList/__tests__/Tabs.a11y.test.tsx +153 -0
  174. package/src/Table/Table.doc.mjs +2 -2
  175. package/src/TextArea/TextArea.doc.mjs +4 -4
  176. package/src/TextArea/TextArea.tsx +2 -2
  177. package/src/TextInput/TextInput.doc.mjs +2 -1
  178. package/src/TextInput/TextInput.test.tsx +94 -0
  179. package/src/TextInput/TextInput.tsx +22 -6
  180. package/src/TimeInput/TimeInput.tsx +18 -7
  181. package/src/Toast/ToastViewport.test.tsx +1 -39
  182. package/src/Typeahead/BaseTypeahead.doc.mjs +229 -33
  183. package/src/Typeahead/BaseTypeahead.spec.md +269 -0
  184. package/src/Typeahead/BaseTypeahead.test.tsx +200 -0
  185. package/src/Typeahead/BaseTypeahead.tsx +99 -30
  186. package/src/hooks/index.ts +13 -0
  187. package/src/hooks/scrollGeometry.ts +155 -0
  188. package/src/hooks/scrollOwnerRegistry.ts +47 -0
  189. package/src/hooks/useFocusTrap.ts +22 -11
  190. package/src/hooks/useFocusTrapEscapeShim.test.tsx +4 -3
  191. package/src/hooks/useScrollableArea.doc.mjs +108 -0
  192. package/src/hooks/useScrollableArea.test.tsx +437 -0
  193. package/src/hooks/useScrollableArea.ts +469 -0
  194. package/src/index.ts +1 -0
  195. package/src/theme/defineTheme.test.ts +65 -105
  196. package/src/theme/defineTheme.ts +3 -9
  197. package/src/theme/derivedVarRegistry.ts +1 -1
  198. package/src/theme/localTokens.ts +25 -96
  199. package/src/theme/publicThemeHelperContract.test.ts +2 -2
  200. package/src/theme/themeAdaptations.test.ts +16 -42
  201. package/src/theme/themeAdaptations.ts +6 -9
  202. package/src/utils/themeProps.test.ts +29 -10
  203. package/src/utils/themeProps.ts +36 -17
@@ -80,40 +80,34 @@ describe('defineTheme', () => {
80
80
  warn.mockRestore();
81
81
  });
82
82
 
83
- it('keeps explicitly enrolled local tokens separate and emits their exact names', () => {
84
- const theme = defineTheme({
85
- name: 'ocean-theme',
86
- localTokens: {
87
- '--astryx-theme-ocean-theme-color-status-fill-accent': [
88
- '#0077b6',
89
- '#48cae4',
90
- ],
91
- },
92
- components: {
93
- badge: {
94
- 'variant:info': {
95
- backgroundColor:
96
- 'var(--astryx-theme-ocean-theme-color-status-fill-accent)',
83
+ it.each([
84
+ ['ocean-theme', '--ac-selection-ink'],
85
+ ['Theme.Owner', '--'],
86
+ ['ocean-theme', '--astryx-theme-ocean-theme-color-status-fill-accent'],
87
+ ])(
88
+ 'accepts valid local token name %s / %s and preserves its exact bytes',
89
+ (themeName, token) => {
90
+ const theme = defineTheme({
91
+ name: themeName,
92
+ localTokens: {[token]: ['#0077b6', '#48cae4']},
93
+ components: {
94
+ badge: {
95
+ 'variant:info': {backgroundColor: `var(${token})`},
97
96
  },
98
97
  },
99
- },
100
- });
101
-
102
- expect(theme.tokens).not.toHaveProperty(
103
- '--astryx-theme-ocean-theme-color-status-fill-accent',
104
- );
105
- expect(theme.localTokens).toEqual({
106
- '--astryx-theme-ocean-theme-color-status-fill-accent':
107
- 'light-dark(#0077b6, #48cae4)',
108
- });
109
- expect(theme.__localTokenOwners).toEqual({
110
- '--astryx-theme-ocean-theme-color-status-fill-accent': 'ocean-theme',
111
- });
112
- expect(theme.__localTokenLineage).toEqual(['ocean-theme']);
113
- expect(generateThemeTestCSS(theme)).toContain(
114
- '--astryx-theme-ocean-theme-color-status-fill-accent: light-dark(#0077b6, #48cae4);',
115
- );
116
- });
98
+ });
99
+
100
+ expect(theme.tokens).not.toHaveProperty(token);
101
+ expect(theme.localTokens).toEqual({
102
+ [token]: 'light-dark(#0077b6, #48cae4)',
103
+ });
104
+ expect(theme.__localTokenOwners).toEqual({[token]: themeName});
105
+ expect(theme.__localTokenLineage).toEqual([themeName]);
106
+ expect(generateThemeTestCSS(theme)).toContain(
107
+ `${token}: light-dark(#0077b6, #48cae4);`,
108
+ );
109
+ },
110
+ );
117
111
 
118
112
  it('rejects a name declared in both tokens and localTokens before CSS and token helpers can disagree', () => {
119
113
  const token = '--astryx-theme-ocean-theme-color-status-fill-accent';
@@ -142,9 +136,16 @@ describe('defineTheme', () => {
142
136
  },
143
137
  }),
144
138
  ).toThrow(/both tokens and localTokens/);
139
+
140
+ expect(() =>
141
+ defineTheme({
142
+ name: 'ocean-theme',
143
+ localTokens: {'--color-accent': '#48cae4'},
144
+ }),
145
+ ).toThrow(/both tokens and localTokens/);
145
146
  });
146
147
 
147
- it('does not reinterpret legacy reserved-prefix references without enrollment', () => {
148
+ it('does not reinterpret unenrolled custom-property references', () => {
148
149
  const theme = defineTheme({
149
150
  name: 'legacy',
150
151
  tokens: {
@@ -164,65 +165,43 @@ describe('defineTheme', () => {
164
165
  expect(theme).not.toHaveProperty('__localTokenLineage');
165
166
  });
166
167
 
167
- it.each([
168
- ['Uppercase', '--astryx-theme-Uppercase-color-accent'],
169
- ['wrong-space', '--astryx-theme-other-color-accent'],
170
- ['wrong-space', '--astryx-theme-wrong-space-Color-accent'],
171
- ])('rejects malformed local token enrollment for %s', (name, token) => {
168
+ it('rejects an invalid CSS custom-property name', () => {
172
169
  expect(() =>
173
170
  defineTheme({
174
- name,
175
- localTokens: {[token]: '#123456'},
171
+ name: 'ocean',
172
+ localTokens: {'selection-ink': '#123456'},
176
173
  }),
177
- ).toThrow(/localTokens|local token/);
174
+ ).toThrow(/valid CSS custom-property name/);
178
175
  });
179
176
 
180
- it.each(['VAR', 'vAr'])(
181
- 'rejects undeclared local references using %s() in nested and media component rules',
182
- functionName => {
183
- expect(() =>
184
- defineTheme({
185
- name: 'ocean',
186
- localTokens: {},
187
- components: {
188
- button: {
189
- base: {
190
- ':hover': {
191
- color: `${functionName}(--astryx-theme-ocean-color-missing)`,
192
- },
193
- },
194
- },
195
- },
196
- }),
197
- ).toThrow(/has no declaration/);
198
-
199
- expect(() =>
200
- defineTheme({
201
- name: 'ocean',
202
- localTokens: {},
203
- onDark: {
204
- components: {
205
- badge: {
206
- base: {
207
- color: `${functionName}(--astryx-theme-ocean-color-missing)`,
208
- },
209
- },
177
+ it('treats case-only and unrelated custom-property references as external', () => {
178
+ expect(() =>
179
+ defineTheme({
180
+ name: 'ocean',
181
+ localTokens: {'--Selection-Ink': '#123456'},
182
+ components: {
183
+ button: {
184
+ base: {
185
+ color: 'var(--selection-ink)',
186
+ backgroundColor: 'var(--product-surface)',
210
187
  },
211
188
  },
212
- }),
213
- ).toThrow(/has no declaration/);
214
- },
215
- );
189
+ },
190
+ }),
191
+ ).not.toThrow();
192
+ });
216
193
 
217
194
  it.each(['VAR', 'vAr'])(
218
195
  'rejects cycles between local tokens using %s()',
219
196
  functionName => {
197
+ const escaped = '--\\66 oo';
198
+ const other = '--astryx-theme-cycle-color-b';
220
199
  expect(() =>
221
200
  defineTheme({
222
201
  name: 'cycle',
223
202
  localTokens: {
224
- '--astryx-theme-cycle-color-a': `${functionName}(--astryx-theme-cycle-color-b)`,
225
- '--astryx-theme-cycle-color-b': `${functionName}(--astryx-theme-cycle-color-a)`,
203
+ [escaped]: `${functionName}(${other})`,
204
+ [other]: `${functionName}(${escaped})`,
226
205
  },
227
206
  }),
228
207
  ).toThrow(/cycle detected/);
@@ -1178,28 +1157,28 @@ describe('container padding mapping', () => {
1178
1157
 
1179
1158
  describe('defineTheme extends', () => {
1180
1159
  it('inherits enrollment, allows exact replacement, and owns new child names', () => {
1160
+ const inherited = '--astryx-theme-base-theme-color-status-fill';
1161
+ const childOwned = '--child-surface-raised';
1181
1162
  const base = defineTheme({
1182
1163
  name: 'base-theme',
1183
- localTokens: {
1184
- '--astryx-theme-base-theme-color-status-fill': '#123456',
1185
- },
1164
+ localTokens: {[inherited]: '#123456'},
1186
1165
  });
1187
1166
  const child = defineTheme({
1188
1167
  name: 'child-theme',
1189
1168
  extends: base,
1190
1169
  localTokens: {
1191
- '--astryx-theme-base-theme-color-status-fill': '#654321',
1192
- '--astryx-theme-child-theme-color-surface-raised': '#abcdef',
1170
+ [inherited]: '#654321',
1171
+ [childOwned]: '#abcdef',
1193
1172
  },
1194
1173
  });
1195
1174
 
1196
1175
  expect(child.localTokens).toEqual({
1197
- '--astryx-theme-base-theme-color-status-fill': '#654321',
1198
- '--astryx-theme-child-theme-color-surface-raised': '#abcdef',
1176
+ [inherited]: '#654321',
1177
+ [childOwned]: '#abcdef',
1199
1178
  });
1200
1179
  expect(child.__localTokenOwners).toEqual({
1201
- '--astryx-theme-base-theme-color-status-fill': 'base-theme',
1202
- '--astryx-theme-child-theme-color-surface-raised': 'child-theme',
1180
+ [inherited]: 'base-theme',
1181
+ [childOwned]: 'child-theme',
1203
1182
  });
1204
1183
  expect(child.__localTokenLineage).toEqual(['base-theme', 'child-theme']);
1205
1184
  });
@@ -1235,25 +1214,6 @@ describe('defineTheme extends', () => {
1235
1214
  ]);
1236
1215
  });
1237
1216
 
1238
- it('rejects a new declaration in another theme namespace', () => {
1239
- const base = defineTheme({
1240
- name: 'base-theme',
1241
- localTokens: {
1242
- '--astryx-theme-base-theme-color-status-fill': '#123456',
1243
- },
1244
- });
1245
-
1246
- expect(() =>
1247
- defineTheme({
1248
- name: 'child-theme',
1249
- extends: base,
1250
- localTokens: {
1251
- '--astryx-theme-base-theme-color-new-role': '#abcdef',
1252
- },
1253
- }),
1254
- ).toThrow(/exact namespace/);
1255
- });
1256
-
1257
1217
  it('inherits tokens from base theme', () => {
1258
1218
  const base = defineTheme({
1259
1219
  name: 'base',
@@ -308,12 +308,8 @@ export interface DefineThemeInput {
308
308
  * Values can be a string or [light, dark] tuple.
309
309
  * Only include tokens you want to override; defaults fill the rest. */
310
310
  tokens?: Partial<Record<TokenName, TokenValue>>;
311
- /**
312
- * Theme-family-local CSS custom properties. Keys use the complete
313
- * `--astryx-theme-${name}-...` spelling and values follow `TokenValue`.
314
- * These names are available only to this enrolled theme lineage and do not
315
- * become portable `TokenName` values.
316
- */
311
+ /** Theme-family-local values keyed by any valid CSS custom-property name;
312
+ * prefixes do not establish ownership. */
317
313
  localTokens?: Record<string, TokenValue>;
318
314
  /**
319
315
  * Component style overrides — keyed by component name (lowercase).
@@ -581,9 +577,7 @@ export function defineTheme(input: DefineThemeInput): ResolvedDefinedTheme {
581
577
  input,
582
578
  base,
583
579
  tokens,
584
- components,
585
- __onDark,
586
- __onLight,
580
+ tokenDefaults,
587
581
  );
588
582
 
589
583
  // Adaptations inherit their breakpoint map and ordered rules. Every rule is
@@ -98,7 +98,7 @@ export const derivedVarRegistry: Record<string, DerivedVarEntry[]> = {
98
98
  * the old key still selects the element. Without this the rule would land but
99
99
  * its derived vars would not expand, and the half that travels through a var
100
100
  * (a hover card's radius, a text area's inline padding) would silently do
101
- * nothing. Drop these with the classes, in the next major.
101
+ * nothing. Keep these aliases aligned with the emitted compatibility classes.
102
102
  */
103
103
  const DEPRECATED_REGISTRY_KEYS: Record<string, string> = {
104
104
  hovercard: 'hover-card',
@@ -3,23 +3,22 @@
3
3
  /**
4
4
  * @file localTokens.ts
5
5
  * @input Root and adaptation theme-local token declarations and references
6
- * @output Validated enrollment metadata plus normalized conditional writes
7
- * @position Theme-local namespace owner; final adaptation-cycle validation is
6
+ * @output Validated exact-name ownership metadata plus normalized conditional writes
7
+ * @position Theme-local ownership boundary; final adaptation-cycle validation is
8
8
  * performed after the reachable ordered cascade is assembled.
9
9
  */
10
10
 
11
- import type {ComponentStyleMap, DefinedTheme, TokenValue} from './defineTheme';
12
- import type {ResolvedOnMedia} from './onMediaTokens';
11
+ import type {DefinedTheme, TokenValue} from './defineTheme';
13
12
 
14
- const LOCAL_TOKEN_PREFIX = '--astryx-theme-';
15
- const THEME_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
16
- const LOCAL_TOKEN_SUFFIX_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
17
- const CSS_VAR_PATTERN = /var\(\s*(--[^,\s)]+)/gi;
18
-
19
- /** Whether a custom-property name belongs to Astryx's reserved local namespace. */
20
- export function isReservedThemeLocalTokenName(name: string): boolean {
21
- return name.startsWith(LOCAL_TOKEN_PREFIX);
22
- }
13
+ const CSS_NAME_CHARACTER = String.raw`(?:[-_a-zA-Z0-9]|\P{ASCII}|\\(?:[0-9a-fA-F]{1,6}[ \t\r\n\f]?|[^\r\n\f]))`;
14
+ const CSS_CUSTOM_PROPERTY_NAME_PATTERN = new RegExp(
15
+ `^--${CSS_NAME_CHARACTER}*$`,
16
+ 'u',
17
+ );
18
+ const CSS_VAR_PATTERN = new RegExp(
19
+ `var\\(\\s*(--${CSS_NAME_CHARACTER}*)(?=\\s*(?:,|\\)))`,
20
+ 'giu',
21
+ );
23
22
 
24
23
  export interface ResolvedLocalTokenContract {
25
24
  localTokens: Record<string, string>;
@@ -31,12 +30,8 @@ function hasOwn(object: object, key: PropertyKey): boolean {
31
30
  return Object.prototype.hasOwnProperty.call(object, key);
32
31
  }
33
32
 
34
- function isExactLocalTokenName(name: string, owner: string): boolean {
35
- const prefix = `${LOCAL_TOKEN_PREFIX}${owner}-`;
36
- return (
37
- name.startsWith(prefix) &&
38
- LOCAL_TOKEN_SUFFIX_PATTERN.test(name.slice(prefix.length))
39
- );
33
+ function isValidCSSCustomPropertyName(name: string): boolean {
34
+ return !name.includes('\0') && CSS_CUSTOM_PROPERTY_NAME_PATTERN.test(name);
40
35
  }
41
36
 
42
37
  function resolveTokenValue(value: TokenValue, path: string): string {
@@ -84,16 +79,6 @@ function collectCustomPropertyReferences(
84
79
  }
85
80
  }
86
81
 
87
- function collectLocalReferences(value: unknown, refs: Set<string>): void {
88
- const customProperties = new Set<string>();
89
- collectCustomPropertyReferences(value, customProperties);
90
- for (const name of customProperties) {
91
- if (isReservedThemeLocalTokenName(name)) {
92
- refs.add(name);
93
- }
94
- }
95
- }
96
-
97
82
  export function assertNoTokenCycles(
98
83
  tokenValues: Record<string, string>,
99
84
  context?: string,
@@ -208,27 +193,6 @@ export function assertNoTokenCycles(
208
193
  }
209
194
  }
210
195
 
211
- function assertDeclaredReferences(
212
- localTokens: Record<string, string>,
213
- components: ComponentStyleMap | undefined,
214
- onDark: ResolvedOnMedia | undefined,
215
- onLight: ResolvedOnMedia | undefined,
216
- ): void {
217
- const refs = new Set<string>();
218
- collectLocalReferences(localTokens, refs);
219
- collectLocalReferences(components, refs);
220
- collectLocalReferences(onDark?.components, refs);
221
- collectLocalReferences(onLight?.components, refs);
222
-
223
- for (const reference of refs) {
224
- if (!hasOwn(localTokens, reference)) {
225
- throw new Error(
226
- `Theme-local token reference "${reference}" has no declaration in the enrolled theme lineage.`,
227
- );
228
- }
229
- }
230
- }
231
-
232
196
  function assertInheritedContract(
233
197
  themeName: string,
234
198
  base: DefinedTheme,
@@ -267,7 +231,7 @@ function assertInheritedContract(
267
231
  typeof value !== 'string' ||
268
232
  !owner ||
269
233
  !lineage.includes(owner) ||
270
- !isExactLocalTokenName(name, owner)
234
+ !isValidCSSCustomPropertyName(name)
271
235
  ) {
272
236
  throw new Error(
273
237
  `defineTheme("${themeName}"): inherited local token "${name}" does not match its exact lineage metadata.`,
@@ -287,7 +251,7 @@ function assertInheritedContract(
287
251
  * Resolve and validate the opt-in theme-local token contract.
288
252
  *
289
253
  * Themes that omit `localTokens` and do not extend an enrolled base bypass this
290
- * function's reserved-namespace checks so legacy token behavior stays intact.
254
+ * function's exact owner and lineage checks so legacy token behavior stays intact.
291
255
  */
292
256
  export function resolveLocalTokenContract(
293
257
  input: {
@@ -296,9 +260,7 @@ export function resolveLocalTokenContract(
296
260
  },
297
261
  base: DefinedTheme | undefined,
298
262
  tokens: Record<string, string>,
299
- components: ComponentStyleMap | undefined,
300
- onDark: ResolvedOnMedia | undefined,
301
- onLight: ResolvedOnMedia | undefined,
263
+ portableTokens: Readonly<Record<string, string>>,
302
264
  ): ResolvedLocalTokenContract | undefined {
303
265
  const directlyEnrolled = hasOwn(input, 'localTokens');
304
266
  const inherited = base?.__localTokenLineage !== undefined;
@@ -307,12 +269,6 @@ export function resolveLocalTokenContract(
307
269
  return undefined;
308
270
  }
309
271
 
310
- if (directlyEnrolled && !THEME_NAME_PATTERN.test(input.name)) {
311
- throw new Error(
312
- `defineTheme("${input.name}"): themes using localTokens require a stable lower-kebab name.`,
313
- );
314
- }
315
-
316
272
  if (inherited && base) {
317
273
  assertInheritedContract(input.name, base);
318
274
  }
@@ -335,13 +291,12 @@ export function resolveLocalTokenContract(
335
291
 
336
292
  for (const [name, value] of Object.entries(declarations ?? {})) {
337
293
  const inheritedOwner = owners[name];
294
+ if (!isValidCSSCustomPropertyName(name)) {
295
+ throw new Error(
296
+ `defineTheme("${input.name}"): local token "${name}" must be a valid CSS custom-property name.`,
297
+ );
298
+ }
338
299
  if (!inheritedOwner) {
339
- const expectedPrefix = `${LOCAL_TOKEN_PREFIX}${input.name}-`;
340
- if (!isExactLocalTokenName(name, input.name)) {
341
- throw new Error(
342
- `defineTheme("${input.name}"): local token "${name}" must use the exact namespace "${expectedPrefix}" followed by a lowercase kebab-case purpose.`,
343
- );
344
- }
345
300
  owners[name] = input.name;
346
301
  }
347
302
  localTokens[name] = resolveTokenValue(
@@ -356,14 +311,13 @@ export function resolveLocalTokenContract(
356
311
  `defineTheme("${input.name}"): inherited local token "${name}" has no owner metadata.`,
357
312
  );
358
313
  }
359
- if (hasOwn(tokens, name)) {
314
+ if (hasOwn(tokens, name) || hasOwn(portableTokens, name)) {
360
315
  throw new Error(
361
316
  `defineTheme("${input.name}"): token "${name}" cannot be declared in both tokens and localTokens.`,
362
317
  );
363
318
  }
364
319
  }
365
320
 
366
- assertDeclaredReferences(localTokens, components, onDark, onLight);
367
321
  assertNoTokenCycles(localTokens, `defineTheme("${input.name}").localTokens`);
368
322
 
369
323
  return {
@@ -377,29 +331,17 @@ export function resolveLocalTokenContract(
377
331
  * Resolve theme-local values written by one adaptation rule.
378
332
  *
379
333
  * Adaptations may replace names already enrolled by the root theme lineage, but
380
- * they never enroll names of their own. This validates one rule's references;
381
- * cycle validation waits until all co-matching writes have cascaded in order.
334
+ * they never enroll names of their own. Cycle validation waits until all
335
+ * co-matching writes have cascaded in order.
382
336
  */
383
337
  export function resolveAdaptationLocalTokens(
384
338
  themeName: string,
385
339
  ruleIndex: number,
386
340
  declarations: Record<string, TokenValue> | undefined,
387
341
  rootLocalTokens: Record<string, string> | undefined,
388
- tokens: Record<string, string>,
389
- components: ComponentStyleMap | undefined,
390
342
  ): Record<string, string> | undefined {
391
343
  const path = `defineTheme("${themeName}").adaptations.rules[${ruleIndex}].value.localTokens`;
392
344
  if (declarations === undefined) {
393
- const refs = new Set<string>();
394
- collectLocalReferences(tokens, refs);
395
- collectLocalReferences(components, refs);
396
- for (const reference of refs) {
397
- if (!rootLocalTokens || !hasOwn(rootLocalTokens, reference)) {
398
- throw new Error(
399
- `${path}: theme-local token reference "${reference}" has no declaration in the enrolled root theme lineage.`,
400
- );
401
- }
402
- }
403
345
  return undefined;
404
346
  }
405
347
  if (
@@ -420,18 +362,5 @@ export function resolveAdaptationLocalTokens(
420
362
  resolved[name] = resolveTokenValue(value, `${path}["${name}"]`);
421
363
  }
422
364
 
423
- const effective = {...rootLocalTokens, ...resolved};
424
- const refs = new Set<string>();
425
- collectLocalReferences(resolved, refs);
426
- collectLocalReferences(tokens, refs);
427
- collectLocalReferences(components, refs);
428
- for (const reference of refs) {
429
- if (!hasOwn(effective, reference)) {
430
- throw new Error(
431
- `${path}: theme-local token reference "${reference}" has no declaration in the enrolled root theme lineage.`,
432
- );
433
- }
434
- }
435
-
436
365
  return Object.keys(resolved).length > 0 ? resolved : undefined;
437
366
  }
@@ -29,9 +29,9 @@ const theme = defineTheme({
29
29
  const selector = '.astryx-button[data-variant="primary"]';
30
30
 
31
31
  describe('public theme helper selector contract', () => {
32
- it('keeps themeProps shape while removing bare value classes', () => {
32
+ it('keeps canonical data attributes alongside bare compatibility classes', () => {
33
33
  expect(themeProps('button', {variant: 'primary', size: 'sm'})).toEqual({
34
- className: 'astryx-button',
34
+ className: 'astryx-button primary sm',
35
35
  'data-variant': 'primary',
36
36
  'data-size': 'sm',
37
37
  });
@@ -443,50 +443,24 @@ describe('theme-local adaptation values', () => {
443
443
  ).toThrow(/write it through value.localTokens/);
444
444
  });
445
445
 
446
- it('rejects reserved local-token names through value.tokens even when unenrolled', () => {
447
- expect(() =>
448
- defineTheme({
449
- name: 'reserved-rule-token',
450
- adaptations: {
451
- rules: [
452
- {
453
- when: {pointer: 'coarse'},
454
- value: {
455
- tokens: {
456
- '--astryx-theme-unrelated-owner-control-height': '44px',
457
- },
458
- },
459
- },
460
- ],
461
- },
462
- } as unknown as DefineThemeInput),
463
- ).toThrow(/reserved.*value\.localTokens/i);
464
- });
446
+ it('treats an unenrolled prefix-like adaptation token as portable', () => {
447
+ const name = '--astryx-theme-unrelated-owner-control-height';
448
+ const theme = defineTheme({
449
+ name: 'external-rule-token',
450
+ adaptations: {
451
+ rules: [
452
+ {
453
+ when: {pointer: 'coarse'},
454
+ value: {tokens: {[name]: '44px'}},
455
+ },
456
+ ],
457
+ },
458
+ } as unknown as DefineThemeInput);
465
459
 
466
- it('rejects undeclared references and conditional cycles', () => {
467
- expect(() =>
468
- defineTheme({
469
- name: 'local-root',
470
- localTokens: {[localName]: '32px'},
471
- adaptations: {
472
- rules: [
473
- {
474
- when: {pointer: 'coarse'},
475
- value: {
476
- components: {
477
- button: {
478
- base: {
479
- minHeight: 'var(--astryx-theme-local-root-missing-name)',
480
- },
481
- },
482
- },
483
- },
484
- },
485
- ],
486
- },
487
- }),
488
- ).toThrow(/has no declaration/);
460
+ expect(theme.__adaptationRules?.[0].tokens[name]).toBe('44px');
461
+ });
489
462
 
463
+ it('rejects conditional local-token cycles', () => {
490
464
  const a = '--astryx-theme-cycle-local-a';
491
465
  const b = '--astryx-theme-cycle-local-b';
492
466
  expect(() =>
@@ -27,11 +27,7 @@ import type {MotionScaleConfig} from './expandMotionScale';
27
27
  import type {RadiusScaleConfig} from './expandRadiusScale';
28
28
  import type {ColorScaleConfig} from './expandColorScale';
29
29
  import {resolveThemeValues, type ThemeValuesInput} from './resolveThemeValues';
30
- import {
31
- assertNoTokenCycles,
32
- isReservedThemeLocalTokenName,
33
- resolveAdaptationLocalTokens,
34
- } from './localTokens';
30
+ import {assertNoTokenCycles, resolveAdaptationLocalTokens} from './localTokens';
35
31
 
36
32
  // =============================================================================
37
33
  // Public authoring vocabulary
@@ -928,9 +924,12 @@ export function resolveThemeAdaptationRules(
928
924
  }
929
925
 
930
926
  for (const name of Object.keys(rule.value.tokens ?? {})) {
931
- if (isReservedThemeLocalTokenName(name)) {
927
+ if (
928
+ rootLocalTokens &&
929
+ Object.prototype.hasOwnProperty.call(rootLocalTokens, name)
930
+ ) {
932
931
  throw new Error(
933
- `defineTheme("${themeName}").adaptations.rules[${index}].value.tokens["${name}"] uses the reserved --astryx-theme-* namespace; write it through value.localTokens instead.`,
932
+ `defineTheme("${themeName}").adaptations.rules[${index}].value.tokens["${name}"] matches an enrolled theme-local declaration; write it through value.localTokens instead.`,
934
933
  );
935
934
  }
936
935
  }
@@ -940,8 +939,6 @@ export function resolveThemeAdaptationRules(
940
939
  index,
941
940
  rule.value.localTokens,
942
941
  rootLocalTokens,
943
- resolved.tokens,
944
- resolved.components,
945
942
  );
946
943
 
947
944
  return {