@ponti-studios/ui 0.1.2

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 (108) hide show
  1. package/README.md +27 -0
  2. package/dist/index.js +2118 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +130 -0
  5. package/src/animations.css +437 -0
  6. package/src/components/data-display/accordion.stories.tsx +57 -0
  7. package/src/components/data-display/accordion.tsx +65 -0
  8. package/src/components/data-display/filter-chip.tsx +41 -0
  9. package/src/components/data-display/index.ts +13 -0
  10. package/src/components/data-display/table.stories.tsx +75 -0
  11. package/src/components/data-display/table.tsx +84 -0
  12. package/src/components/feedback/alert.stories.tsx +51 -0
  13. package/src/components/feedback/alert.tsx +60 -0
  14. package/src/components/feedback/empty-state.tsx +66 -0
  15. package/src/components/feedback/index.ts +6 -0
  16. package/src/components/feedback/progress.stories.tsx +37 -0
  17. package/src/components/feedback/progress.tsx +30 -0
  18. package/src/components/feedback/spinner.stories.tsx +52 -0
  19. package/src/components/feedback/spinner.tsx +48 -0
  20. package/src/components/forms/calendar.stories.tsx +30 -0
  21. package/src/components/forms/calendar.tsx +67 -0
  22. package/src/components/forms/date-picker.stories.tsx +48 -0
  23. package/src/components/forms/date-picker.tsx +60 -0
  24. package/src/components/forms/field.stories.tsx +62 -0
  25. package/src/components/forms/field.tsx +65 -0
  26. package/src/components/forms/field.types.ts +7 -0
  27. package/src/components/forms/index.ts +24 -0
  28. package/src/components/forms/input.stories.tsx +47 -0
  29. package/src/components/forms/input.tsx +20 -0
  30. package/src/components/forms/radio-group.stories.tsx +48 -0
  31. package/src/components/forms/radio-group.tsx +38 -0
  32. package/src/components/forms/select.stories.tsx +88 -0
  33. package/src/components/forms/select.tsx +147 -0
  34. package/src/components/forms/slider.stories.tsx +67 -0
  35. package/src/components/forms/slider.tsx +25 -0
  36. package/src/components/forms/stepper.stories.tsx +111 -0
  37. package/src/components/forms/stepper.tsx +66 -0
  38. package/src/components/forms/switch.stories.tsx +144 -0
  39. package/src/components/forms/switch.tsx +36 -0
  40. package/src/components/forms/text-field.stories.tsx +87 -0
  41. package/src/components/forms/text-field.tsx +40 -0
  42. package/src/components/forms/text-field.types.ts +9 -0
  43. package/src/components/forms/textarea.stories.tsx +150 -0
  44. package/src/components/forms/textarea.tsx +18 -0
  45. package/src/components/layout/index.ts +2 -0
  46. package/src/components/layout/scroll-area.stories.tsx +83 -0
  47. package/src/components/layout/scroll-area.tsx +40 -0
  48. package/src/components/navigation/app-navigation.stories.tsx +81 -0
  49. package/src/components/navigation/app-navigation.tsx +155 -0
  50. package/src/components/navigation/index.ts +3 -0
  51. package/src/components/navigation/tabs.stories.tsx +69 -0
  52. package/src/components/navigation/tabs.tsx +49 -0
  53. package/src/components/overlays/alert-dialog.stories.tsx +45 -0
  54. package/src/components/overlays/alert-dialog.tsx +119 -0
  55. package/src/components/overlays/command.stories.tsx +65 -0
  56. package/src/components/overlays/command.tsx +130 -0
  57. package/src/components/overlays/dialog.stories.tsx +111 -0
  58. package/src/components/overlays/dialog.tsx +162 -0
  59. package/src/components/overlays/dropdown-menu.stories.tsx +79 -0
  60. package/src/components/overlays/dropdown-menu.tsx +214 -0
  61. package/src/components/overlays/index.ts +65 -0
  62. package/src/components/overlays/popover.stories.tsx +39 -0
  63. package/src/components/overlays/popover.tsx +40 -0
  64. package/src/components/overlays/sheet.stories.tsx +68 -0
  65. package/src/components/overlays/sheet.tsx +98 -0
  66. package/src/components/primitives/avatar.stories.tsx +71 -0
  67. package/src/components/primitives/avatar.tsx +132 -0
  68. package/src/components/primitives/badge.stories.tsx +33 -0
  69. package/src/components/primitives/badge.tsx +44 -0
  70. package/src/components/primitives/button.stories.tsx +113 -0
  71. package/src/components/primitives/button.tsx +95 -0
  72. package/src/components/primitives/button.types.ts +10 -0
  73. package/src/components/primitives/card.stories.tsx +59 -0
  74. package/src/components/primitives/card.tsx +69 -0
  75. package/src/components/primitives/index.ts +13 -0
  76. package/src/components/primitives/label.stories.tsx +47 -0
  77. package/src/components/primitives/label.tsx +21 -0
  78. package/src/constants/chart-colors.ts +39 -0
  79. package/src/env.d.ts +9 -0
  80. package/src/hooks/index.ts +6 -0
  81. package/src/hooks/sort.types.ts +12 -0
  82. package/src/hooks/use-api-client.ts +122 -0
  83. package/src/hooks/use-debounced-value.ts +12 -0
  84. package/src/hooks/use-filter-state.ts +68 -0
  85. package/src/hooks/use-media-query.ts +65 -0
  86. package/src/hooks/use-mobile.tsx +19 -0
  87. package/src/index.ts +25 -0
  88. package/src/lib/auth.ts +5 -0
  89. package/src/lib/clipboard.ts +8 -0
  90. package/src/lib/create-memory-storage.ts +24 -0
  91. package/src/lib/index.ts +4 -0
  92. package/src/lib/utils.ts +6 -0
  93. package/src/storybook/controls.ts +117 -0
  94. package/src/storybook/options.ts +52 -0
  95. package/src/styles.css +1052 -0
  96. package/src/tokens/color-systems.stories.tsx +133 -0
  97. package/src/tokens/colors.ts +156 -0
  98. package/src/tokens/foundations.stories.tsx +151 -0
  99. package/src/tokens/index.ts +16 -0
  100. package/src/tokens/layout.stories.tsx +118 -0
  101. package/src/tokens/motion.ts +51 -0
  102. package/src/tokens/radii.ts +10 -0
  103. package/src/tokens/shadows.ts +34 -0
  104. package/src/tokens/spacing.ts +30 -0
  105. package/src/tokens/typography.ts +42 -0
  106. package/src/translations/en.ts +34 -0
  107. package/src/translations/index.ts +32 -0
  108. package/src/types/upload.ts +42 -0
@@ -0,0 +1,62 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { expect, userEvent, within } from "storybook/test";
3
+
4
+ import { Field } from "./field";
5
+ import { Input } from "./input";
6
+
7
+ const meta = {
8
+ title: "Forms/Field",
9
+ component: Field,
10
+ tags: ["autodocs"],
11
+ args: {
12
+ children: <Input placeholder="you@example.com" />,
13
+ },
14
+ } satisfies Meta<typeof Field>;
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ export const Default: Story = {
20
+ render: () => (
21
+ <Field label="Email" helpText="We will never share your address.">
22
+ <Input placeholder="you@example.com" />
23
+ </Field>
24
+ ),
25
+ play: async ({ canvasElement }) => {
26
+ const canvas = within(canvasElement);
27
+ const input = canvas.getByLabelText("Email");
28
+
29
+ await userEvent.type(input, "you@example.com");
30
+
31
+ await expect(input).toHaveValue("you@example.com");
32
+ await expect(canvas.getByText("We will never share your address.")).toBeInTheDocument();
33
+ },
34
+ };
35
+
36
+ export const Error: Story = {
37
+ render: () => (
38
+ <Field label="Email" error="Email is required">
39
+ <Input placeholder="you@example.com" />
40
+ </Field>
41
+ ),
42
+ play: async ({ canvasElement }) => {
43
+ const canvas = within(canvasElement);
44
+
45
+ await expect(canvas.getByLabelText("Email")).toHaveAttribute("aria-invalid", "true");
46
+ await expect(canvas.getByRole("alert")).toHaveTextContent("Email is required");
47
+ },
48
+ };
49
+
50
+ export const Textarea: Story = {
51
+ render: () => (
52
+ <Field label="Notes" helpText="Markdown supported">
53
+ <textarea rows={5} placeholder="Write something..." />
54
+ </Field>
55
+ ),
56
+ play: async ({ canvasElement }) => {
57
+ const canvas = within(canvasElement);
58
+
59
+ await expect(canvas.getByLabelText("Notes")).toBeInTheDocument();
60
+ await expect(canvas.getByText("Markdown supported")).toBeInTheDocument();
61
+ },
62
+ };
@@ -0,0 +1,65 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+ import type { FieldBaseProps } from "./field.types";
5
+
6
+ interface FieldProps extends FieldBaseProps {
7
+ children: React.ReactNode;
8
+ }
9
+
10
+ /**
11
+ * Field — standalone label + control + helper/error slot.
12
+ * Wires label ↔ control association automatically via a generated id.
13
+ * Does NOT require react-hook-form — works with any controlled or uncontrolled input.
14
+ *
15
+ * @example
16
+ * <Field label="Email" error={errors.email}>
17
+ * <Input type="email" />
18
+ * </Field>
19
+ */
20
+ function Field({ label, helpText, error, required, children, id: externalId }: FieldProps) {
21
+ const generatedId = React.useId();
22
+ const id = externalId ?? generatedId;
23
+ const descId = `${id}-desc`;
24
+ const errorId = `${id}-error`;
25
+
26
+ return (
27
+ <div className="flex flex-col gap-1.5">
28
+ {label && (
29
+ <label
30
+ htmlFor={id}
31
+ className={cn(
32
+ "body-3 text-primary font-medium",
33
+ required && "after:text-destructive after:ml-0.5 after:content-['*']",
34
+ )}
35
+ >
36
+ {label}
37
+ </label>
38
+ )}
39
+
40
+ {/* Clone child to inject id and aria attributes */}
41
+ {React.isValidElement(children)
42
+ ? React.cloneElement(
43
+ children as React.ReactElement<React.HTMLAttributes<HTMLElement>>,
44
+ {
45
+ id,
46
+ "aria-describedby": error ? errorId : helpText ? descId : undefined,
47
+ "aria-invalid": error ? true : undefined,
48
+ } as React.HTMLAttributes<HTMLElement>,
49
+ )
50
+ : children}
51
+
52
+ {error ? (
53
+ <p id={errorId} role="alert" className="body-4 text-destructive">
54
+ {error}
55
+ </p>
56
+ ) : helpText ? (
57
+ <p id={descId} className="body-4 text-tertiary">
58
+ {helpText}
59
+ </p>
60
+ ) : null}
61
+ </div>
62
+ );
63
+ }
64
+
65
+ export { Field, type FieldProps };
@@ -0,0 +1,7 @@
1
+ export interface FieldBaseProps {
2
+ error?: string | undefined;
3
+ helpText?: string | undefined;
4
+ id?: string | undefined;
5
+ label?: string | undefined;
6
+ required?: boolean | undefined;
7
+ }
@@ -0,0 +1,24 @@
1
+ export { Calendar } from "./calendar";
2
+ export type { CalendarProps } from "./calendar";
3
+ export { DatePicker } from "./date-picker";
4
+ export type { DatePickerProps } from "./date-picker";
5
+ export { Field } from "./field";
6
+ export type { FieldProps } from "./field";
7
+ export { Input } from "./input";
8
+ export { RadioGroup, RadioGroupItem } from "./radio-group";
9
+ export {
10
+ Select,
11
+ SelectContent,
12
+ SelectGroup,
13
+ SelectItem,
14
+ SelectLabel,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from "./select";
18
+ export { Slider } from "./slider";
19
+ export { Stepper } from "./stepper";
20
+ export type { StepperProps } from "./stepper";
21
+ export { Switch } from "./switch";
22
+ export { TextField } from "./text-field";
23
+ export type { TextFieldProps } from "./text-field";
24
+ export { Textarea } from "./textarea";
@@ -0,0 +1,47 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { booleanControl, selectControl, textControl } from "../../storybook/controls";
4
+ import { inputTypeOptions } from "../../storybook/options";
5
+ import { Input } from "./input";
6
+
7
+ const meta = {
8
+ title: "Forms/Input",
9
+ component: Input,
10
+ tags: ["autodocs"],
11
+ argTypes: {
12
+ type: selectControl(inputTypeOptions, "HTML input type attribute", {
13
+ defaultValue: "text",
14
+ }),
15
+ placeholder: textControl("Placeholder text shown when the input is empty"),
16
+ disabled: booleanControl("Prevents user interaction and applies disabled styling", false),
17
+ required: booleanControl("Marks the input as required for form submission", false),
18
+ },
19
+ } satisfies Meta<typeof Input>;
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ export const Default: Story = {
25
+ args: { placeholder: "Enter text…" },
26
+ };
27
+
28
+ export const WithLabel: Story = {
29
+ render: () => (
30
+ <div className="flex w-72 flex-col gap-2">
31
+ <label htmlFor="email" className="text-primary text-sm leading-none font-medium">
32
+ Email
33
+ </label>
34
+ <Input id="email" type="email" placeholder="you@example.com" />
35
+ </div>
36
+ ),
37
+ };
38
+
39
+ export const States: Story = {
40
+ render: () => (
41
+ <div className="flex w-72 flex-col gap-4">
42
+ <Input placeholder="Default" />
43
+ <Input placeholder="Disabled" disabled />
44
+ <Input defaultValue="With value" />
45
+ </div>
46
+ ),
47
+ };
@@ -0,0 +1,20 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "file:text-primary placeholder:text-secondary selection:bg-accent selection:text-on-accent bg-canvas focus-visible:ring-focus h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base transition-colors file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ "aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
13
+ className,
14
+ )}
15
+ {...props}
16
+ />
17
+ );
18
+ }
19
+
20
+ export { Input };
@@ -0,0 +1,48 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import * as React from "react";
3
+
4
+ import { Label } from "../primitives/label";
5
+ import { RadioGroup, RadioGroupItem } from "./radio-group";
6
+
7
+ const meta: Meta<typeof RadioGroup> = {
8
+ title: "Forms/RadioGroup",
9
+ component: RadioGroup,
10
+ tags: ["autodocs"],
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ const options = [
17
+ { value: "default", label: "Default" },
18
+ { value: "comfortable", label: "Comfortable" },
19
+ { value: "compact", label: "Compact" },
20
+ ];
21
+
22
+ export const Default: Story = {
23
+ render: () => (
24
+ <RadioGroup defaultValue="default" className="gap-3">
25
+ {options.map(({ value, label }) => (
26
+ <div key={value} className="flex items-center gap-3">
27
+ <RadioGroupItem value={value} id={`opt-${value}`} />
28
+ <Label htmlFor={`opt-${value}`} className="cursor-pointer">
29
+ {label}
30
+ </Label>
31
+ </div>
32
+ ))}
33
+ </RadioGroup>
34
+ ),
35
+ };
36
+
37
+ export const Disabled: Story = {
38
+ render: () => (
39
+ <RadioGroup defaultValue="default" disabled className="gap-3">
40
+ {options.map(({ value, label }) => (
41
+ <div key={value} className="flex items-center gap-3">
42
+ <RadioGroupItem value={value} id={`dis-${value}`} />
43
+ <Label htmlFor={`dis-${value}`}>{label}</Label>
44
+ </div>
45
+ ))}
46
+ </RadioGroup>
47
+ ),
48
+ };
@@ -0,0 +1,38 @@
1
+ "use client";
2
+
3
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
4
+ import { Circle } from "lucide-react";
5
+ import * as React from "react";
6
+
7
+ import { cn } from "../../lib/utils";
8
+
9
+ const RadioGroup = React.forwardRef<
10
+ React.ComponentRef<typeof RadioGroupPrimitive.Root>,
11
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
12
+ >(({ className, ...props }, ref) => {
13
+ return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
14
+ });
15
+ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
16
+
17
+ const RadioGroupItem = React.forwardRef<
18
+ React.ComponentRef<typeof RadioGroupPrimitive.Item>,
19
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
20
+ >(({ className, ...props }, ref) => {
21
+ return (
22
+ <RadioGroupPrimitive.Item
23
+ ref={ref}
24
+ className={cn(
25
+ "border-accent bg-canvas text-primary focus-visible:border-foreground focus-visible:bg-accent/25 focus-visible:ring-focus aspect-square h-6 w-6 rounded-full border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
26
+ className,
27
+ )}
28
+ {...props}
29
+ >
30
+ <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
31
+ <Circle className="fill-primary h-4 w-4" />
32
+ </RadioGroupPrimitive.Indicator>
33
+ </RadioGroupPrimitive.Item>
34
+ );
35
+ });
36
+ RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
37
+
38
+ export { RadioGroup, RadioGroupItem };
@@ -0,0 +1,88 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectGroup,
7
+ SelectItem,
8
+ SelectLabel,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from "./select";
12
+
13
+ const meta = {
14
+ title: "Forms/Select",
15
+ component: Select,
16
+ tags: ["autodocs"],
17
+ } satisfies Meta<typeof Select>;
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ export const Default: Story = {
23
+ render: () => (
24
+ <Select>
25
+ <SelectTrigger className="w-[180px]">
26
+ <SelectValue placeholder="Select a fruit" />
27
+ </SelectTrigger>
28
+ <SelectContent>
29
+ <SelectItem value="apple">Apple</SelectItem>
30
+ <SelectItem value="banana">Banana</SelectItem>
31
+ <SelectItem value="blueberry">Blueberry</SelectItem>
32
+ <SelectItem value="grapes">Grapes</SelectItem>
33
+ <SelectItem value="pineapple">Pineapple</SelectItem>
34
+ </SelectContent>
35
+ </Select>
36
+ ),
37
+ };
38
+
39
+ export const WithGroups: Story = {
40
+ render: () => (
41
+ <Select>
42
+ <SelectTrigger className="w-[200px]">
43
+ <SelectValue placeholder="Select timezone" />
44
+ </SelectTrigger>
45
+ <SelectContent>
46
+ <SelectGroup>
47
+ <SelectLabel>North America</SelectLabel>
48
+ <SelectItem value="est">Eastern Standard Time (EST)</SelectItem>
49
+ <SelectItem value="cst">Central Standard Time (CST)</SelectItem>
50
+ <SelectItem value="pst">Pacific Standard Time (PST)</SelectItem>
51
+ </SelectGroup>
52
+ <SelectGroup>
53
+ <SelectLabel>Europe</SelectLabel>
54
+ <SelectItem value="gmt">Greenwich Mean Time (GMT)</SelectItem>
55
+ <SelectItem value="cet">Central European Time (CET)</SelectItem>
56
+ </SelectGroup>
57
+ </SelectContent>
58
+ </Select>
59
+ ),
60
+ };
61
+
62
+ export const Disabled: Story = {
63
+ render: () => (
64
+ <Select disabled>
65
+ <SelectTrigger className="w-[180px]">
66
+ <SelectValue placeholder="Select a fruit" />
67
+ </SelectTrigger>
68
+ <SelectContent>
69
+ <SelectItem value="apple">Apple</SelectItem>
70
+ </SelectContent>
71
+ </Select>
72
+ ),
73
+ };
74
+
75
+ export const Small: Story = {
76
+ render: () => (
77
+ <Select>
78
+ <SelectTrigger size="sm" className="w-[160px]">
79
+ <SelectValue placeholder="Select" />
80
+ </SelectTrigger>
81
+ <SelectContent>
82
+ <SelectItem value="1">Option 1</SelectItem>
83
+ <SelectItem value="2">Option 2</SelectItem>
84
+ <SelectItem value="3">Option 3</SelectItem>
85
+ </SelectContent>
86
+ </Select>
87
+ ),
88
+ };
@@ -0,0 +1,147 @@
1
+ import * as SelectPrimitive from "@radix-ui/react-select";
2
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
3
+ import * as React from "react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
8
+ return <SelectPrimitive.Root data-slot="select" {...props} />;
9
+ }
10
+
11
+ function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
12
+ return <SelectPrimitive.Group data-slot="select-group" {...props} />;
13
+ }
14
+
15
+ function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
16
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />;
17
+ }
18
+
19
+ function SelectTrigger({
20
+ className,
21
+ size = "default",
22
+ children,
23
+ ...props
24
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
25
+ size?: "sm" | "default";
26
+ }) {
27
+ return (
28
+ <SelectPrimitive.Trigger
29
+ data-slot="select-trigger"
30
+ data-size={size}
31
+ className={cn(
32
+ "data-placeholder:text-secondary [&_svg:not([class*='text-'])]:text-secondary focus-visible:border-focus focus-visible:ring-focus/50 aria-invalid:ring-destructive/20 aria-invalid:border-destructive flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-inset disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
33
+ className,
34
+ )}
35
+ {...props}
36
+ >
37
+ {children}
38
+ <SelectPrimitive.Icon asChild>
39
+ <ChevronDownIcon className="size-4 opacity-50" />
40
+ </SelectPrimitive.Icon>
41
+ </SelectPrimitive.Trigger>
42
+ );
43
+ }
44
+
45
+ function SelectContent({
46
+ className,
47
+ children,
48
+ position = "item-aligned",
49
+ ...props
50
+ }: React.ComponentProps<typeof SelectPrimitive.Content>) {
51
+ return (
52
+ <SelectPrimitive.Portal>
53
+ <SelectPrimitive.Content
54
+ data-slot="select-content"
55
+ className={cn(
56
+ "bg-raised text-primary data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 relative z-50 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border",
57
+ position === "popper" &&
58
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
59
+ className,
60
+ )}
61
+ position={position}
62
+ {...props}
63
+ >
64
+ <SelectScrollUpButton />
65
+ <SelectPrimitive.Viewport
66
+ className={cn(
67
+ "p-1",
68
+ position === "popper" &&
69
+ "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1",
70
+ )}
71
+ >
72
+ {children}
73
+ </SelectPrimitive.Viewport>
74
+ <SelectScrollDownButton />
75
+ </SelectPrimitive.Content>
76
+ </SelectPrimitive.Portal>
77
+ );
78
+ }
79
+
80
+ function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
81
+ return (
82
+ <SelectPrimitive.Label
83
+ data-slot="select-label"
84
+ className={cn("text-secondary px-2 py-1.5 text-xs", className)}
85
+ {...props}
86
+ />
87
+ );
88
+ }
89
+
90
+ function SelectItem({
91
+ className,
92
+ children,
93
+ ...props
94
+ }: React.ComponentProps<typeof SelectPrimitive.Item>) {
95
+ return (
96
+ <SelectPrimitive.Item
97
+ data-slot="select-item"
98
+ className={cn(
99
+ "focus:bg-accent focus:text-on-accent [&_svg:not([class*='text-'])]:text-secondary relative flex w-full cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
100
+ className,
101
+ )}
102
+ {...props}
103
+ >
104
+ <span
105
+ data-slot="select-item-indicator"
106
+ className="absolute right-2 flex size-3.5 items-center justify-center"
107
+ >
108
+ <SelectPrimitive.ItemIndicator>
109
+ <CheckIcon className="size-4" />
110
+ </SelectPrimitive.ItemIndicator>
111
+ </span>
112
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
113
+ </SelectPrimitive.Item>
114
+ );
115
+ }
116
+
117
+ function SelectScrollUpButton({
118
+ className,
119
+ ...props
120
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
121
+ return (
122
+ <SelectPrimitive.ScrollUpButton
123
+ data-slot="select-scroll-up-button"
124
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
125
+ {...props}
126
+ >
127
+ <ChevronUpIcon className="size-4" />
128
+ </SelectPrimitive.ScrollUpButton>
129
+ );
130
+ }
131
+
132
+ function SelectScrollDownButton({
133
+ className,
134
+ ...props
135
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
136
+ return (
137
+ <SelectPrimitive.ScrollDownButton
138
+ data-slot="select-scroll-down-button"
139
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
140
+ {...props}
141
+ >
142
+ <ChevronDownIcon className="size-4" />
143
+ </SelectPrimitive.ScrollDownButton>
144
+ );
145
+ }
146
+
147
+ export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue };
@@ -0,0 +1,67 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import * as React from "react";
3
+
4
+ import { Slider } from "./slider";
5
+
6
+ const meta: Meta<typeof Slider> = {
7
+ title: "Forms/Slider",
8
+ component: Slider,
9
+ tags: ["autodocs"],
10
+ decorators: [
11
+ (Story) => (
12
+ <div className="w-full max-w-sm px-1">
13
+ <Story />
14
+ </div>
15
+ ),
16
+ ],
17
+ argTypes: {
18
+ disabled: { control: "boolean" },
19
+ min: { control: "number" },
20
+ max: { control: "number" },
21
+ step: { control: "number" },
22
+ },
23
+ };
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ export const Default: Story = {
29
+ args: {
30
+ defaultValue: [50],
31
+ min: 0,
32
+ max: 100,
33
+ },
34
+ };
35
+
36
+ export const WithStep: Story = {
37
+ args: {
38
+ defaultValue: [0],
39
+ min: 0,
40
+ max: 100,
41
+ step: 10,
42
+ },
43
+ };
44
+
45
+ export const Controlled: Story = {
46
+ render: () => {
47
+ const [value, setValue] = React.useState([33]);
48
+ return (
49
+ <div className="w-full max-w-sm space-y-4 px-1">
50
+ <Slider value={value} onValueChange={setValue} min={0} max={100} />
51
+ <div className="flex justify-between text-sm">
52
+ <span className="text-secondary">Volume</span>
53
+ <span className="font-medium tabular-nums">{value[0]}%</span>
54
+ </div>
55
+ </div>
56
+ );
57
+ },
58
+ };
59
+
60
+ export const Disabled: Story = {
61
+ args: {
62
+ value: [40],
63
+ disabled: true,
64
+ min: 0,
65
+ max: 100,
66
+ },
67
+ };
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import * as SliderPrimitive from "@radix-ui/react-slider";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "../../lib/utils";
7
+
8
+ const Slider = React.forwardRef<
9
+ React.ComponentRef<typeof SliderPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <SliderPrimitive.Root
13
+ ref={ref}
14
+ className={cn("relative flex w-full touch-none items-center select-none", className)}
15
+ {...props}
16
+ >
17
+ <SliderPrimitive.Track className="bg-panel/40 relative h-2 w-full grow overflow-hidden rounded-full">
18
+ <SliderPrimitive.Range className="bg-accent absolute h-full" />
19
+ </SliderPrimitive.Track>
20
+ <SliderPrimitive.Thumb className="border-default focus-visible:border-foreground focus-visible:bg-accent focus-visible:ring-focus block h-6 w-6 rounded-full border bg-white shadow-sm ring-1 ring-black/5 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50" />
21
+ </SliderPrimitive.Root>
22
+ ));
23
+ Slider.displayName = SliderPrimitive.Root.displayName;
24
+
25
+ export { Slider };