@octanejs/shadcn 0.0.7 → 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.
Files changed (37) hide show
  1. package/README.md +170 -3
  2. package/package.json +46 -7
  3. package/src/bases/base-ui/ui/accordion.tsrx +71 -0
  4. package/src/bases/base-ui/ui/alert-dialog.tsrx +149 -0
  5. package/src/bases/base-ui/ui/alert.tsrx +68 -0
  6. package/src/bases/base-ui/ui/aspect-ratio.tsrx +24 -0
  7. package/src/bases/base-ui/ui/avatar.tsrx +89 -0
  8. package/src/bases/base-ui/ui/badge.tsrx +56 -0
  9. package/src/bases/base-ui/ui/breadcrumb.tsrx +109 -0
  10. package/src/bases/base-ui/ui/button.tsrx +69 -0
  11. package/src/bases/base-ui/ui/card.tsrx +76 -0
  12. package/src/bases/base-ui/ui/checkbox.tsrx +43 -0
  13. package/src/bases/base-ui/ui/collapsible.tsrx +25 -0
  14. package/src/bases/base-ui/ui/dialog.tsrx +137 -0
  15. package/src/bases/base-ui/ui/dropdown-menu.tsrx +292 -0
  16. package/src/bases/base-ui/ui/empty.tsrx +88 -0
  17. package/src/bases/base-ui/ui/field.tsrx +201 -0
  18. package/src/bases/base-ui/ui/input.tsrx +28 -0
  19. package/src/bases/base-ui/ui/item.tsrx +163 -0
  20. package/src/bases/base-ui/ui/kbd.tsrx +27 -0
  21. package/src/bases/base-ui/ui/label.tsrx +31 -0
  22. package/src/bases/base-ui/ui/native-select.tsrx +54 -0
  23. package/src/bases/base-ui/ui/pagination.tsrx +125 -0
  24. package/src/bases/base-ui/ui/popover.tsrx +124 -0
  25. package/src/bases/base-ui/ui/progress.tsrx +46 -0
  26. package/src/bases/base-ui/ui/radio-group.tsrx +62 -0
  27. package/src/bases/base-ui/ui/separator.tsrx +34 -0
  28. package/src/bases/base-ui/ui/sheet.tsrx +121 -0
  29. package/src/bases/base-ui/ui/skeleton.tsrx +17 -0
  30. package/src/bases/base-ui/ui/slider.tsrx +111 -0
  31. package/src/bases/base-ui/ui/spinner.tsrx +30 -0
  32. package/src/bases/base-ui/ui/switch.tsrx +43 -0
  33. package/src/bases/base-ui/ui/table.tsrx +85 -0
  34. package/src/bases/base-ui/ui/textarea.tsrx +22 -0
  35. package/src/bases/base-ui/ui/toggle-group.tsrx +124 -0
  36. package/src/bases/base-ui/ui/toggle.tsrx +57 -0
  37. package/src/bases/base-ui/ui/tooltip.tsrx +111 -0
@@ -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,88 @@
1
+ // Base UI base empty — presentational, no headless primitive in any base.
2
+ //
3
+ // UNVERIFIED PROVENANCE: derived from this package's React Aria base rather than transcribed
4
+ // from upstream's `bases/base-ui/ui/empty.tsx`, which was not available at port time.
5
+ // Diff the class strings against the upstream source before treating this as ported.
6
+ import { cva, type VariantProps } from 'class-variance-authority';
7
+
8
+ import { cn } from '../../../lib/utils';
9
+ import type { DivProps, HostProps } from '../../../lib/types';
10
+
11
+ export function Empty({ className, ...props }: DivProps) @{
12
+ <div
13
+ data-slot="empty"
14
+ className={cn(
15
+ 'flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance',
16
+ className,
17
+ )}
18
+ {...props}
19
+ />
20
+ }
21
+
22
+ export function EmptyHeader({ className, ...props }: DivProps) @{
23
+ <div
24
+ data-slot="empty-header"
25
+ className={cn('flex max-w-sm flex-col items-center gap-2', className)}
26
+ {...props}
27
+ />
28
+ }
29
+
30
+ export const emptyMediaVariants = cva(
31
+ 'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
32
+ {
33
+ variants: {
34
+ variant: {
35
+ default: 'bg-transparent',
36
+ icon: 'flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*=\'size-\'])]:size-4',
37
+ },
38
+ },
39
+ defaultVariants: {
40
+ variant: 'default',
41
+ },
42
+ },
43
+ );
44
+
45
+ export interface EmptyMediaProps extends DivProps {
46
+ variant?: VariantProps<typeof emptyMediaVariants>['variant'];
47
+ }
48
+
49
+ export function EmptyMedia({ className, variant = 'default', ...props }: EmptyMediaProps) @{
50
+ <div
51
+ data-slot="empty-icon"
52
+ data-variant={variant}
53
+ className={cn(emptyMediaVariants({ variant, className }))}
54
+ {...props}
55
+ />
56
+ }
57
+
58
+ export function EmptyTitle({ className, ...props }: DivProps) @{
59
+ <div
60
+ data-slot="empty-title"
61
+ className={cn('text-sm font-medium tracking-tight', className)}
62
+ {...props}
63
+ />
64
+ }
65
+
66
+ // Upstream types this part as a `<p>` while rendering a `<div>`; the prop type is transcribed
67
+ // as-authored rather than "corrected", so the surface matches upstream exactly.
68
+ export function EmptyDescription({ className, ...props }: HostProps<'p'>) @{
69
+ <div
70
+ data-slot="empty-description"
71
+ className={cn(
72
+ 'text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
73
+ className,
74
+ )}
75
+ {...props}
76
+ />
77
+ }
78
+
79
+ export function EmptyContent({ className, ...props }: DivProps) @{
80
+ <div
81
+ data-slot="empty-content"
82
+ className={cn(
83
+ 'flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance',
84
+ className,
85
+ )}
86
+ {...props}
87
+ />
88
+ }
@@ -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,28 @@
1
+ // Base UI base input — runs on the @octanejs/base-ui `Input` primitive, which is a native
2
+ // `<input>` wired into Field (it is literally `Field.Control`).
3
+ //
4
+ // No `composeRenderProps` here, unlike the React Aria base: RAC routes `className` through a
5
+ // render-prop pipeline that must be composed, whereas Base UI's `className` is a plain string or
6
+ // a `(state) => string`, so `cn` applies directly.
7
+ //
8
+ // UNVERIFIED PROVENANCE: the class string is taken from this package's React Aria base rather
9
+ // than transcribed from upstream's Base UI source. Nothing in it keys off a primitive-emitted
10
+ // attribute, so it is a lower-risk case than `separator` — but verify before treating as ported.
11
+ //
12
+ // OCTANE DIVERGENCE: per-keystroke handling is the native `onInput` event; native `change`
13
+ // keeps its commit-on-blur meaning.
14
+ import { Input as InputPrimitive } from '@octanejs/base-ui/input';
15
+
16
+ import { cn } from '../../../lib/utils';
17
+
18
+ export function Input({ className, type, ...props }: Record<string, any>) @{
19
+ <InputPrimitive
20
+ type={type}
21
+ data-slot="input"
22
+ className={cn(
23
+ 'h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ }