@luxfi/ui 5.5.2 → 5.6.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 (249) hide show
  1. package/dist/accordion.cjs +213 -0
  2. package/dist/accordion.js +186 -0
  3. package/dist/alert.cjs +553 -0
  4. package/dist/alert.js +531 -0
  5. package/dist/avatar.cjs +149 -0
  6. package/dist/avatar.js +125 -0
  7. package/dist/badge.cjs +611 -0
  8. package/dist/badge.js +589 -0
  9. package/dist/button.cjs +689 -0
  10. package/dist/button.js +664 -0
  11. package/dist/checkbox.cjs +265 -0
  12. package/dist/checkbox.js +241 -0
  13. package/dist/close-button.cjs +73 -0
  14. package/dist/close-button.js +51 -0
  15. package/dist/collapsible.cjs +702 -0
  16. package/dist/collapsible.js +679 -0
  17. package/dist/color-mode.cjs +96 -0
  18. package/dist/color-mode.js +72 -0
  19. package/dist/dialog.cjs +279 -0
  20. package/dist/dialog.js +246 -0
  21. package/dist/drawer.cjs +207 -0
  22. package/dist/drawer.js +175 -0
  23. package/dist/empty-state.cjs +93 -0
  24. package/dist/empty-state.js +71 -0
  25. package/dist/field.cjs +183 -0
  26. package/dist/field.js +160 -0
  27. package/dist/heading.cjs +46 -0
  28. package/dist/heading.js +40 -0
  29. package/dist/icon-button.cjs +491 -0
  30. package/dist/icon-button.js +470 -0
  31. package/dist/image.cjs +572 -0
  32. package/dist/image.js +551 -0
  33. package/dist/index.cjs +5779 -0
  34. package/dist/index.js +5619 -0
  35. package/dist/input-group.cjs +155 -0
  36. package/dist/input-group.js +133 -0
  37. package/dist/input.cjs +65 -0
  38. package/dist/input.js +59 -0
  39. package/dist/link.cjs +630 -0
  40. package/dist/link.js +606 -0
  41. package/dist/menu.cjs +305 -0
  42. package/dist/menu.js +269 -0
  43. package/dist/pin-input.cjs +182 -0
  44. package/dist/pin-input.js +160 -0
  45. package/dist/popover.cjs +327 -0
  46. package/dist/popover.js +294 -0
  47. package/dist/progress-circle.cjs +152 -0
  48. package/dist/progress-circle.js +128 -0
  49. package/dist/progress.cjs +117 -0
  50. package/dist/progress.js +94 -0
  51. package/dist/provider.cjs +62 -0
  52. package/dist/provider.js +40 -0
  53. package/dist/radio.cjs +177 -0
  54. package/dist/radio.js +153 -0
  55. package/dist/rating.cjs +80 -0
  56. package/dist/rating.js +58 -0
  57. package/dist/select.cjs +791 -0
  58. package/dist/select.js +757 -0
  59. package/dist/separator.cjs +57 -0
  60. package/dist/separator.js +51 -0
  61. package/dist/skeleton.cjs +370 -0
  62. package/dist/skeleton.js +346 -0
  63. package/dist/slider.cjs +138 -0
  64. package/dist/slider.js +115 -0
  65. package/dist/switch.cjs +163 -0
  66. package/dist/switch.js +140 -0
  67. package/dist/table.cjs +1044 -0
  68. package/dist/table.js +1013 -0
  69. package/dist/tabs.cjs +240 -0
  70. package/dist/tabs.js +213 -0
  71. package/dist/tag.cjs +651 -0
  72. package/dist/tag.js +628 -0
  73. package/dist/textarea.cjs +65 -0
  74. package/dist/textarea.js +59 -0
  75. package/dist/toaster.cjs +99 -0
  76. package/dist/toaster.js +96 -0
  77. package/dist/tooltip.cjs +171 -0
  78. package/dist/tooltip.js +148 -0
  79. package/dist/utils.cjs +11 -0
  80. package/dist/utils.js +9 -0
  81. package/package.json +270 -66
  82. package/src/accordion.tsx +285 -0
  83. package/src/alert.tsx +221 -0
  84. package/src/avatar.tsx +174 -0
  85. package/src/badge.tsx +158 -0
  86. package/src/button.tsx +411 -0
  87. package/src/checkbox.tsx +307 -0
  88. package/src/close-button.tsx +51 -0
  89. package/src/collapsible.tsx +126 -0
  90. package/src/color-mode.tsx +125 -0
  91. package/src/dialog.tsx +356 -0
  92. package/src/drawer.tsx +186 -0
  93. package/src/empty-state.tsx +97 -0
  94. package/src/field.tsx +202 -0
  95. package/src/heading.tsx +55 -0
  96. package/src/icon-button.tsx +192 -0
  97. package/src/image.tsx +280 -0
  98. package/src/index.ts +192 -0
  99. package/src/input-group.tsx +159 -0
  100. package/src/input.tsx +60 -0
  101. package/src/link.tsx +326 -0
  102. package/src/menu.tsx +471 -0
  103. package/src/pin-input.tsx +187 -0
  104. package/src/popover.tsx +400 -0
  105. package/src/progress-circle.tsx +180 -0
  106. package/src/progress.tsx +109 -0
  107. package/src/provider.tsx +12 -0
  108. package/src/radio.tsx +175 -0
  109. package/src/rating.tsx +79 -0
  110. package/src/select.tsx +696 -0
  111. package/src/separator.tsx +59 -0
  112. package/src/skeleton.tsx +302 -0
  113. package/src/slider.tsx +152 -0
  114. package/src/switch.tsx +158 -0
  115. package/src/table.tsx +621 -0
  116. package/src/tabs.tsx +354 -0
  117. package/src/tag.tsx +159 -0
  118. package/src/textarea.tsx +60 -0
  119. package/src/toaster.tsx +117 -0
  120. package/src/tokens.css +438 -0
  121. package/src/tooltip.tsx +184 -0
  122. package/src/utils/cn.ts +7 -0
  123. package/src/utils.ts +6 -0
  124. package/tokens.css +438 -0
  125. package/commerce/ui/conf.ts +0 -13
  126. package/commerce/ui/context.tsx +0 -123
  127. package/commerce/ui/store.ts +0 -295
  128. package/components/access-code-input.tsx +0 -71
  129. package/components/analytics.tsx +0 -23
  130. package/components/auth/auth-listener.tsx +0 -29
  131. package/components/auth/auth-token/clear-auth-token.tsx +0 -12
  132. package/components/auth/auth-token/set-auth-token.tsx +0 -16
  133. package/components/auth/common-auth-domains.ts +0 -17
  134. package/components/auth/login-panel.tsx +0 -111
  135. package/components/auth/mobile-login-button.tsx +0 -107
  136. package/components/auth/signup-panel.tsx +0 -113
  137. package/components/back-button.tsx +0 -49
  138. package/components/chat-widget.tsx +0 -85
  139. package/components/commerce/bag-button.tsx +0 -98
  140. package/components/commerce/buy-button.tsx +0 -34
  141. package/components/commerce/checkout-button.tsx +0 -129
  142. package/components/commerce/checkout-panel/cart-accordian.tsx +0 -66
  143. package/components/commerce/checkout-panel/checkout-panel-props.ts +0 -10
  144. package/components/commerce/checkout-panel/desktop-bag-carousel.tsx +0 -36
  145. package/components/commerce/checkout-panel/desktop-cp.tsx +0 -83
  146. package/components/commerce/checkout-panel/index.tsx +0 -126
  147. package/components/commerce/checkout-panel/mobile-cp.tsx +0 -67
  148. package/components/commerce/checkout-panel/policy-links.tsx +0 -29
  149. package/components/commerce/checkout-panel/steps-indicator.tsx +0 -39
  150. package/components/commerce/checkout-panel/thank-you.tsx +0 -18
  151. package/components/commerce/desktop-bag-popup.tsx +0 -78
  152. package/components/commerce/drawer/index.tsx +0 -88
  153. package/components/commerce/drawer/micro.tsx +0 -145
  154. package/components/commerce/drawer/shell.tsx +0 -85
  155. package/components/contact-dialog/contact-form.tsx +0 -116
  156. package/components/contact-dialog/disclaimer.tsx +0 -13
  157. package/components/contact-dialog/index.tsx +0 -64
  158. package/components/copyright.tsx +0 -21
  159. package/components/drawer-margin.tsx +0 -28
  160. package/components/footer.tsx +0 -78
  161. package/components/header/desktop-nav-menu.tsx +0 -204
  162. package/components/header/desktop.tsx +0 -65
  163. package/components/header/index.tsx +0 -50
  164. package/components/header/mobile-bag-drawer.tsx +0 -51
  165. package/components/header/mobile-menu-toggle-button.tsx +0 -35
  166. package/components/header/mobile-nav-menu-ai.tsx +0 -51
  167. package/components/header/mobile-nav-menu-item.tsx +0 -47
  168. package/components/header/mobile-nav-menu.tsx +0 -102
  169. package/components/header/mobile.tsx +0 -170
  170. package/components/header/theme-toggle.tsx +0 -26
  171. package/components/icons/avatar.tsx +0 -11
  172. package/components/icons/bag-icon.tsx +0 -10
  173. package/components/icons/index.ts +0 -6
  174. package/components/icons/left-arrow.tsx +0 -11
  175. package/components/icons/lux-logo.tsx +0 -10
  176. package/components/icons/right-arrow.tsx +0 -10
  177. package/components/icons/social-icon.tsx +0 -35
  178. package/components/icons/social-svg.css +0 -3
  179. package/components/index.ts +0 -26
  180. package/components/logo.tsx +0 -92
  181. package/components/main.tsx +0 -27
  182. package/components/mini-chart/index.tsx +0 -8
  183. package/components/mini-chart/mini-chart-props.ts +0 -44
  184. package/components/mini-chart/mini-chart.tsx +0 -85
  185. package/components/mini-chart/wrapper.tsx +0 -23
  186. package/components/not-found/index.tsx +0 -28
  187. package/components/not-found/not-found-content.mdx +0 -5
  188. package/components/tooltip.tsx +0 -31
  189. package/environment.d.ts +0 -6
  190. package/next/analytics/fpixel.ts +0 -16
  191. package/next/analytics/google-analytics.ts +0 -14
  192. package/next/analytics/index.ts +0 -3
  193. package/next/analytics/pixel-analytics.tsx +0 -55
  194. package/next/font/get-app-router-font-classes.ts +0 -17
  195. package/next/font/load-and-return-lux-next-fonts-on-import.ts +0 -68
  196. package/next/font/local/Druk-Wide-Bold.ttf +0 -0
  197. package/next/font/local/Druk-Wide-Medium.ttf +0 -0
  198. package/next/font/local/InterVariable-Italic.ttf +0 -0
  199. package/next/font/local/InterVariable-Italic.woff2 +0 -0
  200. package/next/font/local/InterVariable.ttf +0 -0
  201. package/next/font/local/InterVariable.woff2 +0 -0
  202. package/next/font/next-font-desc.ts +0 -28
  203. package/next/font/pages-router-font-vars.tsx +0 -18
  204. package/next/head-metadata/from-next/metadata-types.ts +0 -158
  205. package/next/head-metadata/from-next/opengraph-types.ts +0 -267
  206. package/next/head-metadata/from-next/twitter-types.ts +0 -92
  207. package/next/head-metadata/index.tsx +0 -208
  208. package/next/index.ts +0 -2
  209. package/next/middleware/determine-device-mw.ts +0 -29
  210. package/root-layout/WHY_THIS_IS_SEPARATE.txt +0 -2
  211. package/root-layout/index.tsx +0 -111
  212. package/server/auth-wrapper.ts +0 -24
  213. package/server-actions/TO-DO.txt +0 -1
  214. package/server-actions/firebase-app.ts +0 -29
  215. package/server-actions/index.ts +0 -5
  216. package/server-actions/store-contact.ts +0 -66
  217. package/site-def/footer/community.tsx +0 -61
  218. package/site-def/footer/company.ts +0 -37
  219. package/site-def/footer/ecosystem.ts +0 -37
  220. package/site-def/footer/index.tsx +0 -26
  221. package/site-def/footer/legal.ts +0 -28
  222. package/site-def/footer/network.ts +0 -45
  223. package/site-def/footer/svg/warpcast-logo.svg +0 -12
  224. package/site-def/index.ts +0 -4
  225. package/site-def/main-nav.tsx +0 -460
  226. package/style/cart-animation.css +0 -29
  227. package/style/checkout-animation.css +0 -23
  228. package/style/drawer-handle-overrides.css +0 -160
  229. package/style/fonts/COPY_TO_PUBLIC_FOR_NON_NEXT.txt +0 -0
  230. package/style/fonts/Druk-Wide-Bold.ttf +0 -0
  231. package/style/fonts/Druk-Wide-Medium.ttf +0 -0
  232. package/style/fonts/InterVariable-Italic.ttf +0 -0
  233. package/style/fonts/InterVariable-Italic.woff2 +0 -0
  234. package/style/fonts/InterVariable.ttf +0 -0
  235. package/style/fonts/InterVariable.woff2 +0 -0
  236. package/style/lux-colors.css +0 -85
  237. package/style/lux-fonts.css +0 -30
  238. package/style/lux-global-non-next.css +0 -52
  239. package/style/lux-global.css +0 -51
  240. package/tailwind/fontFamily.tailwind.lux.ts +0 -18
  241. package/tailwind/index.ts +0 -2
  242. package/tailwind/lux-tw-fonts.ts +0 -40
  243. package/tailwind/tailwind.config.lux-preset.ts +0 -10
  244. package/tsconfig.json +0 -15
  245. package/types/chatbot-config.ts +0 -7
  246. package/types/chatbot-suggested-question.ts +0 -7
  247. package/types/contact-info.ts +0 -11
  248. package/types/index.ts +0 -4
  249. package/types/site-def.ts +0 -46
package/src/menu.tsx ADDED
@@ -0,0 +1,471 @@
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
+ });
@@ -0,0 +1,187 @@
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
+ );