@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.
- package/CHANGELOG.md +66 -0
- package/README.md +68 -0
- 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.map +1 -1
- package/dist/Button/Button.js +2 -0
- 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/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/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/ToggleButton/ToggleButton.d.ts +10 -3
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +64 -18
- package/dist/astryx.css +11 -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.test.tsx +26 -11
- package/src/Button/Button.tsx +2 -0
- 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 +4 -4
- package/src/Item/Item.doc.mjs +2 -2
- 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/Table/BaseTable.tsx +50 -24
- package/src/Table/Table.doc.mjs +3 -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/ToggleButton/ToggleButton.doc.mjs +2 -2
- package/src/ToggleButton/ToggleButton.test.tsx +148 -6
- package/src/ToggleButton/ToggleButton.tsx +83 -20
- 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
package/src/Table/types.ts
CHANGED
|
@@ -261,6 +261,20 @@ export interface HeaderCellRenderProps {
|
|
|
261
261
|
overlay?: ReactNode;
|
|
262
262
|
/** Content rendered below the header label row (e.g. inline filter controls). */
|
|
263
263
|
below?: ReactNode;
|
|
264
|
+
/**
|
|
265
|
+
* Index of this column within the final, ordered list of rendered columns
|
|
266
|
+
* (after column injection/reordering by other plugins). Populated by
|
|
267
|
+
* BaseTable. Optional for backward compatibility with hand-constructed
|
|
268
|
+
* renders in tests.
|
|
269
|
+
*/
|
|
270
|
+
columnIndex?: number;
|
|
271
|
+
/**
|
|
272
|
+
* The full, final ordered list of columns being rendered (after column
|
|
273
|
+
* injection/reordering by other plugins). Populated by BaseTable so plugins
|
|
274
|
+
* can reason about column position — e.g. cumulative sticky offsets. Optional
|
|
275
|
+
* for backward compatibility.
|
|
276
|
+
*/
|
|
277
|
+
columns?: ReadonlyArray<TableColumn<Record<string, unknown>>>;
|
|
264
278
|
}
|
|
265
279
|
|
|
266
280
|
/** Props passed through the plugin pipeline for each body `<tr>` */
|
|
@@ -276,6 +290,49 @@ export interface BodyRowRenderProps {
|
|
|
276
290
|
export interface BodyCellRenderProps {
|
|
277
291
|
htmlProps: TdHTMLAttributes<HTMLTableCellElement>;
|
|
278
292
|
styles: StyleXStyles[];
|
|
293
|
+
/**
|
|
294
|
+
* Index of this cell's column within the final ordered column list.
|
|
295
|
+
* Mirrors the `columnIndex` passed to `transformHeaderCell`. Populated by
|
|
296
|
+
* BaseTable. Optional for backward compatibility with hand-constructed
|
|
297
|
+
* renders in tests.
|
|
298
|
+
*/
|
|
299
|
+
columnIndex?: number;
|
|
300
|
+
/**
|
|
301
|
+
* The full, final ordered list of columns being rendered. Populated by
|
|
302
|
+
* BaseTable so plugins can reason about column position — e.g. cumulative
|
|
303
|
+
* sticky offsets. Optional for backward compatibility.
|
|
304
|
+
*/
|
|
305
|
+
columns?: ReadonlyArray<TableColumn<Record<string, unknown>>>;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Props passed through the plugin pipeline for the scroll-wrapper region — the
|
|
310
|
+
* `<div>` wrapping the `<table>` element (the horizontal scroll container, see
|
|
311
|
+
* the `scrollWrapper` prop on `BaseTableProps`). Lets plugins attach a `ref` to
|
|
312
|
+
* the scrollable element (e.g. for scroll-aware sticky-column shadows or
|
|
313
|
+
* virtualization) and inject chrome before/after the table.
|
|
314
|
+
*
|
|
315
|
+
* Named after `scrollWrapper` (not "layout") to avoid ambiguity: it transforms
|
|
316
|
+
* the wrapper element, not the internal header/body/footer layout of `<table>`.
|
|
317
|
+
*
|
|
318
|
+
* Runs after `transformTable`/cell transforms but inside `transformTableContext`,
|
|
319
|
+
* so plugin chrome added here stays within any context providers but wraps the
|
|
320
|
+
* scroll area.
|
|
321
|
+
*/
|
|
322
|
+
export interface ScrollWrapperRenderProps {
|
|
323
|
+
/**
|
|
324
|
+
* HTML attributes applied to the scroll container `<div>`, including an
|
|
325
|
+
* optional `ref`. Plugins compose refs by reading the existing `ref` and
|
|
326
|
+
* merging their own (see `useTableStickyColumns`).
|
|
327
|
+
*/
|
|
328
|
+
htmlProps: HTMLAttributes<HTMLDivElement> & {
|
|
329
|
+
ref?: Ref<HTMLDivElement>;
|
|
330
|
+
};
|
|
331
|
+
styles: StyleXStyles[];
|
|
332
|
+
/** Content rendered before the `<table>`, inside the scroll container. */
|
|
333
|
+
beforeTable?: ReactNode;
|
|
334
|
+
/** Content rendered after the `<table>`, inside the scroll container. */
|
|
335
|
+
afterTable?: ReactNode;
|
|
279
336
|
}
|
|
280
337
|
|
|
281
338
|
// =============================================================================
|
|
@@ -294,7 +351,8 @@ export interface BodyCellRenderProps {
|
|
|
294
351
|
* 4. `transformHeaderCell` — transform each `<th>` props
|
|
295
352
|
* 5. `transformBodyRow` — transform each body `<tr>` props
|
|
296
353
|
* 6. `transformBodyCell` — transform each body `<td>` props
|
|
297
|
-
* 7. `
|
|
354
|
+
* 7. `transformScrollWrapper` — transform the scroll-container wrapper around the table
|
|
355
|
+
* 8. `transformTableContext` — wrap the table output in context providers
|
|
298
356
|
*/
|
|
299
357
|
export interface TablePlugin<
|
|
300
358
|
T extends Record<string, unknown> = Record<string, unknown>,
|
|
@@ -309,10 +367,17 @@ export interface TablePlugin<
|
|
|
309
367
|
transformTable?: (props: TableRenderProps) => TableRenderProps;
|
|
310
368
|
/** Transform the header `<tr>` props */
|
|
311
369
|
transformHeaderRow?: (props: HeaderRowRenderProps) => HeaderRowRenderProps;
|
|
312
|
-
/**
|
|
370
|
+
/**
|
|
371
|
+
* Transform each `<th>` props.
|
|
372
|
+
*
|
|
373
|
+
* `columnIndex` and the full `columns` list are provided for plugins that
|
|
374
|
+
* need to reason about column position (e.g. cumulative sticky offsets).
|
|
375
|
+
*/
|
|
313
376
|
transformHeaderCell?: (
|
|
314
377
|
props: HeaderCellRenderProps,
|
|
315
378
|
column: TableColumn<T>,
|
|
379
|
+
columnIndex: number,
|
|
380
|
+
columns: ReadonlyArray<TableColumn<T>>,
|
|
316
381
|
) => HeaderCellRenderProps;
|
|
317
382
|
/** Transform each body `<tr>` props */
|
|
318
383
|
transformBodyRow?: (
|
|
@@ -320,12 +385,28 @@ export interface TablePlugin<
|
|
|
320
385
|
item: T,
|
|
321
386
|
index: number,
|
|
322
387
|
) => BodyRowRenderProps;
|
|
323
|
-
/**
|
|
388
|
+
/**
|
|
389
|
+
* Transform each body `<td>` props.
|
|
390
|
+
*
|
|
391
|
+
* `columnIndex` and the full `columns` list are provided for plugins that
|
|
392
|
+
* need to reason about column position (e.g. cumulative sticky offsets).
|
|
393
|
+
*/
|
|
324
394
|
transformBodyCell?: (
|
|
325
395
|
props: BodyCellRenderProps,
|
|
326
396
|
column: TableColumn<T>,
|
|
327
397
|
item: T,
|
|
398
|
+
columnIndex: number,
|
|
399
|
+
columns: ReadonlyArray<TableColumn<T>>,
|
|
328
400
|
) => BodyCellRenderProps;
|
|
401
|
+
/**
|
|
402
|
+
* Transform the scroll-wrapper region — the `<div>` wrapping the `<table>`
|
|
403
|
+
* (see the `scrollWrapper` prop). Use to attach a `ref` to the scrollable
|
|
404
|
+
* element (scroll-aware shadows, virtualization) or to inject chrome
|
|
405
|
+
* before/after the table.
|
|
406
|
+
*/
|
|
407
|
+
transformScrollWrapper?: (
|
|
408
|
+
props: ScrollWrapperRenderProps,
|
|
409
|
+
) => ScrollWrapperRenderProps;
|
|
329
410
|
/** Wrap the table output in context providers */
|
|
330
411
|
transformTableContext?: (children: ReactNode) => ReactNode;
|
|
331
412
|
}
|
|
@@ -390,8 +471,19 @@ export interface BaseTableProps<
|
|
|
390
471
|
* plugin `transformTableContext` layer. Used by `Table` to add a
|
|
391
472
|
* horizontal scroll container so plugin chrome (pagination, toolbars)
|
|
392
473
|
* stays outside the scrollable area.
|
|
474
|
+
*
|
|
475
|
+
* Receives `htmlProps` (including an optional `ref`) and `styles` produced
|
|
476
|
+
* by the plugin `transformScrollWrapper` pipeline, plus `beforeTable`/`afterTable`
|
|
477
|
+
* chrome. The wrapper must spread `htmlProps` (and apply `styles`) onto its
|
|
478
|
+
* scroll-container element so plugins can attach refs / scroll listeners.
|
|
393
479
|
*/
|
|
394
|
-
scrollWrapper?: ComponentType<{
|
|
480
|
+
scrollWrapper?: ComponentType<{
|
|
481
|
+
children: ReactNode;
|
|
482
|
+
htmlProps?: HTMLAttributes<HTMLDivElement> & {ref?: Ref<HTMLDivElement>};
|
|
483
|
+
styles?: StyleXStyles[];
|
|
484
|
+
beforeTable?: ReactNode;
|
|
485
|
+
afterTable?: ReactNode;
|
|
486
|
+
}>;
|
|
395
487
|
/**
|
|
396
488
|
* How default-rendered body cell text behaves when it exceeds column width.
|
|
397
489
|
*
|
|
@@ -39,8 +39,8 @@ export const docs = {
|
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
name: 'pressedChangeAction',
|
|
42
|
-
type: '(isPressed: boolean) => Promise<void>',
|
|
43
|
-
description: '
|
|
42
|
+
type: '(isPressed: boolean) => void | Promise<void>',
|
|
43
|
+
description: 'Action handler for API- or navigation-backed toggles, run in a transition. Shows an optimistic pressed state immediately and a (debounced) spinner while pending; interruptible by re-clicks.',
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
name: 'size',
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {describe, it, expect, vi} from 'vitest';
|
|
12
|
-
import {render, screen} from '@testing-library/react';
|
|
12
|
+
import {render, screen, act, fireEvent, waitFor} from '@testing-library/react';
|
|
13
13
|
import userEvent from '@testing-library/user-event';
|
|
14
14
|
import {useState} from 'react';
|
|
15
15
|
import {ToggleButton} from './ToggleButton';
|
|
@@ -71,11 +71,7 @@ describe('ToggleButton', () => {
|
|
|
71
71
|
|
|
72
72
|
it('sets aria-pressed=true when pressed', () => {
|
|
73
73
|
render(
|
|
74
|
-
<ToggleButton
|
|
75
|
-
label="Bold"
|
|
76
|
-
isPressed={true}
|
|
77
|
-
onPressedChange={() => {}}
|
|
78
|
-
/>,
|
|
74
|
+
<ToggleButton label="Bold" isPressed={true} onPressedChange={() => {}} />,
|
|
79
75
|
);
|
|
80
76
|
expect(screen.getByRole('button')).toHaveAttribute('aria-pressed', 'true');
|
|
81
77
|
});
|
|
@@ -196,6 +192,152 @@ describe('ToggleButton', () => {
|
|
|
196
192
|
);
|
|
197
193
|
expect(screen.getByTestId('bold-toggle')).toBeInTheDocument();
|
|
198
194
|
});
|
|
195
|
+
|
|
196
|
+
it('shows the optimistic pressed state immediately, before any spinner', async () => {
|
|
197
|
+
const user = userEvent.setup();
|
|
198
|
+
let resolveAction: (() => void) | undefined;
|
|
199
|
+
const pressedChangeAction = vi.fn(
|
|
200
|
+
async () =>
|
|
201
|
+
new Promise<void>(resolve => {
|
|
202
|
+
resolveAction = resolve;
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
render(
|
|
207
|
+
<ToggleButton
|
|
208
|
+
label="Favorite"
|
|
209
|
+
isPressed={false}
|
|
210
|
+
onPressedChange={() => {}}
|
|
211
|
+
pressedChangeAction={pressedChangeAction}
|
|
212
|
+
/>,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const button = screen.getByRole('button', {name: 'Favorite'});
|
|
216
|
+
expect(button).toHaveAttribute('aria-pressed', 'false');
|
|
217
|
+
|
|
218
|
+
await user.click(button);
|
|
219
|
+
|
|
220
|
+
// The optimistic state flips immediately. The spinner is debounced, so the
|
|
221
|
+
// button is not disabled or aria-busy yet — it stays interruptible.
|
|
222
|
+
expect(pressedChangeAction).toHaveBeenCalledWith(true);
|
|
223
|
+
expect(button).toHaveAttribute('aria-pressed', 'true');
|
|
224
|
+
expect(button).not.toBeDisabled();
|
|
225
|
+
expect(button).not.toHaveAttribute('aria-busy', 'true');
|
|
226
|
+
|
|
227
|
+
// Settle the action so the pending transition doesn't leak into later tests.
|
|
228
|
+
await act(async () => {
|
|
229
|
+
resolveAction?.();
|
|
230
|
+
await Promise.resolve();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('shows a loading spinner once the action stays pending past the delay', async () => {
|
|
235
|
+
const user = userEvent.setup();
|
|
236
|
+
let resolveAction: (() => void) | undefined;
|
|
237
|
+
const pressedChangeAction = vi.fn(
|
|
238
|
+
async () =>
|
|
239
|
+
new Promise<void>(resolve => {
|
|
240
|
+
resolveAction = resolve;
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
render(
|
|
245
|
+
<ToggleButton
|
|
246
|
+
label="Favorite"
|
|
247
|
+
isPressed={false}
|
|
248
|
+
onPressedChange={() => {}}
|
|
249
|
+
pressedChangeAction={pressedChangeAction}
|
|
250
|
+
/>,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const button = screen.getByRole('button', {name: 'Favorite'});
|
|
254
|
+
await user.click(button);
|
|
255
|
+
|
|
256
|
+
// The optimistic state shows immediately; the spinner is debounced and
|
|
257
|
+
// appears only after the action stays pending past the delay window.
|
|
258
|
+
expect(button).toHaveAttribute('aria-pressed', 'true');
|
|
259
|
+
await waitFor(() => expect(button).toBeDisabled());
|
|
260
|
+
expect(button).toHaveAttribute('aria-busy', 'true');
|
|
261
|
+
|
|
262
|
+
await act(async () => {
|
|
263
|
+
resolveAction?.();
|
|
264
|
+
await Promise.resolve();
|
|
265
|
+
});
|
|
266
|
+
expect(button).not.toBeDisabled();
|
|
267
|
+
expect(button).not.toHaveAttribute('aria-busy', 'true');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('interrupts an in-flight action on re-click (true -> false -> true)', async () => {
|
|
271
|
+
// Each click interrupts the previous transition. The actions are resolved
|
|
272
|
+
// at the end so the pending transition doesn't leak into later tests.
|
|
273
|
+
const resolvers: (() => void)[] = [];
|
|
274
|
+
const pressedChangeAction = vi.fn(
|
|
275
|
+
async () =>
|
|
276
|
+
new Promise<void>(resolve => {
|
|
277
|
+
resolvers.push(resolve);
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
render(
|
|
282
|
+
<ToggleButton
|
|
283
|
+
label="Favorite"
|
|
284
|
+
isPressed={false}
|
|
285
|
+
onPressedChange={() => {}}
|
|
286
|
+
pressedChangeAction={pressedChangeAction}
|
|
287
|
+
/>,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const button = screen.getByRole('button', {name: 'Favorite'});
|
|
291
|
+
|
|
292
|
+
// Each click derives the next state from the optimistic (in-progress)
|
|
293
|
+
// value, so rapid clicks toggle rather than being dropped. fireEvent keeps
|
|
294
|
+
// the clicks within the spinner debounce window so the button stays
|
|
295
|
+
// interruptible.
|
|
296
|
+
await act(async () => {
|
|
297
|
+
fireEvent.click(button);
|
|
298
|
+
});
|
|
299
|
+
expect(button).toHaveAttribute('aria-pressed', 'true');
|
|
300
|
+
await act(async () => {
|
|
301
|
+
fireEvent.click(button);
|
|
302
|
+
});
|
|
303
|
+
expect(button).toHaveAttribute('aria-pressed', 'false');
|
|
304
|
+
await act(async () => {
|
|
305
|
+
fireEvent.click(button);
|
|
306
|
+
});
|
|
307
|
+
expect(button).toHaveAttribute('aria-pressed', 'true');
|
|
308
|
+
|
|
309
|
+
expect(pressedChangeAction).toHaveBeenCalledTimes(3);
|
|
310
|
+
expect(pressedChangeAction).toHaveBeenNthCalledWith(1, true);
|
|
311
|
+
expect(pressedChangeAction).toHaveBeenNthCalledWith(2, false);
|
|
312
|
+
expect(pressedChangeAction).toHaveBeenNthCalledWith(3, true);
|
|
313
|
+
|
|
314
|
+
await act(async () => {
|
|
315
|
+
resolvers.forEach(resolve => resolve());
|
|
316
|
+
await Promise.resolve();
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('supports a synchronous pressedChangeAction', async () => {
|
|
321
|
+
const user = userEvent.setup();
|
|
322
|
+
// A sync handler (e.g. a router navigation) with no returned promise.
|
|
323
|
+
const pressedChangeAction = vi.fn((_next: boolean) => {});
|
|
324
|
+
const onPressedChange = vi.fn();
|
|
325
|
+
|
|
326
|
+
render(
|
|
327
|
+
<ToggleButton
|
|
328
|
+
label="Favorite"
|
|
329
|
+
isPressed={false}
|
|
330
|
+
onPressedChange={onPressedChange}
|
|
331
|
+
pressedChangeAction={pressedChangeAction}
|
|
332
|
+
/>,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const button = screen.getByRole('button', {name: 'Favorite'});
|
|
336
|
+
await user.click(button);
|
|
337
|
+
|
|
338
|
+
expect(onPressedChange).toHaveBeenCalledWith(true);
|
|
339
|
+
expect(pressedChangeAction).toHaveBeenCalledWith(true);
|
|
340
|
+
});
|
|
199
341
|
});
|
|
200
342
|
|
|
201
343
|
// =============================================================================
|
|
@@ -20,7 +20,14 @@
|
|
|
20
20
|
* - /packages/cli/templates/blocks/components/ToggleButton/ (showcase blocks)
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import React, {
|
|
23
|
+
import React, {
|
|
24
|
+
useCallback,
|
|
25
|
+
useEffect,
|
|
26
|
+
useOptimistic,
|
|
27
|
+
useState,
|
|
28
|
+
useTransition,
|
|
29
|
+
type ReactNode,
|
|
30
|
+
} from 'react';
|
|
24
31
|
import * as stylex from '@stylexjs/stylex';
|
|
25
32
|
import {colorVars, fontWeightVars} from '../theme/tokens.stylex';
|
|
26
33
|
|
|
@@ -29,6 +36,37 @@ import {useToggleButtonGroup} from './ToggleButtonGroup';
|
|
|
29
36
|
import type {BaseProps} from '../BaseProps';
|
|
30
37
|
import {themeProps} from '../utils/themeProps';
|
|
31
38
|
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Constants & helpers
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The spinner only appears once the action has been pending for this long.
|
|
45
|
+
* A fast action shows the optimistic pressed state immediately with no spinner
|
|
46
|
+
* flash, and rapid re-clicks can interrupt the in-flight action before the
|
|
47
|
+
* button locks behind the spinner.
|
|
48
|
+
*/
|
|
49
|
+
const PENDING_SPINNER_DELAY_MS = 150;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns `true` only once `active` has stayed `true` for `delayMs`.
|
|
53
|
+
* Used to debounce the loading spinner so the optimistic state shows first.
|
|
54
|
+
*/
|
|
55
|
+
function useDelayed(active: boolean, delayMs: number): boolean {
|
|
56
|
+
const [delayed, setDelayed] = useState(false);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!active) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const timer = setTimeout(() => setDelayed(true), delayMs);
|
|
62
|
+
return () => {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
setDelayed(false);
|
|
65
|
+
};
|
|
66
|
+
}, [active, delayMs]);
|
|
67
|
+
return active && delayed;
|
|
68
|
+
}
|
|
69
|
+
|
|
32
70
|
// =============================================================================
|
|
33
71
|
// Styles
|
|
34
72
|
// =============================================================================
|
|
@@ -91,8 +129,15 @@ export interface ToggleButtonProps extends BaseProps<HTMLButtonElement> {
|
|
|
91
129
|
onPressedChange?: (isPressed: boolean) => void;
|
|
92
130
|
|
|
93
131
|
/**
|
|
94
|
-
*
|
|
95
|
-
* The button shows a loading spinner while the
|
|
132
|
+
* Action handler for API- or navigation-backed toggles, run inside a
|
|
133
|
+
* transition. The button shows a loading spinner while the action is
|
|
134
|
+
* pending — whether it returns a promise or synchronously triggers a
|
|
135
|
+
* suspending update (e.g. a router navigation that suspends on data).
|
|
136
|
+
*
|
|
137
|
+
* Because it runs in a transition, the toggle is *interruptible*: clicking
|
|
138
|
+
* again while an action is pending starts a new transition with the next
|
|
139
|
+
* optimistic state, so the action reflects the latest intent rather than
|
|
140
|
+
* being dropped.
|
|
96
141
|
*
|
|
97
142
|
* @example
|
|
98
143
|
* ```
|
|
@@ -106,7 +151,7 @@ export interface ToggleButtonProps extends BaseProps<HTMLButtonElement> {
|
|
|
106
151
|
* />
|
|
107
152
|
* ```
|
|
108
153
|
*/
|
|
109
|
-
pressedChangeAction?: (isPressed: boolean) => Promise<void>;
|
|
154
|
+
pressedChangeAction?: (isPressed: boolean) => void | Promise<void>;
|
|
110
155
|
|
|
111
156
|
/**
|
|
112
157
|
* The size of the toggle button.
|
|
@@ -218,46 +263,64 @@ export function ToggleButton({
|
|
|
218
263
|
style,
|
|
219
264
|
...props
|
|
220
265
|
}: ToggleButtonProps): ReactNode {
|
|
221
|
-
// Read group context if inside a group
|
|
222
266
|
const group = useToggleButtonGroup();
|
|
223
267
|
|
|
224
|
-
|
|
225
|
-
const isPressed =
|
|
268
|
+
const committedPressed =
|
|
226
269
|
group && value != null
|
|
227
270
|
? group.selectedValues.has(value)
|
|
228
271
|
: (isPressedProp ?? false);
|
|
229
272
|
const size = sizeProp ?? group?.size ?? 'md';
|
|
230
273
|
const isDisabled = group?.isDisabled ?? isDisabledProp;
|
|
231
274
|
|
|
275
|
+
// Track the pressed state optimistically. While an action is pending, the
|
|
276
|
+
// button reflects the intended (optimistic) state immediately, and a click
|
|
277
|
+
// mid-flight derives its next state from this value — so rapid toggles read
|
|
278
|
+
// true -> false -> true rather than stalling on the last committed value.
|
|
279
|
+
const [optimisticPressed, setOptimisticPressed] =
|
|
280
|
+
useOptimistic(committedPressed);
|
|
281
|
+
const isPressed = optimisticPressed;
|
|
282
|
+
|
|
232
283
|
const resolvedIcon = isPressed && pressedIcon ? pressedIcon : icon;
|
|
233
284
|
|
|
285
|
+
// Run the toggle inside a transition. The action is interruptible: clicking
|
|
286
|
+
// again while it is pending starts a fresh transition with the next
|
|
287
|
+
// optimistic state instead of being dropped, so there is no re-entry guard.
|
|
288
|
+
// Both onPressedChange and pressedChangeAction run inside the transition,
|
|
289
|
+
// which means a synchronous-but-suspending handler (e.g. a router navigation
|
|
290
|
+
// that suspends on data) also drives the pending state — not just promises.
|
|
291
|
+
const [isPending, startTransition] = useTransition();
|
|
292
|
+
// Debounce the spinner so a fast action shows the optimistic state without a
|
|
293
|
+
// spinner flash, and rapid re-clicks can interrupt before the button locks.
|
|
294
|
+
const showSpinner = useDelayed(isPending, PENDING_SPINNER_DELAY_MS);
|
|
295
|
+
const isLoadingState = isLoading || showSpinner;
|
|
296
|
+
|
|
234
297
|
const handleClick = useCallback(() => {
|
|
235
|
-
if (isDisabled
|
|
298
|
+
if (isDisabled) {
|
|
236
299
|
return;
|
|
237
300
|
}
|
|
238
301
|
|
|
239
302
|
if (group && value != null) {
|
|
240
|
-
//
|
|
303
|
+
// Group mode delegates selection to the group; no async-action path.
|
|
241
304
|
group.toggle(value);
|
|
242
|
-
|
|
243
|
-
// Standalone toggle
|
|
244
|
-
const newState = !isPressed;
|
|
245
|
-
onPressedChangeProp(newState);
|
|
246
|
-
if (pressedChangeAction) {
|
|
247
|
-
void pressedChangeAction(newState);
|
|
248
|
-
}
|
|
305
|
+
return;
|
|
249
306
|
}
|
|
307
|
+
|
|
308
|
+
const newState = !optimisticPressed;
|
|
309
|
+
startTransition(async () => {
|
|
310
|
+
setOptimisticPressed(newState);
|
|
311
|
+
onPressedChangeProp?.(newState);
|
|
312
|
+
await pressedChangeAction?.(newState);
|
|
313
|
+
});
|
|
250
314
|
}, [
|
|
251
315
|
isDisabled,
|
|
252
|
-
isLoading,
|
|
253
316
|
group,
|
|
254
317
|
value,
|
|
318
|
+
optimisticPressed,
|
|
255
319
|
onPressedChangeProp,
|
|
256
320
|
pressedChangeAction,
|
|
257
|
-
|
|
321
|
+
setOptimisticPressed,
|
|
258
322
|
]);
|
|
259
323
|
|
|
260
|
-
// Label with font weight shift and width reservation
|
|
261
324
|
// isIconOnly prop is the source of truth for icon-only rendering.
|
|
262
325
|
const labelContent =
|
|
263
326
|
children != null ? (
|
|
@@ -289,7 +352,7 @@ export function ToggleButton({
|
|
|
289
352
|
variant="ghost"
|
|
290
353
|
size={size}
|
|
291
354
|
isDisabled={isDisabled}
|
|
292
|
-
isLoading={
|
|
355
|
+
isLoading={isLoadingState}
|
|
293
356
|
isIconOnly={isIconOnly}
|
|
294
357
|
aria-pressed={isPressed}
|
|
295
358
|
icon={resolvedIcon}
|
|
@@ -23,7 +23,7 @@ export const docs = {
|
|
|
23
23
|
],
|
|
24
24
|
usage: {
|
|
25
25
|
description:
|
|
26
|
-
'Returns a StyleX style for animating an element on mount. Only animates when the element is dynamically inserted after the initial page paint; elements rendered on page load are not animated. Uses
|
|
26
|
+
'Returns a StyleX style for animating an element on mount. Only animates when the element is dynamically inserted after the initial page paint; elements rendered on page load are not animated. Uses Astryx motion tokens (duration, easing) for consistent animation timing. Requires "use client"; does not support SSR.',
|
|
27
27
|
bestPractices: [
|
|
28
28
|
{ guidance: true, description: 'Use for conditionally rendered elements like validation messages, toasts, or expanding sections.' },
|
|
29
29
|
{ guidance: true, description: 'Spread the returned style into stylex.props() alongside other styles.' },
|
|
@@ -39,7 +39,7 @@ export const docs = {
|
|
|
39
39
|
/** @type {import('../docs-types').HookTranslationDoc} */
|
|
40
40
|
export const docsDense = {
|
|
41
41
|
description:
|
|
42
|
-
'Returns StyleX style for animating element on mount. Only animates when element dynamically inserted after initial page paint; elements rendered on page load not animated. Uses
|
|
42
|
+
'Returns StyleX style for animating element on mount. Only animates when element dynamically inserted after initial page paint; elements rendered on page load not animated. Uses Astryx motion tokens (duration, easing) for consistent timing. Requires "use client"; does not support SSR.',
|
|
43
43
|
paramDescriptions: {
|
|
44
44
|
preset: 'animation preset applied on mount.',
|
|
45
45
|
},
|
|
@@ -48,7 +48,7 @@ export const docsDense = {
|
|
|
48
48
|
},
|
|
49
49
|
usage: {
|
|
50
50
|
description:
|
|
51
|
-
'Returns StyleX style for animating element on mount. Only animates when element dynamically inserted after initial page paint; elements rendered on page load not animated. Uses
|
|
51
|
+
'Returns StyleX style for animating element on mount. Only animates when element dynamically inserted after initial page paint; elements rendered on page load not animated. Uses Astryx motion tokens (duration, easing) for consistent timing. Requires "use client"; does not support SSR.',
|
|
52
52
|
bestPractices: [
|
|
53
53
|
{ guidance: true, description: 'Use for conditionally rendered elements like validation messages, toasts, expanding sections.' },
|
|
54
54
|
{ guidance: true, description: 'Spread returned style into stylex.props() alongside other styles.' },
|
|
@@ -24,7 +24,7 @@ export const docs = {
|
|
|
24
24
|
description: 'SSR-safe media query hook that subscribes to window.matchMedia changes. Returns whether the given media query matches. Always returns false on first render for SSR compatibility.',
|
|
25
25
|
bestPractices: [
|
|
26
26
|
{ guidance: true, description: 'Use for responsive layout switching based on viewport width, color scheme, or motion preferences.' },
|
|
27
|
-
{ guidance: true, description: 'Prefer
|
|
27
|
+
{ guidance: true, description: 'Prefer Astryx responsive tokens and component props over manual breakpoint logic when possible.' },
|
|
28
28
|
{ guidance: false, description: 'Use for server-rendered content that must match on first paint; the hook always returns false initially.' },
|
|
29
29
|
],
|
|
30
30
|
},
|
|
@@ -47,7 +47,7 @@ export const docsDense = {
|
|
|
47
47
|
description: 'SSR-safe media query hook subscribing to window.matchMedia changes. Returns whether given media query matches. Always returns false on first render for SSR compatibility.',
|
|
48
48
|
bestPractices: [
|
|
49
49
|
{ guidance: true, description: 'Use for responsive layout switching based on viewport width, color scheme, or motion preferences.' },
|
|
50
|
-
{ guidance: true, description: 'Prefer
|
|
50
|
+
{ guidance: true, description: 'Prefer Astryx responsive tokens + component props over manual breakpoint logic when possible.' },
|
|
51
51
|
{ guidance: false, description: 'Use for server-rendered content that must match on first paint; hook always returns false initially.' },
|
|
52
52
|
],
|
|
53
53
|
},
|
|
@@ -41,7 +41,7 @@ export const docs = {
|
|
|
41
41
|
],
|
|
42
42
|
usage: {
|
|
43
43
|
description:
|
|
44
|
-
'Smooths bursty streamed text into a steady character-by-character reveal using requestAnimationFrame. Decouples arrival rate from display rate. Advances on word and syntax boundaries to avoid slicing mid-markdown or mid-word, preventing visual glitches with markdown renderers. Animation timing derives from
|
|
44
|
+
'Smooths bursty streamed text into a steady character-by-character reveal using requestAnimationFrame. Decouples arrival rate from display rate. Advances on word and syntax boundaries to avoid slicing mid-markdown or mid-word, preventing visual glitches with markdown renderers. Animation timing derives from Astryx motion tokens via useTheme when available, with sensible fallbacks outside a theme provider. Snaps to full text when isStreaming becomes false.',
|
|
45
45
|
bestPractices: [
|
|
46
46
|
{ guidance: true, description: 'Pass the accumulated text (not individual chunks) as targetText; the hook handles incremental reveal internally.' },
|
|
47
47
|
{ guidance: true, description: 'Set isStreaming to false when the stream completes to snap to the final text.' },
|
|
@@ -58,7 +58,7 @@ export const docs = {
|
|
|
58
58
|
/** @type {import('../docs-types').HookTranslationDoc} */
|
|
59
59
|
export const docsDense = {
|
|
60
60
|
description:
|
|
61
|
-
'Smooths bursty streamed text into steady character-by-character reveal using requestAnimationFrame. Decouples arrival rate from display rate. Advances on word + syntax boundaries to avoid slicing mid-markdown / mid-word, preventing visual glitches w/ markdown renderers. Animation timing derives from
|
|
61
|
+
'Smooths bursty streamed text into steady character-by-character reveal using requestAnimationFrame. Decouples arrival rate from display rate. Advances on word + syntax boundaries to avoid slicing mid-markdown / mid-word, preventing visual glitches w/ markdown renderers. Animation timing derives from Astryx motion tokens via useTheme when available, w/ sensible fallbacks outside theme provider. Snaps to full text when isStreaming becomes false.',
|
|
62
62
|
paramDescriptions: {
|
|
63
63
|
targetText: 'full target text to reveal. As new chunks arrive, update this value w/ accumulated text.',
|
|
64
64
|
isStreaming: 'whether text currently being streamed. When false, hook returns full targetText immediately.',
|
|
@@ -70,7 +70,7 @@ export const docsDense = {
|
|
|
70
70
|
},
|
|
71
71
|
usage: {
|
|
72
72
|
description:
|
|
73
|
-
'Smooths bursty streamed text into steady character-by-character reveal using requestAnimationFrame. Decouples arrival rate from display rate. Advances on word + syntax boundaries to avoid slicing mid-markdown / mid-word, preventing visual glitches w/ markdown renderers. Animation timing derives from
|
|
73
|
+
'Smooths bursty streamed text into steady character-by-character reveal using requestAnimationFrame. Decouples arrival rate from display rate. Advances on word + syntax boundaries to avoid slicing mid-markdown / mid-word, preventing visual glitches w/ markdown renderers. Animation timing derives from Astryx motion tokens via useTheme when available, w/ sensible fallbacks outside theme provider. Snaps to full text when isStreaming becomes false.',
|
|
74
74
|
bestPractices: [
|
|
75
75
|
{ guidance: true, description: 'Pass accumulated text (not individual chunks) as targetText; hook handles incremental reveal internally.' },
|
|
76
76
|
{ guidance: true, description: 'Set isStreaming to false when stream completes to snap to final text.' },
|
package/src/theme/Theme.doc.mjs
CHANGED
|
@@ -39,7 +39,7 @@ export const docs = {
|
|
|
39
39
|
},
|
|
40
40
|
usage: {
|
|
41
41
|
description:
|
|
42
|
-
'Wraps a subtree with a specific
|
|
42
|
+
'Wraps a subtree with a specific Astryx theme. For static production themes, use `npx astryx theme build` and import the generated CSS plus built theme object for first-paint and SSR performance. Use runtime `defineTheme()` when themes are dynamic or for prototyping.\n\n`defineTheme` accepts a `tokens` object whose keys are CSS custom property names (always prefixed with `--`). Common token names include `--color-accent`, `--color-background-surface`, `--color-background-body`, `--color-text-primary`, `--color-text-secondary`, `--radius-container`, `--spacing-1` through `--spacing-6`. Values can be a string (same for light/dark) or a `[light, dark]` tuple.\n\nExample:\n```ts\nimport {defineTheme} from \'@astryxdesign/core/theme\';\nconst myTheme = defineTheme({\n name: \'ocean\',\n tokens: {\n \'--color-accent\': [\'#0077B6\', \'#48CAE4\'],\n \'--color-background-surface\': [\'#F0F8FF\', \'#0A1628\'],\n \'--color-text-primary\': [\'#0A1317\', \'#FFFFFF\'],\n \'--radius-container\': \'16px\',\n },\n});\n```',
|
|
43
43
|
bestPractices: [
|
|
44
44
|
{
|
|
45
45
|
guidance: true,
|
|
@@ -90,7 +90,7 @@ export const docs = {
|
|
|
90
90
|
export const docsDense = {
|
|
91
91
|
usage: {
|
|
92
92
|
description:
|
|
93
|
-
'Wraps subtree w/ specific
|
|
93
|
+
'Wraps subtree w/ specific Astryx theme. For static production themes, use `npx astryx theme build` + generated CSS + built theme object for first-paint/SSR performance. Use runtime `defineTheme()` for dynamic themes or prototyping. Token names always start with `--` (e.g. `--color-accent`, `--color-background-surface`).',
|
|
94
94
|
bestPractices: [
|
|
95
95
|
{
|
|
96
96
|
guidance: true,
|
package/src/theme/Theme.tsx
CHANGED
|
@@ -123,7 +123,7 @@ function useThemeStyleInjection(theme: DefinedTheme): void {
|
|
|
123
123
|
if (!warnedThemes.has(theme.name)) {
|
|
124
124
|
warnedThemes.add(theme.name);
|
|
125
125
|
console.warn(
|
|
126
|
-
`[
|
|
126
|
+
`[Astryx] Theme "${theme.name}" is using runtime style injection. ` +
|
|
127
127
|
`For better performance, use the pre-built theme:\n\n` +
|
|
128
128
|
` import {${theme.name}Theme} from '@astryxdesign/theme-${theme.name}/built';\n` +
|
|
129
129
|
` import '@astryxdesign/theme-${theme.name}/theme.css';\n\n` +
|
package/src/theme/defineTheme.ts
CHANGED
|
@@ -361,7 +361,7 @@ export interface DefinedTheme {
|
|
|
361
361
|
// =============================================================================
|
|
362
362
|
|
|
363
363
|
/** All Astryx token defaults as a flat map. Useful for resolving full token sets. */
|
|
364
|
-
export const
|
|
364
|
+
export const tokenDefaults: Record<string, string> = {
|
|
365
365
|
...colorDefaults,
|
|
366
366
|
...spacingDefaults,
|
|
367
367
|
...sizeDefaults,
|
package/src/theme/index.ts
CHANGED
|
@@ -134,7 +134,7 @@ export function defineSyntaxTheme(input: SyntaxThemeInput): SyntaxThemeDefinitio
|
|
|
134
134
|
const missing = ALL_SYNTAX_KEYS.filter(key => !(key in input.tokens));
|
|
135
135
|
if (missing.length > 0) {
|
|
136
136
|
console.warn(
|
|
137
|
-
'[
|
|
137
|
+
'[Astryx] defineSyntaxTheme("' + input.name + '"): missing tokens: ' +
|
|
138
138
|
missing.join(', ') + '. All 14 syntax tokens are required.',
|
|
139
139
|
);
|
|
140
140
|
}
|