@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
package/dist/theme/Theme.js
CHANGED
|
@@ -115,7 +115,7 @@ function useThemeStyleInjection(theme) {
|
|
|
115
115
|
// One-time perf hint per theme
|
|
116
116
|
if (!warnedThemes.has(theme.name)) {
|
|
117
117
|
warnedThemes.add(theme.name);
|
|
118
|
-
console.warn(`[
|
|
118
|
+
console.warn(`[Astryx] Theme "${theme.name}" is using runtime style injection. ` + `For better performance, use the pre-built theme:\n\n` + ` import {${theme.name}Theme} from '@astryxdesign/theme-${theme.name}/built';\n` + ` import '@astryxdesign/theme-${theme.name}/theme.css';\n\n` + `For custom themes, run \`npx astryx theme build <file>\` to generate ` + `the built artifacts.`);
|
|
119
119
|
}
|
|
120
120
|
const {
|
|
121
121
|
prose,
|
|
@@ -296,7 +296,7 @@ export interface DefinedTheme {
|
|
|
296
296
|
__onLight?: ResolvedOnMedia;
|
|
297
297
|
}
|
|
298
298
|
/** All Astryx token defaults as a flat map. Useful for resolving full token sets. */
|
|
299
|
-
export declare const
|
|
299
|
+
export declare const tokenDefaults: Record<string, string>;
|
|
300
300
|
/**
|
|
301
301
|
* Create an Astryx theme.
|
|
302
302
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defineTheme.d.ts","sourceRoot":"","sources":["../../src/theme/defineTheme.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"defineTheme.d.ts","sourceRoot":"","sources":["../../src/theme/defineTheme.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,4BAA4B,CAAC;AAC7D,OAAO,KAAK,EAAC,gBAAgB,EAAa,MAAM,SAAS,CAAC;AAC1D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,aAAa,EACb,eAAe,EACf,YAAY,EACZ,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAMzB,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAmB,KAAK,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AAE3E,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAEpD,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,UAAU,CAAC;AAMpD,wCAAwC;AACxC,MAAM,MAAM,aAAa,GACrB,MAAM,OAAO,aAAa,GAC1B,MAAM,OAAO,eAAe,GAC5B,MAAM,OAAO,YAAY,GACzB,MAAM,OAAO,cAAc,GAC3B,MAAM,OAAO,cAAc,GAC3B,MAAM,OAAO,gBAAgB,GAC7B,MAAM,OAAO,YAAY,GACzB,MAAM,OAAO,kBAAkB,GAC/B,MAAM,OAAO,kBAAkB,GAC/B,MAAM,OAAO,gBAAgB,GAC7B,MAAM,OAAO,kBAAkB,GAC/B,MAAM,OAAO,iBAAiB,CAAC;AAEnC,0DAA0D;AAC1D,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,eAAe,CAAC;AAExD;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAEhE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CACpC,MAAM,EACN,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAC/B,CAAC;AAEF,2BAA2B;AAC3B,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB;;;;;;;;;;;;;;;;;;OAkBG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB;;4EAEwE;IACxE,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;IAChD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;;;OAGG;IACH,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B;AAED,iDAAiD;AACjD,MAAM,WAAW,YAAY;IAC3B,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,gCAAgC;IAChC,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,oBAAoB;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9B,kEAAkE;IAClE,OAAO,CAAC,EAAE,IAAI,CAAC;IACf;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IACpD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B;;;OAGG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AAMD,qFAAqF;AACrF,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAchD,CAAC;AA6FF;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,YAAY,CA8JjE;AAMD,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAC;AAM9B,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAQpE"}
|
|
@@ -115,7 +115,7 @@ import { domainTokenDefaults } from "./domainTokens/index.js";
|
|
|
115
115
|
// =============================================================================
|
|
116
116
|
|
|
117
117
|
/** All Astryx token defaults as a flat map. Useful for resolving full token sets. */
|
|
118
|
-
export const
|
|
118
|
+
export const tokenDefaults = {
|
|
119
119
|
...colorDefaults,
|
|
120
120
|
...spacingDefaults,
|
|
121
121
|
...sizeDefaults,
|
package/dist/theme/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
export { Theme } from './Theme';
|
|
14
14
|
export { MediaTheme } from './MediaTheme';
|
|
15
15
|
export type { MediaThemeProps } from './MediaTheme';
|
|
16
|
-
export { defineTheme, generateThemeCSS, generateThemeCSSFlat, generateOnMediaCSS, generateThemeRules, generateThemeRulesSplit, type ThemeCSSOutput, type ThemeRulesSplit, isDefinedTheme,
|
|
16
|
+
export { defineTheme, generateThemeCSS, generateThemeCSSFlat, generateOnMediaCSS, generateThemeRules, generateThemeRulesSplit, type ThemeCSSOutput, type ThemeRulesSplit, isDefinedTheme, tokenDefaults, } from './defineTheme';
|
|
17
17
|
export type { DefineThemeInput, DefinedTheme, CoreTokenName, TokenName, TokenValue, ComponentStyleMap, StyleOverrides, } from './defineTheme';
|
|
18
18
|
export type { SyntaxTokenName, DomainTokenName, DataTokenName, } from './domainTokens';
|
|
19
19
|
export { syntaxTokenDefaults, domainTokenDefaults, dataTokenDefaults, } from './domainTokens';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/theme/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAC,KAAK,EAAC,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAC,UAAU,EAAC,MAAM,cAAc,CAAC;AACxC,YAAY,EAAC,eAAe,EAAC,MAAM,cAAc,CAAC;AAClD,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,cAAc,EACd,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/theme/index.ts"],"names":[],"mappings":"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"}
|
package/dist/theme/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export { Theme } from "./Theme.js";
|
|
18
18
|
export { MediaTheme } from "./MediaTheme.js";
|
|
19
|
-
export { defineTheme, generateThemeCSS, generateThemeCSSFlat, generateOnMediaCSS, generateThemeRules, generateThemeRulesSplit, isDefinedTheme,
|
|
19
|
+
export { defineTheme, generateThemeCSS, generateThemeCSSFlat, generateOnMediaCSS, generateThemeRules, generateThemeRulesSplit, isDefinedTheme, tokenDefaults } from "./defineTheme.js";
|
|
20
20
|
export { syntaxTokenDefaults, domainTokenDefaults, dataTokenDefaults } from "./domainTokens/index.js";
|
|
21
21
|
|
|
22
22
|
// Syntax theme API
|
|
@@ -97,7 +97,7 @@ export function resolveSyntaxTokenForMode(value, mode) {
|
|
|
97
97
|
export function defineSyntaxTheme(input) {
|
|
98
98
|
const missing = ALL_SYNTAX_KEYS.filter(key => !(key in input.tokens));
|
|
99
99
|
if (missing.length > 0) {
|
|
100
|
-
console.warn('[
|
|
100
|
+
console.warn('[Astryx] defineSyntaxTheme("' + input.name + '"): missing tokens: ' + missing.join(', ') + '. All 14 syntax tokens are required.');
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// Resolve tuples to light-dark() CSS strings
|
package/dist/theme/tokens.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export declare const tokenVars: Record<TokenName, string>;
|
|
|
48
48
|
/**
|
|
49
49
|
* Resolve all Astryx token values for a theme and effective color mode.
|
|
50
50
|
*
|
|
51
|
-
* The result starts with `
|
|
51
|
+
* The result starts with `tokenDefaults`, applies `theme.tokens`, then
|
|
52
52
|
* reapplies `theme.__inputTokens` when available so explicit tuple overrides
|
|
53
53
|
* retain their original light/dark sides instead of relying on CSS parsing.
|
|
54
54
|
* This mirrors the token resolution used by `useTheme()` but does not need
|
package/dist/theme/tokens.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - /packages/core/src/theme/index.ts
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { tokenDefaults } from "./defineTheme.js";
|
|
19
19
|
|
|
20
20
|
/** Resolved color mode used when choosing the side of light/dark token values. */
|
|
21
21
|
|
|
@@ -45,7 +45,7 @@ export function tokenVar(name) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** Flat map of every known Astryx token name to its `var(--token-name)` reference. */
|
|
48
|
-
export const tokenVars = Object.fromEntries(Object.keys(
|
|
48
|
+
export const tokenVars = Object.fromEntries(Object.keys(tokenDefaults).map(name => [name, tokenVar(name)]));
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Split the arguments of a CSS function body on the first top-level comma.
|
|
@@ -118,7 +118,7 @@ function resolveXDSTokenValue(value, mode) {
|
|
|
118
118
|
/**
|
|
119
119
|
* Resolve all Astryx token values for a theme and effective color mode.
|
|
120
120
|
*
|
|
121
|
-
* The result starts with `
|
|
121
|
+
* The result starts with `tokenDefaults`, applies `theme.tokens`, then
|
|
122
122
|
* reapplies `theme.__inputTokens` when available so explicit tuple overrides
|
|
123
123
|
* retain their original light/dark sides instead of relying on CSS parsing.
|
|
124
124
|
* This mirrors the token resolution used by `useTheme()` but does not need
|
|
@@ -131,7 +131,7 @@ export function resolveThemeTokens(theme, options) {
|
|
|
131
131
|
mode
|
|
132
132
|
} = options;
|
|
133
133
|
const resolved = {};
|
|
134
|
-
for (const [key, value] of Object.entries(
|
|
134
|
+
for (const [key, value] of Object.entries(tokenDefaults)) {
|
|
135
135
|
resolved[key] = resolveXDSTokenValue(value, mode);
|
|
136
136
|
}
|
|
137
137
|
if (theme == null) {
|
package/dist/theme/useTheme.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface UseThemeReturn {
|
|
|
30
30
|
* For tokens with [light, dark] tuples, returns the value matching
|
|
31
31
|
* the current mode. For single-value tokens, returns the value as-is.
|
|
32
32
|
*
|
|
33
|
-
* Falls back to
|
|
33
|
+
* Falls back to tokenDefaults if the token isn't overridden by the theme.
|
|
34
34
|
*
|
|
35
35
|
* @example
|
|
36
36
|
* ```
|
|
@@ -42,7 +42,7 @@ export interface UseThemeReturn {
|
|
|
42
42
|
/**
|
|
43
43
|
* All tokens resolved for the current color mode.
|
|
44
44
|
*
|
|
45
|
-
* Merges
|
|
45
|
+
* Merges tokenDefaults with the theme's overrides, resolving
|
|
46
46
|
* light-dark() values based on the effective color mode.
|
|
47
47
|
*
|
|
48
48
|
* Memoized — stable reference unless theme or mode changes.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dateParser.d.ts","sourceRoot":"","sources":["../../src/utils/dateParser.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,OAAO,EAAC,KAAK,SAAS,EAAqC,MAAM,aAAa,CAAC;AAE/E,OAAO,EACL,gBAAgB,IAAI,QAAQ,EAC5B,cAAc,IAAI,SAAS,GAC5B,MAAM,aAAa,CAAC;AAErB;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAK1C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"dateParser.d.ts","sourceRoot":"","sources":["../../src/utils/dateParser.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,OAAO,EAAC,KAAK,SAAS,EAAqC,MAAM,aAAa,CAAC;AAE/E,OAAO,EACL,gBAAgB,IAAI,QAAQ,EAC5B,cAAc,IAAI,SAAS,GAC5B,MAAM,aAAa,CAAC;AAErB;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAK1C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAsG9D"}
|
package/dist/utils/dateParser.js
CHANGED
|
@@ -112,10 +112,23 @@ export function parseDateInput(input) {
|
|
|
112
112
|
return parseNumericDate(+first, +second, currentYear);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
// 6. Fall back to native Date parsing for other formats
|
|
115
|
+
// 6. Fall back to native Date parsing for other formats.
|
|
116
|
+
//
|
|
117
|
+
// Skip bare numeric input (e.g. "0", "1", "01", "2026"). These are
|
|
118
|
+
// in-progress values a user is still typing, not complete dates. Native
|
|
119
|
+
// `Date` parsing coerces them into arbitrary dates ("0" -> year 2000 in V8,
|
|
120
|
+
// year 0 in some engines), which is both surprising and — when the year
|
|
121
|
+
// resolves to 0 — produces an out-of-range date that throws downstream.
|
|
122
|
+
// Treat them as not-yet-a-valid-date instead.
|
|
123
|
+
if (/^\d+$/.test(trimmed)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
116
126
|
const parsed = new Date(trimmed);
|
|
117
127
|
if (!isNaN(parsed.getTime())) {
|
|
118
|
-
|
|
128
|
+
const fromDate = plainDateFromDate(parsed);
|
|
129
|
+
// Validate the result so we never return an out-of-range date (e.g. a
|
|
130
|
+
// year of 0), which would throw when later re-parsed.
|
|
131
|
+
return tryCreatePlainDate(fromDate.year, fromDate.month, fromDate.day);
|
|
119
132
|
}
|
|
120
133
|
return null;
|
|
121
134
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1-canary.080d887",
|
|
4
4
|
"displayName": "XDS Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"url": "https://github.com/facebook/astryx/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
|
-
"
|
|
18
|
+
"astryx",
|
|
19
19
|
"design-system",
|
|
20
20
|
"react",
|
|
21
21
|
"components",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"type": "module",
|
|
28
28
|
"main": "./dist/index.js",
|
|
29
29
|
"types": "./dist/index.d.ts",
|
|
30
|
+
"unpkg": "./dist/astryx.umd.js",
|
|
31
|
+
"jsdelivr": "./dist/astryx.umd.js",
|
|
30
32
|
"sideEffects": [
|
|
31
33
|
"**/*.stylex.ts",
|
|
32
34
|
"**/*.stylex.js",
|
|
@@ -47,6 +49,7 @@
|
|
|
47
49
|
"types": "./src/astryx.css.d.ts",
|
|
48
50
|
"default": "./dist/astryx.css"
|
|
49
51
|
},
|
|
52
|
+
"./astryx.umd.js": "./dist/astryx.umd.js",
|
|
50
53
|
"./tailwind-theme.css": {
|
|
51
54
|
"types": "./src/tailwind-theme.css.d.ts",
|
|
52
55
|
"default": "./src/tailwind-theme.css"
|
|
@@ -635,9 +638,10 @@
|
|
|
635
638
|
"@testing-library/react": "^16.3.2"
|
|
636
639
|
},
|
|
637
640
|
"scripts": {
|
|
638
|
-
"build": "rm -rf dist && pnpm build:esm && tsc --project tsconfig.build.json && pnpm build:css",
|
|
641
|
+
"build": "rm -rf dist && pnpm build:esm && tsc --project tsconfig.build.json && pnpm build:css && pnpm build:umd",
|
|
639
642
|
"build:esm": "babel src --out-dir dist --extensions '.ts,.tsx' --out-file-extension '.js' --ignore '**/*.test.ts,**/*.test.tsx,**/__tests__/**,**/*.perf.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css,**/*.css.d.ts' --config-file ./babel.config.json",
|
|
640
643
|
"build:css": "node ../../scripts/build-css.mjs",
|
|
644
|
+
"build:umd": "node ../../scripts/build-umd.mjs",
|
|
641
645
|
"dev": "babel src --watch --out-dir dist --extensions '.ts,.tsx' --out-file-extension '.js' --ignore '**/*.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css*' --config-file ./babel.config.json",
|
|
642
646
|
"test": "vitest run --root ../..",
|
|
643
647
|
"test:watch": "vitest --root ../..",
|
|
@@ -132,6 +132,7 @@ export function AvatarGroupOverflow({
|
|
|
132
132
|
xstyle,
|
|
133
133
|
className,
|
|
134
134
|
style,
|
|
135
|
+
...rest
|
|
135
136
|
}: AvatarGroupOverflowProps): ReactNode {
|
|
136
137
|
const group = useAvatarGroup();
|
|
137
138
|
const numericSize = group?.numericSize ?? 36;
|
|
@@ -147,6 +148,7 @@ export function AvatarGroupOverflow({
|
|
|
147
148
|
type="button"
|
|
148
149
|
onClick={onClick}
|
|
149
150
|
aria-label={label}
|
|
151
|
+
{...rest}
|
|
150
152
|
{...mergeProps(
|
|
151
153
|
themeProps('avatar-group-overflow'),
|
|
152
154
|
stylex.props(
|
|
@@ -170,6 +172,7 @@ export function AvatarGroupOverflow({
|
|
|
170
172
|
<span
|
|
171
173
|
ref={ref}
|
|
172
174
|
aria-label={label}
|
|
175
|
+
{...rest}
|
|
173
176
|
{...mergeProps(
|
|
174
177
|
themeProps('avatar-group-overflow'),
|
|
175
178
|
stylex.props(
|
|
@@ -76,11 +76,22 @@ describe('Banner', () => {
|
|
|
76
76
|
expect(screen.getByText('This is a description')).toBeInTheDocument();
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
it('renders title and description as <div> (never <p>) for composition safety', () => {
|
|
80
|
+
const {container} = render(
|
|
81
|
+
<Banner status="info" title="Title" description="Description" />,
|
|
82
|
+
);
|
|
83
|
+
// Block content can be nested inside Banner text slots without tripping
|
|
84
|
+
// the phrasing-content trap that <p> imposes, so neither slot is a <p>.
|
|
85
|
+
expect(container.querySelector('p')).toBeNull();
|
|
86
|
+
expect(screen.getByText('Title').tagName).toBe('DIV');
|
|
87
|
+
expect(screen.getByText('Description').tagName).toBe('DIV');
|
|
88
|
+
});
|
|
89
|
+
|
|
79
90
|
it('does not render description when not provided', () => {
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
expect(
|
|
91
|
+
render(<Banner status="info" title="Title Only" />);
|
|
92
|
+
// Title renders; no description text is present.
|
|
93
|
+
expect(screen.getByText('Title Only')).toBeInTheDocument();
|
|
94
|
+
expect(screen.queryByText('This is a description')).not.toBeInTheDocument();
|
|
84
95
|
});
|
|
85
96
|
|
|
86
97
|
it('renders dismiss button when isDismissable', () => {
|
|
@@ -158,9 +169,7 @@ describe('Banner', () => {
|
|
|
158
169
|
});
|
|
159
170
|
|
|
160
171
|
it('renders card container by default', () => {
|
|
161
|
-
const {container} = render(
|
|
162
|
-
<Banner status="info" title="Card Container" />,
|
|
163
|
-
);
|
|
172
|
+
const {container} = render(<Banner status="info" title="Card Container" />);
|
|
164
173
|
const root = container.firstElementChild;
|
|
165
174
|
expect(root).toBeInTheDocument();
|
|
166
175
|
});
|
package/src/Banner/Banner.tsx
CHANGED
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
* - Each visual area owns its own border-radius (no overflow:clip on the container)
|
|
17
17
|
* - When children are provided, a collapse/expand toggle button appears in the end area
|
|
18
18
|
*
|
|
19
|
+
* Title and description render as <div> (not <p>): they accept arbitrary
|
|
20
|
+
* ReactNode content, and <p> cannot legally contain block-level children
|
|
21
|
+
* (the HTML parser reparents them, desyncing SSR markup from the hydrated
|
|
22
|
+
* DOM). Using <div> keeps these slots composable with any content. Their
|
|
23
|
+
* StyleX styles set margin: 0 and explicit typography, so the rendered
|
|
24
|
+
* appearance is identical to the previous <p>.
|
|
25
|
+
*
|
|
19
26
|
* SYNC: When modified, update these files to stay in sync:
|
|
20
27
|
* - /packages/core/src/Banner/Banner.doc.mjs (props table, features, implementation notes)
|
|
21
28
|
* - /packages/core/src/Banner/Banner.test.tsx (tests for new/changed behavior)
|
|
@@ -441,9 +448,9 @@ export function Banner({
|
|
|
441
448
|
)}
|
|
442
449
|
</div>
|
|
443
450
|
<div {...stylex.props(styles.headerContent)}>
|
|
444
|
-
<
|
|
451
|
+
<div {...stylex.props(styles.title)}>{title}</div>
|
|
445
452
|
{description != null && (
|
|
446
|
-
<
|
|
453
|
+
<div {...stylex.props(styles.description)}>{description}</div>
|
|
447
454
|
)}
|
|
448
455
|
</div>
|
|
449
456
|
{showEndArea && (
|
|
@@ -78,6 +78,12 @@ export const docs = {
|
|
|
78
78
|
description: 'Shows a loading spinner and disables interaction. Announces "Loading" via a live region.',
|
|
79
79
|
default: 'false',
|
|
80
80
|
},
|
|
81
|
+
{
|
|
82
|
+
name: 'isInterruptible',
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
description: 'Keep the button clickable while a clickAction is pending: the spinner and aria-busy still show, but the button is not disabled and the action is not deduped, so a re-click lands and interrupts the in-flight action with a fresh one.',
|
|
85
|
+
default: 'false',
|
|
86
|
+
},
|
|
81
87
|
{
|
|
82
88
|
name: 'isDisabled',
|
|
83
89
|
type: 'boolean',
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import {describe, it, expect, vi} from 'vitest';
|
|
13
|
-
import {render, screen} from '@testing-library/react';
|
|
13
|
+
import {render, screen, fireEvent, act} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import {Button} from './Button';
|
|
16
16
|
import {Badge} from '../Badge/Badge';
|
|
@@ -71,6 +71,32 @@ describe('Button', () => {
|
|
|
71
71
|
expect(button).toBeDisabled();
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
it('sets aria-busy synchronously while clickAction is pending', async () => {
|
|
75
|
+
// The spinner reveal is visually delayed (CSS animation-delay), but the
|
|
76
|
+
// loading DOM state — aria-busy and disabled — must not be delayed.
|
|
77
|
+
const user = userEvent.setup();
|
|
78
|
+
let resolveAction: (() => void) | undefined;
|
|
79
|
+
const clickAction = vi.fn(
|
|
80
|
+
async () =>
|
|
81
|
+
new Promise<void>(resolve => {
|
|
82
|
+
resolveAction = resolve;
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
render(<Button label="Save" clickAction={clickAction} />);
|
|
86
|
+
const button = screen.getByRole('button');
|
|
87
|
+
|
|
88
|
+
await user.click(button);
|
|
89
|
+
expect(button).toHaveAttribute('aria-busy', 'true');
|
|
90
|
+
expect(button).toBeDisabled();
|
|
91
|
+
|
|
92
|
+
await act(async () => {
|
|
93
|
+
resolveAction?.();
|
|
94
|
+
await Promise.resolve();
|
|
95
|
+
});
|
|
96
|
+
expect(button).not.toHaveAttribute('aria-busy', 'true');
|
|
97
|
+
expect(button).not.toBeDisabled();
|
|
98
|
+
});
|
|
99
|
+
|
|
74
100
|
it('renders the loading spinner with the inherit shade for every variant (#2717)', () => {
|
|
75
101
|
// The spinner must follow the button's resolved foreground color rather
|
|
76
102
|
// than a hardcoded white, so it keeps contrast on themed variants like the
|
|
@@ -228,11 +254,7 @@ describe('Button', () => {
|
|
|
228
254
|
order.push('clickAction');
|
|
229
255
|
});
|
|
230
256
|
render(
|
|
231
|
-
<Button
|
|
232
|
-
label="Test"
|
|
233
|
-
onClick={handleClick}
|
|
234
|
-
clickAction={handleAction}
|
|
235
|
-
/>,
|
|
257
|
+
<Button label="Test" onClick={handleClick} clickAction={handleAction} />,
|
|
236
258
|
);
|
|
237
259
|
|
|
238
260
|
await user.click(screen.getByRole('button'));
|
|
@@ -246,11 +268,7 @@ describe('Button', () => {
|
|
|
246
268
|
const handleClick = vi.fn((e: React.MouseEvent) => e.preventDefault());
|
|
247
269
|
const handleAction = vi.fn();
|
|
248
270
|
render(
|
|
249
|
-
<Button
|
|
250
|
-
label="Test"
|
|
251
|
-
onClick={handleClick}
|
|
252
|
-
clickAction={handleAction}
|
|
253
|
-
/>,
|
|
271
|
+
<Button label="Test" onClick={handleClick} clickAction={handleAction} />,
|
|
254
272
|
);
|
|
255
273
|
|
|
256
274
|
await user.click(screen.getByRole('button'));
|
|
@@ -258,6 +276,82 @@ describe('Button', () => {
|
|
|
258
276
|
expect(handleAction).not.toHaveBeenCalled();
|
|
259
277
|
});
|
|
260
278
|
|
|
279
|
+
it('fires clickAction once on a fast double-click (no double-submit)', async () => {
|
|
280
|
+
let resolveAction: (() => void) | undefined;
|
|
281
|
+
const handleAction = vi.fn(
|
|
282
|
+
async () =>
|
|
283
|
+
new Promise<void>(resolve => {
|
|
284
|
+
resolveAction = resolve;
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
render(<Button label="Pay" clickAction={handleAction} />);
|
|
288
|
+
|
|
289
|
+
const button = screen.getByRole('button');
|
|
290
|
+
await act(async () => {
|
|
291
|
+
fireEvent.click(button);
|
|
292
|
+
fireEvent.click(button);
|
|
293
|
+
});
|
|
294
|
+
expect(handleAction).toHaveBeenCalledTimes(1);
|
|
295
|
+
|
|
296
|
+
await act(async () => {
|
|
297
|
+
resolveAction?.();
|
|
298
|
+
await Promise.resolve();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('stays clickable (not disabled) while a clickAction is pending when isInterruptible', async () => {
|
|
303
|
+
const user = userEvent.setup();
|
|
304
|
+
let resolveAction: (() => void) | undefined;
|
|
305
|
+
const clickAction = vi.fn(
|
|
306
|
+
async () =>
|
|
307
|
+
new Promise<void>(resolve => {
|
|
308
|
+
resolveAction = resolve;
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
render(<Button label="Toggle" isInterruptible clickAction={clickAction} />);
|
|
312
|
+
const button = screen.getByRole('button');
|
|
313
|
+
|
|
314
|
+
await user.click(button);
|
|
315
|
+
// Loading is announced via aria-busy, but the button is not disabled so it
|
|
316
|
+
// can be re-clicked to interrupt the in-flight action.
|
|
317
|
+
expect(button).toHaveAttribute('aria-busy', 'true');
|
|
318
|
+
expect(button).not.toBeDisabled();
|
|
319
|
+
|
|
320
|
+
await act(async () => {
|
|
321
|
+
resolveAction?.();
|
|
322
|
+
await Promise.resolve();
|
|
323
|
+
});
|
|
324
|
+
expect(button).not.toHaveAttribute('aria-busy', 'true');
|
|
325
|
+
expect(button).not.toBeDisabled();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('re-fires clickAction on re-click while pending when isInterruptible (no dedupe)', async () => {
|
|
329
|
+
// Unlike the fire-once default, an interruptible action is not deduped: a
|
|
330
|
+
// re-click while pending starts a fresh action that interrupts the prior.
|
|
331
|
+
const resolvers: (() => void)[] = [];
|
|
332
|
+
const clickAction = vi.fn(
|
|
333
|
+
async () =>
|
|
334
|
+
new Promise<void>(resolve => {
|
|
335
|
+
resolvers.push(resolve);
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
338
|
+
render(<Button label="Toggle" isInterruptible clickAction={clickAction} />);
|
|
339
|
+
|
|
340
|
+
const button = screen.getByRole('button');
|
|
341
|
+
await act(async () => {
|
|
342
|
+
fireEvent.click(button);
|
|
343
|
+
});
|
|
344
|
+
await act(async () => {
|
|
345
|
+
fireEvent.click(button);
|
|
346
|
+
});
|
|
347
|
+
expect(clickAction).toHaveBeenCalledTimes(2);
|
|
348
|
+
|
|
349
|
+
await act(async () => {
|
|
350
|
+
resolvers.forEach(resolve => resolve());
|
|
351
|
+
await Promise.resolve();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
261
355
|
// type/name/value/form props
|
|
262
356
|
it('defaults type to button', () => {
|
|
263
357
|
render(<Button label="Test" />);
|
package/src/Button/Button.tsx
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - /apps/storybook/stories/Button.stories.tsx (storybook stories)
|
|
16
16
|
* - /packages/cli/templates/blocks/components/Button/ (showcase blocks)
|
|
17
17
|
*
|
|
18
|
-
* Last synced props: label, variant, size, isDisabled, isLoading, clickAction, icon, isIconOnly, children, tooltip, endContent, href, as, target, rel
|
|
18
|
+
* Last synced props: label, variant, size, isDisabled, isLoading, isInterruptible, clickAction, icon, isIconOnly, children, tooltip, endContent, href, as, target, rel
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import {useRef, useTransition, type ReactNode} from 'react';
|
|
@@ -320,6 +320,16 @@ export interface ButtonProps extends BaseProps<HTMLButtonElement> {
|
|
|
320
320
|
* @default false
|
|
321
321
|
*/
|
|
322
322
|
isLoading?: boolean;
|
|
323
|
+
/**
|
|
324
|
+
* Keep the button interactive while a `clickAction` is pending. The loading
|
|
325
|
+
* state still renders the spinner and `aria-busy`, but the button is not
|
|
326
|
+
* disabled and the in-flight action is not deduped — so a re-click lands and
|
|
327
|
+
* interrupts the previous action with a fresh one. Use for interruptible
|
|
328
|
+
* actions (e.g. a toggle whose action can be re-triggered before the previous
|
|
329
|
+
* one settles), not fire-once actions (submit/save/pay).
|
|
330
|
+
* @default false
|
|
331
|
+
*/
|
|
332
|
+
isInterruptible?: boolean;
|
|
323
333
|
/**
|
|
324
334
|
* Click handler. For async actions that should show a loading state,
|
|
325
335
|
* use `clickAction` instead.
|
|
@@ -380,6 +390,22 @@ export interface ButtonProps extends BaseProps<HTMLButtonElement> {
|
|
|
380
390
|
rel?: string;
|
|
381
391
|
}
|
|
382
392
|
|
|
393
|
+
const spinnerReveal = stylex.keyframes({
|
|
394
|
+
from: {opacity: 0},
|
|
395
|
+
to: {opacity: 1},
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const contentHide = stylex.keyframes({
|
|
399
|
+
from: {color: 'inherit'},
|
|
400
|
+
to: {color: 'transparent'},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Hold the loading swap for a short delay so a fast action (e.g. clickAction)
|
|
404
|
+
// that settles within the delay never flashes a spinner. The spinner fade-in
|
|
405
|
+
// and the content hide share the same delay so the button never shows an empty
|
|
406
|
+
// frame in between. Reduced motion is instant.
|
|
407
|
+
const SPINNER_DELAY = durationVars['--duration-medium-min'];
|
|
408
|
+
|
|
383
409
|
const loadingStyles = stylex.create({
|
|
384
410
|
// Hide the button's own content while the spinner overlay is shown. Applied
|
|
385
411
|
// to the content wrapper (not the button) so the button keeps its variant
|
|
@@ -387,6 +413,17 @@ const loadingStyles = stylex.create({
|
|
|
387
413
|
hiddenContent: {
|
|
388
414
|
color: 'transparent',
|
|
389
415
|
},
|
|
416
|
+
// Delayed variant: keep content visible, then hide it in lockstep with the
|
|
417
|
+
// spinner reveal once the delay elapses.
|
|
418
|
+
hiddenContentDelayed: {
|
|
419
|
+
animationName: contentHide,
|
|
420
|
+
animationDuration: '1ms',
|
|
421
|
+
animationFillMode: 'forwards',
|
|
422
|
+
animationDelay: {
|
|
423
|
+
default: SPINNER_DELAY,
|
|
424
|
+
'@media (prefers-reduced-motion: reduce)': '0s',
|
|
425
|
+
},
|
|
426
|
+
},
|
|
390
427
|
spinnerOverlay: {
|
|
391
428
|
position: 'absolute',
|
|
392
429
|
top: 0,
|
|
@@ -396,6 +433,15 @@ const loadingStyles = stylex.create({
|
|
|
396
433
|
display: 'grid',
|
|
397
434
|
placeItems: 'center',
|
|
398
435
|
},
|
|
436
|
+
spinnerDelayed: {
|
|
437
|
+
animationName: spinnerReveal,
|
|
438
|
+
animationDuration: durationVars['--duration-fast'],
|
|
439
|
+
animationFillMode: 'backwards',
|
|
440
|
+
animationDelay: {
|
|
441
|
+
default: SPINNER_DELAY,
|
|
442
|
+
'@media (prefers-reduced-motion: reduce)': '0s',
|
|
443
|
+
},
|
|
444
|
+
},
|
|
399
445
|
});
|
|
400
446
|
|
|
401
447
|
const groupStyles = stylex.create({
|
|
@@ -493,6 +539,7 @@ export function Button({
|
|
|
493
539
|
type = 'button',
|
|
494
540
|
isDisabled = false,
|
|
495
541
|
isLoading = false,
|
|
542
|
+
isInterruptible = false,
|
|
496
543
|
clickAction,
|
|
497
544
|
icon,
|
|
498
545
|
isIconOnly = false,
|
|
@@ -513,10 +560,23 @@ export function Button({
|
|
|
513
560
|
const buttonGroup = useButtonGroup();
|
|
514
561
|
|
|
515
562
|
const [isPending, startTransition] = useTransition();
|
|
563
|
+
// clickAction is normally fire-once (submit/save/pay), so a same-tick
|
|
564
|
+
// double-click must dedupe — which neither isPending nor useOptimistic do.
|
|
565
|
+
// Hence the ref guard. Interruptible callers (e.g. ToggleButton) opt out so a
|
|
566
|
+
// re-click can land and interrupt the in-flight action with a fresh one.
|
|
516
567
|
const actionInFlightRef = useRef(false);
|
|
517
568
|
const isLoadingState = isLoading || isPending;
|
|
569
|
+
// Delay the spinner reveal for action-driven loading (clickAction's own
|
|
570
|
+
// transition) so a fast action that settles within the delay does not flash
|
|
571
|
+
// a spinner. Interruptible loading is delayed too, so rapid re-clicks settle
|
|
572
|
+
// before any spinner shows. Explicit isLoading-only stays immediate, since
|
|
573
|
+
// the consumer is deliberately showing it.
|
|
574
|
+
const delaySpinner = isPending || isInterruptible;
|
|
518
575
|
const groupDisabled = buttonGroup?.isDisabled ?? false;
|
|
519
|
-
|
|
576
|
+
// When interruptible, the loading state drives the spinner and aria-busy but
|
|
577
|
+
// not disabled, so clicks keep landing and can interrupt the in-flight action.
|
|
578
|
+
const buttonDisabled =
|
|
579
|
+
isDisabled || groupDisabled || (isLoadingState && !isInterruptible);
|
|
520
580
|
// isIconOnly prop is the source of truth for icon-only rendering.
|
|
521
581
|
// When false (default), label is always rendered as visible text.
|
|
522
582
|
|
|
@@ -531,7 +591,9 @@ export function Button({
|
|
|
531
591
|
const useAriaDisabled = tooltip != null && buttonDisabled;
|
|
532
592
|
|
|
533
593
|
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
534
|
-
|
|
594
|
+
// The ref guard dedupes fire-once actions. Interruptible callers skip it so
|
|
595
|
+
// a re-click while pending starts a fresh action that interrupts the prior.
|
|
596
|
+
if (buttonDisabled || (actionInFlightRef.current && !isInterruptible)) {
|
|
535
597
|
e.preventDefault();
|
|
536
598
|
return;
|
|
537
599
|
}
|
|
@@ -598,7 +660,10 @@ export function Button({
|
|
|
598
660
|
<>
|
|
599
661
|
{isLoadingState && (
|
|
600
662
|
<span
|
|
601
|
-
{...stylex.props(
|
|
663
|
+
{...stylex.props(
|
|
664
|
+
loadingStyles.spinnerOverlay,
|
|
665
|
+
delaySpinner && loadingStyles.spinnerDelayed,
|
|
666
|
+
)}
|
|
602
667
|
aria-hidden="true">
|
|
603
668
|
<Spinner size="sm" shade="inherit" />
|
|
604
669
|
</span>
|
|
@@ -606,7 +671,10 @@ export function Button({
|
|
|
606
671
|
<span
|
|
607
672
|
{...stylex.props(
|
|
608
673
|
styles.contentWrapper,
|
|
609
|
-
isLoadingState &&
|
|
674
|
+
isLoadingState &&
|
|
675
|
+
(delaySpinner
|
|
676
|
+
? loadingStyles.hiddenContentDelayed
|
|
677
|
+
: loadingStyles.hiddenContent),
|
|
610
678
|
)}
|
|
611
679
|
aria-hidden={isLoadingState || undefined}>
|
|
612
680
|
{icon && (
|