@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.
Files changed (121) hide show
  1. package/.changeset/config.json +1 -1
  2. package/.github/workflows/storybook.yml +9 -1
  3. package/CHANGELOG.md +94 -0
  4. package/dist/index.d.mts +643 -131
  5. package/dist/index.d.ts +643 -131
  6. package/dist/index.js +7 -6
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +7 -6
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +6 -3
  11. package/src/charts/DistributionBarChart.tsx +10 -4
  12. package/src/charts/DistributionHistogram.tsx +13 -5
  13. package/src/charts/LineGraph.stories.tsx +60 -0
  14. package/src/charts/LineGraph.tsx +216 -0
  15. package/src/charts/index.ts +2 -0
  16. package/src/docs/DesignSystem.mdx +241 -25
  17. package/src/styles/theme.css +8 -0
  18. package/src/ui/feedback/Alert.stories.tsx +56 -0
  19. package/src/ui/feedback/Alert.tsx +168 -0
  20. package/src/ui/{Dialog.stories.tsx → feedback/Dialog.stories.tsx} +2 -2
  21. package/src/ui/{Dialog.tsx → feedback/Dialog.tsx} +2 -2
  22. package/src/ui/feedback/EmptyState.stories.tsx +57 -0
  23. package/src/ui/feedback/EmptyState.tsx +123 -0
  24. package/src/ui/feedback/Popover.stories.tsx +130 -0
  25. package/src/ui/feedback/Popover.tsx +135 -0
  26. package/src/ui/feedback/Skeleton.stories.tsx +88 -0
  27. package/src/ui/{Skeleton.tsx → feedback/Skeleton.tsx} +2 -2
  28. package/src/ui/feedback/Spinner.stories.tsx +77 -0
  29. package/src/ui/feedback/Spinner.tsx +62 -0
  30. package/src/ui/{Tooltip.stories.tsx → feedback/Tooltip.stories.tsx} +3 -3
  31. package/src/ui/{Tooltip.tsx → feedback/Tooltip.tsx} +2 -2
  32. package/src/ui/feedback/index.ts +7 -0
  33. package/src/ui/forms/Button.stories.tsx +181 -0
  34. package/src/ui/forms/Button.tsx +323 -0
  35. package/src/ui/{Checkbox.stories.tsx → forms/Checkbox.stories.tsx} +2 -2
  36. package/src/ui/{Checkbox.tsx → forms/Checkbox.tsx} +2 -2
  37. package/src/ui/forms/ChipGroup.stories.tsx +118 -0
  38. package/src/ui/forms/ChipGroup.tsx +239 -0
  39. package/src/ui/{Dropdown.tsx → forms/Dropdown.tsx} +1 -1
  40. package/src/ui/{Field.stories.tsx → forms/Field.stories.tsx} +3 -3
  41. package/src/ui/{Field.tsx → forms/Field.tsx} +1 -1
  42. package/src/ui/{Input.stories.tsx → forms/Input.stories.tsx} +6 -6
  43. package/src/ui/{Input.tsx → forms/Input.tsx} +1 -1
  44. package/src/ui/{Label.stories.tsx → forms/Label.stories.tsx} +2 -2
  45. package/src/ui/{Label.tsx → forms/Label.tsx} +2 -2
  46. package/src/ui/{NumberField.stories.tsx → forms/NumberField.stories.tsx} +2 -2
  47. package/src/ui/{NumberField.tsx → forms/NumberField.tsx} +8 -9
  48. package/src/ui/forms/PeerbotsColorSelector.stories.tsx +49 -0
  49. package/src/ui/forms/PeerbotsColorSelector.tsx +100 -0
  50. package/src/ui/{Select.stories.tsx → forms/Select.stories.tsx} +2 -2
  51. package/src/ui/{Select.tsx → forms/Select.tsx} +5 -3
  52. package/src/ui/{Slider.stories.tsx → forms/Slider.stories.tsx} +6 -4
  53. package/src/ui/{Slider.tsx → forms/Slider.tsx} +7 -3
  54. package/src/ui/forms/SliderWithNumberField.stories.tsx +136 -0
  55. package/src/ui/{SliderWithNumberField.tsx → forms/SliderWithNumberField.tsx} +19 -7
  56. package/src/ui/{Switch.stories.tsx → forms/Switch.stories.tsx} +2 -2
  57. package/src/ui/{Switch.tsx → forms/Switch.tsx} +1 -1
  58. package/src/ui/{TabRadio.stories.tsx → forms/TabRadio.stories.tsx} +3 -3
  59. package/src/ui/{TabRadio.tsx → forms/TabRadio.tsx} +1 -1
  60. package/src/ui/{TextArea.stories.tsx → forms/TextArea.stories.tsx} +5 -3
  61. package/src/ui/{TextArea.tsx → forms/TextArea.tsx} +1 -1
  62. package/src/ui/forms/index.ts +15 -0
  63. package/src/ui/foundations/Anchor.stories.tsx +39 -0
  64. package/src/ui/foundations/Anchor.tsx +96 -0
  65. package/src/ui/foundations/CodeBlock.stories.tsx +86 -0
  66. package/src/ui/foundations/CodeBlock.tsx +148 -0
  67. package/src/ui/foundations/Colors.stories.tsx +80 -0
  68. package/src/ui/{Icon.stories.tsx → foundations/Icon.stories.tsx} +1 -1
  69. package/src/ui/{Icon.tsx → foundations/Icon.tsx} +1 -1
  70. package/src/ui/{IconRegistry.tsx → foundations/IconRegistry.tsx} +261 -57
  71. package/src/ui/foundations/SocialLinks.stories.tsx +38 -0
  72. package/src/ui/foundations/SocialLinks.tsx +117 -0
  73. package/src/ui/foundations/Typography.stories.tsx +58 -0
  74. package/src/ui/foundations/Typography.tsx +163 -0
  75. package/src/ui/foundations/index.ts +6 -0
  76. package/src/ui/index.ts +7 -27
  77. package/src/ui/layout/Accordion.stories.tsx +86 -0
  78. package/src/ui/layout/Accordion.tsx +205 -0
  79. package/src/ui/layout/Card.stories.tsx +151 -0
  80. package/src/ui/layout/Card.tsx +124 -0
  81. package/src/ui/layout/Collapsible.stories.tsx +54 -0
  82. package/src/ui/layout/Collapsible.tsx +54 -0
  83. package/src/ui/{Separator.stories.tsx → layout/Separator.stories.tsx} +2 -2
  84. package/src/ui/{Separator.tsx → layout/Separator.tsx} +1 -1
  85. package/src/ui/{SettingsPanel.stories.tsx → layout/SettingsPanel.stories.tsx} +5 -5
  86. package/src/ui/{SettingsPanel.tsx → layout/SettingsPanel.tsx} +33 -29
  87. package/src/ui/layout/index.ts +5 -0
  88. package/src/ui/media/Avatar.stories.tsx +89 -0
  89. package/src/ui/media/Avatar.tsx +122 -0
  90. package/src/ui/media/Dropzone.stories.tsx +95 -0
  91. package/src/ui/media/Dropzone.tsx +328 -0
  92. package/src/ui/media/FileUpload.stories.tsx +62 -0
  93. package/src/ui/media/FileUpload.tsx +175 -0
  94. package/src/ui/media/index.ts +3 -0
  95. package/src/ui/{AuthFormUI.stories.tsx → patterns/AuthFormUI.stories.tsx} +1 -1
  96. package/src/ui/{AuthFormUI.tsx → patterns/AuthFormUI.tsx} +24 -23
  97. package/src/ui/patterns/CopyButton.stories.tsx +77 -0
  98. package/src/ui/patterns/CopyButton.tsx +93 -0
  99. package/src/ui/patterns/SearchInput.stories.tsx +51 -0
  100. package/src/ui/patterns/SearchInput.tsx +93 -0
  101. package/src/ui/patterns/Stepper.stories.tsx +129 -0
  102. package/src/ui/patterns/Stepper.tsx +295 -0
  103. package/src/ui/{TabSelection.stories.tsx → patterns/TabSelection.stories.tsx} +1 -1
  104. package/src/ui/{TabSelection.tsx → patterns/TabSelection.tsx} +2 -2
  105. package/src/ui/patterns/index.ts +6 -0
  106. package/src/ui/patterns/useClipboard.ts +75 -0
  107. package/src/ui/types.ts +48 -0
  108. package/src/ui/Alert.stories.tsx +0 -41
  109. package/src/ui/Alert.tsx +0 -72
  110. package/src/ui/Anchor.stories.tsx +0 -25
  111. package/src/ui/Anchor.tsx +0 -32
  112. package/src/ui/Button.stories.tsx +0 -108
  113. package/src/ui/Button.tsx +0 -121
  114. package/src/ui/Collapsible.stories.tsx +0 -91
  115. package/src/ui/Collapsible.tsx +0 -52
  116. package/src/ui/Colors.stories.tsx +0 -67
  117. package/src/ui/Popover.tsx +0 -42
  118. package/src/ui/Skeleton.stories.tsx +0 -43
  119. package/src/ui/SliderWithNumberField.stories.tsx +0 -101
  120. package/src/ui/Typography.stories.tsx +0 -87
  121. 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,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Menu as BaseMenu } from "@base-ui/react";
3
- import { cn } from "./utils";
3
+ import { cn } from "../utils";
4
4
 
5
5
  export interface DropdownProps {
6
6
  trigger: React.ReactNode;
@@ -1,7 +1,7 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { Field } from "./Field";
3
- import { Icon } from "./Icon";
4
- import { Heading } from "./Typography";
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: "UI/Field",
14
+ title: "Forms/Field",
15
15
  component: Field,
16
16
  tags: ["autodocs"],
17
17
  argTypes: {
@@ -1,6 +1,6 @@
1
1
  import { Field as BaseField } from "@base-ui/react";
2
2
  import React from "react";
3
- import { cn } from "./utils";
3
+ import { cn } from "../utils";
4
4
  import { Label } from "./Label";
5
5
 
6
6
  export interface FieldProps extends BaseField.Root.Props {
@@ -1,9 +1,9 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { Input } from "./Input";
3
- import { Heading, Text } from "./Typography";
3
+ import { Heading, Text } from "../foundations/Typography";
4
4
 
5
5
  const meta: Meta<typeof Input> = {
6
- title: "UI/Input",
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,6 +1,6 @@
1
1
  import { Input as BaseInput } from "@base-ui/react";
2
2
  import React from "react";
3
- import { cn } from "./utils";
3
+ import { cn } from "../utils";
4
4
 
5
5
  export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
6
6
  leftIcon?: React.ReactNode;
@@ -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 "./Typography";
4
+ import { Text } from "../foundations/Typography";
5
5
 
6
6
  const meta: Meta<typeof Label> = {
7
- title: "UI/Label",
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 "./utils";
3
- import { HelperTooltip } from "./Tooltip";
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 "./Typography";
4
+ import { Heading, Text } from "../foundations/Typography";
5
5
 
6
6
  const meta: Meta<typeof NumberField> = {
7
- title: "UI/NumberField",
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 "./utils";
4
- import { Icon } from "./Icon";
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
- <div
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
- </div>
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 "./Typography";
3
+ import { Heading, Text } from "../foundations/Typography";
4
4
 
5
5
  const meta: Meta<typeof Select> = {
6
- title: "UI/Select",
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 "./utils";
4
- import { Icon } from "./Icon";
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 "./Typography";
5
+ import { Heading, Text } from "../foundations/Typography";
6
6
 
7
7
  const meta: Meta<typeof Slider> = {
8
- title: "UI/Slider",
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-400 pb:font-bold pb:text-[10px]"
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-400 pb:font-bold pb:text-[10px]"
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))}