@luxfi/ui 7.4.10 → 7.4.12

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 (137) hide show
  1. package/dist/accordion.cjs +107 -166
  2. package/dist/accordion.d.cts +18 -51
  3. package/dist/accordion.d.ts +18 -51
  4. package/dist/accordion.js +109 -166
  5. package/dist/alert.cjs +68 -46
  6. package/dist/alert.js +69 -46
  7. package/dist/avatar.cjs +49 -46
  8. package/dist/avatar.js +52 -48
  9. package/dist/badge.cjs +82 -49
  10. package/dist/badge.js +83 -50
  11. package/dist/checkbox.cjs +42 -75
  12. package/dist/checkbox.js +43 -76
  13. package/dist/chrome.cjs +196 -386
  14. package/dist/chrome.js +198 -386
  15. package/dist/close-button.cjs +3 -7
  16. package/dist/close-button.js +3 -7
  17. package/dist/collapsible.cjs +35 -26
  18. package/dist/collapsible.d.cts +2 -1
  19. package/dist/collapsible.d.ts +2 -1
  20. package/dist/collapsible.js +34 -24
  21. package/dist/dialog.cjs +3 -7
  22. package/dist/dialog.d.cts +1 -1
  23. package/dist/dialog.d.ts +1 -1
  24. package/dist/dialog.js +3 -7
  25. package/dist/drawer.cjs +95 -122
  26. package/dist/drawer.d.cts +43 -10
  27. package/dist/drawer.d.ts +43 -10
  28. package/dist/drawer.js +97 -122
  29. package/dist/empty-state.cjs +13 -13
  30. package/dist/empty-state.js +13 -13
  31. package/dist/expiration-selector.cjs +58 -63
  32. package/dist/expiration-selector.js +58 -63
  33. package/dist/field.cjs +60 -109
  34. package/dist/field.d.cts +0 -1
  35. package/dist/field.d.ts +0 -1
  36. package/dist/field.js +61 -109
  37. package/dist/greeks-display.cjs +48 -39
  38. package/dist/greeks-display.js +48 -39
  39. package/dist/heading.cjs +10 -17
  40. package/dist/heading.d.cts +0 -2
  41. package/dist/heading.d.ts +0 -2
  42. package/dist/heading.js +10 -17
  43. package/dist/icon-button.cjs +82 -82
  44. package/dist/icon-button.d.cts +16 -12
  45. package/dist/icon-button.d.ts +16 -12
  46. package/dist/icon-button.js +82 -82
  47. package/dist/index.cjs +1812 -2105
  48. package/dist/index.d.cts +1 -5
  49. package/dist/index.d.ts +1 -5
  50. package/dist/index.js +1814 -2100
  51. package/dist/input-group.cjs +11 -12
  52. package/dist/input-group.js +11 -12
  53. package/dist/input.cjs +27 -27
  54. package/dist/input.d.cts +21 -1
  55. package/dist/input.d.ts +21 -1
  56. package/dist/input.js +25 -28
  57. package/dist/menu.cjs +160 -271
  58. package/dist/menu.d.cts +41 -57
  59. package/dist/menu.d.ts +41 -57
  60. package/dist/menu.js +161 -251
  61. package/dist/option-chain.cjs +89 -67
  62. package/dist/option-chain.js +89 -67
  63. package/dist/option-position.cjs +88 -85
  64. package/dist/option-position.js +88 -85
  65. package/dist/pnl-diagram.cjs +20 -20
  66. package/dist/pnl-diagram.js +20 -20
  67. package/dist/popover.cjs +4 -8
  68. package/dist/popover.js +4 -8
  69. package/dist/progress.cjs +15 -22
  70. package/dist/progress.d.cts +6 -6
  71. package/dist/progress.d.ts +6 -6
  72. package/dist/progress.js +15 -22
  73. package/dist/radio.cjs +35 -67
  74. package/dist/radio.d.cts +18 -18
  75. package/dist/radio.d.ts +18 -18
  76. package/dist/radio.js +36 -68
  77. package/dist/select.cjs +40 -39
  78. package/dist/select.d.cts +35 -4
  79. package/dist/select.d.ts +35 -4
  80. package/dist/select.js +40 -38
  81. package/dist/separator.cjs +8 -33
  82. package/dist/separator.js +8 -33
  83. package/dist/slider.cjs +33 -49
  84. package/dist/slider.js +34 -50
  85. package/dist/switch.cjs +48 -55
  86. package/dist/switch.js +49 -56
  87. package/dist/tabs.cjs +108 -197
  88. package/dist/tabs.d.cts +16 -60
  89. package/dist/tabs.d.ts +16 -60
  90. package/dist/tabs.js +109 -196
  91. package/dist/tag.cjs +112 -69
  92. package/dist/tag.js +113 -69
  93. package/dist/textarea.cjs +56 -27
  94. package/dist/textarea.js +57 -28
  95. package/dist/toaster.cjs +97 -79
  96. package/dist/toaster.d.cts +7 -8
  97. package/dist/toaster.d.ts +7 -8
  98. package/dist/toaster.js +78 -80
  99. package/dist/tooltip.cjs +28 -17
  100. package/dist/tooltip.js +30 -18
  101. package/package.json +2 -16
  102. package/src/accordion.tsx +173 -227
  103. package/src/alert.tsx +78 -45
  104. package/src/avatar.tsx +54 -37
  105. package/src/badge.tsx +65 -44
  106. package/src/checkbox.tsx +70 -89
  107. package/src/close-button.tsx +3 -8
  108. package/src/collapsible.tsx +55 -52
  109. package/src/drawer.tsx +123 -115
  110. package/src/empty-state.tsx +21 -17
  111. package/src/expiration-selector.tsx +84 -66
  112. package/src/field.tsx +68 -124
  113. package/src/greeks-display.tsx +59 -51
  114. package/src/heading.tsx +22 -26
  115. package/src/icon-button.tsx +134 -141
  116. package/src/ink.tsx +37 -0
  117. package/src/input-group.tsx +59 -36
  118. package/src/input.tsx +41 -32
  119. package/src/menu.tsx +224 -397
  120. package/src/option-chain.tsx +154 -104
  121. package/src/option-position.tsx +136 -114
  122. package/src/pin-input.tsx +37 -29
  123. package/src/pnl-diagram.tsx +36 -28
  124. package/src/popover.tsx +1 -1
  125. package/src/progress.tsx +17 -21
  126. package/src/radio.tsx +56 -75
  127. package/src/select.tsx +80 -59
  128. package/src/separator.tsx +13 -34
  129. package/src/slider.tsx +43 -44
  130. package/src/strategy-builder.tsx +245 -156
  131. package/src/switch.tsx +63 -64
  132. package/src/tabs.tsx +166 -296
  133. package/src/tag.tsx +89 -51
  134. package/src/textarea.tsx +19 -29
  135. package/src/toaster.tsx +118 -86
  136. package/src/tooltip.tsx +21 -13
  137. package/tokens.css +79 -0
package/src/menu.tsx CHANGED
@@ -1,41 +1,40 @@
1
1
  'use client';
2
2
 
3
- import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
4
- import * as React from 'react';
3
+ import { Menu, Text, View, XStack } from '@hanzo/gui';
5
4
  import { Check, ChevronRight } from 'lucide-react';
5
+ import * as React from 'react';
6
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
- }
7
+ import { ink } from './ink';
8
+
9
+ // The Lux menu, on gui's Menu the same fifteen parts Radix's dropdown has,
10
+ // under the names this package already published.
11
+ //
12
+ // One thing genuinely got simpler rather than merely moving. Radix splits
13
+ // position into `side` + `align`, so the old file carried a `parsePlacement`
14
+ // that took `'bottom-start'` apart into `{ side: 'bottom', align: 'start' }`,
15
+ // plus a context to carry the two halves down to the content. gui's Menu sits
16
+ // on Popper, which takes the floating-ui `placement` string whole and derives
17
+ // side and align itself. The parser, the context, and the four props they fed
18
+ // are all gone: the caller's string reaches the engine untouched.
19
+
20
+ const ITEM_HOVER = 'var(--color-popover-item-hover)';
21
+ const TEXT = 'var(--color-text-primary)';
22
+
23
+ /** Resting/highlighted look shared by every selectable row. */
24
+ const ROW = {
25
+ position: 'relative',
26
+ flexDirection: 'row',
27
+ alignItems: 'center',
28
+ cursor: 'pointer',
29
+ userSelect: 'none',
30
+ borderRadius: 6,
31
+ fontSize: 14,
32
+ backgroundColor: 'transparent',
33
+ hoverStyle: { backgroundColor: ITEM_HOVER },
34
+ focusStyle: { backgroundColor: ITEM_HOVER },
35
+ } as const;
37
36
 
38
- // --- Positioning context (mirrors popover.tsx pattern) ---
37
+ // --- MenuRoot ---
39
38
 
40
39
  interface Positioning {
41
40
  readonly placement?: string;
@@ -45,427 +44,255 @@ interface Positioning {
45
44
  };
46
45
  }
47
46
 
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
47
  export interface MenuRootProps {
65
48
  readonly children?: React.ReactNode;
66
49
  readonly open?: boolean;
67
50
  readonly defaultOpen?: boolean;
68
51
  readonly onOpenChange?: (details: { open: boolean }) => void;
69
52
  readonly positioning?: Positioning;
70
- readonly lazyMount?: boolean;
71
- readonly unmountOnExit?: boolean;
72
53
  readonly modal?: boolean;
73
54
  }
74
55
 
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
- };
56
+ export const MenuRoot = ({
57
+ children, open, defaultOpen, onOpenChange, positioning, modal = true,
58
+ }: MenuRootProps): React.ReactElement => (
59
+ <Menu
60
+ open={ open }
61
+ defaultOpen={ defaultOpen }
62
+ onOpenChange={ onOpenChange ? (isOpen: boolean) => onOpenChange({ open: isOpen }) : undefined }
63
+ modal={ modal }
64
+ placement={ (positioning?.placement ?? 'bottom-start') as never }
65
+ offset={ positioning?.offset?.mainAxis ?? 4 }
66
+ >
67
+ { children }
68
+ </Menu>
69
+ );
126
70
 
127
71
  // --- MenuTrigger ---
128
72
 
129
- export interface MenuTriggerProps extends React.ComponentPropsWithoutRef<'button'> {
130
- readonly asChild?: boolean;
131
- }
73
+ export type MenuTriggerProps = React.ComponentProps<typeof Menu.Trigger>;
132
74
 
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
- });
75
+ export const MenuTrigger = ({ asChild = false, ...rest }: MenuTriggerProps): React.ReactElement => (
76
+ <Menu.Trigger asChild={ asChild } { ...rest }/>
77
+ );
140
78
 
141
79
  // --- MenuContent ---
142
80
 
143
- export interface MenuContentProps extends React.ComponentPropsWithoutRef<'div'> {
81
+ export type MenuContentProps = React.ComponentProps<typeof Menu.Content> & {
144
82
  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
83
  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
- };
84
+ };
166
85
 
86
+ export const MenuContent = ({
87
+ portalled = true, minW, children, ...rest
88
+ }: MenuContentProps): React.ReactElement => {
167
89
  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 }
90
+ <Menu.Content
91
+ unstyled
92
+ zIndex={ 50 }
93
+ minWidth={ (minW ?? 128) as never }
94
+ overflow="hidden"
95
+ borderRadius={ 8 }
96
+ padding={ 4 }
97
+ borderWidth={ 1 }
98
+ borderColor={ 'var(--color-popover-border)' as never }
99
+ backgroundColor={ 'var(--color-popover-bg)' as never }
100
+ transition="quick"
101
+ enterStyle={{ opacity: 0, scale: 0.95 }}
102
+ exitStyle={{ opacity: 0, scale: 0.95 }}
187
103
  { ...rest }
188
- />
104
+ >
105
+ { children }
106
+ </Menu.Content>
189
107
  );
190
108
 
191
- if (!portalled) {
192
- return content;
193
- }
194
-
195
- return (
196
- <DropdownMenu.Portal container={ portalRef?.current ?? undefined }>
197
- { content }
198
- </DropdownMenu.Portal>
199
- );
200
- });
109
+ return portalled ? <Menu.Portal>{ content }</Menu.Portal> : content;
110
+ };
201
111
 
202
112
  // --- MenuItem ---
203
113
 
204
- export interface MenuItemProps extends React.ComponentPropsWithoutRef<'div'> {
114
+ export type MenuItemProps = React.ComponentProps<typeof Menu.Item> & {
205
115
 
206
- /** Informational value identifier (not used by Radix but kept for API compat) */
116
+ /** Informational identifier the row's own name, not read by the engine. */
207
117
  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
- }
118
+ };
214
119
 
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>) }
120
+ export const MenuItem = ({ children, value: _value, asChild, ...rest }: MenuItemProps): React.ReactElement => (
121
+ // `asChild` means the caller brought its own element and owns the look. Its
122
+ // props are then forwarded to that element verbatim, so the skin must NOT be
123
+ // applied: `unstyled` and friends are not DOM attributes and React rejects
124
+ // them out loud ("Received `true` for a non-boolean attribute `unstyled`").
125
+ // Two shapes, said once, rather than one shape that is wrong in one of them.
126
+ asChild ? (
127
+ <Menu.Item asChild { ...rest }>{ children }</Menu.Item>
128
+ ) : (
129
+ <Menu.Item
130
+ unstyled
131
+ { ...ROW }
132
+ gap={ 8 }
133
+ paddingHorizontal={ 8 }
134
+ paddingVertical={ 6 }
135
+ disabledStyle={{ opacity: 0.5, pointerEvents: 'none' }}
136
+ { ...rest }
235
137
  >
236
- { children }
237
- </DropdownMenu.Item>
238
- );
239
- });
240
-
241
- // --- MenuItemText ---
138
+ { ink(children, undefined, { fontSize: 14, color: TEXT }) }
139
+ </Menu.Item>
140
+ )
141
+ );
242
142
 
243
- export interface MenuItemTextProps extends React.ComponentPropsWithoutRef<'span'> {}
143
+ // --- MenuItemText / MenuItemCommand ---
244
144
 
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
- });
145
+ export type MenuItemTextProps = React.ComponentProps<typeof Text>;
252
146
 
253
- // --- MenuItemCommand ---
147
+ export const MenuItemText = (props: MenuItemTextProps): React.ReactElement => (
148
+ <Text flex={ 1 } fontSize={ 14 } color={ TEXT as never } { ...props }/>
149
+ );
254
150
 
255
- export interface MenuItemCommandProps extends React.ComponentPropsWithoutRef<'span'> {}
151
+ export type MenuItemCommandProps = React.ComponentProps<typeof Text>;
256
152
 
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
- });
153
+ /** The keyboard shortcut on the trailing edge of a row. */
154
+ export const MenuItemCommand = (props: MenuItemCommandProps): React.ReactElement => (
155
+ <Text marginLeft="auto" fontSize={ 12 } letterSpacing={ 1 } opacity={ 0.6 } color={ TEXT as never } { ...props }/>
156
+ );
270
157
 
271
158
  // --- MenuSeparator ---
272
159
 
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
- });
160
+ export type MenuSeparatorProps = React.ComponentProps<typeof Menu.Separator>;
161
+
162
+ export const MenuSeparator = (props: MenuSeparatorProps): React.ReactElement => (
163
+ <Menu.Separator
164
+ unstyled
165
+ height={ 1 }
166
+ marginVertical={ 4 }
167
+ marginHorizontal={ -4 }
168
+ backgroundColor={ 'var(--color-border-divider)' as never }
169
+ { ...props }
170
+ />
171
+ );
288
172
 
289
173
  // --- MenuItemGroup ---
290
174
 
291
- export interface MenuItemGroupProps extends React.ComponentPropsWithoutRef<'div'> {
175
+ export type MenuItemGroupProps = React.ComponentProps<typeof Menu.Group> & {
292
176
  readonly title?: string;
293
- }
177
+ };
294
178
 
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
- });
179
+ export const MenuItemGroup = ({ title, children, ...rest }: MenuItemGroupProps): React.ReactElement => (
180
+ <Menu.Group unstyled { ...rest }>
181
+ { title && (
182
+ <Menu.Label
183
+ unstyled
184
+ paddingHorizontal={ 8 }
185
+ paddingVertical={ 6 }
186
+ fontSize={ 12 }
187
+ fontWeight="600"
188
+ userSelect="none"
189
+ opacity={ 0.6 }
190
+ color={ TEXT as never }
191
+ >
192
+ { title }
193
+ </Menu.Label>
194
+ ) }
195
+ { children }
196
+ </Menu.Group>
197
+ );
311
198
 
312
199
  // --- MenuArrow ---
313
200
 
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
- <Check className="h-4 w-4"/>
360
- </DropdownMenu.ItemIndicator>
361
- </span>
362
- { children }
363
- </DropdownMenu.CheckboxItem>
364
- );
365
- });
201
+ export type MenuArrowProps = React.ComponentProps<typeof Menu.Arrow>;
366
202
 
367
- // --- MenuRadioItemGroup ---
368
-
369
- export interface MenuRadioItemGroupProps extends React.ComponentPropsWithoutRef<'div'> {
370
- readonly value?: string;
371
- readonly onValueChange?: (value: string) => void;
372
- }
203
+ export const MenuArrow = (props: MenuArrowProps): React.ReactElement => (
204
+ <Menu.Arrow backgroundColor={ 'var(--color-popover-bg)' as never } { ...props }/>
205
+ );
373
206
 
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
- });
207
+ // --- The indicator well, shared by checkbox and radio rows ---
388
208
 
389
- // --- MenuRadioItem ---
209
+ const Indicator = () => (
210
+ <View position="absolute" left={ 8 } width={ 14 } height={ 14 } alignItems="center" justifyContent="center">
211
+ <Menu.ItemIndicator>
212
+ <Check size={ 16 }/>
213
+ </Menu.ItemIndicator>
214
+ </View>
215
+ );
390
216
 
391
- export interface MenuRadioItemProps extends React.ComponentPropsWithoutRef<'div'> {
392
- readonly value: string;
393
- }
217
+ // --- MenuCheckboxItem ---
394
218
 
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
- <Check className="h-4 w-4"/>
418
- </DropdownMenu.ItemIndicator>
419
- </span>
420
- <span>{ children }</span>
421
- </DropdownMenu.RadioItem>
422
- );
423
- });
219
+ export type MenuCheckboxItemProps = React.ComponentProps<typeof Menu.CheckboxItem>;
220
+
221
+ export const MenuCheckboxItem = ({ children, ...rest }: MenuCheckboxItemProps): React.ReactElement => (
222
+ <Menu.CheckboxItem
223
+ unstyled
224
+ { ...ROW }
225
+ paddingLeft={ 32 }
226
+ paddingRight={ 8 }
227
+ paddingVertical={ 6 }
228
+ disabledStyle={{ opacity: 0.5, pointerEvents: 'none' }}
229
+ { ...rest }
230
+ >
231
+ <Indicator/>
232
+ { ink(children, undefined, { fontSize: 14, color: TEXT }) }
233
+ </Menu.CheckboxItem>
234
+ );
235
+
236
+ // --- MenuRadioItemGroup / MenuRadioItem ---
237
+
238
+ export type MenuRadioItemGroupProps = React.ComponentProps<typeof Menu.RadioGroup>;
239
+
240
+ export const MenuRadioItemGroup = (props: MenuRadioItemGroupProps): React.ReactElement => (
241
+ <Menu.RadioGroup { ...props }/>
242
+ );
243
+
244
+ export type MenuRadioItemProps = React.ComponentProps<typeof Menu.RadioItem>;
245
+
246
+ export const MenuRadioItem = ({ children, ...rest }: MenuRadioItemProps): React.ReactElement => (
247
+ <Menu.RadioItem
248
+ unstyled
249
+ { ...ROW }
250
+ paddingLeft={ 32 }
251
+ paddingRight={ 8 }
252
+ paddingVertical={ 6 }
253
+ disabledStyle={{ opacity: 0.5, pointerEvents: 'none' }}
254
+ { ...rest }
255
+ >
256
+ <Indicator/>
257
+ { ink(children, undefined, { fontSize: 14, color: TEXT }) }
258
+ </Menu.RadioItem>
259
+ );
424
260
 
425
261
  // --- 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
262
 
430
- export interface MenuContextTriggerProps extends React.ComponentPropsWithoutRef<'span'> {
263
+ export type MenuContextTriggerProps = React.ComponentProps<typeof View> & {
431
264
  readonly asChild?: boolean;
432
- }
265
+ };
433
266
 
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
- });
267
+ /**
268
+ * A right-click target. This menu is a DROPDOWN — it opens from its trigger, not
269
+ * from a pointer position — so the wrapper passes its children through. gui does
270
+ * ship a real `ContextMenu`; a surface that wants one should reach for that
271
+ * rather than have this quietly behave like it.
272
+ */
273
+ export const MenuContextTrigger = ({ asChild: _asChild, ...rest }: MenuContextTriggerProps): React.ReactElement => (
274
+ <View { ...rest }/>
275
+ );
441
276
 
442
- // --- MenuTriggerItem ---
277
+ // --- MenuTriggerItem (a submenu's row) ---
443
278
 
444
- export interface MenuTriggerItemProps extends React.ComponentPropsWithoutRef<'div'> {
279
+ export type MenuTriggerItemProps = React.ComponentProps<typeof Menu.SubTrigger> & {
445
280
  readonly startIcon?: React.ReactNode;
446
- }
281
+ };
447
282
 
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
- <ChevronRight className="h-4 w-4"/>
468
- </DropdownMenu.SubTrigger>
469
- </DropdownMenu.Sub>
470
- );
471
- });
283
+ export const MenuTriggerItem = ({ startIcon, children, ...rest }: MenuTriggerItemProps): React.ReactElement => (
284
+ <Menu.Sub>
285
+ <Menu.SubTrigger
286
+ unstyled
287
+ { ...ROW }
288
+ gap={ 8 }
289
+ paddingHorizontal={ 8 }
290
+ paddingVertical={ 6 }
291
+ { ...rest }
292
+ >
293
+ { startIcon }
294
+ <XStack flex={ 1 }>{ ink(children, undefined, { fontSize: 14, color: TEXT }) }</XStack>
295
+ <ChevronRight size={ 16 }/>
296
+ </Menu.SubTrigger>
297
+ </Menu.Sub>
298
+ );