@astryxdesign/core 0.1.1 → 0.1.2-canary.2581eb4

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 (189) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +119 -1
  3. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
  4. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  5. package/dist/AvatarGroup/AvatarGroupOverflow.js +4 -1
  6. package/dist/Banner/Banner.d.ts +7 -0
  7. package/dist/Banner/Banner.d.ts.map +1 -1
  8. package/dist/Banner/Banner.js +9 -2
  9. package/dist/Button/Button.d.ts +12 -2
  10. package/dist/Button/Button.d.ts.map +1 -1
  11. package/dist/Button/Button.js +51 -13
  12. package/dist/Calendar/Calendar.d.ts +5 -4
  13. package/dist/Calendar/Calendar.d.ts.map +1 -1
  14. package/dist/Calendar/Calendar.js +6 -1
  15. package/dist/Calendar/index.d.ts +1 -1
  16. package/dist/Calendar/index.d.ts.map +1 -1
  17. package/dist/DateTimeInput/DateTimeInput.d.ts +7 -2
  18. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  19. package/dist/DateTimeInput/DateTimeInput.js +5 -4
  20. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  21. package/dist/EmptyState/EmptyState.js +7 -1
  22. package/dist/HoverCard/HoverCard.d.ts +2 -2
  23. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  24. package/dist/HoverCard/HoverCard.js +18 -6
  25. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  26. package/dist/HoverCard/useHoverCard.js +7 -3
  27. package/dist/Kbd/Kbd.d.ts.map +1 -1
  28. package/dist/Kbd/Kbd.js +1 -1
  29. package/dist/Layer/anchorName.d.ts +20 -0
  30. package/dist/Layer/anchorName.d.ts.map +1 -0
  31. package/dist/Layer/anchorName.js +36 -0
  32. package/dist/Layer/useLayer.d.ts +19 -0
  33. package/dist/Layer/useLayer.d.ts.map +1 -1
  34. package/dist/Layer/useLayer.js +15 -6
  35. package/dist/Link/Link.d.ts +1 -1
  36. package/dist/Link/Link.js +2 -2
  37. package/dist/Link/LinkProvider.d.ts +1 -0
  38. package/dist/Link/LinkProvider.d.ts.map +1 -1
  39. package/dist/Link/LinkProvider.js +1 -0
  40. package/dist/Markdown/Markdown.d.ts.map +1 -1
  41. package/dist/Markdown/Markdown.js +13 -3
  42. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  43. package/dist/MobileNav/MobileNav.js +13 -0
  44. package/dist/Pagination/Pagination.d.ts.map +1 -1
  45. package/dist/Pagination/Pagination.js +31 -27
  46. package/dist/Slider/Slider.js +2 -2
  47. package/dist/Table/BaseTable.d.ts.map +1 -1
  48. package/dist/Table/BaseTable.js +27 -8
  49. package/dist/Table/Table.d.ts.map +1 -1
  50. package/dist/Table/Table.js +30 -7
  51. package/dist/Table/TableRow.d.ts +6 -1
  52. package/dist/Table/TableRow.d.ts.map +1 -1
  53. package/dist/Table/TableRow.js +15 -7
  54. package/dist/Table/index.d.ts +3 -1
  55. package/dist/Table/index.d.ts.map +1 -1
  56. package/dist/Table/index.js +1 -0
  57. package/dist/Table/plugins/stickyColumns/index.d.ts +3 -0
  58. package/dist/Table/plugins/stickyColumns/index.d.ts.map +1 -0
  59. package/dist/Table/plugins/stickyColumns/index.js +3 -0
  60. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts +25 -0
  61. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -0
  62. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +369 -0
  63. package/dist/Table/types.d.ts +95 -5
  64. package/dist/Table/types.d.ts.map +1 -1
  65. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  66. package/dist/Table/useBaseTablePlugins.js +1 -1
  67. package/dist/Text/text.stylex.d.ts +2 -2
  68. package/dist/Text/text.stylex.js +2 -2
  69. package/dist/Timestamp/Timestamp.d.ts +1 -1
  70. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  71. package/dist/Timestamp/Timestamp.js +28 -3
  72. package/dist/ToggleButton/ToggleButton.d.ts +7 -9
  73. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  74. package/dist/ToggleButton/ToggleButton.js +34 -57
  75. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  76. package/dist/Tokenizer/Tokenizer.js +1 -1
  77. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  78. package/dist/Tooltip/useTooltip.js +1 -0
  79. package/dist/astryx.css +30 -1
  80. package/dist/astryx.umd.js +147 -0
  81. package/dist/astryx.umd.js.map +7 -0
  82. package/dist/theme/generateThemeRules.js +1 -1
  83. package/dist/theme/syntax/SyntaxTheme.d.ts +1 -1
  84. package/dist/theme/syntax/SyntaxTheme.js +1 -1
  85. package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
  86. package/dist/theme/syntax/defineSyntaxTheme.js +2 -2
  87. package/dist/theme/syntax/presets.d.ts +1 -1
  88. package/dist/theme/syntax/presets.d.ts.map +1 -1
  89. package/dist/theme/syntax/presets.js +1 -1
  90. package/dist/theme/syntax/tokens.d.ts +1 -1
  91. package/dist/theme/syntax/tokens.js +1 -1
  92. package/dist/theme/types.d.ts +1 -1
  93. package/dist/utils/dateTypes.d.ts +12 -0
  94. package/dist/utils/dateTypes.d.ts.map +1 -1
  95. package/dist/utils/dateTypes.js +41 -1
  96. package/package.json +6 -2
  97. package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -0
  98. package/src/Banner/Banner.doc.mjs +8 -0
  99. package/src/Banner/Banner.test.tsx +16 -7
  100. package/src/Banner/Banner.tsx +9 -2
  101. package/src/Button/Button.doc.mjs +6 -0
  102. package/src/Button/Button.test.tsx +105 -11
  103. package/src/Button/Button.tsx +73 -5
  104. package/src/Calendar/Calendar.doc.mjs +4 -4
  105. package/src/Calendar/Calendar.test.tsx +21 -26
  106. package/src/Calendar/Calendar.tsx +21 -5
  107. package/src/Calendar/index.ts +1 -0
  108. package/src/Chat/ChatComposer.doc.mjs +3 -0
  109. package/src/Chat/ChatComposerDrawer.doc.mjs +12 -0
  110. package/src/CheckboxInput/CheckboxInput.doc.mjs +8 -0
  111. package/src/ClickableCard/ClickableCard.doc.mjs +15 -0
  112. package/src/DateTimeInput/DateTimeInput.doc.mjs +11 -3
  113. package/src/DateTimeInput/DateTimeInput.test.tsx +23 -3
  114. package/src/DateTimeInput/DateTimeInput.tsx +13 -10
  115. package/src/Dialog/Dialog.doc.mjs +2 -2
  116. package/src/EmptyState/EmptyState.test.tsx +4 -2
  117. package/src/EmptyState/EmptyState.tsx +6 -2
  118. package/src/HoverCard/HoverCard.doc.mjs +3 -0
  119. package/src/HoverCard/HoverCard.test.tsx +189 -2
  120. package/src/HoverCard/HoverCard.tsx +20 -16
  121. package/src/HoverCard/useHoverCard.tsx +13 -10
  122. package/src/Icon/Icon.doc.mjs +8 -0
  123. package/src/Kbd/Kbd.tsx +2 -9
  124. package/src/Layer/anchorName.test.ts +146 -0
  125. package/src/Layer/anchorName.ts +46 -0
  126. package/src/Layer/useLayer.doc.mjs +7 -2
  127. package/src/Layer/useLayer.tsx +33 -9
  128. package/src/Layout/Layout.doc.mjs +1 -1
  129. package/src/Lightbox/Lightbox.doc.mjs +0 -2
  130. package/src/Link/Link.test.tsx +2 -2
  131. package/src/Link/Link.tsx +3 -3
  132. package/src/Link/LinkProvider.tsx +1 -0
  133. package/src/List/List.doc.mjs +7 -0
  134. package/src/Markdown/Markdown.doc.mjs +2 -0
  135. package/src/Markdown/Markdown.test.tsx +17 -26
  136. package/src/Markdown/Markdown.tsx +16 -6
  137. package/src/MobileNav/MobileNav.doc.mjs +8 -8
  138. package/src/MobileNav/MobileNav.tsx +13 -0
  139. package/src/MobileNav/MobileNavReopen.test.tsx +118 -0
  140. package/src/OverflowList/OverflowList.doc.mjs +17 -0
  141. package/src/Pagination/Pagination.test.tsx +137 -13
  142. package/src/Pagination/Pagination.tsx +33 -28
  143. package/src/ProgressBar/ProgressBar.test.tsx +13 -3
  144. package/src/Resizable/Resizable.doc.mjs +1 -1
  145. package/src/Section/Section.doc.mjs +11 -0
  146. package/src/SelectableCard/SelectableCard.doc.mjs +15 -0
  147. package/src/Selector/Selector.doc.mjs +4 -0
  148. package/src/Skeleton/Skeleton.doc.mjs +11 -1
  149. package/src/Slider/Slider.doc.mjs +4 -0
  150. package/src/Slider/Slider.test.tsx +3 -16
  151. package/src/Slider/Slider.tsx +1 -1
  152. package/src/TabList/TabList.doc.mjs +6 -0
  153. package/src/Table/BaseTable.tsx +51 -24
  154. package/src/Table/Table.doc.mjs +12 -0
  155. package/src/Table/Table.test.tsx +29 -30
  156. package/src/Table/Table.tsx +22 -1
  157. package/src/Table/TableRow.tsx +37 -7
  158. package/src/Table/index.ts +3 -0
  159. package/src/Table/plugins/stickyColumns/index.ts +4 -0
  160. package/src/Table/plugins/stickyColumns/useTableStickyColumns.test.tsx +163 -0
  161. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +407 -0
  162. package/src/Table/types.ts +101 -4
  163. package/src/Table/useBaseTablePlugins.ts +1 -0
  164. package/src/Table/useTableColumnResize.doc.mjs +96 -0
  165. package/src/Table/useTableStickyColumns.doc.mjs +59 -0
  166. package/src/Text/Heading.doc.mjs +15 -2
  167. package/src/Text/Text.doc.mjs +7 -1
  168. package/src/Text/text.stylex.ts +2 -2
  169. package/src/Timestamp/Timestamp.doc.mjs +9 -0
  170. package/src/Timestamp/Timestamp.test.tsx +44 -18
  171. package/src/Timestamp/Timestamp.tsx +30 -10
  172. package/src/ToggleButton/ToggleButton.doc.mjs +9 -3
  173. package/src/ToggleButton/ToggleButton.test.tsx +39 -18
  174. package/src/ToggleButton/ToggleButton.tsx +46 -83
  175. package/src/Tokenizer/Tokenizer.tsx +4 -1
  176. package/src/Toolbar/Toolbar.doc.mjs +65 -5
  177. package/src/Tooltip/Tooltip.test.tsx +13 -0
  178. package/src/Tooltip/useTooltip.tsx +2 -3
  179. package/src/hooks/useImageMode.test.ts +133 -0
  180. package/src/theme/derivedVarRegistry.test.ts +4 -0
  181. package/src/theme/generateThemeRules.test.ts +7 -8
  182. package/src/theme/generateThemeRules.ts +1 -1
  183. package/src/theme/syntax/SyntaxTheme.tsx +1 -1
  184. package/src/theme/syntax/defineSyntaxTheme.ts +20 -11
  185. package/src/theme/syntax/presets.ts +5 -2
  186. package/src/theme/syntax/tokens.ts +1 -1
  187. package/src/theme/types.ts +1 -1
  188. package/src/utils/dateTypes.test.ts +40 -0
  189. package/src/utils/dateTypes.ts +40 -0
@@ -103,8 +103,8 @@ export const docs = {
103
103
  },
104
104
  {
105
105
  name: 'weekStartsOn',
106
- type: '0 | 1 | 2 | 3 | 4 | 5 | 6',
107
- description: 'First day of week (0=Sunday).',
106
+ type: "0 | 1 | 2 | 3 | 4 | 5 | 6 | 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'",
107
+ description: 'First day of week. Accepts a number (0=Sunday) or a three-letter day name (e.g. "mon").',
108
108
  default: '0',
109
109
  },
110
110
  ],
@@ -147,7 +147,7 @@ export const docsZh = {
147
147
  {name: 'hasOutsideDays', type: 'boolean', description: '显示相邻月份的日期。', default: 'true'},
148
148
  {name: 'hasWeekNumbers', type: 'boolean', description: '显示 ISO 周数。', default: 'false'},
149
149
  {name: 'hasVariableRowCount', type: 'boolean', description: '可变行数与固定 6 行网格。', default: 'false'},
150
- {name: 'weekStartsOn', type: '0 | 1 | 2 | 3 | 4 | 5 | 6', description: '每周起始日(0=周日)。', default: '0'},
150
+ {name: 'weekStartsOn', type: "0 | 1 | 2 | 3 | 4 | 5 | 6 | 'sun'…'sat'", description: '每周起始日。可为数字(0=周日)或三字母星期缩写(如 "mon")。', default: '0'},
151
151
  ],
152
152
  theming: {
153
153
  targets: [
@@ -200,6 +200,6 @@ export const docsDense = {
200
200
  hasOutsideDays: 'show days from adjacent months',
201
201
  hasWeekNumbers: 'show ISO week numbers',
202
202
  hasVariableRowCount: 'variable vs fixed 6-row grid',
203
- weekStartsOn: 'first day of week (0=Sunday)',
203
+ weekStartsOn: 'first day of week (0=Sunday, or name e.g. "mon")',
204
204
  },
205
205
  };
@@ -146,10 +146,7 @@ describe('Calendar', () => {
146
146
  const handleFocusChange = vi.fn();
147
147
 
148
148
  render(
149
- <Calendar
150
- focusDate="2026-01-01"
151
- onFocusDateChange={handleFocusChange}
152
- />,
149
+ <Calendar focusDate="2026-01-01" onFocusDateChange={handleFocusChange} />,
153
150
  );
154
151
 
155
152
  const nextButton = screen.getByRole('button', {name: 'Next month'});
@@ -189,9 +186,7 @@ describe('Calendar', () => {
189
186
  return day !== 0 && day !== 6;
190
187
  };
191
188
 
192
- render(
193
- <Calendar focusDate="2026-01-01" dateConstraints={[isWeekday]} />,
194
- );
189
+ render(<Calendar focusDate="2026-01-01" dateConstraints={[isWeekday]} />);
195
190
 
196
191
  // January 4, 2026 is a Sunday - should be disabled
197
192
  const sunday = getDayButton(4);
@@ -238,6 +233,21 @@ describe('Calendar', () => {
238
233
  expect(dayNames[0]).toHaveTextContent('Mo');
239
234
  });
240
235
 
236
+ it('accepts a three-letter day name for weekStartsOn', () => {
237
+ render(<Calendar weekStartsOn="mon" />);
238
+
239
+ // "mon" should behave exactly like the numeric 1 (Monday first).
240
+ const dayNames = screen.getAllByText(/^(Mo|Tu|We|Th|Fr|Sa|Su)$/);
241
+ expect(dayNames[0]).toHaveTextContent('Mo');
242
+ });
243
+
244
+ it('treats weekStartsOn day names case-insensitively', () => {
245
+ render(<Calendar weekStartsOn={'WED' as 'wed'} />);
246
+
247
+ const dayNames = screen.getAllByText(/^(Mo|Tu|We|Th|Fr|Sa|Su)$/);
248
+ expect(dayNames[0]).toHaveTextContent('We');
249
+ });
250
+
241
251
  // ─── Range Mode ──────────────────────────────────────────────
242
252
 
243
253
  it('supports range selection mode', async () => {
@@ -245,11 +255,7 @@ describe('Calendar', () => {
245
255
  const handleChange = vi.fn();
246
256
 
247
257
  render(
248
- <Calendar
249
- mode="range"
250
- onChange={handleChange}
251
- focusDate="2026-01-01"
252
- />,
258
+ <Calendar mode="range" onChange={handleChange} focusDate="2026-01-01" />,
253
259
  );
254
260
 
255
261
  // Click start date
@@ -271,11 +277,7 @@ describe('Calendar', () => {
271
277
  const handleChange = vi.fn();
272
278
 
273
279
  render(
274
- <Calendar
275
- mode="range"
276
- onChange={handleChange}
277
- focusDate="2026-01-01"
278
- />,
280
+ <Calendar mode="range" onChange={handleChange} focusDate="2026-01-01" />,
279
281
  );
280
282
 
281
283
  // Click later date first
@@ -346,10 +348,7 @@ describe('Calendar', () => {
346
348
  const handleFocusChange = vi.fn();
347
349
 
348
350
  render(
349
- <Calendar
350
- focusDate="2026-01-01"
351
- onFocusDateChange={handleFocusChange}
352
- />,
351
+ <Calendar focusDate="2026-01-01" onFocusDateChange={handleFocusChange} />,
353
352
  );
354
353
 
355
354
  // Focus Jan 28
@@ -414,11 +413,7 @@ describe('Calendar', () => {
414
413
  const handleChange = vi.fn();
415
414
 
416
415
  render(
417
- <Calendar
418
- mode="range"
419
- onChange={handleChange}
420
- focusDate="2026-01-01"
421
- />,
416
+ <Calendar mode="range" onChange={handleChange} focusDate="2026-01-01" />,
422
417
  );
423
418
 
424
419
  // Click start date to begin range selection
@@ -70,8 +70,19 @@ import {
70
70
  // Types
71
71
  // =============================================================================
72
72
 
73
- export type {ISODateString, DayOfWeek, DateRange} from '../utils/dateTypes';
74
- import type {ISODateString, DayOfWeek, DateRange} from '../utils/dateTypes';
73
+ export type {
74
+ ISODateString,
75
+ DayOfWeek,
76
+ DayOfWeekName,
77
+ DateRange,
78
+ } from '../utils/dateTypes';
79
+ import type {
80
+ ISODateString,
81
+ DayOfWeek,
82
+ DayOfWeekName,
83
+ DateRange,
84
+ } from '../utils/dateTypes';
85
+ import {normalizeDayOfWeek} from '../utils/dateTypes';
75
86
  import {themeProps} from '../utils/themeProps';
76
87
 
77
88
  /** Imperative handle for Calendar handleRef */
@@ -134,10 +145,11 @@ interface CalendarBaseProps extends Omit<
134
145
  hasVariableRowCount?: boolean;
135
146
 
136
147
  /**
137
- * First day of week.
148
+ * First day of week. Accepts a number (0 = Sunday … 6 = Saturday) or a
149
+ * three-letter day name ('sun'–'sat', case-insensitive) for readability.
138
150
  * Default: 0 (Sunday)
139
151
  */
140
- weekStartsOn?: DayOfWeek;
152
+ weekStartsOn?: DayOfWeek | DayOfWeekName;
141
153
  }
142
154
 
143
155
  // ─── Mode-specific Props (discriminated union) ────────────────
@@ -200,13 +212,17 @@ export function Calendar({ref, ...props}: CalendarProps) {
200
212
  hasOutsideDays = true,
201
213
  hasWeekNumbers = false,
202
214
  hasVariableRowCount = false,
203
- weekStartsOn = 0,
215
+ weekStartsOn: weekStartsOnProp = 0,
204
216
  xstyle,
205
217
  className,
206
218
  style,
207
219
  ...rest
208
220
  } = props;
209
221
 
222
+ // Normalize `weekStartsOn` (number or three-letter day name) to a numeric
223
+ // DayOfWeek so all downstream date math keeps working with an index.
224
+ const weekStartsOn = normalizeDayOfWeek(weekStartsOnProp);
225
+
210
226
  // Today's date (memoized)
211
227
  const today = useMemo(() => plainDateToday(), []);
212
228
 
@@ -17,6 +17,7 @@ export type {
17
17
  CalendarHandle,
18
18
  ISODateString,
19
19
  DayOfWeek,
20
+ DayOfWeekName,
20
21
  DateRange,
21
22
  } from './Calendar';
22
23
 
@@ -7,6 +7,9 @@ export const docs = {
7
7
  subComponentOf: 'Chat',
8
8
  displayName: 'Chat Composer',
9
9
  description: 'Layout shell for a chat composer. Arranges named slots (drawer, header, input, footer, send) with page-radius container, hover/focus shadows, and concentric inner radius for child elements.',
10
+ playground: {
11
+ wrapper: {component: 'Stack', props: {width: 480}},
12
+ },
10
13
  props: [
11
14
  {
12
15
  name: 'onSubmit',
@@ -8,6 +8,18 @@ export const docs = {
8
8
  displayName: 'Chat Composer Drawer',
9
9
  isHiddenFromOverview: true,
10
10
  description: "Collapsible drawer panel that sits above the chat input inside ChatComposer. Pass it to the composer's `drawer` slot to show attachments, context chips, or any supplementary content. When `count` is provided the drawer gains a collapse toggle: collapsed state shows a badge and label, expanded state shows all children.",
11
+ playground: {
12
+ wrapper: {component: 'Stack', props: {width: 480}},
13
+ defaults: {
14
+ count: 3,
15
+ label: 'Attachments',
16
+ children: [
17
+ {__element: 'Token', props: {label: 'design-spec.pdf'}},
18
+ {__element: 'Token', props: {label: 'api-schema.json'}},
19
+ {__element: 'Token', props: {label: 'screenshot.png'}},
20
+ ],
21
+ },
22
+ },
11
23
  props: [
12
24
  {
13
25
  name: 'children',
@@ -63,6 +63,13 @@ export const docs = {
63
63
  description: 'Whether the checkbox is disabled.',
64
64
  default: 'false',
65
65
  },
66
+ {
67
+ name: 'isReadOnly',
68
+ type: 'boolean',
69
+ description:
70
+ 'Whether the checkbox is read-only. Displays the current state at full opacity but prevents interaction. Unlike `isDisabled`, read-only checkboxes are not visually dimmed.',
71
+ default: 'false',
72
+ },
66
73
  {
67
74
  name: 'isOptional',
68
75
  type: 'boolean',
@@ -156,6 +163,7 @@ export const docsZh = {
156
163
  },
157
164
  {name: 'isLoading', type: 'boolean', description: '复选框是否处于加载状态。显示旋转器并阻止交互。', default: 'false'},
158
165
  {name: 'isDisabled', type: 'boolean', description: '复选框是否禁用。', default: 'false'},
166
+ {name: 'isReadOnly', type: 'boolean', description: '复选框是否为只读。以完整不透明度显示当前状态但阻止交互。与 isDisabled 不同,只读复选框不会变暗。', default: 'false'},
159
167
  {name: 'isOptional', type: 'boolean', description: '字段是否可选。与 isRequired 互斥。', default: 'false'},
160
168
  {name: 'isRequired', type: 'boolean', description: '复选框是否必填。与 isOptional 互斥。', default: 'false'},
161
169
  {name: 'size', type: "'sm' | 'md'", description: '复选框尺寸。sm 用于紧凑布局,md 为默认。', default: "'md'"},
@@ -37,6 +37,21 @@ export const docs = {
37
37
  container: true,
38
38
  targets: [{className: 'astryx-clickable-card', visualProps: ['variant']}],
39
39
  },
40
+ playground: {
41
+ defaults: {
42
+ label: 'View product details',
43
+ href: '#',
44
+ padding: 4,
45
+ children: {
46
+ __element: 'XDSVStack',
47
+ props: {gap: 1},
48
+ children: [
49
+ {__element: 'XDSHeading', props: {level: 3}, children: 'Wireless Headphones'},
50
+ {__element: 'XDSText', props: {type: 'body'}, children: 'Noise-cancelling over-ear headphones with 30-hour battery life.'},
51
+ ],
52
+ },
53
+ },
54
+ },
40
55
  };
41
56
 
42
57
  /** @type {import('../docs-types').TranslationDoc} */
@@ -108,9 +108,15 @@ export const docs = {
108
108
  {
109
109
  name: 'placeholder',
110
110
  type: 'string',
111
- description: 'Placeholder text shown when no datetime is selected.',
111
+ description: 'Placeholder text shown in the date portion when no date is selected.',
112
112
  default: "'Select a date'",
113
113
  },
114
+ {
115
+ name: 'timePlaceholder',
116
+ type: 'string',
117
+ description: 'Placeholder text shown in the time portion when no time is selected.',
118
+ default: "'Select a time'",
119
+ },
114
120
  {
115
121
  name: 'size',
116
122
  type: "'sm' | 'md' | 'lg'",
@@ -202,7 +208,8 @@ export const docsZh = {
202
208
  {name: 'hourFormat', type: "'12h' | '24h'", description: "控制显示格式。'12h' 显示 AM/PM;'24h' 使用 24 小时制。", default: "'12h'"},
203
209
  {name: 'timeIncrement', type: 'number', description: '在时间输入中按箭头键时增减的分钟数。', default: '1'},
204
210
  {name: 'hasClear', type: 'boolean', description: '当有值时显示清除按钮。', default: 'false'},
205
- {name: 'placeholder', type: 'string', description: '未选择日期时显示的占位符文本。', default: "'Select a date'"},
211
+ {name: 'placeholder', type: 'string', description: '日期部分未选择日期时显示的占位符文本。', default: "'Select a date'"},
212
+ {name: 'timePlaceholder', type: 'string', description: '时间部分未选择时间时显示的占位符文本。', default: "'Select a time'"},
206
213
  {name: 'size', type: "'sm' | 'md' | 'lg'", description: '输入控件的尺寸。', default: "'md'"},
207
214
  {name: 'status', type: 'InputStatus', description: '错误、警告或成功状态的状态指示对象,附带消息。'},
208
215
  {name: 'labelTooltip', type: 'string', description: '通过标签末尾的信息图标显示的提示文本。'},
@@ -249,7 +256,8 @@ export const docsDense = {
249
256
  hourFormat: "display format. '12h' shows AM/PM; '24h' uses 24-hour",
250
257
  timeIncrement: 'minutes to add/subtract on arrow keys in time input',
251
258
  hasClear: 'Shows clear button when datetime is set',
252
- placeholder: 'placeholder text when empty',
259
+ placeholder: 'date-portion placeholder when empty',
260
+ timePlaceholder: 'time-portion placeholder when empty',
253
261
  size: 'input control size',
254
262
  status: 'error/warning/success status w/ message',
255
263
  labelTooltip: 'tooltip text via info icon at label end',
@@ -31,6 +31,28 @@ describe('DateTimeInput', () => {
31
31
  expect(screen.getByPlaceholderText('Pick a date')).toBeInTheDocument();
32
32
  });
33
33
 
34
+ it('defaults the time portion placeholder to "Select a time"', () => {
35
+ render(<DateTimeInput label="Meeting" onChange={() => {}} />);
36
+ expect(screen.getByPlaceholderText('Select a time')).toBeInTheDocument();
37
+ });
38
+
39
+ it('applies a custom timePlaceholder to the time portion', () => {
40
+ render(
41
+ <DateTimeInput
42
+ label="Meeting"
43
+ onChange={() => {}}
44
+ placeholder="Pick a date"
45
+ timePlaceholder="Pick a time"
46
+ />,
47
+ );
48
+ // Time portion uses the override; date portion keeps its own placeholder.
49
+ expect(screen.getByPlaceholderText('Pick a time')).toBeInTheDocument();
50
+ expect(screen.getByPlaceholderText('Pick a date')).toBeInTheDocument();
51
+ expect(
52
+ screen.queryByPlaceholderText('Select a time'),
53
+ ).not.toBeInTheDocument();
54
+ });
55
+
34
56
  it('renders both date and time inputs', () => {
35
57
  render(<DateTimeInput label="Meeting" onChange={() => {}} />);
36
58
  expect(screen.getByRole('combobox')).toBeInTheDocument();
@@ -90,9 +112,7 @@ describe('DateTimeInput', () => {
90
112
  });
91
113
 
92
114
  it('visually hides label when isLabelHidden is true', () => {
93
- render(
94
- <DateTimeInput label="Meeting" isLabelHidden onChange={() => {}} />,
95
- );
115
+ render(<DateTimeInput label="Meeting" isLabelHidden onChange={() => {}} />);
96
116
  const label = screen.getByText('Meeting');
97
117
  expect(label).toBeInTheDocument();
98
118
  expect(screen.getByLabelText('Meeting')).toBeInTheDocument();
@@ -48,11 +48,7 @@ import {
48
48
  } from '../Field';
49
49
  import {Icon} from '../Icon';
50
50
  import {Spinner} from '../Spinner';
51
- import {
52
- Calendar,
53
- type ISODateString,
54
- type CalendarHandle,
55
- } from '../Calendar';
51
+ import {Calendar, type ISODateString, type CalendarHandle} from '../Calendar';
56
52
  import {useCalendarConstraints} from '../Calendar/hooks';
57
53
  import {usePopover} from '../Popover';
58
54
  import {useInputContainer} from '../hooks/useInputContainer';
@@ -278,11 +274,17 @@ export interface DateTimeInputProps extends Omit<
278
274
  hasClear?: boolean;
279
275
 
280
276
  /**
281
- * Placeholder text shown when no date is selected.
277
+ * Placeholder text shown in the date portion when no date is selected.
282
278
  * @default "Select a date"
283
279
  */
284
280
  placeholder?: string;
285
281
 
282
+ /**
283
+ * Placeholder text shown in the time portion when no time is selected.
284
+ * @default "Select a time"
285
+ */
286
+ timePlaceholder?: string;
287
+
286
288
  /**
287
289
  * The size of the inputs.
288
290
  * @default 'md'
@@ -385,6 +387,7 @@ export function DateTimeInput({
385
387
  timeIncrement = 1,
386
388
  hasClear = false,
387
389
  placeholder = 'Select a date',
390
+ timePlaceholder = 'Select a time',
388
391
  size: sizeProp,
389
392
  status,
390
393
  labelTooltip,
@@ -520,12 +523,12 @@ export function DateTimeInput({
520
523
  return isTimeInRange(parsed, timeMin, timeMax);
521
524
  }, [timePendingInput, hasSeconds, timeMin, timeMax]);
522
525
 
523
- const timePlaceholder = useMemo(() => {
526
+ const resolvedTimePlaceholder = useMemo(() => {
524
527
  if (isTimeFocused && !timeDisplayValue) {
525
528
  return hourFormat === '12h' ? 'e.g., 2:30 PM' : 'e.g., 14:30';
526
529
  }
527
- return 'Select a time';
528
- }, [isTimeFocused, timeDisplayValue, hourFormat]);
530
+ return timePlaceholder;
531
+ }, [isTimeFocused, timeDisplayValue, hourFormat, timePlaceholder]);
529
532
 
530
533
  // --- Unified change handler ---
531
534
  const fireChange = useCallback(
@@ -893,7 +896,7 @@ export function DateTimeInput({
893
896
  onFocus={handleTimeFocus}
894
897
  onBlur={handleTimeBlur}
895
898
  onKeyDown={handleTimeKeyDown}
896
- placeholder={timePlaceholder}
899
+ placeholder={resolvedTimePlaceholder}
897
900
  disabled={isEffectivelyDisabled}
898
901
  aria-label="Time"
899
902
  aria-required={isRequired === true ? 'true' : undefined}
@@ -40,13 +40,13 @@ export const docs = {
40
40
  {
41
41
  name: 'isOpen',
42
42
  type: 'boolean',
43
- description: 'Whether the dialog is open (required).',
43
+ description: 'Whether the dialog is open.',
44
44
  required: true,
45
45
  },
46
46
  {
47
47
  name: 'onOpenChange',
48
48
  type: '(isOpen: boolean) => unknown',
49
- description: 'Callback when dialog visibility changes (required).',
49
+ description: 'Callback when dialog visibility changes.',
50
50
  required: true,
51
51
  },
52
52
  {
@@ -45,11 +45,13 @@ describe('EmptyState', () => {
45
45
  />,
46
46
  );
47
47
  expect(screen.getByText('Try adjusting your search.')).toBeInTheDocument();
48
+ // Description renders as <div> (never <p>) so block content composes safely.
49
+ expect(screen.getByText('Try adjusting your search.').tagName).toBe('DIV');
48
50
  });
49
51
 
50
52
  it('does not render description when not provided', () => {
51
- const {container} = render(<EmptyState title="No results" />);
52
- expect(container.querySelector('p')).not.toBeInTheDocument();
53
+ render(<EmptyState title="No results" />);
54
+ expect(screen.queryByText('Try adjusting your search.')).toBeNull();
53
55
  });
54
56
 
55
57
  it('renders with icon', () => {
@@ -174,13 +174,17 @@ export function EmptyState({
174
174
  title,
175
175
  )}
176
176
  {description != null && (
177
- <p
177
+ // Rendered as <div> (not <p>): description accepts ReactNode and a
178
+ // <p> cannot legally contain block children, which causes hydration
179
+ // mismatches. The StyleX style sets margin: 0, so appearance is
180
+ // unchanged.
181
+ <div
178
182
  {...stylex.props(
179
183
  styles.description,
180
184
  isCompact && styles.descriptionCompact,
181
185
  )}>
182
186
  {description}
183
- </p>
187
+ </div>
184
188
  )}
185
189
  </div>
186
190
  {actions != null && (
@@ -108,6 +108,7 @@ export const docs = {
108
108
  { guidance: false, description: 'Place critical actions or required information inside a hover card; users may miss content that only appears on hover.' },
109
109
  { guidance: false, description: 'Use a hover card when a simple Tooltip or Popover would suffice.' },
110
110
  { guidance: false, description: 'Use a HoverCard for content the user must interact with; it disappears when the cursor leaves.' },
111
+ { guidance: false, description: 'Nest a HoverCard whose content has block elements directly inside phrasing-only contexts such as a <p>, <label>, or heading. The card renders inline, so block content there is invalid HTML the browser reparents. Wrap the surrounding text in a block element (e.g. a <div>) instead.' },
111
112
  ],
112
113
  anatomy: [
113
114
  {name: 'Trigger', required: true, description: 'The element that opens the hover card on hover or focus: a button, link, or inline text.'},
@@ -216,6 +217,7 @@ export const docsZh = {
216
217
  { guidance: false, description: 'Place critical actions or required information inside a hover card; users may miss content that only appears on hover.' },
217
218
  { guidance: false, description: 'Use a hover card when a simple Tooltip or Popover would suffice.' },
218
219
  { guidance: false, description: 'Use a HoverCard for content the user must interact with; it disappears when the cursor leaves.' },
220
+ { guidance: false, description: 'Nest a HoverCard whose content has block elements directly inside phrasing-only contexts such as a <p>, <label>, or heading. The card renders inline, so block content there is invalid HTML the browser reparents. Wrap the surrounding text in a block element (e.g. a <div>) instead.' },
219
221
  ],
220
222
  },
221
223
  };
@@ -233,6 +235,7 @@ export const docsDense = {
233
235
  { guidance: false, description: 'Place critical actions or required information inside a hover card; users may miss content that only appears on hover.' },
234
236
  { guidance: false, description: 'Use a hover card when a simple Tooltip or Popover would suffice.' },
235
237
  { guidance: false, description: 'Use a HoverCard for content the user must interact with; it disappears when the cursor leaves.' },
238
+ { guidance: false, description: 'Nest a block-content HoverCard directly inside phrasing-only contexts (<p>, <label>, heading); it renders inline so block content is invalid HTML there. Wrap surrounding text in a block element instead.' },
236
239
  ],
237
240
  },
238
241
  components: [