@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,163 @@
1
+ // Base UI base item — class strings from the maintainer-supplied radix source, verbatim. Ten of the
2
+ // eleven parts are plain host elements, and the eleventh (`ItemSeparator`) delegates to this base's
3
+ // own Separator, so it picks up Base UI's `aria-orientation` dialect automatically.
4
+ //
5
+ // NO DIALECT RISK IN THE VARIANTS, which is worth stating because they look like the ones that
6
+ // caught toggle and slider: `data-[size=…]` and `data-[variant=…]` here read attributes THIS
7
+ // component writes itself, not state emitted by a primitive. Nothing underneath emits anything.
8
+ //
9
+ // `Item` SHIPS WITHOUT `asChild`, the same call as badge and breadcrumb in this base. Radix swaps in
10
+ // `Slot`; React Aria takes a `render` function; Base UI has no Slot, and item has no primitive to
11
+ // borrow a `render` prop from, so nothing here settles the spelling upstream uses. Adding it later
12
+ // is additive; shipping the wrong shape is breaking. A consumer needing a different element can
13
+ // apply `itemVariants({ variant, size })` directly, which is what the class export is for.
14
+ import { cva, type VariantProps } from 'class-variance-authority';
15
+
16
+ import { cn } from '../../../lib/utils';
17
+ import { Separator } from './separator.tsrx';
18
+
19
+ type DivProps = { className?: string } & Record<string, unknown>;
20
+
21
+ export function ItemGroup({ className, ...props }: DivProps) @{
22
+ <div
23
+ role="list"
24
+ data-slot="item-group"
25
+ className={cn(
26
+ 'group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2',
27
+ className,
28
+ )}
29
+ {...props}
30
+ />
31
+ }
32
+
33
+ export function ItemSeparator({ className, ...props }: DivProps) @{
34
+ <Separator
35
+ data-slot="item-separator"
36
+ orientation="horizontal"
37
+ className={cn('my-2', className)}
38
+ {...props}
39
+ />
40
+ }
41
+
42
+ export const itemVariants = cva(
43
+ 'group/item flex w-full flex-wrap items-center rounded-lg border text-sm transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-muted',
44
+ {
45
+ variants: {
46
+ variant: {
47
+ default: 'border-transparent',
48
+ outline: 'border-border',
49
+ muted: 'border-transparent bg-muted/50',
50
+ },
51
+ size: {
52
+ default: 'gap-2.5 px-3 py-2.5',
53
+ sm: 'gap-2.5 px-3 py-2.5',
54
+ xs: 'gap-2 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0',
55
+ },
56
+ },
57
+ defaultVariants: {
58
+ variant: 'default',
59
+ size: 'default',
60
+ },
61
+ },
62
+ );
63
+
64
+ export interface ItemProps extends Record<string, unknown> {
65
+ className?: string;
66
+ /** Not supported in this base — see the header. Declared so it fails to compile, not silently. */
67
+ asChild?: never;
68
+ variant?: VariantProps<typeof itemVariants>['variant'];
69
+ size?: VariantProps<typeof itemVariants>['size'];
70
+ }
71
+
72
+ export function Item({ className, variant = 'default', size = 'default', ...props }: ItemProps) @{
73
+ <div
74
+ data-slot="item"
75
+ data-variant={variant}
76
+ data-size={size}
77
+ className={cn(itemVariants({ variant, size, className }))}
78
+ {...props}
79
+ />
80
+ }
81
+
82
+ export const itemMediaVariants = cva(
83
+ 'flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none',
84
+ {
85
+ variants: {
86
+ variant: {
87
+ default: 'bg-transparent',
88
+ icon: '[&_svg:not([class*=\'size-\'])]:size-4',
89
+ image: 'size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover',
90
+ },
91
+ },
92
+ defaultVariants: {
93
+ variant: 'default',
94
+ },
95
+ },
96
+ );
97
+
98
+ export interface ItemMediaProps extends Record<string, unknown> {
99
+ className?: string;
100
+ variant?: VariantProps<typeof itemMediaVariants>['variant'];
101
+ }
102
+
103
+ export function ItemMedia({ className, variant = 'default', ...props }: ItemMediaProps) @{
104
+ <div
105
+ data-slot="item-media"
106
+ data-variant={variant}
107
+ className={cn(itemMediaVariants({ variant, className }))}
108
+ {...props}
109
+ />
110
+ }
111
+
112
+ export function ItemContent({ className, ...props }: DivProps) @{
113
+ <div
114
+ data-slot="item-content"
115
+ className={cn(
116
+ 'flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0 [&+[data-slot=item-content]]:flex-none',
117
+ className,
118
+ )}
119
+ {...props}
120
+ />
121
+ }
122
+
123
+ export function ItemTitle({ className, ...props }: DivProps) @{
124
+ <div
125
+ data-slot="item-title"
126
+ className={cn(
127
+ 'line-clamp-1 flex w-fit items-center gap-2 text-sm leading-snug font-medium underline-offset-4',
128
+ className,
129
+ )}
130
+ {...props}
131
+ />
132
+ }
133
+
134
+ export function ItemDescription({ className, ...props }: DivProps) @{
135
+ <p
136
+ data-slot="item-description"
137
+ className={cn(
138
+ 'line-clamp-2 text-left text-sm leading-normal font-normal text-muted-foreground group-data-[size=xs]/item:text-xs [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
139
+ className,
140
+ )}
141
+ {...props}
142
+ />
143
+ }
144
+
145
+ export function ItemActions({ className, ...props }: DivProps) @{
146
+ <div data-slot="item-actions" className={cn('flex items-center gap-2', className)} {...props} />
147
+ }
148
+
149
+ export function ItemHeader({ className, ...props }: DivProps) @{
150
+ <div
151
+ data-slot="item-header"
152
+ className={cn('flex basis-full items-center justify-between gap-2', className)}
153
+ {...props}
154
+ />
155
+ }
156
+
157
+ export function ItemFooter({ className, ...props }: DivProps) @{
158
+ <div
159
+ data-slot="item-footer"
160
+ className={cn('flex basis-full items-center justify-between gap-2', className)}
161
+ {...props}
162
+ />
163
+ }
@@ -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,125 @@
1
+ // Base UI base pagination — TRANSCRIBED from upstream's `bases/base-ui/ui/pagination.tsx`
2
+ // (maintainer-supplied), class strings verbatim. There is no pagination primitive in any base;
3
+ // the family is host elements plus button styling, and the only base-specific question is how the
4
+ // link acquires that styling.
5
+ //
6
+ // THE LINK GOES THROUGH `Button`, NOT THROUGH `buttonVariants`. The radix base writes
7
+ // `<Button asChild><a/></Button>`, where Slot is a pure prop-merger that layers no button
8
+ // semantics onto the anchor. Base UI has no Slot, so upstream uses the `render` prop plus
9
+ // `nativeButton={false}` — the documented escape for "this Button is not a native `<button>`".
10
+ //
11
+ // THE TWO BASES THEREFORE DIVERGE OBSERVABLY, and that is upstream's call, not a porting bug:
12
+ // this base's links carry `role="button"` and `tabindex="0"` (verified by rendering) where the
13
+ // radix base's carry neither. `nativeButton={false}` is what keeps keyboard activation working on
14
+ // a non-button element, which is the trade upstream chose. Do not "fix" this toward radix by
15
+ // dropping Button for a bare `buttonVariants()` anchor: it diverges from the source, and it also
16
+ // drops the key handling Base UI attaches.
17
+ //
18
+ // Octane adaptations: lucide-react → @octanejs/lucide, and children are passed explicitly rather
19
+ // than riding along in the props spread — React's `ComponentProps<"a">` carries `children`, octane
20
+ // routes it through its own channel.
21
+ import { createElement, type OctaneNode } from 'octane';
22
+ import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from '@octanejs/lucide';
23
+
24
+ import { cn } from '../../../lib/utils';
25
+ import { Button, type ButtonProps } from './button.tsrx';
26
+
27
+ type Props = { className?: string } & Record<string, unknown>;
28
+
29
+ export function Pagination({ className, ...props }: Props) @{
30
+ <nav
31
+ role="navigation"
32
+ aria-label="pagination"
33
+ data-slot="pagination"
34
+ className={cn('mx-auto flex w-full justify-center', className)}
35
+ {...props}
36
+ />
37
+ }
38
+
39
+ export function PaginationContent({ className, ...props }: Props) @{
40
+ <ul
41
+ data-slot="pagination-content"
42
+ className={cn('flex items-center gap-0.5', className)}
43
+ {...props}
44
+ />
45
+ }
46
+
47
+ export function PaginationItem(props: Record<string, unknown>) @{
48
+ <li data-slot="pagination-item" {...props} />
49
+ }
50
+
51
+ export interface PaginationLinkProps extends Record<string, unknown> {
52
+ className?: string;
53
+ isActive?: boolean;
54
+ size?: ButtonProps['size'];
55
+ children?: OctaneNode;
56
+ }
57
+
58
+ export function PaginationLink({
59
+ className,
60
+ isActive,
61
+ size = 'icon',
62
+ children,
63
+ ...props
64
+ }: PaginationLinkProps) {
65
+ return createElement(Button, {
66
+ variant: isActive ? 'outline' : 'ghost',
67
+ size,
68
+ className: cn(className),
69
+ nativeButton: false,
70
+ render: createElement('a', {
71
+ 'aria-current': isActive ? 'page' : undefined,
72
+ 'data-slot': 'pagination-link',
73
+ 'data-active': isActive,
74
+ ...props,
75
+ children,
76
+ }),
77
+ });
78
+ }
79
+
80
+ export function PaginationPrevious({
81
+ className,
82
+ text = 'Previous',
83
+ ...props
84
+ }: PaginationLinkProps & {
85
+ text?: string;
86
+ }) @{
87
+ <PaginationLink
88
+ aria-label="Go to previous page"
89
+ size="default"
90
+ className={cn('pl-1.5!', className)}
91
+ {...props}
92
+ >
93
+ <ChevronLeftIcon data-icon="inline-start" className="cn-rtl-flip" />
94
+ <span className="hidden sm:block">{text as string}</span>
95
+ </PaginationLink>
96
+ }
97
+
98
+ export function PaginationNext({ className, text = 'Next', ...props }: PaginationLinkProps & {
99
+ text?: string;
100
+ }) @{
101
+ <PaginationLink
102
+ aria-label="Go to next page"
103
+ size="default"
104
+ className={cn('pr-1.5!', className)}
105
+ {...props}
106
+ >
107
+ <span className="hidden sm:block">{text as string}</span>
108
+ <ChevronRightIcon data-icon="inline-end" className="cn-rtl-flip" />
109
+ </PaginationLink>
110
+ }
111
+
112
+ export function PaginationEllipsis({ className, ...props }: Props) @{
113
+ <span
114
+ aria-hidden="true"
115
+ data-slot="pagination-ellipsis"
116
+ className={cn(
117
+ 'flex size-8 items-center justify-center [&_svg:not([class*=\'size-\'])]:size-4',
118
+ className,
119
+ )}
120
+ {...props}
121
+ >
122
+ <MoreHorizontalIcon />
123
+ <span className="sr-only">More pages</span>
124
+ </span>
125
+ }
@@ -0,0 +1,124 @@
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
+ // POSITIONING PROPS BELONG TO THE POSITIONER, NOT THE POPUP. Radix has one `Content` element that
25
+ // takes them all; Base UI splits positioning into its own layer, so anything forwarded to `Popup`
26
+ // instead is inert — `side="top"` silently leaves the popup on the default side, and the prop lands
27
+ // on the DOM as an invalid attribute. They are destructured and routed explicitly.
28
+ //
29
+ import { createElement } from 'octane';
30
+ import { Popover as PopoverPrimitive } from '@octanejs/base-ui/popover';
31
+
32
+ import { cn } from '../../../lib/utils';
33
+
34
+ type Props = { className?: string } & Record<string, unknown>;
35
+
36
+ export function Popover(props: Record<string, unknown>) @{
37
+ <PopoverPrimitive.Root data-slot="popover" {...props} />
38
+ }
39
+
40
+ export function PopoverTrigger(props: Record<string, unknown>) @{
41
+ <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
42
+ }
43
+
44
+ export interface BaseUiPositioningProps {
45
+ /** Forwarded to the Positioner; see the note at the top of this file. */
46
+ side?: 'top' | 'right' | 'bottom' | 'left';
47
+ sideOffset?: number;
48
+ align?: 'start' | 'center' | 'end';
49
+ alignOffset?: number;
50
+ anchor?: unknown;
51
+ positionMethod?: 'absolute' | 'fixed';
52
+ collisionBoundary?: unknown;
53
+ collisionPadding?: unknown;
54
+ collisionAvoidance?: unknown;
55
+ arrowPadding?: number;
56
+ sticky?: boolean;
57
+ disableAnchorTracking?: boolean;
58
+ }
59
+
60
+ export interface PopoverContentProps extends Record<string, unknown>, BaseUiPositioningProps {
61
+ className?: string;
62
+ }
63
+
64
+ export function PopoverContent({
65
+ className,
66
+ align = 'center',
67
+ sideOffset = 4,
68
+ side,
69
+ alignOffset,
70
+ anchor,
71
+ positionMethod,
72
+ collisionBoundary,
73
+ collisionPadding,
74
+ collisionAvoidance,
75
+ arrowPadding,
76
+ sticky,
77
+ disableAnchorTracking,
78
+ ...props
79
+ }: PopoverContentProps) {
80
+ return createElement(PopoverPrimitive.Portal, {
81
+ children: createElement(PopoverPrimitive.Positioner, {
82
+ side,
83
+ sideOffset,
84
+ align,
85
+ alignOffset,
86
+ anchor,
87
+ positionMethod,
88
+ collisionBoundary,
89
+ collisionPadding,
90
+ collisionAvoidance,
91
+ arrowPadding,
92
+ sticky,
93
+ disableAnchorTracking,
94
+ children: createElement(PopoverPrimitive.Popup, {
95
+ 'data-slot': 'popover-content',
96
+ className: cn(
97
+ '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',
98
+ className,
99
+ ),
100
+ ...props,
101
+ }),
102
+ }),
103
+ });
104
+ }
105
+
106
+ export function PopoverHeader({ className, ...props }: Props) @{
107
+ <div
108
+ data-slot="popover-header"
109
+ className={cn('flex flex-col gap-0.5 text-sm', className)}
110
+ {...props}
111
+ />
112
+ }
113
+
114
+ export function PopoverTitle({ className, ...props }: Props) @{
115
+ <div data-slot="popover-title" className={cn('font-medium', className)} {...props} />
116
+ }
117
+
118
+ export function PopoverDescription({ className, ...props }: Props) @{
119
+ <p
120
+ data-slot="popover-description"
121
+ className={cn('text-muted-foreground', className)}
122
+ {...props}
123
+ />
124
+ }
@@ -0,0 +1,46 @@
1
+ // Base UI base progress — runs on @octanejs/base-ui's Progress.
2
+ //
3
+ // TWO STRUCTURAL DIFFERENCES FROM THE RADIX BASE, both verified by rendering the primitive
4
+ // rather than inferred:
5
+ //
6
+ // 1. THERE IS A TRACK PART. Radix goes Root > Indicator; Base UI goes Root > Track > Indicator,
7
+ // and the Track is what carries the rail. Rendering an Indicator directly under Root would
8
+ // put it outside the element the primitive sizes against.
9
+ //
10
+ // 2. THE INDICATOR SIZES ITSELF. Radix leaves the fill entirely to the consumer, which is why
11
+ // its base ships `style={{ transform: translateX(-${100 - value}%) }}`. Base UI's Indicator
12
+ // already emits `width: <value>%` inline. Carrying the radix transform across would shift a
13
+ // correctly-sized bar sideways by the same amount again — a double offset, worst at the
14
+ // midpoint and invisible at 0% and 100%.
15
+ //
16
+ // So `value` is forwarded to the Root (which owns the aria-valuenow/valuetext contract) and NOT
17
+ // turned into a transform.
18
+ //
19
+ // UNVERIFIED PROVENANCE: class strings come from this package's radix base rather than
20
+ // transcribed from upstream's Base UI source; the rail/fill classes are split across Track and
21
+ // Indicator to match the extra part.
22
+ import { Progress as ProgressPrimitive } from '@octanejs/base-ui/progress';
23
+
24
+ import { cn } from '../../../lib/utils';
25
+
26
+ export interface ProgressProps extends Record<string, unknown> {
27
+ className?: string;
28
+ value?: number | null;
29
+ }
30
+
31
+ export function Progress({ className, value, ...props }: ProgressProps) @{
32
+ // Base UI's Root types `value` as `number | null` where shadcn's prop is optional, and null is
33
+ // its documented indeterminate state — so an omitted value maps to null rather than being
34
+ // forwarded as undefined.
35
+ <ProgressPrimitive.Root data-slot="progress" value={value ?? null} {...props}>
36
+ <ProgressPrimitive.Track
37
+ data-slot="progress-track"
38
+ className={cn('relative h-2 w-full overflow-hidden rounded-full bg-primary/20', className)}
39
+ >
40
+ <ProgressPrimitive.Indicator
41
+ data-slot="progress-indicator"
42
+ className="h-full bg-primary transition-all"
43
+ />
44
+ </ProgressPrimitive.Track>
45
+ </ProgressPrimitive.Root>
46
+ }
@@ -0,0 +1,62 @@
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
+ // THE ROOT MUST CARRY ITS OWN DISPLAY, and this is the second consequence of that same `<span>`.
14
+ // Radix's radio root is a `<button>`, which is `display: inline-block` by default, so its class
15
+ // string never needed one and `size-4` just worked. A bare `<span>` is `display: inline`, where
16
+ // width and height are IGNORED — the box collapses and only `border` paints, rendering the control
17
+ // as a thin vertical bar beside its label rather than a circle. The React Aria base hit the same
18
+ // thing (its root is not a button either) and carries `relative flex` for it; this base copied the
19
+ // radix string and lost it. Hence `relative flex … items-center justify-center` here.
20
+ //
21
+ // The indicator is `size-full` rather than `relative` so the dot, which positions itself with
22
+ // `absolute … -translate-1/2`, anchors to the ROOT's 16px box — matching the React Aria base. Left
23
+ // `relative`, it would anchor to a zero-sized flex item and sit off-centre.
24
+ //
25
+ // UNVERIFIED PROVENANCE: the non-conditional utilities come from this package's radix base
26
+ // rather than upstream's Base UI source.
27
+ import { RadioGroup as RadioGroupPrimitive } from '@octanejs/base-ui/radio-group';
28
+ import { Radio as RadioPrimitive } from '@octanejs/base-ui/radio';
29
+ import { CircleIcon } from '@octanejs/lucide';
30
+
31
+ import { cn } from '../../../lib/utils';
32
+
33
+ type Props = { className?: string } & Record<string, unknown>;
34
+
35
+ // Base UI's Radio.Root requires `value` (Radix infers it from the item), so the item's props
36
+ // type demands it. shadcn usage always passes one, so this tightens the type without changing
37
+ // the surface.
38
+ type ItemProps = { className?: string; value: any } & Record<string, unknown>;
39
+
40
+ export function RadioGroup({ className, ...props }: Props) @{
41
+ <RadioGroupPrimitive data-slot="radio-group" className={cn('grid gap-3', className)} {...props} />
42
+ }
43
+
44
+ export function RadioGroupItem({ className, ...props }: ItemProps) @{
45
+ <RadioPrimitive.Root
46
+ data-slot="radio-group-item"
47
+ className={cn(
48
+ 'relative flex aspect-square size-4 shrink-0 items-center justify-center 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',
49
+ className,
50
+ )}
51
+ {...props}
52
+ >
53
+ <RadioPrimitive.Indicator
54
+ data-slot="radio-group-indicator"
55
+ className="flex size-full items-center justify-center"
56
+ >
57
+ <CircleIcon
58
+ className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary"
59
+ />
60
+ </RadioPrimitive.Indicator>
61
+ </RadioPrimitive.Root>
62
+ }
@@ -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
+ }