@octanejs/shadcn 0.0.7 → 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.
- package/package.json +29 -7
- package/src/bases/base-ui/ui/accordion.tsrx +71 -0
- package/src/bases/base-ui/ui/alert-dialog.tsrx +149 -0
- package/src/bases/base-ui/ui/alert.tsrx +68 -0
- package/src/bases/base-ui/ui/aspect-ratio.tsrx +24 -0
- package/src/bases/base-ui/ui/button.tsrx +69 -0
- package/src/bases/base-ui/ui/card.tsrx +76 -0
- package/src/bases/base-ui/ui/checkbox.tsrx +43 -0
- package/src/bases/base-ui/ui/dialog.tsrx +137 -0
- package/src/bases/base-ui/ui/empty.tsrx +88 -0
- package/src/bases/base-ui/ui/input.tsrx +28 -0
- package/src/bases/base-ui/ui/kbd.tsrx +27 -0
- package/src/bases/base-ui/ui/label.tsrx +31 -0
- package/src/bases/base-ui/ui/native-select.tsrx +54 -0
- package/src/bases/base-ui/ui/popover.tsrx +85 -0
- package/src/bases/base-ui/ui/radio-group.tsrx +50 -0
- package/src/bases/base-ui/ui/separator.tsrx +34 -0
- package/src/bases/base-ui/ui/skeleton.tsrx +17 -0
- package/src/bases/base-ui/ui/spinner.tsrx +30 -0
- package/src/bases/base-ui/ui/switch.tsrx +43 -0
- package/src/bases/base-ui/ui/textarea.tsrx +22 -0
- package/src/bases/base-ui/ui/tooltip.tsrx +73 -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
|
+
}
|