@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.
Files changed (81) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/publish.yml +42 -0
  4. package/.github/workflows/storybook.yml +46 -0
  5. package/.storybook/main.ts +28 -0
  6. package/.storybook/preview.ts +22 -0
  7. package/README.md +9 -0
  8. package/dist/index.css +1 -0
  9. package/dist/index.d.mts +704 -0
  10. package/dist/index.d.ts +704 -0
  11. package/dist/index.js +5 -0
  12. package/dist/index.mjs +5 -0
  13. package/package.json +60 -0
  14. package/src/charts/DistributionBarChart.stories.tsx +41 -0
  15. package/src/charts/DistributionBarChart.tsx +170 -0
  16. package/src/charts/DistributionHistogram.stories.tsx +56 -0
  17. package/src/charts/DistributionHistogram.tsx +193 -0
  18. package/src/charts/index.ts +10 -0
  19. package/src/global.d.ts +1 -0
  20. package/src/helpers/SEO.tsx +41 -0
  21. package/src/index.ts +6 -0
  22. package/src/styles/theme.css +60 -0
  23. package/src/ui/Alert.stories.tsx +41 -0
  24. package/src/ui/Alert.tsx +72 -0
  25. package/src/ui/Anchor.stories.tsx +25 -0
  26. package/src/ui/Anchor.tsx +32 -0
  27. package/src/ui/AuthFormUI.stories.tsx +67 -0
  28. package/src/ui/AuthFormUI.tsx +217 -0
  29. package/src/ui/BasePanel.stories.tsx +36 -0
  30. package/src/ui/BasePanel.tsx +59 -0
  31. package/src/ui/Button.stories.tsx +108 -0
  32. package/src/ui/Button.tsx +121 -0
  33. package/src/ui/Checkbox.stories.tsx +61 -0
  34. package/src/ui/Checkbox.tsx +45 -0
  35. package/src/ui/Collapsible.stories.tsx +91 -0
  36. package/src/ui/Collapsible.tsx +52 -0
  37. package/src/ui/Colors.stories.tsx +67 -0
  38. package/src/ui/Dialog.stories.tsx +29 -0
  39. package/src/ui/Dialog.tsx +56 -0
  40. package/src/ui/Dropdown.tsx +66 -0
  41. package/src/ui/Field.stories.tsx +181 -0
  42. package/src/ui/Field.tsx +108 -0
  43. package/src/ui/Icon.stories.tsx +192 -0
  44. package/src/ui/Icon.tsx +42 -0
  45. package/src/ui/IconRegistry.tsx +189 -0
  46. package/src/ui/Input.stories.tsx +67 -0
  47. package/src/ui/Input.tsx +43 -0
  48. package/src/ui/Label.stories.tsx +42 -0
  49. package/src/ui/Label.tsx +26 -0
  50. package/src/ui/NumberField.stories.tsx +86 -0
  51. package/src/ui/NumberField.tsx +116 -0
  52. package/src/ui/Popover.tsx +42 -0
  53. package/src/ui/Select.stories.tsx +74 -0
  54. package/src/ui/Select.tsx +122 -0
  55. package/src/ui/Separator.stories.tsx +61 -0
  56. package/src/ui/Separator.tsx +28 -0
  57. package/src/ui/SettingsPanel.stories.tsx +83 -0
  58. package/src/ui/SettingsPanel.tsx +81 -0
  59. package/src/ui/Skeleton.stories.tsx +43 -0
  60. package/src/ui/Skeleton.tsx +15 -0
  61. package/src/ui/Slider.stories.tsx +140 -0
  62. package/src/ui/Slider.tsx +95 -0
  63. package/src/ui/SliderWithNumberField.stories.tsx +101 -0
  64. package/src/ui/SliderWithNumberField.tsx +88 -0
  65. package/src/ui/Switch.stories.tsx +81 -0
  66. package/src/ui/Switch.tsx +60 -0
  67. package/src/ui/TabRadio.stories.tsx +153 -0
  68. package/src/ui/TabRadio.tsx +68 -0
  69. package/src/ui/TabSelection.stories.tsx +44 -0
  70. package/src/ui/TabSelection.tsx +91 -0
  71. package/src/ui/TextArea.stories.tsx +64 -0
  72. package/src/ui/TextArea.tsx +24 -0
  73. package/src/ui/Tooltip.stories.tsx +84 -0
  74. package/src/ui/Tooltip.tsx +61 -0
  75. package/src/ui/Typography.stories.tsx +87 -0
  76. package/src/ui/Typography.tsx +80 -0
  77. package/src/ui/index.ts +28 -0
  78. package/src/ui/utils.ts +6 -0
  79. package/tsconfig.json +12 -0
  80. package/vitest.config.ts +36 -0
  81. 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
+ };
@@ -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";