@luxfi/ui 5.6.0 → 6.0.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/README.md +109 -0
- package/package.json +81 -278
- package/dist/accordion.cjs +0 -213
- package/dist/accordion.js +0 -186
- package/dist/alert.cjs +0 -553
- package/dist/alert.js +0 -531
- package/dist/avatar.cjs +0 -149
- package/dist/avatar.js +0 -125
- package/dist/badge.cjs +0 -611
- package/dist/badge.js +0 -589
- package/dist/button.cjs +0 -689
- package/dist/button.js +0 -664
- package/dist/checkbox.cjs +0 -265
- package/dist/checkbox.js +0 -241
- package/dist/close-button.cjs +0 -73
- package/dist/close-button.js +0 -51
- package/dist/collapsible.cjs +0 -702
- package/dist/collapsible.js +0 -679
- package/dist/color-mode.cjs +0 -96
- package/dist/color-mode.js +0 -72
- package/dist/dialog.cjs +0 -279
- package/dist/dialog.js +0 -246
- package/dist/drawer.cjs +0 -207
- package/dist/drawer.js +0 -175
- package/dist/empty-state.cjs +0 -93
- package/dist/empty-state.js +0 -71
- package/dist/field.cjs +0 -183
- package/dist/field.js +0 -160
- package/dist/heading.cjs +0 -46
- package/dist/heading.js +0 -40
- package/dist/icon-button.cjs +0 -491
- package/dist/icon-button.js +0 -470
- package/dist/image.cjs +0 -572
- package/dist/image.js +0 -551
- package/dist/index.cjs +0 -5779
- package/dist/index.js +0 -5619
- package/dist/input-group.cjs +0 -155
- package/dist/input-group.js +0 -133
- package/dist/input.cjs +0 -65
- package/dist/input.js +0 -59
- package/dist/link.cjs +0 -630
- package/dist/link.js +0 -606
- package/dist/menu.cjs +0 -305
- package/dist/menu.js +0 -269
- package/dist/pin-input.cjs +0 -182
- package/dist/pin-input.js +0 -160
- package/dist/popover.cjs +0 -327
- package/dist/popover.js +0 -294
- package/dist/progress-circle.cjs +0 -152
- package/dist/progress-circle.js +0 -128
- package/dist/progress.cjs +0 -117
- package/dist/progress.js +0 -94
- package/dist/provider.cjs +0 -62
- package/dist/provider.js +0 -40
- package/dist/radio.cjs +0 -177
- package/dist/radio.js +0 -153
- package/dist/rating.cjs +0 -80
- package/dist/rating.js +0 -58
- package/dist/select.cjs +0 -791
- package/dist/select.js +0 -757
- package/dist/separator.cjs +0 -57
- package/dist/separator.js +0 -51
- package/dist/skeleton.cjs +0 -370
- package/dist/skeleton.js +0 -346
- package/dist/slider.cjs +0 -138
- package/dist/slider.js +0 -115
- package/dist/switch.cjs +0 -163
- package/dist/switch.js +0 -140
- package/dist/table.cjs +0 -1044
- package/dist/table.js +0 -1013
- package/dist/tabs.cjs +0 -240
- package/dist/tabs.js +0 -213
- package/dist/tag.cjs +0 -651
- package/dist/tag.js +0 -628
- package/dist/textarea.cjs +0 -65
- package/dist/textarea.js +0 -59
- package/dist/toaster.cjs +0 -99
- package/dist/toaster.js +0 -96
- package/dist/tooltip.cjs +0 -171
- package/dist/tooltip.js +0 -148
- package/dist/utils.cjs +0 -11
- package/dist/utils.js +0 -9
- package/src/accordion.tsx +0 -285
- package/src/alert.tsx +0 -221
- package/src/avatar.tsx +0 -174
- package/src/badge.tsx +0 -158
- package/src/button.tsx +0 -411
- package/src/checkbox.tsx +0 -307
- package/src/close-button.tsx +0 -51
- package/src/collapsible.tsx +0 -126
- package/src/color-mode.tsx +0 -125
- package/src/dialog.tsx +0 -356
- package/src/drawer.tsx +0 -186
- package/src/empty-state.tsx +0 -97
- package/src/field.tsx +0 -202
- package/src/heading.tsx +0 -55
- package/src/icon-button.tsx +0 -192
- package/src/image.tsx +0 -280
- package/src/index.ts +0 -192
- package/src/input-group.tsx +0 -159
- package/src/input.tsx +0 -60
- package/src/link.tsx +0 -326
- package/src/menu.tsx +0 -471
- package/src/pin-input.tsx +0 -187
- package/src/popover.tsx +0 -400
- package/src/progress-circle.tsx +0 -180
- package/src/progress.tsx +0 -109
- package/src/provider.tsx +0 -12
- package/src/radio.tsx +0 -175
- package/src/rating.tsx +0 -79
- package/src/select.tsx +0 -696
- package/src/separator.tsx +0 -59
- package/src/skeleton.tsx +0 -302
- package/src/slider.tsx +0 -152
- package/src/switch.tsx +0 -158
- package/src/table.tsx +0 -621
- package/src/tabs.tsx +0 -354
- package/src/tag.tsx +0 -159
- package/src/textarea.tsx +0 -60
- package/src/toaster.tsx +0 -117
- package/src/tokens.css +0 -438
- package/src/tooltip.tsx +0 -184
- package/src/utils/cn.ts +0 -7
- package/src/utils.ts +0 -6
- package/tokens.css +0 -438
package/src/menu.tsx
DELETED
|
@@ -1,471 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
|
4
|
-
import * as React from 'react';
|
|
5
|
-
import { LuCheck, LuChevronRight } from 'react-icons/lu';
|
|
6
|
-
|
|
7
|
-
import { cn } from './utils';
|
|
8
|
-
|
|
9
|
-
// --- Utility: map Chakra-style placement string to Radix side + align ---
|
|
10
|
-
|
|
11
|
-
type Side = 'top' | 'right' | 'bottom' | 'left';
|
|
12
|
-
type Align = 'start' | 'center' | 'end';
|
|
13
|
-
|
|
14
|
-
interface PlacementMapping {
|
|
15
|
-
readonly side: Side;
|
|
16
|
-
readonly align: Align;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function parsePlacement(placement: string | undefined): PlacementMapping {
|
|
20
|
-
if (!placement) {
|
|
21
|
-
return { side: 'bottom', align: 'start' };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const parts = placement.split('-');
|
|
25
|
-
const side = (parts[0] as Side) ?? 'bottom';
|
|
26
|
-
const alignPart = parts[1];
|
|
27
|
-
|
|
28
|
-
let align: Align = 'center';
|
|
29
|
-
if (alignPart === 'start') {
|
|
30
|
-
align = 'start';
|
|
31
|
-
} else if (alignPart === 'end') {
|
|
32
|
-
align = 'end';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return { side, align };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// --- Positioning context (mirrors popover.tsx pattern) ---
|
|
39
|
-
|
|
40
|
-
interface Positioning {
|
|
41
|
-
readonly placement?: string;
|
|
42
|
-
readonly offset?: {
|
|
43
|
-
readonly mainAxis?: number;
|
|
44
|
-
readonly crossAxis?: number;
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface MenuPositioning {
|
|
49
|
-
readonly side: Side;
|
|
50
|
-
readonly align: Align;
|
|
51
|
-
readonly sideOffset: number;
|
|
52
|
-
readonly alignOffset: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const PositioningContext = React.createContext<MenuPositioning>({
|
|
56
|
-
side: 'bottom',
|
|
57
|
-
align: 'start',
|
|
58
|
-
sideOffset: 4,
|
|
59
|
-
alignOffset: 0,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// --- MenuRoot ---
|
|
63
|
-
|
|
64
|
-
export interface MenuRootProps {
|
|
65
|
-
readonly children?: React.ReactNode;
|
|
66
|
-
readonly open?: boolean;
|
|
67
|
-
readonly defaultOpen?: boolean;
|
|
68
|
-
readonly onOpenChange?: (details: { open: boolean }) => void;
|
|
69
|
-
readonly positioning?: Positioning;
|
|
70
|
-
readonly lazyMount?: boolean;
|
|
71
|
-
readonly unmountOnExit?: boolean;
|
|
72
|
-
readonly modal?: boolean;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export const MenuRoot = (props: MenuRootProps): React.ReactElement => {
|
|
76
|
-
const {
|
|
77
|
-
children,
|
|
78
|
-
open,
|
|
79
|
-
defaultOpen,
|
|
80
|
-
onOpenChange,
|
|
81
|
-
positioning,
|
|
82
|
-
modal = true,
|
|
83
|
-
// lazyMount / unmountOnExit have no direct Radix equivalent; Radix handles
|
|
84
|
-
// mount/unmount automatically.
|
|
85
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
86
|
-
lazyMount: _lazyMount,
|
|
87
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
88
|
-
unmountOnExit: _unmountOnExit,
|
|
89
|
-
} = props;
|
|
90
|
-
|
|
91
|
-
const mergedPositioning: Positioning = {
|
|
92
|
-
placement: 'bottom-start',
|
|
93
|
-
...positioning,
|
|
94
|
-
offset: {
|
|
95
|
-
mainAxis: 4,
|
|
96
|
-
...positioning?.offset,
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const { side, align } = parsePlacement(mergedPositioning.placement);
|
|
101
|
-
|
|
102
|
-
const positioningValue = React.useMemo<MenuPositioning>(() => ({
|
|
103
|
-
side,
|
|
104
|
-
align,
|
|
105
|
-
sideOffset: mergedPositioning.offset?.mainAxis ?? 4,
|
|
106
|
-
alignOffset: mergedPositioning.offset?.crossAxis ?? 0,
|
|
107
|
-
}), [ side, align, mergedPositioning.offset?.mainAxis, mergedPositioning.offset?.crossAxis ]);
|
|
108
|
-
|
|
109
|
-
const handleOpenChange = React.useCallback((isOpen: boolean) => {
|
|
110
|
-
onOpenChange?.({ open: isOpen });
|
|
111
|
-
}, [ onOpenChange ]);
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<PositioningContext.Provider value={ positioningValue }>
|
|
115
|
-
<DropdownMenu.Root
|
|
116
|
-
open={ open }
|
|
117
|
-
defaultOpen={ defaultOpen }
|
|
118
|
-
onOpenChange={ handleOpenChange }
|
|
119
|
-
modal={ modal }
|
|
120
|
-
>
|
|
121
|
-
{ children }
|
|
122
|
-
</DropdownMenu.Root>
|
|
123
|
-
</PositioningContext.Provider>
|
|
124
|
-
);
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// --- MenuTrigger ---
|
|
128
|
-
|
|
129
|
-
export interface MenuTriggerProps extends React.ComponentPropsWithoutRef<'button'> {
|
|
130
|
-
readonly asChild?: boolean;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export const MenuTrigger = React.forwardRef<
|
|
134
|
-
HTMLButtonElement,
|
|
135
|
-
MenuTriggerProps
|
|
136
|
-
>(function MenuTrigger(props, ref) {
|
|
137
|
-
const { asChild = false, ...rest } = props;
|
|
138
|
-
return <DropdownMenu.Trigger asChild={ asChild } ref={ ref } { ...rest }/>;
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// --- MenuContent ---
|
|
142
|
-
|
|
143
|
-
export interface MenuContentProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
144
|
-
readonly portalled?: boolean;
|
|
145
|
-
readonly portalRef?: React.RefObject<HTMLElement>;
|
|
146
|
-
|
|
147
|
-
/** Legacy Chakra zIndex prop - mapped to inline style */
|
|
148
|
-
readonly zIndex?: string | number;
|
|
149
|
-
|
|
150
|
-
/** Legacy Chakra minW prop - mapped to inline style */
|
|
151
|
-
readonly minW?: string | number;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export const MenuContent = React.forwardRef<
|
|
155
|
-
HTMLDivElement,
|
|
156
|
-
MenuContentProps
|
|
157
|
-
>(function MenuContent(props, ref) {
|
|
158
|
-
const { portalled = true, portalRef, className, zIndex, minW, style, ...rest } = props;
|
|
159
|
-
const positioning = React.useContext(PositioningContext);
|
|
160
|
-
|
|
161
|
-
const mergedStyle: React.CSSProperties = {
|
|
162
|
-
...style,
|
|
163
|
-
...(zIndex !== undefined ? { zIndex: typeof zIndex === 'string' ? `var(--z-index-${ zIndex }, ${ zIndex })` : zIndex } : {}),
|
|
164
|
-
...(minW !== undefined ? { minWidth: minW } : {}),
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const content = (
|
|
168
|
-
<DropdownMenu.Content
|
|
169
|
-
ref={ ref }
|
|
170
|
-
side={ positioning.side }
|
|
171
|
-
align={ positioning.align }
|
|
172
|
-
sideOffset={ positioning.sideOffset }
|
|
173
|
-
alignOffset={ positioning.alignOffset }
|
|
174
|
-
className={ cn(
|
|
175
|
-
'z-50 min-w-[8rem] overflow-hidden rounded-lg p-1',
|
|
176
|
-
'border border-[var(--color-popover-border,var(--color-border-divider))]',
|
|
177
|
-
'bg-[var(--color-popover-bg,var(--color-dialog-bg))]',
|
|
178
|
-
'shadow-[0_4px_12px_var(--color-popover-shadow)]',
|
|
179
|
-
'outline-none',
|
|
180
|
-
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
181
|
-
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
182
|
-
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
|
|
183
|
-
'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
184
|
-
className,
|
|
185
|
-
) }
|
|
186
|
-
style={ Object.keys(mergedStyle).length > 0 ? mergedStyle : undefined }
|
|
187
|
-
{ ...rest }
|
|
188
|
-
/>
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
if (!portalled) {
|
|
192
|
-
return content;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return (
|
|
196
|
-
<DropdownMenu.Portal container={ portalRef?.current ?? undefined }>
|
|
197
|
-
{ content }
|
|
198
|
-
</DropdownMenu.Portal>
|
|
199
|
-
);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
// --- MenuItem ---
|
|
203
|
-
|
|
204
|
-
export interface MenuItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
205
|
-
|
|
206
|
-
/** Informational value identifier (not used by Radix but kept for API compat) */
|
|
207
|
-
readonly value?: string;
|
|
208
|
-
readonly disabled?: boolean;
|
|
209
|
-
readonly asChild?: boolean;
|
|
210
|
-
|
|
211
|
-
/** Chakra compat - accepted but not used by Radix */
|
|
212
|
-
readonly closeOnSelect?: boolean;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export const MenuItem = React.forwardRef<
|
|
216
|
-
HTMLDivElement,
|
|
217
|
-
MenuItemProps
|
|
218
|
-
>(function MenuItem(props, ref) {
|
|
219
|
-
const { className, value: _value, asChild, closeOnSelect: _closeOnSelect, disabled, children, onClick, ...rest } = props;
|
|
220
|
-
return (
|
|
221
|
-
<DropdownMenu.Item
|
|
222
|
-
ref={ ref }
|
|
223
|
-
asChild={ asChild }
|
|
224
|
-
disabled={ disabled }
|
|
225
|
-
onClick={ onClick }
|
|
226
|
-
className={ cn(
|
|
227
|
-
'relative flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm',
|
|
228
|
-
'outline-none transition-colors',
|
|
229
|
-
'text-[var(--color-text-primary,inherit)]',
|
|
230
|
-
'data-[highlighted]:bg-[var(--color-popover-item-hover,rgba(0,0,0,0.04))]',
|
|
231
|
-
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
232
|
-
className,
|
|
233
|
-
) }
|
|
234
|
-
{ ...(rest as React.ComponentPropsWithoutRef<typeof DropdownMenu.Item>) }
|
|
235
|
-
>
|
|
236
|
-
{ children }
|
|
237
|
-
</DropdownMenu.Item>
|
|
238
|
-
);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// --- MenuItemText ---
|
|
242
|
-
|
|
243
|
-
export interface MenuItemTextProps extends React.ComponentPropsWithoutRef<'span'> {}
|
|
244
|
-
|
|
245
|
-
export const MenuItemText = React.forwardRef<
|
|
246
|
-
HTMLSpanElement,
|
|
247
|
-
MenuItemTextProps
|
|
248
|
-
>(function MenuItemText(props, ref) {
|
|
249
|
-
const { className, ...rest } = props;
|
|
250
|
-
return <span ref={ ref } className={ cn('flex-1', className) } { ...rest }/>;
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// --- MenuItemCommand ---
|
|
254
|
-
|
|
255
|
-
export interface MenuItemCommandProps extends React.ComponentPropsWithoutRef<'span'> {}
|
|
256
|
-
|
|
257
|
-
export const MenuItemCommand = React.forwardRef<
|
|
258
|
-
HTMLSpanElement,
|
|
259
|
-
MenuItemCommandProps
|
|
260
|
-
>(function MenuItemCommand(props, ref) {
|
|
261
|
-
const { className, ...rest } = props;
|
|
262
|
-
return (
|
|
263
|
-
<span
|
|
264
|
-
ref={ ref }
|
|
265
|
-
className={ cn('ml-auto text-xs tracking-widest opacity-60', className) }
|
|
266
|
-
{ ...rest }
|
|
267
|
-
/>
|
|
268
|
-
);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// --- MenuSeparator ---
|
|
272
|
-
|
|
273
|
-
export interface MenuSeparatorProps extends React.ComponentPropsWithoutRef<'div'> {}
|
|
274
|
-
|
|
275
|
-
export const MenuSeparator = React.forwardRef<
|
|
276
|
-
HTMLDivElement,
|
|
277
|
-
MenuSeparatorProps
|
|
278
|
-
>(function MenuSeparator(props, ref) {
|
|
279
|
-
const { className, ...rest } = props;
|
|
280
|
-
return (
|
|
281
|
-
<DropdownMenu.Separator
|
|
282
|
-
ref={ ref }
|
|
283
|
-
className={ cn('-mx-1 my-1 h-px bg-[var(--color-border-divider)]', className) }
|
|
284
|
-
{ ...rest }
|
|
285
|
-
/>
|
|
286
|
-
);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// --- MenuItemGroup ---
|
|
290
|
-
|
|
291
|
-
export interface MenuItemGroupProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
292
|
-
readonly title?: string;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export const MenuItemGroup = React.forwardRef<
|
|
296
|
-
HTMLDivElement,
|
|
297
|
-
MenuItemGroupProps
|
|
298
|
-
>(function MenuItemGroup(props, ref) {
|
|
299
|
-
const { title, children, className, ...rest } = props;
|
|
300
|
-
return (
|
|
301
|
-
<DropdownMenu.Group ref={ ref } className={ className } { ...rest }>
|
|
302
|
-
{ title && (
|
|
303
|
-
<DropdownMenu.Label className="px-2 py-1.5 text-xs font-semibold select-none opacity-60">
|
|
304
|
-
{ title }
|
|
305
|
-
</DropdownMenu.Label>
|
|
306
|
-
) }
|
|
307
|
-
{ children }
|
|
308
|
-
</DropdownMenu.Group>
|
|
309
|
-
);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
// --- MenuArrow ---
|
|
313
|
-
|
|
314
|
-
export interface MenuArrowProps extends React.ComponentPropsWithoutRef<'svg'> {}
|
|
315
|
-
|
|
316
|
-
export const MenuArrow = React.forwardRef<
|
|
317
|
-
SVGSVGElement,
|
|
318
|
-
MenuArrowProps
|
|
319
|
-
>(function MenuArrow(props, ref) {
|
|
320
|
-
const { className, ...rest } = props;
|
|
321
|
-
return (
|
|
322
|
-
<DropdownMenu.Arrow
|
|
323
|
-
ref={ ref }
|
|
324
|
-
className={ cn('fill-[var(--color-popover-bg,var(--color-dialog-bg))]', className) }
|
|
325
|
-
{ ...rest }
|
|
326
|
-
/>
|
|
327
|
-
);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
// --- MenuCheckboxItem ---
|
|
331
|
-
|
|
332
|
-
export interface MenuCheckboxItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
333
|
-
readonly checked?: boolean;
|
|
334
|
-
readonly onCheckedChange?: (checked: boolean) => void;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export const MenuCheckboxItem = React.forwardRef<
|
|
338
|
-
HTMLDivElement,
|
|
339
|
-
MenuCheckboxItemProps
|
|
340
|
-
>(function MenuCheckboxItem(props, ref) {
|
|
341
|
-
const { className, children, checked, onCheckedChange, onClick, ...rest } = props;
|
|
342
|
-
return (
|
|
343
|
-
<DropdownMenu.CheckboxItem
|
|
344
|
-
ref={ ref }
|
|
345
|
-
checked={ checked }
|
|
346
|
-
onCheckedChange={ onCheckedChange }
|
|
347
|
-
onClick={ onClick }
|
|
348
|
-
className={ cn(
|
|
349
|
-
'relative flex cursor-pointer select-none items-center rounded-md py-1.5 pr-2 pl-8 text-sm',
|
|
350
|
-
'outline-none transition-colors',
|
|
351
|
-
'data-[highlighted]:bg-[var(--color-popover-item-hover,rgba(0,0,0,0.04))]',
|
|
352
|
-
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
353
|
-
className,
|
|
354
|
-
) }
|
|
355
|
-
{ ...(rest as React.ComponentPropsWithoutRef<typeof DropdownMenu.CheckboxItem>) }
|
|
356
|
-
>
|
|
357
|
-
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
358
|
-
<DropdownMenu.ItemIndicator>
|
|
359
|
-
<LuCheck className="h-4 w-4"/>
|
|
360
|
-
</DropdownMenu.ItemIndicator>
|
|
361
|
-
</span>
|
|
362
|
-
{ children }
|
|
363
|
-
</DropdownMenu.CheckboxItem>
|
|
364
|
-
);
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// --- MenuRadioItemGroup ---
|
|
368
|
-
|
|
369
|
-
export interface MenuRadioItemGroupProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
370
|
-
readonly value?: string;
|
|
371
|
-
readonly onValueChange?: (value: string) => void;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
export const MenuRadioItemGroup = React.forwardRef<
|
|
375
|
-
HTMLDivElement,
|
|
376
|
-
MenuRadioItemGroupProps
|
|
377
|
-
>(function MenuRadioItemGroup(props, ref) {
|
|
378
|
-
const { value, onValueChange, ...rest } = props;
|
|
379
|
-
return (
|
|
380
|
-
<DropdownMenu.RadioGroup
|
|
381
|
-
ref={ ref }
|
|
382
|
-
value={ value }
|
|
383
|
-
onValueChange={ onValueChange }
|
|
384
|
-
{ ...rest }
|
|
385
|
-
/>
|
|
386
|
-
);
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
// --- MenuRadioItem ---
|
|
390
|
-
|
|
391
|
-
export interface MenuRadioItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
392
|
-
readonly value: string;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export const MenuRadioItem = React.forwardRef<
|
|
396
|
-
HTMLDivElement,
|
|
397
|
-
MenuRadioItemProps
|
|
398
|
-
>(function MenuRadioItem(props, ref) {
|
|
399
|
-
const { className, children, value, onClick, ...rest } = props;
|
|
400
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
401
|
-
const { value: _v, ...radixRest } = rest as Record<string, unknown>;
|
|
402
|
-
return (
|
|
403
|
-
<DropdownMenu.RadioItem
|
|
404
|
-
ref={ ref }
|
|
405
|
-
value={ value }
|
|
406
|
-
onClick={ onClick }
|
|
407
|
-
className={ cn(
|
|
408
|
-
'relative flex cursor-pointer select-none items-center rounded-md py-1.5 pr-2 pl-8 text-sm',
|
|
409
|
-
'outline-none transition-colors',
|
|
410
|
-
'data-[highlighted]:bg-[var(--color-popover-item-hover,rgba(0,0,0,0.04))]',
|
|
411
|
-
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
412
|
-
className,
|
|
413
|
-
) }
|
|
414
|
-
>
|
|
415
|
-
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
416
|
-
<DropdownMenu.ItemIndicator>
|
|
417
|
-
<LuCheck className="h-4 w-4"/>
|
|
418
|
-
</DropdownMenu.ItemIndicator>
|
|
419
|
-
</span>
|
|
420
|
-
<span>{ children }</span>
|
|
421
|
-
</DropdownMenu.RadioItem>
|
|
422
|
-
);
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
// --- MenuContextTrigger ---
|
|
426
|
-
// Radix DropdownMenu does not have a native context-menu trigger.
|
|
427
|
-
// For API compatibility, export a no-op wrapper. Real context-menu
|
|
428
|
-
// support would require @radix-ui/react-context-menu.
|
|
429
|
-
|
|
430
|
-
export interface MenuContextTriggerProps extends React.ComponentPropsWithoutRef<'span'> {
|
|
431
|
-
readonly asChild?: boolean;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
export const MenuContextTrigger = React.forwardRef<
|
|
435
|
-
HTMLSpanElement,
|
|
436
|
-
MenuContextTriggerProps
|
|
437
|
-
>(function MenuContextTrigger(props, ref) {
|
|
438
|
-
const { asChild: _asChild, ...rest } = props;
|
|
439
|
-
return <span ref={ ref } { ...rest }/>;
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// --- MenuTriggerItem ---
|
|
443
|
-
|
|
444
|
-
export interface MenuTriggerItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
445
|
-
readonly startIcon?: React.ReactNode;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
export const MenuTriggerItem = React.forwardRef<
|
|
449
|
-
HTMLDivElement,
|
|
450
|
-
MenuTriggerItemProps
|
|
451
|
-
>(function MenuTriggerItem(props, ref) {
|
|
452
|
-
const { startIcon, children, className, ...rest } = props;
|
|
453
|
-
return (
|
|
454
|
-
<DropdownMenu.Sub>
|
|
455
|
-
<DropdownMenu.SubTrigger
|
|
456
|
-
ref={ ref }
|
|
457
|
-
className={ cn(
|
|
458
|
-
'relative flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm',
|
|
459
|
-
'outline-none transition-colors',
|
|
460
|
-
'data-[highlighted]:bg-[var(--color-popover-item-hover,rgba(0,0,0,0.04))]',
|
|
461
|
-
className,
|
|
462
|
-
) }
|
|
463
|
-
{ ...rest }
|
|
464
|
-
>
|
|
465
|
-
{ startIcon }
|
|
466
|
-
<span className="flex-1">{ children }</span>
|
|
467
|
-
<LuChevronRight className="h-4 w-4"/>
|
|
468
|
-
</DropdownMenu.SubTrigger>
|
|
469
|
-
</DropdownMenu.Sub>
|
|
470
|
-
);
|
|
471
|
-
});
|
package/src/pin-input.tsx
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
import { cn } from './utils';
|
|
4
|
-
|
|
5
|
-
export interface PinInputProps {
|
|
6
|
-
readonly rootRef?: React.Ref<HTMLDivElement>;
|
|
7
|
-
readonly count?: number;
|
|
8
|
-
readonly inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
|
9
|
-
readonly attached?: boolean;
|
|
10
|
-
readonly placeholder?: string;
|
|
11
|
-
readonly value?: Array<string>;
|
|
12
|
-
readonly onValueChange?: (details: { value: Array<string> }) => void;
|
|
13
|
-
readonly onValueComplete?: (details: { value: Array<string> }) => void;
|
|
14
|
-
readonly disabled?: boolean;
|
|
15
|
-
readonly invalid?: boolean;
|
|
16
|
-
readonly otp?: boolean;
|
|
17
|
-
readonly name?: string;
|
|
18
|
-
readonly bgColor?: string;
|
|
19
|
-
readonly className?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const INPUT_BASE = [
|
|
23
|
-
'w-10 h-10',
|
|
24
|
-
'text-center text-sm font-medium',
|
|
25
|
-
'border-2 rounded-md',
|
|
26
|
-
'outline-none appearance-none',
|
|
27
|
-
'bg-[var(--color-input-bg)] text-[var(--color-input-fg)]',
|
|
28
|
-
'border-[var(--color-input-border)]',
|
|
29
|
-
'placeholder:text-[var(--color-input-placeholder)]',
|
|
30
|
-
'hover:border-[var(--color-input-border-hover)]',
|
|
31
|
-
'focus:border-[var(--color-input-border-focus)] focus:shadow-md',
|
|
32
|
-
'disabled:opacity-40',
|
|
33
|
-
].join(' ');
|
|
34
|
-
|
|
35
|
-
const INPUT_FILLED = 'border-[var(--color-input-border)]';
|
|
36
|
-
const INPUT_INVALID = 'border-[var(--color-border-error)] hover:border-[var(--color-border-error)]';
|
|
37
|
-
|
|
38
|
-
export const PinInput = React.forwardRef<HTMLInputElement, PinInputProps>(
|
|
39
|
-
function PinInput(props, ref) {
|
|
40
|
-
const {
|
|
41
|
-
count = 6,
|
|
42
|
-
inputProps,
|
|
43
|
-
rootRef,
|
|
44
|
-
attached,
|
|
45
|
-
placeholder = ' ',
|
|
46
|
-
value: controlledValue,
|
|
47
|
-
onValueChange,
|
|
48
|
-
onValueComplete,
|
|
49
|
-
disabled,
|
|
50
|
-
invalid,
|
|
51
|
-
otp,
|
|
52
|
-
name,
|
|
53
|
-
bgColor,
|
|
54
|
-
className,
|
|
55
|
-
} = props;
|
|
56
|
-
|
|
57
|
-
const inputRefs = React.useRef<Array<HTMLInputElement | null>>([]);
|
|
58
|
-
|
|
59
|
-
const values = React.useMemo(
|
|
60
|
-
() => controlledValue ?? Array.from<string>({ length: count }).fill(''),
|
|
61
|
-
[ controlledValue, count ],
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
const updateValue = React.useCallback((index: number, char: string): void => {
|
|
65
|
-
const next = [ ...values ];
|
|
66
|
-
next[index] = char;
|
|
67
|
-
onValueChange?.({ value: next });
|
|
68
|
-
|
|
69
|
-
if (next.every((v) => v.length > 0)) {
|
|
70
|
-
onValueComplete?.({ value: next });
|
|
71
|
-
}
|
|
72
|
-
}, [ values, onValueChange, onValueComplete ]);
|
|
73
|
-
|
|
74
|
-
const focusInput = React.useCallback((index: number): void => {
|
|
75
|
-
const clamped = Math.max(0, Math.min(index, count - 1));
|
|
76
|
-
inputRefs.current[clamped]?.focus();
|
|
77
|
-
}, [ count ]);
|
|
78
|
-
|
|
79
|
-
const handleInput = React.useCallback((index: number, e: React.FormEvent<HTMLInputElement>): void => {
|
|
80
|
-
const target = e.currentTarget;
|
|
81
|
-
const char = target.value.slice(-1);
|
|
82
|
-
updateValue(index, char);
|
|
83
|
-
|
|
84
|
-
if (char && index < count - 1) {
|
|
85
|
-
focusInput(index + 1);
|
|
86
|
-
}
|
|
87
|
-
}, [ count, updateValue, focusInput ]);
|
|
88
|
-
|
|
89
|
-
const handleKeyDown = React.useCallback((index: number, e: React.KeyboardEvent<HTMLInputElement>): void => {
|
|
90
|
-
if (e.key === 'Backspace') {
|
|
91
|
-
if (values[index]) {
|
|
92
|
-
updateValue(index, '');
|
|
93
|
-
} else if (index > 0) {
|
|
94
|
-
updateValue(index - 1, '');
|
|
95
|
-
focusInput(index - 1);
|
|
96
|
-
}
|
|
97
|
-
e.preventDefault();
|
|
98
|
-
} else if (e.key === 'ArrowLeft' && index > 0) {
|
|
99
|
-
focusInput(index - 1);
|
|
100
|
-
e.preventDefault();
|
|
101
|
-
} else if (e.key === 'ArrowRight' && index < count - 1) {
|
|
102
|
-
focusInput(index + 1);
|
|
103
|
-
e.preventDefault();
|
|
104
|
-
}
|
|
105
|
-
}, [ count, values, updateValue, focusInput ]);
|
|
106
|
-
|
|
107
|
-
const handlePaste = React.useCallback((e: React.ClipboardEvent<HTMLInputElement>): void => {
|
|
108
|
-
e.preventDefault();
|
|
109
|
-
const pasted = e.clipboardData.getData('text/plain').trim();
|
|
110
|
-
if (!pasted) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const chars = pasted.slice(0, count).split('');
|
|
114
|
-
const next = [ ...values ];
|
|
115
|
-
chars.forEach((char, i) => {
|
|
116
|
-
next[i] = char;
|
|
117
|
-
});
|
|
118
|
-
onValueChange?.({ value: next });
|
|
119
|
-
|
|
120
|
-
if (next.every((v) => v.length > 0)) {
|
|
121
|
-
onValueComplete?.({ value: next });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
focusInput(Math.min(chars.length, count - 1));
|
|
125
|
-
}, [ count, values, onValueChange, onValueComplete, focusInput ]);
|
|
126
|
-
|
|
127
|
-
const handleFocus = React.useCallback((e: React.FocusEvent<HTMLInputElement>): void => {
|
|
128
|
-
e.currentTarget.select();
|
|
129
|
-
}, []);
|
|
130
|
-
|
|
131
|
-
const handleNoop = React.useCallback((): void => { /* noop */ }, []);
|
|
132
|
-
|
|
133
|
-
const setInputRef = React.useCallback((index: number) => (el: HTMLInputElement | null): void => {
|
|
134
|
-
inputRefs.current[index] = el;
|
|
135
|
-
}, []);
|
|
136
|
-
|
|
137
|
-
const onInputAtIndex = React.useCallback((index: number) => (e: React.FormEvent<HTMLInputElement>): void => {
|
|
138
|
-
handleInput(index, e);
|
|
139
|
-
}, [ handleInput ]);
|
|
140
|
-
|
|
141
|
-
const onKeyDownAtIndex = React.useCallback((index: number) => (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
|
142
|
-
handleKeyDown(index, e);
|
|
143
|
-
}, [ handleKeyDown ]);
|
|
144
|
-
|
|
145
|
-
const bgStyle = bgColor ? { backgroundColor: `var(--color-${ bgColor.replace(/\./g, '-') })` } : undefined;
|
|
146
|
-
|
|
147
|
-
return (
|
|
148
|
-
<div ref={ rootRef } className={ cn('inline-flex items-center', attached ? 'gap-0' : 'gap-2', className) }>
|
|
149
|
-
{ /* Hidden input for form submission */ }
|
|
150
|
-
<input
|
|
151
|
-
ref={ ref }
|
|
152
|
-
type="hidden"
|
|
153
|
-
name={ name }
|
|
154
|
-
value={ values.join('') }
|
|
155
|
-
{ ...inputProps }
|
|
156
|
-
/>
|
|
157
|
-
{ Array.from({ length: count }).map((_, index) => (
|
|
158
|
-
<input
|
|
159
|
-
key={ index }
|
|
160
|
-
ref={ setInputRef(index) }
|
|
161
|
-
type="text"
|
|
162
|
-
inputMode="numeric"
|
|
163
|
-
autoComplete={ otp ? 'one-time-code' : 'off' }
|
|
164
|
-
pattern="[0-9]*"
|
|
165
|
-
maxLength={ 1 }
|
|
166
|
-
placeholder={ placeholder }
|
|
167
|
-
disabled={ disabled }
|
|
168
|
-
aria-invalid={ invalid || undefined }
|
|
169
|
-
value={ values[index] || '' }
|
|
170
|
-
className={ cn(
|
|
171
|
-
INPUT_BASE,
|
|
172
|
-
values[index] && INPUT_FILLED,
|
|
173
|
-
invalid && INPUT_INVALID,
|
|
174
|
-
attached && index > 0 && '-ml-0.5',
|
|
175
|
-
) }
|
|
176
|
-
style={ bgStyle }
|
|
177
|
-
onInput={ onInputAtIndex(index) }
|
|
178
|
-
onKeyDown={ onKeyDownAtIndex(index) }
|
|
179
|
-
onPaste={ handlePaste }
|
|
180
|
-
onFocus={ handleFocus }
|
|
181
|
-
onChange={ handleNoop }
|
|
182
|
-
/>
|
|
183
|
-
)) }
|
|
184
|
-
</div>
|
|
185
|
-
);
|
|
186
|
-
},
|
|
187
|
-
);
|