@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.
- package/README.md +170 -3
- package/package.json +46 -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/avatar.tsrx +89 -0
- package/src/bases/base-ui/ui/badge.tsrx +56 -0
- package/src/bases/base-ui/ui/breadcrumb.tsrx +109 -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/collapsible.tsrx +25 -0
- package/src/bases/base-ui/ui/dialog.tsrx +137 -0
- package/src/bases/base-ui/ui/dropdown-menu.tsrx +292 -0
- package/src/bases/base-ui/ui/empty.tsrx +88 -0
- package/src/bases/base-ui/ui/field.tsrx +201 -0
- package/src/bases/base-ui/ui/input.tsrx +28 -0
- package/src/bases/base-ui/ui/item.tsrx +163 -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/pagination.tsrx +125 -0
- package/src/bases/base-ui/ui/popover.tsrx +124 -0
- package/src/bases/base-ui/ui/progress.tsrx +46 -0
- package/src/bases/base-ui/ui/radio-group.tsrx +62 -0
- package/src/bases/base-ui/ui/separator.tsrx +34 -0
- package/src/bases/base-ui/ui/sheet.tsrx +121 -0
- package/src/bases/base-ui/ui/skeleton.tsrx +17 -0
- package/src/bases/base-ui/ui/slider.tsrx +111 -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/table.tsrx +85 -0
- package/src/bases/base-ui/ui/textarea.tsrx +22 -0
- package/src/bases/base-ui/ui/toggle-group.tsrx +124 -0
- package/src/bases/base-ui/ui/toggle.tsrx +57 -0
- 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
|
+
}
|