@commercetools/nimbus 0.0.1

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 (298) hide show
  1. package/.storybook/apca-check/index.ts +150 -0
  2. package/.storybook/main.ts +64 -0
  3. package/.storybook/preview.tsx +92 -0
  4. package/.storybook/vitest.setup.ts +13 -0
  5. package/README.md +13 -0
  6. package/docs/architecture-decisions/adr-0001-consumer-component-apis.md +177 -0
  7. package/docs/architecture-decisions/adr-0002-compound-component-extraction.md +82 -0
  8. package/package.json +64 -0
  9. package/src/components/accordion/accordion-context.tsx +17 -0
  10. package/src/components/accordion/accordion.mdx +172 -0
  11. package/src/components/accordion/accordion.recipe.tsx +98 -0
  12. package/src/components/accordion/accordion.slots.tsx +39 -0
  13. package/src/components/accordion/accordion.stories.tsx +188 -0
  14. package/src/components/accordion/accordion.tsx +16 -0
  15. package/src/components/accordion/accordion.types.tsx +54 -0
  16. package/src/components/accordion/components/accordion-content.tsx +28 -0
  17. package/src/components/accordion/components/accordion-group.tsx +27 -0
  18. package/src/components/accordion/components/accordion-header.tsx +63 -0
  19. package/src/components/accordion/components/accordion-item.tsx +87 -0
  20. package/src/components/accordion/index.ts +2 -0
  21. package/src/components/alert/alert.mdx +92 -0
  22. package/src/components/alert/alert.recipe.tsx +65 -0
  23. package/src/components/alert/alert.slots.tsx +46 -0
  24. package/src/components/alert/alert.stories.tsx +308 -0
  25. package/src/components/alert/alert.tsx +18 -0
  26. package/src/components/alert/alert.types.tsx +70 -0
  27. package/src/components/alert/components/alert.actions.tsx +27 -0
  28. package/src/components/alert/components/alert.description.tsx +27 -0
  29. package/src/components/alert/components/alert.dismiss-button.tsx +41 -0
  30. package/src/components/alert/components/alert.root.tsx +92 -0
  31. package/src/components/alert/components/alert.title.tsx +29 -0
  32. package/src/components/alert/index.ts +2 -0
  33. package/src/components/avatar/avatar.mdx +80 -0
  34. package/src/components/avatar/avatar.recipe.tsx +36 -0
  35. package/src/components/avatar/avatar.slots.tsx +16 -0
  36. package/src/components/avatar/avatar.stories.tsx +136 -0
  37. package/src/components/avatar/avatar.tsx +34 -0
  38. package/src/components/avatar/avatar.types.ts +33 -0
  39. package/src/components/avatar/index.ts +2 -0
  40. package/src/components/badge/badge.mdx +91 -0
  41. package/src/components/badge/badge.recipe.tsx +72 -0
  42. package/src/components/badge/badge.slots.tsx +8 -0
  43. package/src/components/badge/badge.stories.tsx +124 -0
  44. package/src/components/badge/badge.tsx +35 -0
  45. package/src/components/badge/badge.types.tsx +40 -0
  46. package/src/components/badge/index.ts +2 -0
  47. package/src/components/bleed/bleed.tsx +1 -0
  48. package/src/components/bleed/index.ts +1 -0
  49. package/src/components/box/box.mdx +115 -0
  50. package/src/components/box/box.stories.tsx +71 -0
  51. package/src/components/box/box.tsx +11 -0
  52. package/src/components/box/index.ts +1 -0
  53. package/src/components/button/button.mdx +169 -0
  54. package/src/components/button/button.recipe.ts +185 -0
  55. package/src/components/button/button.slots.tsx +45 -0
  56. package/src/components/button/button.stories.tsx +369 -0
  57. package/src/components/button/button.tsx +37 -0
  58. package/src/components/button/button.types.ts +14 -0
  59. package/src/components/button/index.ts +2 -0
  60. package/src/components/card/card.mdx +92 -0
  61. package/src/components/card/card.recipe.tsx +71 -0
  62. package/src/components/card/card.slots.tsx +50 -0
  63. package/src/components/card/card.stories.tsx +175 -0
  64. package/src/components/card/card.tsx +9 -0
  65. package/src/components/card/card.types.ts +22 -0
  66. package/src/components/card/components/card.content.tsx +29 -0
  67. package/src/components/card/components/card.header.tsx +28 -0
  68. package/src/components/card/components/card.root.tsx +62 -0
  69. package/src/components/card/index.ts +2 -0
  70. package/src/components/checkbox/checkbox.mdx +78 -0
  71. package/src/components/checkbox/checkbox.recipe.tsx +116 -0
  72. package/src/components/checkbox/checkbox.slots.tsx +33 -0
  73. package/src/components/checkbox/checkbox.stories.tsx +200 -0
  74. package/src/components/checkbox/checkbox.tsx +77 -0
  75. package/src/components/checkbox/checkbox.types.tsx +22 -0
  76. package/src/components/checkbox/index.ts +2 -0
  77. package/src/components/code/code.mdx +17 -0
  78. package/src/components/code/code.recipe.ts +63 -0
  79. package/src/components/code/code.tsx +1 -0
  80. package/src/components/code/index.ts +1 -0
  81. package/src/components/dialog/dialog.mdx +20 -0
  82. package/src/components/dialog/dialog.recipe.ts +254 -0
  83. package/src/components/dialog/dialog.tsx +61 -0
  84. package/src/components/dialog/index.ts +1 -0
  85. package/src/components/em/em.mdx +17 -0
  86. package/src/components/em/em.tsx +1 -0
  87. package/src/components/em/index.ts +1 -0
  88. package/src/components/flex/flex.mdx +41 -0
  89. package/src/components/flex/flex.tsx +1 -0
  90. package/src/components/flex/index.ts +1 -0
  91. package/src/components/grid/grid.mdx +156 -0
  92. package/src/components/grid/grid.stories.tsx +151 -0
  93. package/src/components/grid/grid.tsx +29 -0
  94. package/src/components/grid/index.ts +1 -0
  95. package/src/components/heading/heading.mdx +23 -0
  96. package/src/components/heading/heading.recipe.ts +49 -0
  97. package/src/components/heading/heading.tsx +1 -0
  98. package/src/components/heading/index.ts +1 -0
  99. package/src/components/highlight/highlight.mdx +18 -0
  100. package/src/components/highlight/highlight.tsx +1 -0
  101. package/src/components/highlight/index.ts +1 -0
  102. package/src/components/icon-button/icon-button.mdx +98 -0
  103. package/src/components/icon-button/icon-button.stories.tsx +188 -0
  104. package/src/components/icon-button/icon-button.tsx +21 -0
  105. package/src/components/icon-button/icon-button.types.tsx +10 -0
  106. package/src/components/icon-button/index.ts +2 -0
  107. package/src/components/index.ts +33 -0
  108. package/src/components/input/index.ts +1 -0
  109. package/src/components/input/input.mdx +20 -0
  110. package/src/components/input/input.recipe.ts +95 -0
  111. package/src/components/input/input.tsx +1 -0
  112. package/src/components/input-group/index.ts +1 -0
  113. package/src/components/input-group/input-group.mdx +20 -0
  114. package/src/components/input-group/input-group.tsx +44 -0
  115. package/src/components/kbd/index.ts +1 -0
  116. package/src/components/kbd/kbd.mdx +18 -0
  117. package/src/components/kbd/kbd.recipe.ts +57 -0
  118. package/src/components/kbd/kbd.tsx +1 -0
  119. package/src/components/link/index.ts +2 -0
  120. package/src/components/link/link.mdx +77 -0
  121. package/src/components/link/link.recipe.ts +52 -0
  122. package/src/components/link/link.slots.tsx +29 -0
  123. package/src/components/link/link.stories.tsx +204 -0
  124. package/src/components/link/link.tsx +38 -0
  125. package/src/components/link/link.types.tsx +26 -0
  126. package/src/components/list/index.ts +1 -0
  127. package/src/components/list/list.mdx +18 -0
  128. package/src/components/list/list.recipe.ts +68 -0
  129. package/src/components/list/list.tsx +9 -0
  130. package/src/components/loading-spinner/index.ts +2 -0
  131. package/src/components/loading-spinner/loading-spinner.mdx +92 -0
  132. package/src/components/loading-spinner/loading-spinner.recipe.tsx +70 -0
  133. package/src/components/loading-spinner/loading-spinner.slots.tsx +38 -0
  134. package/src/components/loading-spinner/loading-spinner.stories.tsx +97 -0
  135. package/src/components/loading-spinner/loading-spinner.tsx +50 -0
  136. package/src/components/loading-spinner/loading-spinner.types.tsx +21 -0
  137. package/src/components/nimbus-provider/color-mode.tsx +32 -0
  138. package/src/components/nimbus-provider/index.ts +2 -0
  139. package/src/components/nimbus-provider/nimbus-provider.mdx +21 -0
  140. package/src/components/nimbus-provider/nimbus-provider.tsx +51 -0
  141. package/src/components/select/components/select.clear-button.tsx +31 -0
  142. package/src/components/select/components/select.option-group.tsx +48 -0
  143. package/src/components/select/components/select.option.tsx +21 -0
  144. package/src/components/select/components/select.options.tsx +23 -0
  145. package/src/components/select/components/select.root.tsx +81 -0
  146. package/src/components/select/index.ts +2 -0
  147. package/src/components/select/select.mdx +170 -0
  148. package/src/components/select/select.recipe.tsx +216 -0
  149. package/src/components/select/select.slots.tsx +58 -0
  150. package/src/components/select/select.stories.tsx +841 -0
  151. package/src/components/select/select.tsx +18 -0
  152. package/src/components/select/select.types.tsx +37 -0
  153. package/src/components/simple-grid/index.ts +1 -0
  154. package/src/components/simple-grid/simple-grid.mdx +133 -0
  155. package/src/components/simple-grid/simple-grid.stories.tsx +143 -0
  156. package/src/components/simple-grid/simple-grid.tsx +31 -0
  157. package/src/components/stack/index.ts +1 -0
  158. package/src/components/stack/stack.mdx +88 -0
  159. package/src/components/stack/stack.stories.tsx +82 -0
  160. package/src/components/stack/stack.tsx +15 -0
  161. package/src/components/table/index.ts +1 -0
  162. package/src/components/table/table.mdx +23 -0
  163. package/src/components/table/table.recipe.ts +170 -0
  164. package/src/components/table/table.tsx +43 -0
  165. package/src/components/text/index.ts +1 -0
  166. package/src/components/text/text.mdx +244 -0
  167. package/src/components/text/text.tsx +23 -0
  168. package/src/components/text-input/index.ts +2 -0
  169. package/src/components/text-input/text-input.mdx +118 -0
  170. package/src/components/text-input/text-input.recipe.tsx +68 -0
  171. package/src/components/text-input/text-input.slots.tsx +24 -0
  172. package/src/components/text-input/text-input.stories.tsx +282 -0
  173. package/src/components/text-input/text-input.tsx +39 -0
  174. package/src/components/text-input/text-input.types.ts +7 -0
  175. package/src/components/toggle-button-group/components/toggle-button-group.button.tsx +14 -0
  176. package/src/components/toggle-button-group/components/toggle-button-group.root.tsx +15 -0
  177. package/src/components/toggle-button-group/index.ts +2 -0
  178. package/src/components/toggle-button-group/toggle-button-group.mdx +94 -0
  179. package/src/components/toggle-button-group/toggle-button-group.recipe.tsx +64 -0
  180. package/src/components/toggle-button-group/toggle-button-group.slots.tsx +26 -0
  181. package/src/components/toggle-button-group/toggle-button-group.stories.tsx +311 -0
  182. package/src/components/toggle-button-group/toggle-button-group.tsx +12 -0
  183. package/src/components/toggle-button-group/toggle-button-group.types.tsx +62 -0
  184. package/src/components/tooltip/index.ts +4 -0
  185. package/src/components/tooltip/make-element-focusable.stories.tsx +56 -0
  186. package/src/components/tooltip/make-element-focusable.tsx +57 -0
  187. package/src/components/tooltip/tooltip-trigger.stories.tsx +157 -0
  188. package/src/components/tooltip/tooltip-trigger.tsx +15 -0
  189. package/src/components/tooltip/tooltip.mdx +48 -0
  190. package/src/components/tooltip/tooltip.recipe.ts +26 -0
  191. package/src/components/tooltip/tooltip.slots.ts +35 -0
  192. package/src/components/tooltip/tooltip.stories.tsx +139 -0
  193. package/src/components/tooltip/tooltip.tsx +31 -0
  194. package/src/components/tooltip/tooltip.types.ts +44 -0
  195. package/src/components/visually-hidden/index.ts +1 -0
  196. package/src/components/visually-hidden/visually-hidden.mdx +61 -0
  197. package/src/components/visually-hidden/visually-hidden.stories.tsx +124 -0
  198. package/src/components/visually-hidden/visually-hidden.tsx +18 -0
  199. package/src/docs/accessibility.mdx +21 -0
  200. package/src/docs/background.mdx +154 -0
  201. package/src/docs/border.mdx +226 -0
  202. package/src/docs/changelog.mdx +17 -0
  203. package/src/docs/components-layout.mdx +22 -0
  204. package/src/docs/components.mdx +17 -0
  205. package/src/docs/data-display.mdx +23 -0
  206. package/src/docs/display.mdx +55 -0
  207. package/src/docs/effects.mdx +73 -0
  208. package/src/docs/feedback.mdx +22 -0
  209. package/src/docs/filters.mdx +268 -0
  210. package/src/docs/flex-and-grid.mdx +445 -0
  211. package/src/docs/forms.mdx +22 -0
  212. package/src/docs/generated/index.mdx +16 -0
  213. package/src/docs/getting-started.mdx +17 -0
  214. package/src/docs/home.mdx +56 -0
  215. package/src/docs/hooks.mdx +16 -0
  216. package/src/docs/inputs.mdx +21 -0
  217. package/src/docs/installation.mdx +60 -0
  218. package/src/docs/interactivity.mdx +278 -0
  219. package/src/docs/known-issues.mdx +19 -0
  220. package/src/docs/layout.mdx +301 -0
  221. package/src/docs/list.mdx +82 -0
  222. package/src/docs/markdown.mdx +234 -0
  223. package/src/docs/media.mdx +22 -0
  224. package/src/docs/naivgation.mdx +22 -0
  225. package/src/docs/playground.mdx +16 -0
  226. package/src/docs/rfcs-component-structure-rfcs.mdx +17 -0
  227. package/src/docs/rfcs-component-structure.mdx +74 -0
  228. package/src/docs/rfcs-hook-structure.mdx +59 -0
  229. package/src/docs/sizing.mdx +210 -0
  230. package/src/docs/spacing.mdx +193 -0
  231. package/src/docs/style-props-typography.mdx +373 -0
  232. package/src/docs/style-props.mdx +15 -0
  233. package/src/docs/svg.mdx +58 -0
  234. package/src/docs/tables.mdx +95 -0
  235. package/src/docs/toc.mdx +39 -0
  236. package/src/docs/tokens/animations.mdx +68 -0
  237. package/src/docs/tokens/aspect-ratios.mdx +21 -0
  238. package/src/docs/tokens/blurs.mdx +20 -0
  239. package/src/docs/tokens/borders.mdx +25 -0
  240. package/src/docs/tokens/breakpoints.mdx +35 -0
  241. package/src/docs/tokens/colors.mdx +86 -0
  242. package/src/docs/tokens/cursors.mdx +21 -0
  243. package/src/docs/tokens/radii.mdx +23 -0
  244. package/src/docs/tokens/shadows.mdx +21 -0
  245. package/src/docs/tokens/sizes.mdx +54 -0
  246. package/src/docs/tokens/spacing.mdx +35 -0
  247. package/src/docs/tokens/typography.mdx +61 -0
  248. package/src/docs/tokens/z-indices.mdx +23 -0
  249. package/src/docs/tokens-other.mdx +17 -0
  250. package/src/docs/tokens.mdx +16 -0
  251. package/src/docs/transforms.mdx +150 -0
  252. package/src/docs/transitions.mdx +164 -0
  253. package/src/docs/typography.mdx +17 -0
  254. package/src/docs/utilities.mdx +17 -0
  255. package/src/hooks/index.ts +2 -0
  256. package/src/hooks/use-copy-to-clipboard/use-copy-to-clipboard.mdx +54 -0
  257. package/src/hooks/use-copy-to-clipboard/use-copy-to-clipboard.ts +1 -0
  258. package/src/hooks/use-hotkeys/use-hotkeys.mdx +48 -0
  259. package/src/hooks/use-hotkeys/use-hotkeys.stories.tsx +69 -0
  260. package/src/hooks/use-hotkeys/use-hotkeys.ts +1 -0
  261. package/src/index.ts +3 -0
  262. package/src/test/utils.tsx +20 -0
  263. package/src/theme/animation-styles.ts +52 -0
  264. package/src/theme/breakpoints.ts +32 -0
  265. package/src/theme/global-css.ts +53 -0
  266. package/src/theme/index.ts +35 -0
  267. package/src/theme/keyframes.ts +192 -0
  268. package/src/theme/layer-styles.ts +12 -0
  269. package/src/theme/recipes/index.ts +21 -0
  270. package/src/theme/semantic-tokens/colors.ts +55 -0
  271. package/src/theme/semantic-tokens/index.ts +9 -0
  272. package/src/theme/semantic-tokens/radii.ts +3 -0
  273. package/src/theme/semantic-tokens/shadows.ts +4 -0
  274. package/src/theme/slot-recipes/index.ts +15 -0
  275. package/src/theme/text-styles.ts +8 -0
  276. package/src/theme/tokens/animations.ts +4 -0
  277. package/src/theme/tokens/aspect-ratios.ts +5 -0
  278. package/src/theme/tokens/blurs.ts +5 -0
  279. package/src/theme/tokens/borders.ts +4 -0
  280. package/src/theme/tokens/colors.ts +8 -0
  281. package/src/theme/tokens/cursor.ts +4 -0
  282. package/src/theme/tokens/durations.ts +4 -0
  283. package/src/theme/tokens/easings.ts +4 -0
  284. package/src/theme/tokens/font-sizes.ts +4 -0
  285. package/src/theme/tokens/font-weights.ts +4 -0
  286. package/src/theme/tokens/fonts.ts +4 -0
  287. package/src/theme/tokens/index.ts +57 -0
  288. package/src/theme/tokens/letter-spacings.ts +24 -0
  289. package/src/theme/tokens/line-heights.ts +4 -0
  290. package/src/theme/tokens/radii.ts +4 -0
  291. package/src/theme/tokens/sizes.ts +120 -0
  292. package/src/theme/tokens/spacing.ts +4 -0
  293. package/src/theme/tokens/z-index.ts +4 -0
  294. package/src/utils/extractStyleProps.ts +26 -0
  295. package/src/utils/fixedForwardRef.ts +17 -0
  296. package/tsconfig.json +38 -0
  297. package/vite.config.ts +54 -0
  298. package/vitest.config.ts +50 -0
@@ -0,0 +1,185 @@
1
+ import { defineRecipe } from "@chakra-ui/react";
2
+
3
+ export const buttonRecipe = defineRecipe({
4
+ className: "nimbus-button",
5
+ base: {
6
+ borderRadius: "200",
7
+ display: "inline-flex",
8
+ appearance: "none",
9
+ alignItems: "center",
10
+ justifyContent: "center",
11
+ userSelect: "none",
12
+ position: "relative",
13
+ whiteSpace: "nowrap",
14
+ verticalAlign: "middle",
15
+ borderWidth: "1px",
16
+ borderColor: "transparent",
17
+ cursor: "button",
18
+ flexShrink: "0",
19
+ outline: "0",
20
+ lineHeight: "1.2",
21
+ isolation: "isolate",
22
+ fontWeight: "500",
23
+ transitionProperty: "common",
24
+ transitionDuration: "moderate",
25
+ focusVisibleRing: "outside",
26
+ _disabled: {
27
+ layerStyle: "disabled",
28
+ },
29
+ _icon: {
30
+ flexShrink: "0",
31
+ },
32
+ },
33
+ variants: {
34
+ size: {
35
+ "2xs": {
36
+ h: "600",
37
+ minW: "600",
38
+ fontSize: "300",
39
+ fontWeight: "500",
40
+ lineHeight: "400",
41
+ px: "200",
42
+ gap: "100",
43
+ _icon: {
44
+ width: "400",
45
+ height: "400",
46
+ },
47
+ },
48
+ xs: {
49
+ h: "800",
50
+ minW: "800",
51
+ fontSize: "350",
52
+ fontWeight: "500",
53
+ lineHeight: "400",
54
+ px: "300",
55
+ gap: "100",
56
+ _icon: {
57
+ width: "500",
58
+ height: "500",
59
+ },
60
+ },
61
+ /* sm: {
62
+ h: "900",
63
+ minW: "900",
64
+ px: "350",
65
+ textStyle: "sm",
66
+ gap: "200",
67
+ _icon: {
68
+ width: "400",
69
+ height: "400",
70
+ },
71
+ }, */
72
+ md: {
73
+ h: "1000",
74
+ minW: "1000",
75
+ fontSize: "400",
76
+ lineHeight: "500",
77
+ px: "400",
78
+ gap: "200",
79
+ _icon: {
80
+ width: "600",
81
+ height: "600",
82
+ },
83
+ },
84
+ /* lg: {
85
+ h: "1100",
86
+ minW: "1100",
87
+ textStyle: "md",
88
+ px: "500",
89
+ gap: "300",
90
+ _icon: {
91
+ width: "500",
92
+ height: "500",
93
+ },
94
+ }, */
95
+ /* xl: {
96
+ h: "1200",
97
+ minW: "1200",
98
+ textStyle: "md",
99
+ px: "500",
100
+ gap: "250",
101
+ _icon: {
102
+ width: "500",
103
+ height: "500",
104
+ },
105
+ }, */
106
+ /* "2xl": {
107
+ h: "1600",
108
+ minW: "1600",
109
+ textStyle: "lg",
110
+ px: "700",
111
+ gap: "300",
112
+ _icon: {
113
+ width: "600",
114
+ height: "600",
115
+ },
116
+ }, */
117
+ },
118
+ variant: {
119
+ solid: {
120
+ bg: "colorPalette.9",
121
+ color: "colorPalette.contrast",
122
+ _hover: {
123
+ bg: "colorPalette.10",
124
+ },
125
+ _expanded: {
126
+ bg: "colorPalette.10",
127
+ },
128
+ },
129
+ subtle: {
130
+ bg: "colorPalette.3",
131
+ color: "colorPalette.11",
132
+ _hover: {
133
+ bg: "colorPalette.4",
134
+ },
135
+ _expanded: {
136
+ bg: "colorPalette.4",
137
+ },
138
+ },
139
+ outline: {
140
+ borderWidth: "1px",
141
+ borderColor: "colorPalette.7",
142
+ color: "colorPalette.11",
143
+ transitionProperty: "background-color, border-color, color",
144
+ transitionDuration: "moderate",
145
+ _hover: {
146
+ bg: "colorPalette.3",
147
+ borderColor: "colorPalette.8",
148
+ },
149
+ _expanded: {
150
+ bg: "colorPalette.subtle",
151
+ },
152
+ },
153
+ ghost: {
154
+ color: "colorPalette.11",
155
+ _hover: {
156
+ bg: "colorPalette.4",
157
+ },
158
+ _expanded: {
159
+ bg: "colorPalette.4",
160
+ },
161
+ },
162
+ link: {
163
+ color: "colorPalette.11",
164
+ _hover: {
165
+ textDecoration: "underline",
166
+ },
167
+ },
168
+ },
169
+ tone: {
170
+ primary: {
171
+ colorPalette: "primary",
172
+ },
173
+ critical: {
174
+ colorPalette: "error",
175
+ },
176
+ neutral: {
177
+ colorPalette: "neutral",
178
+ },
179
+ },
180
+ },
181
+ defaultVariants: {
182
+ size: "md",
183
+ variant: "subtle",
184
+ },
185
+ });
@@ -0,0 +1,45 @@
1
+ import {
2
+ type HTMLChakraProps,
3
+ type RecipeProps,
4
+ type UnstyledProp,
5
+ createRecipeContext,
6
+ defaultSystem,
7
+ } from "@chakra-ui/react";
8
+ import { buttonRecipe } from "./button.recipe";
9
+ import shouldForwardProp from "@emotion/is-prop-valid";
10
+
11
+ /**
12
+ * Base recipe props interface that combines Chakra UI's recipe props
13
+ * with the unstyled prop option for the button element.
14
+ */
15
+ interface ButtonRecipeProps extends RecipeProps<"button">, UnstyledProp {}
16
+
17
+ /**
18
+ * Root props interface that extends Chakra's HTML props with our recipe props.
19
+ * This creates a complete set of props for the root element, combining
20
+ * HTML attributes, Chakra's styling system, and our custom recipe props.
21
+ */
22
+ export type ButtonRootProps = HTMLChakraProps<"button", ButtonRecipeProps>;
23
+
24
+ const { withContext } = createRecipeContext({
25
+ recipe: buttonRecipe,
26
+ });
27
+
28
+ /**
29
+ * Root component that provides the styling context for the Battn component.
30
+ * Uses Chakra UI's recipe context system for consistent styling across instances.
31
+ */
32
+ export const ButtonRoot = withContext<HTMLButtonElement, ButtonRootProps>(
33
+ "button",
34
+ {
35
+ defaultProps: {
36
+ type: "button",
37
+ },
38
+ /** make sure the `onPress` properties won't end up as attribute on the rendered DOM element */
39
+ shouldForwardProp(prop, variantKeys) {
40
+ const chakraSfp =
41
+ !variantKeys?.includes(prop) && !defaultSystem.isValidProperty(prop);
42
+ return shouldForwardProp(prop) && chakraSfp && !prop.includes("onPress");
43
+ },
44
+ }
45
+ );
@@ -0,0 +1,369 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Button } from "./button";
3
+ import { Box, Stack } from "@/components";
4
+ import type { ButtonProps } from "./button.types";
5
+ import { userEvent, within, expect, fn } from "@storybook/test";
6
+ import { ArrowRight as DemoIcon } from "@commercetools/nimbus-icons";
7
+ import { createRef, useState } from "react";
8
+
9
+ const meta: Meta<typeof Button> = {
10
+ title: "components/Button",
11
+ component: Button,
12
+ };
13
+
14
+ export default meta;
15
+
16
+ type Story = StoryObj<typeof Button>;
17
+
18
+ const sizes: ButtonProps["size"][] = [
19
+ //"2xl",
20
+ //"xl",
21
+ //"lg",
22
+ "md",
23
+ //"sm",
24
+ "xs",
25
+ "2xs",
26
+ ];
27
+
28
+ const variants: ButtonProps["variant"][] = [
29
+ "solid",
30
+ "subtle",
31
+ "outline",
32
+ "ghost",
33
+ "link",
34
+ ];
35
+
36
+ const tones: ButtonProps["tone"][] = [
37
+ "primary",
38
+ "neutral",
39
+ "critical",
40
+ ] as const;
41
+
42
+ export const Base: Story = {
43
+ args: {
44
+ children: "Button",
45
+ onPress: fn(),
46
+
47
+ ["data-testid"]: "test",
48
+ ["aria-label"]: "test-button",
49
+ },
50
+ play: async ({ canvasElement, args, step }) => {
51
+ const canvas = within(canvasElement);
52
+ const button = canvas.getByTestId("test");
53
+ const onPress = args.onPress;
54
+
55
+ await step("Uses a <button> element by default", async () => {
56
+ await expect(button.tagName).toBe("BUTTON");
57
+ });
58
+
59
+ await step("Forwards data- & aria-attributes", async () => {
60
+ await expect(button).toHaveAttribute("data-testid", "test");
61
+ await expect(button).toHaveAttribute("aria-label", "test-button");
62
+ });
63
+
64
+ // ATTENTION: react-aria does some complicated science,
65
+ // if there is a **KEYSTROKE** before the click (like a tab-key aiming to focus the button),
66
+ // the first click is not counted as a valid click
67
+ await step("Is clickable", async () => {
68
+ button.click();
69
+ await expect(onPress).toHaveBeenCalledTimes(1);
70
+ button.blur();
71
+ });
72
+
73
+ await step("Is focusable with <tab> key", async () => {
74
+ await userEvent.tab();
75
+ await expect(button).toHaveFocus();
76
+ });
77
+
78
+ await step("Can be triggered with enter", async () => {
79
+ await userEvent.keyboard("{enter}");
80
+ await expect(onPress).toHaveBeenCalledTimes(2);
81
+ });
82
+
83
+ await step("Can be triggered with space-bar", async () => {
84
+ await expect(button).toHaveFocus();
85
+ await userEvent.keyboard(" ");
86
+ await expect(onPress).toHaveBeenCalledTimes(3);
87
+ });
88
+ },
89
+ };
90
+
91
+ export const Disabled: Story = {
92
+ args: {
93
+ children: "Disabled Button",
94
+ isDisabled: true,
95
+ onPress: fn(),
96
+
97
+ ["data-testid"]: "test",
98
+ },
99
+ play: async ({ canvasElement, step, args }) => {
100
+ const canvas = within(canvasElement);
101
+ const button = canvas.getByTestId("test");
102
+
103
+ await step("Can not be clicked", async () => {
104
+ await userEvent.click(button);
105
+ await userEvent.click(button);
106
+ await expect(args.onPress).toHaveBeenCalledTimes(0);
107
+ });
108
+
109
+ await step("Can not be focused", async () => {
110
+ await userEvent.tab();
111
+ await expect(button).not.toHaveFocus();
112
+ });
113
+ },
114
+ };
115
+
116
+ export const AsLink: Story = {
117
+ args: {
118
+ children: "Link disguised as Button",
119
+ as: "a",
120
+ href: "/",
121
+ ["data-testid"]: "test",
122
+ },
123
+ play: async ({ canvasElement, step }) => {
124
+ const canvas = within(canvasElement);
125
+ const link = canvas.getByTestId("test");
126
+
127
+ await step("Uses an <a> element", async () => {
128
+ await expect(link.tagName).toBe("A");
129
+ });
130
+ },
131
+ };
132
+
133
+ export const WithAsChild: Story = {
134
+ args: {
135
+ children: (
136
+ <a>
137
+ <DemoIcon /> I look like a button but am using an a-tag
138
+ </a>
139
+ ),
140
+ asChild: true,
141
+
142
+ ["data-testid"]: "test",
143
+ },
144
+ play: async ({ canvasElement, step }) => {
145
+ const canvas = within(canvasElement);
146
+ const link = canvas.getByTestId("test");
147
+
148
+ await step("Uses an <a> element", async () => {
149
+ await expect(link.tagName).toBe("A");
150
+ });
151
+ },
152
+ };
153
+
154
+ export const Sizes: Story = {
155
+ args: {
156
+ children: "Demo Button",
157
+ },
158
+ render: (args) => {
159
+ return (
160
+ <Stack direction="row" gap="400" alignItems="center">
161
+ {sizes.map((size) => (
162
+ <Button key={size as string} {...args} size={size} />
163
+ ))}
164
+ </Stack>
165
+ );
166
+ },
167
+ };
168
+
169
+ export const Variants: Story = {
170
+ args: {
171
+ children: "Demo Button",
172
+ },
173
+ render: (args) => {
174
+ return (
175
+ <Stack direction="row" gap="400" alignItems="center">
176
+ {variants.map((size) => (
177
+ <Button key={size as string} {...args} variant={size} />
178
+ ))}
179
+ </Stack>
180
+ );
181
+ },
182
+ };
183
+
184
+ export const Tones: Story = {
185
+ args: {
186
+ children: "Demo Button",
187
+ },
188
+ render: (args) => {
189
+ return (
190
+ <Stack>
191
+ {tones.map((tone) => (
192
+ <Stack
193
+ key={tone as string}
194
+ direction="row"
195
+ gap="400"
196
+ alignItems="center"
197
+ >
198
+ {variants.map((variant) => (
199
+ <Button
200
+ key={variant as string}
201
+ {...args}
202
+ variant={variant}
203
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
204
+ tone={tone}
205
+ />
206
+ ))}
207
+ </Stack>
208
+ ))}
209
+ </Stack>
210
+ );
211
+ },
212
+ };
213
+
214
+ const buttonRef = createRef<HTMLButtonElement>();
215
+
216
+ export const WithRef: Story = {
217
+ args: {
218
+ children: "Demo Button",
219
+ },
220
+ render: (args) => {
221
+ return (
222
+ <Button ref={buttonRef} {...args}>
223
+ {args.children}
224
+ </Button>
225
+ );
226
+ },
227
+ play: async ({ canvasElement, step }) => {
228
+ const canvas = within(canvasElement);
229
+ const button = canvas.getByRole("button");
230
+
231
+ await step("Does accept ref's", async () => {
232
+ await expect(buttonRef.current).toBe(button);
233
+ });
234
+ },
235
+ };
236
+
237
+ const Spacer = () => <Box flexGrow="1" />;
238
+ export const ComplexIconLayouts: Story = {
239
+ args: {
240
+ children: "Demo Button",
241
+ },
242
+ render: (args) => {
243
+ const [dir, setDir] = useState<"ltr" | "rtl">("ltr");
244
+ return (
245
+ <>
246
+ <Button mb="800" onClick={() => setDir(dir === "ltr" ? "rtl" : "ltr")}>
247
+ Change direction
248
+ </Button>
249
+ <Stack
250
+ direction="column"
251
+ gap="400"
252
+ width="1/3"
253
+ style={{ direction: dir }}
254
+ >
255
+ <Button {...args}>
256
+ <DemoIcon />
257
+ {args.children}
258
+ </Button>
259
+ <Button {...args}>
260
+ Demo
261
+ <DemoIcon />
262
+ Button
263
+ </Button>
264
+ <Button {...args}>
265
+ <Spacer />
266
+ <DemoIcon />
267
+ {args.children}
268
+ </Button>
269
+ <Button {...args}>
270
+ <DemoIcon />
271
+ {args.children}
272
+ <Spacer />
273
+ </Button>
274
+ <Button {...args}>
275
+ <DemoIcon />
276
+ <Spacer />
277
+ {args.children}
278
+ </Button>
279
+ <Button {...args}>
280
+ {args.children}
281
+ <Spacer />
282
+ <DemoIcon />
283
+ </Button>
284
+ </Stack>
285
+ </>
286
+ );
287
+ },
288
+ };
289
+
290
+ export const SmokeTest: Story = {
291
+ args: {
292
+ children: "Button",
293
+ onPress: fn(),
294
+ ["data-testid"]: "test",
295
+ ["aria-label"]: "test-button",
296
+ },
297
+ render: (args) => {
298
+ return (
299
+ <Stack gap="1200">
300
+ {tones.map((tone) => (
301
+ <Stack key={tone as string} direction="column" gap="400">
302
+ {sizes.map((size) => (
303
+ <Stack direction="row" key={size as string}>
304
+ {variants.map((variant) => (
305
+ <Box key={variant as string}>
306
+ <Stack direction="column" css={{ "> *": { flex: "none" } }}>
307
+ <Box>
308
+ <Button
309
+ {...args}
310
+ variant={variant}
311
+ size={size}
312
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
313
+ tone={tone}
314
+ >
315
+ <DemoIcon />
316
+ {JSON.stringify(variant)} {args.children}
317
+ <DemoIcon />
318
+ </Button>
319
+ </Box>
320
+ <Box>
321
+ <Button
322
+ {...args}
323
+ as="a"
324
+ variant={variant}
325
+ size={size}
326
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
327
+ tone={tone}
328
+ isDisabled
329
+ >
330
+ <DemoIcon />
331
+ {JSON.stringify(variant)} {args.children}
332
+ <DemoIcon />
333
+ </Button>
334
+ </Box>
335
+ <Box>
336
+ <Button
337
+ {...args}
338
+ variant={variant}
339
+ size={size}
340
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
341
+ tone={tone}
342
+ >
343
+ <DemoIcon />
344
+ {JSON.stringify(variant)} {args.children}
345
+ </Button>
346
+ </Box>
347
+ <Box>
348
+ <Button
349
+ {...args}
350
+ variant={variant}
351
+ size={size}
352
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
353
+ tone={tone}
354
+ >
355
+ {JSON.stringify(variant)} {args.children}
356
+ <DemoIcon />
357
+ </Button>
358
+ </Box>
359
+ </Stack>
360
+ </Box>
361
+ ))}
362
+ </Stack>
363
+ ))}
364
+ </Stack>
365
+ ))}
366
+ </Stack>
367
+ );
368
+ },
369
+ };
@@ -0,0 +1,37 @@
1
+ import { forwardRef, useRef } from "react";
2
+ import { useButton, useObjectRef, mergeProps } from "react-aria";
3
+ import { mergeRefs } from "@chakra-ui/react";
4
+ import { ButtonRoot } from "./button.slots.tsx";
5
+ import type { ButtonProps } from "./button.types.ts";
6
+
7
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
8
+ (props, forwardedRef) => {
9
+ const { as, asChild, children, ...rest } = props;
10
+
11
+ // create a local ref (because the consumer may not provide a forwardedRef)
12
+ const localRef = useRef<HTMLButtonElement>(null);
13
+ // merge the local ref with a potentially forwarded ref
14
+ const ref = useObjectRef(mergeRefs(localRef, forwardedRef));
15
+
16
+ // if asChild is set, for react-aria to add the button-role, the elementType
17
+ // has to be manually set to something else than button
18
+ const elementType = as || (asChild ? "a" : "button") || "button";
19
+
20
+ const { buttonProps } = useButton(
21
+ {
22
+ ...rest,
23
+ elementType,
24
+ },
25
+ ref
26
+ );
27
+
28
+ return (
29
+ <ButtonRoot {...mergeProps(rest, buttonProps, { as, asChild, ref })}>
30
+ {children}
31
+ </ButtonRoot>
32
+ );
33
+ }
34
+ );
35
+
36
+ // Manually assign a displayName for debugging purposes
37
+ Button.displayName = "Button";
@@ -0,0 +1,14 @@
1
+ import type { ButtonRootProps } from "./button.slots.tsx";
2
+ import type { AriaButtonProps } from "react-aria";
3
+
4
+ /** combine chakra-button props with aria-button props */
5
+ type FunctionalButtonProps = ButtonRootProps &
6
+ AriaButtonProps & {
7
+ [key: `data-${string}`]: unknown;
8
+ };
9
+
10
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
11
+ export interface ButtonProps extends FunctionalButtonProps {
12
+ // TODO: evaluate if we should require setting a tone
13
+ // tone: FunctionalButtonProps["tone"];
14
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./button.tsx";
2
+ export * from "./button.types.ts";