@astryxdesign/core 0.1.0 → 0.1.1-canary.129bf0e

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 (155) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +68 -0
  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.map +1 -1
  10. package/dist/Button/Button.js +2 -0
  11. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  12. package/dist/Chat/ChatLayoutScrollButton.js +5 -1
  13. package/dist/ContextMenu/ContextMenu.js +2 -2
  14. package/dist/DropdownMenu/DropdownMenu.js +2 -2
  15. package/dist/DropdownMenu/{renderXDSDropdownItems.d.ts → renderDropdownItems.d.ts} +3 -3
  16. package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -0
  17. package/dist/DropdownMenu/{renderXDSDropdownItems.js → renderDropdownItems.js} +2 -2
  18. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  19. package/dist/EmptyState/EmptyState.js +7 -1
  20. package/dist/HoverCard/HoverCard.d.ts +2 -2
  21. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  22. package/dist/HoverCard/HoverCard.js +18 -6
  23. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  24. package/dist/HoverCard/useHoverCard.js +6 -3
  25. package/dist/Layer/useLayer.d.ts +13 -0
  26. package/dist/Layer/useLayer.d.ts.map +1 -1
  27. package/dist/Layer/useLayer.js +7 -2
  28. package/dist/Layout/Layout.d.ts +10 -1
  29. package/dist/Layout/Layout.d.ts.map +1 -1
  30. package/dist/Layout/Layout.js +5 -1
  31. package/dist/Markdown/Markdown.d.ts.map +1 -1
  32. package/dist/Markdown/Markdown.js +13 -3
  33. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  34. package/dist/MobileNav/MobileNav.js +13 -0
  35. package/dist/Outline/Outline.d.ts +3 -2
  36. package/dist/Outline/Outline.d.ts.map +1 -1
  37. package/dist/Outline/Outline.js +23 -4
  38. package/dist/Outline/useScrollSpy.d.ts +14 -1
  39. package/dist/Outline/useScrollSpy.d.ts.map +1 -1
  40. package/dist/Outline/useScrollSpy.js +161 -50
  41. package/dist/Pagination/Pagination.d.ts.map +1 -1
  42. package/dist/Pagination/Pagination.js +31 -27
  43. package/dist/Resizable/useResizable.d.ts.map +1 -1
  44. package/dist/Resizable/useResizable.js +1 -5
  45. package/dist/Selector/Selector.d.ts.map +1 -1
  46. package/dist/Selector/Selector.js +1 -1
  47. package/dist/Table/BaseTable.d.ts.map +1 -1
  48. package/dist/Table/BaseTable.js +26 -8
  49. package/dist/Table/Table.d.ts.map +1 -1
  50. package/dist/Table/Table.js +30 -7
  51. package/dist/Table/index.d.ts +3 -1
  52. package/dist/Table/index.d.ts.map +1 -1
  53. package/dist/Table/index.js +1 -0
  54. package/dist/Table/plugins/stickyColumns/index.d.ts +3 -0
  55. package/dist/Table/plugins/stickyColumns/index.d.ts.map +1 -0
  56. package/dist/Table/plugins/stickyColumns/index.js +3 -0
  57. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts +25 -0
  58. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -0
  59. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +376 -0
  60. package/dist/Table/types.d.ts +90 -5
  61. package/dist/Table/types.d.ts.map +1 -1
  62. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  63. package/dist/Table/useBaseTablePlugins.js +1 -1
  64. package/dist/ToggleButton/ToggleButton.d.ts +10 -3
  65. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  66. package/dist/ToggleButton/ToggleButton.js +64 -18
  67. package/dist/astryx.css +11 -0
  68. package/dist/astryx.umd.js +147 -0
  69. package/dist/astryx.umd.js.map +7 -0
  70. package/dist/theme/Theme.js +1 -1
  71. package/dist/theme/defineTheme.d.ts +1 -1
  72. package/dist/theme/defineTheme.d.ts.map +1 -1
  73. package/dist/theme/defineTheme.js +1 -1
  74. package/dist/theme/index.d.ts +1 -1
  75. package/dist/theme/index.d.ts.map +1 -1
  76. package/dist/theme/index.js +1 -1
  77. package/dist/theme/syntax/defineSyntaxTheme.js +1 -1
  78. package/dist/theme/tokens.d.ts +1 -1
  79. package/dist/theme/tokens.js +4 -4
  80. package/dist/theme/useTheme.d.ts +2 -2
  81. package/dist/utils/dateParser.d.ts.map +1 -1
  82. package/dist/utils/dateParser.js +15 -2
  83. package/package.json +7 -3
  84. package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -0
  85. package/src/Banner/Banner.test.tsx +16 -7
  86. package/src/Banner/Banner.tsx +9 -2
  87. package/src/Button/Button.test.tsx +26 -11
  88. package/src/Button/Button.tsx +2 -0
  89. package/src/Chat/ChatLayoutScrollButton.tsx +7 -1
  90. package/src/Collapsible/useCollapsible.doc.mjs +2 -2
  91. package/src/ContextMenu/ContextMenu.tsx +2 -2
  92. package/src/DateInput/DateInput.test.tsx +68 -20
  93. package/src/Divider/Divider.doc.mjs +1 -1
  94. package/src/DropdownMenu/DropdownMenu.tsx +2 -2
  95. package/src/DropdownMenu/{renderXDSDropdownItems.tsx → renderDropdownItems.tsx} +2 -2
  96. package/src/EmptyState/EmptyState.test.tsx +4 -2
  97. package/src/EmptyState/EmptyState.tsx +6 -2
  98. package/src/FormLayout/FormLayout.doc.mjs +3 -3
  99. package/src/HoverCard/HoverCard.doc.mjs +3 -0
  100. package/src/HoverCard/HoverCard.test.tsx +178 -2
  101. package/src/HoverCard/HoverCard.tsx +20 -16
  102. package/src/HoverCard/useHoverCard.tsx +12 -10
  103. package/src/Icon/Icon.doc.mjs +4 -4
  104. package/src/Item/Item.doc.mjs +2 -2
  105. package/src/Layer/useLayer.doc.mjs +7 -2
  106. package/src/Layer/useLayer.tsx +19 -2
  107. package/src/Layout/Layout.doc.mjs +2 -1
  108. package/src/Layout/Layout.tsx +15 -1
  109. package/src/Layout/__tests__/childrenAsContent.test.tsx +59 -0
  110. package/src/Lightbox/Lightbox.doc.mjs +0 -2
  111. package/src/Link/Link.doc.mjs +3 -3
  112. package/src/Link/LinkProvider.doc.mjs +3 -3
  113. package/src/Markdown/Markdown.doc.mjs +6 -4
  114. package/src/Markdown/Markdown.test.tsx +17 -26
  115. package/src/Markdown/Markdown.tsx +16 -6
  116. package/src/MobileNav/MobileNav.doc.mjs +8 -8
  117. package/src/MobileNav/MobileNav.tsx +13 -0
  118. package/src/MobileNav/MobileNavReopen.test.tsx +118 -0
  119. package/src/Outline/Outline.doc.mjs +1 -1
  120. package/src/Outline/Outline.test.tsx +76 -38
  121. package/src/Outline/Outline.tsx +23 -4
  122. package/src/Outline/useScrollSpy.ts +196 -63
  123. package/src/Pagination/Pagination.test.tsx +137 -13
  124. package/src/Pagination/Pagination.tsx +33 -28
  125. package/src/Resizable/Resizable.doc.mjs +3 -3
  126. package/src/Resizable/useResizable.ts +1 -7
  127. package/src/Selector/Selector.doc.mjs +4 -0
  128. package/src/Selector/Selector.tsx +5 -6
  129. package/src/Skeleton/Skeleton.doc.mjs +11 -1
  130. package/src/Table/BaseTable.tsx +50 -24
  131. package/src/Table/Table.doc.mjs +3 -3
  132. package/src/Table/Table.tsx +22 -1
  133. package/src/Table/index.ts +3 -0
  134. package/src/Table/plugins/stickyColumns/index.ts +4 -0
  135. package/src/Table/plugins/stickyColumns/useTableStickyColumns.test.tsx +163 -0
  136. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +414 -0
  137. package/src/Table/types.ts +96 -4
  138. package/src/Table/useBaseTablePlugins.ts +1 -0
  139. package/src/ToggleButton/ToggleButton.doc.mjs +2 -2
  140. package/src/ToggleButton/ToggleButton.test.tsx +148 -6
  141. package/src/ToggleButton/ToggleButton.tsx +83 -20
  142. package/src/Toolbar/Toolbar.doc.mjs +1 -1
  143. package/src/hooks/useEntryAnimation.doc.mjs +3 -3
  144. package/src/hooks/useMediaQuery.doc.mjs +2 -2
  145. package/src/hooks/useStreamingText.doc.mjs +3 -3
  146. package/src/theme/Theme.doc.mjs +2 -2
  147. package/src/theme/Theme.tsx +1 -1
  148. package/src/theme/defineTheme.ts +1 -1
  149. package/src/theme/index.ts +1 -1
  150. package/src/theme/syntax/defineSyntaxTheme.ts +1 -1
  151. package/src/theme/tokens.ts +4 -4
  152. package/src/theme/useTheme.ts +2 -2
  153. package/src/utils/dateParser.test.ts +26 -0
  154. package/src/utils/dateParser.ts +16 -2
  155. package/dist/DropdownMenu/renderXDSDropdownItems.d.ts.map +0 -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
  ],
@@ -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
 
@@ -104,7 +104,7 @@ export const docs = {
104
104
  'Table displays structured data in rows and columns with consistent dimensionality. It supports rich cell content, sorting, selection, pagination, and column management through a composable plugin system. Use Table for data sets with uniform structure; for simpler or inconsistent data, consider a list or card layout instead.',
105
105
  bestPractices: [
106
106
  { guidance: true, description: 'Use density and divider variants to match the information density and scanning needs of your data.' },
107
- { guidance: true, description: 'Compose rich cell content with XDS components like Badge, StatusDot, and Avatar via renderCell.' },
107
+ { guidance: true, description: 'Compose rich cell content with Astryx components like Badge, StatusDot, and Avatar via renderCell.' },
108
108
  { guidance: true, description: 'Set explicit width on every column using proportional() or pixel(). proportional(1) gives equal flex distribution with a 120px minimum that prevents columns from collapsing on narrow viewports. Omitting width skips the minimum.' },
109
109
  { guidance: false, description: 'Use a table for data without consistent columns. Use a list or card layout for heterogeneous content.' },
110
110
  { guidance: false, description: 'Enable every plugin at once. Add only the features your use case requires to keep the interface focused.' },
@@ -128,7 +128,7 @@ export const docsZh = {
128
128
  'Table displays structured data in rows and columns with consistent dimensionality. It supports rich cell content, sorting, selection, pagination, and column management through a composable plugin system. Use Table for data sets with uniform structure; for simpler or inconsistent data, consider a list or card layout instead.',
129
129
  bestPractices: [
130
130
  { guidance: true, description: 'Use density and divider variants to match the information density and scanning needs of your data.' },
131
- { guidance: true, description: 'Compose rich cell content with XDS components like Badge, StatusDot, and Avatar via renderCell.' },
131
+ { guidance: true, description: 'Compose rich cell content with Astryx components like Badge, StatusDot, and Avatar via renderCell.' },
132
132
  { guidance: false, description: 'Use a table for data without consistent columns. Use a list or card layout for heterogeneous content.' },
133
133
  { guidance: false, description: 'Enable every plugin at once. Add only the features your use case requires to keep the interface focused.' },
134
134
  ],
@@ -151,7 +151,7 @@ export const docsDense = {
151
151
  'Table displays structured data in rows and columns with consistent dimensionality. It supports rich cell content, sorting, selection, pagination, and column management through a composable plugin system. Use Table for data sets with uniform structure; for simpler or inconsistent data, consider a list or card layout instead.',
152
152
  bestPractices: [
153
153
  { guidance: true, description: 'Use density and divider variants to match the information density and scanning needs of your data.' },
154
- { guidance: true, description: 'Compose rich cell content with XDS components like Badge, StatusDot, and Avatar via renderCell.' },
154
+ { guidance: true, description: 'Compose rich cell content with Astryx components like Badge, StatusDot, and Avatar via renderCell.' },
155
155
  { guidance: true, description: 'Set explicit width on every column via proportional() or pixel(). proportional(1) = equal flex w/ 120px min preventing collapse on narrow viewports. Omitting width skips the minimum.' },
156
156
  { guidance: false, description: 'Use a table for data without consistent columns. Use a list or card layout for heterogeneous content.' },
157
157
  { guidance: false, description: 'Enable every plugin at once. Add only the features your use case requires to keep the interface focused.' },
@@ -30,6 +30,7 @@ import type {
30
30
  TablePlugin,
31
31
  TableRenderProps,
32
32
  } from './types';
33
+ import type {StyleXStyles} from '../theme/types';
33
34
 
34
35
  // =============================================================================
35
36
  // Table Types
@@ -131,17 +132,37 @@ const scrollWrapperStyles = stylex.create({
131
132
  },
132
133
  });
133
134
 
134
- function TableScrollWrapper({children}: {children: React.ReactNode}) {
135
+ function TableScrollWrapper({
136
+ children,
137
+ htmlProps,
138
+ styles: pluginStyles,
139
+ beforeTable,
140
+ afterTable,
141
+ }: {
142
+ children: React.ReactNode;
143
+ htmlProps?: React.HTMLAttributes<HTMLDivElement> & {
144
+ ref?: React.Ref<HTMLDivElement>;
145
+ };
146
+ styles?: StyleXStyles[];
147
+ beforeTable?: React.ReactNode;
148
+ afterTable?: React.ReactNode;
149
+ }) {
150
+ const {ref, ...restHtmlProps} = htmlProps ?? {};
135
151
  return (
136
152
  <div
153
+ ref={ref}
154
+ {...restHtmlProps}
137
155
  {...mergeProps(
138
156
  themeProps('table-scroll-wrapper'),
139
157
  stylex.props(
140
158
  scrollWrapperStyles.base,
141
159
  scrollWrapperStyles.containerBleed,
160
+ ...(pluginStyles ?? []),
142
161
  ),
143
162
  )}>
163
+ {beforeTable}
144
164
  {children}
165
+ {afterTable}
145
166
  </div>
146
167
  );
147
168
  }
@@ -27,6 +27,7 @@ export {useTablePagination, paginateData} from './plugins/pagination';
27
27
  export {useTableColumnSettings} from './plugins/columnSettings';
28
28
  export {useTableColumnSettingsState} from './plugins/columnSettings';
29
29
  export {useTableColumnResize} from './plugins/columnResize';
30
+ export {useTableStickyColumns} from './plugins/stickyColumns';
30
31
  export {
31
32
  useTableFiltering,
32
33
  useTableFilterState,
@@ -53,6 +54,7 @@ export type {
53
54
  HeaderCellRenderProps,
54
55
  BodyRowRenderProps,
55
56
  BodyCellRenderProps,
57
+ ScrollWrapperRenderProps,
56
58
  BaseTableProps,
57
59
  } from './types';
58
60
  export type {
@@ -93,6 +95,7 @@ export type {
93
95
  UseTableColumnSettingsStateReturn,
94
96
  } from './plugins/columnSettings';
95
97
  export type {UseTableColumnResizeConfig} from './plugins/columnResize';
98
+ export type {UseTableStickyColumnsConfig} from './plugins/stickyColumns';
96
99
  export type {
97
100
  UseTableFilteringConfig,
98
101
  TableFilterState,
@@ -0,0 +1,4 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ export {useTableStickyColumns} from './useTableStickyColumns';
4
+ export type {UseTableStickyColumnsConfig} from './useTableStickyColumns';