@peerbots/core 0.1.0
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/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/publish.yml +42 -0
- package/.github/workflows/storybook.yml +46 -0
- package/.storybook/main.ts +28 -0
- package/.storybook/preview.ts +22 -0
- package/README.md +9 -0
- package/dist/index.css +1 -0
- package/dist/index.d.mts +704 -0
- package/dist/index.d.ts +704 -0
- package/dist/index.js +5 -0
- package/dist/index.mjs +5 -0
- package/package.json +60 -0
- package/src/charts/DistributionBarChart.stories.tsx +41 -0
- package/src/charts/DistributionBarChart.tsx +170 -0
- package/src/charts/DistributionHistogram.stories.tsx +56 -0
- package/src/charts/DistributionHistogram.tsx +193 -0
- package/src/charts/index.ts +10 -0
- package/src/global.d.ts +1 -0
- package/src/helpers/SEO.tsx +41 -0
- package/src/index.ts +6 -0
- package/src/styles/theme.css +60 -0
- package/src/ui/Alert.stories.tsx +41 -0
- package/src/ui/Alert.tsx +72 -0
- package/src/ui/Anchor.stories.tsx +25 -0
- package/src/ui/Anchor.tsx +32 -0
- package/src/ui/AuthFormUI.stories.tsx +67 -0
- package/src/ui/AuthFormUI.tsx +217 -0
- package/src/ui/BasePanel.stories.tsx +36 -0
- package/src/ui/BasePanel.tsx +59 -0
- package/src/ui/Button.stories.tsx +108 -0
- package/src/ui/Button.tsx +121 -0
- package/src/ui/Checkbox.stories.tsx +61 -0
- package/src/ui/Checkbox.tsx +45 -0
- package/src/ui/Collapsible.stories.tsx +91 -0
- package/src/ui/Collapsible.tsx +52 -0
- package/src/ui/Colors.stories.tsx +67 -0
- package/src/ui/Dialog.stories.tsx +29 -0
- package/src/ui/Dialog.tsx +56 -0
- package/src/ui/Dropdown.tsx +66 -0
- package/src/ui/Field.stories.tsx +181 -0
- package/src/ui/Field.tsx +108 -0
- package/src/ui/Icon.stories.tsx +192 -0
- package/src/ui/Icon.tsx +42 -0
- package/src/ui/IconRegistry.tsx +189 -0
- package/src/ui/Input.stories.tsx +67 -0
- package/src/ui/Input.tsx +43 -0
- package/src/ui/Label.stories.tsx +42 -0
- package/src/ui/Label.tsx +26 -0
- package/src/ui/NumberField.stories.tsx +86 -0
- package/src/ui/NumberField.tsx +116 -0
- package/src/ui/Popover.tsx +42 -0
- package/src/ui/Select.stories.tsx +74 -0
- package/src/ui/Select.tsx +122 -0
- package/src/ui/Separator.stories.tsx +61 -0
- package/src/ui/Separator.tsx +28 -0
- package/src/ui/SettingsPanel.stories.tsx +83 -0
- package/src/ui/SettingsPanel.tsx +81 -0
- package/src/ui/Skeleton.stories.tsx +43 -0
- package/src/ui/Skeleton.tsx +15 -0
- package/src/ui/Slider.stories.tsx +140 -0
- package/src/ui/Slider.tsx +95 -0
- package/src/ui/SliderWithNumberField.stories.tsx +101 -0
- package/src/ui/SliderWithNumberField.tsx +88 -0
- package/src/ui/Switch.stories.tsx +81 -0
- package/src/ui/Switch.tsx +60 -0
- package/src/ui/TabRadio.stories.tsx +153 -0
- package/src/ui/TabRadio.tsx +68 -0
- package/src/ui/TabSelection.stories.tsx +44 -0
- package/src/ui/TabSelection.tsx +91 -0
- package/src/ui/TextArea.stories.tsx +64 -0
- package/src/ui/TextArea.tsx +24 -0
- package/src/ui/Tooltip.stories.tsx +84 -0
- package/src/ui/Tooltip.tsx +61 -0
- package/src/ui/Typography.stories.tsx +87 -0
- package/src/ui/Typography.tsx +80 -0
- package/src/ui/index.ts +28 -0
- package/src/ui/utils.ts +6 -0
- package/tsconfig.json +12 -0
- package/vitest.config.ts +36 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Skeleton } from "./Skeleton";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Skeleton> = {
|
|
5
|
+
title: "UI/Skeleton",
|
|
6
|
+
component: Skeleton,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "centered",
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof Skeleton>;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
args: {
|
|
18
|
+
className: "w-[250px] h-4",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Variations: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<div className="flex flex-col space-y-3">
|
|
25
|
+
{/* Circle Skeleton */}
|
|
26
|
+
<Skeleton className="h-12 w-12 rounded-full" />
|
|
27
|
+
|
|
28
|
+
{/* Rectangular Skeletons */}
|
|
29
|
+
<div className="space-y-2">
|
|
30
|
+
<Skeleton className="h-4 w-[250px]" />
|
|
31
|
+
<Skeleton className="h-4 w-[200px]" />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className="flex items-center space-x-4 mt-6">
|
|
35
|
+
<Skeleton className="h-12 w-12 rounded-full" />
|
|
36
|
+
<div className="space-y-2">
|
|
37
|
+
<Skeleton className="h-4 w-[250px]" />
|
|
38
|
+
<Skeleton className="h-4 w-[200px]" />
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
),
|
|
43
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "./utils";
|
|
3
|
+
|
|
4
|
+
export interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Skeleton({ className, ...props }: SkeletonProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={cn("animate-pulse rounded-md bg-gray-200/50", className)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Slider } from "./Slider";
|
|
4
|
+
import { Input } from "./Input";
|
|
5
|
+
import { Heading, Text } from "./Typography";
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Slider> = {
|
|
8
|
+
title: "UI/Slider",
|
|
9
|
+
component: Slider,
|
|
10
|
+
argTypes: {
|
|
11
|
+
onChange: { action: "changed" },
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof Slider>;
|
|
17
|
+
|
|
18
|
+
const InteractiveSlider = (props: React.ComponentProps<typeof Slider>) => {
|
|
19
|
+
const [value, setValue] = useState(
|
|
20
|
+
Array.isArray(props.value) ? props.value[0] : (props.value ?? 0),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="space-y-2">
|
|
25
|
+
<div className="w-80">
|
|
26
|
+
<Slider
|
|
27
|
+
{...props}
|
|
28
|
+
value={value}
|
|
29
|
+
onChange={(v) => {
|
|
30
|
+
const newVal = Array.isArray(v) ? v[0] : v;
|
|
31
|
+
setValue(newVal);
|
|
32
|
+
props.onChange?.(v, null as unknown as Event);
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
<Text variant="small" className="text-slate-500">
|
|
37
|
+
Value: {value}
|
|
38
|
+
</Text>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const RangeChangeableSlider = ({
|
|
44
|
+
args,
|
|
45
|
+
}: {
|
|
46
|
+
args: React.ComponentProps<typeof Slider>;
|
|
47
|
+
}) => {
|
|
48
|
+
const [min, setMin] = useState(0);
|
|
49
|
+
const [max, setMax] = useState(100);
|
|
50
|
+
const [val, setVal] = useState(50);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="space-y-4 border p-4 rounded-lg bg-white shadow-sm">
|
|
54
|
+
<Slider
|
|
55
|
+
{...args}
|
|
56
|
+
min={min}
|
|
57
|
+
max={max}
|
|
58
|
+
value={val}
|
|
59
|
+
onChange={(v: number | number[]) => setVal(Array.isArray(v) ? v[0] : v)}
|
|
60
|
+
/>
|
|
61
|
+
<div className="flex space-x-4">
|
|
62
|
+
<div className="flex flex-col gap-1">
|
|
63
|
+
<Text
|
|
64
|
+
variant="small"
|
|
65
|
+
className="uppercase text-slate-400 font-bold text-[10px]"
|
|
66
|
+
>
|
|
67
|
+
Min
|
|
68
|
+
</Text>
|
|
69
|
+
<Input
|
|
70
|
+
type="number"
|
|
71
|
+
value={min}
|
|
72
|
+
onChange={(e) => setMin(Number(e.target.value))}
|
|
73
|
+
className="w-20"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
<div className="flex flex-col gap-1">
|
|
77
|
+
<Text
|
|
78
|
+
variant="small"
|
|
79
|
+
className="uppercase text-slate-400 font-bold text-[10px]"
|
|
80
|
+
>
|
|
81
|
+
Max
|
|
82
|
+
</Text>
|
|
83
|
+
<Input
|
|
84
|
+
type="number"
|
|
85
|
+
value={max}
|
|
86
|
+
onChange={(e) => setMax(Number(e.target.value))}
|
|
87
|
+
className="w-20"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="flex flex-col justify-end">
|
|
91
|
+
<Text variant="small" className="text-slate-600 font-medium">
|
|
92
|
+
Current: {val}
|
|
93
|
+
</Text>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const Default: Story = {
|
|
101
|
+
args: {
|
|
102
|
+
min: 0,
|
|
103
|
+
max: 100,
|
|
104
|
+
value: 50,
|
|
105
|
+
},
|
|
106
|
+
render: (args) => <InteractiveSlider {...args} />,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const Variations: Story = {
|
|
110
|
+
render: () => (
|
|
111
|
+
<div className="flex flex-col gap-12 p-4">
|
|
112
|
+
<div className="space-y-4">
|
|
113
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
114
|
+
Width Responsiveness
|
|
115
|
+
</Heading>
|
|
116
|
+
<div className="space-y-6">
|
|
117
|
+
<div className="space-y-1">
|
|
118
|
+
<Text variant="small" className="italic text-black">
|
|
119
|
+
Full Width (Container Default)
|
|
120
|
+
</Text>
|
|
121
|
+
<Slider defaultValue={30} />
|
|
122
|
+
</div>
|
|
123
|
+
<div className="space-y-1 w-40">
|
|
124
|
+
<Text variant="small" className="italic text-black">
|
|
125
|
+
Narrow (w-40)
|
|
126
|
+
</Text>
|
|
127
|
+
<Slider defaultValue={70} />
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="space-y-4">
|
|
133
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
134
|
+
Dynamic Range Test
|
|
135
|
+
</Heading>
|
|
136
|
+
<RangeChangeableSlider args={{}} />
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
),
|
|
140
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Slider as BaseSlider } from "@base-ui/react";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export interface SliderProps {
|
|
6
|
+
min?: number;
|
|
7
|
+
max?: number;
|
|
8
|
+
step?: number;
|
|
9
|
+
value?: number;
|
|
10
|
+
defaultValue?: number;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
onChange?: (value: number | number[], event: Event) => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
icon?: React.ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
|
19
|
+
(
|
|
20
|
+
{
|
|
21
|
+
className,
|
|
22
|
+
min = 0,
|
|
23
|
+
max = 100,
|
|
24
|
+
step = 1,
|
|
25
|
+
value,
|
|
26
|
+
defaultValue,
|
|
27
|
+
disabled,
|
|
28
|
+
onChange,
|
|
29
|
+
name,
|
|
30
|
+
icon,
|
|
31
|
+
...props
|
|
32
|
+
},
|
|
33
|
+
ref,
|
|
34
|
+
) => {
|
|
35
|
+
const handleChange = (val: number | number[], event: Event) => {
|
|
36
|
+
if (onChange) {
|
|
37
|
+
onChange(val, event);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className={cn("w-full flex items-center gap-4", className)}>
|
|
43
|
+
{icon && <div className="text-gray-500 shrink-0">{icon}</div>}
|
|
44
|
+
|
|
45
|
+
<div className="flex-grow flex items-center h-8 min-w-[120px]">
|
|
46
|
+
<BaseSlider.Root
|
|
47
|
+
ref={ref}
|
|
48
|
+
min={min}
|
|
49
|
+
max={max}
|
|
50
|
+
step={step}
|
|
51
|
+
value={
|
|
52
|
+
value !== undefined
|
|
53
|
+
? Array.isArray(value)
|
|
54
|
+
? value
|
|
55
|
+
: [value]
|
|
56
|
+
: undefined
|
|
57
|
+
}
|
|
58
|
+
defaultValue={
|
|
59
|
+
defaultValue !== undefined
|
|
60
|
+
? Array.isArray(defaultValue)
|
|
61
|
+
? defaultValue
|
|
62
|
+
: [defaultValue]
|
|
63
|
+
: undefined
|
|
64
|
+
}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
orientation="horizontal"
|
|
67
|
+
{...props}
|
|
68
|
+
onValueChange={(val, details) => {
|
|
69
|
+
const event =
|
|
70
|
+
(details as { event?: Event })?.event ||
|
|
71
|
+
(window.event as unknown as Event);
|
|
72
|
+
handleChange(val as number | number[], event);
|
|
73
|
+
}}
|
|
74
|
+
className={cn(
|
|
75
|
+
"relative flex items-center w-full h-full group touch-none select-none",
|
|
76
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<BaseSlider.Control className="flex items-center w-full h-full relative cursor-pointer">
|
|
81
|
+
<BaseSlider.Track className="relative bg-gray-200 rounded-full w-full h-1.5 overflow-hidden transition-colors group-hover:bg-gray-300">
|
|
82
|
+
<BaseSlider.Indicator className="absolute bg-primary rounded-full h-full" />
|
|
83
|
+
</BaseSlider.Track>
|
|
84
|
+
<BaseSlider.Thumb className="z-10 block w-4.5 h-4.5 bg-white shadow-md rounded-full border-2 border-primary focus:outline-none focus:ring-4 focus:ring-primary/20 cursor-grab active:cursor-grabbing hover:scale-110 active:scale-95 transition-transform" />
|
|
85
|
+
</BaseSlider.Control>
|
|
86
|
+
</BaseSlider.Root>
|
|
87
|
+
</div>
|
|
88
|
+
{/* Hidden input to support form submission if needed */}
|
|
89
|
+
{name && <input type="hidden" name={name} value={value} />}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
Slider.displayName = "Slider";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import SliderWithNumberField from "./SliderWithNumberField";
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { Heading, Text } from "./Typography";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof SliderWithNumberField> = {
|
|
7
|
+
title: "UI/SliderWithNumberField",
|
|
8
|
+
component: SliderWithNumberField,
|
|
9
|
+
argTypes: {
|
|
10
|
+
onChange: { action: "changed" },
|
|
11
|
+
min: { control: { type: "number" } },
|
|
12
|
+
max: { control: { type: "number" } },
|
|
13
|
+
step: { control: { type: "number" } },
|
|
14
|
+
disabled: { control: "boolean" },
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof SliderWithNumberField>;
|
|
20
|
+
|
|
21
|
+
const InteractiveSliderWithNumberField = (
|
|
22
|
+
props: React.ComponentProps<typeof SliderWithNumberField>,
|
|
23
|
+
) => {
|
|
24
|
+
const [value, setValue] = useState(props.value || props.defaultValue || 0);
|
|
25
|
+
return (
|
|
26
|
+
<SliderWithNumberField
|
|
27
|
+
{...props}
|
|
28
|
+
value={value}
|
|
29
|
+
onChange={(v) => {
|
|
30
|
+
setValue(v);
|
|
31
|
+
props.onChange?.(v as number, new Event("change")); // Match signature
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
min: 0,
|
|
40
|
+
max: 100,
|
|
41
|
+
defaultValue: 50,
|
|
42
|
+
},
|
|
43
|
+
render: (args) => (
|
|
44
|
+
<div className="w-80 p-4">
|
|
45
|
+
<InteractiveSliderWithNumberField
|
|
46
|
+
{...(args as React.ComponentProps<typeof SliderWithNumberField>)}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const Variations: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<div className="flex flex-col gap-12 p-4">
|
|
55
|
+
<div className="space-y-4">
|
|
56
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
57
|
+
Responsive Layouts
|
|
58
|
+
</Heading>
|
|
59
|
+
<div className="space-y-8 max-w-lg">
|
|
60
|
+
<div className="space-y-2">
|
|
61
|
+
<Text variant="small" className="font-bold underline">
|
|
62
|
+
Flexible (Full width of container)
|
|
63
|
+
</Text>
|
|
64
|
+
<InteractiveSliderWithNumberField defaultValue={50} />
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="space-y-2 w-64 border-l border-r border-dotted px-2 bg-gray-50 py-4">
|
|
68
|
+
<Text variant="small" className="font-bold underline">
|
|
69
|
+
Narrow Container (w-64)
|
|
70
|
+
</Text>
|
|
71
|
+
<InteractiveSliderWithNumberField defaultValue={50} />
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div className="space-y-2 w-48 border-l border-r border-dotted px-2 bg-gray-50 py-4">
|
|
75
|
+
<Text variant="small" className="font-bold underline">
|
|
76
|
+
Very Narrow (w-48) - Wraps
|
|
77
|
+
</Text>
|
|
78
|
+
<InteractiveSliderWithNumberField defaultValue={50} />
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div className="space-y-4">
|
|
84
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
85
|
+
Configuration
|
|
86
|
+
</Heading>
|
|
87
|
+
<div className="space-y-4 max-w-sm">
|
|
88
|
+
<div className="space-y-1">
|
|
89
|
+
<Text variant="small">Custom Step (0.5)</Text>
|
|
90
|
+
<InteractiveSliderWithNumberField
|
|
91
|
+
step={0.5}
|
|
92
|
+
defaultValue={2.5}
|
|
93
|
+
min={0}
|
|
94
|
+
max={10}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
),
|
|
101
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Slider } from "./Slider";
|
|
3
|
+
import { NumberField } from "./NumberField";
|
|
4
|
+
import { cn } from "./utils";
|
|
5
|
+
|
|
6
|
+
export interface SliderWithNumberFieldProps {
|
|
7
|
+
min?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
step?: number;
|
|
10
|
+
value?: number;
|
|
11
|
+
defaultValue?: number;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
onChange?: (value: number, event: Event) => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
inputWidth?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const SliderWithNumberField = React.forwardRef<
|
|
19
|
+
HTMLDivElement,
|
|
20
|
+
SliderWithNumberFieldProps
|
|
21
|
+
>(
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
className,
|
|
25
|
+
min = 0,
|
|
26
|
+
max = 100,
|
|
27
|
+
step = 1,
|
|
28
|
+
value,
|
|
29
|
+
defaultValue,
|
|
30
|
+
disabled,
|
|
31
|
+
onChange,
|
|
32
|
+
inputWidth = "w-20",
|
|
33
|
+
...props
|
|
34
|
+
},
|
|
35
|
+
ref,
|
|
36
|
+
) => {
|
|
37
|
+
const handleChange = (val: number | number[], event: Event) => {
|
|
38
|
+
if (onChange) {
|
|
39
|
+
const newValue = Array.isArray(val) ? val[0] : val;
|
|
40
|
+
onChange(newValue, event);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cn(
|
|
48
|
+
"flex flex-row items-center gap-4 w-full min-w-0 sm:flex-nowrap flex-wrap",
|
|
49
|
+
className,
|
|
50
|
+
)}
|
|
51
|
+
{...props}
|
|
52
|
+
>
|
|
53
|
+
<div className="flex-grow min-w-[120px]">
|
|
54
|
+
<Slider
|
|
55
|
+
min={min}
|
|
56
|
+
max={max}
|
|
57
|
+
step={step}
|
|
58
|
+
value={value}
|
|
59
|
+
defaultValue={defaultValue}
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
onChange={handleChange}
|
|
62
|
+
className="w-full"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
<div className={cn(inputWidth, "shrink-0")}>
|
|
66
|
+
<NumberField
|
|
67
|
+
value={value}
|
|
68
|
+
defaultValue={defaultValue}
|
|
69
|
+
min={min}
|
|
70
|
+
max={max}
|
|
71
|
+
step={step}
|
|
72
|
+
disabled={disabled}
|
|
73
|
+
onChange={(val, event) => {
|
|
74
|
+
if (val !== null) {
|
|
75
|
+
handleChange(val, event);
|
|
76
|
+
}
|
|
77
|
+
}}
|
|
78
|
+
showButtons={false}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
SliderWithNumberField.displayName = "SliderWithNumberField";
|
|
87
|
+
|
|
88
|
+
export default SliderWithNumberField;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Switch } from "./Switch";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Heading, Text } from "./Typography";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Switch> = {
|
|
7
|
+
title: "UI/Switch",
|
|
8
|
+
component: Switch,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
argTypes: {
|
|
11
|
+
checked: { control: "boolean" },
|
|
12
|
+
disabled: { control: "boolean" },
|
|
13
|
+
onChange: { action: "changed" },
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof Switch>;
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
args: {
|
|
22
|
+
"aria-label": "Default switch",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const InteractiveSwitch = () => {
|
|
27
|
+
const [checked, setChecked] = useState(false);
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex flex-col gap-2">
|
|
30
|
+
<Switch
|
|
31
|
+
checked={checked}
|
|
32
|
+
onChange={(val) => setChecked(val)}
|
|
33
|
+
aria-label="Interactive switch"
|
|
34
|
+
/>
|
|
35
|
+
<Text variant="small">
|
|
36
|
+
The switch is currently {checked ? "on" : "off"}.
|
|
37
|
+
</Text>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const Variations: Story = {
|
|
43
|
+
render: () => (
|
|
44
|
+
<div className="flex flex-col gap-12 p-4">
|
|
45
|
+
<div className="space-y-4">
|
|
46
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
47
|
+
States
|
|
48
|
+
</Heading>
|
|
49
|
+
<div className="flex flex-col gap-4">
|
|
50
|
+
<div className="flex items-center gap-2">
|
|
51
|
+
<Switch defaultChecked={false} aria-label="Unchecked switch" />
|
|
52
|
+
<Text variant="small">Unchecked</Text>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="flex items-center gap-2">
|
|
55
|
+
<Switch defaultChecked={true} aria-label="Checked switch" />
|
|
56
|
+
<Text variant="small">Checked</Text>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="flex items-center gap-2">
|
|
59
|
+
<Switch disabled aria-label="Disabled switch" />
|
|
60
|
+
<Text variant="small">Disabled Unchecked</Text>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="flex items-center gap-2">
|
|
63
|
+
<Switch
|
|
64
|
+
disabled
|
|
65
|
+
defaultChecked={true}
|
|
66
|
+
aria-label="Disabled checked switch"
|
|
67
|
+
/>
|
|
68
|
+
<Text variant="small">Disabled Checked</Text>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="space-y-4">
|
|
74
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
75
|
+
Interactive
|
|
76
|
+
</Heading>
|
|
77
|
+
<InteractiveSwitch />
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
),
|
|
81
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Switch as BaseSwitch } from "@base-ui/react";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export interface SwitchProps extends React.AriaAttributes {
|
|
6
|
+
checked?: boolean;
|
|
7
|
+
defaultChecked?: boolean;
|
|
8
|
+
onChange?: (checked: boolean, event: Event) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
className?: string;
|
|
11
|
+
id?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
|
|
16
|
+
(
|
|
17
|
+
{
|
|
18
|
+
checked,
|
|
19
|
+
defaultChecked,
|
|
20
|
+
onChange,
|
|
21
|
+
disabled,
|
|
22
|
+
className,
|
|
23
|
+
id: externalId,
|
|
24
|
+
...props
|
|
25
|
+
},
|
|
26
|
+
ref,
|
|
27
|
+
) => {
|
|
28
|
+
const autoId = React.useId();
|
|
29
|
+
const id = externalId ?? autoId;
|
|
30
|
+
return (
|
|
31
|
+
<div className={cn("flex items-center gap-2", className)}>
|
|
32
|
+
<BaseSwitch.Root
|
|
33
|
+
ref={ref}
|
|
34
|
+
id={id}
|
|
35
|
+
checked={checked}
|
|
36
|
+
defaultChecked={defaultChecked}
|
|
37
|
+
onCheckedChange={(checked, eventDetails) => {
|
|
38
|
+
onChange?.(checked, eventDetails?.event as Event);
|
|
39
|
+
}}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
className={cn(
|
|
42
|
+
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2",
|
|
43
|
+
"data-[checked]:bg-primary data-[unchecked]:bg-gray-200",
|
|
44
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
<BaseSwitch.Thumb
|
|
49
|
+
className={cn(
|
|
50
|
+
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out",
|
|
51
|
+
"data-[checked]:translate-x-5 data-[unchecked]:translate-x-0",
|
|
52
|
+
)}
|
|
53
|
+
/>
|
|
54
|
+
</BaseSwitch.Root>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
Switch.displayName = "Switch";
|