@octanejs/shadcn 0.0.6 → 0.0.8

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 (100) hide show
  1. package/package.json +109 -10
  2. package/src/bases/base-ui/ui/accordion.tsrx +71 -0
  3. package/src/bases/base-ui/ui/alert-dialog.tsrx +149 -0
  4. package/src/bases/base-ui/ui/alert.tsrx +68 -0
  5. package/src/bases/base-ui/ui/aspect-ratio.tsrx +24 -0
  6. package/src/bases/base-ui/ui/button.tsrx +69 -0
  7. package/src/bases/base-ui/ui/card.tsrx +76 -0
  8. package/src/bases/base-ui/ui/checkbox.tsrx +43 -0
  9. package/src/bases/base-ui/ui/dialog.tsrx +137 -0
  10. package/src/bases/base-ui/ui/empty.tsrx +88 -0
  11. package/src/bases/base-ui/ui/input.tsrx +28 -0
  12. package/src/bases/base-ui/ui/kbd.tsrx +27 -0
  13. package/src/bases/base-ui/ui/label.tsrx +31 -0
  14. package/src/bases/base-ui/ui/native-select.tsrx +54 -0
  15. package/src/bases/base-ui/ui/popover.tsrx +85 -0
  16. package/src/bases/base-ui/ui/radio-group.tsrx +50 -0
  17. package/src/bases/base-ui/ui/separator.tsrx +34 -0
  18. package/src/bases/base-ui/ui/skeleton.tsrx +17 -0
  19. package/src/bases/base-ui/ui/spinner.tsrx +30 -0
  20. package/src/bases/base-ui/ui/switch.tsrx +43 -0
  21. package/src/bases/base-ui/ui/textarea.tsrx +22 -0
  22. package/src/bases/base-ui/ui/tooltip.tsrx +73 -0
  23. package/src/{ui → bases/radix/ui}/accordion.tsrx +1 -1
  24. package/src/{ui → bases/radix/ui}/alert-dialog.tsrx +1 -1
  25. package/src/{ui → bases/radix/ui}/alert.tsrx +2 -2
  26. package/src/{ui → bases/radix/ui}/avatar.tsrx +1 -1
  27. package/src/{ui → bases/radix/ui}/badge.tsrx +2 -2
  28. package/src/{ui → bases/radix/ui}/breadcrumb.tsrx +1 -1
  29. package/src/{ui → bases/radix/ui}/button.tsrx +2 -2
  30. package/src/{ui → bases/radix/ui}/card.tsrx +2 -2
  31. package/src/{ui → bases/radix/ui}/checkbox.tsrx +1 -1
  32. package/src/{ui → bases/radix/ui}/context-menu.tsrx +1 -1
  33. package/src/{ui → bases/radix/ui}/dialog.tsrx +1 -1
  34. package/src/{ui → bases/radix/ui}/dropdown-menu.tsrx +1 -1
  35. package/src/{ui → bases/radix/ui}/empty.tsrx +1 -1
  36. package/src/{ui → bases/radix/ui}/field.tsrx +1 -1
  37. package/src/{ui → bases/radix/ui}/hover-card.tsrx +1 -1
  38. package/src/{ui → bases/radix/ui}/input.tsrx +2 -2
  39. package/src/{ui → bases/radix/ui}/item.tsrx +1 -1
  40. package/src/{ui → bases/radix/ui}/kbd.tsrx +2 -2
  41. package/src/{ui → bases/radix/ui}/label.tsrx +1 -1
  42. package/src/{ui → bases/radix/ui}/menubar.tsrx +1 -1
  43. package/src/{ui → bases/radix/ui}/native-select.tsrx +1 -1
  44. package/src/{ui → bases/radix/ui}/navigation-menu.tsrx +1 -1
  45. package/src/{ui → bases/radix/ui}/pagination.tsrx +1 -1
  46. package/src/{ui → bases/radix/ui}/popover.tsrx +1 -1
  47. package/src/{ui → bases/radix/ui}/progress.tsrx +1 -1
  48. package/src/{ui → bases/radix/ui}/radio-group.tsrx +1 -1
  49. package/src/{ui → bases/radix/ui}/scroll-area.tsrx +1 -1
  50. package/src/{ui → bases/radix/ui}/select.tsrx +1 -1
  51. package/src/{ui → bases/radix/ui}/separator.tsrx +1 -1
  52. package/src/{ui → bases/radix/ui}/sheet.tsrx +1 -1
  53. package/src/{ui → bases/radix/ui}/sidebar.tsrx +2 -2
  54. package/src/{ui → bases/radix/ui}/skeleton.tsrx +2 -2
  55. package/src/{ui → bases/radix/ui}/slider.tsrx +1 -1
  56. package/src/{ui → bases/radix/ui}/spinner.tsrx +1 -1
  57. package/src/{ui → bases/radix/ui}/switch.tsrx +1 -1
  58. package/src/{ui → bases/radix/ui}/table.tsrx +1 -1
  59. package/src/{ui → bases/radix/ui}/tabs.tsrx +1 -1
  60. package/src/{ui → bases/radix/ui}/textarea.tsrx +2 -2
  61. package/src/{ui → bases/radix/ui}/toggle-group.tsrx +1 -1
  62. package/src/{ui → bases/radix/ui}/toggle.tsrx +1 -1
  63. package/src/{ui → bases/radix/ui}/tooltip.tsrx +1 -1
  64. package/src/bases/react-aria/ui/accordion.tsrx +73 -0
  65. package/src/bases/react-aria/ui/alert-dialog.tsrx +154 -0
  66. package/src/bases/react-aria/ui/alert.tsrx +60 -0
  67. package/src/bases/react-aria/ui/aspect-ratio.tsrx +13 -0
  68. package/src/bases/react-aria/ui/avatar.tsrx +83 -0
  69. package/src/bases/react-aria/ui/badge.tsrx +48 -0
  70. package/src/bases/react-aria/ui/breadcrumb.tsrx +100 -0
  71. package/src/bases/react-aria/ui/button.tsrx +96 -0
  72. package/src/bases/react-aria/ui/card.tsrx +73 -0
  73. package/src/bases/react-aria/ui/checkbox.tsrx +37 -0
  74. package/src/bases/react-aria/ui/collapsible.tsrx +20 -0
  75. package/src/bases/react-aria/ui/dialog.tsrx +131 -0
  76. package/src/bases/react-aria/ui/empty.tsrx +79 -0
  77. package/src/bases/react-aria/ui/field.tsrx +170 -0
  78. package/src/bases/react-aria/ui/input.tsrx +28 -0
  79. package/src/bases/react-aria/ui/item.tsrx +148 -0
  80. package/src/bases/react-aria/ui/kbd.tsrx +30 -0
  81. package/src/bases/react-aria/ui/label.tsrx +22 -0
  82. package/src/bases/react-aria/ui/native-select.tsrx +44 -0
  83. package/src/bases/react-aria/ui/pagination.tsrx +92 -0
  84. package/src/bases/react-aria/ui/popover.tsrx +57 -0
  85. package/src/bases/react-aria/ui/radio-group.tsrx +46 -0
  86. package/src/bases/react-aria/ui/scroll-area.tsrx +14 -0
  87. package/src/bases/react-aria/ui/separator.tsrx +28 -0
  88. package/src/bases/react-aria/ui/sheet.tsrx +143 -0
  89. package/src/bases/react-aria/ui/skeleton.tsrx +10 -0
  90. package/src/bases/react-aria/ui/slider.tsrx +56 -0
  91. package/src/bases/react-aria/ui/spinner.tsrx +28 -0
  92. package/src/bases/react-aria/ui/switch.tsrx +38 -0
  93. package/src/bases/react-aria/ui/table.tsrx +98 -0
  94. package/src/bases/react-aria/ui/tabs.tsrx +67 -0
  95. package/src/bases/react-aria/ui/textarea.tsrx +26 -0
  96. package/src/bases/react-aria/ui/toggle.tsrx +38 -0
  97. package/src/index.ts +0 -263
  98. /package/src/{ui → bases/radix/ui}/aspect-ratio.tsrx +0 -0
  99. /package/src/{ui → bases/radix/ui}/collapsible.tsrx +0 -0
  100. /package/src/{ui → bases/radix/ui}/sonner.tsrx +0 -0
@@ -0,0 +1,137 @@
1
+ // Base UI base dialog — runs on @octanejs/base-ui's Dialog.
2
+ // Part mapping: Overlay -> Backdrop, Content -> Popup, and the close affordance composes a
3
+ // Button through Base UI's render-as-ELEMENT contract rather than radix's `asChild`.
4
+ //
5
+ // `data-open:`/`data-closed:` need no adaptation — the radix base already uses that exact form
6
+ // and Base UI emits the same attributes.
7
+ //
8
+ // Drops upstream's `cn-font-heading` on the title, matching the react-aria base and this base's
9
+ // alert-dialog: the package ships the utilities-inlined flavor and nothing defines that hook.
10
+ //
11
+ // UNVERIFIED PROVENANCE: class strings come from this package's radix base rather than
12
+ // transcribed from upstream's Base UI source.
13
+ import { createElement, type OctaneNode } from 'octane';
14
+ import { Dialog as DialogPrimitive } from '@octanejs/base-ui/dialog';
15
+ import { XIcon } from '@octanejs/lucide';
16
+
17
+ import { cn } from '../../../lib/utils';
18
+ import { Button } from './button.tsrx';
19
+
20
+ type Props = { className?: string; children?: OctaneNode } & Record<string, unknown>;
21
+
22
+ export function Dialog(props: Record<string, unknown>) @{
23
+ <DialogPrimitive.Root data-slot="dialog" {...props} />
24
+ }
25
+
26
+ export function DialogTrigger(props: Record<string, unknown>) @{
27
+ <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
28
+ }
29
+
30
+ export function DialogPortal(props: Record<string, unknown>) @{
31
+ <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
32
+ }
33
+
34
+ export function DialogClose(props: Record<string, unknown>) @{
35
+ <DialogPrimitive.Close data-slot="dialog-close" {...props} />
36
+ }
37
+
38
+ export function DialogOverlay({ className, ...props }: Props) @{
39
+ <DialogPrimitive.Backdrop
40
+ data-slot="dialog-overlay"
41
+ className={cn(
42
+ 'fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0',
43
+ className,
44
+ )}
45
+ {...props}
46
+ />
47
+ }
48
+
49
+ export interface DialogContentProps extends Props {
50
+ showCloseButton?: boolean;
51
+ }
52
+
53
+ export function DialogContent({
54
+ className,
55
+ children,
56
+ showCloseButton = true,
57
+ ...props
58
+ }: DialogContentProps) {
59
+ return createElement(
60
+ DialogPrimitive.Portal,
61
+ { 'data-slot': 'dialog-portal' },
62
+ createElement(DialogOverlay, { key: 'overlay' }),
63
+ createElement(
64
+ DialogPrimitive.Popup,
65
+ {
66
+ key: 'content',
67
+ 'data-slot': 'dialog-content',
68
+ className: cn(
69
+ 'fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm 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',
70
+ className,
71
+ ),
72
+ ...props,
73
+ },
74
+ children,
75
+ showCloseButton
76
+ ? createElement(DialogPrimitive.Close, {
77
+ key: 'close',
78
+ 'data-slot': 'dialog-close',
79
+ className: 'absolute top-2 right-2',
80
+ render: createElement(
81
+ Button,
82
+ { variant: 'ghost', size: 'icon-sm' },
83
+ createElement(XIcon, { key: 'icon' }),
84
+ createElement('span', { key: 'sr', className: 'sr-only' }, 'Close'),
85
+ ),
86
+ })
87
+ : null,
88
+ ),
89
+ );
90
+ }
91
+
92
+ export function DialogHeader({ className, ...props }: Props) @{
93
+ <div data-slot="dialog-header" className={cn('flex flex-col gap-2', className)} {...props} />
94
+ }
95
+
96
+ export interface DialogFooterProps extends Props {
97
+ showCloseButton?: boolean;
98
+ }
99
+
100
+ export function DialogFooter(props: DialogFooterProps) @{
101
+ const { className, showCloseButton = false, children: _children, ...rest } = props;
102
+
103
+ <div
104
+ data-slot="dialog-footer"
105
+ className={cn(
106
+ '-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end',
107
+ className,
108
+ )}
109
+ {...rest}
110
+ >
111
+ {props.children}
112
+ {showCloseButton
113
+ ? createElement(DialogPrimitive.Close, {
114
+ render: createElement(Button, { variant: 'outline' }, 'Close'),
115
+ })
116
+ : null}
117
+ </div>
118
+ }
119
+
120
+ export function DialogTitle({ className, ...props }: Props) @{
121
+ <DialogPrimitive.Title
122
+ data-slot="dialog-title"
123
+ className={cn('text-base leading-none font-medium', className)}
124
+ {...props}
125
+ />
126
+ }
127
+
128
+ export function DialogDescription({ className, ...props }: Props) @{
129
+ <DialogPrimitive.Description
130
+ data-slot="dialog-description"
131
+ className={cn(
132
+ 'text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground',
133
+ className,
134
+ )}
135
+ {...props}
136
+ />
137
+ }
@@ -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,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
+ }
@@ -0,0 +1,27 @@
1
+ // Base UI base kbd — plain host `<kbd>` elements. Base UI ships no Keyboard primitive (the
2
+ // React Aria base renders both parts through RAC's `Keyboard`), so this matches the Radix
3
+ // base's plain-host shape.
4
+ //
5
+ // UNVERIFIED PROVENANCE: class strings taken from this package's React Aria base rather than
6
+ // transcribed from upstream's Base UI source.
7
+ import { cn } from '../../../lib/utils';
8
+ import type { HostProps } from '../../../lib/types';
9
+
10
+ export function Kbd({ className, ...props }: HostProps<'kbd'>) @{
11
+ <kbd
12
+ data-slot="kbd"
13
+ className={cn(
14
+ 'pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*=\'size-\'])]:size-3',
15
+ className,
16
+ )}
17
+ {...props}
18
+ />
19
+ }
20
+
21
+ export function KbdGroup({ className, ...props }: HostProps<'kbd'>) @{
22
+ <kbd
23
+ data-slot="kbd-group"
24
+ className={cn('inline-flex items-center gap-1', className)}
25
+ {...props}
26
+ />
27
+ }
@@ -0,0 +1,31 @@
1
+ // Base UI base label — a plain host `<label>`.
2
+ //
3
+ // NOT `Field.Label`, and that is the whole point of this file. Base UI's label part is
4
+ // `Field.Label`, but it calls `useFieldRootContext(false)` — it HARD-REQUIRES a `<Field.Root>`
5
+ // ancestor and throws "FieldRootContext is missing" without one. Upstream's own FieldLabel does
6
+ // the same, so that is faithful behavior, not a porting defect.
7
+ //
8
+ // shadcn's `Label`, by contrast, is a standalone component: the docs use it beside an Input with
9
+ // no Field wrapper at all, which is exactly how the playground uses it. Routing it through
10
+ // `Field.Label` made every such use a runtime crash. A plain `<label>` works standalone AND
11
+ // inside a Field, and consumers who want Base UI's automatic label/control association can reach
12
+ // for `Field.Label` directly.
13
+ //
14
+ // Regression coverage: `tests/base-ui-standalone.test.ts`.
15
+ //
16
+ // UNVERIFIED PROVENANCE: the class string is taken from this package's React Aria base rather
17
+ // than transcribed from upstream's Base UI source. It carries one utility the Radix base omits
18
+ // (`peer-data-disabled:opacity-50`).
19
+ import { cn } from '../../../lib/utils';
20
+ import type { HostProps } from '../../../lib/types';
21
+
22
+ export function Label({ className, ...props }: HostProps<'label'>) @{
23
+ <label
24
+ data-slot="label"
25
+ className={cn(
26
+ 'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 peer-data-disabled:opacity-50',
27
+ className,
28
+ )}
29
+ {...props}
30
+ />
31
+ }
@@ -0,0 +1,54 @@
1
+ // Base UI base native-select — a real `<select>`, so 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/native-select.tsx`, which was not available at port time.
5
+ // Diff the class strings against the upstream source before treating this as ported.
6
+ //
7
+ // OCTANE DIVERGENCE: upstream's IconPlaceholder resolves to lucide via @octanejs/lucide.
8
+ // Events are native: a `<select>`'s `change` keeps its platform commit meaning, so consumer
9
+ // `onChange` handlers are passed through unrenamed.
10
+ import { ChevronDownIcon } from '@octanejs/lucide';
11
+
12
+ import { cn } from '../../../lib/utils';
13
+ import type { HostProps } from '../../../lib/types';
14
+
15
+ export type NativeSelectProps =
16
+ Omit<HostProps<'select'>, 'size'> & {
17
+ size?: 'sm' | 'default';
18
+ };
19
+
20
+ export function NativeSelect({ className, size = 'default', ...props }: NativeSelectProps) @{
21
+ <div
22
+ className={cn('group/native-select relative w-fit has-[select:disabled]:opacity-50', className)}
23
+ data-slot="native-select-wrapper"
24
+ data-size={size}
25
+ >
26
+ <select
27
+ data-slot="native-select"
28
+ data-size={size}
29
+ className="h-8 w-full min-w-0 appearance-none rounded-lg border border-input bg-transparent py-1 pr-8 pl-2.5 text-sm transition-colors outline-none select-none selection:bg-primary selection:text-primary-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 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-[size=sm]:py-0.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40"
30
+ {...props}
31
+ />
32
+ <ChevronDownIcon
33
+ className="pointer-events-none absolute top-1/2 right-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none"
34
+ aria-hidden="true"
35
+ data-slot="native-select-icon"
36
+ />
37
+ </div>
38
+ }
39
+
40
+ export function NativeSelectOption({ className, ...props }: HostProps<'option'>) @{
41
+ <option
42
+ data-slot="native-select-option"
43
+ className={cn('bg-[Canvas] text-[CanvasText]', className)}
44
+ {...props}
45
+ />
46
+ }
47
+
48
+ export function NativeSelectOptGroup({ className, ...props }: HostProps<'optgroup'>) @{
49
+ <optgroup
50
+ data-slot="native-select-optgroup"
51
+ className={cn('bg-[Canvas] text-[CanvasText]', className)}
52
+ {...props}
53
+ />
54
+ }
@@ -0,0 +1,85 @@
1
+ // Base UI base popover — runs on @octanejs/base-ui's Popover.
2
+ //
3
+ // TWO POSITIONING DIFFERENCES FROM THE RADIX BASE, both verified by rendering the primitive and
4
+ // reading the DOM rather than inferred:
5
+ //
6
+ // 1. THE TRANSFORM-ORIGIN CSS VAR IS NAMED DIFFERENTLY. Radix publishes
7
+ // `--radix-popover-content-transform-origin`; Base UI's Positioner publishes plain
8
+ // `--transform-origin`. Copying radix's `origin-(--radix-popover-content-transform-origin)`
9
+ // leaves the utility referencing a variable nothing ever sets, so the popup would scale
10
+ // from the wrong corner on open — visible only in motion, and invisible to any class
11
+ // comparison.
12
+ //
13
+ // 2. THERE IS A POSITIONER LAYER. Radix goes Portal > Content; Base UI goes
14
+ // Portal > Positioner > Popup, and the Positioner owns side/align/offset. `data-side` and
15
+ // `data-align` land on BOTH the positioner and the popup, so the `data-[side=…]` slide
16
+ // utilities still resolve on the popup where the radix base put them.
17
+ //
18
+ // NO PopoverAnchor. Radix ships an Anchor part for positioning against something other than the
19
+ // trigger; Base UI's Positioner takes an `anchor` prop instead, so there is no element to render
20
+ // and no slot to emit. Recorded in tests/cross-base.test.ts rather than faked.
21
+ //
22
+ // UNVERIFIED PROVENANCE: class strings come from this package's radix base rather than
23
+ // transcribed from upstream's Base UI source.
24
+ import { createElement } from 'octane';
25
+ import { Popover as PopoverPrimitive } from '@octanejs/base-ui/popover';
26
+
27
+ import { cn } from '../../../lib/utils';
28
+
29
+ type Props = { className?: string } & Record<string, unknown>;
30
+
31
+ export function Popover(props: Record<string, unknown>) @{
32
+ <PopoverPrimitive.Root data-slot="popover" {...props} />
33
+ }
34
+
35
+ export function PopoverTrigger(props: Record<string, unknown>) @{
36
+ <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
37
+ }
38
+
39
+ export interface PopoverContentProps extends Record<string, unknown> {
40
+ className?: string;
41
+ align?: 'start' | 'center' | 'end';
42
+ sideOffset?: number;
43
+ }
44
+
45
+ export function PopoverContent({
46
+ className,
47
+ align = 'center',
48
+ sideOffset = 4,
49
+ ...props
50
+ }: PopoverContentProps) {
51
+ return createElement(PopoverPrimitive.Portal, {
52
+ children: createElement(PopoverPrimitive.Positioner, {
53
+ align,
54
+ sideOffset,
55
+ children: createElement(PopoverPrimitive.Popup, {
56
+ 'data-slot': 'popover-content',
57
+ className: cn(
58
+ 'z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden 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',
59
+ className,
60
+ ),
61
+ ...props,
62
+ }),
63
+ }),
64
+ });
65
+ }
66
+
67
+ export function PopoverHeader({ className, ...props }: Props) @{
68
+ <div
69
+ data-slot="popover-header"
70
+ className={cn('flex flex-col gap-0.5 text-sm', className)}
71
+ {...props}
72
+ />
73
+ }
74
+
75
+ export function PopoverTitle({ className, ...props }: Props) @{
76
+ <div data-slot="popover-title" className={cn('font-medium', className)} {...props} />
77
+ }
78
+
79
+ export function PopoverDescription({ className, ...props }: Props) @{
80
+ <p
81
+ data-slot="popover-description"
82
+ className={cn('text-muted-foreground', className)}
83
+ {...props}
84
+ />
85
+ }
@@ -0,0 +1,50 @@
1
+ // Base UI base radio-group — runs on @octanejs/base-ui's `RadioGroup` plus `Radio`.
2
+ //
3
+ // PART MAPPING DIFFERS FROM RADIX. Radix nests everything under one namespace
4
+ // (RadioGroup.Root / .Item / .Indicator); Base UI splits the group from the item, so the item
5
+ // is `Radio.Root` from a separate subpath and the indicator is `Radio.Indicator`. The exported
6
+ // component names and every data-slot stay as shadcn defines them, so consumers see no
7
+ // difference — this is wiring, not API.
8
+ //
9
+ // `disabled:` becomes `data-disabled:` for the same reason as checkbox and switch: Radio.Root
10
+ // renders a `<span role="radio">`, which is never `:disabled`, so the pseudo-class variant could
11
+ // never match. The primitive publishes `data-disabled` instead.
12
+ //
13
+ // UNVERIFIED PROVENANCE: the non-conditional utilities come from this package's radix base
14
+ // rather than upstream's Base UI source.
15
+ import { RadioGroup as RadioGroupPrimitive } from '@octanejs/base-ui/radio-group';
16
+ import { Radio as RadioPrimitive } from '@octanejs/base-ui/radio';
17
+ import { CircleIcon } from '@octanejs/lucide';
18
+
19
+ import { cn } from '../../../lib/utils';
20
+
21
+ type Props = { className?: string } & Record<string, unknown>;
22
+
23
+ // Base UI's Radio.Root requires `value` (Radix infers it from the item), so the item's props
24
+ // type demands it. shadcn usage always passes one, so this tightens the type without changing
25
+ // the surface.
26
+ type ItemProps = { className?: string; value: any } & Record<string, unknown>;
27
+
28
+ export function RadioGroup({ className, ...props }: Props) @{
29
+ <RadioGroupPrimitive data-slot="radio-group" className={cn('grid gap-3', className)} {...props} />
30
+ }
31
+
32
+ export function RadioGroupItem({ className, ...props }: ItemProps) @{
33
+ <RadioPrimitive.Root
34
+ data-slot="radio-group-item"
35
+ className={cn(
36
+ 'aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 data-disabled:cursor-not-allowed data-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40',
37
+ className,
38
+ )}
39
+ {...props}
40
+ >
41
+ <RadioPrimitive.Indicator
42
+ data-slot="radio-group-indicator"
43
+ className="relative flex items-center justify-center"
44
+ >
45
+ <CircleIcon
46
+ className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary"
47
+ />
48
+ </RadioPrimitive.Indicator>
49
+ </RadioPrimitive.Root>
50
+ }
@@ -0,0 +1,34 @@
1
+ // Base UI base separator — runs on the @octanejs/base-ui `Separator` primitive.
2
+ //
3
+ // THE CLASS STRING IS DELIBERATELY THE ARIA BASE'S, NOT THE RADIX BASE'S, and this is the one
4
+ // family where copying the wrong base is a real bug rather than a cosmetic risk. The
5
+ // orientation-conditional utilities must key off the attribute the primitive actually emits:
6
+ //
7
+ // Radix sets `data-orientation` -> `data-horizontal:h-px`
8
+ // RAC sets `aria-orientation` -> `aria-[orientation=horizontal]:h-px`
9
+ // Base UI sets `aria-orientation` -> same as RAC (verified in the primitive: it renders
10
+ // `role="separator"` plus `aria-orientation={orientation}`)
11
+ //
12
+ // Taking the Radix string here would produce a separator with no height or width at all, and
13
+ // nothing in a class-string comparison would flag it. Upstream's own bases diverge here for
14
+ // exactly this reason. The non-conditional utilities stay in step with the other bases.
15
+ import { Separator as SeparatorPrimitive } from '@octanejs/base-ui/separator';
16
+
17
+ import { cn } from '../../../lib/utils';
18
+
19
+ export interface SeparatorProps extends Record<string, unknown> {
20
+ className?: string;
21
+ orientation?: 'horizontal' | 'vertical';
22
+ }
23
+
24
+ export function Separator({ className, orientation = 'horizontal', ...props }: SeparatorProps) @{
25
+ <SeparatorPrimitive
26
+ data-slot="separator"
27
+ orientation={orientation}
28
+ className={cn(
29
+ 'block shrink-0 border-0 bg-border aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=vertical]:w-px aria-[orientation=vertical]:self-stretch [:is(hr)]:h-px [:is(hr)]:w-full',
30
+ className,
31
+ )}
32
+ {...props}
33
+ />
34
+ }
@@ -0,0 +1,17 @@
1
+ // Base UI base skeleton — 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/skeleton.tsx`, which was not available at port time.
5
+ // Skeleton is the WORST case for this, and the reason the whole batch is labelled: the Radix
6
+ // base renders `bg-accent` where the React Aria base renders `bg-muted`, so the one token that
7
+ // matters here is known to differ ACROSS bases. Verify against the upstream source.
8
+ import { cn } from '../../../lib/utils';
9
+ import type { DivProps } from '../../../lib/types';
10
+
11
+ export function Skeleton({ className, ...props }: DivProps) @{
12
+ <div
13
+ data-slot="skeleton"
14
+ className={cn('animate-pulse rounded-md bg-muted', className)}
15
+ {...props}
16
+ />
17
+ }
@@ -0,0 +1,30 @@
1
+ // Base UI base spinner — 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/spinner.tsx`, which was not available at port time.
5
+ // Diff the class strings against the upstream source before treating this as ported.
6
+ //
7
+ // OCTANE DIVERGENCE: upstream's IconPlaceholder (the CLI-resolved iconLibrary axis) is
8
+ // resolved at port time to the default library, lucide, via @octanejs/lucide. Other icon
9
+ // libraries are a registry-emit concern.
10
+ //
11
+ // The props are a permissive record rather than HostProps<'svg'>: these are forwarded to a
12
+ // lucide component, not applied to a host element, so the host SVG attribute types do not
13
+ // describe them.
14
+ import { Loader2Icon } from '@octanejs/lucide';
15
+
16
+ import { cn } from '../../../lib/utils';
17
+
18
+ export interface SpinnerProps extends Record<string, unknown> {
19
+ className?: string;
20
+ }
21
+
22
+ export function Spinner({ className, ...props }: SpinnerProps) @{
23
+ <Loader2Icon
24
+ data-slot="spinner"
25
+ role="status"
26
+ aria-label="Loading"
27
+ className={cn('size-4 animate-spin', className)}
28
+ {...props}
29
+ />
30
+ }
@@ -0,0 +1,43 @@
1
+ // Base UI base switch — runs on the @octanejs/base-ui `Switch` primitive.
2
+ //
3
+ // TWO ATTRIBUTE DIALECTS DIFFER FROM THE RADIX BASE, both verified by rendering the primitive:
4
+ //
5
+ // 1. Checked state. Radix publishes `data-state="checked"`, Base UI publishes bare
6
+ // `data-checked` / `data-unchecked` attributes:
7
+ // radix: `data-[state=checked]:bg-primary`
8
+ // base-ui: `data-checked:bg-primary`
9
+ // Copying the radix form would leave the switch permanently showing its unchecked colours
10
+ // and the thumb never translating — the exact symptom of a wrong-base copy.
11
+ //
12
+ // 2. Disabled. Switch.Root renders a `<span role="switch">`, never `:disabled`, so
13
+ // `disabled:` cannot match; the primitive emits `data-disabled` instead.
14
+ //
15
+ // UNVERIFIED PROVENANCE: the non-conditional utilities come from this package's radix base
16
+ // rather than upstream's Base UI source.
17
+ import { Switch as SwitchPrimitive } from '@octanejs/base-ui/switch';
18
+
19
+ import { cn } from '../../../lib/utils';
20
+
21
+ export interface SwitchProps extends Record<string, unknown> {
22
+ className?: string;
23
+ size?: 'sm' | 'default';
24
+ }
25
+
26
+ export function Switch({ className, size = 'default', ...props }: SwitchProps) @{
27
+ <SwitchPrimitive.Root
28
+ data-slot="switch"
29
+ data-size={size}
30
+ className={cn(
31
+ 'peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80',
32
+ className,
33
+ )}
34
+ {...props}
35
+ >
36
+ <SwitchPrimitive.Thumb
37
+ data-slot="switch-thumb"
38
+ className={cn(
39
+ 'pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-checked:translate-x-[calc(100%-2px)] data-unchecked:translate-x-0 dark:data-checked:bg-primary-foreground dark:data-unchecked:bg-foreground',
40
+ )}
41
+ />
42
+ </SwitchPrimitive.Root>
43
+ }
@@ -0,0 +1,22 @@
1
+ // Base UI base textarea — a plain host `<textarea>`. Base UI ships no textarea primitive (its
2
+ // Input is a native `<input>` via Field.Control), so unlike the React Aria base there is no
3
+ // `TextArea` component to compose and this matches the Radix base's plain-host shape.
4
+ //
5
+ // UNVERIFIED PROVENANCE: class string taken from this package's React Aria base rather than
6
+ // transcribed from upstream's Base UI source.
7
+ //
8
+ // OCTANE DIVERGENCE: per-keystroke handling is the native `onInput` event, not a synthetic
9
+ // `onChange`; native `change` keeps its commit-on-blur meaning.
10
+ import { cn } from '../../../lib/utils';
11
+ import type { HostProps } from '../../../lib/types';
12
+
13
+ export function Textarea({ className, ...props }: HostProps<'textarea'>) @{
14
+ <textarea
15
+ data-slot="textarea"
16
+ className={cn(
17
+ 'flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 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',
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ }