@octanejs/shadcn 0.0.10 → 0.0.11

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.
@@ -0,0 +1,288 @@
1
+ // Base UI base menubar — the bar runs on @octanejs/base-ui's `Menubar`, and every menu inside it
2
+ // runs on `Menu`, the same primitive `dropdown-menu` uses. Class strings from the maintainer-supplied
3
+ // radix source.
4
+ //
5
+ // PART MAPPING. Radix nests the whole family under one `Menubar` namespace; Base UI has a `Menubar`
6
+ // component for the bar ONLY, and the menus are ordinary `Menu` parts:
7
+ //
8
+ // radix Menubar.Root / .Menu / .Trigger / .Content / .Item / .Sub / …
9
+ // Base UI Menubar + Menu.Root / .Trigger / .Portal > .Positioner > .Popup / .Item / …
10
+ //
11
+ // CARRIED OVER FROM dropdown-menu, re-verified against this composition:
12
+ // - `--radix-menubar-content-transform-origin` -> `--transform-origin`. A utility naming a
13
+ // variable nothing sets does nothing, silently.
14
+ // - The SUBMENU trigger marks itself open with `data-popup-open`, not the `data-open` the popup
15
+ // carries, so radix's `data-open:bg-accent` there would never match.
16
+ // - `Label` and `Separator` are plain host elements: `Menu.GroupLabel` throws outside a
17
+ // `Menu.Group` while shadcn's label is standalone, and the `Menu` namespace ships no Separator
18
+ // part at all (its sibling `ContextMenu` does).
19
+ // - Radix's `focus:` item utilities carry over, because Base UI moves real DOM focus onto the
20
+ // highlighted item.
21
+ // - Positioning props are routed to the Positioner, not swept onto the Popup where they are inert.
22
+ //
23
+ // THE BAR TRIGGER IS THE ONE PLACE THIS FAMILY DIFFERS from the dropdown. Its radix class keys off
24
+ // `aria-expanded:bg-muted` rather than a data attribute, and that carries over UNCHANGED — Base UI's
25
+ // trigger publishes `aria-expanded="true"` when its menu is open (verified by rendering). So the
26
+ // `data-popup-open` rewrite above applies only to the submenu trigger, not to this one.
27
+ import { type OctaneNode } from 'octane';
28
+ import { Menubar as MenubarPrimitive } from '@octanejs/base-ui/menubar';
29
+ import { Menu as MenuPrimitive } from '@octanejs/base-ui/menu';
30
+ import { CheckIcon, ChevronRightIcon } from '@octanejs/lucide';
31
+
32
+ import { cn } from '../../../lib/utils';
33
+
34
+ type Props = { className?: string; children?: OctaneNode } & Record<string, unknown>;
35
+
36
+ export interface BaseUiPositioningProps {
37
+ /** Forwarded to the Positioner; see the note at the top of this file. */
38
+ side?: 'top' | 'right' | 'bottom' | 'left';
39
+ sideOffset?: number;
40
+ align?: 'start' | 'center' | 'end';
41
+ alignOffset?: number;
42
+ anchor?: unknown;
43
+ positionMethod?: 'absolute' | 'fixed';
44
+ collisionBoundary?: unknown;
45
+ collisionPadding?: unknown;
46
+ collisionAvoidance?: unknown;
47
+ arrowPadding?: number;
48
+ sticky?: boolean;
49
+ disableAnchorTracking?: boolean;
50
+ }
51
+
52
+ export function Menubar({ className, ...props }: Props) @{
53
+ <MenubarPrimitive
54
+ data-slot="menubar"
55
+ className={cn('flex h-8 items-center gap-0.5 rounded-lg border p-[3px]', className)}
56
+ {...props}
57
+ />
58
+ }
59
+
60
+ export function MenubarMenu(props: Record<string, unknown>) @{
61
+ <MenuPrimitive.Root data-slot="menubar-menu" {...props} />
62
+ }
63
+
64
+ export function MenubarGroup(props: Record<string, unknown>) @{
65
+ <MenuPrimitive.Group data-slot="menubar-group" {...props} />
66
+ }
67
+
68
+ export function MenubarPortal(props: Record<string, unknown>) @{
69
+ <MenuPrimitive.Portal data-slot="menubar-portal" {...props} />
70
+ }
71
+
72
+ export function MenubarRadioGroup(props: Record<string, unknown>) @{
73
+ <MenuPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
74
+ }
75
+
76
+ export function MenubarTrigger({ className, ...props }: Props) @{
77
+ <MenuPrimitive.Trigger
78
+ data-slot="menubar-trigger"
79
+ className={cn(
80
+ 'flex items-center rounded-sm px-1.5 py-[2px] text-sm font-medium outline-hidden select-none hover:bg-muted aria-expanded:bg-muted',
81
+ className,
82
+ )}
83
+ {...props}
84
+ />
85
+ }
86
+
87
+ export function MenubarContent({
88
+ className,
89
+ align = 'start',
90
+ alignOffset = -4,
91
+ sideOffset = 8,
92
+ side,
93
+ anchor,
94
+ positionMethod,
95
+ collisionBoundary,
96
+ collisionPadding,
97
+ collisionAvoidance,
98
+ arrowPadding,
99
+ sticky,
100
+ disableAnchorTracking,
101
+ ...props
102
+ }: Props & BaseUiPositioningProps) @{
103
+ <MenuPrimitive.Portal>
104
+ <MenuPrimitive.Positioner
105
+ align={align}
106
+ alignOffset={alignOffset}
107
+ sideOffset={sideOffset}
108
+ side={side}
109
+ anchor={anchor}
110
+ positionMethod={positionMethod}
111
+ collisionBoundary={collisionBoundary}
112
+ collisionPadding={collisionPadding}
113
+ collisionAvoidance={collisionAvoidance}
114
+ arrowPadding={arrowPadding}
115
+ sticky={sticky}
116
+ disableAnchorTracking={disableAnchorTracking}
117
+ >
118
+ <MenuPrimitive.Popup
119
+ data-slot="menubar-content"
120
+ className={cn(
121
+ 'z-50 min-w-36 origin-(--transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95',
122
+ className,
123
+ )}
124
+ {...props}
125
+ />
126
+ </MenuPrimitive.Positioner>
127
+ </MenuPrimitive.Portal>
128
+ }
129
+
130
+ export function MenubarItem({ className, inset, variant = 'default', ...props }: Props & {
131
+ inset?: boolean;
132
+ variant?: 'default' | 'destructive';
133
+ }) @{
134
+ <MenuPrimitive.Item
135
+ data-slot="menubar-item"
136
+ data-inset={inset}
137
+ data-variant={variant}
138
+ className={cn(
139
+ 'group/menubar-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive!',
140
+ className,
141
+ )}
142
+ {...props}
143
+ />
144
+ }
145
+
146
+ export function MenubarCheckboxItem(props: Props & {
147
+ checked?: boolean;
148
+ inset?: boolean;
149
+ }) @{
150
+ const { className, children: _children, checked, inset, ...rest } = props;
151
+
152
+ <MenuPrimitive.CheckboxItem
153
+ data-slot="menubar-checkbox-item"
154
+ data-inset={inset}
155
+ className={cn(
156
+ 'relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-1.5 pl-7 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0',
157
+ className,
158
+ )}
159
+ checked={checked}
160
+ {...rest}
161
+ >
162
+ <span
163
+ className="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4"
164
+ >
165
+ <MenuPrimitive.CheckboxItemIndicator>
166
+ <CheckIcon />
167
+ </MenuPrimitive.CheckboxItemIndicator>
168
+ </span>
169
+ {props.children}
170
+ </MenuPrimitive.CheckboxItem>
171
+ }
172
+
173
+ export function MenubarRadioItem(props: Props & { inset?: boolean }) @{
174
+ const { className, children: _children, inset, ...rest } = props;
175
+
176
+ <MenuPrimitive.RadioItem
177
+ data-slot="menubar-radio-item"
178
+ data-inset={inset}
179
+ className={cn(
180
+ 'relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-1.5 pl-7 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
181
+ className,
182
+ )}
183
+ {...rest}
184
+ >
185
+ <span
186
+ className="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4"
187
+ >
188
+ <MenuPrimitive.RadioItemIndicator>
189
+ <CheckIcon />
190
+ </MenuPrimitive.RadioItemIndicator>
191
+ </span>
192
+ {props.children}
193
+ </MenuPrimitive.RadioItem>
194
+ }
195
+
196
+ export function MenubarLabel({ className, inset, ...props }: Props & { inset?: boolean }) @{
197
+ <div
198
+ data-slot="menubar-label"
199
+ data-inset={inset}
200
+ className={cn('px-1.5 py-1 text-sm font-medium data-inset:pl-7', className)}
201
+ {...props}
202
+ />
203
+ }
204
+
205
+ export function MenubarSeparator({ className, ...props }: Props) @{
206
+ <div
207
+ role="separator"
208
+ aria-orientation="horizontal"
209
+ data-slot="menubar-separator"
210
+ className={cn('-mx-1 my-1 h-px bg-border', className)}
211
+ {...props}
212
+ />
213
+ }
214
+
215
+ export function MenubarShortcut({ className, ...props }: Props) @{
216
+ <span
217
+ data-slot="menubar-shortcut"
218
+ className={cn(
219
+ 'ml-auto text-xs tracking-widest text-muted-foreground group-focus/menubar-item:text-accent-foreground',
220
+ className,
221
+ )}
222
+ {...props}
223
+ />
224
+ }
225
+
226
+ export function MenubarSub(props: Record<string, unknown>) @{
227
+ <MenuPrimitive.SubmenuRoot data-slot="menubar-sub" {...props} />
228
+ }
229
+
230
+ export function MenubarSubTrigger(props: Props & { inset?: boolean }) @{
231
+ const { className, children: _children, inset, ...rest } = props;
232
+
233
+ <MenuPrimitive.SubmenuTrigger
234
+ data-slot="menubar-sub-trigger"
235
+ data-inset={inset}
236
+ className={cn(
237
+ 'flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-none select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground [&_svg:not([class*=\'size-\'])]:size-4',
238
+ className,
239
+ )}
240
+ {...rest}
241
+ >
242
+ {props.children}
243
+ <ChevronRightIcon className="cn-rtl-flip ml-auto" />
244
+ </MenuPrimitive.SubmenuTrigger>
245
+ }
246
+
247
+ export function MenubarSubContent({
248
+ className,
249
+ side,
250
+ sideOffset,
251
+ align,
252
+ alignOffset,
253
+ anchor,
254
+ positionMethod,
255
+ collisionBoundary,
256
+ collisionPadding,
257
+ collisionAvoidance,
258
+ arrowPadding,
259
+ sticky,
260
+ disableAnchorTracking,
261
+ ...props
262
+ }: Props & BaseUiPositioningProps) @{
263
+ <MenuPrimitive.Portal>
264
+ <MenuPrimitive.Positioner
265
+ side={side}
266
+ sideOffset={sideOffset}
267
+ align={align}
268
+ alignOffset={alignOffset}
269
+ anchor={anchor}
270
+ positionMethod={positionMethod}
271
+ collisionBoundary={collisionBoundary}
272
+ collisionPadding={collisionPadding}
273
+ collisionAvoidance={collisionAvoidance}
274
+ arrowPadding={arrowPadding}
275
+ sticky={sticky}
276
+ disableAnchorTracking={disableAnchorTracking}
277
+ >
278
+ <MenuPrimitive.Popup
279
+ data-slot="menubar-sub-content"
280
+ className={cn(
281
+ 'z-50 min-w-32 origin-(--transform-origin) overflow-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
282
+ className,
283
+ )}
284
+ {...props}
285
+ />
286
+ </MenuPrimitive.Positioner>
287
+ </MenuPrimitive.Portal>
288
+ }