@astryxdesign/core 0.1.1 → 0.1.2-canary.2581eb4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +119 -1
  3. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
  4. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  5. package/dist/AvatarGroup/AvatarGroupOverflow.js +4 -1
  6. package/dist/Banner/Banner.d.ts +7 -0
  7. package/dist/Banner/Banner.d.ts.map +1 -1
  8. package/dist/Banner/Banner.js +9 -2
  9. package/dist/Button/Button.d.ts +12 -2
  10. package/dist/Button/Button.d.ts.map +1 -1
  11. package/dist/Button/Button.js +51 -13
  12. package/dist/Calendar/Calendar.d.ts +5 -4
  13. package/dist/Calendar/Calendar.d.ts.map +1 -1
  14. package/dist/Calendar/Calendar.js +6 -1
  15. package/dist/Calendar/index.d.ts +1 -1
  16. package/dist/Calendar/index.d.ts.map +1 -1
  17. package/dist/DateTimeInput/DateTimeInput.d.ts +7 -2
  18. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  19. package/dist/DateTimeInput/DateTimeInput.js +5 -4
  20. package/dist/EmptyState/EmptyState.d.ts.map +1 -1
  21. package/dist/EmptyState/EmptyState.js +7 -1
  22. package/dist/HoverCard/HoverCard.d.ts +2 -2
  23. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  24. package/dist/HoverCard/HoverCard.js +18 -6
  25. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  26. package/dist/HoverCard/useHoverCard.js +7 -3
  27. package/dist/Kbd/Kbd.d.ts.map +1 -1
  28. package/dist/Kbd/Kbd.js +1 -1
  29. package/dist/Layer/anchorName.d.ts +20 -0
  30. package/dist/Layer/anchorName.d.ts.map +1 -0
  31. package/dist/Layer/anchorName.js +36 -0
  32. package/dist/Layer/useLayer.d.ts +19 -0
  33. package/dist/Layer/useLayer.d.ts.map +1 -1
  34. package/dist/Layer/useLayer.js +15 -6
  35. package/dist/Link/Link.d.ts +1 -1
  36. package/dist/Link/Link.js +2 -2
  37. package/dist/Link/LinkProvider.d.ts +1 -0
  38. package/dist/Link/LinkProvider.d.ts.map +1 -1
  39. package/dist/Link/LinkProvider.js +1 -0
  40. package/dist/Markdown/Markdown.d.ts.map +1 -1
  41. package/dist/Markdown/Markdown.js +13 -3
  42. package/dist/MobileNav/MobileNav.d.ts.map +1 -1
  43. package/dist/MobileNav/MobileNav.js +13 -0
  44. package/dist/Pagination/Pagination.d.ts.map +1 -1
  45. package/dist/Pagination/Pagination.js +31 -27
  46. package/dist/Slider/Slider.js +2 -2
  47. package/dist/Table/BaseTable.d.ts.map +1 -1
  48. package/dist/Table/BaseTable.js +27 -8
  49. package/dist/Table/Table.d.ts.map +1 -1
  50. package/dist/Table/Table.js +30 -7
  51. package/dist/Table/TableRow.d.ts +6 -1
  52. package/dist/Table/TableRow.d.ts.map +1 -1
  53. package/dist/Table/TableRow.js +15 -7
  54. package/dist/Table/index.d.ts +3 -1
  55. package/dist/Table/index.d.ts.map +1 -1
  56. package/dist/Table/index.js +1 -0
  57. package/dist/Table/plugins/stickyColumns/index.d.ts +3 -0
  58. package/dist/Table/plugins/stickyColumns/index.d.ts.map +1 -0
  59. package/dist/Table/plugins/stickyColumns/index.js +3 -0
  60. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts +25 -0
  61. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.d.ts.map +1 -0
  62. package/dist/Table/plugins/stickyColumns/useTableStickyColumns.js +369 -0
  63. package/dist/Table/types.d.ts +95 -5
  64. package/dist/Table/types.d.ts.map +1 -1
  65. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  66. package/dist/Table/useBaseTablePlugins.js +1 -1
  67. package/dist/Text/text.stylex.d.ts +2 -2
  68. package/dist/Text/text.stylex.js +2 -2
  69. package/dist/Timestamp/Timestamp.d.ts +1 -1
  70. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  71. package/dist/Timestamp/Timestamp.js +28 -3
  72. package/dist/ToggleButton/ToggleButton.d.ts +7 -9
  73. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  74. package/dist/ToggleButton/ToggleButton.js +34 -57
  75. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  76. package/dist/Tokenizer/Tokenizer.js +1 -1
  77. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  78. package/dist/Tooltip/useTooltip.js +1 -0
  79. package/dist/astryx.css +30 -1
  80. package/dist/astryx.umd.js +147 -0
  81. package/dist/astryx.umd.js.map +7 -0
  82. package/dist/theme/generateThemeRules.js +1 -1
  83. package/dist/theme/syntax/SyntaxTheme.d.ts +1 -1
  84. package/dist/theme/syntax/SyntaxTheme.js +1 -1
  85. package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
  86. package/dist/theme/syntax/defineSyntaxTheme.js +2 -2
  87. package/dist/theme/syntax/presets.d.ts +1 -1
  88. package/dist/theme/syntax/presets.d.ts.map +1 -1
  89. package/dist/theme/syntax/presets.js +1 -1
  90. package/dist/theme/syntax/tokens.d.ts +1 -1
  91. package/dist/theme/syntax/tokens.js +1 -1
  92. package/dist/theme/types.d.ts +1 -1
  93. package/dist/utils/dateTypes.d.ts +12 -0
  94. package/dist/utils/dateTypes.d.ts.map +1 -1
  95. package/dist/utils/dateTypes.js +41 -1
  96. package/package.json +6 -2
  97. package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -0
  98. package/src/Banner/Banner.doc.mjs +8 -0
  99. package/src/Banner/Banner.test.tsx +16 -7
  100. package/src/Banner/Banner.tsx +9 -2
  101. package/src/Button/Button.doc.mjs +6 -0
  102. package/src/Button/Button.test.tsx +105 -11
  103. package/src/Button/Button.tsx +73 -5
  104. package/src/Calendar/Calendar.doc.mjs +4 -4
  105. package/src/Calendar/Calendar.test.tsx +21 -26
  106. package/src/Calendar/Calendar.tsx +21 -5
  107. package/src/Calendar/index.ts +1 -0
  108. package/src/Chat/ChatComposer.doc.mjs +3 -0
  109. package/src/Chat/ChatComposerDrawer.doc.mjs +12 -0
  110. package/src/CheckboxInput/CheckboxInput.doc.mjs +8 -0
  111. package/src/ClickableCard/ClickableCard.doc.mjs +15 -0
  112. package/src/DateTimeInput/DateTimeInput.doc.mjs +11 -3
  113. package/src/DateTimeInput/DateTimeInput.test.tsx +23 -3
  114. package/src/DateTimeInput/DateTimeInput.tsx +13 -10
  115. package/src/Dialog/Dialog.doc.mjs +2 -2
  116. package/src/EmptyState/EmptyState.test.tsx +4 -2
  117. package/src/EmptyState/EmptyState.tsx +6 -2
  118. package/src/HoverCard/HoverCard.doc.mjs +3 -0
  119. package/src/HoverCard/HoverCard.test.tsx +189 -2
  120. package/src/HoverCard/HoverCard.tsx +20 -16
  121. package/src/HoverCard/useHoverCard.tsx +13 -10
  122. package/src/Icon/Icon.doc.mjs +8 -0
  123. package/src/Kbd/Kbd.tsx +2 -9
  124. package/src/Layer/anchorName.test.ts +146 -0
  125. package/src/Layer/anchorName.ts +46 -0
  126. package/src/Layer/useLayer.doc.mjs +7 -2
  127. package/src/Layer/useLayer.tsx +33 -9
  128. package/src/Layout/Layout.doc.mjs +1 -1
  129. package/src/Lightbox/Lightbox.doc.mjs +0 -2
  130. package/src/Link/Link.test.tsx +2 -2
  131. package/src/Link/Link.tsx +3 -3
  132. package/src/Link/LinkProvider.tsx +1 -0
  133. package/src/List/List.doc.mjs +7 -0
  134. package/src/Markdown/Markdown.doc.mjs +2 -0
  135. package/src/Markdown/Markdown.test.tsx +17 -26
  136. package/src/Markdown/Markdown.tsx +16 -6
  137. package/src/MobileNav/MobileNav.doc.mjs +8 -8
  138. package/src/MobileNav/MobileNav.tsx +13 -0
  139. package/src/MobileNav/MobileNavReopen.test.tsx +118 -0
  140. package/src/OverflowList/OverflowList.doc.mjs +17 -0
  141. package/src/Pagination/Pagination.test.tsx +137 -13
  142. package/src/Pagination/Pagination.tsx +33 -28
  143. package/src/ProgressBar/ProgressBar.test.tsx +13 -3
  144. package/src/Resizable/Resizable.doc.mjs +1 -1
  145. package/src/Section/Section.doc.mjs +11 -0
  146. package/src/SelectableCard/SelectableCard.doc.mjs +15 -0
  147. package/src/Selector/Selector.doc.mjs +4 -0
  148. package/src/Skeleton/Skeleton.doc.mjs +11 -1
  149. package/src/Slider/Slider.doc.mjs +4 -0
  150. package/src/Slider/Slider.test.tsx +3 -16
  151. package/src/Slider/Slider.tsx +1 -1
  152. package/src/TabList/TabList.doc.mjs +6 -0
  153. package/src/Table/BaseTable.tsx +51 -24
  154. package/src/Table/Table.doc.mjs +12 -0
  155. package/src/Table/Table.test.tsx +29 -30
  156. package/src/Table/Table.tsx +22 -1
  157. package/src/Table/TableRow.tsx +37 -7
  158. package/src/Table/index.ts +3 -0
  159. package/src/Table/plugins/stickyColumns/index.ts +4 -0
  160. package/src/Table/plugins/stickyColumns/useTableStickyColumns.test.tsx +163 -0
  161. package/src/Table/plugins/stickyColumns/useTableStickyColumns.tsx +407 -0
  162. package/src/Table/types.ts +101 -4
  163. package/src/Table/useBaseTablePlugins.ts +1 -0
  164. package/src/Table/useTableColumnResize.doc.mjs +96 -0
  165. package/src/Table/useTableStickyColumns.doc.mjs +59 -0
  166. package/src/Text/Heading.doc.mjs +15 -2
  167. package/src/Text/Text.doc.mjs +7 -1
  168. package/src/Text/text.stylex.ts +2 -2
  169. package/src/Timestamp/Timestamp.doc.mjs +9 -0
  170. package/src/Timestamp/Timestamp.test.tsx +44 -18
  171. package/src/Timestamp/Timestamp.tsx +30 -10
  172. package/src/ToggleButton/ToggleButton.doc.mjs +9 -3
  173. package/src/ToggleButton/ToggleButton.test.tsx +39 -18
  174. package/src/ToggleButton/ToggleButton.tsx +46 -83
  175. package/src/Tokenizer/Tokenizer.tsx +4 -1
  176. package/src/Toolbar/Toolbar.doc.mjs +65 -5
  177. package/src/Tooltip/Tooltip.test.tsx +13 -0
  178. package/src/Tooltip/useTooltip.tsx +2 -3
  179. package/src/hooks/useImageMode.test.ts +133 -0
  180. package/src/theme/derivedVarRegistry.test.ts +4 -0
  181. package/src/theme/generateThemeRules.test.ts +7 -8
  182. package/src/theme/generateThemeRules.ts +1 -1
  183. package/src/theme/syntax/SyntaxTheme.tsx +1 -1
  184. package/src/theme/syntax/defineSyntaxTheme.ts +20 -11
  185. package/src/theme/syntax/presets.ts +5 -2
  186. package/src/theme/syntax/tokens.ts +1 -1
  187. package/src/theme/types.ts +1 -1
  188. package/src/utils/dateTypes.test.ts +40 -0
  189. package/src/utils/dateTypes.ts +40 -0
@@ -388,7 +388,7 @@ function generateColorOverrides(components, parts) {
388
388
  secondary: 'var(--color-text-secondary)',
389
389
  disabled: 'var(--color-text-disabled)',
390
390
  placeholder: 'var(--color-text-secondary)',
391
- active: 'var(--color-accent)'
391
+ accent: 'var(--color-text-accent)'
392
392
  };
393
393
  const touchesText = 'text' in components;
394
394
  const touchesHeading = 'heading' in components;
@@ -4,7 +4,7 @@
4
4
  * @output SyntaxTheme provider, useSyntaxTheme hook
5
5
  * @position Provider component; wraps code surfaces to apply syntax coloring
6
6
  *
7
- * @see https://github.com/facebookexperimental/xds/issues/1148
7
+ * @see https://github.com/facebook/astryx/issues/1148
8
8
  */
9
9
  import React from 'react';
10
10
  import { type SyntaxThemeDefinition, type SyntaxThemeTokenKey } from './defineSyntaxTheme';
@@ -8,7 +8,7 @@
8
8
  * @output SyntaxTheme provider, useSyntaxTheme hook
9
9
  * @position Provider component; wraps code surfaces to apply syntax coloring
10
10
  *
11
- * @see https://github.com/facebookexperimental/xds/issues/1148
11
+ * @see https://github.com/facebook/astryx/issues/1148
12
12
  */
13
13
  import React, { createContext, use, useMemo } from 'react';
14
14
  import { syntaxThemeStyle, resolveSyntaxTokenForMode, ALL_SYNTAX_KEYS } from "./defineSyntaxTheme.js";
@@ -1 +1 @@
1
- {"version":3,"file":"defineSyntaxTheme.d.ts","sourceRoot":"","sources":["../../../src/theme/syntax/defineSyntaxTheme.ts"],"names":[],"mappings":"AAmBA,6EAA6E;AAC7E,MAAM,MAAM,mBAAmB,GAC3B,SAAS,GACT,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,UAAU,GACV,MAAM,GACN,UAAU,GACV,UAAU,GACV,UAAU,GACV,KAAK,GACL,WAAW,GACX,UAAU,GACV,aAAa,GACb,YAAY,CAAC;AAEjB;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAEtE,+EAA+E;AAC/E,MAAM,MAAM,qBAAqB,GAAG,MAAM,CAAC,mBAAmB,EAAE,gBAAgB,CAAC,CAAC;AAElF,yFAAyF;AACzF,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;AAEtE,kCAAkC;AAClC,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,qBAAqB,CAAC;CAC/B;AAED,mEAAmE;AACnE,MAAM,WAAW,qBAAqB;IACpC,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,MAAM,EAAE,mBAAmB,CAAC;IAC5B,mEAAmE;IACnE,aAAa,EAAE,qBAAqB,CAAC;CACtC;AAYD,sEAAsE;AACtE,eAAO,MAAM,eAAe,EAAE,mBAAmB,EACY,CAAC;AAc9D;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,OAAO,GAAG,MAAM,GACrB,MAAM,CASR;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,qBAAqB,CAoBhF;AAMD,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,qBAAqB,GAC3B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,CAIrE"}
1
+ {"version":3,"file":"defineSyntaxTheme.d.ts","sourceRoot":"","sources":["../../../src/theme/syntax/defineSyntaxTheme.ts"],"names":[],"mappings":"AAmBA,6EAA6E;AAC7E,MAAM,MAAM,mBAAmB,GAC3B,SAAS,GACT,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,UAAU,GACV,MAAM,GACN,UAAU,GACV,UAAU,GACV,UAAU,GACV,KAAK,GACL,WAAW,GACX,UAAU,GACV,aAAa,GACb,YAAY,CAAC;AAEjB;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAEtE,+EAA+E;AAC/E,MAAM,MAAM,qBAAqB,GAAG,MAAM,CACxC,mBAAmB,EACnB,gBAAgB,CACjB,CAAC;AAEF,yFAAyF;AACzF,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;AAEtE,kCAAkC;AAClC,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,qBAAqB,CAAC;CAC/B;AAED,mEAAmE;AACnE,MAAM,WAAW,qBAAqB;IACpC,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,MAAM,EAAE,mBAAmB,CAAC;IAC5B,mEAAmE;IACnE,aAAa,EAAE,qBAAqB,CAAC;CACtC;AAYD,sEAAsE;AACtE,eAAO,MAAM,eAAe,EAAE,mBAAmB,EAEW,CAAC;AAc7D;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,OAAO,GAAG,MAAM,GACrB,MAAM,CASR;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,gBAAgB,GACtB,qBAAqB,CAuBvB;AAMD,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,qBAAqB,GAC3B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,MAAM,CAIrE"}
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- "use strict";
3
+ 'use strict';
4
4
 
5
5
  /**
6
6
  * @file defineSyntaxTheme.ts
@@ -8,7 +8,7 @@
8
8
  * @output defineSyntaxTheme, SyntaxTheme, syntaxThemeStyle
9
9
  * @position Syntax theme definition API; consumed by presets, SyntaxTheme, defineTheme
10
10
  *
11
- * @see https://github.com/facebookexperimental/xds/issues/1148
11
+ * @see https://github.com/facebook/astryx/issues/1148
12
12
  */
13
13
  import { syntaxTokenDefaults } from "./tokens.js";
14
14
 
@@ -6,7 +6,7 @@
6
6
  * All original themes are MIT licensed. Only color values are extracted.
7
7
  * See THIRD_PARTY_LICENSES.md for full attribution.
8
8
  *
9
- * @see https://github.com/facebookexperimental/xds/issues/1148
9
+ * @see https://github.com/facebook/astryx/issues/1148
10
10
  */
11
11
  import { type SyntaxThemeDefinition } from './defineSyntaxTheme';
12
12
  /** One Dark Pro (Binaryify) — MIT */
@@ -1 +1 @@
1
- {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../../src/theme/syntax/presets.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,OAAO,EAAoB,KAAK,qBAAqB,EAAC,MAAM,qBAAqB,CAAC;AAElF,qCAAqC;AACrC,eAAO,MAAM,UAAU,EAAE,qBAkBvB,CAAC;AAEH,iCAAiC;AACjC,eAAO,MAAM,OAAO,EAAE,qBAkBpB,CAAC;AAEH,wDAAwD;AACxD,eAAO,MAAM,OAAO,EAAE,qBAkBpB,CAAC;AAEH,iDAAiD;AACjD,eAAO,MAAM,IAAI,EAAE,qBAkBjB,CAAC;AAEH,gCAAgC;AAChC,eAAO,MAAM,UAAU,EAAE,qBAkBvB,CAAC;AAEH,uDAAuD;AACvD,eAAO,MAAM,eAAe,EAAE,qBAkB5B,CAAC;AAEH,8CAA8C;AAC9C,eAAO,MAAM,WAAW,EAAE,qBAkBxB,CAAC;AAEH,6CAA6C;AAC7C,eAAO,MAAM,UAAU,EAAE,qBAkBvB,CAAC;AAEH,+CAA+C;AAC/C,eAAO,MAAM,cAAc,EAAE,qBAkB3B,CAAC;AAEH,2CAA2C;AAC3C,eAAO,MAAM,QAAQ,EAAE,qBAkBrB,CAAC;AAEH,uDAAuD;AACvD,eAAO,MAAM,eAAe,EAAE,qBAkB5B,CAAC;AAEH,sCAAsC;AACtC,eAAO,MAAM,eAAe,EAAE,qBAkB5B,CAAC;AAEH,oCAAoC;AACpC,eAAO,MAAM,iBAAiB,4KAQpB,CAAC;AAEX,qCAAqC;AACrC,eAAO,MAAM,kBAAkB,8HAMrB,CAAC;AAEX,+BAA+B;AAC/B,eAAO,MAAM,gBAAgB,+RAGnB,CAAC"}
1
+ {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../../src/theme/syntax/presets.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AAEH,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,qBAAqB,CAAC;AAE7B,qCAAqC;AACrC,eAAO,MAAM,UAAU,EAAE,qBAkBvB,CAAC;AAEH,iCAAiC;AACjC,eAAO,MAAM,OAAO,EAAE,qBAkBpB,CAAC;AAEH,wDAAwD;AACxD,eAAO,MAAM,OAAO,EAAE,qBAkBpB,CAAC;AAEH,iDAAiD;AACjD,eAAO,MAAM,IAAI,EAAE,qBAkBjB,CAAC;AAEH,gCAAgC;AAChC,eAAO,MAAM,UAAU,EAAE,qBAkBvB,CAAC;AAEH,uDAAuD;AACvD,eAAO,MAAM,eAAe,EAAE,qBAkB5B,CAAC;AAEH,8CAA8C;AAC9C,eAAO,MAAM,WAAW,EAAE,qBAkBxB,CAAC;AAEH,6CAA6C;AAC7C,eAAO,MAAM,UAAU,EAAE,qBAkBvB,CAAC;AAEH,+CAA+C;AAC/C,eAAO,MAAM,cAAc,EAAE,qBAkB3B,CAAC;AAEH,2CAA2C;AAC3C,eAAO,MAAM,QAAQ,EAAE,qBAkBrB,CAAC;AAEH,uDAAuD;AACvD,eAAO,MAAM,eAAe,EAAE,qBAkB5B,CAAC;AAEH,sCAAsC;AACtC,eAAO,MAAM,eAAe,EAAE,qBAkB5B,CAAC;AAEH,oCAAoC;AACpC,eAAO,MAAM,iBAAiB,4KAQpB,CAAC;AAEX,qCAAqC;AACrC,eAAO,MAAM,kBAAkB,8HAMrB,CAAC;AAEX,+BAA+B;AAC/B,eAAO,MAAM,gBAAgB,+RAGnB,CAAC"}
@@ -8,7 +8,7 @@
8
8
  * All original themes are MIT licensed. Only color values are extracted.
9
9
  * See THIRD_PARTY_LICENSES.md for full attribution.
10
10
  *
11
- * @see https://github.com/facebookexperimental/xds/issues/1148
11
+ * @see https://github.com/facebook/astryx/issues/1148
12
12
  */
13
13
 
14
14
  import { defineSyntaxTheme } from "./defineSyntaxTheme.js";
@@ -14,7 +14,7 @@
14
14
  * 14-token architecture validated against 11 community code themes.
15
15
  * All themes map cleanly to these 14 slots.
16
16
  *
17
- * @see https://github.com/facebookexperimental/xds/issues/1148
17
+ * @see https://github.com/facebook/astryx/issues/1148
18
18
  */
19
19
  export declare const syntaxTokenDefaults: {
20
20
  readonly '--color-syntax-keyword': "var(--color-text-accent)";
@@ -16,7 +16,7 @@
16
16
  * 14-token architecture validated against 11 community code themes.
17
17
  * All themes map cleanly to these 14 slots.
18
18
  *
19
- * @see https://github.com/facebookexperimental/xds/issues/1148
19
+ * @see https://github.com/facebook/astryx/issues/1148
20
20
  */
21
21
 
22
22
  export const syntaxTokenDefaults = {
@@ -116,7 +116,7 @@ export type TextWeight = 'normal' | 'medium' | 'semibold' | 'bold';
116
116
  /**
117
117
  * Text color variants for Text/Heading
118
118
  */
119
- export type TextColor = 'primary' | 'secondary' | 'disabled' | 'placeholder' | 'active' | 'inherit';
119
+ export type TextColor = 'primary' | 'secondary' | 'disabled' | 'placeholder' | 'accent' | 'inherit';
120
120
  /**
121
121
  * Display mode for Text/Heading
122
122
  */
@@ -11,6 +11,18 @@
11
11
  export type ISODateString = `${number}${number}${number}${number}-${number}${number}-${number}${number}`;
12
12
  /** Day of week: 0 = Sunday through 6 = Saturday */
13
13
  export type DayOfWeek = 0 | 1 | 2 | 3 | 4 | 5 | 6;
14
+ /**
15
+ * Three-letter lowercase weekday name, 'sun' through 'sat'.
16
+ * A more self-documenting alternative to the numeric {@link DayOfWeek}.
17
+ */
18
+ export type DayOfWeekName = 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat';
19
+ /**
20
+ * Normalize a weekday to its numeric {@link DayOfWeek}. Accepts either a
21
+ * number (0–6) or a three-letter day name ('sun'–'sat', case-insensitive),
22
+ * so APIs like `weekStartsOn` can take whichever is more readable at the
23
+ * call site. Unrecognized values fall back to 0 (Sunday).
24
+ */
25
+ export declare function normalizeDayOfWeek(value: DayOfWeek | DayOfWeekName): DayOfWeek;
14
26
  /** Immutable date record with 1-based month. */
15
27
  export interface PlainDate {
16
28
  readonly year: number;
@@ -1 +1 @@
1
- {"version":3,"file":"dateTypes.d.ts","sourceRoot":"","sources":["../../src/utils/dateTypes.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH;;;GAGG;AACH,MAAM,MAAM,aAAa,GACvB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,EAAE,CAAC;AAE/E,mDAAmD;AACnD,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAElD,gDAAgD;AAChD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,0CAA0C;AAC1C,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,aAAa,CAAC;IACrB,GAAG,EAAE,aAAa,CAAC;CACpB"}
1
+ {"version":3,"file":"dateTypes.d.ts","sourceRoot":"","sources":["../../src/utils/dateTypes.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH;;;GAGG;AACH,MAAM,MAAM,aAAa,GACvB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,EAAE,CAAC;AAE/E,mDAAmD;AACnD,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAElD;;;GAGG;AACH,MAAM,MAAM,aAAa,GACrB,KAAK,GACL,KAAK,GACL,KAAK,GACL,KAAK,GACL,KAAK,GACL,KAAK,GACL,KAAK,CAAC;AAaV;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,GAAG,aAAa,GAC/B,SAAS,CAMX;AAED,gDAAgD;AAChD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,0CAA0C;AAC1C,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,aAAa,CAAC;IACrB,GAAG,EAAE,aAAa,CAAC;CACpB"}
@@ -1 +1,41 @@
1
- export {};
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file dateTypes.ts
5
+ * @input None
6
+ * @output Shared date type definitions used across Calendar, DateInput, and other components
7
+ * @position Core type definitions; imported by Calendar, DateInput, DateRangeInput, DateTimeInput, plainDate, dateParser
8
+ */
9
+
10
+ /**
11
+ * ISO 8601 date string in YYYY-MM-DD format.
12
+ * Example: "2026-01-28"
13
+ */
14
+
15
+ /** Day of week: 0 = Sunday through 6 = Saturday */
16
+
17
+ /**
18
+ * Three-letter lowercase weekday name, 'sun' through 'sat'.
19
+ * A more self-documenting alternative to the numeric {@link DayOfWeek}.
20
+ */
21
+
22
+ /** Weekday names indexed by {@link DayOfWeek} (0 = Sunday). */
23
+ const WEEKDAY_NAMES = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
24
+
25
+ /**
26
+ * Normalize a weekday to its numeric {@link DayOfWeek}. Accepts either a
27
+ * number (0–6) or a three-letter day name ('sun'–'sat', case-insensitive),
28
+ * so APIs like `weekStartsOn` can take whichever is more readable at the
29
+ * call site. Unrecognized values fall back to 0 (Sunday).
30
+ */
31
+ export function normalizeDayOfWeek(value) {
32
+ if (typeof value === 'number') {
33
+ return value;
34
+ }
35
+ const index = WEEKDAY_NAMES.indexOf(value.toLowerCase());
36
+ return index === -1 ? 0 : index;
37
+ }
38
+
39
+ /** Immutable date record with 1-based month. */
40
+
41
+ /** Date range with start and end dates */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.2-canary.2581eb4",
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",
@@ -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(
@@ -87,6 +87,13 @@ export const docs = {
87
87
  description:
88
88
  'Content rendered in the card-background area below the colored header.',
89
89
  },
90
+ {
91
+ name: 'defaultIsExpanded',
92
+ type: 'boolean',
93
+ description:
94
+ 'Whether the content area (children) starts expanded. Only relevant when children are provided.',
95
+ default: 'false',
96
+ },
90
97
  {
91
98
  name: 'xstyle',
92
99
  type: 'StyleXStyles',
@@ -150,6 +157,7 @@ export const docsZh = {
150
157
  {name: 'endContent', type: 'ReactNode', description: '渲染在头部区域末端对齐的操作内容,通常是按钮或链接。'},
151
158
  {name: 'container', type: "'card' | 'section'", description: '视觉变体:card 带圆角;section 无圆角全宽,适用于页面级场景。', default: "'card'"},
152
159
  {name: 'children', type: 'ReactNode', description: '渲染在彩色头部下方卡片背景区域的内容。'},
160
+ {name: 'defaultIsExpanded', type: 'boolean', description: '内容区域(children)是否初始展开。仅在提供 children 时相关。', default: 'false'},
153
161
  {
154
162
  name: 'xstyle',
155
163
  type: 'StyleXStyles',
@@ -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
- const {container} = render(<Banner status="info" title="Title Only" />);
81
- // Only one <p> for the title, no description
82
- const paragraphs = container.querySelectorAll('p');
83
- expect(paragraphs).toHaveLength(1);
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
  });
@@ -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
- <p {...stylex.props(styles.title)}>{title}</p>
451
+ <div {...stylex.props(styles.title)}>{title}</div>
445
452
  {description != null && (
446
- <p {...stylex.props(styles.description)}>{description}</p>
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" />);
@@ -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
- const buttonDisabled = isDisabled || groupDisabled || isLoadingState;
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
- if (buttonDisabled || actionInFlightRef.current) {
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(loadingStyles.spinnerOverlay)}
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 && loadingStyles.hiddenContent,
674
+ isLoadingState &&
675
+ (delaySpinner
676
+ ? loadingStyles.hiddenContentDelayed
677
+ : loadingStyles.hiddenContent),
610
678
  )}
611
679
  aria-hidden={isLoadingState || undefined}>
612
680
  {icon && (