@luxfi/ui 7.0.0 → 7.1.0

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 (60) hide show
  1. package/dist/alert.cjs +162 -285
  2. package/dist/alert.js +162 -285
  3. package/dist/badge.cjs +162 -321
  4. package/dist/badge.d.cts +3 -7
  5. package/dist/badge.d.ts +3 -7
  6. package/dist/badge.js +162 -320
  7. package/dist/button.cjs +373 -484
  8. package/dist/button.d.cts +10 -7
  9. package/dist/button.d.ts +10 -7
  10. package/dist/button.js +373 -484
  11. package/dist/close-button.cjs +37 -18
  12. package/dist/close-button.d.cts +1 -1
  13. package/dist/close-button.d.ts +1 -1
  14. package/dist/close-button.js +37 -18
  15. package/dist/collapsible.cjs +125 -272
  16. package/dist/collapsible.js +125 -272
  17. package/dist/dialog.cjs +60 -44
  18. package/dist/dialog.d.cts +7 -8
  19. package/dist/dialog.d.ts +7 -8
  20. package/dist/dialog.js +60 -43
  21. package/dist/drawer.cjs +37 -13
  22. package/dist/drawer.js +37 -13
  23. package/dist/heading.cjs +3 -8
  24. package/dist/heading.js +3 -8
  25. package/dist/icon-button.cjs +213 -337
  26. package/dist/icon-button.d.cts +4 -4
  27. package/dist/icon-button.d.ts +4 -4
  28. package/dist/icon-button.js +213 -338
  29. package/dist/image.cjs +125 -272
  30. package/dist/image.js +125 -272
  31. package/dist/index.cjs +604 -700
  32. package/dist/index.d.cts +0 -3
  33. package/dist/index.d.ts +0 -3
  34. package/dist/index.js +604 -698
  35. package/dist/link.cjs +125 -272
  36. package/dist/link.js +125 -272
  37. package/dist/popover.cjs +55 -40
  38. package/dist/popover.js +55 -39
  39. package/dist/select.cjs +125 -272
  40. package/dist/select.js +125 -272
  41. package/dist/skeleton.cjs +125 -272
  42. package/dist/skeleton.js +125 -272
  43. package/dist/table.cjs +127 -274
  44. package/dist/table.js +127 -274
  45. package/dist/tag.cjs +184 -306
  46. package/dist/tag.js +184 -305
  47. package/dist/tooltip.cjs +22 -21
  48. package/dist/tooltip.d.cts +2 -3
  49. package/dist/tooltip.d.ts +2 -3
  50. package/dist/tooltip.js +22 -20
  51. package/package.json +9 -7
  52. package/src/badge.tsx +20 -31
  53. package/src/button.tsx +304 -238
  54. package/src/close-button.tsx +48 -22
  55. package/src/dialog.tsx +37 -47
  56. package/src/heading.tsx +8 -19
  57. package/src/icon-button.tsx +129 -104
  58. package/src/popover.tsx +30 -44
  59. package/src/skeleton.tsx +96 -144
  60. package/src/tooltip.tsx +54 -47
package/src/button.tsx CHANGED
@@ -1,199 +1,212 @@
1
- import { Slot } from '@radix-ui/react-slot';
2
- import { cva } from 'class-variance-authority';
3
- import type { VariantProps } from 'class-variance-authority';
1
+ import { styled, View } from '@hanzogui/core';
4
2
  import * as React from 'react';
5
3
 
6
- import { cn } from './utils';
7
-
8
4
  import { Skeleton } from './skeleton';
9
5
 
10
6
  // ---------------------------------------------------------------------------
11
- // CVA variant definition
7
+ // Spinner (CSS-only, no react-native ActivityIndicator)
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const SpinnerFrame = styled(View, {
11
+ name: 'ButtonSpinner',
12
+ render: <span />,
13
+ width: 16,
14
+ height: 16,
15
+ borderRadius: 1000,
16
+ borderWidth: 2,
17
+ borderColor: 'currentColor' as any,
18
+ borderBottomColor: 'transparent' as any,
19
+ borderLeftColor: 'transparent' as any,
20
+ });
21
+
22
+ function Spinner({ className }: { readonly className?: string }): React.ReactElement {
23
+ return (
24
+ <SpinnerFrame
25
+ role="status"
26
+ aria-label="Loading"
27
+ className={[
28
+ 'animate-spin',
29
+ className,
30
+ ].filter(Boolean).join(' ')}
31
+ />
32
+ );
33
+ }
34
+
12
35
  // ---------------------------------------------------------------------------
13
- // All colors reference CSS custom properties from styles/tokens.css so they
14
- // automatically adapt to light/dark mode via the .dark class.
15
- //
16
- // Sizes use Tailwind spacing utilities that match the old Chakra recipe values:
17
- // 2xs → h-5 (20px) xs → h-6 (24px) sm → h-8 (32px) md → h-10 (40px)
36
+ // ButtonFrame styled base
18
37
  // ---------------------------------------------------------------------------
19
38
 
20
- const buttonVariants = cva(
21
- // base styles (shared across all variants)
22
- [
23
- 'inline-flex items-center justify-center',
24
- 'gap-0 font-semibold overflow-hidden rounded-md',
25
- 'cursor-pointer select-none whitespace-nowrap',
26
- 'transition-colors duration-150',
27
- 'disabled:opacity-[0.2] disabled:cursor-not-allowed',
28
- ],
29
- {
30
- variants: {
31
- variant: {
32
- // --- Solid ---
33
- solid: [
34
- 'bg-[var(--color-button-solid-bg)] text-[var(--color-button-solid-text)]',
35
- 'hover:bg-[var(--color-hover)]',
36
- 'data-[expanded]:bg-[var(--color-hover)]',
37
- ],
38
- // --- Solid Danger ---
39
- solid_danger: [
40
- 'bg-red-600 text-[var(--color-button-solid-text)]',
41
- 'hover:bg-red-500',
42
- 'data-[expanded]:bg-red-500',
43
- ],
44
- // --- Outline ---
45
- outline: [
46
- 'border-2 border-solid bg-transparent',
47
- 'text-[var(--color-button-outline-fg)] border-[var(--color-button-outline-fg)]',
48
- 'hover:bg-transparent hover:text-[var(--color-hover)] hover:border-[var(--color-hover)]',
49
- ],
50
- // --- Outline Danger ---
51
- outline_danger: [
52
- 'border-2 border-solid bg-transparent',
53
- 'text-red-600 border-red-600',
54
- 'hover:bg-transparent hover:text-red-500 hover:border-red-500',
55
- ],
56
- // --- Dropdown ---
57
- dropdown: [
58
- 'border-2 border-solid bg-transparent',
59
- 'text-[var(--color-button-dropdown-fg)] border-[var(--color-button-dropdown-border)]',
60
- 'hover:bg-transparent hover:text-[var(--color-hover)] hover:border-[var(--color-hover)]',
61
- 'data-[expanded]:bg-transparent data-[expanded]:text-[var(--color-hover)] data-[expanded]:border-[var(--color-hover)]',
62
- 'data-[selected]:bg-[var(--color-selected-control-bg)] data-[selected]:text-[var(--color-selected-control-text)] data-[selected]:border-transparent',
63
- 'data-[selected]:hover:bg-[var(--color-selected-control-bg)]',
64
- 'data-[selected]:hover:text-[var(--color-hover)] data-[selected]:hover:border-transparent',
65
- 'data-[selected]:data-[expanded]:text-[var(--color-hover)]',
66
- ],
67
- // --- Header ---
68
- header: [
69
- 'bg-transparent border-2 border-solid',
70
- 'text-[var(--color-button-header-fg)] border-[var(--color-button-header-border)]',
71
- 'hover:bg-transparent hover:text-[var(--color-hover)] hover:border-[var(--color-hover)]',
72
- // selected
73
- 'data-[selected]:bg-[var(--color-button-header-bg-selected)]',
74
- 'data-[selected]:text-[var(--color-button-header-fg-selected)] data-[selected]:border-transparent data-[selected]:border-0',
75
- 'data-[selected]:hover:bg-[var(--color-button-header-bg-selected)] data-[selected]:hover:text-[var(--color-hover)]',
76
- 'data-[selected]:data-[expanded]:text-[var(--color-hover)]',
77
- // selected + highlighted
78
- 'data-[selected]:data-[highlighted]:bg-[var(--color-button-header-bg-highlighted)]',
79
- 'data-[selected]:data-[highlighted]:text-[var(--color-button-header-fg-highlighted)]',
80
- 'data-[selected]:data-[highlighted]:border-transparent data-[selected]:data-[highlighted]:border-0',
81
- 'data-[selected]:data-[highlighted]:hover:bg-[var(--color-button-header-bg-highlighted)]',
82
- 'data-[selected]:data-[highlighted]:hover:text-[var(--color-hover)]',
83
- 'data-[selected]:data-[highlighted]:data-[expanded]:text-[var(--color-hover)]',
84
- ],
85
- // --- Hero ---
86
- hero: [
87
- 'bg-[var(--color-button-hero-bg)] text-[var(--color-button-hero-fg)]',
88
- 'hover:bg-[var(--color-button-hero-bg-hover)] hover:text-[var(--color-button-hero-fg-hover)]',
89
- 'data-[selected]:bg-[var(--color-button-hero-bg-selected)] data-[selected]:text-[var(--color-button-hero-fg-selected)]',
90
- 'data-[selected]:hover:bg-[var(--color-button-hero-bg-selected)] data-[selected]:hover:text-[var(--color-hover)]',
91
- 'data-[selected]:data-[expanded]:text-[var(--color-hover)]',
92
- ],
93
- // --- Segmented ---
94
- segmented: [
95
- 'bg-transparent border-2 border-solid rounded-none',
96
- 'text-[var(--color-button-segmented-fg)] border-[var(--color-selected-control-bg)]',
97
- 'hover:text-[var(--color-hover)]',
98
- // selected
99
- 'data-[selected]:bg-[var(--color-selected-control-bg)] data-[selected]:text-[var(--color-selected-control-text)]',
100
- 'data-[selected]:hover:bg-[var(--color-selected-control-bg)] data-[selected]:hover:text-[var(--color-selected-control-text)]',
101
- // border collapse: hide right border except when selected; first/last get rounded
102
- '[&:not(:last-child)]:border-r-0 data-[selected]:[&:not(:last-child)]:border-r-2',
103
- 'data-[selected]+*:border-l-0',
104
- 'first:rounded-l-md last:rounded-r-md',
105
- ],
106
- // --- Plain ---
107
- plain: [
108
- 'bg-transparent text-inherit border-none',
109
- 'hover:bg-transparent',
110
- ],
111
- // --- Subtle ---
112
- subtle: [
113
- 'bg-[var(--color-button-subtle-bg)] text-[var(--color-button-subtle-fg)]',
114
- 'hover:bg-[var(--color-button-subtle-bg)] hover:text-[var(--color-hover)]',
115
- 'disabled:bg-[var(--color-button-subtle-bg)] disabled:text-[var(--color-button-subtle-fg)]',
116
- ],
117
- // --- Link ---
118
- link: [
119
- 'bg-transparent text-[var(--color-link-primary)] border-none font-normal',
120
- 'px-0 h-auto',
121
- 'hover:bg-transparent hover:text-[var(--color-link-primary-hover)]',
122
- 'disabled:text-[var(--color-text-secondary)]',
123
- ],
124
- // --- Icon Secondary ---
125
- icon_secondary: [
126
- 'bg-transparent text-[var(--color-icon-secondary)] border-none',
127
- 'hover:text-[var(--color-hover)]',
128
- 'data-[selected]:bg-[var(--color-selected-control-bg)] data-[selected]:text-[var(--color-selected-control-text)]',
129
- 'data-[selected]:hover:bg-[var(--color-selected-control-bg)] data-[selected]:hover:text-[var(--color-hover)]',
130
- 'data-[selected]:data-[expanded]:text-[var(--color-hover)]',
131
- 'data-[expanded]:text-[var(--color-hover)]',
132
- ],
133
- // --- Icon Background ---
134
- icon_background: [
135
- 'bg-[var(--color-button-icon-background-bg)] text-[var(--color-icon-secondary)] border-none',
136
- 'hover:text-[var(--color-hover)]',
137
- 'data-[selected]:bg-[var(--color-selected-control-bg)] data-[selected]:text-[var(--color-selected-control-text)]',
138
- 'data-[selected]:hover:bg-[var(--color-selected-control-bg)] data-[selected]:hover:text-[var(--color-hover)]',
139
- 'data-[selected]:data-[expanded]:text-[var(--color-hover)]',
140
- 'data-[expanded]:text-[var(--color-hover)]',
141
- ],
142
- // --- Pagination ---
143
- pagination: [
144
- 'border-2 border-solid bg-transparent',
145
- 'text-[var(--color-button-pagination-fg)] border-[var(--color-button-pagination-border)]',
146
- 'hover:bg-transparent hover:text-[var(--color-hover)] hover:border-[var(--color-hover)]',
147
- 'data-[selected]:bg-[var(--color-selected-control-bg)] data-[selected]:text-[var(--color-selected-control-text)] data-[selected]:border-transparent',
148
- 'data-[selected]:hover:bg-[var(--color-selected-control-bg)]',
149
- 'data-[selected]:hover:text-[var(--color-selected-control-text)] data-[selected]:hover:border-transparent',
150
- ],
151
- },
39
+ const ButtonFrame = styled(View, {
40
+ name: 'LuxButton',
41
+ render: <button type="button" />,
42
+ role: 'button',
43
+ tabIndex: 0,
44
+ cursor: 'pointer',
45
+ userSelect: 'none' as any,
46
+ display: 'inline-flex',
47
+ alignItems: 'center',
48
+ justifyContent: 'center',
49
+ gap: 0,
50
+ overflow: 'hidden',
51
+ borderRadius: 6,
52
+ // Whitespace nowrap via className since hanzogui doesn't have a direct token
53
+ // Transition via className
54
+
55
+ variants: {
56
+ size: {
57
+ '2xs': { paddingHorizontal: 8, height: 20, minWidth: 20, fontSize: 12, borderRadius: 4, gap: 4 },
58
+ xs: { paddingHorizontal: 8, height: 24, minWidth: 24, fontSize: 14, borderRadius: 4, gap: 4 },
59
+ sm: { paddingHorizontal: 12, height: 32, minWidth: 32, fontSize: 14, borderRadius: 6, gap: 4 },
60
+ md: { paddingHorizontal: 12, height: 40, minWidth: 40, fontSize: 16, borderRadius: 6, gap: 8 },
61
+ },
152
62
 
153
- size: {
154
- '2xs': 'px-2 h-5 min-w-5 text-xs rounded-sm gap-1',
155
- xs: 'px-2 h-6 min-w-6 text-sm rounded-sm gap-1',
156
- sm: 'px-3 h-8 min-w-8 text-sm rounded-md gap-1',
157
- md: 'px-3 h-10 min-w-10 text-base rounded-md gap-2',
63
+ disabled: {
64
+ true: {
65
+ opacity: 0.2,
66
+ cursor: 'not-allowed',
67
+ pointerEvents: 'none',
158
68
  },
159
69
  },
70
+ } as const,
160
71
 
161
- defaultVariants: {
162
- variant: 'solid',
163
- size: 'md',
164
- },
72
+ defaultVariants: {
73
+ size: 'md',
165
74
  },
166
- );
75
+ });
167
76
 
168
77
  // ---------------------------------------------------------------------------
169
- // Spinner (replaces Chakra's built-in loading spinner)
78
+ // Variant class maps CSS custom properties referenced via Tailwind classes.
79
+ // hanzogui styled() handles layout/sizing; className handles color theming
80
+ // so the existing tokens.css contract is honored without duplication.
170
81
  // ---------------------------------------------------------------------------
171
82
 
172
- interface SpinnerProps {
173
- readonly className?: string;
174
- }
175
-
176
- function Spinner({ className }: SpinnerProps): React.ReactElement {
177
- return (
178
- <span
179
- className={ cn(
180
- 'inline-block h-4 w-4 animate-spin rounded-full',
181
- 'border-2 border-current border-b-transparent border-l-transparent',
182
- className,
183
- ) }
184
- role="status"
185
- aria-label="Loading"
186
- />
187
- );
188
- }
83
+ const VARIANT_CLASSES: Record<string, string> = {
84
+ solid: [
85
+ 'bg-[var(--color-button-solid-bg)] text-[var(--color-button-solid-text)]',
86
+ 'hover:bg-[var(--color-hover)]',
87
+ 'data-[expanded]:bg-[var(--color-hover)]',
88
+ ].join(' '),
89
+
90
+ solid_danger: [
91
+ 'bg-red-600 text-[var(--color-button-solid-text)]',
92
+ 'hover:bg-red-500',
93
+ 'data-[expanded]:bg-red-500',
94
+ ].join(' '),
95
+
96
+ outline: [
97
+ 'border-2 border-solid bg-transparent',
98
+ 'text-[var(--color-button-outline-fg)] border-[var(--color-button-outline-fg)]',
99
+ 'hover:bg-transparent hover:text-[var(--color-hover)] hover:border-[var(--color-hover)]',
100
+ ].join(' '),
101
+
102
+ outline_danger: [
103
+ 'border-2 border-solid bg-transparent',
104
+ 'text-red-600 border-red-600',
105
+ 'hover:bg-transparent hover:text-red-500 hover:border-red-500',
106
+ ].join(' '),
107
+
108
+ dropdown: [
109
+ 'border-2 border-solid bg-transparent',
110
+ 'text-[var(--color-button-dropdown-fg)] border-[var(--color-button-dropdown-border)]',
111
+ 'hover:bg-transparent hover:text-[var(--color-hover)] hover:border-[var(--color-hover)]',
112
+ 'data-[expanded]:bg-transparent data-[expanded]:text-[var(--color-hover)] data-[expanded]:border-[var(--color-hover)]',
113
+ 'data-[selected]:bg-[var(--color-selected-control-bg)] data-[selected]:text-[var(--color-selected-control-text)] data-[selected]:border-transparent',
114
+ 'data-[selected]:hover:bg-[var(--color-selected-control-bg)]',
115
+ 'data-[selected]:hover:text-[var(--color-hover)] data-[selected]:hover:border-transparent',
116
+ 'data-[selected]:data-[expanded]:text-[var(--color-hover)]',
117
+ ].join(' '),
118
+
119
+ header: [
120
+ 'bg-transparent border-2 border-solid',
121
+ 'text-[var(--color-button-header-fg)] border-[var(--color-button-header-border)]',
122
+ 'hover:bg-transparent hover:text-[var(--color-hover)] hover:border-[var(--color-hover)]',
123
+ 'data-[selected]:bg-[var(--color-button-header-bg-selected)]',
124
+ 'data-[selected]:text-[var(--color-button-header-fg-selected)] data-[selected]:border-transparent data-[selected]:border-0',
125
+ 'data-[selected]:hover:bg-[var(--color-button-header-bg-selected)] data-[selected]:hover:text-[var(--color-hover)]',
126
+ 'data-[selected]:data-[expanded]:text-[var(--color-hover)]',
127
+ 'data-[selected]:data-[highlighted]:bg-[var(--color-button-header-bg-highlighted)]',
128
+ 'data-[selected]:data-[highlighted]:text-[var(--color-button-header-fg-highlighted)]',
129
+ 'data-[selected]:data-[highlighted]:border-transparent data-[selected]:data-[highlighted]:border-0',
130
+ 'data-[selected]:data-[highlighted]:hover:bg-[var(--color-button-header-bg-highlighted)]',
131
+ 'data-[selected]:data-[highlighted]:hover:text-[var(--color-hover)]',
132
+ 'data-[selected]:data-[highlighted]:data-[expanded]:text-[var(--color-hover)]',
133
+ ].join(' '),
134
+
135
+ hero: [
136
+ 'bg-[var(--color-button-hero-bg)] text-[var(--color-button-hero-fg)]',
137
+ 'hover:bg-[var(--color-button-hero-bg-hover)] hover:text-[var(--color-button-hero-fg-hover)]',
138
+ 'data-[selected]:bg-[var(--color-button-hero-bg-selected)] data-[selected]:text-[var(--color-button-hero-fg-selected)]',
139
+ 'data-[selected]:hover:bg-[var(--color-button-hero-bg-selected)] data-[selected]:hover:text-[var(--color-hover)]',
140
+ 'data-[selected]:data-[expanded]:text-[var(--color-hover)]',
141
+ ].join(' '),
142
+
143
+ segmented: [
144
+ 'bg-transparent border-2 border-solid rounded-none',
145
+ 'text-[var(--color-button-segmented-fg)] border-[var(--color-selected-control-bg)]',
146
+ 'hover:text-[var(--color-hover)]',
147
+ 'data-[selected]:bg-[var(--color-selected-control-bg)] data-[selected]:text-[var(--color-selected-control-text)]',
148
+ 'data-[selected]:hover:bg-[var(--color-selected-control-bg)] data-[selected]:hover:text-[var(--color-selected-control-text)]',
149
+ '[&:not(:last-child)]:border-r-0 data-[selected]:[&:not(:last-child)]:border-r-2',
150
+ 'data-[selected]+*:border-l-0',
151
+ 'first:rounded-l-md last:rounded-r-md',
152
+ ].join(' '),
153
+
154
+ plain: 'bg-transparent text-inherit border-none hover:bg-transparent',
155
+
156
+ subtle: [
157
+ 'bg-[var(--color-button-subtle-bg)] text-[var(--color-button-subtle-fg)]',
158
+ 'hover:bg-[var(--color-button-subtle-bg)] hover:text-[var(--color-hover)]',
159
+ 'disabled:bg-[var(--color-button-subtle-bg)] disabled:text-[var(--color-button-subtle-fg)]',
160
+ ].join(' '),
161
+
162
+ link: [
163
+ 'bg-transparent text-[var(--color-link-primary)] border-none font-normal',
164
+ 'px-0 h-auto',
165
+ 'hover:bg-transparent hover:text-[var(--color-link-primary-hover)]',
166
+ 'disabled:text-[var(--color-text-secondary)]',
167
+ ].join(' '),
168
+
169
+ icon_secondary: [
170
+ 'bg-transparent text-[var(--color-icon-secondary)] border-none',
171
+ 'hover:text-[var(--color-hover)]',
172
+ 'data-[selected]:bg-[var(--color-selected-control-bg)] data-[selected]:text-[var(--color-selected-control-text)]',
173
+ 'data-[selected]:hover:bg-[var(--color-selected-control-bg)] data-[selected]:hover:text-[var(--color-hover)]',
174
+ 'data-[selected]:data-[expanded]:text-[var(--color-hover)]',
175
+ 'data-[expanded]:text-[var(--color-hover)]',
176
+ ].join(' '),
177
+
178
+ icon_background: [
179
+ 'bg-[var(--color-button-icon-background-bg)] text-[var(--color-icon-secondary)] border-none',
180
+ 'hover:text-[var(--color-hover)]',
181
+ 'data-[selected]:bg-[var(--color-selected-control-bg)] data-[selected]:text-[var(--color-selected-control-text)]',
182
+ 'data-[selected]:hover:bg-[var(--color-selected-control-bg)] data-[selected]:hover:text-[var(--color-hover)]',
183
+ 'data-[selected]:data-[expanded]:text-[var(--color-hover)]',
184
+ 'data-[expanded]:text-[var(--color-hover)]',
185
+ ].join(' '),
186
+
187
+ pagination: [
188
+ 'border-2 border-solid bg-transparent',
189
+ 'text-[var(--color-button-pagination-fg)] border-[var(--color-button-pagination-border)]',
190
+ 'hover:bg-transparent hover:text-[var(--color-hover)] hover:border-[var(--color-hover)]',
191
+ 'data-[selected]:bg-[var(--color-selected-control-bg)] data-[selected]:text-[var(--color-selected-control-text)] data-[selected]:border-transparent',
192
+ 'data-[selected]:hover:bg-[var(--color-selected-control-bg)]',
193
+ 'data-[selected]:hover:text-[var(--color-selected-control-text)] data-[selected]:hover:border-transparent',
194
+ ].join(' '),
195
+ };
196
+
197
+ const BASE_CLASSES = 'font-semibold whitespace-nowrap transition-colors duration-150';
198
+
199
+ type ButtonVariant = keyof typeof VARIANT_CLASSES;
200
+ type ButtonSize = '2xs' | 'xs' | 'sm' | 'md';
189
201
 
190
202
  // ---------------------------------------------------------------------------
191
- // Button
203
+ // ButtonProps
192
204
  // ---------------------------------------------------------------------------
193
205
 
194
206
  export interface ButtonProps
195
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
196
- VariantProps<typeof buttonVariants> {
207
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'size'> {
208
+ readonly variant?: ButtonVariant;
209
+ readonly size?: ButtonSize;
197
210
  readonly asChild?: boolean;
198
211
  readonly loading?: boolean;
199
212
  readonly loadingText?: React.ReactNode;
@@ -224,12 +237,65 @@ export interface ButtonProps
224
237
  readonly 'data-call-strategy'?: string;
225
238
  }
226
239
 
240
+ // ---------------------------------------------------------------------------
241
+ // Chakra shim style builder
242
+ // ---------------------------------------------------------------------------
243
+
244
+ const SP = 4;
245
+
246
+ function buildShimStyle(props: {
247
+ style?: React.CSSProperties;
248
+ mt?: number | string;
249
+ ml?: number | string;
250
+ mr?: number | string;
251
+ px?: number | string;
252
+ py?: number | string;
253
+ fontWeight?: number | string;
254
+ gap?: number | string;
255
+ flexShrink?: number;
256
+ columnGap?: number | string;
257
+ gridColumn?: number | string;
258
+ gridRow?: string;
259
+ justifySelf?: string;
260
+ alignSelf?: string;
261
+ h?: string;
262
+ w?: string;
263
+ display?: string;
264
+ borderBottomRightRadius?: number | string;
265
+ borderTopRightRadius?: number | string;
266
+ }): React.CSSProperties | undefined {
267
+ const s: React.CSSProperties = { ...props.style };
268
+ if (props.mt !== undefined) s.marginTop = typeof props.mt === 'number' ? `${props.mt * SP}px` : props.mt;
269
+ if (props.ml !== undefined) s.marginLeft = typeof props.ml === 'number' ? `${props.ml * SP}px` : props.ml;
270
+ if (props.mr !== undefined) s.marginRight = typeof props.mr === 'number' ? `${props.mr * SP}px` : props.mr;
271
+ if (props.px !== undefined) { const v = typeof props.px === 'number' ? `${props.px * SP}px` : props.px; s.paddingLeft = v; s.paddingRight = v; }
272
+ if (props.py !== undefined) { const v = typeof props.py === 'number' ? `${props.py * SP}px` : props.py; s.paddingTop = v; s.paddingBottom = v; }
273
+ if (props.fontWeight !== undefined) s.fontWeight = props.fontWeight;
274
+ if (props.gap !== undefined) s.gap = typeof props.gap === 'number' ? `${props.gap * SP}px` : props.gap;
275
+ if (props.flexShrink !== undefined) s.flexShrink = props.flexShrink;
276
+ if (props.columnGap !== undefined) s.columnGap = typeof props.columnGap === 'number' ? `${props.columnGap * SP}px` : props.columnGap;
277
+ if (props.gridColumn !== undefined) s.gridColumn = typeof props.gridColumn === 'number' ? String(props.gridColumn) : props.gridColumn;
278
+ if (props.gridRow) s.gridRow = props.gridRow;
279
+ if (props.justifySelf) s.justifySelf = props.justifySelf;
280
+ if (props.alignSelf) s.alignSelf = props.alignSelf;
281
+ if (props.h) s.height = props.h;
282
+ if (props.w) s.width = props.w;
283
+ if (props.display) s.display = props.display;
284
+ if (props.borderBottomRightRadius !== undefined) s.borderBottomRightRadius = typeof props.borderBottomRightRadius === 'number' ? `${props.borderBottomRightRadius}px` : props.borderBottomRightRadius;
285
+ if (props.borderTopRightRadius !== undefined) s.borderTopRightRadius = typeof props.borderTopRightRadius === 'number' ? `${props.borderTopRightRadius}px` : props.borderTopRightRadius;
286
+ return Object.keys(s).length > 0 ? s : undefined;
287
+ }
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // Button
291
+ // ---------------------------------------------------------------------------
292
+
227
293
  export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
228
294
  function Button(props, ref) {
229
295
  const {
230
296
  className,
231
- variant,
232
- size,
297
+ variant = 'solid',
298
+ size = 'md',
233
299
  asChild = false,
234
300
  loading = false,
235
301
  loadingText,
@@ -241,40 +307,21 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
241
307
  children,
242
308
  style: styleProp,
243
309
  // Strip Chakra style props
244
- mt: _mt, ml: _ml, mr: _mr, px: _px, py: _py,
245
- fontWeight: _fontWeight, gap: _gap, flexShrink: _flexShrink,
246
- columnGap: _columnGap, gridColumn: _gridColumn, gridRow: _gridRow,
247
- justifySelf: _justifySelf, alignSelf: _alignSelf, textStyle: _textStyle,
248
- h: _h, w: _w, display: _display,
249
- borderBottomRightRadius: _bbrr, borderTopRightRadius: _btrr,
310
+ mt, ml, mr, px, py,
311
+ fontWeight, gap, flexShrink,
312
+ columnGap, gridColumn, gridRow,
313
+ justifySelf, alignSelf, textStyle: _textStyle,
314
+ h, w, display,
315
+ borderBottomRightRadius, borderTopRightRadius,
250
316
  ...rest
251
317
  } = props;
252
318
 
253
- const SP = 4;
254
- const shimStyle: React.CSSProperties = { ...styleProp };
255
- if (_mt !== undefined) shimStyle.marginTop = typeof _mt === 'number' ? `${ _mt * SP }px` : _mt;
256
- if (_ml !== undefined) shimStyle.marginLeft = typeof _ml === 'number' ? `${ _ml * SP }px` : _ml;
257
- if (_mr !== undefined) shimStyle.marginRight = typeof _mr === 'number' ? `${ _mr * SP }px` : _mr;
258
- if (_px !== undefined) { const v = typeof _px === 'number' ? `${ _px * SP }px` : _px; shimStyle.paddingLeft = v; shimStyle.paddingRight = v; }
259
- if (_py !== undefined) { const v = typeof _py === 'number' ? `${ _py * SP }px` : _py; shimStyle.paddingTop = v; shimStyle.paddingBottom = v; }
260
- if (_fontWeight !== undefined) shimStyle.fontWeight = _fontWeight;
261
- if (_gap !== undefined) shimStyle.gap = typeof _gap === 'number' ? `${ _gap * SP }px` : _gap;
262
- if (_flexShrink !== undefined) shimStyle.flexShrink = _flexShrink;
263
- if (_columnGap !== undefined) shimStyle.columnGap = typeof _columnGap === 'number' ? `${ _columnGap * SP }px` : _columnGap;
264
- if (_gridColumn !== undefined) shimStyle.gridColumn = typeof _gridColumn === 'number' ? String(_gridColumn) : _gridColumn;
265
- if (_gridRow) shimStyle.gridRow = _gridRow;
266
- if (_justifySelf) shimStyle.justifySelf = _justifySelf;
267
- if (_alignSelf) shimStyle.alignSelf = _alignSelf;
268
- if (_h) shimStyle.height = _h;
269
- if (_w) shimStyle.width = _w;
270
- if (_display) shimStyle.display = _display;
271
- if (_bbrr !== undefined) shimStyle.borderBottomRightRadius = typeof _bbrr === 'number' ? `${ _bbrr }px` : _bbrr;
272
- if (_btrr !== undefined) shimStyle.borderTopRightRadius = typeof _btrr === 'number' ? `${ _btrr }px` : _btrr;
273
- const mergedStyle = Object.keys(shimStyle).length > 0 ? shimStyle : undefined;
274
-
275
- const Comp = asChild ? Slot : 'button';
276
-
277
- // Build data-* attributes for state-driven variant styles
319
+ const mergedStyle = buildShimStyle({
320
+ style: styleProp, mt, ml, mr, px, py, fontWeight, gap, flexShrink,
321
+ columnGap, gridColumn, gridRow, justifySelf, alignSelf, h, w, display,
322
+ borderBottomRightRadius, borderTopRightRadius,
323
+ });
324
+
278
325
  const dataAttrs: Record<string, true> = {};
279
326
  if (expanded) dataAttrs['data-expanded'] = true;
280
327
  if (selected) dataAttrs['data-selected'] = true;
@@ -285,30 +332,35 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
285
332
 
286
333
  const inner = loading ? (
287
334
  <>
288
- <Spinner/>
289
- { loadingText != null && <span className="ml-2">{ loadingText }</span> }
335
+ <Spinner />
336
+ {loadingText != null && <span style={{ marginLeft: 8 }}>{loadingText}</span>}
290
337
  </>
291
338
  ) : (
292
339
  children
293
340
  );
294
341
 
342
+ const variantClass = VARIANT_CLASSES[variant] ?? VARIANT_CLASSES.solid;
343
+ const combinedClassName = [BASE_CLASSES, variantClass, className].filter(Boolean).join(' ');
344
+
295
345
  const button = (
296
- <Comp
297
- ref={ ref }
298
- className={ cn(buttonVariants({ variant, size }), className) }
299
- style={ mergedStyle }
300
- disabled={ isDisabled || undefined }
301
- { ...dataAttrs }
302
- { ...rest }
346
+ <ButtonFrame
347
+ ref={ref as any}
348
+ size={size as any}
349
+ disabled={isDisabled || undefined}
350
+ asChild={asChild || undefined}
351
+ className={combinedClassName}
352
+ style={mergedStyle}
353
+ {...dataAttrs}
354
+ {...(rest as any)}
303
355
  >
304
- { inner }
305
- </Comp>
356
+ {inner}
357
+ </ButtonFrame>
306
358
  );
307
359
 
308
360
  if (loadingSkeleton) {
309
361
  return (
310
- <Skeleton loading={ loadingSkeleton } asChild ref={ ref as React.ForwardedRef<HTMLDivElement> }>
311
- { button }
362
+ <Skeleton loading={loadingSkeleton} asChild ref={ref as React.ForwardedRef<HTMLDivElement>}>
363
+ {button}
312
364
  </Skeleton>
313
365
  );
314
366
  }
@@ -317,6 +369,24 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
317
369
  },
318
370
  );
319
371
 
372
+ // ---------------------------------------------------------------------------
373
+ // buttonVariants — compatibility shim for consumers that call buttonVariants()
374
+ // Returns the className string for a given variant + size combo.
375
+ // ---------------------------------------------------------------------------
376
+
377
+ const SIZE_CLASSES: Record<string, string> = {
378
+ '2xs': 'px-2 h-5 min-w-5 text-xs rounded-sm gap-1',
379
+ xs: 'px-2 h-6 min-w-6 text-sm rounded-sm gap-1',
380
+ sm: 'px-3 h-8 min-w-8 text-sm rounded-md gap-1',
381
+ md: 'px-3 h-10 min-w-10 text-base rounded-md gap-2',
382
+ };
383
+
384
+ export function buttonVariants(opts?: { variant?: string; size?: string }): string {
385
+ const v = opts?.variant ?? 'solid';
386
+ const s = opts?.size ?? 'md';
387
+ return [BASE_CLASSES, VARIANT_CLASSES[v] ?? '', SIZE_CLASSES[s] ?? ''].filter(Boolean).join(' ');
388
+ }
389
+
320
390
  // ---------------------------------------------------------------------------
321
391
  // ButtonGroup
322
392
  // ---------------------------------------------------------------------------
@@ -326,13 +396,12 @@ export interface ButtonGroupProps extends React.HTMLAttributes<HTMLDivElement> {
326
396
  export const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
327
397
  function ButtonGroup(props, ref) {
328
398
  const { className, ...rest } = props;
329
-
330
399
  return (
331
400
  <div
332
- ref={ ref }
333
- className={ cn('inline-flex', className) }
401
+ ref={ref}
402
+ className={['inline-flex', className].filter(Boolean).join(' ')}
334
403
  role="group"
335
- { ...rest }
404
+ {...rest}
336
405
  />
337
406
  );
338
407
  },
@@ -367,15 +436,15 @@ export const ButtonGroupRadio = React.forwardRef<HTMLDivElement, ButtonGroupRadi
367
436
  const firstChildValue = React.useMemo(() => {
368
437
  const firstChild = Array.isArray(children) ? children[0] : undefined;
369
438
  return typeof firstChild?.props.value === 'string' ? firstChild.props.value : undefined;
370
- }, [ children ]);
439
+ }, [children]);
371
440
 
372
- const [ value, setValue ] = React.useState<string | undefined>(defaultValue ?? firstChildValue);
441
+ const [value, setValue] = React.useState<string | undefined>(defaultValue ?? firstChildValue);
373
442
 
374
443
  const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
375
444
  const v = event.currentTarget.value;
376
445
  setValue(v);
377
446
  onChange?.(v);
378
- }, [ onChange ]);
447
+ }, [onChange]);
379
448
 
380
449
  const clonedChildren = React.Children.map(children, (child: React.ReactElement<ButtonProps>) => {
381
450
  return React.cloneElement(child, {
@@ -388,24 +457,21 @@ export const ButtonGroupRadio = React.forwardRef<HTMLDivElement, ButtonGroupRadi
388
457
  const childrenLength = React.Children.count(children);
389
458
 
390
459
  return (
391
- <Skeleton loading={ loading }>
460
+ <Skeleton loading={loading}>
392
461
  <div
393
- ref={ ref }
394
- className={ cn(
462
+ ref={ref}
463
+ className={[
395
464
  'inline-flex gap-0',
396
- equalWidth && 'grid',
465
+ equalWidth ? 'grid' : '',
397
466
  className,
398
- ) }
399
- style={ equalWidth ? { gridTemplateColumns: `repeat(${ childrenLength }, 1fr)` } : undefined }
467
+ ].filter(Boolean).join(' ')}
468
+ style={equalWidth ? { gridTemplateColumns: `repeat(${childrenLength}, 1fr)` } : undefined}
400
469
  role="group"
401
- { ...rest }
470
+ {...rest}
402
471
  >
403
- { clonedChildren }
472
+ {clonedChildren}
404
473
  </div>
405
474
  </Skeleton>
406
475
  );
407
476
  },
408
477
  );
409
-
410
- // Re-export the variant function for consumers that need it (e.g., IconButton)
411
- export { buttonVariants };