@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.
- package/README.md +170 -3
- package/package.json +25 -8
- package/src/bases/base-ui/ui/avatar.tsrx +89 -0
- package/src/bases/base-ui/ui/badge.tsrx +56 -0
- package/src/bases/base-ui/ui/breadcrumb.tsrx +109 -0
- package/src/bases/base-ui/ui/collapsible.tsrx +25 -0
- package/src/bases/base-ui/ui/dropdown-menu.tsrx +292 -0
- package/src/bases/base-ui/ui/field.tsrx +201 -0
- package/src/bases/base-ui/ui/item.tsrx +163 -0
- package/src/bases/base-ui/ui/pagination.tsrx +125 -0
- package/src/bases/base-ui/ui/popover.tsrx +43 -4
- package/src/bases/base-ui/ui/progress.tsrx +46 -0
- package/src/bases/base-ui/ui/radio-group.tsrx +14 -2
- package/src/bases/base-ui/ui/sheet.tsrx +121 -0
- package/src/bases/base-ui/ui/slider.tsrx +111 -0
- package/src/bases/base-ui/ui/table.tsrx +85 -0
- package/src/bases/base-ui/ui/toggle-group.tsrx +124 -0
- package/src/bases/base-ui/ui/toggle.tsrx +57 -0
- package/src/bases/base-ui/ui/tooltip.tsrx +39 -1
|
@@ -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
|
+
}
|