@octanejs/shadcn 0.0.8 → 0.0.9

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,292 @@
1
+ // Base UI base dropdown-menu — runs on @octanejs/base-ui's `Menu`. Class strings come from the
2
+ // maintainer-supplied radix source; every difference below was verified by rendering the primitive
3
+ // and reading the DOM, not inferred.
4
+ //
5
+ // PART TREE. Radix's `Content` is one element; Base UI splits positioning out, exactly as this
6
+ // base's popover already does:
7
+ //
8
+ // radix Root > Portal > Content
9
+ // Base UI Root > Portal > Positioner > Popup (Positioner owns align/sideOffset)
10
+ //
11
+ // THREE CSS VARIABLES ARE RENAMED. Radix publishes per-component names; Base UI's Positioner
12
+ // publishes generic ones, and a utility pointing at a variable nothing sets silently does nothing:
13
+ //
14
+ // --radix-dropdown-menu-content-available-height -> --available-height
15
+ // --radix-dropdown-menu-trigger-width -> --anchor-width
16
+ // --radix-dropdown-menu-content-transform-origin -> --transform-origin
17
+ //
18
+ // THE SUBMENU TRIGGER'S OPEN ATTRIBUTE DIFFERS. Base UI marks an open trigger with
19
+ // `data-popup-open` (triggerOpenStateMapping), not the `data-open` the popup itself carries — so
20
+ // radix's `data-open:bg-accent` on the sub-trigger would never match and an open submenu's parent
21
+ // row would not stay highlighted.
22
+ //
23
+ // WHAT DOES CARRY OVER UNCHANGED, and is worth recording because it looks like it should not:
24
+ // radix styles item highlight with `focus:`, which works here because Base UI moves REAL DOM focus
25
+ // onto the highlighted item (checked by driving ArrowDown and reading document.activeElement) as
26
+ // well as publishing `data-highlighted`. `data-side`, `data-align`, `data-open`, `data-closed` and
27
+ // `data-disabled` are all emitted with the same spellings radix uses.
28
+ //
29
+ // TWO PARTS ARE PLAIN HOST ELEMENTS ON PURPOSE:
30
+ // - `Label` is a `<div>`, NOT `Menu.GroupLabel`. That part calls useMenuGroupRootContext and
31
+ // THROWS "MenuGroupContext is missing" outside a `Menu.Group`, while shadcn's label is
32
+ // routinely used standalone. Same trap as `Field.Label`, which already shipped as a crash once.
33
+ // - `Separator` is a `<div>`, because the Menu namespace has no Separator part at all (its
34
+ // sibling ContextMenu does). The radix classes are pure styling, so nothing is lost.
35
+ // POSITIONING PROPS BELONG TO THE POSITIONER, NOT THE POPUP. Radix has one `Content` element that
36
+ // takes them all; Base UI splits positioning into its own layer, so anything forwarded to `Popup`
37
+ // instead is inert — `side="top"` silently leaves the menu on the default side, and both `side` and
38
+ // `alignOffset` land on the DOM as invalid attributes. They are therefore destructured and routed
39
+ // explicitly rather than swept up by the rest spread.
40
+ import { type OctaneNode } from 'octane';
41
+ import { Menu as MenuPrimitive } from '@octanejs/base-ui/menu';
42
+ import { CheckIcon, ChevronRightIcon } from '@octanejs/lucide';
43
+
44
+ import { cn } from '../../../lib/utils';
45
+
46
+ type Props = { className?: string; children?: OctaneNode } & Record<string, unknown>;
47
+
48
+ export function DropdownMenu(props: Record<string, unknown>) @{
49
+ <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
50
+ }
51
+
52
+ export function DropdownMenuPortal(props: Record<string, unknown>) @{
53
+ <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
54
+ }
55
+
56
+ export function DropdownMenuTrigger(props: Record<string, unknown>) @{
57
+ <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
58
+ }
59
+
60
+ export interface BaseUiPositioningProps {
61
+ /** Forwarded to the Positioner; see the note at the top of this file. */
62
+ side?: 'top' | 'right' | 'bottom' | 'left';
63
+ sideOffset?: number;
64
+ align?: 'start' | 'center' | 'end';
65
+ alignOffset?: number;
66
+ anchor?: unknown;
67
+ positionMethod?: 'absolute' | 'fixed';
68
+ collisionBoundary?: unknown;
69
+ collisionPadding?: unknown;
70
+ collisionAvoidance?: unknown;
71
+ arrowPadding?: number;
72
+ sticky?: boolean;
73
+ disableAnchorTracking?: boolean;
74
+ }
75
+
76
+ export interface DropdownMenuContentProps extends Props, BaseUiPositioningProps {}
77
+
78
+ export function DropdownMenuContent({
79
+ className,
80
+ align = 'start',
81
+ sideOffset = 4,
82
+ side,
83
+ alignOffset,
84
+ anchor,
85
+ positionMethod,
86
+ collisionBoundary,
87
+ collisionPadding,
88
+ collisionAvoidance,
89
+ arrowPadding,
90
+ sticky,
91
+ disableAnchorTracking,
92
+ ...props
93
+ }: DropdownMenuContentProps) @{
94
+ <MenuPrimitive.Portal>
95
+ <MenuPrimitive.Positioner
96
+ side={side}
97
+ sideOffset={sideOffset}
98
+ align={align}
99
+ alignOffset={alignOffset}
100
+ anchor={anchor}
101
+ positionMethod={positionMethod}
102
+ collisionBoundary={collisionBoundary}
103
+ collisionPadding={collisionPadding}
104
+ collisionAvoidance={collisionAvoidance}
105
+ arrowPadding={arrowPadding}
106
+ sticky={sticky}
107
+ disableAnchorTracking={disableAnchorTracking}
108
+ >
109
+ <MenuPrimitive.Popup
110
+ data-slot="dropdown-menu-content"
111
+ className={cn(
112
+ 'z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto 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-closed:overflow-hidden 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',
113
+ className,
114
+ )}
115
+ {...props}
116
+ />
117
+ </MenuPrimitive.Positioner>
118
+ </MenuPrimitive.Portal>
119
+ }
120
+
121
+ export function DropdownMenuGroup(props: Record<string, unknown>) @{
122
+ <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
123
+ }
124
+
125
+ export function DropdownMenuItem({ className, inset, variant = 'default', ...props }: Props & {
126
+ inset?: boolean;
127
+ variant?: 'default' | 'destructive';
128
+ }) @{
129
+ <MenuPrimitive.Item
130
+ data-slot="dropdown-menu-item"
131
+ data-inset={inset}
132
+ data-variant={variant}
133
+ className={cn(
134
+ 'group/dropdown-menu-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',
135
+ className,
136
+ )}
137
+ {...props}
138
+ />
139
+ }
140
+
141
+ export function DropdownMenuCheckboxItem(props: Props & {
142
+ checked?: boolean;
143
+ inset?: boolean;
144
+ }) @{
145
+ const { className, children: _children, checked, inset, ...rest } = props;
146
+
147
+ <MenuPrimitive.CheckboxItem
148
+ data-slot="dropdown-menu-checkbox-item"
149
+ data-inset={inset}
150
+ className={cn(
151
+ 'relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 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',
152
+ className,
153
+ )}
154
+ checked={checked}
155
+ {...rest}
156
+ >
157
+ <span
158
+ className="pointer-events-none absolute right-2 flex items-center justify-center"
159
+ data-slot="dropdown-menu-checkbox-item-indicator"
160
+ >
161
+ <MenuPrimitive.CheckboxItemIndicator>
162
+ <CheckIcon />
163
+ </MenuPrimitive.CheckboxItemIndicator>
164
+ </span>
165
+ {props.children}
166
+ </MenuPrimitive.CheckboxItem>
167
+ }
168
+
169
+ export function DropdownMenuRadioGroup(props: Record<string, unknown>) @{
170
+ <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />
171
+ }
172
+
173
+ export function DropdownMenuRadioItem(props: Props & { inset?: boolean }) @{
174
+ const { className, children: _children, inset, ...rest } = props;
175
+
176
+ <MenuPrimitive.RadioItem
177
+ data-slot="dropdown-menu-radio-item"
178
+ data-inset={inset}
179
+ className={cn(
180
+ 'relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 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 right-2 flex items-center justify-center"
187
+ data-slot="dropdown-menu-radio-item-indicator"
188
+ >
189
+ <MenuPrimitive.RadioItemIndicator>
190
+ <CheckIcon />
191
+ </MenuPrimitive.RadioItemIndicator>
192
+ </span>
193
+ {props.children}
194
+ </MenuPrimitive.RadioItem>
195
+ }
196
+
197
+ export function DropdownMenuLabel({ className, inset, ...props }: Props & { inset?: boolean }) @{
198
+ <div
199
+ data-slot="dropdown-menu-label"
200
+ data-inset={inset}
201
+ className={cn(
202
+ 'px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7',
203
+ className,
204
+ )}
205
+ {...props}
206
+ />
207
+ }
208
+
209
+ export function DropdownMenuSeparator({ className, ...props }: Props) @{
210
+ <div
211
+ role="separator"
212
+ aria-orientation="horizontal"
213
+ data-slot="dropdown-menu-separator"
214
+ className={cn('-mx-1 my-1 h-px bg-border', className)}
215
+ {...props}
216
+ />
217
+ }
218
+
219
+ export function DropdownMenuShortcut({ className, ...props }: Props) @{
220
+ <span
221
+ data-slot="dropdown-menu-shortcut"
222
+ className={cn(
223
+ 'ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground',
224
+ className,
225
+ )}
226
+ {...props}
227
+ />
228
+ }
229
+
230
+ export function DropdownMenuSub(props: Record<string, unknown>) @{
231
+ <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />
232
+ }
233
+
234
+ export function DropdownMenuSubTrigger(props: Props & { inset?: boolean }) @{
235
+ const { className, children: _children, inset, ...rest } = props;
236
+
237
+ <MenuPrimitive.SubmenuTrigger
238
+ data-slot="dropdown-menu-sub-trigger"
239
+ data-inset={inset}
240
+ className={cn(
241
+ '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-popup-open:bg-accent data-popup-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
242
+ className,
243
+ )}
244
+ {...rest}
245
+ >
246
+ {props.children}
247
+ <ChevronRightIcon className="cn-rtl-flip ml-auto" />
248
+ </MenuPrimitive.SubmenuTrigger>
249
+ }
250
+
251
+ export function DropdownMenuSubContent({
252
+ className,
253
+ align,
254
+ sideOffset,
255
+ side,
256
+ alignOffset,
257
+ anchor,
258
+ positionMethod,
259
+ collisionBoundary,
260
+ collisionPadding,
261
+ collisionAvoidance,
262
+ arrowPadding,
263
+ sticky,
264
+ disableAnchorTracking,
265
+ ...props
266
+ }: Props & BaseUiPositioningProps) @{
267
+ <MenuPrimitive.Portal>
268
+ <MenuPrimitive.Positioner
269
+ side={side}
270
+ sideOffset={sideOffset}
271
+ align={align}
272
+ alignOffset={alignOffset}
273
+ anchor={anchor}
274
+ positionMethod={positionMethod}
275
+ collisionBoundary={collisionBoundary}
276
+ collisionPadding={collisionPadding}
277
+ collisionAvoidance={collisionAvoidance}
278
+ arrowPadding={arrowPadding}
279
+ sticky={sticky}
280
+ disableAnchorTracking={disableAnchorTracking}
281
+ >
282
+ <MenuPrimitive.Popup
283
+ data-slot="dropdown-menu-sub-content"
284
+ className={cn(
285
+ 'z-50 min-w-[96px] 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',
286
+ className,
287
+ )}
288
+ {...props}
289
+ />
290
+ </MenuPrimitive.Positioner>
291
+ </MenuPrimitive.Portal>
292
+ }
@@ -0,0 +1,201 @@
1
+ // Base UI base field — TRANSCRIBED from upstream's `bases/base-ui/ui/field.tsx` (maintainer-supplied),
2
+ // class strings verbatim. Byte-for-byte the same component as the radix base apart from which
3
+ // `Label` and `Separator` it imports.
4
+ //
5
+ // UPSTREAM DOES NOT USE THE `Field` PRIMITIVE HERE, even though @octanejs/base-ui ships one whose
6
+ // parts line up exactly (Root/Control/Label/Description/Error/Validity/Item). That is worth stating
7
+ // because the opposite is the natural guess, and it decides the class strings: `data-invalid` and
8
+ // `data-disabled` on these elements are written by the CONSUMER, so `data-[invalid=true]:` and
9
+ // `group-data-[disabled=true]/field:` are correct as written. Had this routed through `Field.Root`,
10
+ // the primitive would emit BARE `data-invalid=""` (see fieldValidityMapping) and every one of those
11
+ // variants would silently match nothing.
12
+ //
13
+ // It also means `FieldLabel` is safe standalone: it renders this base's plain `<label>`, not
14
+ // `Field.Label`, which hard-requires a `<Field.Root>` ancestor and throws without one.
15
+ //
16
+ // Octane adaptations: "use client" dropped; Label/Separator come from this base's own ports;
17
+ // FieldSeparator and FieldError compose their conditional children with createElement (a
18
+ // `children && …` branch beside a sibling descriptor), everything else is template JSX.
19
+ import { createElement, useMemo, type OctaneNode } from 'octane';
20
+ import { cva, type VariantProps } from 'class-variance-authority';
21
+
22
+ import { cn } from '../../../lib/utils';
23
+ import { Label } from './label.tsrx';
24
+ import { Separator } from './separator.tsrx';
25
+
26
+ type Props = { className?: string; children?: OctaneNode } & Record<string, unknown>;
27
+
28
+ export function FieldSet({ className, ...props }: Props) @{
29
+ <fieldset
30
+ data-slot="field-set"
31
+ className={cn(
32
+ 'flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
33
+ className,
34
+ )}
35
+ {...props}
36
+ />
37
+ }
38
+
39
+ export function FieldLegend({ className, variant = 'legend', ...props }: Props & {
40
+ variant?: 'legend' | 'label';
41
+ }) @{
42
+ <legend
43
+ data-slot="field-legend"
44
+ data-variant={variant}
45
+ className={cn(
46
+ 'mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base',
47
+ className,
48
+ )}
49
+ {...props}
50
+ />
51
+ }
52
+
53
+ export function FieldGroup({ className, ...props }: Props) @{
54
+ <div
55
+ data-slot="field-group"
56
+ className={cn(
57
+ 'group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4',
58
+ className,
59
+ )}
60
+ {...props}
61
+ />
62
+ }
63
+
64
+ export const fieldVariants = cva(
65
+ 'group/field flex w-full gap-2 data-[invalid=true]:text-destructive',
66
+ {
67
+ variants: {
68
+ orientation: {
69
+ vertical: 'flex-col *:w-full [&>.sr-only]:w-auto',
70
+ horizontal: 'flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
71
+ responsive: 'flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
72
+ },
73
+ },
74
+ defaultVariants: {
75
+ orientation: 'vertical',
76
+ },
77
+ },
78
+ );
79
+
80
+ export function Field({ className, orientation = 'vertical', ...props }: Props & {
81
+ orientation?: VariantProps<typeof fieldVariants>['orientation'];
82
+ }) @{
83
+ <div
84
+ role="group"
85
+ data-slot="field"
86
+ data-orientation={orientation}
87
+ className={cn(fieldVariants({ orientation }), className)}
88
+ {...props}
89
+ />
90
+ }
91
+
92
+ export function FieldContent({ className, ...props }: Props) @{
93
+ <div
94
+ data-slot="field-content"
95
+ className={cn('group/field-content flex flex-1 flex-col gap-0.5 leading-snug', className)}
96
+ {...props}
97
+ />
98
+ }
99
+
100
+ export function FieldLabel({ className, ...props }: Props) @{
101
+ <Label
102
+ data-slot="field-label"
103
+ className={cn(
104
+ 'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10',
105
+ 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col',
106
+ className,
107
+ )}
108
+ {...props}
109
+ />
110
+ }
111
+
112
+ export function FieldTitle({ className, ...props }: Props) @{
113
+ <div
114
+ data-slot="field-label"
115
+ className={cn(
116
+ 'flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50',
117
+ className,
118
+ )}
119
+ {...props}
120
+ />
121
+ }
122
+
123
+ export function FieldDescription({ className, ...props }: Props) @{
124
+ <p
125
+ data-slot="field-description"
126
+ className={cn(
127
+ 'text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5',
128
+ 'last:mt-0 nth-last-2:-mt-1',
129
+ '[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
130
+ className,
131
+ )}
132
+ {...props}
133
+ />
134
+ }
135
+
136
+ export function FieldSeparator({ children, className, ...props }: Props) {
137
+ return createElement(
138
+ 'div',
139
+ {
140
+ 'data-slot': 'field-separator',
141
+ 'data-content': !!children,
142
+ className: cn(
143
+ 'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
144
+ className,
145
+ ),
146
+ ...props,
147
+ },
148
+ createElement(Separator, { key: 'rule', className: 'absolute inset-0 top-1/2' }),
149
+ children
150
+ ? createElement(
151
+ 'span',
152
+ {
153
+ key: 'content',
154
+ className: 'relative mx-auto block w-fit bg-background px-2 text-muted-foreground',
155
+ 'data-slot': 'field-separator-content',
156
+ },
157
+ children,
158
+ )
159
+ : null,
160
+ );
161
+ }
162
+
163
+ export function FieldError(props: Props & { errors?: Array<{ message?: string } | undefined> }) {
164
+ const { className, children: _children, errors: _errors, ...rest } = props;
165
+ // Read through `props` inside the callback: octane's destructured-parameter
166
+ // locals are not the value channel the compiler threads into a closure.
167
+ const content = useMemo(() => {
168
+ if (props.children) return props.children;
169
+ const errors = props.errors;
170
+ if (!errors?.length) return null;
171
+
172
+ const uniqueErrors = [
173
+ ...new Map(errors.map((error) => [error?.message, error])).values(),
174
+ ];
175
+
176
+ if (uniqueErrors?.length == 1) return uniqueErrors[0]?.message;
177
+
178
+ return createElement(
179
+ 'ul',
180
+ { className: 'ml-4 flex list-disc flex-col gap-1' },
181
+ uniqueErrors.map(
182
+ (error, index) => error?.message
183
+ ? createElement('li', { key: index }, error.message)
184
+ : null,
185
+ ),
186
+ );
187
+ });
188
+
189
+ if (!content) return null;
190
+
191
+ return createElement(
192
+ 'div',
193
+ {
194
+ role: 'alert',
195
+ 'data-slot': 'field-error',
196
+ className: cn('text-sm font-normal text-destructive', className),
197
+ ...rest,
198
+ },
199
+ content,
200
+ );
201
+ }
@@ -0,0 +1,163 @@
1
+ // Base UI base item — class strings from the maintainer-supplied radix source, verbatim. Ten of the
2
+ // eleven parts are plain host elements, and the eleventh (`ItemSeparator`) delegates to this base's
3
+ // own Separator, so it picks up Base UI's `aria-orientation` dialect automatically.
4
+ //
5
+ // NO DIALECT RISK IN THE VARIANTS, which is worth stating because they look like the ones that
6
+ // caught toggle and slider: `data-[size=…]` and `data-[variant=…]` here read attributes THIS
7
+ // component writes itself, not state emitted by a primitive. Nothing underneath emits anything.
8
+ //
9
+ // `Item` SHIPS WITHOUT `asChild`, the same call as badge and breadcrumb in this base. Radix swaps in
10
+ // `Slot`; React Aria takes a `render` function; Base UI has no Slot, and item has no primitive to
11
+ // borrow a `render` prop from, so nothing here settles the spelling upstream uses. Adding it later
12
+ // is additive; shipping the wrong shape is breaking. A consumer needing a different element can
13
+ // apply `itemVariants({ variant, size })` directly, which is what the class export is for.
14
+ import { cva, type VariantProps } from 'class-variance-authority';
15
+
16
+ import { cn } from '../../../lib/utils';
17
+ import { Separator } from './separator.tsrx';
18
+
19
+ type DivProps = { className?: string } & Record<string, unknown>;
20
+
21
+ export function ItemGroup({ className, ...props }: DivProps) @{
22
+ <div
23
+ role="list"
24
+ data-slot="item-group"
25
+ className={cn(
26
+ 'group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2',
27
+ className,
28
+ )}
29
+ {...props}
30
+ />
31
+ }
32
+
33
+ export function ItemSeparator({ className, ...props }: DivProps) @{
34
+ <Separator
35
+ data-slot="item-separator"
36
+ orientation="horizontal"
37
+ className={cn('my-2', className)}
38
+ {...props}
39
+ />
40
+ }
41
+
42
+ export const itemVariants = cva(
43
+ 'group/item flex w-full flex-wrap items-center rounded-lg border text-sm transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-muted',
44
+ {
45
+ variants: {
46
+ variant: {
47
+ default: 'border-transparent',
48
+ outline: 'border-border',
49
+ muted: 'border-transparent bg-muted/50',
50
+ },
51
+ size: {
52
+ default: 'gap-2.5 px-3 py-2.5',
53
+ sm: 'gap-2.5 px-3 py-2.5',
54
+ xs: 'gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0',
55
+ },
56
+ },
57
+ defaultVariants: {
58
+ variant: 'default',
59
+ size: 'default',
60
+ },
61
+ },
62
+ );
63
+
64
+ export interface ItemProps extends Record<string, unknown> {
65
+ className?: string;
66
+ /** Not supported in this base — see the header. Declared so it fails to compile, not silently. */
67
+ asChild?: never;
68
+ variant?: VariantProps<typeof itemVariants>['variant'];
69
+ size?: VariantProps<typeof itemVariants>['size'];
70
+ }
71
+
72
+ export function Item({ className, variant = 'default', size = 'default', ...props }: ItemProps) @{
73
+ <div
74
+ data-slot="item"
75
+ data-variant={variant}
76
+ data-size={size}
77
+ className={cn(itemVariants({ variant, size, className }))}
78
+ {...props}
79
+ />
80
+ }
81
+
82
+ export const itemMediaVariants = cva(
83
+ 'flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none',
84
+ {
85
+ variants: {
86
+ variant: {
87
+ default: 'bg-transparent',
88
+ icon: '[&_svg:not([class*=\'size-\'])]:size-4',
89
+ image: 'size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover',
90
+ },
91
+ },
92
+ defaultVariants: {
93
+ variant: 'default',
94
+ },
95
+ },
96
+ );
97
+
98
+ export interface ItemMediaProps extends Record<string, unknown> {
99
+ className?: string;
100
+ variant?: VariantProps<typeof itemMediaVariants>['variant'];
101
+ }
102
+
103
+ export function ItemMedia({ className, variant = 'default', ...props }: ItemMediaProps) @{
104
+ <div
105
+ data-slot="item-media"
106
+ data-variant={variant}
107
+ className={cn(itemMediaVariants({ variant, className }))}
108
+ {...props}
109
+ />
110
+ }
111
+
112
+ export function ItemContent({ className, ...props }: DivProps) @{
113
+ <div
114
+ data-slot="item-content"
115
+ className={cn(
116
+ 'flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none',
117
+ className,
118
+ )}
119
+ {...props}
120
+ />
121
+ }
122
+
123
+ export function ItemTitle({ className, ...props }: DivProps) @{
124
+ <div
125
+ data-slot="item-title"
126
+ className={cn(
127
+ 'line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4',
128
+ className,
129
+ )}
130
+ {...props}
131
+ />
132
+ }
133
+
134
+ export function ItemDescription({ className, ...props }: DivProps) @{
135
+ <p
136
+ data-slot="item-description"
137
+ className={cn(
138
+ 'line-clamp-2 text-left text-sm leading-normal font-normal text-muted-foreground group-data-[size=xs]/item:text-xs [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
139
+ className,
140
+ )}
141
+ {...props}
142
+ />
143
+ }
144
+
145
+ export function ItemActions({ className, ...props }: DivProps) @{
146
+ <div data-slot="item-actions" className={cn('flex items-center gap-2', className)} {...props} />
147
+ }
148
+
149
+ export function ItemHeader({ className, ...props }: DivProps) @{
150
+ <div
151
+ data-slot="item-header"
152
+ className={cn('flex basis-full items-center justify-between gap-2', className)}
153
+ {...props}
154
+ />
155
+ }
156
+
157
+ export function ItemFooter({ className, ...props }: DivProps) @{
158
+ <div
159
+ data-slot="item-footer"
160
+ className={cn('flex basis-full items-center justify-between gap-2', className)}
161
+ {...props}
162
+ />
163
+ }