@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,91 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Collapsible } from "./Collapsible";
|
|
3
|
+
import { Heading } from "./Typography";
|
|
4
|
+
import React from "react";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Collapsible> = {
|
|
7
|
+
title: "UI/Collapsible",
|
|
8
|
+
component: Collapsible,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof Collapsible>;
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
title: "Click to expand",
|
|
18
|
+
children: (
|
|
19
|
+
<div className="p-4 bg-gray-50 rounded-md">
|
|
20
|
+
This is the collapsible content. It can contain any elements.
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Variations: Story = {
|
|
27
|
+
render: () => (
|
|
28
|
+
<div className="flex flex-col gap-8 p-4">
|
|
29
|
+
<div className="space-y-4">
|
|
30
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
31
|
+
States & Styling
|
|
32
|
+
</Heading>
|
|
33
|
+
<div className="space-y-4">
|
|
34
|
+
<Collapsible title="Initially Open" defaultOpen={true}>
|
|
35
|
+
<div className="p-4 bg-gray-50 rounded-md">
|
|
36
|
+
This content is visible by default.
|
|
37
|
+
</div>
|
|
38
|
+
</Collapsible>
|
|
39
|
+
<Collapsible
|
|
40
|
+
title={<span className="font-bold text-primary">Styled Title</span>}
|
|
41
|
+
className="border border-gray-200 rounded-lg overflow-hidden"
|
|
42
|
+
>
|
|
43
|
+
<div className="p-4">Custom styled wrapper and title.</div>
|
|
44
|
+
</Collapsible>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="space-y-4">
|
|
49
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
50
|
+
Variants
|
|
51
|
+
</Heading>
|
|
52
|
+
<div className="space-y-4">
|
|
53
|
+
<Collapsible title="Secondary (Default)" variant="secondary">
|
|
54
|
+
<div className="p-4 bg-gray-50 rounded-md">
|
|
55
|
+
Secondary variant content.
|
|
56
|
+
</div>
|
|
57
|
+
</Collapsible>
|
|
58
|
+
<Collapsible title="Ghost" variant="ghost">
|
|
59
|
+
<div className="p-4 bg-gray-50 rounded-md">
|
|
60
|
+
Ghost variant content.
|
|
61
|
+
</div>
|
|
62
|
+
</Collapsible>
|
|
63
|
+
<Collapsible title="Primary" variant="primary">
|
|
64
|
+
<div className="p-4 bg-gray-50 rounded-md">
|
|
65
|
+
Primary variant content.
|
|
66
|
+
</div>
|
|
67
|
+
</Collapsible>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className="space-y-4">
|
|
72
|
+
<Heading level={4} className="text-sm font-medium text-black uppercase">
|
|
73
|
+
Sizes
|
|
74
|
+
</Heading>
|
|
75
|
+
<div className="space-y-4">
|
|
76
|
+
<Collapsible title="Small" size="sm">
|
|
77
|
+
<div className="p-4 bg-gray-50 rounded-md">Small size content.</div>
|
|
78
|
+
</Collapsible>
|
|
79
|
+
<Collapsible title="Medium (Default)" size="md">
|
|
80
|
+
<div className="p-4 bg-gray-50 rounded-md">
|
|
81
|
+
Medium size content.
|
|
82
|
+
</div>
|
|
83
|
+
</Collapsible>
|
|
84
|
+
<Collapsible title="Large" size="lg">
|
|
85
|
+
<div className="p-4 bg-gray-50 rounded-md">Large size content.</div>
|
|
86
|
+
</Collapsible>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
),
|
|
91
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { cn } from "./utils";
|
|
3
|
+
import { Button, ButtonProps } from "./Button";
|
|
4
|
+
import { Icon } from "./Icon";
|
|
5
|
+
|
|
6
|
+
export interface CollapsibleProps {
|
|
7
|
+
title: React.ReactNode;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
/** The variant of the trigger button */
|
|
10
|
+
variant?: ButtonProps["variant"];
|
|
11
|
+
/** The size of the trigger button */
|
|
12
|
+
size?: ButtonProps["size"];
|
|
13
|
+
defaultOpen?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Collapsible({
|
|
18
|
+
title,
|
|
19
|
+
children,
|
|
20
|
+
variant = "secondary",
|
|
21
|
+
size = "md",
|
|
22
|
+
defaultOpen = false,
|
|
23
|
+
className,
|
|
24
|
+
}: CollapsibleProps) {
|
|
25
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={cn("w-full", className)}>
|
|
29
|
+
<Button
|
|
30
|
+
variant={variant}
|
|
31
|
+
size={size}
|
|
32
|
+
className="w-full justify-between font-medium"
|
|
33
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
34
|
+
aria-expanded={isOpen}
|
|
35
|
+
rightIcon={
|
|
36
|
+
<Icon className="h-5 w-5 text-gray-600" strokeWidth="2.5">
|
|
37
|
+
{isOpen ? (
|
|
38
|
+
<path d="M4.5 15.75l7.5-7.5 7.5 7.5" />
|
|
39
|
+
) : (
|
|
40
|
+
<path d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
41
|
+
)}
|
|
42
|
+
</Icon>
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
<span>{title}</span>
|
|
46
|
+
</Button>
|
|
47
|
+
{isOpen && (
|
|
48
|
+
<div className="px-4 pt-4 pb-2 text-sm text-gray-500">{children}</div>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
|
|
3
|
+
const meta: Meta = {
|
|
4
|
+
title: "UI/Colors",
|
|
5
|
+
tags: ["autodocs"],
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default meta;
|
|
9
|
+
|
|
10
|
+
export const Variations: StoryObj = {
|
|
11
|
+
render: () => (
|
|
12
|
+
<div className="flex flex-col gap-8 p-4 bg-white">
|
|
13
|
+
<div>
|
|
14
|
+
<h3 className="text-sm font-medium text-black uppercase tracking-wider border-b pb-1 mb-4">
|
|
15
|
+
Theme Colors
|
|
16
|
+
</h3>
|
|
17
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
|
18
|
+
<ColorItem name="Primary" variable="var(--color-primary)" />
|
|
19
|
+
<ColorItem name="Secondary" variable="var(--color-secondary)" />
|
|
20
|
+
<ColorItem name="Accent" variable="var(--color-accent)" />
|
|
21
|
+
<ColorItem name="Danger" variable="var(--color-danger)" />
|
|
22
|
+
<ColorItem name="Light BG" variable="var(--color-light-bg)" />
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div>
|
|
27
|
+
<h3 className="text-sm font-medium text-black uppercase tracking-wider border-b pb-1 mb-4">
|
|
28
|
+
Extended Accent Colors
|
|
29
|
+
</h3>
|
|
30
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
31
|
+
<ColorItem name="Accent HC" variable="var(--color-accent-hc)" />
|
|
32
|
+
<ColorItem name="Accent Two" variable="var(--color-accent-two)" />
|
|
33
|
+
<ColorItem
|
|
34
|
+
name="Accent Two HC"
|
|
35
|
+
variable="var(--color-accent-two-hc)"
|
|
36
|
+
/>
|
|
37
|
+
<ColorItem name="Accent Three" variable="var(--color-accent-three)" />
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div>
|
|
42
|
+
<h3 className="text-sm font-medium text-black uppercase tracking-wider border-b pb-1 mb-4">
|
|
43
|
+
UI Elements
|
|
44
|
+
</h3>
|
|
45
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
46
|
+
<ColorItem name="Sidebar BG" variable="var(--sidebar-bg)" />
|
|
47
|
+
<ColorItem name="Dark Primary" variable="var(--color-dark-primary)" />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function ColorItem({ name, variable }: { name: string; variable: string }) {
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex flex-col gap-2">
|
|
57
|
+
<div
|
|
58
|
+
className="h-20 w-full rounded-md shadow-inner border border-gray-100"
|
|
59
|
+
style={{ backgroundColor: variable }}
|
|
60
|
+
/>
|
|
61
|
+
<div className="flex flex-col">
|
|
62
|
+
<span className="text-sm font-bold text-gray-900">{name}</span>
|
|
63
|
+
<code className="text-xs text-gray-500">{variable}</code>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Dialog, DialogTitle, DialogDescription, DialogClose } from "./Dialog";
|
|
3
|
+
import { Button } from "./Button";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Dialog> = {
|
|
6
|
+
title: "UI/Dialog",
|
|
7
|
+
component: Dialog,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "centered",
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof Dialog>;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<Dialog trigger={<Button>Open Default Dialog</Button>}>
|
|
19
|
+
<DialogTitle className="text-xl font-bold mb-2">Dialog Title</DialogTitle>
|
|
20
|
+
<DialogDescription className="text-black mb-6">
|
|
21
|
+
This is a description of the dialog content.
|
|
22
|
+
</DialogDescription>
|
|
23
|
+
<div className="flex justify-end gap-2">
|
|
24
|
+
<DialogClose render={<Button variant="secondary">Cancel</Button>} />
|
|
25
|
+
<DialogClose render={<Button variant="primary">Confirm</Button>} />
|
|
26
|
+
</div>
|
|
27
|
+
</Dialog>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Dialog as BaseDialog } from "@base-ui/react";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export interface DialogProps {
|
|
6
|
+
open?: boolean;
|
|
7
|
+
onOpenChange?: (open: boolean, event?: Event) => void;
|
|
8
|
+
trigger?: React.ReactNode;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Dialog({
|
|
14
|
+
open,
|
|
15
|
+
onOpenChange,
|
|
16
|
+
trigger,
|
|
17
|
+
children,
|
|
18
|
+
className,
|
|
19
|
+
}: DialogProps) {
|
|
20
|
+
return (
|
|
21
|
+
<BaseDialog.Root
|
|
22
|
+
open={open}
|
|
23
|
+
onOpenChange={(open, eventDetails) => {
|
|
24
|
+
onOpenChange?.(open, eventDetails?.event as Event | undefined);
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
{trigger &&
|
|
28
|
+
(React.isValidElement(trigger) ? (
|
|
29
|
+
<BaseDialog.Trigger render={trigger as React.ReactElement} />
|
|
30
|
+
) : (
|
|
31
|
+
<BaseDialog.Trigger>{trigger}</BaseDialog.Trigger>
|
|
32
|
+
))}
|
|
33
|
+
<BaseDialog.Portal>
|
|
34
|
+
<BaseDialog.Backdrop className="fixed inset-0 z-50 bg-black/40 transition-all duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
|
|
35
|
+
<BaseDialog.Popup
|
|
36
|
+
className={cn(
|
|
37
|
+
"fixed left-[50%] top-[50%] z-50 grid w-auto max-w-[90vw] translate-x-[-50%] translate-y-[-50%] gap-4 border bg-white p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
|
38
|
+
className,
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</BaseDialog.Popup>
|
|
43
|
+
</BaseDialog.Portal>
|
|
44
|
+
</BaseDialog.Root>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const DialogTitle = (
|
|
49
|
+
props: React.ComponentProps<typeof BaseDialog.Title>,
|
|
50
|
+
) => <BaseDialog.Title {...props} />;
|
|
51
|
+
export const DialogDescription = (
|
|
52
|
+
props: React.ComponentProps<typeof BaseDialog.Description>,
|
|
53
|
+
) => <BaseDialog.Description {...props} />;
|
|
54
|
+
export const DialogClose = (
|
|
55
|
+
props: React.ComponentProps<typeof BaseDialog.Close>,
|
|
56
|
+
) => <BaseDialog.Close {...props} />;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Menu as BaseMenu } from "@base-ui/react";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export interface DropdownProps {
|
|
6
|
+
trigger: React.ReactNode;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
align?: "start" | "center" | "end";
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Dropdown({
|
|
13
|
+
trigger,
|
|
14
|
+
children,
|
|
15
|
+
align = "end",
|
|
16
|
+
className,
|
|
17
|
+
}: DropdownProps) {
|
|
18
|
+
return (
|
|
19
|
+
<BaseMenu.Root>
|
|
20
|
+
{React.isValidElement(trigger) ? (
|
|
21
|
+
<BaseMenu.Trigger render={trigger} />
|
|
22
|
+
) : (
|
|
23
|
+
<BaseMenu.Trigger>{trigger}</BaseMenu.Trigger>
|
|
24
|
+
)}
|
|
25
|
+
<BaseMenu.Portal>
|
|
26
|
+
<BaseMenu.Positioner align={align} sideOffset={5}>
|
|
27
|
+
<BaseMenu.Popup
|
|
28
|
+
className={cn(
|
|
29
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-white p-1 shadow-md 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",
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</BaseMenu.Popup>
|
|
35
|
+
</BaseMenu.Positioner>
|
|
36
|
+
</BaseMenu.Portal>
|
|
37
|
+
</BaseMenu.Root>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const DropdownItem = React.forwardRef<
|
|
42
|
+
HTMLDivElement,
|
|
43
|
+
React.ComponentPropsWithoutRef<typeof BaseMenu.Item>
|
|
44
|
+
>(({ className, ...props }, ref) => (
|
|
45
|
+
<BaseMenu.Item
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cn(
|
|
48
|
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[highlighted]:bg-gray-100 data-[highlighted]:text-gray-900",
|
|
49
|
+
className,
|
|
50
|
+
)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
));
|
|
54
|
+
DropdownItem.displayName = "DropdownItem";
|
|
55
|
+
|
|
56
|
+
export const DropdownSeparator = React.forwardRef<
|
|
57
|
+
HTMLDivElement,
|
|
58
|
+
React.ComponentPropsWithoutRef<typeof BaseMenu.Separator>
|
|
59
|
+
>(({ className, ...props }, ref) => (
|
|
60
|
+
<BaseMenu.Separator
|
|
61
|
+
ref={ref}
|
|
62
|
+
className={cn("-mx-1 my-1 h-px bg-gray-100", className)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
));
|
|
66
|
+
DropdownSeparator.displayName = "DropdownSeparator";
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Field } from "./Field";
|
|
3
|
+
import { Icon } from "./Icon";
|
|
4
|
+
import { Heading } from "./Typography";
|
|
5
|
+
import { Input } from "./Input";
|
|
6
|
+
import { Slider } from "./Slider";
|
|
7
|
+
import { Select } from "./Select";
|
|
8
|
+
import { Switch } from "./Switch";
|
|
9
|
+
import { NumberField } from "./NumberField";
|
|
10
|
+
import { SliderWithNumberField } from "./SliderWithNumberField";
|
|
11
|
+
import { Checkbox } from "./Checkbox";
|
|
12
|
+
|
|
13
|
+
const meta: Meta<typeof Field> = {
|
|
14
|
+
title: "UI/Field",
|
|
15
|
+
component: Field,
|
|
16
|
+
tags: ["autodocs"],
|
|
17
|
+
argTypes: {
|
|
18
|
+
label: { control: "text" },
|
|
19
|
+
description: { control: "text" },
|
|
20
|
+
error: { control: "text" },
|
|
21
|
+
tooltip: { control: "text" },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
type Story = StoryObj<typeof Field>;
|
|
27
|
+
|
|
28
|
+
export const Default: Story = {
|
|
29
|
+
args: {
|
|
30
|
+
label: "Email Address",
|
|
31
|
+
description: "We'll never share your email.",
|
|
32
|
+
tooltip: "Enter your primary email address",
|
|
33
|
+
children: <Input placeholder="you@example.com" />,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Variations: Story = {
|
|
38
|
+
render: () => (
|
|
39
|
+
<div className="flex flex-col gap-12 p-4 max-w-2xl">
|
|
40
|
+
<div className="space-y-4">
|
|
41
|
+
<Heading
|
|
42
|
+
level={4}
|
|
43
|
+
className="text-sm font-medium text-black uppercase tracking-wider"
|
|
44
|
+
>
|
|
45
|
+
Basic Inputs
|
|
46
|
+
</Heading>
|
|
47
|
+
<div className="grid gap-6">
|
|
48
|
+
<Field
|
|
49
|
+
label="Text Input"
|
|
50
|
+
description="Standard text field"
|
|
51
|
+
tooltip="Helping text"
|
|
52
|
+
>
|
|
53
|
+
<Input placeholder="Enter something..." />
|
|
54
|
+
</Field>
|
|
55
|
+
<Field label="Password" error="Password is too weak">
|
|
56
|
+
<Input type="password" defaultValue="123" />
|
|
57
|
+
</Field>
|
|
58
|
+
<Field label="Number Field">
|
|
59
|
+
<NumberField showButtons defaultValue={10} />
|
|
60
|
+
</Field>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="space-y-4">
|
|
65
|
+
<Heading
|
|
66
|
+
level={4}
|
|
67
|
+
className="text-sm font-medium text-black uppercase tracking-wider"
|
|
68
|
+
>
|
|
69
|
+
Selection & Toggles
|
|
70
|
+
</Heading>
|
|
71
|
+
<div className="grid gap-6">
|
|
72
|
+
<Field label="Select Language">
|
|
73
|
+
<Select
|
|
74
|
+
options={[
|
|
75
|
+
{ label: "English", value: "en" },
|
|
76
|
+
{ label: "Spanish", value: "es" },
|
|
77
|
+
{ label: "French", value: "fr" },
|
|
78
|
+
]}
|
|
79
|
+
placeholder="Choose a language"
|
|
80
|
+
/>
|
|
81
|
+
</Field>
|
|
82
|
+
<Field
|
|
83
|
+
label="Enable Notifications"
|
|
84
|
+
className="flex-row items-center justify-between"
|
|
85
|
+
>
|
|
86
|
+
<Switch />
|
|
87
|
+
</Field>
|
|
88
|
+
<Field
|
|
89
|
+
label="Enable automation"
|
|
90
|
+
labelPlacement="right"
|
|
91
|
+
labelWeight="normal"
|
|
92
|
+
>
|
|
93
|
+
<Checkbox id="automation" />
|
|
94
|
+
</Field>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="space-y-4">
|
|
99
|
+
<Heading
|
|
100
|
+
level={4}
|
|
101
|
+
className="text-sm font-medium text-black uppercase tracking-wider"
|
|
102
|
+
>
|
|
103
|
+
Sliders
|
|
104
|
+
</Heading>
|
|
105
|
+
<div className="grid gap-6">
|
|
106
|
+
<Field label="Simple Slider" tooltip="Adjust volume">
|
|
107
|
+
<Slider defaultValue={50} />
|
|
108
|
+
</Field>
|
|
109
|
+
<Field label="Sensitivity">
|
|
110
|
+
<SliderWithNumberField
|
|
111
|
+
min={0}
|
|
112
|
+
max={10}
|
|
113
|
+
step={0.1}
|
|
114
|
+
defaultValue={5}
|
|
115
|
+
/>
|
|
116
|
+
</Field>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div className="space-y-4">
|
|
121
|
+
<Heading
|
|
122
|
+
level={4}
|
|
123
|
+
className="text-sm font-medium text-black uppercase tracking-wider"
|
|
124
|
+
>
|
|
125
|
+
Layout Variations
|
|
126
|
+
</Heading>
|
|
127
|
+
<div className="grid gap-6">
|
|
128
|
+
<Field
|
|
129
|
+
label="Label on Left"
|
|
130
|
+
labelPlacement="left"
|
|
131
|
+
description="Label to the left"
|
|
132
|
+
>
|
|
133
|
+
<Input placeholder="Enter text..." />
|
|
134
|
+
</Field>
|
|
135
|
+
<Field
|
|
136
|
+
label="Label on Right"
|
|
137
|
+
labelPlacement="right"
|
|
138
|
+
description="Label to the right"
|
|
139
|
+
>
|
|
140
|
+
<Input placeholder="Enter text..." />
|
|
141
|
+
</Field>
|
|
142
|
+
<Field label="With Search Icon" icon={<Icon name="search" />}>
|
|
143
|
+
<Input placeholder="Search..." />
|
|
144
|
+
</Field>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="space-y-4">
|
|
149
|
+
<Heading
|
|
150
|
+
level={4}
|
|
151
|
+
className="text-sm font-medium text-black uppercase tracking-wider"
|
|
152
|
+
>
|
|
153
|
+
Field Groups
|
|
154
|
+
</Heading>
|
|
155
|
+
<div className="border rounded-lg p-6 bg-gray-50/50">
|
|
156
|
+
<Heading level={5} className="font-bold mb-4">
|
|
157
|
+
Settings Group
|
|
158
|
+
</Heading>
|
|
159
|
+
<div className="flex flex-col gap-4">
|
|
160
|
+
<Field
|
|
161
|
+
label="Break up long sentences"
|
|
162
|
+
labelPlacement="right"
|
|
163
|
+
labelWeight="normal"
|
|
164
|
+
description="Ensures buttons don't have too much text"
|
|
165
|
+
>
|
|
166
|
+
<Checkbox id="group-opt-1" />
|
|
167
|
+
</Field>
|
|
168
|
+
<Field
|
|
169
|
+
label="Detect emotions"
|
|
170
|
+
labelPlacement="right"
|
|
171
|
+
labelWeight="normal"
|
|
172
|
+
error="Requires active connection"
|
|
173
|
+
>
|
|
174
|
+
<Checkbox id="group-opt-2" />
|
|
175
|
+
</Field>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
),
|
|
181
|
+
};
|
package/src/ui/Field.tsx
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Field as BaseField } from "@base-ui/react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
import { Label } from "./Label";
|
|
5
|
+
|
|
6
|
+
export interface FieldProps extends BaseField.Root.Props {
|
|
7
|
+
label?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
tooltip?: string;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
icon?: React.ReactNode;
|
|
13
|
+
labelPlacement?: "top" | "left" | "right";
|
|
14
|
+
labelWeight?: "bold" | "normal";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A wrapper component that provides a label, description, and error message for form controls.
|
|
19
|
+
* Uses Base UI's Field primitives for accessibility.
|
|
20
|
+
*/
|
|
21
|
+
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
|
|
22
|
+
(
|
|
23
|
+
{
|
|
24
|
+
label,
|
|
25
|
+
description,
|
|
26
|
+
error,
|
|
27
|
+
tooltip,
|
|
28
|
+
children,
|
|
29
|
+
className,
|
|
30
|
+
icon,
|
|
31
|
+
labelPlacement = "top",
|
|
32
|
+
labelWeight = "bold",
|
|
33
|
+
...props
|
|
34
|
+
},
|
|
35
|
+
ref,
|
|
36
|
+
) => {
|
|
37
|
+
const isHorizontal =
|
|
38
|
+
labelPlacement === "left" || labelPlacement === "right";
|
|
39
|
+
const labelOnRight = labelPlacement === "right";
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<BaseField.Root
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={cn(
|
|
45
|
+
"flex w-full",
|
|
46
|
+
isHorizontal ? "flex-col gap-1" : "flex-col gap-1.5",
|
|
47
|
+
className as string,
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
<div
|
|
52
|
+
className={cn(
|
|
53
|
+
"flex",
|
|
54
|
+
isHorizontal ? "flex-row items-center gap-2" : "flex-col gap-1.5",
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
{labelOnRight && <div className="relative">{children}</div>}
|
|
58
|
+
|
|
59
|
+
{label && (
|
|
60
|
+
<BaseField.Label
|
|
61
|
+
render={(props) => (
|
|
62
|
+
<Label
|
|
63
|
+
tooltip={tooltip}
|
|
64
|
+
className={cn(
|
|
65
|
+
"mb-0",
|
|
66
|
+
labelWeight === "normal" && "font-normal",
|
|
67
|
+
)}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
<span className="flex items-center gap-1">
|
|
71
|
+
{icon && <span className="flex-shrink-0">{icon}</span>}
|
|
72
|
+
{label}
|
|
73
|
+
</span>
|
|
74
|
+
</Label>
|
|
75
|
+
)}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{!labelOnRight && <div className="relative">{children}</div>}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{description && (
|
|
83
|
+
<BaseField.Description
|
|
84
|
+
className={cn(
|
|
85
|
+
"text-xs text-gray-500",
|
|
86
|
+
labelOnRight && "ml-7", // Aligns description with label text when child is on left
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{description}
|
|
90
|
+
</BaseField.Description>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{error && (
|
|
94
|
+
<BaseField.Error
|
|
95
|
+
className={cn(
|
|
96
|
+
"text-xs text-red-600 font-medium",
|
|
97
|
+
labelOnRight && "ml-7",
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
{error}
|
|
101
|
+
</BaseField.Error>
|
|
102
|
+
)}
|
|
103
|
+
</BaseField.Root>
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
Field.displayName = "Field";
|