@astryxdesign/core 0.1.1 → 0.1.2-canary.3893cd5

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 (169) 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/EmptyState/EmptyState.d.ts.map +1 -1
  13. package/dist/EmptyState/EmptyState.js +7 -1
  14. package/dist/HoverCard/HoverCard.d.ts +2 -2
  15. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  16. package/dist/HoverCard/HoverCard.js +18 -6
  17. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  18. package/dist/HoverCard/useHoverCard.js +7 -3
  19. package/dist/Kbd/Kbd.d.ts.map +1 -1
  20. package/dist/Kbd/Kbd.js +1 -1
  21. package/dist/Layer/anchorName.d.ts +20 -0
  22. package/dist/Layer/anchorName.d.ts.map +1 -0
  23. package/dist/Layer/anchorName.js +36 -0
  24. package/dist/Layer/useLayer.d.ts +19 -0
  25. package/dist/Layer/useLayer.d.ts.map +1 -1
  26. package/dist/Layer/useLayer.js +15 -6
  27. package/dist/Link/Link.d.ts +1 -1
  28. package/dist/Link/Link.js +2 -2
  29. package/dist/Link/LinkProvider.d.ts +1 -0
  30. package/dist/Link/LinkProvider.d.ts.map +1 -1
  31. package/dist/Link/LinkProvider.js +1 -0
  32. package/dist/Markdown/Markdown.d.ts.map +1 -1
  33. package/dist/Markdown/Markdown.js +13 -3
  34. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  35. package/dist/MobileNav/MobileNav.js +13 -0
  36. package/dist/Pagination/Pagination.d.ts.map +1 -1
  37. package/dist/Pagination/Pagination.js +31 -27
  38. package/dist/Slider/Slider.js +2 -2
  39. package/dist/Table/BaseTable.d.ts.map +1 -1
  40. package/dist/Table/BaseTable.js +27 -8
  41. package/dist/Table/Table.d.ts.map +1 -1
  42. package/dist/Table/Table.js +30 -7
  43. package/dist/Table/TableRow.d.ts +6 -1
  44. package/dist/Table/TableRow.d.ts.map +1 -1
  45. package/dist/Table/TableRow.js +15 -7
  46. package/dist/Table/index.d.ts +3 -1
  47. package/dist/Table/index.d.ts.map +1 -1
  48. package/dist/Table/index.js +1 -0
  49. package/dist/Table/plugins/stickyColumns/index.d.ts +3 -0
  50. package/dist/Table/plugins/stickyColumns/index.d.ts.map +1 -0
  51. package/dist/Table/plugins/stickyColumns/index.js +3 -0
  52. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts +25 -0
  53. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -0
  54. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +369 -0
  55. package/dist/Table/types.d.ts +95 -5
  56. package/dist/Table/types.d.ts.map +1 -1
  57. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  58. package/dist/Table/useBaseTablePlugins.js +1 -1
  59. package/dist/Text/text.stylex.d.ts +2 -2
  60. package/dist/Text/text.stylex.js +2 -2
  61. package/dist/Timestamp/Timestamp.d.ts +1 -1
  62. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  63. package/dist/Timestamp/Timestamp.js +28 -3
  64. package/dist/ToggleButton/ToggleButton.d.ts +7 -9
  65. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  66. package/dist/ToggleButton/ToggleButton.js +34 -57
  67. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  68. package/dist/Tokenizer/Tokenizer.js +1 -1
  69. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  70. package/dist/Tooltip/useTooltip.js +1 -0
  71. package/dist/astryx.css +30 -1
  72. package/dist/astryx.umd.js +147 -0
  73. package/dist/astryx.umd.js.map +7 -0
  74. package/dist/theme/generateThemeRules.js +1 -1
  75. package/dist/theme/syntax/SyntaxTheme.d.ts +1 -1
  76. package/dist/theme/syntax/SyntaxTheme.js +1 -1
  77. package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
  78. package/dist/theme/syntax/defineSyntaxTheme.js +2 -2
  79. package/dist/theme/syntax/presets.d.ts +1 -1
  80. package/dist/theme/syntax/presets.d.ts.map +1 -1
  81. package/dist/theme/syntax/presets.js +1 -1
  82. package/dist/theme/syntax/tokens.d.ts +1 -1
  83. package/dist/theme/syntax/tokens.js +1 -1
  84. package/dist/theme/types.d.ts +1 -1
  85. package/package.json +6 -2
  86. package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -0
  87. package/src/Banner/Banner.doc.mjs +8 -0
  88. package/src/Banner/Banner.test.tsx +16 -7
  89. package/src/Banner/Banner.tsx +9 -2
  90. package/src/Button/Button.doc.mjs +6 -0
  91. package/src/Button/Button.test.tsx +105 -11
  92. package/src/Button/Button.tsx +73 -5
  93. package/src/Chat/ChatComposer.doc.mjs +3 -0
  94. package/src/Chat/ChatComposerDrawer.doc.mjs +12 -0
  95. package/src/CheckboxInput/CheckboxInput.doc.mjs +8 -0
  96. package/src/ClickableCard/ClickableCard.doc.mjs +15 -0
  97. package/src/Dialog/Dialog.doc.mjs +2 -2
  98. package/src/EmptyState/EmptyState.test.tsx +4 -2
  99. package/src/EmptyState/EmptyState.tsx +6 -2
  100. package/src/HoverCard/HoverCard.doc.mjs +3 -0
  101. package/src/HoverCard/HoverCard.test.tsx +189 -2
  102. package/src/HoverCard/HoverCard.tsx +20 -16
  103. package/src/HoverCard/useHoverCard.tsx +13 -10
  104. package/src/Icon/Icon.doc.mjs +8 -0
  105. package/src/Kbd/Kbd.tsx +2 -9
  106. package/src/Layer/anchorName.test.ts +146 -0
  107. package/src/Layer/anchorName.ts +46 -0
  108. package/src/Layer/useLayer.doc.mjs +7 -2
  109. package/src/Layer/useLayer.tsx +33 -9
  110. package/src/Layout/Layout.doc.mjs +1 -1
  111. package/src/Lightbox/Lightbox.doc.mjs +0 -2
  112. package/src/Link/Link.test.tsx +2 -2
  113. package/src/Link/Link.tsx +3 -3
  114. package/src/Link/LinkProvider.tsx +1 -0
  115. package/src/List/List.doc.mjs +7 -0
  116. package/src/Markdown/Markdown.doc.mjs +2 -0
  117. package/src/Markdown/Markdown.test.tsx +17 -26
  118. package/src/Markdown/Markdown.tsx +16 -6
  119. package/src/MobileNav/MobileNav.doc.mjs +8 -8
  120. package/src/MobileNav/MobileNav.tsx +13 -0
  121. package/src/MobileNav/MobileNavReopen.test.tsx +118 -0
  122. package/src/OverflowList/OverflowList.doc.mjs +17 -0
  123. package/src/Pagination/Pagination.test.tsx +137 -13
  124. package/src/Pagination/Pagination.tsx +33 -28
  125. package/src/ProgressBar/ProgressBar.test.tsx +13 -3
  126. package/src/Resizable/Resizable.doc.mjs +1 -1
  127. package/src/Section/Section.doc.mjs +11 -0
  128. package/src/SelectableCard/SelectableCard.doc.mjs +15 -0
  129. package/src/Selector/Selector.doc.mjs +4 -0
  130. package/src/Skeleton/Skeleton.doc.mjs +11 -1
  131. package/src/Slider/Slider.doc.mjs +4 -0
  132. package/src/Slider/Slider.test.tsx +3 -16
  133. package/src/Slider/Slider.tsx +1 -1
  134. package/src/TabList/TabList.doc.mjs +6 -0
  135. package/src/Table/BaseTable.tsx +51 -24
  136. package/src/Table/Table.doc.mjs +12 -0
  137. package/src/Table/Table.test.tsx +29 -30
  138. package/src/Table/Table.tsx +22 -1
  139. package/src/Table/TableRow.tsx +37 -7
  140. package/src/Table/index.ts +3 -0
  141. package/src/Table/plugins/stickyColumns/index.ts +4 -0
  142. package/src/Table/plugins/stickyColumns/useTableStickyColumns.test.tsx +163 -0
  143. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +407 -0
  144. package/src/Table/types.ts +101 -4
  145. package/src/Table/useBaseTablePlugins.ts +1 -0
  146. package/src/Table/useTableColumnResize.doc.mjs +96 -0
  147. package/src/Table/useTableStickyColumns.doc.mjs +59 -0
  148. package/src/Text/Heading.doc.mjs +15 -2
  149. package/src/Text/Text.doc.mjs +7 -1
  150. package/src/Text/text.stylex.ts +2 -2
  151. package/src/Timestamp/Timestamp.doc.mjs +9 -0
  152. package/src/Timestamp/Timestamp.test.tsx +44 -18
  153. package/src/Timestamp/Timestamp.tsx +30 -10
  154. package/src/ToggleButton/ToggleButton.doc.mjs +9 -3
  155. package/src/ToggleButton/ToggleButton.test.tsx +39 -18
  156. package/src/ToggleButton/ToggleButton.tsx +46 -83
  157. package/src/Tokenizer/Tokenizer.tsx +4 -1
  158. package/src/Toolbar/Toolbar.doc.mjs +65 -5
  159. package/src/Tooltip/Tooltip.test.tsx +13 -0
  160. package/src/Tooltip/useTooltip.tsx +2 -3
  161. package/src/hooks/useImageMode.test.ts +133 -0
  162. package/src/theme/derivedVarRegistry.test.ts +4 -0
  163. package/src/theme/generateThemeRules.test.ts +7 -8
  164. package/src/theme/generateThemeRules.ts +1 -1
  165. package/src/theme/syntax/SyntaxTheme.tsx +1 -1
  166. package/src/theme/syntax/defineSyntaxTheme.ts +20 -11
  167. package/src/theme/syntax/presets.ts +5 -2
  168. package/src/theme/syntax/tokens.ts +1 -1
  169. package/src/theme/types.ts +1 -1
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi} from 'vitest';
13
- import {render, screen, within} from '@testing-library/react';
13
+ import {render, screen, within, fireEvent, act} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {Pagination, generatePageRange} from './Pagination';
16
16
 
@@ -346,6 +346,140 @@ describe('Pagination', () => {
346
346
  });
347
347
  });
348
348
 
349
+ // ---------------------------------------------------------------------------
350
+ // changeAction (interruptible, optimistic)
351
+ // ---------------------------------------------------------------------------
352
+
353
+ describe('changeAction', () => {
354
+ it('fires onChange then changeAction with the new page', async () => {
355
+ const user = userEvent.setup();
356
+ const order: string[] = [];
357
+ const onChange = vi.fn(() => order.push('onChange'));
358
+ const changeAction = vi.fn(() => {
359
+ order.push('changeAction');
360
+ });
361
+ render(
362
+ <Pagination
363
+ page={1}
364
+ onChange={onChange}
365
+ changeAction={changeAction}
366
+ totalPages={5}
367
+ />,
368
+ );
369
+ await user.click(screen.getByRole('button', {name: 'Go to next page'}));
370
+ expect(onChange).toHaveBeenCalledWith(2);
371
+ expect(changeAction).toHaveBeenCalledWith(2);
372
+ expect(order).toEqual(['onChange', 'changeAction']);
373
+ });
374
+
375
+ it('shows the optimistic page while changeAction is pending', async () => {
376
+ const user = userEvent.setup();
377
+ let resolveAction: (() => void) | undefined;
378
+ const changeAction = vi.fn(
379
+ async () =>
380
+ new Promise<void>(resolve => {
381
+ resolveAction = resolve;
382
+ }),
383
+ );
384
+ render(
385
+ <Pagination
386
+ page={1}
387
+ onChange={() => {}}
388
+ changeAction={changeAction}
389
+ totalPages={5}
390
+ variant="compact"
391
+ />,
392
+ );
393
+
394
+ // The committed `page` prop stays at 1, but the indicator optimistically
395
+ // reflects the page being navigated to.
396
+ await user.click(screen.getByRole('button', {name: 'Go to next page'}));
397
+ expect(changeAction).toHaveBeenCalledWith(2);
398
+ expect(screen.getByText('Page 2 of 5')).toBeInTheDocument();
399
+
400
+ await act(async () => {
401
+ resolveAction?.();
402
+ await Promise.resolve();
403
+ });
404
+ });
405
+
406
+ it('interrupts an in-flight action on rapid next clicks', async () => {
407
+ // Each click derives its target from the optimistic page, so clicking
408
+ // next twice before the action settles advances 1 -> 2 -> 3 instead of
409
+ // being dropped by a re-entry guard.
410
+ const resolvers: (() => void)[] = [];
411
+ const changeAction = vi.fn(
412
+ async () =>
413
+ new Promise<void>(resolve => {
414
+ resolvers.push(resolve);
415
+ }),
416
+ );
417
+ render(
418
+ <Pagination
419
+ page={1}
420
+ onChange={() => {}}
421
+ changeAction={changeAction}
422
+ totalPages={5}
423
+ variant="compact"
424
+ />,
425
+ );
426
+
427
+ const next = screen.getByRole('button', {name: 'Go to next page'});
428
+ await act(async () => {
429
+ fireEvent.click(next);
430
+ });
431
+ expect(screen.getByText('Page 2 of 5')).toBeInTheDocument();
432
+ await act(async () => {
433
+ fireEvent.click(next);
434
+ });
435
+ expect(screen.getByText('Page 3 of 5')).toBeInTheDocument();
436
+
437
+ expect(changeAction).toHaveBeenCalledTimes(2);
438
+ expect(changeAction).toHaveBeenNthCalledWith(1, 2);
439
+ expect(changeAction).toHaveBeenNthCalledWith(2, 3);
440
+
441
+ await act(async () => {
442
+ resolvers.forEach(resolve => resolve());
443
+ await Promise.resolve();
444
+ });
445
+ });
446
+
447
+ it('supports a synchronous changeAction', async () => {
448
+ const user = userEvent.setup();
449
+ const changeAction = vi.fn((_page: number) => {});
450
+ const onChange = vi.fn();
451
+ render(
452
+ <Pagination
453
+ page={2}
454
+ onChange={onChange}
455
+ changeAction={changeAction}
456
+ totalPages={5}
457
+ />,
458
+ );
459
+ await user.click(
460
+ screen.getByRole('button', {name: 'Go to previous page'}),
461
+ );
462
+ expect(onChange).toHaveBeenCalledWith(1);
463
+ expect(changeAction).toHaveBeenCalledWith(1);
464
+ });
465
+
466
+ it('does not fire changeAction when disabled', async () => {
467
+ const user = userEvent.setup();
468
+ const changeAction = vi.fn();
469
+ render(
470
+ <Pagination
471
+ page={1}
472
+ onChange={() => {}}
473
+ changeAction={changeAction}
474
+ totalPages={5}
475
+ isDisabled
476
+ />,
477
+ );
478
+ await user.click(screen.getByRole('button', {name: 'Go to next page'}));
479
+ expect(changeAction).not.toHaveBeenCalled();
480
+ });
481
+ });
482
+
349
483
  // ---------------------------------------------------------------------------
350
484
  // Boundary states
351
485
  // ---------------------------------------------------------------------------
@@ -459,12 +593,7 @@ describe('Pagination', () => {
459
593
  describe('disabled state', () => {
460
594
  it('disables all page buttons when isDisabled', () => {
461
595
  render(
462
- <Pagination
463
- page={3}
464
- onChange={() => {}}
465
- totalPages={5}
466
- isDisabled
467
- />,
596
+ <Pagination page={3} onChange={() => {}} totalPages={5} isDisabled />,
468
597
  );
469
598
  expect(
470
599
  screen.getByRole('button', {name: 'Go to previous page'}),
@@ -480,12 +609,7 @@ describe('Pagination', () => {
480
609
  const user = userEvent.setup();
481
610
  const onChange = vi.fn();
482
611
  render(
483
- <Pagination
484
- page={3}
485
- onChange={onChange}
486
- totalPages={5}
487
- isDisabled
488
- />,
612
+ <Pagination page={3} onChange={onChange} totalPages={5} isDisabled />,
489
613
  );
490
614
  // Disabled buttons can't be clicked
491
615
  await user.click(screen.getByRole('button', {name: 'Go to page 1'}));
@@ -19,7 +19,7 @@
19
19
  * label, data-testid, xstyle
20
20
  */
21
21
 
22
- import {useTransition} from 'react';
22
+ import {useOptimistic, useTransition} from 'react';
23
23
  import * as stylex from '@stylexjs/stylex';
24
24
  import {
25
25
  colorVars,
@@ -349,16 +349,22 @@ export function Pagination({
349
349
  style,
350
350
  ref,
351
351
  }: PaginationProps) {
352
- const [isPending, startTransition] = useTransition();
352
+ const [, startTransition] = useTransition();
353
+
354
+ // Track the page optimistically so rapid prev/next clicks advance from the
355
+ // in-flight target instead of stalling on the last committed page.
356
+ const [optimisticPage, setOptimisticPage] = useOptimistic(page);
353
357
 
354
358
  // Compute pagination state
355
359
  const computedTotalPages =
356
360
  totalPagesProp ??
357
361
  (totalItems != null ? Math.ceil(totalItems / pageSize) : undefined);
358
362
 
359
- const hasPrevious = page > 1;
363
+ const hasPrevious = optimisticPage > 1;
360
364
  const hasNext =
361
- computedTotalPages != null ? page < computedTotalPages : (hasMore ?? false);
365
+ computedTotalPages != null
366
+ ? optimisticPage < computedTotalPages
367
+ : (hasMore ?? false);
362
368
 
363
369
  // Return null for empty state
364
370
  if (totalItems != null && totalItems <= 0) {
@@ -368,48 +374,47 @@ export function Pagination({
368
374
  return null;
369
375
  }
370
376
 
377
+ // Interruptible: re-clicking before the transition settles starts a fresh one
378
+ // with the next optimistic page rather than being dropped, so there is no
379
+ // re-entry guard.
371
380
  const handlePageChange = (newPage: number) => {
372
- if (isDisabled || isPending) {
381
+ if (isDisabled) {
373
382
  return;
374
383
  }
384
+ // Keep onChange urgent so controlled page state updates in the same commit
385
+ // as the click; only the optimistic indicator and changeAction defer.
375
386
  onChange(newPage);
376
- if (changeAction) {
377
- startTransition(async () => {
378
- await changeAction(newPage);
379
- });
380
- }
387
+ startTransition(async () => {
388
+ setOptimisticPage(newPage);
389
+ await changeAction?.(newPage);
390
+ });
381
391
  };
382
392
 
383
393
  const handlePrevious = () => {
384
394
  if (hasPrevious) {
385
- handlePageChange(page - 1);
395
+ handlePageChange(optimisticPage - 1);
386
396
  }
387
397
  };
388
398
 
389
399
  const handleNext = () => {
390
400
  if (hasNext) {
391
- handlePageChange(page + 1);
401
+ handlePageChange(optimisticPage + 1);
392
402
  }
393
403
  };
394
404
 
395
405
  const handlePageSizeChange = (value: string) => {
396
406
  const newSize = Number(value);
397
407
  onPageSizeChange?.(newSize);
398
- // Reset to page 1 when page size changes
399
- onChange(1);
400
- if (changeAction) {
401
- startTransition(async () => {
402
- await changeAction(1);
403
- });
404
- }
408
+ // Reset to page 1 when page size changes.
409
+ handlePageChange(1);
405
410
  };
406
411
 
407
412
  // Item range for count display
408
- const rangeStart = (page - 1) * pageSize + 1;
413
+ const rangeStart = (optimisticPage - 1) * pageSize + 1;
409
414
  const rangeEnd =
410
415
  totalItems != null
411
- ? Math.min(page * pageSize, totalItems)
412
- : page * pageSize;
416
+ ? Math.min(optimisticPage * pageSize, totalItems)
417
+ : optimisticPage * pageSize;
413
418
 
414
419
  const buttonSize = size === 'sm' ? 'sm' : 'md';
415
420
  const isSm = size === 'sm';
@@ -421,7 +426,7 @@ export function Pagination({
421
426
  return null;
422
427
  }
423
428
  const pageRange = generatePageRange(
424
- page,
429
+ optimisticPage,
425
430
  computedTotalPages,
426
431
  siblingCount,
427
432
  );
@@ -443,7 +448,7 @@ export function Pagination({
443
448
  </span>
444
449
  );
445
450
  }
446
- const isActive = item === page;
451
+ const isActive = item === optimisticPage;
447
452
  return (
448
453
  <Button
449
454
  key={item}
@@ -483,7 +488,7 @@ export function Pagination({
483
488
  return (
484
489
  <span {...stylex.props(styles.infoText)}>
485
490
  <Text type="body" size="sm" color="secondary">
486
- {`Page ${page} of ${computedTotalPages}`}
491
+ {`Page ${optimisticPage} of ${computedTotalPages}`}
487
492
  </Text>
488
493
  </span>
489
494
  );
@@ -503,18 +508,18 @@ export function Pagination({
503
508
  key={i + 1}
504
509
  type="button"
505
510
  aria-label={`Go to page ${i + 1}`}
506
- aria-current={i + 1 === page ? 'page' : undefined}
511
+ aria-current={i + 1 === optimisticPage ? 'page' : undefined}
507
512
  onClick={() => handlePageChange(i + 1)}
508
513
  disabled={isDisabled}
509
514
  {...mergeProps(
510
515
  themeProps('pagination-dot', {
511
- active: i + 1 === page ? 'active' : null,
516
+ active: i + 1 === optimisticPage ? 'active' : null,
512
517
  size,
513
518
  }),
514
519
  stylex.props(
515
520
  styles.dot,
516
521
  isSm && styles.dotSm,
517
- i + 1 === page && styles.dotActive,
522
+ i + 1 === optimisticPage && styles.dotActive,
518
523
  isDisabled && styles.dotDisabled,
519
524
  ),
520
525
  )}
@@ -79,9 +79,7 @@ describe('ProgressBar', () => {
79
79
  });
80
80
 
81
81
  it('passes data-testid', () => {
82
- render(
83
- <ProgressBar value={50} label="Test" data-testid="my-progress" />,
84
- );
82
+ render(<ProgressBar value={50} label="Test" data-testid="my-progress" />);
85
83
  expect(screen.getByTestId('my-progress')).toBeInTheDocument();
86
84
  });
87
85
 
@@ -115,6 +113,18 @@ describe('ProgressBar', () => {
115
113
  expect(screen.getByText('Hidden')).toBeInTheDocument();
116
114
  });
117
115
 
116
+ it('renders no visible value label when isLabelHidden without hasValueLabel', () => {
117
+ // Mirrors the intended "accessible label only" composition: the text
118
+ // label is kept for assistive tech (visually hidden) while no extra
119
+ // visible value label is surfaced.
120
+ render(<ProgressBar value={42} label="Context usage" isLabelHidden />);
121
+ expect(screen.queryByText('42%')).not.toBeInTheDocument();
122
+ const label = screen.getByText('Context usage');
123
+ expect(label).toBeInTheDocument();
124
+ const meter = screen.getByRole('meter');
125
+ expect(meter).toHaveAttribute('aria-labelledby', label.id);
126
+ });
127
+
118
128
  it('handles zero max gracefully', () => {
119
129
  render(<ProgressBar value={0} max={0} label="Empty" />);
120
130
  const meter = screen.getByRole('meter');
@@ -147,7 +147,7 @@ export const docs = {
147
147
  type: 'boolean',
148
148
  description:
149
149
  'Show the pill grip at rest instead of only on hover. Use when discoverability is important.',
150
- default: 'false',
150
+ default: 'true',
151
151
  },
152
152
  {
153
153
  name: 'pillPlacement',
@@ -53,6 +53,11 @@ export const docs = {
53
53
  description: 'Internal padding using the spacing scale (0, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10). Use padding={0} for edge-to-edge content.',
54
54
  default: '4',
55
55
  },
56
+ {
57
+ name: 'paddingBlock',
58
+ type: 'SpacingStep',
59
+ description: 'Block (vertical) padding override. Overrides only the block-axis padding while preserving inline padding from `padding` or the container theme default. Accepts the spacing scale (0, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10).',
60
+ },
56
61
  {
57
62
  name: 'xstyle',
58
63
  type: 'StyleXStyles',
@@ -130,6 +135,11 @@ export const docsZh = {
130
135
  description: '使用间距比例的内部内边距(0, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10)。使用 padding={0} 实现全宽内容。',
131
136
  default: '4',
132
137
  },
138
+ {
139
+ name: 'paddingBlock',
140
+ type: 'SpacingStep',
141
+ description: '块(垂直)方向内边距覆盖。仅覆盖块轴内边距,同时保留来自 padding 或容器主题默认值的行内内边距。使用间距比例(0, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10)。',
142
+ },
133
143
  {
134
144
  name: 'xstyle',
135
145
  type: 'StyleXStyles',
@@ -182,6 +192,7 @@ export const docsDense = {
182
192
  children: 'Content rendered inside section.',
183
193
  dividers: 'Which sides of section have divider borders.',
184
194
  padding: 'Internal padding via spacing scale; 0 for edge-to-edge content.',
195
+ paddingBlock: 'Block-axis padding override; preserves inline padding from padding/theme.',
185
196
  xstyle: 'StyleX styles for layout customization; must be stylex.create() value.',
186
197
  },
187
198
  };
@@ -36,6 +36,21 @@ export const docs = {
36
36
  container: true,
37
37
  targets: [{className: 'astryx-selectable-card', visualProps: ['selected']}],
38
38
  },
39
+ playground: {
40
+ defaults: {
41
+ label: 'Pro plan',
42
+ isSelected: true,
43
+ padding: 4,
44
+ children: {
45
+ __element: 'XDSVStack',
46
+ props: {gap: 1},
47
+ children: [
48
+ {__element: 'XDSHeading', props: {level: 3}, children: 'Pro plan'},
49
+ {__element: 'XDSText', props: {type: 'body'}, children: '$29/month — unlimited projects and priority support.'},
50
+ ],
51
+ },
52
+ },
53
+ },
39
54
  };
40
55
 
41
56
  /** @type {import('../docs-types').TranslationDoc} */
@@ -84,11 +84,13 @@ export const docs = {
84
84
  name: 'isDisabled',
85
85
  type: 'boolean',
86
86
  description: 'Disables the selector.',
87
+ default: 'false',
87
88
  },
88
89
  {
89
90
  name: 'isLabelHidden',
90
91
  type: 'boolean',
91
92
  description: 'Visually hides the label while keeping it accessible.',
93
+ default: 'false',
92
94
  },
93
95
  {
94
96
  name: 'description',
@@ -99,11 +101,13 @@ export const docs = {
99
101
  name: 'isOptional',
100
102
  type: 'boolean',
101
103
  description: 'Marks the field as optional.',
104
+ default: 'false',
102
105
  },
103
106
  {
104
107
  name: 'isRequired',
105
108
  type: 'boolean',
106
109
  description: 'Marks the field as required.',
110
+ default: 'false',
107
111
  },
108
112
  {
109
113
  name: 'status',
@@ -33,7 +33,17 @@ export const docs = {
33
33
  'Index for staggered animation timing. For element at index n, animation starts at DELAY_TIME + (STAGGER_TIME × n).',
34
34
  default: '0',
35
35
  },
36
- ], theming: {
36
+ ],
37
+ playground: {
38
+ // Skeleton width/height default to '100%', which collapses to a zero-size
39
+ // (invisible) element in the properties-tab preview. Give the example
40
+ // explicit pixel dimensions so the shimmer placeholder is visible.
41
+ defaults: {
42
+ width: 320,
43
+ height: 80,
44
+ },
45
+ },
46
+ theming: {
37
47
  targets: [
38
48
  {className: 'astryx-skeleton'},
39
49
  ],
@@ -11,6 +11,10 @@ export const docs = {
11
11
  defaults: {
12
12
  label: 'Volume',
13
13
  value: 50,
14
+ // Give the properties-tab preview a defined width so the track has room
15
+ // to render and stays draggable/interactive (the slider track grows to
16
+ // fill its container, which can collapse without an explicit width).
17
+ width: 300,
14
18
  },
15
19
  },
16
20
  props: [
@@ -24,9 +24,7 @@ describe('Slider', () => {
24
24
  });
25
25
 
26
26
  it('range thumbs have correct aria-labels', () => {
27
- render(
28
- <Slider label="Price range" value={[20, 80] as [number, number]} />,
29
- );
27
+ render(<Slider label="Price range" value={[20, 80] as [number, number]} />);
30
28
  const sliders = screen.getAllByRole('slider');
31
29
  expect(sliders[0]).toHaveAttribute(
32
30
  'aria-label',
@@ -172,12 +170,7 @@ describe('Slider', () => {
172
170
  it('does not fire onChange on keyboard when disabled', () => {
173
171
  const handleChange = vi.fn();
174
172
  render(
175
- <Slider
176
- label="Volume"
177
- value={50}
178
- onChange={handleChange}
179
- isDisabled
180
- />,
173
+ <Slider label="Volume" value={50} onChange={handleChange} isDisabled />,
181
174
  );
182
175
  const slider = screen.getByRole('slider');
183
176
  fireEvent.keyDown(slider, {key: 'ArrowRight'});
@@ -297,13 +290,7 @@ describe('Slider', () => {
297
290
 
298
291
  it('focuses closest thumb on track click', () => {
299
292
  render(
300
- <Slider
301
- label="Volume"
302
- value={50}
303
- min={0}
304
- max={100}
305
- onChange={vi.fn()}
306
- />,
293
+ <Slider label="Volume" value={50} min={0} max={100} onChange={vi.fn()} />,
307
294
  );
308
295
  const slider = screen.getByRole('slider');
309
296
  const trackContainer = slider.parentElement!;
@@ -159,7 +159,7 @@ const styles = stylex.create({
159
159
  },
160
160
  track: {
161
161
  position: 'absolute',
162
- backgroundColor: colorVars['--color-background-muted'],
162
+ backgroundColor: colorVars['--color-track'],
163
163
  borderRadius: radiusVars['--radius-full'],
164
164
  },
165
165
  trackHorizontal: {
@@ -43,6 +43,12 @@ export const docs = {
43
43
  description: 'Size variant applied to all child tabs.',
44
44
  default: "'md'",
45
45
  },
46
+ {
47
+ name: 'layout',
48
+ type: "'hug' | 'fill'",
49
+ description: "Layout mode for tab sizing. 'hug': each tab hugs its content width. 'fill': tabs stretch equally to fill the container width.",
50
+ default: "'hug'",
51
+ },
46
52
  {
47
53
  name: 'hasDivider',
48
54
  type: 'boolean',