@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,121 @@
1
+ // Base UI base sheet — TRANSCRIBED from upstream's `bases/base-ui/ui/sheet.tsx`
2
+ // (maintainer-supplied), class strings verbatim. Runs on @octanejs/base-ui's Dialog:
3
+ // Overlay -> Backdrop, Content -> Popup.
4
+ //
5
+ // UPSTREAM ANIMATES THIS WITH CSS TRANSITIONS, NOT KEYFRAMES, which is why the file is transcribed
6
+ // rather than derived. Base UI publishes `data-starting-style` and `data-ending-style` (see
7
+ // utils/popupStateMapping.ts) and this source drives motion off them with a plain `transition`,
8
+ // where the radix base uses `animate-in` / `animate-out` keyframe utilities.
9
+ //
10
+ // A derived version of this file did jump visibly on close, which is what prompted the
11
+ // transcription. Do NOT infer from that a general rule that keyframes cannot work on Base UI: the
12
+ // same mapping also emits `data-open` and `data-closed`, so `data-closed:animate-out` does match,
13
+ // and the precise cause of that jump was never isolated. What is established here is only what
14
+ // upstream ships, which is this.
15
+ //
16
+ // So the motion here reads: `transition duration-200` plus per-side
17
+ // `data-starting-style:translate-*` / `data-ending-style:translate-*` and an opacity pair —
18
+ // the element sits offset-and-transparent before open and after close, and transitions to its
19
+ // resting position in between.
20
+ //
21
+ // ONE DEPARTURE FROM THE SOURCE: upstream's title carries `cn-font-heading`, a semantic hook from
22
+ // the pinned cn-* system. This package ships the utilities-inlined flavor, the react-aria base
23
+ // drops the same class, and nothing defines it here (0 occurrences in the built CSS).
24
+ //
25
+ // Octane adaptations: no `"use client"`; refs are props.
26
+ import { type OctaneNode } from 'octane';
27
+ import { Dialog as SheetPrimitive } from '@octanejs/base-ui/dialog';
28
+ import { XIcon } from '@octanejs/lucide';
29
+
30
+ import { cn } from '../../../lib/utils';
31
+ import { Button } from './button.tsrx';
32
+
33
+ type Props = { className?: string; children?: OctaneNode } & Record<string, unknown>;
34
+
35
+ export function Sheet(props: Record<string, unknown>) @{
36
+ <SheetPrimitive.Root data-slot="sheet" {...props} />
37
+ }
38
+
39
+ export function SheetTrigger(props: Record<string, unknown>) @{
40
+ <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
41
+ }
42
+
43
+ export function SheetClose(props: Record<string, unknown>) @{
44
+ <SheetPrimitive.Close data-slot="sheet-close" {...props} />
45
+ }
46
+
47
+ export function SheetPortal(props: Record<string, unknown>) @{
48
+ <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
49
+ }
50
+
51
+ export function SheetOverlay({ className, ...props }: Props) @{
52
+ <SheetPrimitive.Backdrop
53
+ data-slot="sheet-overlay"
54
+ className={cn(
55
+ 'fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs',
56
+ className,
57
+ )}
58
+ {...props}
59
+ />
60
+ }
61
+
62
+ export interface SheetContentProps extends Props {
63
+ side?: 'top' | 'right' | 'bottom' | 'left';
64
+ showCloseButton?: boolean;
65
+ }
66
+
67
+ export function SheetContent(props: SheetContentProps) @{
68
+ const { className, side = 'right', showCloseButton = true, children: _children, ...rest } = props;
69
+
70
+ <SheetPortal>
71
+ <SheetOverlay />
72
+ <SheetPrimitive.Popup
73
+ data-slot="sheet-content"
74
+ data-side={side}
75
+ className={cn(
76
+ 'fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm',
77
+ className,
78
+ )}
79
+ {...rest}
80
+ >
81
+ {props.children}
82
+ @if (showCloseButton) {
83
+ <SheetPrimitive.Close
84
+ data-slot="sheet-close"
85
+ render={<Button variant="ghost" className="absolute top-3 right-3" size="icon-sm">
86
+ <XIcon />
87
+ <span className="sr-only">Close</span>
88
+ </Button>}
89
+ />
90
+ }
91
+ </SheetPrimitive.Popup>
92
+ </SheetPortal>
93
+ }
94
+
95
+ export function SheetHeader({ className, ...props }: Props) @{
96
+ <div data-slot="sheet-header" className={cn('flex flex-col gap-0.5 p-4', className)} {...props} />
97
+ }
98
+
99
+ export function SheetFooter({ className, ...props }: Props) @{
100
+ <div
101
+ data-slot="sheet-footer"
102
+ className={cn('mt-auto flex flex-col gap-2 p-4', className)}
103
+ {...props}
104
+ />
105
+ }
106
+
107
+ export function SheetTitle({ className, ...props }: Props) @{
108
+ <SheetPrimitive.Title
109
+ data-slot="sheet-title"
110
+ className={cn('text-base font-medium text-foreground', className)}
111
+ {...props}
112
+ />
113
+ }
114
+
115
+ export function SheetDescription({ className, ...props }: Props) @{
116
+ <SheetPrimitive.Description
117
+ data-slot="sheet-description"
118
+ className={cn('text-sm text-muted-foreground', className)}
119
+ {...props}
120
+ />
121
+ }
@@ -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,111 @@
1
+ // Base UI base slider — TRANSCRIBED from upstream's `bases/base-ui/ui/slider.tsx`
2
+ // (maintainer-supplied). Structure, part tree and class strings follow that source.
3
+ //
4
+ // THE PART TREE IS NOT THE RADIX ONE:
5
+ //
6
+ // radix Root > Track > Range, Thumb a SIBLING of Track (under Root)
7
+ // Base UI Root > Control > Track > Indicator, Thumb a SIBLING of Track (under Control)
8
+ //
9
+ // The thumb sits OUTSIDE the track in both. That matters: the track carries `overflow-hidden` —
10
+ // which is what gives the fill its rounded ends — and is only `h-1` tall, so a round thumb nested
11
+ // inside it is clipped to a 4px sliver that reads as a missing thumb. Base UI positions the thumb
12
+ // from slider context rather than from its DOM parent (`inset-inline-start: <pct>` plus a
13
+ // centering `translate`), so Control is the box those percentages resolve against.
14
+ //
15
+ // ONE DEPARTURE FROM THE SOURCE, AND IT IS DELIBERATE: upstream writes its orientation variants as
16
+ // `data-horizontal:` / `data-vertical:`. No primitive in @octanejs/base-ui emits those attributes —
17
+ // this one publishes `data-orientation="horizontal" | "vertical"` on Root, Control, Track,
18
+ // Indicator and Thumb (verified by rendering it), so the variants are written as
19
+ // `data-[orientation=…]:` instead. Copied verbatim, upstream's spelling would match nothing and the
20
+ // track would render with no height at all: an invisible rail. Whether the primitive should be
21
+ // emitting the newer attribute names is a question about @octanejs/base-ui, not about this file.
22
+ //
23
+ // `thumbAlignment="edge"` is forwarded as upstream does: it insets the thumb so the two extremes
24
+ // sit flush with the ends of the track instead of overhanging them.
25
+ //
26
+ // Thumbs are composed with createElement for the same reason as the radix base: one per entry of
27
+ // `_values`, and mapped descriptors are octane's channel for computed children.
28
+ import { createElement, useMemo } from 'octane';
29
+ import { Slider as SliderPrimitive } from '@octanejs/base-ui/slider';
30
+
31
+ import { cn } from '../../../lib/utils';
32
+
33
+ export interface SliderProps extends Record<string, unknown> {
34
+ className?: string;
35
+ defaultValue?: number | number[];
36
+ value?: number | number[];
37
+ min?: number;
38
+ max?: number;
39
+ }
40
+
41
+ export function Slider({
42
+ className,
43
+ defaultValue,
44
+ value,
45
+ min = 0,
46
+ max = 100,
47
+ ...props
48
+ }: SliderProps) {
49
+ // This drives HOW MANY THUMBS get rendered, so a value shape it fails to recognise is a visible
50
+ // bug rather than a type nicety.
51
+ //
52
+ // DEPARTS FROM THE TRANSCRIBED SOURCE, deliberately. Upstream only tests `Array.isArray` and
53
+ // otherwise falls back to `[min, max]` — but Base UI's Slider accepts a SCALAR for a
54
+ // single-value slider, and upstream types these props as `SliderPrimitive.Root.Props`, which
55
+ // permits one. So `defaultValue={30}` renders two thumbs against a one-value slider: the range
56
+ // fallback fires, and the second thumb has no value behind it.
57
+ const _values = useMemo(() => {
58
+ // A controlled `value` wins when present; `??` rather than `||` so a legitimate 0 counts.
59
+ const provided = value ?? defaultValue;
60
+ if (Array.isArray(provided)) {
61
+ return provided;
62
+ }
63
+ if (typeof provided === 'number') {
64
+ return [provided];
65
+ }
66
+ // Neither given: upstream's range-across-the-track default, kept as-is in both bases.
67
+ return [min, max];
68
+ }, [value, defaultValue, min, max]);
69
+
70
+ return createElement(
71
+ SliderPrimitive.Root,
72
+ {
73
+ className: cn(
74
+ 'data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full',
75
+ className,
76
+ ),
77
+ 'data-slot': 'slider',
78
+ defaultValue,
79
+ value,
80
+ min,
81
+ max,
82
+ thumbAlignment: 'edge',
83
+ ...props,
84
+ },
85
+ createElement(
86
+ SliderPrimitive.Control,
87
+ {
88
+ className: 'relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
89
+ },
90
+ createElement(
91
+ SliderPrimitive.Track,
92
+ {
93
+ 'data-slot': 'slider-track',
94
+ className: 'relative grow overflow-hidden rounded-full bg-muted select-none data-[orientation=horizontal]:h-1 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1',
95
+ },
96
+ createElement(SliderPrimitive.Indicator, {
97
+ 'data-slot': 'slider-range',
98
+ className: 'bg-primary select-none data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full',
99
+ }),
100
+ ),
101
+ ...Array.from(
102
+ { length: _values.length },
103
+ (_, index) => createElement(SliderPrimitive.Thumb, {
104
+ 'data-slot': 'slider-thumb',
105
+ key: index,
106
+ className: 'relative block size-3 shrink-0 rounded-full border border-ring bg-white ring-ring/50 transition-[color,box-shadow] select-none after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-hidden active:ring-3 disabled:pointer-events-none disabled:opacity-50',
107
+ }),
108
+ ),
109
+ ),
110
+ );
111
+ }
@@ -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,85 @@
1
+ // Base UI base table — THIS FAMILY IS BASE-INDEPENDENT. Neither Radix nor Base UI publishes a
2
+ // table primitive, so upstream ships the same plain host elements in both bases and this file is
3
+ // the radix base's content unchanged. (The React Aria base is the odd one out: RAC does have a
4
+ // table, so that base runs on real collection components with generic item types.)
5
+ //
6
+ // There is consequently no dialect to get wrong here and nothing to verify against a primitive:
7
+ // `data-[state=selected]` on the row is written by the CONSUMER — a data-table library setting
8
+ // selection state — not emitted by anything underneath, so unlike checkbox or toggle it is not a
9
+ // Radix-ism that needs translating.
10
+ import { cn } from '../../../lib/utils';
11
+
12
+ // Multi-host family (div/table/thead/tbody/tr/th/td/caption): no single host
13
+ // prop table applies, so the shared alias stays structural.
14
+ type Props = { className?: string } & Record<string, unknown>;
15
+
16
+ export function Table({ className, ...props }: Props) @{
17
+ <div data-slot="table-container" className="relative w-full overflow-x-auto">
18
+ <table
19
+ data-slot="table"
20
+ className={cn('w-full caption-bottom text-sm', className)}
21
+ {...props}
22
+ />
23
+ </div>
24
+ }
25
+
26
+ export function TableHeader({ className, ...props }: Props) @{
27
+ <thead data-slot="table-header" className={cn('[&_tr]:border-b', className)} {...props} />
28
+ }
29
+
30
+ export function TableBody({ className, ...props }: Props) @{
31
+ <tbody
32
+ data-slot="table-body"
33
+ className={cn('[&_tr:last-child]:border-0', className)}
34
+ {...props}
35
+ />
36
+ }
37
+
38
+ export function TableFooter({ className, ...props }: Props) @{
39
+ <tfoot
40
+ data-slot="table-footer"
41
+ className={cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className)}
42
+ {...props}
43
+ />
44
+ }
45
+
46
+ export function TableRow({ className, ...props }: Props) @{
47
+ <tr
48
+ data-slot="table-row"
49
+ className={cn(
50
+ 'border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted',
51
+ className,
52
+ )}
53
+ {...props}
54
+ />
55
+ }
56
+
57
+ export function TableHead({ className, ...props }: Props) @{
58
+ <th
59
+ data-slot="table-head"
60
+ className={cn(
61
+ 'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
62
+ className,
63
+ )}
64
+ {...props}
65
+ />
66
+ }
67
+
68
+ export function TableCell({ className, ...props }: Props) @{
69
+ <td
70
+ data-slot="table-cell"
71
+ className={cn(
72
+ 'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
73
+ className,
74
+ )}
75
+ {...props}
76
+ />
77
+ }
78
+
79
+ export function TableCaption({ className, ...props }: Props) @{
80
+ <caption
81
+ data-slot="table-caption"
82
+ className={cn('text-muted-foreground mt-4 text-sm', className)}
83
+ {...props}
84
+ />
85
+ }
@@ -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
+ }
@@ -0,0 +1,124 @@
1
+ // Base UI base toggle-group — runs on @octanejs/base-ui's ToggleGroup.
2
+ //
3
+ // THERE IS NO `Item` PART. Verified by rendering the primitive: Base UI's ToggleGroup is a single
4
+ // component whose children are ordinary `Toggle`s carrying a `value`, where radix pairs
5
+ // `ToggleGroup.Root` with `ToggleGroup.Item`. So `ToggleGroupItem` here wraps `Toggle` — the same
6
+ // primitive `toggle.tsrx` uses — and the exported names and `data-slot` values stay as shadcn
7
+ // defines them, so consumers see no difference between bases.
8
+ //
9
+ // TWO API DIFFERENCES A CONSUMER CAN OBSERVE, both inherent to the primitive:
10
+ //
11
+ // 1. `type="single" | "multiple"` is translated to Base UI's `multiple` boolean, so markup
12
+ // written against the radix base keeps working.
13
+ //
14
+ // 2. VALUES ARE ALWAYS ARRAYS. Base UI's `value` / `defaultValue` / `onValueChange` speak
15
+ // `string[]` in both modes, where radix's single mode speaks a bare `string`. That is not
16
+ // translated: faking a scalar would mean inventing conversion behavior on every change, and
17
+ // silently reshaping the argument to the consumer's handler is worse than a documented
18
+ // difference. Single mode still yields at most one entry.
19
+ //
20
+ // The pressed dialect is `data-pressed`, not radix's `data-[state=on]` — see toggle.tsrx, whose
21
+ // `toggleVariants` this file reuses, so the two bases cannot drift apart.
22
+ //
23
+ // ToggleGroup is composed with createElement: the consumer's children must sit INSIDE the context
24
+ // Provider, and opaque compiled .tsrx children can only flow through the props.children channel
25
+ // (see breadcrumb.tsrx's comment).
26
+ //
27
+ // UNVERIFIED PROVENANCE: class strings come from this package's radix base rather than
28
+ // transcribed from upstream's Base UI source.
29
+ import { createContext, createElement, useContext, type OctaneNode } from 'octane';
30
+ import { type VariantProps } from 'class-variance-authority';
31
+ import { ToggleGroup as ToggleGroupPrimitive } from '@octanejs/base-ui/toggle-group';
32
+ import { Toggle as TogglePrimitive } from '@octanejs/base-ui/toggle';
33
+
34
+ import { cn } from '../../../lib/utils';
35
+ import { toggleVariants } from './toggle.tsrx';
36
+
37
+ const ToggleGroupContext = createContext<
38
+ VariantProps<typeof toggleVariants> & {
39
+ spacing?: number;
40
+ orientation?: 'horizontal' | 'vertical';
41
+ }
42
+ >({
43
+ size: 'default',
44
+ variant: 'default',
45
+ spacing: 2,
46
+ orientation: 'horizontal',
47
+ });
48
+
49
+ export interface ToggleGroupProps extends Record<string, unknown> {
50
+ className?: string;
51
+ variant?: VariantProps<typeof toggleVariants>['variant'];
52
+ size?: VariantProps<typeof toggleVariants>['size'];
53
+ spacing?: number;
54
+ orientation?: 'horizontal' | 'vertical';
55
+ type?: 'single' | 'multiple';
56
+ children?: OctaneNode;
57
+ }
58
+
59
+ export function ToggleGroup({
60
+ className,
61
+ variant,
62
+ size,
63
+ spacing = 2,
64
+ orientation = 'horizontal',
65
+ type,
66
+ children,
67
+ ...props
68
+ }: ToggleGroupProps) {
69
+ return createElement(
70
+ ToggleGroupPrimitive,
71
+ {
72
+ 'data-slot': 'toggle-group',
73
+ 'data-variant': variant,
74
+ 'data-size': size,
75
+ 'data-spacing': spacing,
76
+ // Forwarded to the primitive: it drives the arrow-key axis (a vertical group must
77
+ // ignore left/right and use up/down). The primitive also emits `data-orientation`
78
+ // itself, so unlike the radix base this is not set by hand.
79
+ orientation,
80
+ multiple: type === 'multiple',
81
+ style: { '--gap': spacing },
82
+ className: cn(
83
+ 'group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs',
84
+ className,
85
+ ),
86
+ ...props,
87
+ },
88
+ createElement(
89
+ ToggleGroupContext.Provider,
90
+ { value: { variant, size, spacing, orientation } },
91
+ children,
92
+ ),
93
+ );
94
+ }
95
+
96
+ export interface ToggleGroupItemProps extends Record<string, unknown> {
97
+ className?: string;
98
+ variant?: VariantProps<typeof toggleVariants>['variant'];
99
+ size?: VariantProps<typeof toggleVariants>['size'];
100
+ }
101
+
102
+ export function ToggleGroupItem({
103
+ className,
104
+ variant = 'default',
105
+ size = 'default',
106
+ ...props
107
+ }: ToggleGroupItemProps) @{
108
+ const context = useContext(ToggleGroupContext);
109
+ <TogglePrimitive
110
+ data-slot="toggle-group-item"
111
+ data-variant={context.variant || variant}
112
+ data-size={context.size || size}
113
+ data-spacing={context.spacing}
114
+ className={cn(
115
+ 'min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l',
116
+ toggleVariants({
117
+ variant: context.variant || variant,
118
+ size: context.size || size,
119
+ }),
120
+ className,
121
+ )}
122
+ {...props}
123
+ />
124
+ }
@@ -0,0 +1,57 @@
1
+ // Base UI base toggle — runs on @octanejs/base-ui's Toggle.
2
+ //
3
+ // THE PRESSED-STATE DIALECT DIFFERS, verified by rendering the primitive: Base UI emits a bare
4
+ // `data-pressed` attribute (alongside `aria-pressed`), where radix emits `data-state="on"`. So
5
+ // the radix base's `data-[state=on]:` utilities are replaced with `data-pressed:` — copying them
6
+ // unchanged would leave a toggle that never shows its pressed styling, with nothing failing.
7
+ //
8
+ // `disabled:` is kept rather than converted: Base UI's Toggle renders a real `<button>` (again
9
+ // verified by rendering), so the `:disabled` pseudo-class does match here — unlike checkbox,
10
+ // switch and radio, whose Roots are spans.
11
+ //
12
+ // UNVERIFIED PROVENANCE: the non-conditional utilities come from this package's radix base
13
+ // rather than transcribed from upstream's Base UI source.
14
+ import { cva, type VariantProps } from 'class-variance-authority';
15
+ import { Toggle as TogglePrimitive } from '@octanejs/base-ui/toggle';
16
+
17
+ import { cn } from '../../../lib/utils';
18
+
19
+ export const toggleVariants = cva(
20
+ 'inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-pressed:bg-accent data-pressed:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
21
+ {
22
+ variants: {
23
+ variant: {
24
+ default: 'bg-transparent',
25
+ outline: 'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground',
26
+ },
27
+ size: {
28
+ default: 'h-9 min-w-9 px-2',
29
+ sm: 'h-8 min-w-8 px-1.5',
30
+ lg: 'h-10 min-w-10 px-2.5',
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ variant: 'default',
35
+ size: 'default',
36
+ },
37
+ },
38
+ );
39
+
40
+ export interface ToggleProps extends Record<string, unknown> {
41
+ className?: string;
42
+ variant?: VariantProps<typeof toggleVariants>['variant'];
43
+ size?: VariantProps<typeof toggleVariants>['size'];
44
+ }
45
+
46
+ export function Toggle({
47
+ className,
48
+ variant = 'default',
49
+ size = 'default',
50
+ ...props
51
+ }: ToggleProps) @{
52
+ <TogglePrimitive
53
+ data-slot="toggle"
54
+ className={cn(toggleVariants({ variant, size, className }))}
55
+ {...props}
56
+ />
57
+ }