@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.
- package/CHANGELOG.md +66 -0
- package/README.md +119 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +4 -1
- package/dist/Banner/Banner.d.ts +7 -0
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +9 -2
- package/dist/Button/Button.d.ts +12 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +51 -13
- package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.js +5 -1
- package/dist/ContextMenu/ContextMenu.js +2 -2
- package/dist/DropdownMenu/DropdownMenu.js +2 -2
- package/dist/DropdownMenu/{renderXDSDropdownItems.d.ts → renderDropdownItems.d.ts} +3 -3
- package/dist/DropdownMenu/renderDropdownItems.d.ts.map +1 -0
- package/dist/DropdownMenu/{renderXDSDropdownItems.js → renderDropdownItems.js} +2 -2
- package/dist/EmptyState/EmptyState.d.ts.map +1 -1
- package/dist/EmptyState/EmptyState.js +7 -1
- package/dist/HoverCard/HoverCard.d.ts +2 -2
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +18 -6
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +6 -3
- package/dist/Kbd/Kbd.d.ts.map +1 -1
- package/dist/Kbd/Kbd.js +1 -1
- package/dist/Layer/useLayer.d.ts +13 -0
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +7 -2
- package/dist/Layout/Layout.d.ts +10 -1
- package/dist/Layout/Layout.d.ts.map +1 -1
- package/dist/Layout/Layout.js +5 -1
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +13 -3
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +13 -0
- package/dist/Outline/Outline.d.ts +3 -2
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +23 -4
- package/dist/Outline/useScrollSpy.d.ts +14 -1
- package/dist/Outline/useScrollSpy.d.ts.map +1 -1
- package/dist/Outline/useScrollSpy.js +161 -50
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +31 -27
- package/dist/Resizable/useResizable.d.ts.map +1 -1
- package/dist/Resizable/useResizable.js +1 -5
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +1 -1
- package/dist/Slider/Slider.js +2 -2
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +26 -8
- package/dist/Table/Table.d.ts.map +1 -1
- package/dist/Table/Table.js +30 -7
- package/dist/Table/index.d.ts +3 -1
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +1 -0
- package/dist/Table/plugins/stickyColumns/index.d.ts +3 -0
- package/dist/Table/plugins/stickyColumns/index.d.ts.map +1 -0
- package/dist/Table/plugins/stickyColumns/index.js +3 -0
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts +25 -0
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -0
- package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +376 -0
- package/dist/Table/types.d.ts +90 -5
- package/dist/Table/types.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.js +1 -1
- package/dist/Timestamp/Timestamp.d.ts +1 -1
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +28 -3
- package/dist/ToggleButton/ToggleButton.d.ts +10 -5
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +40 -17
- package/dist/astryx.css +20 -0
- package/dist/astryx.umd.js +147 -0
- package/dist/astryx.umd.js.map +7 -0
- package/dist/theme/Theme.js +1 -1
- package/dist/theme/defineTheme.d.ts +1 -1
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/syntax/defineSyntaxTheme.js +1 -1
- package/dist/theme/tokens.d.ts +1 -1
- package/dist/theme/tokens.js +4 -4
- package/dist/theme/useTheme.d.ts +2 -2
- package/dist/utils/dateParser.d.ts.map +1 -1
- package/dist/utils/dateParser.js +15 -2
- package/package.json +7 -3
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -0
- package/src/Banner/Banner.test.tsx +16 -7
- package/src/Banner/Banner.tsx +9 -2
- package/src/Button/Button.doc.mjs +6 -0
- package/src/Button/Button.test.tsx +105 -11
- package/src/Button/Button.tsx +73 -5
- package/src/Chat/ChatLayoutScrollButton.tsx +7 -1
- package/src/Collapsible/useCollapsible.doc.mjs +2 -2
- package/src/ContextMenu/ContextMenu.tsx +2 -2
- package/src/DateInput/DateInput.test.tsx +68 -20
- package/src/Divider/Divider.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenu.tsx +2 -2
- package/src/DropdownMenu/{renderXDSDropdownItems.tsx → renderDropdownItems.tsx} +2 -2
- package/src/EmptyState/EmptyState.test.tsx +4 -2
- package/src/EmptyState/EmptyState.tsx +6 -2
- package/src/FormLayout/FormLayout.doc.mjs +3 -3
- package/src/HoverCard/HoverCard.doc.mjs +3 -0
- package/src/HoverCard/HoverCard.test.tsx +178 -2
- package/src/HoverCard/HoverCard.tsx +20 -16
- package/src/HoverCard/useHoverCard.tsx +12 -10
- package/src/Icon/Icon.doc.mjs +12 -4
- package/src/Item/Item.doc.mjs +2 -2
- package/src/Kbd/Kbd.tsx +2 -9
- package/src/Layer/useLayer.doc.mjs +7 -2
- package/src/Layer/useLayer.tsx +19 -2
- package/src/Layout/Layout.doc.mjs +2 -1
- package/src/Layout/Layout.tsx +15 -1
- package/src/Layout/__tests__/childrenAsContent.test.tsx +59 -0
- package/src/Lightbox/Lightbox.doc.mjs +0 -2
- package/src/Link/Link.doc.mjs +3 -3
- package/src/Link/LinkProvider.doc.mjs +3 -3
- package/src/Markdown/Markdown.doc.mjs +6 -4
- package/src/Markdown/Markdown.test.tsx +17 -26
- package/src/Markdown/Markdown.tsx +16 -6
- package/src/MobileNav/MobileNav.doc.mjs +8 -8
- package/src/MobileNav/MobileNav.tsx +13 -0
- package/src/MobileNav/MobileNavReopen.test.tsx +118 -0
- package/src/Outline/Outline.doc.mjs +1 -1
- package/src/Outline/Outline.test.tsx +76 -38
- package/src/Outline/Outline.tsx +23 -4
- package/src/Outline/useScrollSpy.ts +196 -63
- package/src/Pagination/Pagination.test.tsx +137 -13
- package/src/Pagination/Pagination.tsx +33 -28
- package/src/Resizable/Resizable.doc.mjs +3 -3
- package/src/Resizable/useResizable.ts +1 -7
- package/src/Selector/Selector.doc.mjs +4 -0
- package/src/Selector/Selector.tsx +5 -6
- package/src/Skeleton/Skeleton.doc.mjs +11 -1
- package/src/Slider/Slider.doc.mjs +4 -0
- package/src/Slider/Slider.test.tsx +3 -16
- package/src/Slider/Slider.tsx +1 -1
- package/src/Table/BaseTable.tsx +50 -24
- package/src/Table/Table.doc.mjs +15 -3
- package/src/Table/Table.tsx +22 -1
- package/src/Table/index.ts +3 -0
- package/src/Table/plugins/stickyColumns/index.ts +4 -0
- package/src/Table/plugins/stickyColumns/useTableStickyColumns.test.tsx +163 -0
- package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +414 -0
- package/src/Table/types.ts +96 -4
- package/src/Table/useBaseTablePlugins.ts +1 -0
- package/src/Text/Heading.doc.mjs +13 -0
- package/src/Timestamp/Timestamp.test.tsx +44 -18
- package/src/Timestamp/Timestamp.tsx +30 -10
- package/src/ToggleButton/ToggleButton.doc.mjs +4 -4
- package/src/ToggleButton/ToggleButton.test.tsx +172 -9
- package/src/ToggleButton/ToggleButton.tsx +57 -31
- package/src/Toolbar/Toolbar.doc.mjs +1 -1
- package/src/hooks/useEntryAnimation.doc.mjs +3 -3
- package/src/hooks/useMediaQuery.doc.mjs +2 -2
- package/src/hooks/useStreamingText.doc.mjs +3 -3
- package/src/theme/Theme.doc.mjs +2 -2
- package/src/theme/Theme.tsx +1 -1
- package/src/theme/defineTheme.ts +1 -1
- package/src/theme/index.ts +1 -1
- package/src/theme/syntax/defineSyntaxTheme.ts +1 -1
- package/src/theme/tokens.ts +4 -4
- package/src/theme/useTheme.ts +2 -2
- package/src/utils/dateParser.test.ts +26 -0
- package/src/utils/dateParser.ts +16 -2
- 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 [
|
|
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 =
|
|
363
|
+
const hasPrevious = optimisticPage > 1;
|
|
360
364
|
const hasNext =
|
|
361
|
-
computedTotalPages != null
|
|
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
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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(
|
|
395
|
+
handlePageChange(optimisticPage - 1);
|
|
386
396
|
}
|
|
387
397
|
};
|
|
388
398
|
|
|
389
399
|
const handleNext = () => {
|
|
390
400
|
if (hasNext) {
|
|
391
|
-
handlePageChange(
|
|
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
|
-
|
|
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 = (
|
|
413
|
+
const rangeStart = (optimisticPage - 1) * pageSize + 1;
|
|
409
414
|
const rangeEnd =
|
|
410
415
|
totalItems != null
|
|
411
|
-
? Math.min(
|
|
412
|
-
:
|
|
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
|
-
|
|
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 ===
|
|
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 ${
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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
|
|
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: '
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
],
|
|
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!;
|
package/src/Slider/Slider.tsx
CHANGED
|
@@ -159,7 +159,7 @@ const styles = stylex.create({
|
|
|
159
159
|
},
|
|
160
160
|
track: {
|
|
161
161
|
position: 'absolute',
|
|
162
|
-
backgroundColor: colorVars['--color-
|
|
162
|
+
backgroundColor: colorVars['--color-track'],
|
|
163
163
|
borderRadius: radiusVars['--radius-full'],
|
|
164
164
|
},
|
|
165
165
|
trackHorizontal: {
|
package/src/Table/BaseTable.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|