@astryxdesign/core 0.1.3 → 0.1.4-canary.02643db

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 (168) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/Button/Button.d.ts.map +1 -1
  3. package/dist/Button/Button.js +29 -8
  4. package/dist/Calendar/Calendar.d.ts.map +1 -1
  5. package/dist/Calendar/Calendar.js +49 -20
  6. package/dist/Calendar/dayCellUtils.d.ts +67 -5
  7. package/dist/Calendar/dayCellUtils.d.ts.map +1 -1
  8. package/dist/Calendar/dayCellUtils.js +139 -18
  9. package/dist/CheckboxInput/CheckboxInput.d.ts +6 -1
  10. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  11. package/dist/CheckboxInput/CheckboxInput.js +7 -1
  12. package/dist/Code/Code.d.ts +15 -1
  13. package/dist/Code/Code.d.ts.map +1 -1
  14. package/dist/Code/Code.js +32 -2
  15. package/dist/Code/index.d.ts +1 -1
  16. package/dist/Code/index.d.ts.map +1 -1
  17. package/dist/CodeBlock/index.d.ts +1 -1
  18. package/dist/CodeBlock/index.d.ts.map +1 -1
  19. package/dist/CommandPalette/CommandPaletteEmpty.d.ts +1 -1
  20. package/dist/CommandPalette/CommandPaletteEmpty.d.ts.map +1 -1
  21. package/dist/CommandPalette/CommandPaletteEmpty.js +24 -4
  22. package/dist/ContextMenu/ContextMenu.d.ts +9 -1
  23. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  24. package/dist/ContextMenu/ContextMenu.js +6 -3
  25. package/dist/DateRangeInput/DateRangeInput.js +2 -2
  26. package/dist/DateTimeInput/DateTimeInput.d.ts +0 -5
  27. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  28. package/dist/Divider/Divider.d.ts +0 -22
  29. package/dist/Divider/Divider.d.ts.map +1 -1
  30. package/dist/Markdown/parser.d.ts +7 -0
  31. package/dist/Markdown/parser.d.ts.map +1 -1
  32. package/dist/Markdown/parser.js +300 -6
  33. package/dist/MultiSelector/MultiSelector.d.ts +7 -1
  34. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  35. package/dist/MultiSelector/MultiSelector.js +10 -1
  36. package/dist/RadioList/RadioList.d.ts +7 -1
  37. package/dist/RadioList/RadioList.d.ts.map +1 -1
  38. package/dist/RadioList/RadioList.js +3 -1
  39. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  40. package/dist/RadioList/RadioListItem.js +6 -1
  41. package/dist/Selector/Selector.d.ts +6 -0
  42. package/dist/Selector/Selector.d.ts.map +1 -1
  43. package/dist/Selector/Selector.js +9 -0
  44. package/dist/Slider/Slider.d.ts +6 -0
  45. package/dist/Slider/Slider.d.ts.map +1 -1
  46. package/dist/Slider/Slider.js +10 -1
  47. package/dist/Spinner/Spinner.d.ts.map +1 -1
  48. package/dist/Spinner/Spinner.js +1 -1
  49. package/dist/Switch/Switch.d.ts +6 -1
  50. package/dist/Switch/Switch.d.ts.map +1 -1
  51. package/dist/Switch/Switch.js +7 -1
  52. package/dist/Table/BaseTable.d.ts.map +1 -1
  53. package/dist/Table/BaseTable.js +8 -1
  54. package/dist/Table/TableCell.d.ts.map +1 -1
  55. package/dist/Table/TableCell.js +73 -4
  56. package/dist/Table/index.d.ts +2 -0
  57. package/dist/Table/index.d.ts.map +1 -1
  58. package/dist/Table/index.js +1 -0
  59. package/dist/Table/plugins/rowExpansion/index.d.ts +3 -0
  60. package/dist/Table/plugins/rowExpansion/index.d.ts.map +1 -0
  61. package/dist/Table/plugins/rowExpansion/index.js +3 -0
  62. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts +90 -0
  63. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -0
  64. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +398 -0
  65. package/dist/Table/tableContextMenu.d.ts +5 -1
  66. package/dist/Table/tableContextMenu.d.ts.map +1 -1
  67. package/dist/Table/tableContextMenu.js +10 -3
  68. package/dist/Text/Text.d.ts +1 -1
  69. package/dist/Text/Text.d.ts.map +1 -1
  70. package/dist/Text/Text.js +4 -3
  71. package/dist/Text/text.stylex.d.ts +35 -0
  72. package/dist/Text/text.stylex.d.ts.map +1 -1
  73. package/dist/Text/text.stylex.js +52 -1
  74. package/dist/Tokenizer/Tokenizer.d.ts +6 -1
  75. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  76. package/dist/Tokenizer/Tokenizer.js +10 -1
  77. package/dist/astryx.css +16 -0
  78. package/dist/astryx.umd.js +51 -49
  79. package/dist/astryx.umd.js.map +4 -4
  80. package/dist/docs-types.d.ts +12 -0
  81. package/dist/docs-types.d.ts.map +1 -1
  82. package/package.json +1 -1
  83. package/src/AspectRatio/AspectRatio.doc.mjs +1 -1
  84. package/src/Avatar/Avatar.doc.mjs +2 -2
  85. package/src/Button/Button.tsx +39 -6
  86. package/src/ButtonGroup/ButtonGroup.doc.mjs +1 -1
  87. package/src/Calendar/Calendar.test.tsx +106 -0
  88. package/src/Calendar/Calendar.tsx +59 -22
  89. package/src/Calendar/dayCellUtils.test.ts +306 -1
  90. package/src/Calendar/dayCellUtils.ts +206 -13
  91. package/src/Card/Card.doc.mjs +1 -1
  92. package/src/Chat/useChatNewMessages.test.tsx +4 -4
  93. package/src/CheckboxInput/CheckboxInput.doc.mjs +9 -1
  94. package/src/CheckboxInput/CheckboxInput.test.tsx +36 -0
  95. package/src/CheckboxInput/CheckboxInput.tsx +11 -0
  96. package/src/CheckboxList/CheckboxList.doc.mjs +1 -1
  97. package/src/Code/Code.test.tsx +54 -0
  98. package/src/Code/Code.tsx +48 -4
  99. package/src/Code/index.ts +1 -1
  100. package/src/CodeBlock/index.ts +1 -1
  101. package/src/CommandPalette/CommandPaletteEmpty.tsx +15 -1
  102. package/src/ContextMenu/ContextMenu.tsx +17 -1
  103. package/src/DateInput/DateInput.doc.mjs +6 -1
  104. package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
  105. package/src/DateRangeInput/DateRangeInput.tsx +2 -2
  106. package/src/DateTimeInput/DateTimeInput.doc.mjs +1 -1
  107. package/src/DateTimeInput/DateTimeInput.tsx +1 -6
  108. package/src/Divider/Divider.tsx +1 -23
  109. package/src/DropdownMenu/DropdownMenu.test.tsx +5 -3
  110. package/src/EmptyState/EmptyState.doc.mjs +1 -1
  111. package/src/Field/Field.doc.mjs +1 -1
  112. package/src/FileInput/FileInput.doc.mjs +1 -1
  113. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -3
  114. package/src/Grid/Grid.doc.mjs +3 -3
  115. package/src/Layout/__tests__/edgeCompensation.test.tsx +72 -0
  116. package/src/Markdown/Markdown.test.tsx +23 -0
  117. package/src/Markdown/incremental.test.ts +36 -0
  118. package/src/Markdown/parser.test.ts +210 -0
  119. package/src/Markdown/parser.ts +301 -6
  120. package/src/MobileNav/MobileNav.doc.mjs +20 -0
  121. package/src/MultiSelector/MultiSelector.doc.mjs +14 -1
  122. package/src/MultiSelector/MultiSelector.test.tsx +33 -0
  123. package/src/MultiSelector/MultiSelector.tsx +20 -0
  124. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  125. package/src/RadioList/RadioList.doc.mjs +7 -1
  126. package/src/RadioList/RadioList.test.tsx +46 -0
  127. package/src/RadioList/RadioList.tsx +10 -1
  128. package/src/RadioList/RadioListItem.tsx +4 -0
  129. package/src/Section/Section.test.tsx +4 -10
  130. package/src/SegmentedControl/SegmentedControl.doc.mjs +1 -1
  131. package/src/Selector/Selector.doc.mjs +12 -1
  132. package/src/Selector/Selector.test.tsx +30 -0
  133. package/src/Selector/Selector.tsx +18 -0
  134. package/src/Slider/Slider.doc.mjs +14 -2
  135. package/src/Slider/Slider.test.tsx +30 -0
  136. package/src/Slider/Slider.tsx +20 -0
  137. package/src/Spinner/Spinner.tsx +1 -0
  138. package/src/Switch/Switch.doc.mjs +14 -2
  139. package/src/Switch/Switch.test.tsx +38 -0
  140. package/src/Switch/Switch.tsx +12 -0
  141. package/src/TabList/TabList.doc.mjs +3 -3
  142. package/src/Table/BaseTable.tsx +9 -1
  143. package/src/Table/Table.test.tsx +23 -0
  144. package/src/Table/TableCell.tsx +87 -8
  145. package/src/Table/index.ts +5 -0
  146. package/src/Table/plugins/rowExpansion/index.ts +7 -0
  147. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +155 -0
  148. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +590 -0
  149. package/src/Table/tableContextMenu.test.tsx +27 -3
  150. package/src/Table/tableContextMenu.tsx +20 -2
  151. package/src/Table/useTablePagination.doc.mjs +4 -1
  152. package/src/Table/useTableRowExpansion.doc.mjs +80 -0
  153. package/src/Table/useTableSortable.doc.mjs +4 -1
  154. package/src/Text/Text.doc.mjs +2 -2
  155. package/src/Text/Text.test.tsx +11 -0
  156. package/src/Text/Text.tsx +4 -2
  157. package/src/Text/text.stylex.ts +41 -0
  158. package/src/TextArea/TextArea.doc.mjs +1 -1
  159. package/src/TextInput/TextInput.doc.mjs +1 -1
  160. package/src/TimeInput/TimeInput.doc.mjs +11 -6
  161. package/src/Tokenizer/Tokenizer.doc.mjs +14 -2
  162. package/src/Tokenizer/Tokenizer.test.tsx +33 -0
  163. package/src/Tokenizer/Tokenizer.tsx +19 -0
  164. package/src/Toolbar/Toolbar.test.tsx +1 -1
  165. package/src/Typeahead/BaseTypeahead.doc.mjs +10 -0
  166. package/src/Typeahead/Typeahead.doc.mjs +6 -1
  167. package/src/docs-types.ts +12 -0
  168. package/src/hooks/useKeyboardHint.doc.mjs +6 -6
@@ -0,0 +1,80 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../docs-types').ComponentDoc} */
4
+
5
+ export const docs = {
6
+ name: 'useTableRowExpansion',
7
+ subComponentOf: 'Table',
8
+ displayName: 'useTableRowExpansion',
9
+ description:
10
+ 'Hook that returns a TablePlugin implementing expandable rows with inherited columns. Child rows use the same columns as their parents, indented by depth. Clicking the chevron (or right-click context menu) toggles expansion. Pair with useTableRowExpansionState, which flattens the tree and derives this config (expand/collapse handlers + expand-all state) from a single expandedKeys set.',
11
+ props: [
12
+ {
13
+ name: 'expandedKeys',
14
+ type: 'Set<string>',
15
+ description: 'Set of currently-expanded row keys.',
16
+ required: true,
17
+ },
18
+ {
19
+ name: 'onToggle',
20
+ type: '(key: string) => void',
21
+ description: 'Called when a row expansion is toggled.',
22
+ required: true,
23
+ },
24
+ {
25
+ name: 'getRowKey',
26
+ type: '(item: T) => string',
27
+ description: 'Derive a stable unique key from a row item.',
28
+ required: true,
29
+ },
30
+ {
31
+ name: 'getChildren',
32
+ type: '(item: T) => T[]',
33
+ description: 'Return the children of a row (determines expandability).',
34
+ required: true,
35
+ },
36
+ {
37
+ name: 'getDepth',
38
+ type: '(item: T) => number',
39
+ description: 'Return the depth of a row in the hierarchy (0 = top-level). Used for indentation.',
40
+ },
41
+ {
42
+ name: 'getIsItemExpandable',
43
+ type: '(item: T) => boolean',
44
+ description: 'Control which rows are expandable. Defaults to checking getChildren length.',
45
+ },
46
+ {
47
+ name: 'hasRowClickExpansion',
48
+ type: 'boolean',
49
+ description: 'When true, clicking anywhere on the row toggles expansion.',
50
+ default: 'false',
51
+ },
52
+ {
53
+ name: 'isAllExpanded',
54
+ type: "boolean | 'indeterminate'",
55
+ description: 'State of the expand-all toggle in the header. Enables the header toggle button.',
56
+ },
57
+ {
58
+ name: 'onToggleExpandAll',
59
+ type: '(expand: boolean) => void',
60
+ description: 'Callback when the expand-all header toggle is clicked.',
61
+ },
62
+ ],
63
+ };
64
+
65
+ /** @type {import('../docs-types').TranslationDoc} */
66
+ export const docsDense = {
67
+ description:
68
+ 'Returns a TablePlugin for expandable rows w/ inherited columns. Child rows reuse parent columns, indented by depth. Chevron click (or right-click menu) toggles expansion. Pair w/ useTableRowExpansionState, which flattens the tree + derives this config from one expandedKeys set.',
69
+ propDescriptions: {
70
+ expandedKeys: 'Set of currently-expanded row keys.',
71
+ onToggle: 'Called when a row expansion is toggled.',
72
+ getRowKey: 'Derive a stable unique key from a row item.',
73
+ getChildren: 'Return children of a row; determines expandability.',
74
+ getDepth: 'Return depth of a row (0 = top-level). Used for indentation.',
75
+ getIsItemExpandable: 'Control which rows are expandable. Defaults to checking getChildren length.',
76
+ hasRowClickExpansion: 'If true, clicking anywhere on the row toggles expansion. Default false.',
77
+ isAllExpanded: 'State of the expand-all header toggle. Enables the header toggle button.',
78
+ onToggleExpandAll: 'Callback when the expand-all header toggle is clicked.',
79
+ },
80
+ };
@@ -6,7 +6,10 @@ export const docs = {
6
6
  name: 'useTableSortable',
7
7
  subComponentOf: 'Table',
8
8
  displayName: 'useTableSortable',
9
- description: 'Headless multi-sort plugin for Table. The consumer owns sort state and provides a callback. Shift+click enables secondary sort columns. Sort indicators render in header cells automatically.',
9
+ description: 'Headless multi-sort plugin for Table. Call with a config object: `useTableSortable({ sort, onSortChange })`. Returns a TablePlugin to pass to `<Table plugins={{ sort: sortPlugin }} />`.',
10
+ usage: {
11
+ description: 'Call useTableSortable with a config object containing sort state and callback. Pass the returned plugin to Table via the plugins prop.',
12
+ },
10
13
  props: [
11
14
  {
12
15
  name: 'sort',
@@ -16,7 +16,7 @@ export const docs = {
16
16
  theming: {
17
17
  targets: [
18
18
  {className: 'astryx-heading', visualProps: ['level', 'color']},
19
- {className: 'astryx-text', visualProps: ['type', 'color']},
19
+ {className: 'astryx-text', visualProps: ['type', 'size', 'color']},
20
20
  ],
21
21
  },
22
22
  description: 'Semantic body text component that renders text with type-based styling from the theme, with optional truncation, decoration, and layout props.',
@@ -174,4 +174,4 @@ export const docsDense = {
174
174
  { guidance: false, description: 'Text for headings: use Heading with level (1\u20136).' },
175
175
  ],
176
176
  },
177
- };
177
+ };
@@ -114,6 +114,17 @@ describe('Text', () => {
114
114
  expect(screen.getByText('Bold text')).toBeInTheDocument();
115
115
  });
116
116
 
117
+ it('reflects explicit size overrides for styling and theming', () => {
118
+ render(
119
+ <Text type="code" size="2xs">
120
+ Tiny code
121
+ </Text>,
122
+ );
123
+ const element = screen.getByText('Tiny code');
124
+ expect(element).toHaveAttribute('data-size', '2xs');
125
+ expect(element.className).toContain('size-2xs');
126
+ });
127
+
117
128
  it('accepts display prop', () => {
118
129
  render(
119
130
  <Text type="body" display="block">
package/src/Text/Text.tsx CHANGED
@@ -32,6 +32,7 @@ import type {
32
32
  import {
33
33
  colorStyles,
34
34
  defaultWeightByTypeStyles,
35
+ sizeStyles,
35
36
  sizeByTypeStyles,
36
37
  weightStyles,
37
38
  displayStyles,
@@ -201,7 +202,7 @@ function resolveStyleType(type: TextType): BuiltinTextType {
201
202
  */
202
203
  export function Text({
203
204
  type = 'body',
204
- size: _size,
205
+ size,
205
206
  color,
206
207
  weight,
207
208
  display = 'inline',
@@ -254,10 +255,11 @@ export function Text({
254
255
  <Component
255
256
  ref={mergeRefs(ref, truncation.ref, textRef)}
256
257
  {...mergeProps(
257
- themeProps('text', {type, color: resolvedColor}),
258
+ themeProps('text', {type, size, color: resolvedColor}),
258
259
  stylex.props(
259
260
  colorStyles[resolvedColor],
260
261
  sizeByTypeStyles[styleType],
262
+ size && sizeStyles[size],
261
263
  defaultWeightByTypeStyles[styleType],
262
264
  weight && weightStyles[weight],
263
265
  // Display: use truncation styles when maxLines > 0
@@ -14,6 +14,7 @@ import * as stylex from '@stylexjs/stylex';
14
14
  import {
15
15
  colorVars,
16
16
  fontWeightVars,
17
+ textSizeVars,
17
18
  typeScaleVars,
18
19
  typographyVars,
19
20
  } from '../theme/tokens.stylex';
@@ -145,6 +146,46 @@ export const sizeByTypeStyles = stylex.create({
145
146
  },
146
147
  });
147
148
 
149
+ // =============================================================================
150
+ // Explicit Size Override
151
+ // =============================================================================
152
+
153
+ export const sizeStyles = stylex.create({
154
+ '4xs': {
155
+ fontSize: textSizeVars['--font-size-4xs'],
156
+ },
157
+ '3xs': {
158
+ fontSize: textSizeVars['--font-size-3xs'],
159
+ },
160
+ '2xs': {
161
+ fontSize: textSizeVars['--font-size-2xs'],
162
+ },
163
+ xsm: {
164
+ fontSize: textSizeVars['--font-size-xs'],
165
+ },
166
+ sm: {
167
+ fontSize: textSizeVars['--font-size-sm'],
168
+ },
169
+ base: {
170
+ fontSize: textSizeVars['--font-size-base'],
171
+ },
172
+ lg: {
173
+ fontSize: textSizeVars['--font-size-lg'],
174
+ },
175
+ xl: {
176
+ fontSize: textSizeVars['--font-size-xl'],
177
+ },
178
+ '2xl': {
179
+ fontSize: textSizeVars['--font-size-2xl'],
180
+ },
181
+ '3xl': {
182
+ fontSize: textSizeVars['--font-size-3xl'],
183
+ },
184
+ '4xl': {
185
+ fontSize: textSizeVars['--font-size-4xl'],
186
+ },
187
+ });
188
+
148
189
  // =============================================================================
149
190
  // Baseline Size/Leading by Heading Level (from type-scale tokens)
150
191
  //
@@ -74,7 +74,7 @@ export const docs = {
74
74
  name: 'disabledMessage',
75
75
  type: 'string',
76
76
  description:
77
- 'Explains why the textarea is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the textarea focusable via aria-disabled (the field becomes read-only). Use this instead of wrapping a disabled TextArea in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
77
+ 'Explains why the textarea is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the textarea focusable via aria-disabled (the field becomes read-only). Use this instead of wrapping a disabled TextArea in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
78
78
  },
79
79
  {
80
80
  name: 'isLoading',
@@ -82,7 +82,7 @@ export const docs = {
82
82
  name: 'disabledMessage',
83
83
  type: 'string',
84
84
  description:
85
- 'Explains why the input is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the input focusable via aria-disabled (the field becomes read-only). Use this instead of wrapping a disabled TextInput in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
85
+ 'Explains why the input is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the input focusable via aria-disabled (the field becomes read-only). Use this instead of wrapping a disabled TextInput in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
86
86
  },
87
87
  {
88
88
  name: 'isLoading',
@@ -26,7 +26,7 @@ export const docs = {
26
26
  {
27
27
  guidance: true,
28
28
  description:
29
- 'Choose the hour format (12h or 24h) that matches your audience\'s locale; 12-hour with AM/PM for US-centric UIs, 24-hour for international or technical contexts.',
29
+ 'Choose the hour format (12h or 24h) that matches your audience\'s locale: 12-hour with AM/PM for US-centric UIs, 24-hour for international or technical contexts.',
30
30
  },
31
31
  {
32
32
  guidance: true,
@@ -41,7 +41,7 @@ export const docs = {
41
41
  {
42
42
  guidance: true,
43
43
  description:
44
- 'Use the status prop to surface validation errors inline; show a message like "Time must be during business hours" so users know exactly what to fix.',
44
+ 'Use the status prop to surface validation errors inline: show a message like "Time must be during business hours" so users know exactly what to fix.',
45
45
  },
46
46
  {
47
47
  guidance: true,
@@ -146,7 +146,7 @@ export const docs = {
146
146
  name: 'disabledMessage',
147
147
  type: 'string',
148
148
  description:
149
- 'Explains why the input is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the field focusable via aria-disabled (activation stays blocked). Use this instead of wrapping a disabled TimeInput in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
149
+ 'Explains why the input is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the field focusable via aria-disabled (activation stays blocked). Use this instead of wrapping a disabled TimeInput in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
150
150
  },
151
151
  {
152
152
  name: 'value',
@@ -396,7 +396,7 @@ export const docsZh = {
396
396
  {
397
397
  guidance: true,
398
398
  description:
399
- 'Choose the hour format (12h or 24h) that matches your audience\'s locale; 12-hour with AM/PM for US-centric UIs, 24-hour for international or technical contexts.',
399
+ 'Choose the hour format (12h or 24h) that matches your audience\'s locale: 12-hour with AM/PM for US-centric UIs, 24-hour for international or technical contexts.',
400
400
  },
401
401
  {
402
402
  guidance: true,
@@ -411,7 +411,7 @@ export const docsZh = {
411
411
  {
412
412
  guidance: true,
413
413
  description:
414
- 'Use the status prop to surface validation errors inline; show a message like "Time must be during business hours" so users know exactly what to fix.',
414
+ 'Use the status prop to surface validation errors inline: show a message like "Time must be during business hours" so users know exactly what to fix.',
415
415
  },
416
416
  {
417
417
  guidance: true,
@@ -480,7 +480,7 @@ export const docsDense = {
480
480
  {
481
481
  guidance: true,
482
482
  description:
483
- 'Choose hour format (12h/24h) to match locale; 12h for US, 24h for international.',
483
+ 'Choose hour format (12h/24h) to match locale: 12h for US, 24h for international.',
484
484
  },
485
485
  {
486
486
  guidance: true,
@@ -497,6 +497,11 @@ export const docsDense = {
497
497
  description: 'Use status prop for inline validation errors.',
498
498
  },
499
499
  {guidance: true, description: 'Enable hasClear for optional fields.'},
500
+ {
501
+ guidance: true,
502
+ description:
503
+ 'Place inside InputGroup for a single-line prefix or suffix addon, like a label or timezone marker.',
504
+ },
500
505
  {
501
506
  guidance: false,
502
507
  description:
@@ -70,11 +70,17 @@ export const docs = {
70
70
  description: 'Disables the input and all token interactions.',
71
71
  default: 'false',
72
72
  },
73
+ {
74
+ name: 'htmlName',
75
+ type: 'string',
76
+ description:
77
+ 'The HTML name attribute for form submissions. Renders one hidden input per selected item id.',
78
+ },
73
79
  {
74
80
  name: 'disabledMessage',
75
81
  type: 'string',
76
82
  description:
77
- 'Explains why the tokenizer is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the input focusable via aria-disabled (input stays blocked). Use this instead of wrapping a disabled Tokenizer in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
83
+ 'Explains why the tokenizer is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the input focusable via aria-disabled (input stays blocked). Use this instead of wrapping a disabled Tokenizer in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
78
84
  },
79
85
  {
80
86
  name: 'status',
@@ -277,11 +283,16 @@ export const docsZh = {
277
283
  description: '\u7981\u7528\u8f93\u5165\u6846\u548c\u6240\u6709\u6807\u8bb0\u4ea4\u4e92\u3002',
278
284
  default: 'false',
279
285
  },
286
+ {
287
+ name: 'htmlName',
288
+ type: 'string',
289
+ description: '用于表单提交的 HTML name 属性。为每个已选项目的 id 渲染一个隐藏输入。',
290
+ },
280
291
  {
281
292
  name: 'disabledMessage',
282
293
  type: 'string',
283
294
  description:
284
- 'Explains why the tokenizer is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the input focusable via aria-disabled (input stays blocked). Use this instead of wrapping a disabled Tokenizer in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
295
+ 'Explains why the tokenizer is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the input focusable via aria-disabled (input stays blocked). Use this instead of wrapping a disabled Tokenizer in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
285
296
  },
286
297
  {
287
298
  name: 'status',
@@ -435,6 +446,7 @@ export const docsDense = {
435
446
  renderToken: 'Custom token render. Default renders Token w/ label+onRemove.',
436
447
  renderItem: 'Custom dropdown item render. Default renders TypeaheadItem.',
437
448
  isDisabled: 'Disables input+all token interactions.',
449
+ htmlName: 'HTML name attr; one hidden input per selected item id.',
438
450
  status: 'Validation status w/ type+message for error/warning/success.',
439
451
  isLabelHidden: 'Visually hides label; keeps a11y.',
440
452
  description: 'Helper text below label.',
@@ -867,4 +867,37 @@ describe('Tokenizer', () => {
867
867
  expect(screen.getByRole('combobox')).toBeDisabled();
868
868
  });
869
869
  });
870
+ describe('form participation', () => {
871
+ it('submits one entry per token id under htmlName', () => {
872
+ const {container} = render(
873
+ <form>
874
+ <Tokenizer
875
+ label="Users"
876
+ htmlName="users"
877
+ searchSource={userSource}
878
+ value={[users[0], users[1]]}
879
+ onChange={() => {}}
880
+ />
881
+ </form>,
882
+ );
883
+ const data = new FormData(container.querySelector('form')!);
884
+ expect(data.getAll('users')).toEqual([users[0].id, users[1].id]);
885
+ });
886
+
887
+ it('is excluded from form data when disabled', () => {
888
+ const {container} = render(
889
+ <form>
890
+ <Tokenizer
891
+ label="Users"
892
+ htmlName="users"
893
+ searchSource={userSource}
894
+ value={[users[0]]}
895
+ onChange={() => {}}
896
+ isDisabled
897
+ />
898
+ </form>,
899
+ );
900
+ expect([...new FormData(container.querySelector('form')!).keys()]).toEqual([]);
901
+ });
902
+ });
870
903
  });
@@ -128,6 +128,12 @@ export interface TokenizerProps<T extends SearchableItem> extends Omit<
128
128
  searchSource: SearchSource<T>;
129
129
  /** Currently selected items. */
130
130
  value: T[];
131
+
132
+ /**
133
+ * The HTML name attribute for form submissions. When set, hidden inputs
134
+ * carry one entry per selected item's id under this name.
135
+ */
136
+ htmlName?: string;
131
137
  /** Callback when selection changes. Includes change metadata. */
132
138
  onChange: (items: T[], change: TokenizerChange<T>) => void;
133
139
  /** Render function for dropdown items. Default: TypeaheadItem. */
@@ -377,6 +383,7 @@ export function Tokenizer<T extends SearchableItem>({
377
383
  maxMenuItems,
378
384
  emptySearchResultsText,
379
385
  isDisabled = false,
386
+ htmlName,
380
387
  disabledMessage,
381
388
  hasClear = false,
382
389
  endContent,
@@ -760,6 +767,18 @@ export function Tokenizer<T extends SearchableItem>({
760
767
  : undefined
761
768
  }
762
769
  />
770
+ {htmlName != null &&
771
+ value.map(item => (
772
+ <input
773
+ key={item.id}
774
+ type="hidden"
775
+ name={htmlName}
776
+ value={item.id}
777
+ // Disabled native controls are excluded from form submission;
778
+ // mirror that for the hidden carriers.
779
+ disabled={isDisabled}
780
+ />
781
+ ))}
763
782
  {(endContent || (hasClear && value.length > 0 && !isDisabled)) && (
764
783
  <div {...stylex.props(styles.endSection, endSectionSizeStyles[size])}>
765
784
  {endContent}
@@ -168,7 +168,7 @@ describe('Toolbar', () => {
168
168
 
169
169
  it('passes variant to Section', () => {
170
170
  const {container} = render(<Toolbar label="Actions" variant="muted" />);
171
- // Section renders with xds-section class containing the variant
171
+ // Section renders with astryx-section class containing the variant
172
172
  const sectionInner = container.querySelector('.astryx-section');
173
173
  expect(sectionInner).toBeInTheDocument();
174
174
  expect(sectionInner?.className).toContain('muted');
@@ -8,6 +8,16 @@ export const docs = {
8
8
  displayName: 'Base Typeahead',
9
9
  isHiddenFromOverview: true,
10
10
  description: 'Unstyled combobox engine providing input, search, keyboard navigation, and dropdown. No wrapper div, no border styling, no token rendering. Used by Typeahead and Tokenizer for custom compositions.',
11
+ usage: {
12
+ description: 'Unstyled combobox engine providing input, search, keyboard navigation, and dropdown. No wrapper div, no border styling, no token rendering. Used by Typeahead and Tokenizer for custom compositions.',
13
+ bestPractices: [
14
+ { guidance: true, description: 'Use Typeahead or Tokenizer for standard fields — they wrap BaseTypeahead with the wrapper div, border styling, and token rendering it intentionally omits.' },
15
+ { guidance: true, description: 'Provide your own wrapper div with border and layout when composing directly — BaseTypeahead renders no visual chrome of its own.' },
16
+ { guidance: true, description: 'Pass anchorRef pointing to your wrapper so the dropdown positions against your custom input chrome, not just the bare input element.' },
17
+ { guidance: false, description: 'Expect a wrapper div, border, or token rendering — BaseTypeahead is an engine only; all visual chrome is the caller\'s responsibility.' },
18
+ { guidance: false, description: 'Use BaseTypeahead when Typeahead or Tokenizer would suffice — the extra wrapper and styling work is only justified for truly custom compositions.' },
19
+ ],
20
+ },
11
21
  props: [
12
22
  {
13
23
  name: 'searchSource',
@@ -75,7 +75,7 @@ export const docs = {
75
75
  name: 'disabledMessage',
76
76
  type: 'string',
77
77
  description:
78
- 'Explains why the input is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the field focusable via aria-disabled (activation stays blocked). Use this instead of wrapping a disabled Typeahead in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
78
+ 'Explains why the input is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the field focusable via aria-disabled (activation stays blocked). Use this instead of wrapping a disabled Typeahead in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
79
79
  },
80
80
  {
81
81
  name: 'maxMenuItems',
@@ -287,6 +287,11 @@ export const docsDense = {
287
287
  description:
288
288
  'Add a search delay for remote data sources to avoid excessive network requests.',
289
289
  },
290
+ {
291
+ guidance: true,
292
+ description:
293
+ 'Use inside InputGroup when the typeahead needs a single-line prefix or suffix addon.',
294
+ },
290
295
  {
291
296
  guidance: false,
292
297
  description:
package/src/docs-types.ts CHANGED
@@ -217,6 +217,11 @@ export interface ComponentEntry {
217
217
  examples?: ExampleDoc[];
218
218
  /** When true, this sub-component is excluded from the overview page. */
219
219
  isHiddenFromOverview?: boolean;
220
+ /** Playground configuration for this specific component. Falls back to
221
+ * the directory doc's `playground` when omitted — declare one here when
222
+ * siblings must not share it (e.g. an overlay drawer whose toggle
223
+ * sub-component should not inherit `overlay: true`). */
224
+ playground?: PlaygroundConfig;
220
225
  }
221
226
 
222
227
  /**
@@ -390,6 +395,13 @@ export interface PlaygroundConfig {
390
395
  /** Initial prop values for the playground preview.
391
396
  * Keys are prop names. Values are primitives or ElementDescriptors. */
392
397
  defaults?: Record<string, unknown>;
398
+ /** The component opens as a full-viewport overlay (e.g. via
399
+ * `dialog.showModal()`) and renders nothing inline while closed. The
400
+ * interactive preview shows an open-trigger placeholder instead of an
401
+ * empty stage while `isOpen` is false, and lets the real overlay render
402
+ * when opened. Include `isOpen: false` in `defaults` so the preview can
403
+ * bridge `onOpenChange` back into playground state. */
404
+ overlay?: boolean;
393
405
  /** Required parent wrapper for sub-components that depend on a parent
394
406
  * context provider (e.g. `Tab` calls `useTabListContext()` and throws
395
407
  * standalone). The preview wraps the component in this parent before
@@ -38,7 +38,7 @@ export const docs = {
38
38
  {
39
39
  name: 'hintElement',
40
40
  type: 'ReactNode',
41
- description: 'The popover hint element to render inside the composite container (as the last child). Portals to the top layer via popover="manual", renders arrow keys with Kbd, and manages its own visibility — render it unconditionally.',
41
+ description: 'The popover hint element to render inside the composite container (as the last child). Portals to the top layer via popover="manual", renders arrow keys with Kbd, and manages its own visibility; render it unconditionally.',
42
42
  },
43
43
  {
44
44
  name: 'onFocus',
@@ -58,12 +58,12 @@ export const docs = {
58
58
  ],
59
59
  usage: {
60
60
  description:
61
- 'Shows an ephemeral "← → to navigate" hint anchored to the focused item the first time a roving-tabindex composite (Toolbar, TabList, SegmentedControl, etc.) receives keyboard focus. It teaches sighted keyboard users that arrow keys move within the group. The hint renders arrow keys with Kbd in the top layer (popover="manual") and is CSS-anchor-positioned to the focused element, so overflow containers never clip it. It auto-dismisses on the first arrow press, on timeout, or on blur, and does not re-show for that instance. Toolbar, TabList, and SegmentedControl wire this in automatically — reach for the hook directly only when building a custom roving-tabindex widget.',
61
+ 'Shows an ephemeral "← → to navigate" hint anchored to the focused item the first time a roving-tabindex composite (Toolbar, TabList, SegmentedControl, etc.) receives keyboard focus. It teaches sighted keyboard users that arrow keys move within the group. The hint renders arrow keys with Kbd in the top layer (popover="manual") and is CSS-anchor-positioned to the focused element, so overflow containers never clip it. It auto-dismisses on the first arrow press, on timeout, or on blur, and does not re-show for that instance. Toolbar, TabList, and SegmentedControl wire this in automatically; reach for the hook directly only when building a custom roving-tabindex widget.',
62
62
  bestPractices: [
63
- { guidance: true, description: 'Compose the returned onFocus/onKeyDown with your existing focus handlers rather than replacing them — call onKeyDown first (it only dismisses, never prevents), then your navigation handler.' },
63
+ { guidance: true, description: 'Compose the returned onFocus/onKeyDown with your existing focus handlers rather than replacing them: call onKeyDown first (it only dismisses, never prevents), then your navigation handler.' },
64
64
  { guidance: true, description: 'Render hintElement as the last child of the composite container; it is position:fixed in the top layer and aria-hidden, so it never affects layout or the accessibility tree.' },
65
65
  { guidance: true, description: 'Match orientation to the arrow keys your widget actually responds to so the hint shows the correct icons.' },
66
- { guidance: false, description: 'Use for single controls or widgets without roving-tabindex navigation — the hint only makes sense where arrows move focus within a group.' },
66
+ { guidance: false, description: 'Use for single controls or widgets without roving-tabindex navigation; the hint only makes sense where arrows move focus within a group.' },
67
67
  ],
68
68
  },
69
69
  relatedComponents: ['Toolbar', 'TabList', 'SegmentedControl'],
@@ -83,7 +83,7 @@ export const docsDense = {
83
83
  'options.isEnabled': 'whether the hint is enabled; false suppresses it (e.g. disabled/read-only widget).',
84
84
  },
85
85
  returnDescriptions: {
86
- hintElement: 'popover hint to render as last child of the container. Kbd-rendered arrows, top-layer, self-managing — render unconditionally.',
86
+ hintElement: 'popover hint to render as last child of the container. Kbd-rendered arrows, top-layer, self-managing; render unconditionally.',
87
87
  onFocus: 'container onFocus: shows hint on first :focus-visible entry from outside.',
88
88
  onBlur: 'container onBlur: hides on leaving the composite; re-anchors on internal moves.',
89
89
  onKeyDown: 'container onKeyDown: dismisses on first arrow press. Never prevents default.',
@@ -92,7 +92,7 @@ export const docsDense = {
92
92
  description:
93
93
  'Ephemeral "← → to navigate" hint on first keyboard focus of a roving-tabindex composite. Top-layer anchored popover, aria-hidden. Auto-dismisses on first arrow press, timeout, or blur; no re-show. Toolbar/TabList/SegmentedControl wire it in automatically.',
94
94
  bestPractices: [
95
- { guidance: true, description: 'Compose returned onFocus/onKeyDown with existing handlers (call onKeyDown first — it only dismisses).' },
95
+ { guidance: true, description: 'Compose returned onFocus/onKeyDown with existing handlers (call onKeyDown first; it only dismisses).' },
96
96
  { guidance: true, description: 'Render hintElement as last child; top-layer + aria-hidden, no layout/a11y impact.' },
97
97
  { guidance: true, description: 'Match orientation to the arrows the widget responds to.' },
98
98
  { guidance: false, description: 'Use for single controls / non-roving widgets.' },