@peerbots/core 0.2.6 → 1.0.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/config.json +1 -1
- package/.github/workflows/storybook.yml +9 -1
- package/CHANGELOG.md +94 -0
- package/dist/index.d.mts +643 -131
- package/dist/index.d.ts +643 -131
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
- package/src/charts/DistributionBarChart.tsx +10 -4
- package/src/charts/DistributionHistogram.tsx +13 -5
- package/src/charts/LineGraph.stories.tsx +60 -0
- package/src/charts/LineGraph.tsx +216 -0
- package/src/charts/index.ts +2 -0
- package/src/docs/DesignSystem.mdx +241 -25
- package/src/styles/theme.css +8 -0
- package/src/ui/feedback/Alert.stories.tsx +56 -0
- package/src/ui/feedback/Alert.tsx +168 -0
- package/src/ui/{Dialog.stories.tsx → feedback/Dialog.stories.tsx} +2 -2
- package/src/ui/{Dialog.tsx → feedback/Dialog.tsx} +2 -2
- package/src/ui/feedback/EmptyState.stories.tsx +57 -0
- package/src/ui/feedback/EmptyState.tsx +123 -0
- package/src/ui/feedback/Popover.stories.tsx +130 -0
- package/src/ui/feedback/Popover.tsx +135 -0
- package/src/ui/feedback/Skeleton.stories.tsx +88 -0
- package/src/ui/{Skeleton.tsx → feedback/Skeleton.tsx} +2 -2
- package/src/ui/feedback/Spinner.stories.tsx +77 -0
- package/src/ui/feedback/Spinner.tsx +62 -0
- package/src/ui/{Tooltip.stories.tsx → feedback/Tooltip.stories.tsx} +3 -3
- package/src/ui/{Tooltip.tsx → feedback/Tooltip.tsx} +2 -2
- package/src/ui/feedback/index.ts +7 -0
- package/src/ui/forms/Button.stories.tsx +181 -0
- package/src/ui/forms/Button.tsx +323 -0
- package/src/ui/{Checkbox.stories.tsx → forms/Checkbox.stories.tsx} +2 -2
- package/src/ui/{Checkbox.tsx → forms/Checkbox.tsx} +2 -2
- package/src/ui/forms/ChipGroup.stories.tsx +118 -0
- package/src/ui/forms/ChipGroup.tsx +239 -0
- package/src/ui/{Dropdown.tsx → forms/Dropdown.tsx} +1 -1
- package/src/ui/{Field.stories.tsx → forms/Field.stories.tsx} +3 -3
- package/src/ui/{Field.tsx → forms/Field.tsx} +1 -1
- package/src/ui/{Input.stories.tsx → forms/Input.stories.tsx} +6 -6
- package/src/ui/{Input.tsx → forms/Input.tsx} +1 -1
- package/src/ui/{Label.stories.tsx → forms/Label.stories.tsx} +2 -2
- package/src/ui/{Label.tsx → forms/Label.tsx} +2 -2
- package/src/ui/{NumberField.stories.tsx → forms/NumberField.stories.tsx} +2 -2
- package/src/ui/{NumberField.tsx → forms/NumberField.tsx} +8 -9
- package/src/ui/forms/PeerbotsColorSelector.stories.tsx +49 -0
- package/src/ui/forms/PeerbotsColorSelector.tsx +100 -0
- package/src/ui/{Select.stories.tsx → forms/Select.stories.tsx} +2 -2
- package/src/ui/{Select.tsx → forms/Select.tsx} +5 -3
- package/src/ui/{Slider.stories.tsx → forms/Slider.stories.tsx} +6 -4
- package/src/ui/{Slider.tsx → forms/Slider.tsx} +7 -3
- package/src/ui/forms/SliderWithNumberField.stories.tsx +136 -0
- package/src/ui/{SliderWithNumberField.tsx → forms/SliderWithNumberField.tsx} +19 -7
- package/src/ui/{Switch.stories.tsx → forms/Switch.stories.tsx} +2 -2
- package/src/ui/{Switch.tsx → forms/Switch.tsx} +1 -1
- package/src/ui/{TabRadio.stories.tsx → forms/TabRadio.stories.tsx} +3 -3
- package/src/ui/{TabRadio.tsx → forms/TabRadio.tsx} +1 -1
- package/src/ui/{TextArea.stories.tsx → forms/TextArea.stories.tsx} +5 -3
- package/src/ui/{TextArea.tsx → forms/TextArea.tsx} +1 -1
- package/src/ui/forms/index.ts +15 -0
- package/src/ui/foundations/Anchor.stories.tsx +39 -0
- package/src/ui/foundations/Anchor.tsx +96 -0
- package/src/ui/foundations/CodeBlock.stories.tsx +86 -0
- package/src/ui/foundations/CodeBlock.tsx +148 -0
- package/src/ui/foundations/Colors.stories.tsx +80 -0
- package/src/ui/{Icon.stories.tsx → foundations/Icon.stories.tsx} +1 -1
- package/src/ui/{Icon.tsx → foundations/Icon.tsx} +1 -1
- package/src/ui/{IconRegistry.tsx → foundations/IconRegistry.tsx} +261 -57
- package/src/ui/foundations/SocialLinks.stories.tsx +38 -0
- package/src/ui/foundations/SocialLinks.tsx +117 -0
- package/src/ui/foundations/Typography.stories.tsx +58 -0
- package/src/ui/foundations/Typography.tsx +163 -0
- package/src/ui/foundations/index.ts +6 -0
- package/src/ui/index.ts +7 -27
- package/src/ui/layout/Accordion.stories.tsx +86 -0
- package/src/ui/layout/Accordion.tsx +205 -0
- package/src/ui/layout/Card.stories.tsx +151 -0
- package/src/ui/layout/Card.tsx +124 -0
- package/src/ui/layout/Collapsible.stories.tsx +54 -0
- package/src/ui/layout/Collapsible.tsx +54 -0
- package/src/ui/{Separator.stories.tsx → layout/Separator.stories.tsx} +2 -2
- package/src/ui/{Separator.tsx → layout/Separator.tsx} +1 -1
- package/src/ui/{SettingsPanel.stories.tsx → layout/SettingsPanel.stories.tsx} +5 -5
- package/src/ui/{SettingsPanel.tsx → layout/SettingsPanel.tsx} +33 -29
- package/src/ui/layout/index.ts +5 -0
- package/src/ui/media/Avatar.stories.tsx +89 -0
- package/src/ui/media/Avatar.tsx +122 -0
- package/src/ui/media/Dropzone.stories.tsx +95 -0
- package/src/ui/media/Dropzone.tsx +328 -0
- package/src/ui/media/FileUpload.stories.tsx +62 -0
- package/src/ui/media/FileUpload.tsx +175 -0
- package/src/ui/media/index.ts +3 -0
- package/src/ui/{AuthFormUI.stories.tsx → patterns/AuthFormUI.stories.tsx} +1 -1
- package/src/ui/{AuthFormUI.tsx → patterns/AuthFormUI.tsx} +24 -23
- package/src/ui/patterns/CopyButton.stories.tsx +77 -0
- package/src/ui/patterns/CopyButton.tsx +93 -0
- package/src/ui/patterns/SearchInput.stories.tsx +51 -0
- package/src/ui/patterns/SearchInput.tsx +93 -0
- package/src/ui/patterns/Stepper.stories.tsx +129 -0
- package/src/ui/patterns/Stepper.tsx +295 -0
- package/src/ui/{TabSelection.stories.tsx → patterns/TabSelection.stories.tsx} +1 -1
- package/src/ui/{TabSelection.tsx → patterns/TabSelection.tsx} +2 -2
- package/src/ui/patterns/index.ts +6 -0
- package/src/ui/patterns/useClipboard.ts +75 -0
- package/src/ui/types.ts +48 -0
- package/src/ui/Alert.stories.tsx +0 -41
- package/src/ui/Alert.tsx +0 -72
- package/src/ui/Anchor.stories.tsx +0 -25
- package/src/ui/Anchor.tsx +0 -32
- package/src/ui/Button.stories.tsx +0 -108
- package/src/ui/Button.tsx +0 -121
- package/src/ui/Collapsible.stories.tsx +0 -91
- package/src/ui/Collapsible.tsx +0 -52
- package/src/ui/Colors.stories.tsx +0 -67
- package/src/ui/Popover.tsx +0 -42
- package/src/ui/Skeleton.stories.tsx +0 -43
- package/src/ui/SliderWithNumberField.stories.tsx +0 -101
- package/src/ui/Typography.stories.tsx +0 -87
- package/src/ui/Typography.tsx +0 -80
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { cn } from "../utils";
|
|
3
|
+
import { ComponentColor, ComponentRadius, ComponentSize, FillVariant } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface ChipOption<T = string> {
|
|
6
|
+
value: T;
|
|
7
|
+
label: React.ReactNode;
|
|
8
|
+
badge?: React.ReactNode;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ChipGroupProps<T = string> {
|
|
14
|
+
/** Array of selectable chip options */
|
|
15
|
+
options: ChipOption<T>[];
|
|
16
|
+
/** Controlled single selected value */
|
|
17
|
+
value?: T;
|
|
18
|
+
/** Uncontrolled initial single selected value */
|
|
19
|
+
defaultValue?: T;
|
|
20
|
+
/** Callback fired when single selection changes */
|
|
21
|
+
onChange?: (value: T) => void;
|
|
22
|
+
/** Enable multiple selection mode */
|
|
23
|
+
multiple?: boolean;
|
|
24
|
+
/** Controlled array of selected values (for multi mode) */
|
|
25
|
+
values?: T[];
|
|
26
|
+
/** Uncontrolled initial array of selected values (for multi mode) */
|
|
27
|
+
defaultValues?: T[];
|
|
28
|
+
/** Callback fired when multiple selection changes */
|
|
29
|
+
onMultipleChange?: (values: T[]) => void;
|
|
30
|
+
/** Color theme */
|
|
31
|
+
color?: ComponentColor;
|
|
32
|
+
/** Visual fill style */
|
|
33
|
+
variant?: "solid" | "soft" | "outline";
|
|
34
|
+
/** Size scale */
|
|
35
|
+
size?: ComponentSize;
|
|
36
|
+
/** Border radius scale */
|
|
37
|
+
radius?: ComponentRadius;
|
|
38
|
+
/** Enable horizontal scrollable rail */
|
|
39
|
+
scrollable?: boolean;
|
|
40
|
+
className?: string;
|
|
41
|
+
id?: string;
|
|
42
|
+
name?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const activeColorStyles: Record<string, Record<string, string>> = {
|
|
46
|
+
solid: {
|
|
47
|
+
primary:
|
|
48
|
+
"pb:bg-primary pb:text-gray-900 pb:border-primary pb:shadow-sm pb:font-bold",
|
|
49
|
+
teal: "pb:bg-peerbots-teal pb:text-gray-900 pb:border-peerbots-teal pb:shadow-sm pb:font-bold",
|
|
50
|
+
pink: "pb:bg-rose-700 pb:text-white pb:border-rose-700 pb:shadow-sm pb:font-bold",
|
|
51
|
+
darkblue:
|
|
52
|
+
"pb:bg-peerbots-darkblue pb:text-white pb:border-peerbots-darkblue pb:shadow-sm pb:font-bold",
|
|
53
|
+
neutral:
|
|
54
|
+
"pb:bg-gray-800 pb:text-white pb:border-gray-800 pb:shadow-sm pb:font-bold",
|
|
55
|
+
danger: "pb:bg-red-600 pb:text-white pb:border-red-600 pb:shadow-sm pb:font-bold",
|
|
56
|
+
},
|
|
57
|
+
soft: {
|
|
58
|
+
primary:
|
|
59
|
+
"pb:bg-primary/20 pb:text-teal-950 pb:border-primary/40 pb:font-bold",
|
|
60
|
+
teal: "pb:bg-peerbots-teal/20 pb:text-teal-950 pb:border-peerbots-teal/40 pb:font-bold",
|
|
61
|
+
pink: "pb:bg-peerbots-pink/20 pb:text-rose-950 pb:border-peerbots-pink/40 pb:font-bold",
|
|
62
|
+
darkblue:
|
|
63
|
+
"pb:bg-peerbots-darkblue/20 pb:text-peerbots-darkblue pb:border-peerbots-darkblue/40 pb:font-bold",
|
|
64
|
+
neutral:
|
|
65
|
+
"pb:bg-gray-200 pb:text-gray-900 pb:border-gray-300 pb:font-bold",
|
|
66
|
+
danger: "pb:bg-red-100 pb:text-red-900 pb:border-red-300 pb:font-bold",
|
|
67
|
+
},
|
|
68
|
+
outline: {
|
|
69
|
+
primary:
|
|
70
|
+
"pb:bg-primary/10 pb:text-teal-950 pb:border-teal-700 pb:font-bold pb:border-2",
|
|
71
|
+
teal: "pb:bg-peerbots-teal/10 pb:text-peerbots-darkteal pb:border-peerbots-darkteal pb:font-bold pb:border-2",
|
|
72
|
+
pink: "pb:bg-peerbots-pink/10 pb:text-rose-800 pb:border-rose-700 pb:font-bold pb:border-2",
|
|
73
|
+
darkblue:
|
|
74
|
+
"pb:bg-peerbots-darkblue/10 pb:text-peerbots-darkblue pb:border-peerbots-darkblue pb:font-bold pb:border-2",
|
|
75
|
+
neutral:
|
|
76
|
+
"pb:bg-gray-100 pb:text-gray-900 pb:border-gray-400 pb:font-bold pb:border-2",
|
|
77
|
+
danger: "pb:bg-red-50 pb:text-red-900 pb:border-red-500 pb:font-bold pb:border-2",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const inactiveStyles =
|
|
82
|
+
"pb:bg-white pb:text-gray-700 pb:border-gray-300 pb:hover:border-gray-400 pb:hover:bg-gray-50 pb:hover:text-gray-950";
|
|
83
|
+
|
|
84
|
+
const radiusClasses: Record<ComponentRadius, string> = {
|
|
85
|
+
none: "pb:rounded-none",
|
|
86
|
+
sm: "pb:rounded-md",
|
|
87
|
+
md: "pb:rounded-xl",
|
|
88
|
+
lg: "pb:rounded-2xl",
|
|
89
|
+
"2xl": "pb:rounded-2xl",
|
|
90
|
+
pill: "pb:rounded-full",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const sizeClasses: Record<ComponentSize, { chip: string; text: string; badge: string }> = {
|
|
94
|
+
xs: {
|
|
95
|
+
chip: "pb:px-2 pb:py-0.5 pb:gap-1",
|
|
96
|
+
text: "pb:text-[10px]",
|
|
97
|
+
badge: "pb:text-[8px] pb:px-1 pb:py-0.5",
|
|
98
|
+
},
|
|
99
|
+
sm: {
|
|
100
|
+
chip: "pb:px-3 pb:py-1 pb:gap-1.5",
|
|
101
|
+
text: "pb:text-xs",
|
|
102
|
+
badge: "pb:text-[9px] pb:px-1.5 pb:py-0.5",
|
|
103
|
+
},
|
|
104
|
+
md: {
|
|
105
|
+
chip: "pb:px-3.5 pb:py-1.5 pb:gap-2",
|
|
106
|
+
text: "pb:text-sm",
|
|
107
|
+
badge: "pb:text-[10px] pb:px-1.5 pb:py-0.5",
|
|
108
|
+
},
|
|
109
|
+
lg: {
|
|
110
|
+
chip: "pb:px-4.5 pb:py-2 pb:gap-2.5",
|
|
111
|
+
text: "pb:text-base",
|
|
112
|
+
badge: "pb:text-xs pb:px-2 pb:py-0.5",
|
|
113
|
+
},
|
|
114
|
+
xl: {
|
|
115
|
+
chip: "pb:px-6 pb:py-2.5 pb:gap-3",
|
|
116
|
+
text: "pb:text-lg",
|
|
117
|
+
badge: "pb:text-xs pb:px-2.5 pb:py-1",
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export function ChipGroup<T = string>({
|
|
122
|
+
options,
|
|
123
|
+
value: controlledValue,
|
|
124
|
+
defaultValue,
|
|
125
|
+
onChange,
|
|
126
|
+
multiple = false,
|
|
127
|
+
values: controlledValues,
|
|
128
|
+
defaultValues,
|
|
129
|
+
onMultipleChange,
|
|
130
|
+
color = "teal",
|
|
131
|
+
variant = "soft",
|
|
132
|
+
size = "md",
|
|
133
|
+
radius = "pill",
|
|
134
|
+
scrollable = false,
|
|
135
|
+
className,
|
|
136
|
+
id,
|
|
137
|
+
name,
|
|
138
|
+
}: ChipGroupProps<T>) {
|
|
139
|
+
const [internalValue, setInternalValue] = useState<T | undefined>(
|
|
140
|
+
defaultValue !== undefined ? defaultValue : options[0]?.value,
|
|
141
|
+
);
|
|
142
|
+
const [internalValues, setInternalValues] = useState<T[]>(
|
|
143
|
+
defaultValues || [],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const activeValue = controlledValue !== undefined ? controlledValue : internalValue;
|
|
147
|
+
const activeValues = controlledValues !== undefined ? controlledValues : internalValues;
|
|
148
|
+
|
|
149
|
+
const handleSelect = (optionValue: T, disabled?: boolean) => {
|
|
150
|
+
if (disabled) return;
|
|
151
|
+
|
|
152
|
+
if (multiple) {
|
|
153
|
+
const exists = activeValues.includes(optionValue);
|
|
154
|
+
const nextValues = exists
|
|
155
|
+
? activeValues.filter((v) => v !== optionValue)
|
|
156
|
+
: [...activeValues, optionValue];
|
|
157
|
+
|
|
158
|
+
if (controlledValues === undefined) {
|
|
159
|
+
setInternalValues(nextValues);
|
|
160
|
+
}
|
|
161
|
+
onMultipleChange?.(nextValues);
|
|
162
|
+
} else {
|
|
163
|
+
if (controlledValue === undefined) {
|
|
164
|
+
setInternalValue(optionValue);
|
|
165
|
+
}
|
|
166
|
+
onChange?.(optionValue);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const activeStyle =
|
|
171
|
+
activeColorStyles[variant]?.[color] ||
|
|
172
|
+
activeColorStyles[variant]?.teal ||
|
|
173
|
+
activeColorStyles.soft.teal;
|
|
174
|
+
|
|
175
|
+
const currentSize = sizeClasses[size] || sizeClasses.md;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
id={id}
|
|
180
|
+
role={multiple ? "group" : "radiogroup"}
|
|
181
|
+
aria-label={name}
|
|
182
|
+
className={cn(
|
|
183
|
+
"pb:flex pb:items-center pb:gap-2 pb:box-border",
|
|
184
|
+
scrollable
|
|
185
|
+
? "pb:overflow-x-auto pb:no-scrollbar pb:py-1 pb:px-0.5"
|
|
186
|
+
: "pb:flex-wrap",
|
|
187
|
+
className,
|
|
188
|
+
)}
|
|
189
|
+
>
|
|
190
|
+
{options.map((option, idx) => {
|
|
191
|
+
const isSelected = multiple
|
|
192
|
+
? activeValues.includes(option.value)
|
|
193
|
+
: activeValue === option.value;
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<button
|
|
197
|
+
key={idx}
|
|
198
|
+
type="button"
|
|
199
|
+
role={multiple ? "checkbox" : "radio"}
|
|
200
|
+
aria-checked={isSelected}
|
|
201
|
+
disabled={option.disabled}
|
|
202
|
+
onClick={() => handleSelect(option.value, option.disabled)}
|
|
203
|
+
className={cn(
|
|
204
|
+
"pb:inline-flex pb:items-center pb:border pb:font-medium pb:transition-all pb:duration-150 pb:cursor-pointer pb:select-none pb:shrink-0 pb:focus:outline-none pb:focus-visible:ring-2 pb:focus-visible:ring-peerbots-teal pb:focus-visible:ring-offset-1",
|
|
205
|
+
radiusClasses[radius] || radiusClasses.pill,
|
|
206
|
+
currentSize.chip,
|
|
207
|
+
currentSize.text,
|
|
208
|
+
isSelected ? activeStyle : inactiveStyles,
|
|
209
|
+
option.disabled &&
|
|
210
|
+
"pb:opacity-40 pb:cursor-not-allowed pb:bg-gray-100 pb:text-gray-400 pb:border-gray-200",
|
|
211
|
+
)}
|
|
212
|
+
>
|
|
213
|
+
{option.icon && (
|
|
214
|
+
<span className="pb:shrink-0 pb:flex pb:items-center">
|
|
215
|
+
{option.icon}
|
|
216
|
+
</span>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
<span>{option.label}</span>
|
|
220
|
+
|
|
221
|
+
{option.badge && (
|
|
222
|
+
<span
|
|
223
|
+
className={cn(
|
|
224
|
+
"pb:rounded-full pb:font-bold pb:leading-none pb:tracking-tight",
|
|
225
|
+
currentSize.badge,
|
|
226
|
+
isSelected
|
|
227
|
+
? "pb:bg-black/15 pb:text-current"
|
|
228
|
+
: "pb:bg-gray-200 pb:text-gray-800",
|
|
229
|
+
)}
|
|
230
|
+
>
|
|
231
|
+
{option.badge}
|
|
232
|
+
</span>
|
|
233
|
+
)}
|
|
234
|
+
</button>
|
|
235
|
+
);
|
|
236
|
+
})}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { Field } from "./Field";
|
|
3
|
-
import { Icon } from "
|
|
4
|
-
import { Heading } from "
|
|
3
|
+
import { Icon } from "../foundations/Icon";
|
|
4
|
+
import { Heading } from "../foundations/Typography";
|
|
5
5
|
import { Input } from "./Input";
|
|
6
6
|
import { Slider } from "./Slider";
|
|
7
7
|
import { Select } from "./Select";
|
|
@@ -11,7 +11,7 @@ import { SliderWithNumberField } from "./SliderWithNumberField";
|
|
|
11
11
|
import { Checkbox } from "./Checkbox";
|
|
12
12
|
|
|
13
13
|
const meta: Meta<typeof Field> = {
|
|
14
|
-
title: "
|
|
14
|
+
title: "Forms/Field",
|
|
15
15
|
component: Field,
|
|
16
16
|
tags: ["autodocs"],
|
|
17
17
|
argTypes: {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { Input } from "./Input";
|
|
3
|
-
import { Heading, Text } from "
|
|
3
|
+
import { Heading, Text } from "../foundations/Typography";
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof Input> = {
|
|
6
|
-
title: "
|
|
6
|
+
title: "Forms/Input",
|
|
7
7
|
component: Input,
|
|
8
8
|
tags: ["autodocs"],
|
|
9
9
|
argTypes: {
|
|
@@ -32,19 +32,19 @@ export const Variations: Story = {
|
|
|
32
32
|
<Text variant="small" className="pb:font-bold">
|
|
33
33
|
With Placeholder
|
|
34
34
|
</Text>
|
|
35
|
-
<Input placeholder="Type here..." />
|
|
35
|
+
<Input aria-label="With placeholder" placeholder="Type here..." />
|
|
36
36
|
</div>
|
|
37
37
|
<div className="pb:space-y-1">
|
|
38
38
|
<Text variant="small" className="pb:font-bold">
|
|
39
39
|
With Value
|
|
40
40
|
</Text>
|
|
41
|
-
<Input defaultValue="Initial value" />
|
|
41
|
+
<Input aria-label="With value" defaultValue="Initial value" />
|
|
42
42
|
</div>
|
|
43
43
|
<div className="pb:space-y-1">
|
|
44
44
|
<Text variant="small" className="pb:font-bold">
|
|
45
45
|
Password Type
|
|
46
46
|
</Text>
|
|
47
|
-
<Input type="password" placeholder="Password..." />
|
|
47
|
+
<Input aria-label="Password" type="password" placeholder="Password..." />
|
|
48
48
|
</div>
|
|
49
49
|
</div>
|
|
50
50
|
</div>
|
|
@@ -58,7 +58,7 @@ export const Variations: Story = {
|
|
|
58
58
|
<Text variant="small" className="pb:font-bold">
|
|
59
59
|
Disabled
|
|
60
60
|
</Text>
|
|
61
|
-
<Input disabled defaultValue="Can't edit me" />
|
|
61
|
+
<Input aria-label="Disabled input" disabled defaultValue="Can't edit me" />
|
|
62
62
|
</div>
|
|
63
63
|
</div>
|
|
64
64
|
</div>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { Label } from "./Label";
|
|
3
3
|
import { Input } from "./Input";
|
|
4
|
-
import { Text } from "
|
|
4
|
+
import { Text } from "../foundations/Typography";
|
|
5
5
|
|
|
6
6
|
const meta: Meta<typeof Label> = {
|
|
7
|
-
title: "
|
|
7
|
+
title: "Forms/Label",
|
|
8
8
|
component: Label,
|
|
9
9
|
tags: ["autodocs"],
|
|
10
10
|
args: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { cn } from "
|
|
3
|
-
import { HelperTooltip } from "
|
|
2
|
+
import { cn } from "../utils";
|
|
3
|
+
import { HelperTooltip } from "../feedback/Tooltip";
|
|
4
4
|
|
|
5
5
|
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
6
6
|
children: React.ReactNode;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { NumberField } from "./NumberField";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import { Heading, Text } from "
|
|
4
|
+
import { Heading, Text } from "../foundations/Typography";
|
|
5
5
|
|
|
6
6
|
const meta: Meta<typeof NumberField> = {
|
|
7
|
-
title: "
|
|
7
|
+
title: "Forms/NumberField",
|
|
8
8
|
component: NumberField,
|
|
9
9
|
tags: ["autodocs"],
|
|
10
10
|
argTypes: {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { NumberField as BaseNumberField } from "@base-ui/react";
|
|
3
|
-
import { cn } from "
|
|
4
|
-
import { Icon } from "
|
|
3
|
+
import { cn } from "../utils";
|
|
4
|
+
import { Icon } from "../foundations/Icon";
|
|
5
5
|
|
|
6
|
-
export interface NumberFieldProps {
|
|
6
|
+
export interface NumberFieldProps extends React.AriaAttributes {
|
|
7
7
|
value?: number | null;
|
|
8
8
|
defaultValue?: number;
|
|
9
9
|
min?: number;
|
|
@@ -33,17 +33,16 @@ export const NumberField = React.forwardRef<HTMLDivElement, NumberFieldProps>(
|
|
|
33
33
|
id: propsId,
|
|
34
34
|
placeholder,
|
|
35
35
|
showButtons = false,
|
|
36
|
+
"aria-label": ariaLabel,
|
|
36
37
|
...props
|
|
37
38
|
},
|
|
38
39
|
ref,
|
|
39
40
|
) => {
|
|
40
|
-
const generatedId = React.useId();
|
|
41
|
-
const numberId = propsId || generatedId;
|
|
42
|
-
|
|
43
41
|
return (
|
|
44
42
|
<div className="pb:w-full">
|
|
45
43
|
<BaseNumberField.Root
|
|
46
44
|
ref={ref}
|
|
45
|
+
id={propsId}
|
|
47
46
|
value={value}
|
|
48
47
|
defaultValue={defaultValue}
|
|
49
48
|
min={min}
|
|
@@ -60,7 +59,7 @@ export const NumberField = React.forwardRef<HTMLDivElement, NumberFieldProps>(
|
|
|
60
59
|
className={cn("pb:flex pb:flex-col pb:gap-1", className)}
|
|
61
60
|
{...props}
|
|
62
61
|
>
|
|
63
|
-
<
|
|
62
|
+
<BaseNumberField.Group
|
|
64
63
|
className={cn(
|
|
65
64
|
"pb:group pb:relative pb:flex pb:items-center pb:rounded-md pb:border-0 pb:bg-primary/10 pb:ring-1 pb:ring-inset pb:ring-gray-300 pb:focus-within:ring-2 pb:focus-within:ring-primary pb:shadow-sm",
|
|
66
65
|
disabled && "pb:opacity-50 pb:cursor-not-allowed",
|
|
@@ -82,8 +81,8 @@ export const NumberField = React.forwardRef<HTMLDivElement, NumberFieldProps>(
|
|
|
82
81
|
)}
|
|
83
82
|
|
|
84
83
|
<BaseNumberField.Input
|
|
85
|
-
id={numberId}
|
|
86
84
|
placeholder={placeholder}
|
|
85
|
+
aria-label={ariaLabel || (typeof placeholder === "string" ? placeholder : "Number input")}
|
|
87
86
|
className={cn(
|
|
88
87
|
"pb:block pb:w-full pb:bg-transparent pb:border-0 pb:px-3 pb:py-1.5 pb:text-gray-900 pb:placeholder:text-gray-400 pb:focus:ring-0 pb:sm:text-sm pb:sm:leading-6 pb:text-center pb:appearance-none pb:relative pb:z-0",
|
|
89
88
|
!showButtons && "pb:rounded-md",
|
|
@@ -106,7 +105,7 @@ export const NumberField = React.forwardRef<HTMLDivElement, NumberFieldProps>(
|
|
|
106
105
|
)}
|
|
107
106
|
|
|
108
107
|
<BaseNumberField.ScrubArea className="pb:absolute pb:inset-0 pb:z-10 pb:cursor-ew-resize pb:opacity-0 pb:hover:opacity-10 pb:transition-opacity pb:bg-primary/5 pb:rounded-md pb:pointer-events-none group-pb:hover:pointer-events-auto" />
|
|
109
|
-
</
|
|
108
|
+
</BaseNumberField.Group>
|
|
110
109
|
</BaseNumberField.Root>
|
|
111
110
|
</div>
|
|
112
111
|
);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { PeerbotsColorSelector, PeerbotsColor } from "./PeerbotsColorSelector";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof PeerbotsColorSelector> = {
|
|
6
|
+
title: "Forms/PeerbotsColorSelector",
|
|
7
|
+
component: PeerbotsColorSelector,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof PeerbotsColorSelector>;
|
|
12
|
+
|
|
13
|
+
function InteractiveSelector(props: Partial<React.ComponentProps<typeof PeerbotsColorSelector>>) {
|
|
14
|
+
const [selected, setSelected] = useState<PeerbotsColor>("Light Blue");
|
|
15
|
+
return (
|
|
16
|
+
<PeerbotsColorSelector
|
|
17
|
+
selectedColor={selected}
|
|
18
|
+
onColorSelect={setSelected}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
render: () => <InteractiveSelector />,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const Variations: Story = {
|
|
29
|
+
render: () => (
|
|
30
|
+
<div className="pb:flex pb:flex-col pb:gap-6 pb:p-4">
|
|
31
|
+
<div>
|
|
32
|
+
<span className="pb:text-xs pb:font-bold pb:text-gray-500 pb:block pb:mb-2">Small Size</span>
|
|
33
|
+
<InteractiveSelector size="sm" />
|
|
34
|
+
</div>
|
|
35
|
+
<div>
|
|
36
|
+
<span className="pb:text-xs pb:font-bold pb:text-gray-500 pb:block pb:mb-2">Medium Size (Default)</span>
|
|
37
|
+
<InteractiveSelector size="md" />
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<span className="pb:text-xs pb:font-bold pb:text-gray-500 pb:block pb:mb-2">Large Size</span>
|
|
41
|
+
<InteractiveSelector size="lg" />
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<span className="pb:text-xs pb:font-bold pb:text-gray-500 pb:block pb:mb-2">Subset of Colors</span>
|
|
45
|
+
<InteractiveSelector colors={["Light Blue", "Blue", "Green", "Red", "Purple"]} />
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
),
|
|
49
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "../utils";
|
|
3
|
+
|
|
4
|
+
export type PeerbotsColor =
|
|
5
|
+
| "Light Blue"
|
|
6
|
+
| "Blue"
|
|
7
|
+
| "Green"
|
|
8
|
+
| "Red"
|
|
9
|
+
| "Purple"
|
|
10
|
+
| "Pink"
|
|
11
|
+
| "Yellow"
|
|
12
|
+
| "Orange"
|
|
13
|
+
| "Grey"
|
|
14
|
+
| "Black"
|
|
15
|
+
| "White";
|
|
16
|
+
|
|
17
|
+
export const PEERBOTS_COLOR_MAP: Record<PeerbotsColor, string> = {
|
|
18
|
+
"Light Blue": "#ABE3FA",
|
|
19
|
+
Blue: "#5F94D6",
|
|
20
|
+
Green: "#65C77D",
|
|
21
|
+
Red: "#E6817E",
|
|
22
|
+
Purple: "#9B86F3",
|
|
23
|
+
Pink: "#EBA4E4",
|
|
24
|
+
Yellow: "#F8EDB1",
|
|
25
|
+
Orange: "#E6A575",
|
|
26
|
+
Grey: "#CECECE",
|
|
27
|
+
Black: "#4D4D4D",
|
|
28
|
+
White: "#FDFEFE",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const PEERBOTS_FONT_COLOR_MAP: Record<PeerbotsColor, string> = {
|
|
32
|
+
"Light Blue": "#020307",
|
|
33
|
+
Blue: "#020307",
|
|
34
|
+
Green: "#020307",
|
|
35
|
+
Red: "#020307",
|
|
36
|
+
Purple: "#020307",
|
|
37
|
+
Pink: "#020307",
|
|
38
|
+
Yellow: "#020307",
|
|
39
|
+
Orange: "#020307",
|
|
40
|
+
Grey: "#020307",
|
|
41
|
+
Black: "#FDFEFE",
|
|
42
|
+
White: "#020307",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface PeerbotsColorSelectorProps {
|
|
46
|
+
selectedColor?: PeerbotsColor | string;
|
|
47
|
+
onColorSelect: (color: PeerbotsColor) => void;
|
|
48
|
+
colors?: PeerbotsColor[];
|
|
49
|
+
size?: "sm" | "md" | "lg";
|
|
50
|
+
className?: string;
|
|
51
|
+
ariaLabelPrefix?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function PeerbotsColorSelector({
|
|
55
|
+
selectedColor,
|
|
56
|
+
onColorSelect,
|
|
57
|
+
colors = Object.keys(PEERBOTS_COLOR_MAP) as PeerbotsColor[],
|
|
58
|
+
size = "md",
|
|
59
|
+
className,
|
|
60
|
+
ariaLabelPrefix = "Select color",
|
|
61
|
+
}: PeerbotsColorSelectorProps) {
|
|
62
|
+
const sizeClasses = {
|
|
63
|
+
sm: "pb:h-6 pb:w-6",
|
|
64
|
+
md: "pb:h-8 pb:w-8",
|
|
65
|
+
lg: "pb:h-10 pb:w-10",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
role="radiogroup"
|
|
71
|
+
aria-label="Color selector"
|
|
72
|
+
className={cn(
|
|
73
|
+
"pb:inline-flex pb:flex-wrap pb:gap-1 pb:items-center",
|
|
74
|
+
className,
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{colors.map((colorName) => {
|
|
78
|
+
const isSelected = selectedColor === colorName;
|
|
79
|
+
return (
|
|
80
|
+
<button
|
|
81
|
+
key={colorName}
|
|
82
|
+
type="button"
|
|
83
|
+
role="radio"
|
|
84
|
+
aria-checked={isSelected}
|
|
85
|
+
aria-label={`${ariaLabelPrefix} ${colorName}`}
|
|
86
|
+
className={cn(
|
|
87
|
+
"pb:rounded-full pb:cursor-pointer pb:border pb:transition-all pb:duration-150 pb:focus-visible:ring-2 pb:focus-visible:ring-gray-400 pb:focus-visible:outline-none",
|
|
88
|
+
sizeClasses[size],
|
|
89
|
+
isSelected
|
|
90
|
+
? "pb:border-2 pb:border-black pb:scale-110 pb:shadow-sm"
|
|
91
|
+
: "pb:border-gray-300 pb:hover:border-gray-500 pb:hover:scale-105",
|
|
92
|
+
)}
|
|
93
|
+
style={{ background: PEERBOTS_COLOR_MAP[colorName] || colorName }}
|
|
94
|
+
onClick={() => onColorSelect(colorName)}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { Select } from "./Select";
|
|
3
|
-
import { Heading, Text } from "
|
|
3
|
+
import { Heading, Text } from "../foundations/Typography";
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof Select> = {
|
|
6
|
-
title: "
|
|
6
|
+
title: "Forms/Select",
|
|
7
7
|
component: Select,
|
|
8
8
|
tags: ["autodocs"],
|
|
9
9
|
argTypes: {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Select as BaseSelect } from "@base-ui/react";
|
|
3
|
-
import { cn } from "
|
|
4
|
-
import { Icon } from "
|
|
3
|
+
import { cn } from "../utils";
|
|
4
|
+
import { Icon } from "../foundations/Icon";
|
|
5
5
|
|
|
6
6
|
export interface SelectOption {
|
|
7
7
|
label: string;
|
|
8
8
|
value: string | number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export interface SelectProps {
|
|
11
|
+
export interface SelectProps extends React.AriaAttributes {
|
|
12
12
|
options: (string | SelectOption)[];
|
|
13
13
|
value?: string | number;
|
|
14
14
|
defaultValue?: string | number;
|
|
@@ -32,6 +32,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(
|
|
|
32
32
|
disabled,
|
|
33
33
|
placeholder = "Select...",
|
|
34
34
|
onFocus,
|
|
35
|
+
"aria-label": ariaLabel,
|
|
35
36
|
...props
|
|
36
37
|
},
|
|
37
38
|
ref,
|
|
@@ -61,6 +62,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(
|
|
|
61
62
|
<BaseSelect.Trigger
|
|
62
63
|
ref={ref}
|
|
63
64
|
id={selectId}
|
|
65
|
+
aria-label={ariaLabel || (typeof placeholder === "string" ? placeholder : "Select an option")}
|
|
64
66
|
className={cn(
|
|
65
67
|
"pb:flex pb:w-full pb:items-center pb:justify-between pb:rounded-md pb:border-0 pb:py-1.5 pb:pl-3 pb:pr-3 pb:text-gray-900 pb:ring-1 pb:ring-inset pb:ring-gray-300 pb:focus:ring-2 pb:focus:ring-primary pb:sm:text-sm pb:sm:leading-6 pb:bg-primary/10 pb:min-h-[36px]",
|
|
66
68
|
disabled && "pb:opacity-50 pb:cursor-not-allowed",
|
|
@@ -2,10 +2,10 @@ import type { Meta, StoryObj } from "@storybook/react";
|
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { Slider } from "./Slider";
|
|
4
4
|
import { Input } from "./Input";
|
|
5
|
-
import { Heading, Text } from "
|
|
5
|
+
import { Heading, Text } from "../foundations/Typography";
|
|
6
6
|
|
|
7
7
|
const meta: Meta<typeof Slider> = {
|
|
8
|
-
title: "
|
|
8
|
+
title: "Forms/Slider",
|
|
9
9
|
component: Slider,
|
|
10
10
|
argTypes: {
|
|
11
11
|
onChange: { action: "changed" },
|
|
@@ -62,11 +62,12 @@ const RangeChangeableSlider = ({
|
|
|
62
62
|
<div className="pb:flex pb:flex-col pb:gap-1">
|
|
63
63
|
<Text
|
|
64
64
|
variant="small"
|
|
65
|
-
className="pb:uppercase pb:text-slate-
|
|
65
|
+
className="pb:uppercase pb:text-slate-700 pb:font-bold pb:text-[10px]"
|
|
66
66
|
>
|
|
67
67
|
Min
|
|
68
68
|
</Text>
|
|
69
69
|
<Input
|
|
70
|
+
aria-label="Minimum value"
|
|
70
71
|
type="number"
|
|
71
72
|
value={min}
|
|
72
73
|
onChange={(e) => setMin(Number(e.target.value))}
|
|
@@ -76,11 +77,12 @@ const RangeChangeableSlider = ({
|
|
|
76
77
|
<div className="pb:flex pb:flex-col pb:gap-1">
|
|
77
78
|
<Text
|
|
78
79
|
variant="small"
|
|
79
|
-
className="pb:uppercase pb:text-slate-
|
|
80
|
+
className="pb:uppercase pb:text-slate-700 pb:font-bold pb:text-[10px]"
|
|
80
81
|
>
|
|
81
82
|
Max
|
|
82
83
|
</Text>
|
|
83
84
|
<Input
|
|
85
|
+
aria-label="Maximum value"
|
|
84
86
|
type="number"
|
|
85
87
|
value={max}
|
|
86
88
|
onChange={(e) => setMax(Number(e.target.value))}
|