@astryxdesign/core 0.1.0 → 0.1.1-canary.080d887

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 (170) hide show
  1. package/CHANGELOG.md +66 -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/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  13. package/dist/Chat/ChatLayoutScrollButton.js +5 -1
  14. package/dist/ContextMenu/ContextMenu.js +2 -2
  15. package/dist/DropdownMenu/DropdownMenu.js +2 -2
  16. package/dist/DropdownMenu/{renderXDSDropdownItems.d.ts → renderDropdownItems.d.ts} +3 -3
  17. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -0
  18. package/dist/DropdownMenu/{renderXDSDropdownItems.js → renderDropdownItems.js} +2 -2
  19. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  20. package/dist/EmptyState/EmptyState.js +7 -1
  21. package/dist/HoverCard/HoverCard.d.ts +2 -2
  22. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  23. package/dist/HoverCard/HoverCard.js +18 -6
  24. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  25. package/dist/HoverCard/useHoverCard.js +6 -3
  26. package/dist/Kbd/Kbd.d.ts.map +1 -1
  27. package/dist/Kbd/Kbd.js +1 -1
  28. package/dist/Layer/useLayer.d.ts +13 -0
  29. package/dist/Layer/useLayer.d.ts.map +1 -1
  30. package/dist/Layer/useLayer.js +7 -2
  31. package/dist/Layout/Layout.d.ts +10 -1
  32. package/dist/Layout/Layout.d.ts.map +1 -1
  33. package/dist/Layout/Layout.js +5 -1
  34. package/dist/Markdown/Markdown.d.ts.map +1 -1
  35. package/dist/Markdown/Markdown.js +13 -3
  36. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  37. package/dist/MobileNav/MobileNav.js +13 -0
  38. package/dist/Outline/Outline.d.ts +3 -2
  39. package/dist/Outline/Outline.d.ts.map +1 -1
  40. package/dist/Outline/Outline.js +23 -4
  41. package/dist/Outline/useScrollSpy.d.ts +14 -1
  42. package/dist/Outline/useScrollSpy.d.ts.map +1 -1
  43. package/dist/Outline/useScrollSpy.js +161 -50
  44. package/dist/Pagination/Pagination.d.ts.map +1 -1
  45. package/dist/Pagination/Pagination.js +31 -27
  46. package/dist/Resizable/useResizable.d.ts.map +1 -1
  47. package/dist/Resizable/useResizable.js +1 -5
  48. package/dist/Selector/Selector.d.ts.map +1 -1
  49. package/dist/Selector/Selector.js +1 -1
  50. package/dist/Slider/Slider.js +2 -2
  51. package/dist/Table/BaseTable.d.ts.map +1 -1
  52. package/dist/Table/BaseTable.js +26 -8
  53. package/dist/Table/Table.d.ts.map +1 -1
  54. package/dist/Table/Table.js +30 -7
  55. package/dist/Table/index.d.ts +3 -1
  56. package/dist/Table/index.d.ts.map +1 -1
  57. package/dist/Table/index.js +1 -0
  58. package/dist/Table/plugins/stickyColumns/index.d.ts +3 -0
  59. package/dist/Table/plugins/stickyColumns/index.d.ts.map +1 -0
  60. package/dist/Table/plugins/stickyColumns/index.js +3 -0
  61. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts +25 -0
  62. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -0
  63. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +376 -0
  64. package/dist/Table/types.d.ts +90 -5
  65. package/dist/Table/types.d.ts.map +1 -1
  66. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  67. package/dist/Table/useBaseTablePlugins.js +1 -1
  68. package/dist/Timestamp/Timestamp.d.ts +1 -1
  69. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  70. package/dist/Timestamp/Timestamp.js +28 -3
  71. package/dist/ToggleButton/ToggleButton.d.ts +10 -5
  72. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  73. package/dist/ToggleButton/ToggleButton.js +40 -17
  74. package/dist/astryx.css +20 -0
  75. package/dist/astryx.umd.js +147 -0
  76. package/dist/astryx.umd.js.map +7 -0
  77. package/dist/theme/Theme.js +1 -1
  78. package/dist/theme/defineTheme.d.ts +1 -1
  79. package/dist/theme/defineTheme.d.ts.map +1 -1
  80. package/dist/theme/defineTheme.js +1 -1
  81. package/dist/theme/index.d.ts +1 -1
  82. package/dist/theme/index.d.ts.map +1 -1
  83. package/dist/theme/index.js +1 -1
  84. package/dist/theme/syntax/defineSyntaxTheme.js +1 -1
  85. package/dist/theme/tokens.d.ts +1 -1
  86. package/dist/theme/tokens.js +4 -4
  87. package/dist/theme/useTheme.d.ts +2 -2
  88. package/dist/utils/dateParser.d.ts.map +1 -1
  89. package/dist/utils/dateParser.js +15 -2
  90. package/package.json +7 -3
  91. package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -0
  92. package/src/Banner/Banner.test.tsx +16 -7
  93. package/src/Banner/Banner.tsx +9 -2
  94. package/src/Button/Button.doc.mjs +6 -0
  95. package/src/Button/Button.test.tsx +105 -11
  96. package/src/Button/Button.tsx +73 -5
  97. package/src/Chat/ChatLayoutScrollButton.tsx +7 -1
  98. package/src/Collapsible/useCollapsible.doc.mjs +2 -2
  99. package/src/ContextMenu/ContextMenu.tsx +2 -2
  100. package/src/DateInput/DateInput.test.tsx +68 -20
  101. package/src/Divider/Divider.doc.mjs +1 -1
  102. package/src/DropdownMenu/DropdownMenu.tsx +2 -2
  103. package/src/DropdownMenu/{renderXDSDropdownItems.tsx → renderDropdownItems.tsx} +2 -2
  104. package/src/EmptyState/EmptyState.test.tsx +4 -2
  105. package/src/EmptyState/EmptyState.tsx +6 -2
  106. package/src/FormLayout/FormLayout.doc.mjs +3 -3
  107. package/src/HoverCard/HoverCard.doc.mjs +3 -0
  108. package/src/HoverCard/HoverCard.test.tsx +178 -2
  109. package/src/HoverCard/HoverCard.tsx +20 -16
  110. package/src/HoverCard/useHoverCard.tsx +12 -10
  111. package/src/Icon/Icon.doc.mjs +12 -4
  112. package/src/Item/Item.doc.mjs +2 -2
  113. package/src/Kbd/Kbd.tsx +2 -9
  114. package/src/Layer/useLayer.doc.mjs +7 -2
  115. package/src/Layer/useLayer.tsx +19 -2
  116. package/src/Layout/Layout.doc.mjs +2 -1
  117. package/src/Layout/Layout.tsx +15 -1
  118. package/src/Layout/__tests__/childrenAsContent.test.tsx +59 -0
  119. package/src/Lightbox/Lightbox.doc.mjs +0 -2
  120. package/src/Link/Link.doc.mjs +3 -3
  121. package/src/Link/LinkProvider.doc.mjs +3 -3
  122. package/src/Markdown/Markdown.doc.mjs +6 -4
  123. package/src/Markdown/Markdown.test.tsx +17 -26
  124. package/src/Markdown/Markdown.tsx +16 -6
  125. package/src/MobileNav/MobileNav.doc.mjs +8 -8
  126. package/src/MobileNav/MobileNav.tsx +13 -0
  127. package/src/MobileNav/MobileNavReopen.test.tsx +118 -0
  128. package/src/Outline/Outline.doc.mjs +1 -1
  129. package/src/Outline/Outline.test.tsx +76 -38
  130. package/src/Outline/Outline.tsx +23 -4
  131. package/src/Outline/useScrollSpy.ts +196 -63
  132. package/src/Pagination/Pagination.test.tsx +137 -13
  133. package/src/Pagination/Pagination.tsx +33 -28
  134. package/src/Resizable/Resizable.doc.mjs +3 -3
  135. package/src/Resizable/useResizable.ts +1 -7
  136. package/src/Selector/Selector.doc.mjs +4 -0
  137. package/src/Selector/Selector.tsx +5 -6
  138. package/src/Skeleton/Skeleton.doc.mjs +11 -1
  139. package/src/Slider/Slider.doc.mjs +4 -0
  140. package/src/Slider/Slider.test.tsx +3 -16
  141. package/src/Slider/Slider.tsx +1 -1
  142. package/src/Table/BaseTable.tsx +50 -24
  143. package/src/Table/Table.doc.mjs +15 -3
  144. package/src/Table/Table.tsx +22 -1
  145. package/src/Table/index.ts +3 -0
  146. package/src/Table/plugins/stickyColumns/index.ts +4 -0
  147. package/src/Table/plugins/stickyColumns/useTableStickyColumns.test.tsx +163 -0
  148. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +414 -0
  149. package/src/Table/types.ts +96 -4
  150. package/src/Table/useBaseTablePlugins.ts +1 -0
  151. package/src/Text/Heading.doc.mjs +13 -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 +4 -4
  155. package/src/ToggleButton/ToggleButton.test.tsx +172 -9
  156. package/src/ToggleButton/ToggleButton.tsx +57 -31
  157. package/src/Toolbar/Toolbar.doc.mjs +1 -1
  158. package/src/hooks/useEntryAnimation.doc.mjs +3 -3
  159. package/src/hooks/useMediaQuery.doc.mjs +2 -2
  160. package/src/hooks/useStreamingText.doc.mjs +3 -3
  161. package/src/theme/Theme.doc.mjs +2 -2
  162. package/src/theme/Theme.tsx +1 -1
  163. package/src/theme/defineTheme.ts +1 -1
  164. package/src/theme/index.ts +1 -1
  165. package/src/theme/syntax/defineSyntaxTheme.ts +1 -1
  166. package/src/theme/tokens.ts +4 -4
  167. package/src/theme/useTheme.ts +2 -2
  168. package/src/utils/dateParser.test.ts +26 -0
  169. package/src/utils/dateParser.ts +16 -2
  170. package/dist/DropdownMenu/renderXDSDropdownItems.d.ts.map +0 -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
  )}
@@ -27,7 +27,7 @@ export const docs = {
27
27
  {
28
28
  guidance: true,
29
29
  description:
30
- 'Use useResizable() with existing XDS layout components. ' +
30
+ 'Use useResizable() with existing Astryx layout components. ' +
31
31
  'Pass the returned props to the resizable prop on LayoutPanel or SideNav.',
32
32
  },
33
33
  {
@@ -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',
@@ -195,7 +195,7 @@ export const docsDense = {
195
195
  description:
196
196
  'Hook-based resizable panel system. useResizable() manages size state; ResizeHandle provides interactive pill-grip separator. Pass resize props to existing layout components via their resizable prop.',
197
197
  bestPractices: [
198
- {guidance: true, description: 'Use useResizable() w/ existing XDS layout components. Pass returned props to resizable prop on LayoutPanel or SideNav.'},
198
+ {guidance: true, description: 'Use useResizable() w/ existing Astryx layout components. Pass returned props to resizable prop on LayoutPanel or SideNav.'},
199
199
  {guidance: true, description: 'Provide accessible label on each ResizeHandle when multiple handles exist (e.g. "Resize sidebar", "Resize terminal").'},
200
200
  {guidance: false, description: 'Wrap panels in extra container components for resize. Hook-first architecture avoids extra DOM; use it directly on existing components.'},
201
201
  ],
@@ -107,10 +107,6 @@ export interface ResizableProps {
107
107
  const DEFAULT_MIN = 50;
108
108
  const DEFAULT_COLLAPSED_SIZE = 40;
109
109
  const STORAGE_PREFIX = 'astryx-resizable:';
110
- // Legacy key prefix read during the compat window so persisted panel sizes
111
- // survive the xds -> astryx rename. Read-only fallback; we always write the
112
- // new prefix. Removed at final cutover.
113
- const LEGACY_STORAGE_PREFIX = 'xds-resizable:';
114
110
 
115
111
  // =============================================================================
116
112
  // Helpers
@@ -147,9 +143,7 @@ function loadPersistedSize(key: string): number | null {
147
143
  return null;
148
144
  }
149
145
  try {
150
- const raw =
151
- localStorage.getItem(STORAGE_PREFIX + key) ??
152
- localStorage.getItem(LEGACY_STORAGE_PREFIX + key);
146
+ const raw = localStorage.getItem(STORAGE_PREFIX + key);
153
147
  if (raw != null) {
154
148
  const parsed = JSON.parse(raw);
155
149
  if (typeof parsed === 'number') {
@@ -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',
@@ -107,12 +107,11 @@ const styles = stylex.create({
107
107
  lineHeight: 'inherit',
108
108
  color: 'inherit',
109
109
  cursor: 'pointer',
110
- outline: {
111
- default: 'none',
112
- ':focus-visible': `${borderVars['--border-width']} solid ${colorVars['--color-accent']}`,
113
- },
114
- outlineOffset: '0',
115
- borderRadius: radiusVars['--radius-element'],
110
+ // The wrapper (inputWrapperStyles.base) renders the focus ring via
111
+ // :focus-within when this button is focused, matching TextInput/NumberInput.
112
+ // The button must not draw its own :focus-visible outline or the two stack
113
+ // into a doubled ring over the trigger.
114
+ outline: 'none',
116
115
  },
117
116
  triggerPlaceholder: {
118
117
  color: colorVars['--color-text-secondary'],
@@ -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: {
@@ -26,6 +26,7 @@ import type {
26
26
  HeaderCellRenderProps,
27
27
  BodyRowRenderProps,
28
28
  BodyCellRenderProps,
29
+ ScrollWrapperRenderProps,
29
30
  TableRowComponentProps,
30
31
  TableCellComponentProps,
31
32
  TableHeaderCellComponentProps,
@@ -152,22 +153,27 @@ function TableRowInner<T extends Record<string, unknown>>({
152
153
  CellComponent,
153
154
  }: TableRowProps<T>): ReactElement {
154
155
  // Build cells first
155
- const cells = columns.map(col => {
156
+ const cells = columns.map((col, columnIndex) => {
156
157
  // Apply column alignment to body cells
157
158
  const initialCellHtmlProps: Record<string, unknown> = {};
158
159
  if (col.align) {
159
160
  initialCellHtmlProps.style = {textAlign: col.align};
160
161
  }
161
162
 
163
+ const initialBodyCellRenderProps: BodyCellRenderProps = {
164
+ htmlProps: initialCellHtmlProps,
165
+ styles: [],
166
+ columnIndex,
167
+ columns: columns as ReadonlyArray<TableColumn<Record<string, unknown>>>,
168
+ };
162
169
  const cellRenderProps = applyPlugins(
163
170
  plugins,
164
171
  p => p.transformBodyCell,
165
- {
166
- htmlProps: initialCellHtmlProps,
167
- styles: [],
168
- } satisfies BodyCellRenderProps,
172
+ initialBodyCellRenderProps,
169
173
  col,
170
174
  item,
175
+ columnIndex,
176
+ columns,
171
177
  );
172
178
 
173
179
  const isDefaultRenderer = !col.renderCell;
@@ -313,8 +319,7 @@ function BaseTableInner<T extends Record<string, unknown>>({
313
319
  // Use stable empty array when no plugins provided
314
320
  const plugins = pluginsProp ?? (EMPTY_PLUGINS as TablePlugin<T>[]);
315
321
 
316
- const RowComponent =
317
- TableRow as React.ComponentType<TableRowComponentProps>;
322
+ const RowComponent = TableRow as React.ComponentType<TableRowComponentProps>;
318
323
  const CellComponent =
319
324
  TableCell as React.ComponentType<TableCellComponentProps>;
320
325
  const HeaderCellComponent =
@@ -354,7 +359,7 @@ function BaseTableInner<T extends Record<string, unknown>>({
354
359
  } satisfies TableRenderProps);
355
360
 
356
361
  // --- Plugin pipeline: header cells ---
357
- const headerCells = resolvedColumns.map(col => {
362
+ const headerCells = resolvedColumns.map((col, columnIndex) => {
358
363
  const headerContent = col.header ?? col.key;
359
364
 
360
365
  // Build initial htmlProps with column alignment if specified
@@ -365,15 +370,22 @@ function BaseTableInner<T extends Record<string, unknown>>({
365
370
  initialHeaderHtmlProps.style = {textAlign: col.align};
366
371
  }
367
372
 
373
+ const initialHeaderRenderProps: HeaderCellRenderProps = {
374
+ htmlProps: initialHeaderHtmlProps,
375
+ styles: [],
376
+ content: headerContent,
377
+ columnIndex,
378
+ columns: resolvedColumns as ReadonlyArray<
379
+ TableColumn<Record<string, unknown>>
380
+ >,
381
+ };
368
382
  const cellRenderProps = applyPlugins(
369
383
  plugins,
370
384
  p => p.transformHeaderCell,
371
- {
372
- htmlProps: initialHeaderHtmlProps,
373
- styles: [],
374
- content: headerContent,
375
- } satisfies HeaderCellRenderProps,
385
+ initialHeaderRenderProps,
376
386
  col,
387
+ columnIndex,
388
+ resolvedColumns,
377
389
  );
378
390
 
379
391
  // Apply pre-computed column width styles on the <th>.
@@ -497,9 +509,7 @@ function BaseTableInner<T extends Record<string, unknown>>({
497
509
  emptyState !== false && (
498
510
  <tr>
499
511
  <td colSpan={resolvedColumns.length}>
500
- {emptyState ?? (
501
- <EmptyState title="No data" isCompact />
502
- )}
512
+ {emptyState ?? <EmptyState title="No data" isCompact />}
503
513
  </td>
504
514
  </tr>
505
515
  )}
@@ -513,8 +523,29 @@ function BaseTableInner<T extends Record<string, unknown>>({
513
523
  // when columns exceed the container width. This wrapper sits between
514
524
  // the <table> and transformTableContext, so plugin chrome (pagination,
515
525
  // toolbars) renders outside the scroll area.
526
+ //
527
+ // Before rendering the wrapper, run the plugin `transformScrollWrapper`
528
+ // pipeline so plugins can attach a ref to the scroll container (scroll-aware
529
+ // sticky shadows, virtualization) and inject before/after chrome.
516
530
  if (ScrollWrapper) {
517
- tableElement = <ScrollWrapper>{tableElement}</ScrollWrapper>;
531
+ const scrollWrapperRenderProps = applyPlugins(
532
+ plugins,
533
+ p => p.transformScrollWrapper,
534
+ {
535
+ htmlProps: {},
536
+ styles: [],
537
+ } satisfies ScrollWrapperRenderProps,
538
+ );
539
+
540
+ tableElement = (
541
+ <ScrollWrapper
542
+ htmlProps={scrollWrapperRenderProps.htmlProps}
543
+ styles={scrollWrapperRenderProps.styles}
544
+ beforeTable={scrollWrapperRenderProps.beforeTable}
545
+ afterTable={scrollWrapperRenderProps.afterTable}>
546
+ {tableElement}
547
+ </ScrollWrapper>
548
+ );
518
549
  }
519
550
 
520
551
  // Apply transformTableContext from each plugin.
@@ -527,10 +558,7 @@ function BaseTableInner<T extends Record<string, unknown>>({
527
558
  try {
528
559
  tableElement = plugin.transformTableContext(tableElement);
529
560
  } catch (error) {
530
- console.error(
531
- '[Table] Plugin threw in transformTableContext:',
532
- error,
533
- );
561
+ console.error('[Table] Plugin threw in transformTableContext:', error);
534
562
  }
535
563
  }
536
564
  }
@@ -556,9 +584,7 @@ function BaseTableInner<T extends Record<string, unknown>>({
556
584
  * />
557
585
  * ```
558
586
  */
559
- export const BaseTable = BaseTableInner as <
560
- T extends Record<string, unknown>,
561
- >(
587
+ export const BaseTable = BaseTableInner as <T extends Record<string, unknown>>(
562
588
  props: BaseTableProps<T> & {ref?: Ref<HTMLTableElement>},
563
589
  ) => ReactElement;
564
590