@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.
- package/README.md +27 -0
- package/dist/index.js +2118 -0
- package/dist/index.js.map +1 -0
- package/package.json +130 -0
- package/src/animations.css +437 -0
- package/src/components/data-display/accordion.stories.tsx +57 -0
- package/src/components/data-display/accordion.tsx +65 -0
- package/src/components/data-display/filter-chip.tsx +41 -0
- package/src/components/data-display/index.ts +13 -0
- package/src/components/data-display/table.stories.tsx +75 -0
- package/src/components/data-display/table.tsx +84 -0
- package/src/components/feedback/alert.stories.tsx +51 -0
- package/src/components/feedback/alert.tsx +60 -0
- package/src/components/feedback/empty-state.tsx +66 -0
- package/src/components/feedback/index.ts +6 -0
- package/src/components/feedback/progress.stories.tsx +37 -0
- package/src/components/feedback/progress.tsx +30 -0
- package/src/components/feedback/spinner.stories.tsx +52 -0
- package/src/components/feedback/spinner.tsx +48 -0
- package/src/components/forms/calendar.stories.tsx +30 -0
- package/src/components/forms/calendar.tsx +67 -0
- package/src/components/forms/date-picker.stories.tsx +48 -0
- package/src/components/forms/date-picker.tsx +60 -0
- package/src/components/forms/field.stories.tsx +62 -0
- package/src/components/forms/field.tsx +65 -0
- package/src/components/forms/field.types.ts +7 -0
- package/src/components/forms/index.ts +24 -0
- package/src/components/forms/input.stories.tsx +47 -0
- package/src/components/forms/input.tsx +20 -0
- package/src/components/forms/radio-group.stories.tsx +48 -0
- package/src/components/forms/radio-group.tsx +38 -0
- package/src/components/forms/select.stories.tsx +88 -0
- package/src/components/forms/select.tsx +147 -0
- package/src/components/forms/slider.stories.tsx +67 -0
- package/src/components/forms/slider.tsx +25 -0
- package/src/components/forms/stepper.stories.tsx +111 -0
- package/src/components/forms/stepper.tsx +66 -0
- package/src/components/forms/switch.stories.tsx +144 -0
- package/src/components/forms/switch.tsx +36 -0
- package/src/components/forms/text-field.stories.tsx +87 -0
- package/src/components/forms/text-field.tsx +40 -0
- package/src/components/forms/text-field.types.ts +9 -0
- package/src/components/forms/textarea.stories.tsx +150 -0
- package/src/components/forms/textarea.tsx +18 -0
- package/src/components/layout/index.ts +2 -0
- package/src/components/layout/scroll-area.stories.tsx +83 -0
- package/src/components/layout/scroll-area.tsx +40 -0
- package/src/components/navigation/app-navigation.stories.tsx +81 -0
- package/src/components/navigation/app-navigation.tsx +155 -0
- package/src/components/navigation/index.ts +3 -0
- package/src/components/navigation/tabs.stories.tsx +69 -0
- package/src/components/navigation/tabs.tsx +49 -0
- package/src/components/overlays/alert-dialog.stories.tsx +45 -0
- package/src/components/overlays/alert-dialog.tsx +119 -0
- package/src/components/overlays/command.stories.tsx +65 -0
- package/src/components/overlays/command.tsx +130 -0
- package/src/components/overlays/dialog.stories.tsx +111 -0
- package/src/components/overlays/dialog.tsx +162 -0
- package/src/components/overlays/dropdown-menu.stories.tsx +79 -0
- package/src/components/overlays/dropdown-menu.tsx +214 -0
- package/src/components/overlays/index.ts +65 -0
- package/src/components/overlays/popover.stories.tsx +39 -0
- package/src/components/overlays/popover.tsx +40 -0
- package/src/components/overlays/sheet.stories.tsx +68 -0
- package/src/components/overlays/sheet.tsx +98 -0
- package/src/components/primitives/avatar.stories.tsx +71 -0
- package/src/components/primitives/avatar.tsx +132 -0
- package/src/components/primitives/badge.stories.tsx +33 -0
- package/src/components/primitives/badge.tsx +44 -0
- package/src/components/primitives/button.stories.tsx +113 -0
- package/src/components/primitives/button.tsx +95 -0
- package/src/components/primitives/button.types.ts +10 -0
- package/src/components/primitives/card.stories.tsx +59 -0
- package/src/components/primitives/card.tsx +69 -0
- package/src/components/primitives/index.ts +13 -0
- package/src/components/primitives/label.stories.tsx +47 -0
- package/src/components/primitives/label.tsx +21 -0
- package/src/constants/chart-colors.ts +39 -0
- package/src/env.d.ts +9 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/sort.types.ts +12 -0
- package/src/hooks/use-api-client.ts +122 -0
- package/src/hooks/use-debounced-value.ts +12 -0
- package/src/hooks/use-filter-state.ts +68 -0
- package/src/hooks/use-media-query.ts +65 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/index.ts +25 -0
- package/src/lib/auth.ts +5 -0
- package/src/lib/clipboard.ts +8 -0
- package/src/lib/create-memory-storage.ts +24 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/utils.ts +6 -0
- package/src/storybook/controls.ts +117 -0
- package/src/storybook/options.ts +52 -0
- package/src/styles.css +1052 -0
- package/src/tokens/color-systems.stories.tsx +133 -0
- package/src/tokens/colors.ts +156 -0
- package/src/tokens/foundations.stories.tsx +151 -0
- package/src/tokens/index.ts +16 -0
- package/src/tokens/layout.stories.tsx +118 -0
- package/src/tokens/motion.ts +51 -0
- package/src/tokens/radii.ts +10 -0
- package/src/tokens/shadows.ts +34 -0
- package/src/tokens/spacing.ts +30 -0
- package/src/tokens/typography.ts +42 -0
- package/src/translations/en.ts +34 -0
- package/src/translations/index.ts +32 -0
- 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,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 };
|