@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,86 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { NumberField } from "./NumberField";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Heading, Text } from "./Typography";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof NumberField> = {
|
|
7
|
+
title: "UI/NumberField",
|
|
8
|
+
component: NumberField,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
argTypes: {
|
|
11
|
+
disabled: { control: "boolean" },
|
|
12
|
+
showButtons: { control: "boolean" },
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj<typeof NumberField>;
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
placeholder: "Enter a number...",
|
|
22
|
+
min: 0,
|
|
23
|
+
max: 100,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const InteractiveExample = (args: React.ComponentProps<typeof NumberField>) => {
|
|
28
|
+
const [value, setValue] = useState<number | null>(args.defaultValue ?? 10);
|
|
29
|
+
return (
|
|
30
|
+
<div className="space-y-2">
|
|
31
|
+
<div className="max-w-[200px]">
|
|
32
|
+
<NumberField
|
|
33
|
+
{...args}
|
|
34
|
+
value={value}
|
|
35
|
+
onChange={(val) => setValue(val)}
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
<Text variant="small">Value: {value}</Text>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Variations: Story = {
|
|
44
|
+
render: () => (
|
|
45
|
+
<div className="flex flex-col gap-12 p-4">
|
|
46
|
+
<div className="space-y-4">
|
|
47
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
48
|
+
Configuration
|
|
49
|
+
</Heading>
|
|
50
|
+
<div className="grid gap-8">
|
|
51
|
+
<div className="space-y-2">
|
|
52
|
+
<Text variant="small" className="font-bold">
|
|
53
|
+
With Buttons & Large Step
|
|
54
|
+
</Text>
|
|
55
|
+
<NumberField showButtons defaultValue={50} step={10} />
|
|
56
|
+
</div>
|
|
57
|
+
<div className="space-y-2">
|
|
58
|
+
<Text variant="small" className="font-bold">
|
|
59
|
+
Small Step (0.1) & Limits (0-1)
|
|
60
|
+
</Text>
|
|
61
|
+
<NumberField
|
|
62
|
+
showButtons
|
|
63
|
+
defaultValue={0.5}
|
|
64
|
+
step={0.1}
|
|
65
|
+
min={0}
|
|
66
|
+
max={1}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="space-y-2">
|
|
70
|
+
<Text variant="small" className="font-bold">
|
|
71
|
+
Disabled
|
|
72
|
+
</Text>
|
|
73
|
+
<NumberField disabled defaultValue={123} />
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className="space-y-4">
|
|
79
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
80
|
+
Interactive State
|
|
81
|
+
</Heading>
|
|
82
|
+
<InteractiveExample showButtons={true} />
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
),
|
|
86
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { NumberField as BaseNumberField } from "@base-ui/react";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
import { Icon } from "./Icon";
|
|
5
|
+
|
|
6
|
+
export interface NumberFieldProps {
|
|
7
|
+
value?: number | null;
|
|
8
|
+
defaultValue?: number;
|
|
9
|
+
min?: number;
|
|
10
|
+
max?: number;
|
|
11
|
+
step?: number;
|
|
12
|
+
onChange?: (value: number | null, event: Event) => void;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
readOnly?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
id?: string;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
showButtons?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const NumberField = React.forwardRef<HTMLDivElement, NumberFieldProps>(
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
className,
|
|
25
|
+
value,
|
|
26
|
+
defaultValue,
|
|
27
|
+
min,
|
|
28
|
+
max,
|
|
29
|
+
step,
|
|
30
|
+
onChange,
|
|
31
|
+
disabled,
|
|
32
|
+
readOnly,
|
|
33
|
+
id: propsId,
|
|
34
|
+
placeholder,
|
|
35
|
+
showButtons = false,
|
|
36
|
+
...props
|
|
37
|
+
},
|
|
38
|
+
ref,
|
|
39
|
+
) => {
|
|
40
|
+
const generatedId = React.useId();
|
|
41
|
+
const numberId = propsId || generatedId;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="w-full">
|
|
45
|
+
<BaseNumberField.Root
|
|
46
|
+
ref={ref}
|
|
47
|
+
value={value}
|
|
48
|
+
defaultValue={defaultValue}
|
|
49
|
+
min={min}
|
|
50
|
+
max={max}
|
|
51
|
+
step={step}
|
|
52
|
+
onValueChange={(val, details) => {
|
|
53
|
+
if (onChange) {
|
|
54
|
+
const event = details?.event || (details as unknown as Event);
|
|
55
|
+
onChange(val, event);
|
|
56
|
+
}
|
|
57
|
+
}}
|
|
58
|
+
disabled={disabled}
|
|
59
|
+
readOnly={readOnly}
|
|
60
|
+
className={cn("flex flex-col gap-1", className)}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
<div
|
|
64
|
+
className={cn(
|
|
65
|
+
"group relative flex items-center rounded-md border-0 bg-primary/10 ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-primary shadow-sm",
|
|
66
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{showButtons && (
|
|
70
|
+
<BaseNumberField.Decrement
|
|
71
|
+
aria-label="Decrement"
|
|
72
|
+
className="relative z-20 flex items-center justify-center w-8 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100/50 rounded-l-md border-r border-gray-300 disabled:opacity-30 cursor-pointer"
|
|
73
|
+
>
|
|
74
|
+
<Icon size="sm">
|
|
75
|
+
<path
|
|
76
|
+
strokeLinecap="round"
|
|
77
|
+
strokeLinejoin="round"
|
|
78
|
+
d="M19.5 12h-15"
|
|
79
|
+
/>
|
|
80
|
+
</Icon>
|
|
81
|
+
</BaseNumberField.Decrement>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
<BaseNumberField.Input
|
|
85
|
+
id={numberId}
|
|
86
|
+
placeholder={placeholder}
|
|
87
|
+
className={cn(
|
|
88
|
+
"block w-full bg-transparent border-0 px-3 py-1.5 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6 text-center appearance-none relative z-0",
|
|
89
|
+
!showButtons && "rounded-md",
|
|
90
|
+
)}
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
{showButtons && (
|
|
94
|
+
<BaseNumberField.Increment
|
|
95
|
+
aria-label="Increment"
|
|
96
|
+
className="relative z-20 flex items-center justify-center w-8 h-9 text-gray-500 hover:text-gray-700 hover:bg-gray-100/50 rounded-r-md border-l border-gray-300 disabled:opacity-30 cursor-pointer"
|
|
97
|
+
>
|
|
98
|
+
<Icon size="sm">
|
|
99
|
+
<path
|
|
100
|
+
strokeLinecap="round"
|
|
101
|
+
strokeLinejoin="round"
|
|
102
|
+
d="M12 4.5v15m7.5-7.5h-15"
|
|
103
|
+
/>
|
|
104
|
+
</Icon>
|
|
105
|
+
</BaseNumberField.Increment>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
<BaseNumberField.ScrubArea className="absolute inset-0 z-10 cursor-ew-resize opacity-0 hover:opacity-10 transition-opacity bg-primary/5 rounded-md pointer-events-none group-hover:pointer-events-auto" />
|
|
109
|
+
</div>
|
|
110
|
+
</BaseNumberField.Root>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
NumberField.displayName = "NumberField";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Popover as BasePopover } from "@base-ui/react";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export interface PopoverProps {
|
|
6
|
+
trigger: React.ReactNode;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
9
|
+
align?: "start" | "center" | "end";
|
|
10
|
+
className?: string;
|
|
11
|
+
open?: boolean;
|
|
12
|
+
onOpenChange?: (open: boolean) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Popover({
|
|
16
|
+
trigger,
|
|
17
|
+
children,
|
|
18
|
+
side = "bottom",
|
|
19
|
+
align = "center",
|
|
20
|
+
className,
|
|
21
|
+
open,
|
|
22
|
+
onOpenChange,
|
|
23
|
+
}: PopoverProps) {
|
|
24
|
+
return (
|
|
25
|
+
<BasePopover.Root open={open} onOpenChange={onOpenChange}>
|
|
26
|
+
<BasePopover.Trigger>{trigger}</BasePopover.Trigger>
|
|
27
|
+
<BasePopover.Portal>
|
|
28
|
+
<BasePopover.Positioner side={side} align={align} sideOffset={5}>
|
|
29
|
+
<BasePopover.Popup
|
|
30
|
+
className={cn(
|
|
31
|
+
"z-50 w-72 rounded-md border bg-white p-4 shadow-md outline-none 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",
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
<BasePopover.Arrow className="fill-white stroke-gray-200" />
|
|
37
|
+
</BasePopover.Popup>
|
|
38
|
+
</BasePopover.Positioner>
|
|
39
|
+
</BasePopover.Portal>
|
|
40
|
+
</BasePopover.Root>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Select } from "./Select";
|
|
3
|
+
import { Heading, Text } from "./Typography";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Select> = {
|
|
6
|
+
title: "UI/Select",
|
|
7
|
+
component: Select,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
argTypes: {
|
|
10
|
+
disabled: { control: "boolean" },
|
|
11
|
+
onChange: { action: "changed" },
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof Select>;
|
|
17
|
+
|
|
18
|
+
const simpleOptions = ["Option 1", "Option 2", "Option 3"];
|
|
19
|
+
const objectOptions = [
|
|
20
|
+
{ label: "Banana", value: "banana" },
|
|
21
|
+
{ label: "Apple", value: "apple" },
|
|
22
|
+
{ label: "Orange", value: "orange" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const Default: Story = {
|
|
26
|
+
args: {
|
|
27
|
+
options: simpleOptions,
|
|
28
|
+
placeholder: "Select an option...",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Variations: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<div className="flex flex-col gap-8 p-4 max-w-sm">
|
|
35
|
+
<div className="space-y-4">
|
|
36
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
37
|
+
Option Types
|
|
38
|
+
</Heading>
|
|
39
|
+
<div className="space-y-4">
|
|
40
|
+
<div className="space-y-1">
|
|
41
|
+
<Text variant="small" className="font-bold">
|
|
42
|
+
String Options
|
|
43
|
+
</Text>
|
|
44
|
+
<Select options={simpleOptions} />
|
|
45
|
+
</div>
|
|
46
|
+
<div className="space-y-1">
|
|
47
|
+
<Text variant="small" className="font-bold">
|
|
48
|
+
Object Options (Label/Value)
|
|
49
|
+
</Text>
|
|
50
|
+
<Select options={objectOptions} defaultValue="apple" />
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="space-y-4">
|
|
56
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
57
|
+
States
|
|
58
|
+
</Heading>
|
|
59
|
+
<div className="space-y-4">
|
|
60
|
+
<div className="space-y-1">
|
|
61
|
+
<Text variant="small" className="font-bold">
|
|
62
|
+
Disabled
|
|
63
|
+
</Text>
|
|
64
|
+
<Select
|
|
65
|
+
options={simpleOptions}
|
|
66
|
+
disabled
|
|
67
|
+
placeholder="Can't select"
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
),
|
|
74
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Select as BaseSelect } from "@base-ui/react";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
import { Icon } from "./Icon";
|
|
5
|
+
|
|
6
|
+
export interface SelectOption {
|
|
7
|
+
label: string;
|
|
8
|
+
value: string | number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SelectProps {
|
|
12
|
+
options: (string | SelectOption)[];
|
|
13
|
+
value?: string | number;
|
|
14
|
+
defaultValue?: string | number;
|
|
15
|
+
onChange?: (value: string | number | null, event: Event) => void;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
id?: string;
|
|
20
|
+
onFocus?: React.FocusEventHandler<HTMLButtonElement>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(
|
|
24
|
+
(
|
|
25
|
+
{
|
|
26
|
+
className,
|
|
27
|
+
options,
|
|
28
|
+
id: propsId,
|
|
29
|
+
value,
|
|
30
|
+
defaultValue,
|
|
31
|
+
onChange,
|
|
32
|
+
disabled,
|
|
33
|
+
placeholder = "Select...",
|
|
34
|
+
onFocus,
|
|
35
|
+
...props
|
|
36
|
+
},
|
|
37
|
+
ref,
|
|
38
|
+
) => {
|
|
39
|
+
const generatedId = React.useId();
|
|
40
|
+
const selectId = propsId || generatedId;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="w-full">
|
|
44
|
+
<BaseSelect.Root
|
|
45
|
+
value={value === undefined ? null : String(value)}
|
|
46
|
+
defaultValue={
|
|
47
|
+
defaultValue === undefined ? undefined : String(defaultValue)
|
|
48
|
+
}
|
|
49
|
+
onValueChange={(
|
|
50
|
+
val: string | null,
|
|
51
|
+
details?: { event: Event } | null,
|
|
52
|
+
) => {
|
|
53
|
+
if (onChange) {
|
|
54
|
+
// Base UI Select passes eventDetails as the second argument, which has an 'event' property.
|
|
55
|
+
const event = details?.event || (details as unknown as Event);
|
|
56
|
+
onChange(val, event);
|
|
57
|
+
}
|
|
58
|
+
}}
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
>
|
|
61
|
+
<BaseSelect.Trigger
|
|
62
|
+
ref={ref}
|
|
63
|
+
id={selectId}
|
|
64
|
+
className={cn(
|
|
65
|
+
"flex w-full items-center justify-between rounded-md border-0 py-1.5 pl-3 pr-3 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-primary sm:text-sm sm:leading-6 bg-primary/10 min-h-[36px]",
|
|
66
|
+
disabled && "opacity-50 cursor-not-allowed",
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
onFocus={onFocus}
|
|
70
|
+
{...props}
|
|
71
|
+
>
|
|
72
|
+
<BaseSelect.Value placeholder={placeholder} />
|
|
73
|
+
<BaseSelect.Icon>
|
|
74
|
+
<Icon size="sm">
|
|
75
|
+
<path
|
|
76
|
+
strokeLinecap="round"
|
|
77
|
+
strokeLinejoin="round"
|
|
78
|
+
d="m19.5 8.25-7.5 7.5-7.5-7.5"
|
|
79
|
+
/>
|
|
80
|
+
</Icon>
|
|
81
|
+
</BaseSelect.Icon>
|
|
82
|
+
</BaseSelect.Trigger>
|
|
83
|
+
<BaseSelect.Portal>
|
|
84
|
+
<BaseSelect.Positioner
|
|
85
|
+
sideOffset={4}
|
|
86
|
+
className="z-50 w-[var(--anchor-width)]"
|
|
87
|
+
>
|
|
88
|
+
<BaseSelect.Popup className="max-h-60 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
|
89
|
+
{options.map((option, index) => {
|
|
90
|
+
const label =
|
|
91
|
+
typeof option === "object" ? option.label : option;
|
|
92
|
+
const value =
|
|
93
|
+
typeof option === "object" ? option.value : option;
|
|
94
|
+
return (
|
|
95
|
+
<BaseSelect.Item
|
|
96
|
+
key={`${value}-${index}`}
|
|
97
|
+
value={value}
|
|
98
|
+
className="relative cursor-default select-none py-2 pl-3 pr-9 text-gray-900 hover:bg-gray-100 data-[highlighted]:bg-gray-100 data-[selected]:font-semibold"
|
|
99
|
+
>
|
|
100
|
+
<BaseSelect.ItemText>{label}</BaseSelect.ItemText>
|
|
101
|
+
<BaseSelect.ItemIndicator className="absolute inset-y-0 right-0 flex items-center pr-4 text-primary">
|
|
102
|
+
<Icon size="sm">
|
|
103
|
+
<path
|
|
104
|
+
strokeLinecap="round"
|
|
105
|
+
strokeLinejoin="round"
|
|
106
|
+
d="M4.5 12.75l6 6 9-13.5"
|
|
107
|
+
/>
|
|
108
|
+
</Icon>
|
|
109
|
+
</BaseSelect.ItemIndicator>
|
|
110
|
+
</BaseSelect.Item>
|
|
111
|
+
);
|
|
112
|
+
})}
|
|
113
|
+
</BaseSelect.Popup>
|
|
114
|
+
</BaseSelect.Positioner>
|
|
115
|
+
</BaseSelect.Portal>
|
|
116
|
+
</BaseSelect.Root>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
Select.displayName = "Select";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Separator } from "./Separator";
|
|
3
|
+
import { Heading, Text } from "./Typography";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Separator> = {
|
|
6
|
+
title: "UI/Separator",
|
|
7
|
+
component: Separator,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
argTypes: {
|
|
10
|
+
orientation: {
|
|
11
|
+
control: "radio",
|
|
12
|
+
options: ["horizontal", "vertical"],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof Separator>;
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
args: {
|
|
22
|
+
orientation: "horizontal",
|
|
23
|
+
},
|
|
24
|
+
render: (args) => (
|
|
25
|
+
<div className="w-64">
|
|
26
|
+
<Text>Above the separator</Text>
|
|
27
|
+
<Separator {...args} className="my-4" />
|
|
28
|
+
<Text>Below the separator</Text>
|
|
29
|
+
</div>
|
|
30
|
+
),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Variations: Story = {
|
|
34
|
+
render: () => (
|
|
35
|
+
<div className="flex flex-col gap-12 p-4">
|
|
36
|
+
<div className="space-y-4">
|
|
37
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
38
|
+
Horizontal (Default)
|
|
39
|
+
</Heading>
|
|
40
|
+
<div className="w-64">
|
|
41
|
+
<Text variant="small">Item A</Text>
|
|
42
|
+
<Separator className="my-2" />
|
|
43
|
+
<Text variant="small">Item B</Text>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div className="space-y-4">
|
|
48
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
49
|
+
Vertical
|
|
50
|
+
</Heading>
|
|
51
|
+
<div className="flex h-8 items-center gap-4 text-sm">
|
|
52
|
+
<Text variant="small">Option 1</Text>
|
|
53
|
+
<Separator orientation="vertical" />
|
|
54
|
+
<Text variant="small">Option 2</Text>
|
|
55
|
+
<Separator orientation="vertical" />
|
|
56
|
+
<Text variant="small">Option 3</Text>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
),
|
|
61
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Separator as BaseSeparator } from "@base-ui/react/separator";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export interface SeparatorProps extends React.ComponentPropsWithoutRef<
|
|
6
|
+
typeof BaseSeparator
|
|
7
|
+
> {
|
|
8
|
+
orientation?: "horizontal" | "vertical";
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Separator = React.forwardRef<
|
|
13
|
+
React.ElementRef<typeof BaseSeparator>,
|
|
14
|
+
SeparatorProps
|
|
15
|
+
>(({ className, orientation = "horizontal", ...props }, ref) => (
|
|
16
|
+
<BaseSeparator
|
|
17
|
+
ref={ref}
|
|
18
|
+
orientation={orientation}
|
|
19
|
+
className={cn(
|
|
20
|
+
"shrink-0 bg-gray-200",
|
|
21
|
+
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
));
|
|
27
|
+
|
|
28
|
+
Separator.displayName = BaseSeparator.displayName;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { SettingsPanel } from "./SettingsPanel";
|
|
3
|
+
import { Button } from "./Button";
|
|
4
|
+
import { Checkbox } from "./Checkbox";
|
|
5
|
+
import { Heading, Text } from "./Typography";
|
|
6
|
+
import { Label } from "./Label";
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof SettingsPanel> = {
|
|
9
|
+
title: "UI/SettingsPanel",
|
|
10
|
+
component: SettingsPanel,
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
argTypes: {},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
title: "Account Settings",
|
|
21
|
+
children: (
|
|
22
|
+
<div className="space-y-4">
|
|
23
|
+
<Text>This is the content of the settings panel.</Text>
|
|
24
|
+
<div className="flex gap-2">
|
|
25
|
+
<Button variant="primary">Save Changes</Button>
|
|
26
|
+
<Button variant="secondary">Cancel</Button>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Variations: Story = {
|
|
34
|
+
render: () => (
|
|
35
|
+
<div className="flex flex-col gap-12 p-4 max-w-2xl bg-gray-50">
|
|
36
|
+
<div className="space-y-4">
|
|
37
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
38
|
+
Default
|
|
39
|
+
</Heading>
|
|
40
|
+
<SettingsPanel title="Notification Settings">
|
|
41
|
+
<div className="space-y-4">
|
|
42
|
+
<Text>Manage how you receive notifications.</Text>
|
|
43
|
+
<div className="flex items-center gap-2 p-2 rounded bg-white">
|
|
44
|
+
<Checkbox id="email-notif" />
|
|
45
|
+
<Label htmlFor="email-notif" className="text-sm">
|
|
46
|
+
Email Notifications
|
|
47
|
+
</Label>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</SettingsPanel>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className="space-y-4">
|
|
54
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
55
|
+
No Title
|
|
56
|
+
</Heading>
|
|
57
|
+
<SettingsPanel>
|
|
58
|
+
<Text className="italic">
|
|
59
|
+
Panel without a title rendered as a simple container.
|
|
60
|
+
</Text>
|
|
61
|
+
</SettingsPanel>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="space-y-4">
|
|
65
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
66
|
+
Collapsible States
|
|
67
|
+
</Heading>
|
|
68
|
+
<div className="space-y-4">
|
|
69
|
+
<SettingsPanel title="Collapsible (Default Open)" collapsible={true}>
|
|
70
|
+
<Text>This panel can be toggled and starts open.</Text>
|
|
71
|
+
</SettingsPanel>
|
|
72
|
+
<SettingsPanel
|
|
73
|
+
title="Collapsible (Default Closed)"
|
|
74
|
+
collapsible={true}
|
|
75
|
+
defaultOpen={false}
|
|
76
|
+
>
|
|
77
|
+
<Text>This panel starts closed to save space.</Text>
|
|
78
|
+
</SettingsPanel>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
),
|
|
83
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { Heading } from "./Typography";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
import { Collapsible } from "./Collapsible";
|
|
5
|
+
|
|
6
|
+
export interface SettingsPanelProps {
|
|
7
|
+
/** The title of the section */
|
|
8
|
+
title?: ReactNode;
|
|
9
|
+
/** The content of the panel */
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
/** Custom class for the panel wrapper */
|
|
12
|
+
className?: string;
|
|
13
|
+
/** Custom class for the content area */
|
|
14
|
+
contentClassName?: string;
|
|
15
|
+
/** Whether the panel is collapsible */
|
|
16
|
+
collapsible?: boolean;
|
|
17
|
+
/** Default state of the collapsible panel */
|
|
18
|
+
defaultOpen?: boolean;
|
|
19
|
+
/** The heading level for the title */
|
|
20
|
+
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A standardized panel for settings sections.
|
|
25
|
+
* Provides a consistent look with white background, shadow, and rounded corners.
|
|
26
|
+
*/
|
|
27
|
+
export function SettingsPanel({
|
|
28
|
+
title,
|
|
29
|
+
children,
|
|
30
|
+
className,
|
|
31
|
+
contentClassName,
|
|
32
|
+
collapsible,
|
|
33
|
+
defaultOpen = true,
|
|
34
|
+
headingLevel = 3,
|
|
35
|
+
}: SettingsPanelProps) {
|
|
36
|
+
const content = (
|
|
37
|
+
<div className={cn("flex flex-col", contentClassName)}>{children}</div>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
className={cn(
|
|
43
|
+
"m-2 p-4 bg-white shadow-xl rounded-lg flex flex-1 flex-col",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{collapsible ? (
|
|
48
|
+
<Collapsible
|
|
49
|
+
title={
|
|
50
|
+
typeof title === "string" ? (
|
|
51
|
+
<Heading level={headingLevel} className="text-gray-600">
|
|
52
|
+
{title}
|
|
53
|
+
</Heading>
|
|
54
|
+
) : (
|
|
55
|
+
title
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
variant="ghost"
|
|
59
|
+
defaultOpen={defaultOpen}
|
|
60
|
+
>
|
|
61
|
+
{content}
|
|
62
|
+
</Collapsible>
|
|
63
|
+
) : (
|
|
64
|
+
<>
|
|
65
|
+
{title && (
|
|
66
|
+
<div className="flex items-center justify-between mb-2 empty:hidden">
|
|
67
|
+
{typeof title === "string" ? (
|
|
68
|
+
<Heading level={headingLevel} className="text-gray-600">
|
|
69
|
+
{title}
|
|
70
|
+
</Heading>
|
|
71
|
+
) : (
|
|
72
|
+
title
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
{content}
|
|
77
|
+
</>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|