@octanejs/shadcn 0.0.8 → 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 +25 -8
- 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/collapsible.tsrx +25 -0
- package/src/bases/base-ui/ui/dropdown-menu.tsrx +292 -0
- package/src/bases/base-ui/ui/field.tsrx +201 -0
- package/src/bases/base-ui/ui/item.tsrx +163 -0
- package/src/bases/base-ui/ui/pagination.tsrx +125 -0
- package/src/bases/base-ui/ui/popover.tsrx +43 -4
- package/src/bases/base-ui/ui/progress.tsrx +46 -0
- package/src/bases/base-ui/ui/radio-group.tsrx +14 -2
- package/src/bases/base-ui/ui/sheet.tsrx +121 -0
- package/src/bases/base-ui/ui/slider.tsrx +111 -0
- package/src/bases/base-ui/ui/table.tsrx +85 -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 +39 -1
|
@@ -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
|
+
}
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
//
|
|
22
22
|
// UNVERIFIED PROVENANCE: class strings come from this package's radix base rather than
|
|
23
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
|
+
//
|
|
24
29
|
import { createElement } from 'octane';
|
|
25
30
|
import { Popover as PopoverPrimitive } from '@octanejs/base-ui/popover';
|
|
26
31
|
|
|
@@ -36,22 +41,56 @@ export function PopoverTrigger(props: Record<string, unknown>) @{
|
|
|
36
41
|
<PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
export interface
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
export interface BaseUiPositioningProps {
|
|
45
|
+
/** Forwarded to the Positioner; see the note at the top of this file. */
|
|
46
|
+
side?: 'top' | 'right' | 'bottom' | 'left';
|
|
42
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;
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
export function PopoverContent({
|
|
46
65
|
className,
|
|
47
66
|
align = 'center',
|
|
48
67
|
sideOffset = 4,
|
|
68
|
+
side,
|
|
69
|
+
alignOffset,
|
|
70
|
+
anchor,
|
|
71
|
+
positionMethod,
|
|
72
|
+
collisionBoundary,
|
|
73
|
+
collisionPadding,
|
|
74
|
+
collisionAvoidance,
|
|
75
|
+
arrowPadding,
|
|
76
|
+
sticky,
|
|
77
|
+
disableAnchorTracking,
|
|
49
78
|
...props
|
|
50
79
|
}: PopoverContentProps) {
|
|
51
80
|
return createElement(PopoverPrimitive.Portal, {
|
|
52
81
|
children: createElement(PopoverPrimitive.Positioner, {
|
|
53
|
-
|
|
82
|
+
side,
|
|
54
83
|
sideOffset,
|
|
84
|
+
align,
|
|
85
|
+
alignOffset,
|
|
86
|
+
anchor,
|
|
87
|
+
positionMethod,
|
|
88
|
+
collisionBoundary,
|
|
89
|
+
collisionPadding,
|
|
90
|
+
collisionAvoidance,
|
|
91
|
+
arrowPadding,
|
|
92
|
+
sticky,
|
|
93
|
+
disableAnchorTracking,
|
|
55
94
|
children: createElement(PopoverPrimitive.Popup, {
|
|
56
95
|
'data-slot': 'popover-content',
|
|
57
96
|
className: cn(
|
|
@@ -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
|
+
}
|
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
// renders a `<span role="radio">`, which is never `:disabled`, so the pseudo-class variant could
|
|
11
11
|
// never match. The primitive publishes `data-disabled` instead.
|
|
12
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
|
+
//
|
|
13
25
|
// UNVERIFIED PROVENANCE: the non-conditional utilities come from this package's radix base
|
|
14
26
|
// rather than upstream's Base UI source.
|
|
15
27
|
import { RadioGroup as RadioGroupPrimitive } from '@octanejs/base-ui/radio-group';
|
|
@@ -33,14 +45,14 @@ export function RadioGroupItem({ className, ...props }: ItemProps) @{
|
|
|
33
45
|
<RadioPrimitive.Root
|
|
34
46
|
data-slot="radio-group-item"
|
|
35
47
|
className={cn(
|
|
36
|
-
'aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 data-disabled:cursor-not-allowed data-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40',
|
|
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',
|
|
37
49
|
className,
|
|
38
50
|
)}
|
|
39
51
|
{...props}
|
|
40
52
|
>
|
|
41
53
|
<RadioPrimitive.Indicator
|
|
42
54
|
data-slot="radio-group-indicator"
|
|
43
|
-
className="
|
|
55
|
+
className="flex size-full items-center justify-center"
|
|
44
56
|
>
|
|
45
57
|
<CircleIcon
|
|
46
58
|
className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary"
|
|
@@ -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,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,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
|
+
}
|