@ponti-studios/ui 1.8.0 → 2.0.1
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 +54 -20
- package/dist/styles.css +3 -0
- package/package.json +28 -9
- package/scripts/create-project.mjs +86 -0
- package/src/components/forms/checkbox.stories.tsx +35 -0
- package/src/components/forms/checkbox.tsx +38 -0
- package/src/components/forms/index.ts +2 -0
- package/src/components/forms/label.stories.tsx +16 -0
- package/src/components/forms/label.tsx +25 -0
- package/src/components/forms/select.tsx +3 -1
- package/src/components/overlays/bottom-sheet.stories.tsx +52 -0
- package/src/components/overlays/bottom-sheet.tsx +179 -0
- package/src/components/overlays/command.tsx +1 -1
- package/src/components/overlays/dropdown-menu.tsx +6 -2
- package/src/components/overlays/index.ts +2 -0
- package/src/components/overlays/popover.tsx +12 -3
- package/src/components/overlays/tooltip.stories.tsx +29 -0
- package/src/components/overlays/tooltip.tsx +41 -0
- package/src/components/primitives/index.ts +2 -0
- package/src/components/primitives/segmented-control.stories.tsx +54 -0
- package/src/components/primitives/segmented-control.tsx +73 -0
- package/src/components/primitives/separator.stories.tsx +34 -0
- package/src/components/primitives/separator.tsx +27 -0
- package/src/index.ts +2 -0
- package/src/native/components.tsx +198 -0
- package/src/native/index.ts +2 -0
- package/src/native/theme.tsx +69 -0
- package/src/styles/build.ts +1 -0
- package/src/styles/index.css +94 -0
- package/src/styles/package.css +3 -0
- package/src/styles/tokens/generated/dark-class.css +39 -0
- package/src/styles/tokens/generated/dark.colors.generated.ts +31 -31
- package/src/styles/tokens/generated/dark.css +31 -31
- package/src/styles/tokens/generated/foundations.css +2 -2
- package/src/styles/tokens/generated/foundations.generated.ts +2 -0
- package/src/styles/tokens/generated/light.colors.generated.ts +30 -30
- package/src/styles/tokens/generated/light.css +30 -30
- package/src/styles/tokens/index.ts +6 -6
- package/src/styles/tokens/scripts/check-source.mjs +9 -6
- package/src/styles/tokens/source/dark.tokens.json +33 -32
- package/src/styles/tokens/source/foundations.tokens.json +2 -0
- package/src/styles/tokens/source/light.tokens.json +32 -31
- package/src/styles/tokens/style-dictionary.dark-class.json +16 -0
- package/templates/web-vite/index.html +13 -0
- package/templates/web-vite/package.json +27 -0
- package/templates/web-vite/src/App.tsx +31 -0
- package/templates/web-vite/src/main.tsx +12 -0
- package/templates/web-vite/src/styles.css +110 -0
- package/templates/web-vite/tsconfig.json +21 -0
- package/templates/web-vite/vite.config.ts +6 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { BottomSheet } from "./bottom-sheet";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Overlays/BottomSheet",
|
|
7
|
+
component: BottomSheet,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
} satisfies Meta<typeof BottomSheet>;
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof meta>;
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
label: "Example panel",
|
|
17
|
+
children: "Panel content",
|
|
18
|
+
},
|
|
19
|
+
render: () => (
|
|
20
|
+
<div className="bg-muted relative h-105 w-120 overflow-hidden rounded-xl border">
|
|
21
|
+
<BottomSheet
|
|
22
|
+
label="Example panel"
|
|
23
|
+
defaultExpanded
|
|
24
|
+
className="bg-card absolute inset-x-4 bottom-4 max-h-[70%] rounded-2xl border shadow-lg"
|
|
25
|
+
scrollClassName="p-4"
|
|
26
|
+
>
|
|
27
|
+
<p className="text-sm font-semibold">Drag the handle or press it to collapse.</p>
|
|
28
|
+
<p className="text-muted-foreground mt-1 text-sm">
|
|
29
|
+
Non-modal persistent panel. Positioning and sizing are app-owned via className.
|
|
30
|
+
</p>
|
|
31
|
+
</BottomSheet>
|
|
32
|
+
</div>
|
|
33
|
+
),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Collapsed: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
label: "Example panel",
|
|
39
|
+
children: "Panel content",
|
|
40
|
+
},
|
|
41
|
+
render: () => (
|
|
42
|
+
<div className="bg-muted relative h-105 w-120 overflow-hidden rounded-xl border">
|
|
43
|
+
<BottomSheet
|
|
44
|
+
label="Example panel"
|
|
45
|
+
className="bg-card absolute inset-x-4 bottom-4 max-h-[70%] rounded-2xl border shadow-lg"
|
|
46
|
+
scrollClassName="p-4"
|
|
47
|
+
>
|
|
48
|
+
<p className="text-sm font-semibold">Starts collapsed to its 48px peek.</p>
|
|
49
|
+
</BottomSheet>
|
|
50
|
+
</div>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
// Non-modal persistent bottom panel with drag-to-collapse/expand.
|
|
8
|
+
// Positioning and sizing are app-owned via className (the kit only sets
|
|
9
|
+
// structure + the translateY transform; Tailwind translate utilities compose
|
|
10
|
+
// via the independent CSS `translate` property).
|
|
11
|
+
//
|
|
12
|
+
// Distinct from Sheet: Sheet is a modal dialog; BottomSheet is always
|
|
13
|
+
// mounted chrome (maps, players, detail panels).
|
|
14
|
+
|
|
15
|
+
const FLICK_VELOCITY = 0.4;
|
|
16
|
+
const DRAG_THRESHOLD = 0.25;
|
|
17
|
+
|
|
18
|
+
export type BottomSheetProps = {
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
/** Controlled expanded state. */
|
|
21
|
+
expanded?: boolean;
|
|
22
|
+
/** Uncontrolled initial state. @default false */
|
|
23
|
+
defaultExpanded?: boolean;
|
|
24
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
25
|
+
/** Visible stub height when collapsed. @default 48 */
|
|
26
|
+
peek?: number;
|
|
27
|
+
/** Accessible name for the panel region. @default "Details panel" */
|
|
28
|
+
label?: string;
|
|
29
|
+
/** Positioning/sizing (app-owned). */
|
|
30
|
+
className?: string;
|
|
31
|
+
/** Applied to the scrollable content wrapper alongside its defaults. */
|
|
32
|
+
scrollClassName?: string;
|
|
33
|
+
/** Applied to the drag handle button alongside its defaults. */
|
|
34
|
+
handleClassName?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function BottomSheet({
|
|
38
|
+
children,
|
|
39
|
+
expanded: controlledExpanded,
|
|
40
|
+
defaultExpanded = false,
|
|
41
|
+
onExpandedChange,
|
|
42
|
+
peek = 48,
|
|
43
|
+
label = "Details panel",
|
|
44
|
+
className,
|
|
45
|
+
scrollClassName,
|
|
46
|
+
handleClassName,
|
|
47
|
+
}: BottomSheetProps) {
|
|
48
|
+
const sheetRef = React.useRef<HTMLElement>(null);
|
|
49
|
+
const [translateY, setTranslateY] = React.useState(0);
|
|
50
|
+
const [dragOffset, setDragOffset] = React.useState(0);
|
|
51
|
+
const [internalExpanded, setInternalExpanded] = React.useState(defaultExpanded);
|
|
52
|
+
const isDragging = React.useRef(false);
|
|
53
|
+
const didDrag = React.useRef(false);
|
|
54
|
+
const dragRef = React.useRef<{ startY: number; startTime: number } | null>(null);
|
|
55
|
+
const reduceMotion = React.useRef(
|
|
56
|
+
typeof window !== "undefined" &&
|
|
57
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const expanded = controlledExpanded ?? internalExpanded;
|
|
61
|
+
|
|
62
|
+
const getHeight = React.useCallback(() => sheetRef.current?.offsetHeight ?? 400, []);
|
|
63
|
+
const closedY = React.useCallback(() => Math.max(0, getHeight() - peek), [getHeight, peek]);
|
|
64
|
+
|
|
65
|
+
const setExpanded = React.useCallback(
|
|
66
|
+
(next: boolean) => {
|
|
67
|
+
onExpandedChange?.(next);
|
|
68
|
+
if (controlledExpanded === undefined) setInternalExpanded(next);
|
|
69
|
+
},
|
|
70
|
+
[controlledExpanded, onExpandedChange],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const snapTo = React.useCallback((targetY: number) => {
|
|
74
|
+
setTranslateY(targetY);
|
|
75
|
+
setDragOffset(0);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
// Sync visuals with expanded state (and settle closed on mount).
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
snapTo(expanded ? 0 : closedY());
|
|
81
|
+
}, [expanded, closedY, snapTo]);
|
|
82
|
+
|
|
83
|
+
const open = React.useCallback(() => setExpanded(true), [setExpanded]);
|
|
84
|
+
const close = React.useCallback(() => setExpanded(false), [setExpanded]);
|
|
85
|
+
|
|
86
|
+
const onPointerDown = React.useCallback((e: React.PointerEvent) => {
|
|
87
|
+
isDragging.current = true;
|
|
88
|
+
didDrag.current = false;
|
|
89
|
+
dragRef.current = { startY: e.clientY, startTime: Date.now() };
|
|
90
|
+
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const onPointerMove = React.useCallback(
|
|
94
|
+
(e: React.PointerEvent) => {
|
|
95
|
+
if (!isDragging.current || !dragRef.current) return;
|
|
96
|
+
const delta = e.clientY - dragRef.current.startY;
|
|
97
|
+
if (Math.abs(delta) > 4) didDrag.current = true;
|
|
98
|
+
setDragOffset(Math.max(-translateY, delta));
|
|
99
|
+
},
|
|
100
|
+
[translateY],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const endDrag = React.useCallback(
|
|
104
|
+
(e: React.PointerEvent) => {
|
|
105
|
+
if (!isDragging.current || !dragRef.current) return;
|
|
106
|
+
isDragging.current = false;
|
|
107
|
+
|
|
108
|
+
const delta = e.clientY - dragRef.current.startY;
|
|
109
|
+
const elapsed = Date.now() - dragRef.current.startTime;
|
|
110
|
+
const velocity = elapsed > 0 ? delta / elapsed : 0;
|
|
111
|
+
dragRef.current = null;
|
|
112
|
+
setDragOffset(0);
|
|
113
|
+
|
|
114
|
+
if (didDrag.current) {
|
|
115
|
+
const height = getHeight();
|
|
116
|
+
const newY = Math.max(0, translateY + delta);
|
|
117
|
+
const fraction = height > 0 ? newY / height : 0;
|
|
118
|
+
|
|
119
|
+
if (velocity > FLICK_VELOCITY) close();
|
|
120
|
+
else if (velocity < -FLICK_VELOCITY) open();
|
|
121
|
+
else if (fraction > DRAG_THRESHOLD) close();
|
|
122
|
+
else open();
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
[translateY, getHeight, open, close],
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const onHandleClick = React.useCallback(() => {
|
|
129
|
+
if (didDrag.current) return;
|
|
130
|
+
if (expanded) close();
|
|
131
|
+
else open();
|
|
132
|
+
}, [expanded, open, close]);
|
|
133
|
+
|
|
134
|
+
const currentY = Math.max(0, translateY + dragOffset);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<section
|
|
138
|
+
ref={sheetRef}
|
|
139
|
+
data-slot="bottom-sheet"
|
|
140
|
+
aria-label={label}
|
|
141
|
+
className={cn("flex touch-none flex-col overflow-hidden", className)}
|
|
142
|
+
style={{
|
|
143
|
+
transform: `translateY(${currentY}px)`,
|
|
144
|
+
transition: isDragging.current
|
|
145
|
+
? "none"
|
|
146
|
+
: reduceMotion.current
|
|
147
|
+
? "none"
|
|
148
|
+
: "transform 0.35s cubic-bezier(0.32, 0.72, 0, 1)",
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
data-slot="bottom-sheet-handle"
|
|
154
|
+
aria-expanded={expanded}
|
|
155
|
+
aria-label={expanded ? "Collapse panel" : "Expand panel"}
|
|
156
|
+
onPointerDown={onPointerDown}
|
|
157
|
+
onPointerMove={onPointerMove}
|
|
158
|
+
onPointerUp={endDrag}
|
|
159
|
+
onPointerCancel={endDrag}
|
|
160
|
+
onClick={onHandleClick}
|
|
161
|
+
onKeyDown={(e) => {
|
|
162
|
+
if (e.key === "Escape") close();
|
|
163
|
+
}}
|
|
164
|
+
className={cn(
|
|
165
|
+
"flex w-full shrink-0 cursor-grab touch-none justify-center py-3.5 active:cursor-grabbing",
|
|
166
|
+
handleClassName,
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
169
|
+
<span aria-hidden="true" className="bg-border h-1 w-9 rounded-full" />
|
|
170
|
+
</button>
|
|
171
|
+
<div
|
|
172
|
+
data-slot="bottom-sheet-scroll"
|
|
173
|
+
className={cn("flex-1 overflow-y-auto", scrollClassName)}
|
|
174
|
+
>
|
|
175
|
+
{children}
|
|
176
|
+
</div>
|
|
177
|
+
</section>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -169,7 +169,7 @@ function CommandItem({ className, onSelect, onClick, ...props }: CommandItemProp
|
|
|
169
169
|
<CommandPrimitive.Item
|
|
170
170
|
data-slot="command-item"
|
|
171
171
|
className={cn(
|
|
172
|
-
"data-highlighted:bg-muted data-highlighted:text-muted-foreground relative flex cursor-
|
|
172
|
+
"data-highlighted:bg-muted data-highlighted:text-muted-foreground relative flex cursor-pointer items-center gap-3 rounded-md px-3 py-2.5 text-sm outline-hidden select-none",
|
|
173
173
|
"data-disabled:pointer-events-none data-disabled:opacity-50",
|
|
174
174
|
className,
|
|
175
175
|
)}
|
|
@@ -38,11 +38,15 @@ function DropdownMenuContent({
|
|
|
38
38
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Popup> & { sideOffset?: number }) {
|
|
39
39
|
return (
|
|
40
40
|
<DropdownMenuPrimitive.Portal>
|
|
41
|
-
<DropdownMenuPrimitive.
|
|
41
|
+
<DropdownMenuPrimitive.Backdrop className="ui-mobile-sheet-backdrop" />
|
|
42
|
+
<DropdownMenuPrimitive.Positioner
|
|
43
|
+
className="ui-mobile-sheet-positioner"
|
|
44
|
+
sideOffset={sideOffset}
|
|
45
|
+
>
|
|
42
46
|
<DropdownMenuPrimitive.Popup
|
|
43
47
|
data-slot="dropdown-menu-content"
|
|
44
48
|
className={cn(
|
|
45
|
-
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 z-50 max-h-(--available-height) min-w-[8rem] origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 outline-none",
|
|
49
|
+
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 z-50 max-h-(--available-height) min-w-[8rem] origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 outline-none ui-mobile-sheet-popup",
|
|
46
50
|
className,
|
|
47
51
|
)}
|
|
48
52
|
{...props}
|
|
@@ -40,6 +40,8 @@ export {
|
|
|
40
40
|
DropdownMenuTrigger,
|
|
41
41
|
} from "./dropdown-menu";
|
|
42
42
|
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "./popover";
|
|
43
|
+
export { BottomSheet, type BottomSheetProps } from "./bottom-sheet";
|
|
44
|
+
export { Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger } from "./tooltip";
|
|
43
45
|
export {
|
|
44
46
|
Sheet,
|
|
45
47
|
SheetClose,
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
|
|
2
|
+
import { XIcon } from "lucide-react";
|
|
2
3
|
import * as React from "react";
|
|
3
4
|
|
|
5
|
+
import { useIsMobile } from "../../hooks/use-mobile";
|
|
4
6
|
import { cn } from "../../lib/utils";
|
|
5
7
|
|
|
6
|
-
function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
|
7
|
-
|
|
8
|
+
function Popover({ modal, ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
|
9
|
+
const isMobile = useIsMobile();
|
|
10
|
+
return <PopoverPrimitive.Root data-slot="popover" modal={modal ?? isMobile} {...props} />;
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
function PopoverTrigger({
|
|
@@ -41,7 +44,9 @@ function PopoverContent({
|
|
|
41
44
|
}) {
|
|
42
45
|
return (
|
|
43
46
|
<PopoverPrimitive.Portal>
|
|
47
|
+
<PopoverPrimitive.Backdrop className="ui-mobile-sheet-backdrop" />
|
|
44
48
|
<PopoverPrimitive.Positioner
|
|
49
|
+
className="ui-mobile-sheet-positioner"
|
|
45
50
|
align={align}
|
|
46
51
|
side={side}
|
|
47
52
|
sideOffset={sideOffset}
|
|
@@ -50,12 +55,16 @@ function PopoverContent({
|
|
|
50
55
|
<PopoverPrimitive.Popup
|
|
51
56
|
data-slot="popover-content"
|
|
52
57
|
className={cn(
|
|
53
|
-
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 z-50 w-72 origin-(--transform-origin) rounded-md border p-4 outline-hidden",
|
|
58
|
+
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 relative z-50 w-72 origin-(--transform-origin) rounded-md border p-4 outline-hidden ui-mobile-sheet-popup ui-mobile-popover-popup",
|
|
54
59
|
className,
|
|
55
60
|
)}
|
|
56
61
|
{...props}
|
|
57
62
|
>
|
|
58
63
|
{children}
|
|
64
|
+
<PopoverPrimitive.Close className="ui-mobile-sheet-close">
|
|
65
|
+
<XIcon aria-hidden="true" />
|
|
66
|
+
<span className="sr-only">Close</span>
|
|
67
|
+
</PopoverPrimitive.Close>
|
|
59
68
|
</PopoverPrimitive.Popup>
|
|
60
69
|
</PopoverPrimitive.Positioner>
|
|
61
70
|
</PopoverPrimitive.Portal>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { Button } from "../primitives/button";
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Overlays/Tooltip",
|
|
8
|
+
component: TooltipContent,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
} satisfies Meta<typeof TooltipContent>;
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof meta>;
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
args: { children: "Save changes" },
|
|
17
|
+
render: (args) => (
|
|
18
|
+
<Tooltip>
|
|
19
|
+
<TooltipTrigger
|
|
20
|
+
render={
|
|
21
|
+
<Button variant="outline" size="sm">
|
|
22
|
+
Hover me
|
|
23
|
+
</Button>
|
|
24
|
+
}
|
|
25
|
+
/>
|
|
26
|
+
<TooltipContent {...args} />
|
|
27
|
+
</Tooltip>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
|
|
8
|
+
const TooltipProvider = TooltipPrimitive.Provider;
|
|
9
|
+
const Tooltip = TooltipPrimitive.Root;
|
|
10
|
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
11
|
+
const TooltipPortal = TooltipPrimitive.Portal;
|
|
12
|
+
|
|
13
|
+
function TooltipContent({
|
|
14
|
+
className,
|
|
15
|
+
sideOffset = 4,
|
|
16
|
+
children,
|
|
17
|
+
...props
|
|
18
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Popup> & { sideOffset?: number }) {
|
|
19
|
+
return (
|
|
20
|
+
<TooltipPrimitive.Portal>
|
|
21
|
+
<TooltipPrimitive.Positioner sideOffset={sideOffset}>
|
|
22
|
+
<TooltipPrimitive.Popup
|
|
23
|
+
data-slot="tooltip-content"
|
|
24
|
+
className={cn(
|
|
25
|
+
"bg-foreground text-background z-50 w-fit max-w-xs rounded-md px-2.5 py-1.5 text-xs text-balance",
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
<TooltipPrimitive.Arrow
|
|
32
|
+
data-slot="tooltip-arrow"
|
|
33
|
+
className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px]"
|
|
34
|
+
/>
|
|
35
|
+
</TooltipPrimitive.Popup>
|
|
36
|
+
</TooltipPrimitive.Positioner>
|
|
37
|
+
</TooltipPrimitive.Portal>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage } from "./avatar";
|
|
2
2
|
export { Badge, badgeVariants } from "./badge";
|
|
3
3
|
export { Button, buttonVariants } from "./button";
|
|
4
|
+
export { SegmentedControl, type SegmentedControlProps, type SegmentedOption } from "./segmented-control";
|
|
5
|
+
export { Separator } from "./separator";
|
|
4
6
|
export {
|
|
5
7
|
Card,
|
|
6
8
|
CardAction,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { SegmentedControl } from "./segmented-control";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Primitives/SegmentedControl",
|
|
7
|
+
component: SegmentedControl,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
} satisfies Meta<typeof SegmentedControl>;
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof meta>;
|
|
13
|
+
|
|
14
|
+
export const Single: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
label: "Layers",
|
|
17
|
+
options: [
|
|
18
|
+
{ value: "cameras", label: "Cameras" },
|
|
19
|
+
{ value: "places", label: "Places" },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
render: () => (
|
|
23
|
+
<SegmentedControl
|
|
24
|
+
label="Layers"
|
|
25
|
+
defaultValue={["cameras"]}
|
|
26
|
+
options={[
|
|
27
|
+
{ value: "cameras", label: "Cameras" },
|
|
28
|
+
{ value: "places", label: "Places" },
|
|
29
|
+
]}
|
|
30
|
+
/>
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const Multiple: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
label: "Layers",
|
|
37
|
+
options: [
|
|
38
|
+
{ value: "cameras", label: "Cameras" },
|
|
39
|
+
{ value: "places", label: "Places" },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
render: () => (
|
|
43
|
+
<SegmentedControl
|
|
44
|
+
label="Layers"
|
|
45
|
+
multiple
|
|
46
|
+
defaultValue={["cameras", "places"]}
|
|
47
|
+
options={[
|
|
48
|
+
{ value: "cameras", label: "Cameras" },
|
|
49
|
+
{ value: "places", label: "Places" },
|
|
50
|
+
{ value: "trains", label: "Trains" },
|
|
51
|
+
]}
|
|
52
|
+
/>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ToggleGroup as SegmentedPrimitive } from "@base-ui/react/toggle-group";
|
|
4
|
+
import { Toggle as SegmentedItemPrimitive } from "@base-ui/react/toggle";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils";
|
|
8
|
+
|
|
9
|
+
export type SegmentedOption = {
|
|
10
|
+
value: string;
|
|
11
|
+
label: React.ReactNode;
|
|
12
|
+
ariaLabel?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SegmentedControlProps = {
|
|
16
|
+
options: SegmentedOption[];
|
|
17
|
+
/** Selected values. Single mode: pass at most one. */
|
|
18
|
+
value?: string[];
|
|
19
|
+
defaultValue?: string[];
|
|
20
|
+
onValueChange?: (value: string[]) => void;
|
|
21
|
+
/** @default false — single-select. True allows several pressed. */
|
|
22
|
+
multiple?: boolean;
|
|
23
|
+
/** Accessible name for the group. Falls back to joining option labels. */
|
|
24
|
+
label?: string;
|
|
25
|
+
className?: string;
|
|
26
|
+
itemClassName?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Pill-group single/multi toggle. Shape + pressed states are kit-owned;
|
|
30
|
+
// elevation and placement are app-owned via className.
|
|
31
|
+
export function SegmentedControl({
|
|
32
|
+
options,
|
|
33
|
+
value,
|
|
34
|
+
defaultValue,
|
|
35
|
+
onValueChange,
|
|
36
|
+
multiple = false,
|
|
37
|
+
label,
|
|
38
|
+
className,
|
|
39
|
+
itemClassName,
|
|
40
|
+
}: SegmentedControlProps) {
|
|
41
|
+
return (
|
|
42
|
+
<SegmentedPrimitive
|
|
43
|
+
data-slot="segmented-control"
|
|
44
|
+
aria-label={label ?? options.map((o) => o.ariaLabel ?? o.value).join(", ")}
|
|
45
|
+
multiple={multiple}
|
|
46
|
+
value={value}
|
|
47
|
+
defaultValue={defaultValue}
|
|
48
|
+
onValueChange={onValueChange}
|
|
49
|
+
className={cn(
|
|
50
|
+
"border-border bg-card inline-flex items-center gap-0.5 rounded-full border p-0.5",
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
{options.map((option) => (
|
|
55
|
+
<SegmentedItemPrimitive
|
|
56
|
+
key={option.value}
|
|
57
|
+
data-slot="segmented-control-item"
|
|
58
|
+
value={option.value}
|
|
59
|
+
aria-label={option.ariaLabel ?? (typeof option.label === "string" ? option.label : option.value)}
|
|
60
|
+
className={cn(
|
|
61
|
+
"text-muted-foreground hover:text-foreground rounded-full px-4 py-1.5 text-[11px] font-semibold tracking-wider whitespace-nowrap uppercase transition-colors",
|
|
62
|
+
"data-pressed:bg-foreground data-pressed:text-background",
|
|
63
|
+
"focus-visible:outline-ring focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
64
|
+
"data-disabled:pointer-events-none data-disabled:opacity-50",
|
|
65
|
+
itemClassName,
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
{option.label}
|
|
69
|
+
</SegmentedItemPrimitive>
|
|
70
|
+
))}
|
|
71
|
+
</SegmentedPrimitive>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { Separator } from "./separator";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Primitives/Separator",
|
|
7
|
+
component: Separator,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
} satisfies Meta<typeof Separator>;
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof meta>;
|
|
13
|
+
|
|
14
|
+
export const Horizontal: Story = {
|
|
15
|
+
args: {},
|
|
16
|
+
render: () => (
|
|
17
|
+
<div className="w-64">
|
|
18
|
+
<p className="text-sm">Above</p>
|
|
19
|
+
<Separator className="my-2" />
|
|
20
|
+
<p className="text-sm">Below</p>
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Vertical: Story = {
|
|
26
|
+
args: { orientation: "vertical" },
|
|
27
|
+
render: () => (
|
|
28
|
+
<div className="flex h-8 items-center gap-2">
|
|
29
|
+
<p className="text-sm">Left</p>
|
|
30
|
+
<Separator orientation="vertical" />
|
|
31
|
+
<p className="text-sm">Right</p>
|
|
32
|
+
</div>
|
|
33
|
+
),
|
|
34
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
|
|
8
|
+
function Separator({
|
|
9
|
+
className,
|
|
10
|
+
orientation = "horizontal",
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof SeparatorPrimitive>) {
|
|
13
|
+
return (
|
|
14
|
+
<SeparatorPrimitive
|
|
15
|
+
data-slot="separator"
|
|
16
|
+
orientation={orientation}
|
|
17
|
+
className={cn(
|
|
18
|
+
"bg-border shrink-0",
|
|
19
|
+
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
|
20
|
+
className,
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { Separator };
|
package/src/index.ts
CHANGED