@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.
- package/dist/alert.cjs +162 -285
- package/dist/alert.js +162 -285
- package/dist/badge.cjs +162 -321
- package/dist/badge.d.cts +3 -7
- package/dist/badge.d.ts +3 -7
- package/dist/badge.js +162 -320
- package/dist/button.cjs +373 -484
- package/dist/button.d.cts +10 -7
- package/dist/button.d.ts +10 -7
- package/dist/button.js +373 -484
- package/dist/close-button.cjs +37 -18
- package/dist/close-button.d.cts +1 -1
- package/dist/close-button.d.ts +1 -1
- package/dist/close-button.js +37 -18
- package/dist/collapsible.cjs +125 -272
- package/dist/collapsible.js +125 -272
- package/dist/dialog.cjs +60 -44
- package/dist/dialog.d.cts +7 -8
- package/dist/dialog.d.ts +7 -8
- package/dist/dialog.js +60 -43
- package/dist/drawer.cjs +37 -13
- package/dist/drawer.js +37 -13
- package/dist/heading.cjs +3 -8
- package/dist/heading.js +3 -8
- package/dist/icon-button.cjs +213 -337
- package/dist/icon-button.d.cts +4 -4
- package/dist/icon-button.d.ts +4 -4
- package/dist/icon-button.js +213 -338
- package/dist/image.cjs +125 -272
- package/dist/image.js +125 -272
- package/dist/index.cjs +604 -700
- package/dist/index.d.cts +0 -3
- package/dist/index.d.ts +0 -3
- package/dist/index.js +604 -698
- package/dist/link.cjs +125 -272
- package/dist/link.js +125 -272
- package/dist/popover.cjs +55 -40
- package/dist/popover.js +55 -39
- package/dist/select.cjs +125 -272
- package/dist/select.js +125 -272
- package/dist/skeleton.cjs +125 -272
- package/dist/skeleton.js +125 -272
- package/dist/table.cjs +127 -274
- package/dist/table.js +127 -274
- package/dist/tag.cjs +184 -306
- package/dist/tag.js +184 -305
- package/dist/tooltip.cjs +22 -21
- package/dist/tooltip.d.cts +2 -3
- package/dist/tooltip.d.ts +2 -3
- package/dist/tooltip.js +22 -20
- package/package.json +9 -7
- package/src/badge.tsx +20 -31
- package/src/button.tsx +304 -238
- package/src/close-button.tsx +48 -22
- package/src/dialog.tsx +37 -47
- package/src/heading.tsx +8 -19
- package/src/icon-button.tsx +129 -104
- package/src/popover.tsx +30 -44
- package/src/skeleton.tsx +96 -144
- package/src/tooltip.tsx +54 -47
package/src/close-button.tsx
CHANGED
|
@@ -1,11 +1,48 @@
|
|
|
1
|
+
import { styled, View } from '@hanzogui/core';
|
|
1
2
|
import * as React from 'react';
|
|
2
3
|
|
|
3
|
-
import { cn } from './utils';
|
|
4
|
-
|
|
5
4
|
// eslint-disable-next-line max-len
|
|
6
5
|
const CLOSE_ICON_PATH = 'M9.44 8.035a.791.791 0 0 0 1.12 0l3.802-3.803a.791.791 0 0 1 1.119 0l.287.287a.79.79 0 0 1 0 1.119L11.965 9.44a.79.79 0 0 0 0 1.118l3.803 3.803a.791.791 0 0 1 0 1.119l-.287.287a.791.791 0 0 1-1.119 0l-3.803-3.803a.79.79 0 0 0-1.118 0l-3.803 3.803a.79.79 0 0 1-1.119 0l-.287-.287a.791.791 0 0 1 0-1.119l3.803-3.803a.791.791 0 0 0 0-1.118L4.232 5.638a.791.791 0 0 1 0-1.119l.287-.287a.791.791 0 0 1 1.119 0L9.44 8.035Z';
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
const CloseButtonFrame = styled(View, {
|
|
8
|
+
name: 'LuxCloseButton',
|
|
9
|
+
render: <button type="button" aria-label="Close" />,
|
|
10
|
+
role: 'button',
|
|
11
|
+
cursor: 'pointer',
|
|
12
|
+
display: 'inline-flex',
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
justifyContent: 'center',
|
|
15
|
+
width: 20,
|
|
16
|
+
height: 20,
|
|
17
|
+
minWidth: 0,
|
|
18
|
+
flexShrink: 1,
|
|
19
|
+
padding: 0,
|
|
20
|
+
borderRadius: 4,
|
|
21
|
+
borderWidth: 0,
|
|
22
|
+
overflow: 'hidden',
|
|
23
|
+
backgroundColor: 'transparent',
|
|
24
|
+
|
|
25
|
+
hoverStyle: {
|
|
26
|
+
backgroundColor: 'transparent',
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
variants: {
|
|
30
|
+
disabled: {
|
|
31
|
+
true: {
|
|
32
|
+
opacity: 0.4,
|
|
33
|
+
pointerEvents: 'none',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
} as const,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const CLOSE_BUTTON_CLASSES = [
|
|
40
|
+
'text-[var(--closeButton-fg,currentColor)]',
|
|
41
|
+
'hover:text-[var(--hover-color,currentColor)]',
|
|
42
|
+
'disabled:opacity-40',
|
|
43
|
+
].join(' ');
|
|
44
|
+
|
|
45
|
+
export interface CloseButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'size'> {
|
|
9
46
|
readonly variant?: 'plain';
|
|
10
47
|
readonly size?: 'md';
|
|
11
48
|
}
|
|
@@ -17,23 +54,12 @@ export const CloseButton = React.forwardRef<
|
|
|
17
54
|
const { variant: _variant, size: _size, className, children, ...rest } = props;
|
|
18
55
|
|
|
19
56
|
return (
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
className={ cn(
|
|
25
|
-
'inline-flex items-center justify-center',
|
|
26
|
-
'size-5 min-w-0 shrink-0 p-0',
|
|
27
|
-
'rounded-sm border-0 overflow-hidden',
|
|
28
|
-
'bg-transparent text-[var(--closeButton-fg,currentColor)]',
|
|
29
|
-
'hover:bg-transparent hover:text-[var(--hover-color,currentColor)]',
|
|
30
|
-
'disabled:opacity-40',
|
|
31
|
-
'cursor-pointer',
|
|
32
|
-
className,
|
|
33
|
-
) }
|
|
34
|
-
{ ...rest }
|
|
57
|
+
<CloseButtonFrame
|
|
58
|
+
ref={ref as any}
|
|
59
|
+
className={[CLOSE_BUTTON_CLASSES, className].filter(Boolean).join(' ')}
|
|
60
|
+
{...(rest as any)}
|
|
35
61
|
>
|
|
36
|
-
{
|
|
62
|
+
{children ?? (
|
|
37
63
|
<svg
|
|
38
64
|
className="size-5"
|
|
39
65
|
viewBox="0 0 20 20"
|
|
@@ -41,11 +67,11 @@ export const CloseButton = React.forwardRef<
|
|
|
41
67
|
aria-hidden="true"
|
|
42
68
|
>
|
|
43
69
|
<path
|
|
44
|
-
d={
|
|
70
|
+
d={CLOSE_ICON_PATH}
|
|
45
71
|
fill="currentColor"
|
|
46
72
|
/>
|
|
47
73
|
</svg>
|
|
48
|
-
)
|
|
49
|
-
</
|
|
74
|
+
)}
|
|
75
|
+
</CloseButtonFrame>
|
|
50
76
|
);
|
|
51
77
|
});
|
package/src/dialog.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Dialog as GuiDialog } from '@hanzogui/dialog';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
|
|
4
4
|
import { cn } from './utils';
|
|
@@ -35,14 +35,14 @@ type DialogSize = 'sm' | 'md' | 'full' | 'cover';
|
|
|
35
35
|
*/
|
|
36
36
|
type ResponsiveSize = DialogSize | { base?: DialogSize; lgDown?: DialogSize; lg?: DialogSize };
|
|
37
37
|
|
|
38
|
-
interface
|
|
38
|
+
interface DialogSizeContextValue {
|
|
39
39
|
size: ResponsiveSize;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
const
|
|
42
|
+
const DialogSizeContext = React.createContext<DialogSizeContextValue>({ size: 'md' });
|
|
43
43
|
|
|
44
|
-
function
|
|
45
|
-
return React.useContext(
|
|
44
|
+
function useDialogSizeContext(): DialogSizeContextValue {
|
|
45
|
+
return React.useContext(DialogSizeContext);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// ---------------------------------------------------------------------------
|
|
@@ -61,7 +61,7 @@ const CONTENT_SIZE_MAP: Record<DialogSize, string> = {
|
|
|
61
61
|
*
|
|
62
62
|
* For a plain value we return the matching static class. For a breakpoint
|
|
63
63
|
* object we compose mobile-first responsive classes. The Tailwind config in
|
|
64
|
-
* this project defines `lg: 1000px` as breakpoint
|
|
64
|
+
* this project defines `lg: 1000px` as breakpoint -- that aligns with Chakra's
|
|
65
65
|
* `lgDown` / `lg` pattern.
|
|
66
66
|
*/
|
|
67
67
|
function sizeClasses(size: ResponsiveSize): string {
|
|
@@ -100,7 +100,7 @@ export interface DialogRootProps {
|
|
|
100
100
|
onOpenChange?: (details: { open: boolean }) => void;
|
|
101
101
|
size?: ResponsiveSize;
|
|
102
102
|
|
|
103
|
-
/** Accepted for API compat but not implemented
|
|
103
|
+
/** Accepted for API compat but not implemented -- animations are CSS-only. */
|
|
104
104
|
motionPreset?: string;
|
|
105
105
|
modal?: boolean;
|
|
106
106
|
}
|
|
@@ -112,7 +112,7 @@ export const DialogRoot: React.FC<DialogRootProps> = ({
|
|
|
112
112
|
onOpenChange,
|
|
113
113
|
size = 'md',
|
|
114
114
|
modal = true,
|
|
115
|
-
// motionPreset is intentionally unused
|
|
115
|
+
// motionPreset is intentionally unused -- kept for API compat
|
|
116
116
|
}) => {
|
|
117
117
|
const handleOpenChange = React.useCallback(
|
|
118
118
|
(nextOpen: boolean) => {
|
|
@@ -121,19 +121,19 @@ export const DialogRoot: React.FC<DialogRootProps> = ({
|
|
|
121
121
|
[ onOpenChange ],
|
|
122
122
|
);
|
|
123
123
|
|
|
124
|
-
const ctx = React.useMemo<
|
|
124
|
+
const ctx = React.useMemo<DialogSizeContextValue>(() => ({ size }), [ size ]);
|
|
125
125
|
|
|
126
126
|
return (
|
|
127
|
-
<
|
|
128
|
-
<
|
|
127
|
+
<DialogSizeContext.Provider value={ ctx }>
|
|
128
|
+
<GuiDialog
|
|
129
129
|
open={ open }
|
|
130
130
|
defaultOpen={ defaultOpen }
|
|
131
131
|
onOpenChange={ handleOpenChange }
|
|
132
132
|
modal={ modal }
|
|
133
133
|
>
|
|
134
134
|
{ children }
|
|
135
|
-
</
|
|
136
|
-
</
|
|
135
|
+
</GuiDialog>
|
|
136
|
+
</DialogSizeContext.Provider>
|
|
137
137
|
);
|
|
138
138
|
};
|
|
139
139
|
|
|
@@ -155,8 +155,8 @@ export const DialogContent = React.forwardRef<
|
|
|
155
155
|
>(function DialogContent(props, ref) {
|
|
156
156
|
const {
|
|
157
157
|
children,
|
|
158
|
-
portalled = true,
|
|
159
|
-
portalRef,
|
|
158
|
+
portalled: _portalled = true,
|
|
159
|
+
portalRef: _portalRef,
|
|
160
160
|
backdrop = true,
|
|
161
161
|
className,
|
|
162
162
|
paddingTop: _paddingTop,
|
|
@@ -169,29 +169,17 @@ export const DialogContent = React.forwardRef<
|
|
|
169
169
|
...(_paddingTop !== undefined ? { paddingTop: typeof _paddingTop === 'number' ? `${ _paddingTop * 4 }px` : _paddingTop } : {}),
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
-
const { size } =
|
|
173
|
-
|
|
174
|
-
let portalProps: RadixDialog.DialogPortalProps;
|
|
175
|
-
if (!portalled) {
|
|
176
|
-
portalProps = { container: undefined };
|
|
177
|
-
} else if (portalRef) {
|
|
178
|
-
portalProps = { container: portalRef.current };
|
|
179
|
-
} else {
|
|
180
|
-
portalProps = {};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// When portalled=false we skip the Radix Portal wrapper entirely
|
|
184
|
-
const Wrapper = portalled ? RadixDialog.Portal : React.Fragment;
|
|
185
|
-
const wrapperProps = portalled ? portalProps : {};
|
|
172
|
+
const { size } = useDialogSizeContext();
|
|
186
173
|
|
|
187
174
|
return (
|
|
188
|
-
<
|
|
175
|
+
<GuiDialog.Portal>
|
|
189
176
|
{ backdrop && (
|
|
190
|
-
<
|
|
177
|
+
<GuiDialog.Overlay
|
|
178
|
+
unstyled
|
|
191
179
|
className="fixed inset-0 z-[1400] bg-black/80"
|
|
192
180
|
/>
|
|
193
181
|
) }
|
|
194
|
-
{ /* Positioner
|
|
182
|
+
{ /* Positioner -- centers the content panel */ }
|
|
195
183
|
<div
|
|
196
184
|
className={ cn(
|
|
197
185
|
'fixed inset-0 z-[1400] flex w-screen h-dvh',
|
|
@@ -199,8 +187,9 @@ export const DialogContent = React.forwardRef<
|
|
|
199
187
|
'overflow-hidden',
|
|
200
188
|
) }
|
|
201
189
|
>
|
|
202
|
-
<
|
|
190
|
+
<GuiDialog.Content
|
|
203
191
|
ref={ ref }
|
|
192
|
+
unstyled
|
|
204
193
|
className={ cn(
|
|
205
194
|
// Base content styles
|
|
206
195
|
'relative flex flex-col w-full p-6 outline-none text-base',
|
|
@@ -213,12 +202,12 @@ export const DialogContent = React.forwardRef<
|
|
|
213
202
|
className,
|
|
214
203
|
) }
|
|
215
204
|
style={ Object.keys(contentInlineStyle).length > 0 ? contentInlineStyle : undefined }
|
|
216
|
-
{ ...rest }
|
|
205
|
+
{ ...rest as any }
|
|
217
206
|
>
|
|
218
207
|
{ children }
|
|
219
|
-
</
|
|
208
|
+
</GuiDialog.Content>
|
|
220
209
|
</div>
|
|
221
|
-
</
|
|
210
|
+
</GuiDialog.Portal>
|
|
222
211
|
);
|
|
223
212
|
});
|
|
224
213
|
|
|
@@ -235,11 +224,11 @@ export const DialogCloseTrigger = React.forwardRef<
|
|
|
235
224
|
const { className, ...rest } = props;
|
|
236
225
|
|
|
237
226
|
return (
|
|
238
|
-
<
|
|
227
|
+
<GuiDialog.Close asChild>
|
|
239
228
|
<CloseButton ref={ ref } className={ className } { ...rest }>
|
|
240
229
|
{ props.children }
|
|
241
230
|
</CloseButton>
|
|
242
|
-
</
|
|
231
|
+
</GuiDialog.Close>
|
|
243
232
|
);
|
|
244
233
|
});
|
|
245
234
|
|
|
@@ -271,14 +260,15 @@ export const DialogHeader = React.forwardRef<
|
|
|
271
260
|
{ ...rest }
|
|
272
261
|
>
|
|
273
262
|
{ startElement }
|
|
274
|
-
<
|
|
263
|
+
<GuiDialog.Title
|
|
264
|
+
unstyled
|
|
275
265
|
className={ cn(
|
|
276
266
|
'text-base lg:text-lg font-medium',
|
|
277
267
|
'whitespace-nowrap overflow-hidden text-ellipsis',
|
|
278
268
|
) }
|
|
279
269
|
>
|
|
280
270
|
{ children }
|
|
281
|
-
</
|
|
271
|
+
</GuiDialog.Title>
|
|
282
272
|
<DialogCloseTrigger className="ml-auto"/>
|
|
283
273
|
</div>
|
|
284
274
|
);
|
|
@@ -335,22 +325,22 @@ export const DialogFooter = React.forwardRef<
|
|
|
335
325
|
});
|
|
336
326
|
|
|
337
327
|
// ---------------------------------------------------------------------------
|
|
338
|
-
// Simple wrappers that re-export
|
|
328
|
+
// Simple wrappers that re-export @hanzogui primitives under the old names
|
|
339
329
|
// ---------------------------------------------------------------------------
|
|
340
330
|
|
|
341
|
-
export const DialogBackdrop =
|
|
331
|
+
export const DialogBackdrop = GuiDialog.Overlay;
|
|
342
332
|
|
|
343
|
-
export const DialogTitle =
|
|
333
|
+
export const DialogTitle = GuiDialog.Title;
|
|
344
334
|
|
|
345
|
-
export const DialogDescription =
|
|
335
|
+
export const DialogDescription = GuiDialog.Description;
|
|
346
336
|
|
|
347
|
-
export interface DialogTriggerProps extends React.ComponentPropsWithoutRef<
|
|
337
|
+
export interface DialogTriggerProps extends React.ComponentPropsWithoutRef<'button'> {}
|
|
348
338
|
|
|
349
|
-
export const DialogTrigger =
|
|
339
|
+
export const DialogTrigger = GuiDialog.Trigger;
|
|
350
340
|
|
|
351
341
|
/**
|
|
352
342
|
* `DialogActionTrigger` renders its child and closes the dialog on click.
|
|
353
343
|
* This mirrors the Chakra `Dialog.ActionTrigger` behavior which wraps
|
|
354
344
|
* children in a close action.
|
|
355
345
|
*/
|
|
356
|
-
export const DialogActionTrigger =
|
|
346
|
+
export const DialogActionTrigger = GuiDialog.Close;
|
package/src/heading.tsx
CHANGED
|
@@ -9,25 +9,13 @@ export interface HeadingProps extends React.ComponentPropsWithoutRef<'h1'> {
|
|
|
9
9
|
size?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const LEVEL_TAG = {
|
|
13
|
-
'1': 'h1',
|
|
14
|
-
'2': 'h2',
|
|
15
|
-
'3': 'h3',
|
|
16
|
-
} as const;
|
|
12
|
+
const LEVEL_TAG = { '1': 'h1', '2': 'h2', '3': 'h3' } as const;
|
|
17
13
|
|
|
18
14
|
// base (mobile) + lg (desktop) responsive text styles per level
|
|
19
15
|
//
|
|
20
|
-
// Level 1:
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// Level 2: heading.sm -> heading.lg
|
|
25
|
-
// base: 16px/24px font-500
|
|
26
|
-
// lg: 24px/32px font-500
|
|
27
|
-
//
|
|
28
|
-
// Level 3: heading.xs -> heading.md
|
|
29
|
-
// base: 14px/20px font-600
|
|
30
|
-
// lg: 18px/24px font-500
|
|
16
|
+
// Level 1: 18px/24px font-500 -> 32px/40px font-500 tracking=-0.5px
|
|
17
|
+
// Level 2: 16px/24px font-500 -> 24px/32px font-500
|
|
18
|
+
// Level 3: 14px/20px font-600 -> 18px/24px font-500
|
|
31
19
|
const LEVEL_CLASSES = {
|
|
32
20
|
'1': 'text-[18px] leading-[24px] font-medium lg:text-[32px] lg:leading-[40px] lg:tracking-[-0.5px]',
|
|
33
21
|
'2': 'text-[16px] leading-[24px] font-medium lg:text-[24px] lg:leading-[32px]',
|
|
@@ -40,14 +28,15 @@ export const Heading = React.forwardRef<HTMLHeadingElement, HeadingProps>(
|
|
|
40
28
|
function Heading({ level, className, mb, size: _size, style: styleProp, ...rest }, ref) {
|
|
41
29
|
const Tag = level ? LEVEL_TAG[level] : 'h2';
|
|
42
30
|
const levelClasses = level ? LEVEL_CLASSES[level] : undefined;
|
|
43
|
-
const
|
|
44
|
-
|
|
31
|
+
const mergedStyle = mb !== undefined
|
|
32
|
+
? { ...styleProp, marginBottom: typeof mb === 'number' ? `${ mb * 4 }px` : mb }
|
|
33
|
+
: styleProp;
|
|
45
34
|
|
|
46
35
|
return (
|
|
47
36
|
<Tag
|
|
48
37
|
ref={ ref }
|
|
49
38
|
className={ cn(BASE_CLASSES, levelClasses, className) }
|
|
50
|
-
style={
|
|
39
|
+
style={ mergedStyle }
|
|
51
40
|
{ ...rest }
|
|
52
41
|
/>
|
|
53
42
|
);
|
package/src/icon-button.tsx
CHANGED
|
@@ -1,105 +1,120 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import React from 'react';
|
|
3
|
-
|
|
4
|
-
import { cn } from './utils';
|
|
1
|
+
import { styled, View } from '@hanzogui/core';
|
|
2
|
+
import * as React from 'react';
|
|
5
3
|
|
|
6
4
|
import { Skeleton } from './skeleton';
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
icon_background: [
|
|
32
|
-
'bg-[var(--color-button-icon-background-bg)] text-icon-secondary border-none',
|
|
33
|
-
'hover:text-hover',
|
|
34
|
-
'data-[selected]:bg-selected-control-bg data-[selected]:text-selected-control-text',
|
|
35
|
-
'data-[selected]:hover:text-hover',
|
|
36
|
-
'data-[expanded]:text-hover',
|
|
37
|
-
],
|
|
38
|
-
link: [
|
|
39
|
-
'bg-transparent text-link-primary border-none font-normal px-0 h-auto',
|
|
40
|
-
'hover:bg-transparent hover:text-link-primary-hover',
|
|
41
|
-
'disabled:text-text-secondary',
|
|
42
|
-
],
|
|
43
|
-
dropdown: [
|
|
44
|
-
'border-2 border-solid bg-transparent',
|
|
45
|
-
'text-[var(--color-button-dropdown-fg)] border-[var(--color-button-dropdown-border)]',
|
|
46
|
-
'hover:bg-transparent hover:text-hover hover:border-hover',
|
|
47
|
-
'data-[expanded]:bg-transparent data-[expanded]:text-hover data-[expanded]:border-hover',
|
|
48
|
-
'data-[selected]:bg-selected-control-bg data-[selected]:text-selected-control-text data-[selected]:border-transparent',
|
|
49
|
-
'data-[selected]:hover:text-hover',
|
|
50
|
-
],
|
|
51
|
-
pagination: [
|
|
52
|
-
'border-2 border-solid bg-transparent',
|
|
53
|
-
'text-[var(--color-button-pagination-fg)] border-[var(--color-button-pagination-border)]',
|
|
54
|
-
'hover:bg-transparent hover:text-hover hover:border-hover',
|
|
55
|
-
'data-[selected]:bg-selected-control-bg data-[selected]:text-selected-control-text data-[selected]:border-transparent',
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
|
-
size: {
|
|
59
|
-
'2xs': 'size-5 rounded-sm [&_svg]:size-5',
|
|
60
|
-
'2xs_alt': 'size-5 rounded-sm [&_svg]:size-3',
|
|
61
|
-
md: 'size-8 rounded-[var(--radius-base,8px)] [&_svg]:size-5',
|
|
62
|
-
},
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// IconButtonFrame — styled base
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
const IconButtonFrame = styled(View, {
|
|
11
|
+
name: 'LuxIconButton',
|
|
12
|
+
render: <button type="button" />,
|
|
13
|
+
role: 'button',
|
|
14
|
+
cursor: 'pointer',
|
|
15
|
+
display: 'inline-flex',
|
|
16
|
+
alignItems: 'center',
|
|
17
|
+
justifyContent: 'center',
|
|
18
|
+
padding: 0,
|
|
19
|
+
minWidth: 'auto' as any,
|
|
20
|
+
flexShrink: 1,
|
|
21
|
+
backgroundColor: 'transparent',
|
|
22
|
+
borderWidth: 0,
|
|
23
|
+
|
|
24
|
+
variants: {
|
|
25
|
+
size: {
|
|
26
|
+
'2xs': { width: 20, height: 20, borderRadius: 4 },
|
|
27
|
+
'2xs_alt': { width: 20, height: 20, borderRadius: 4 },
|
|
28
|
+
md: { width: 32, height: 32, borderRadius: 'var(--radius-base, 8px)' as any },
|
|
63
29
|
},
|
|
64
|
-
defaultVariants: {
|
|
65
|
-
variant: 'plain',
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
);
|
|
69
30
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
31
|
+
disabled: {
|
|
32
|
+
true: {
|
|
33
|
+
opacity: 0.4,
|
|
34
|
+
cursor: 'not-allowed',
|
|
35
|
+
pointerEvents: 'none',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
} as const,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Variant class maps (CSS custom properties via Tailwind)
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const VARIANT_CLASSES: Record<string, string> = {
|
|
46
|
+
plain: 'bg-transparent text-inherit border-none hover:bg-transparent',
|
|
47
|
+
|
|
48
|
+
icon_secondary: [
|
|
49
|
+
'bg-transparent text-icon-secondary border-none',
|
|
50
|
+
'hover:text-hover',
|
|
51
|
+
'data-[selected]:bg-selected-control-bg data-[selected]:text-selected-control-text',
|
|
52
|
+
'data-[selected]:hover:text-hover',
|
|
53
|
+
'data-[expanded]:text-hover',
|
|
54
|
+
].join(' '),
|
|
55
|
+
|
|
56
|
+
icon_background: [
|
|
57
|
+
'bg-[var(--color-button-icon-background-bg)] text-icon-secondary border-none',
|
|
58
|
+
'hover:text-hover',
|
|
59
|
+
'data-[selected]:bg-selected-control-bg data-[selected]:text-selected-control-text',
|
|
60
|
+
'data-[selected]:hover:text-hover',
|
|
61
|
+
'data-[expanded]:text-hover',
|
|
62
|
+
].join(' '),
|
|
63
|
+
|
|
64
|
+
link: [
|
|
65
|
+
'bg-transparent text-link-primary border-none font-normal px-0 h-auto',
|
|
66
|
+
'hover:bg-transparent hover:text-link-primary-hover',
|
|
67
|
+
'disabled:text-text-secondary',
|
|
68
|
+
].join(' '),
|
|
69
|
+
|
|
70
|
+
dropdown: [
|
|
71
|
+
'border-2 border-solid bg-transparent',
|
|
72
|
+
'text-[var(--color-button-dropdown-fg)] border-[var(--color-button-dropdown-border)]',
|
|
73
|
+
'hover:bg-transparent hover:text-hover hover:border-hover',
|
|
74
|
+
'data-[expanded]:bg-transparent data-[expanded]:text-hover data-[expanded]:border-hover',
|
|
75
|
+
'data-[selected]:bg-selected-control-bg data-[selected]:text-selected-control-text data-[selected]:border-transparent',
|
|
76
|
+
'data-[selected]:hover:text-hover',
|
|
77
|
+
].join(' '),
|
|
78
|
+
|
|
79
|
+
pagination: [
|
|
80
|
+
'border-2 border-solid bg-transparent',
|
|
81
|
+
'text-[var(--color-button-pagination-fg)] border-[var(--color-button-pagination-border)]',
|
|
82
|
+
'hover:bg-transparent hover:text-hover hover:border-hover',
|
|
83
|
+
'data-[selected]:bg-selected-control-bg data-[selected]:text-selected-control-text data-[selected]:border-transparent',
|
|
84
|
+
].join(' '),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// SVG icon size classes per size variant
|
|
88
|
+
const ICON_SIZE_CLASSES: Record<string, string> = {
|
|
89
|
+
'2xs': '[&_svg]:size-5',
|
|
90
|
+
'2xs_alt': '[&_svg]:size-3',
|
|
91
|
+
md: '[&_svg]:size-5',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Types
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
73
97
|
|
|
74
98
|
type Variant = 'plain' | 'icon_secondary' | 'icon_background' | 'link' | 'dropdown' | 'pagination';
|
|
75
99
|
type Size = '2xs' | '2xs_alt' | 'md';
|
|
76
100
|
|
|
77
|
-
export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
78
|
-
|
|
101
|
+
export interface IconButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'size'> {
|
|
79
102
|
/** Visual variant */
|
|
80
103
|
variant?: Variant;
|
|
81
|
-
|
|
82
104
|
/** Icon-button size */
|
|
83
105
|
size?: Size;
|
|
84
|
-
|
|
85
106
|
/** Show a loading spinner (disables button) */
|
|
86
107
|
loading?: boolean;
|
|
87
|
-
|
|
88
108
|
/** Render a skeleton placeholder instead of the button */
|
|
89
109
|
loadingSkeleton?: boolean;
|
|
90
|
-
|
|
91
110
|
/** Maps to data-expanded for popover trigger styling */
|
|
92
111
|
expanded?: boolean;
|
|
93
|
-
|
|
94
112
|
/** Maps to data-selected for toggle styling */
|
|
95
113
|
selected?: boolean;
|
|
96
|
-
|
|
97
114
|
/** Maps to data-highlighted */
|
|
98
115
|
highlighted?: boolean;
|
|
99
|
-
|
|
100
116
|
/** Polymorphic element type (e.g. "div") */
|
|
101
117
|
as?: React.ElementType;
|
|
102
|
-
|
|
103
118
|
// Legacy Chakra style-prop shims
|
|
104
119
|
boxSize?: number | string;
|
|
105
120
|
color?: string;
|
|
@@ -111,9 +126,11 @@ export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonEl
|
|
|
111
126
|
_expanded?: Record<string, string>;
|
|
112
127
|
}
|
|
113
128
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Component
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
const SP = 4;
|
|
117
134
|
|
|
118
135
|
export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
119
136
|
function IconButton(props, ref) {
|
|
@@ -128,7 +145,7 @@ export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
|
128
145
|
disabled,
|
|
129
146
|
className,
|
|
130
147
|
children,
|
|
131
|
-
as:
|
|
148
|
+
as: _as,
|
|
132
149
|
style: styleProp,
|
|
133
150
|
// Strip Chakra style props
|
|
134
151
|
boxSize: _boxSize,
|
|
@@ -142,47 +159,55 @@ export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
|
142
159
|
...rest
|
|
143
160
|
} = props;
|
|
144
161
|
|
|
145
|
-
const SP = 4;
|
|
146
162
|
const shimStyle: React.CSSProperties = { ...styleProp };
|
|
147
163
|
if (_boxSize !== undefined) {
|
|
148
|
-
const bs = typeof _boxSize === 'number' ? `${
|
|
164
|
+
const bs = typeof _boxSize === 'number' ? `${_boxSize * SP}px` : _boxSize;
|
|
149
165
|
shimStyle.width = bs;
|
|
150
166
|
shimStyle.height = bs;
|
|
151
167
|
}
|
|
152
168
|
if (_color) shimStyle.color = _color;
|
|
153
169
|
if (_px !== undefined) {
|
|
154
|
-
const v = typeof _px === 'number' ? `${
|
|
170
|
+
const v = typeof _px === 'number' ? `${_px * SP}px` : _px;
|
|
155
171
|
shimStyle.paddingLeft = v;
|
|
156
172
|
shimStyle.paddingRight = v;
|
|
157
173
|
}
|
|
158
174
|
if (_borderRadius) shimStyle.borderRadius = _borderRadius;
|
|
159
|
-
if (_ml !== undefined) shimStyle.marginLeft = typeof _ml === 'number' ? `${
|
|
160
|
-
if (_mr !== undefined) shimStyle.marginRight = typeof _mr === 'number' ? `${
|
|
161
|
-
const
|
|
175
|
+
if (_ml !== undefined) shimStyle.marginLeft = typeof _ml === 'number' ? `${_ml * SP}px` : _ml;
|
|
176
|
+
if (_mr !== undefined) shimStyle.marginRight = typeof _mr === 'number' ? `${_mr * SP}px` : _mr;
|
|
177
|
+
const mergedStyle = Object.keys(shimStyle).length > 0 ? shimStyle : undefined;
|
|
178
|
+
|
|
179
|
+
const variantClass = VARIANT_CLASSES[variant] ?? VARIANT_CLASSES.plain;
|
|
180
|
+
const iconSizeClass = size ? (ICON_SIZE_CLASSES[size] ?? '') : '';
|
|
181
|
+
const combinedClassName = [variantClass, iconSizeClass, className].filter(Boolean).join(' ');
|
|
162
182
|
|
|
163
183
|
const button = (
|
|
164
|
-
<
|
|
165
|
-
ref={
|
|
166
|
-
|
|
167
|
-
disabled={
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
{
|
|
171
|
-
{
|
|
172
|
-
{
|
|
173
|
-
{
|
|
174
|
-
{
|
|
184
|
+
<IconButtonFrame
|
|
185
|
+
ref={ref as any}
|
|
186
|
+
size={size as any}
|
|
187
|
+
disabled={(!loadingSkeleton && (loading || disabled)) || undefined}
|
|
188
|
+
render={_as ? (React.createElement(_as) as React.ReactElement) : undefined}
|
|
189
|
+
className={combinedClassName}
|
|
190
|
+
style={mergedStyle}
|
|
191
|
+
{...(expanded ? { 'data-expanded': true } : {})}
|
|
192
|
+
{...(selected ? { 'data-selected': true } : {})}
|
|
193
|
+
{...(highlighted ? { 'data-highlighted': true } : {})}
|
|
194
|
+
{...(loadingSkeleton ? { 'data-loading-skeleton': true } : {})}
|
|
195
|
+
{...(rest as any)}
|
|
175
196
|
>
|
|
176
|
-
{
|
|
177
|
-
<span
|
|
178
|
-
|
|
179
|
-
|
|
197
|
+
{loading ? (
|
|
198
|
+
<span
|
|
199
|
+
className="inline-block size-5 animate-spin rounded-full border-2 border-current border-b-transparent border-l-transparent"
|
|
200
|
+
role="status"
|
|
201
|
+
aria-label="Loading"
|
|
202
|
+
/>
|
|
203
|
+
) : children}
|
|
204
|
+
</IconButtonFrame>
|
|
180
205
|
);
|
|
181
206
|
|
|
182
207
|
if (loadingSkeleton) {
|
|
183
208
|
return (
|
|
184
|
-
<Skeleton loading={
|
|
185
|
-
{
|
|
209
|
+
<Skeleton loading={loadingSkeleton} asChild ref={ref as React.ForwardedRef<HTMLDivElement>}>
|
|
210
|
+
{button}
|
|
186
211
|
</Skeleton>
|
|
187
212
|
);
|
|
188
213
|
}
|