@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.
Files changed (50) hide show
  1. package/README.md +54 -20
  2. package/dist/styles.css +3 -0
  3. package/package.json +28 -9
  4. package/scripts/create-project.mjs +86 -0
  5. package/src/components/forms/checkbox.stories.tsx +35 -0
  6. package/src/components/forms/checkbox.tsx +38 -0
  7. package/src/components/forms/index.ts +2 -0
  8. package/src/components/forms/label.stories.tsx +16 -0
  9. package/src/components/forms/label.tsx +25 -0
  10. package/src/components/forms/select.tsx +3 -1
  11. package/src/components/overlays/bottom-sheet.stories.tsx +52 -0
  12. package/src/components/overlays/bottom-sheet.tsx +179 -0
  13. package/src/components/overlays/command.tsx +1 -1
  14. package/src/components/overlays/dropdown-menu.tsx +6 -2
  15. package/src/components/overlays/index.ts +2 -0
  16. package/src/components/overlays/popover.tsx +12 -3
  17. package/src/components/overlays/tooltip.stories.tsx +29 -0
  18. package/src/components/overlays/tooltip.tsx +41 -0
  19. package/src/components/primitives/index.ts +2 -0
  20. package/src/components/primitives/segmented-control.stories.tsx +54 -0
  21. package/src/components/primitives/segmented-control.tsx +73 -0
  22. package/src/components/primitives/separator.stories.tsx +34 -0
  23. package/src/components/primitives/separator.tsx +27 -0
  24. package/src/index.ts +2 -0
  25. package/src/native/components.tsx +198 -0
  26. package/src/native/index.ts +2 -0
  27. package/src/native/theme.tsx +69 -0
  28. package/src/styles/build.ts +1 -0
  29. package/src/styles/index.css +94 -0
  30. package/src/styles/package.css +3 -0
  31. package/src/styles/tokens/generated/dark-class.css +39 -0
  32. package/src/styles/tokens/generated/dark.colors.generated.ts +31 -31
  33. package/src/styles/tokens/generated/dark.css +31 -31
  34. package/src/styles/tokens/generated/foundations.css +2 -2
  35. package/src/styles/tokens/generated/foundations.generated.ts +2 -0
  36. package/src/styles/tokens/generated/light.colors.generated.ts +30 -30
  37. package/src/styles/tokens/generated/light.css +30 -30
  38. package/src/styles/tokens/index.ts +6 -6
  39. package/src/styles/tokens/scripts/check-source.mjs +9 -6
  40. package/src/styles/tokens/source/dark.tokens.json +33 -32
  41. package/src/styles/tokens/source/foundations.tokens.json +2 -0
  42. package/src/styles/tokens/source/light.tokens.json +32 -31
  43. package/src/styles/tokens/style-dictionary.dark-class.json +16 -0
  44. package/templates/web-vite/index.html +13 -0
  45. package/templates/web-vite/package.json +27 -0
  46. package/templates/web-vite/src/App.tsx +31 -0
  47. package/templates/web-vite/src/main.tsx +12 -0
  48. package/templates/web-vite/src/styles.css +110 -0
  49. package/templates/web-vite/tsconfig.json +21 -0
  50. 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-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-hidden select-none",
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.Positioner sideOffset={sideOffset}>
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
- return <PopoverPrimitive.Root data-slot="popover" {...props} />;
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
@@ -1,3 +1,5 @@
1
+ import "../dist/styles.css";
2
+
1
3
  export { CHART_COLORS, CHART_CSS_VARS } from "./constants/chart-colors";
2
4
  export type { SortDirection, SortField, SortOption } from "./hooks/sort.types";
3
5
  export { useApiClient } from "./hooks/use-api-client";