@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,129 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Stepper } from "./Stepper";
3
+ import { Button } from "../forms/Button";
4
+ import { Heading, Text } from "../foundations/Typography";
5
+ import React, { useState } from "react";
6
+
7
+ const meta: Meta<typeof Stepper> = {
8
+ title: "Patterns/Stepper",
9
+ component: Stepper,
10
+ parameters: {
11
+ layout: "centered",
12
+ },
13
+ tags: ["autodocs"],
14
+ argTypes: {
15
+ orientation: {
16
+ control: "select",
17
+ options: ["horizontal", "vertical", "chips"],
18
+ },
19
+ color: {
20
+ control: "select",
21
+ options: ["teal", "pink", "primary", "neutral"],
22
+ },
23
+ },
24
+ };
25
+
26
+ export default meta;
27
+ type Story = StoryObj<typeof Stepper>;
28
+
29
+ const sampleSteps = [
30
+ { id: "account", title: "Account", description: "Enter profile & email" },
31
+ { id: "robot", title: "Select Bot", description: "Choose CAD model" },
32
+ { id: "config", title: "Configure", description: "Set face parameters" },
33
+ { id: "review", title: "Review", description: "Confirm and deploy" },
34
+ ];
35
+
36
+ export const Default: Story = {
37
+ args: {
38
+ steps: sampleSteps,
39
+ activeStepId: "config",
40
+ orientation: "horizontal",
41
+ color: "teal",
42
+ className: "pb:w-[600px]",
43
+ },
44
+ };
45
+
46
+ export const InteractiveWizard: Story = {
47
+ render: () => {
48
+ const [currentStepId, setCurrentStepId] = useState("robot");
49
+ const stepIdx = sampleSteps.findIndex((s) => s.id === currentStepId);
50
+
51
+ return (
52
+ <div className="pb:flex pb:flex-col pb:gap-6 pb:p-6 pb:bg-white pb:rounded-2xl pb:border pb:border-gray-100 pb:shadow-md pb:w-[560px]">
53
+ <Stepper
54
+ steps={sampleSteps}
55
+ activeStepId={currentStepId}
56
+ onStepClick={setCurrentStepId}
57
+ color="teal"
58
+ />
59
+
60
+ <div className="pb:p-6 pb:bg-gray-50 pb:rounded-xl pb:text-center pb:space-y-2">
61
+ <Heading level={3} color="darkblue">
62
+ Step {stepIdx + 1}: {sampleSteps[stepIdx].title}
63
+ </Heading>
64
+ <Text size="sm" color="muted">
65
+ {sampleSteps[stepIdx].description}
66
+ </Text>
67
+ </div>
68
+
69
+ <div className="pb:flex pb:items-center pb:justify-between">
70
+ <Button
71
+ color="neutral"
72
+ disabled={stepIdx === 0}
73
+ onClick={() => setCurrentStepId(sampleSteps[stepIdx - 1].id)}
74
+ >
75
+ Previous
76
+ </Button>
77
+ <Button
78
+ color="teal"
79
+ disabled={stepIdx === sampleSteps.length - 1}
80
+ onClick={() => setCurrentStepId(sampleSteps[stepIdx + 1].id)}
81
+ >
82
+ Next Step
83
+ </Button>
84
+ </div>
85
+ </div>
86
+ );
87
+ },
88
+ };
89
+
90
+ export const BuildChipsGuide: Story = {
91
+ render: () => {
92
+ const [focused, setFocused] = useState("face");
93
+ const buildSteps = [
94
+ { id: "base", title: "1. Neck Base" },
95
+ { id: "servos", title: "2. Pan/Tilt Servos" },
96
+ { id: "face", title: "3. Face Plate" },
97
+ { id: "eyes", title: "4. OLED Displays" },
98
+ { id: "cover", title: "5. Outer Shell" },
99
+ ];
100
+
101
+ return (
102
+ <div className="pb:p-6 pb:bg-gray-50 pb:rounded-2xl pb:max-w-xl pb:space-y-4">
103
+ <Text size="xs" weight="bold" color="muted" className="pb:uppercase">
104
+ Assembly Steps — Click to Jump
105
+ </Text>
106
+ <Stepper
107
+ orientation="chips"
108
+ steps={buildSteps}
109
+ activeStepId={focused}
110
+ onStepClick={setFocused}
111
+ color="teal"
112
+ />
113
+ </div>
114
+ );
115
+ },
116
+ };
117
+
118
+ export const VerticalOrientation: Story = {
119
+ render: () => (
120
+ <div className="pb:p-6 pb:bg-white pb:rounded-2xl pb:border pb:border-gray-100 pb:w-[380px]">
121
+ <Stepper
122
+ orientation="vertical"
123
+ steps={sampleSteps}
124
+ activeStepId="robot"
125
+ color="pink"
126
+ />
127
+ </div>
128
+ ),
129
+ };
@@ -0,0 +1,295 @@
1
+ import React from "react";
2
+ import { cn } from "../utils";
3
+ import { Icon } from "../foundations/Icon";
4
+ import { ComponentColor, ComponentSize } from "../types";
5
+
6
+ export type StepStatus = "complete" | "current" | "upcoming" | "disabled";
7
+
8
+ export interface StepItem {
9
+ id: string;
10
+ title: React.ReactNode;
11
+ description?: React.ReactNode;
12
+ status?: StepStatus;
13
+ icon?: React.ReactNode;
14
+ }
15
+
16
+ export interface StepperProps {
17
+ /** Array of sequential steps */
18
+ steps: StepItem[];
19
+ /** Controlled currently active step id */
20
+ activeStepId?: string;
21
+ /** Callback fired when a step is clicked */
22
+ onStepClick?: (stepId: string) => void;
23
+ /** Layout orientation */
24
+ orientation?: "horizontal" | "vertical" | "chips";
25
+ /** Brand color theme */
26
+ color?: ComponentColor;
27
+ /** Size scale */
28
+ size?: ComponentSize;
29
+ className?: string;
30
+ }
31
+
32
+ const colorBadgeStyles: Record<string, { current: string; complete: string; line: string }> = {
33
+ teal: {
34
+ current: "pb:bg-peerbots-teal pb:text-gray-900 pb:ring-4 pb:ring-peerbots-teal/30",
35
+ complete: "pb:bg-peerbots-darkteal pb:text-white",
36
+ line: "pb:bg-peerbots-darkteal",
37
+ },
38
+ pink: {
39
+ current: "pb:bg-rose-700 pb:text-white pb:ring-4 pb:ring-rose-200",
40
+ complete: "pb:bg-rose-800 pb:text-white",
41
+ line: "pb:bg-rose-700",
42
+ },
43
+ primary: {
44
+ current: "pb:bg-primary pb:text-gray-900 pb:ring-4 pb:ring-primary/30",
45
+ complete: "pb:bg-dark-primary pb:text-white",
46
+ line: "pb:bg-dark-primary",
47
+ },
48
+ darkblue: {
49
+ current: "pb:bg-peerbots-darkblue pb:text-white pb:ring-4 pb:ring-peerbots-darkblue/20",
50
+ complete: "pb:bg-peerbots-darkblue pb:text-white",
51
+ line: "pb:bg-peerbots-darkblue",
52
+ },
53
+ neutral: {
54
+ current: "pb:bg-gray-900 pb:text-white pb:ring-4 pb:ring-gray-300",
55
+ complete: "pb:bg-gray-800 pb:text-white",
56
+ line: "pb:bg-gray-600",
57
+ },
58
+ };
59
+
60
+ export function Stepper({
61
+ steps,
62
+ activeStepId,
63
+ onStepClick,
64
+ orientation = "horizontal",
65
+ color = "teal",
66
+ size = "md",
67
+ className,
68
+ }: StepperProps) {
69
+ const activeIndex = steps.findIndex((s) => s.id === activeStepId);
70
+ const colorStyle = colorBadgeStyles[color] || colorBadgeStyles.teal;
71
+
72
+ const resolveStatus = (step: StepItem, index: number): StepStatus => {
73
+ if (step.status) return step.status;
74
+ if (activeIndex === -1) return index === 0 ? "current" : "upcoming";
75
+ if (index < activeIndex) return "complete";
76
+ if (index === activeIndex) return "current";
77
+ return "upcoming";
78
+ };
79
+
80
+ // 1. "chips" compact jump mode (as in Moddy build guide)
81
+ if (orientation === "chips") {
82
+ return (
83
+ <ol
84
+ className={cn(
85
+ "pb:flex pb:flex-row pb:flex-wrap pb:items-center pb:gap-2 pb:list-none pb:p-0 pb:m-0",
86
+ className,
87
+ )}
88
+ >
89
+ {steps.map((step, idx) => {
90
+ const status = resolveStatus(step, idx);
91
+ const isCurrent = status === "current";
92
+ const isComplete = status === "complete";
93
+ const isClickable = Boolean(onStepClick) && status !== "disabled";
94
+ const stepLabel = typeof step.title === "string" ? `Step ${idx + 1}: ${step.title}` : `Step ${idx + 1}`;
95
+
96
+ return (
97
+ <li key={step.id}>
98
+ <button
99
+ type="button"
100
+ aria-label={stepLabel}
101
+ disabled={!isClickable}
102
+ onClick={() => onStepClick?.(step.id)}
103
+ className={cn(
104
+ "pb:inline-flex pb:items-center pb:gap-1.5 pb:pl-1.5 pb:pr-3 pb:py-1 pb:rounded-full pb:border pb:transition-all pb:duration-150 pb:text-xs pb:font-bold",
105
+ isClickable ? "pb:cursor-pointer" : "pb:cursor-default",
106
+ isCurrent
107
+ ? cn(colorStyle.current, "pb:shadow-sm")
108
+ : isComplete
109
+ ? "pb:bg-gray-100 pb:text-gray-900 pb:border-gray-300 pb:hover:border-peerbots-darkteal"
110
+ : "pb:bg-white pb:text-gray-600 pb:border-gray-300 pb:hover:text-gray-900",
111
+ )}
112
+ >
113
+ <span
114
+ className={cn(
115
+ "pb:flex pb:items-center pb:justify-center pb:w-4 pb:h-4 pb:rounded-full pb:text-[10px] pb:font-bold",
116
+ isCurrent
117
+ ? "pb:bg-black/15 pb:text-current"
118
+ : isComplete
119
+ ? "pb:bg-peerbots-darkteal pb:text-white"
120
+ : "pb:bg-gray-200 pb:text-gray-700",
121
+ )}
122
+ >
123
+ {isComplete ? (
124
+ <Icon name="check" className="pb:w-2.5 pb:pb:h-2.5" />
125
+ ) : (
126
+ idx + 1
127
+ )}
128
+ </span>
129
+ <span className="pb:uppercase pb:tracking-tight pb:whitespace-nowrap">
130
+ {step.title}
131
+ </span>
132
+ </button>
133
+ </li>
134
+ );
135
+ })}
136
+ </ol>
137
+ );
138
+ }
139
+
140
+ // 2. "vertical" layout mode
141
+ if (orientation === "vertical") {
142
+ return (
143
+ <ol
144
+ className={cn(
145
+ "pb:flex pb:flex-col pb:gap-0 pb:list-none pb:p-0 pb:m-0",
146
+ className,
147
+ )}
148
+ >
149
+ {steps.map((step, idx) => {
150
+ const status = resolveStatus(step, idx);
151
+ const isCurrent = status === "current";
152
+ const isComplete = status === "complete";
153
+ const isLast = idx === steps.length - 1;
154
+ const isClickable = Boolean(onStepClick) && status !== "disabled";
155
+ const stepLabel = typeof step.title === "string" ? `Step ${idx + 1}: ${step.title}` : `Step ${idx + 1}`;
156
+
157
+ return (
158
+ <li key={step.id} className="pb:relative pb:flex pb:items-start pb:gap-4 pb:pb-6">
159
+ {!isLast && (
160
+ <div
161
+ className={cn(
162
+ "pb:absolute pb:left-4 pb:top-8 pb:-bottom-0 pb:w-0.5 pb:bg-gray-200",
163
+ isComplete && colorStyle.line,
164
+ )}
165
+ />
166
+ )}
167
+
168
+ <button
169
+ type="button"
170
+ aria-label={stepLabel}
171
+ disabled={!isClickable}
172
+ onClick={() => onStepClick?.(step.id)}
173
+ className={cn(
174
+ "pb:flex pb:items-center pb:justify-center pb:w-8 pb:h-8 pb:rounded-full pb:font-bold pb:text-xs pb:shrink-0 pb:transition-all pb:duration-200 pb:z-10",
175
+ isClickable ? "pb:cursor-pointer" : "pb:cursor-default",
176
+ isCurrent
177
+ ? colorStyle.current
178
+ : isComplete
179
+ ? colorStyle.complete
180
+ : "pb:bg-gray-100 pb:text-gray-700 pb:border pb:border-gray-300",
181
+ )}
182
+ >
183
+ {step.icon || (isComplete ? <Icon name="check" className="pb:w-4 pb:h-4" /> : idx + 1)}
184
+ </button>
185
+
186
+ <div
187
+ onClick={() => isClickable && onStepClick?.(step.id)}
188
+ className={cn(
189
+ "pb:pt-0.5 pb:flex-1",
190
+ isClickable && "pb:cursor-pointer",
191
+ )}
192
+ >
193
+ <div
194
+ className={cn(
195
+ "pb:text-sm pb:font-bold",
196
+ isCurrent
197
+ ? "pb:text-gray-950"
198
+ : isComplete
199
+ ? "pb:text-gray-900"
200
+ : "pb:text-gray-600",
201
+ )}
202
+ >
203
+ {step.title}
204
+ </div>
205
+ {step.description && (
206
+ <div className="pb:text-xs pb:text-gray-600 pb:mt-0.5 pb:leading-relaxed">
207
+ {step.description}
208
+ </div>
209
+ )}
210
+ </div>
211
+ </li>
212
+ );
213
+ })}
214
+ </ol>
215
+ );
216
+ }
217
+
218
+ // 3. "horizontal" rail mode (default)
219
+ return (
220
+ <ol
221
+ className={cn(
222
+ "pb:flex pb:w-full pb:items-center pb:justify-between pb:gap-0 pb:list-none pb:p-0 pb:m-0",
223
+ className,
224
+ )}
225
+ >
226
+ {steps.map((step, idx) => {
227
+ const status = resolveStatus(step, idx);
228
+ const isCurrent = status === "current";
229
+ const isComplete = status === "complete";
230
+ const isLast = idx === steps.length - 1;
231
+ const isClickable = Boolean(onStepClick) && status !== "disabled";
232
+ const stepLabel = typeof step.title === "string" ? `Step ${idx + 1}: ${step.title}` : `Step ${idx + 1}`;
233
+
234
+ return (
235
+ <li
236
+ key={step.id}
237
+ className={cn(
238
+ "pb:flex pb:items-center",
239
+ isLast ? "pb:flex-none" : "pb:flex-1",
240
+ )}
241
+ >
242
+ <div className="pb:flex pb:flex-col pb:items-center pb:text-center pb:relative">
243
+ <button
244
+ type="button"
245
+ aria-label={stepLabel}
246
+ disabled={!isClickable}
247
+ onClick={() => onStepClick?.(step.id)}
248
+ className={cn(
249
+ "pb:flex pb:items-center pb:justify-center pb:w-9 pb:h-9 pb:rounded-full pb:font-bold pb:text-xs pb:transition-all pb:duration-200 pb:z-10",
250
+ isClickable ? "pb:cursor-pointer" : "pb:cursor-default",
251
+ isCurrent
252
+ ? colorStyle.current
253
+ : isComplete
254
+ ? colorStyle.complete
255
+ : "pb:bg-gray-100 pb:text-gray-700 pb:border pb:border-gray-300",
256
+ )}
257
+ >
258
+ {step.icon || (isComplete ? <Icon name="check" className="pb:w-4 pb:h-4" /> : idx + 1)}
259
+ </button>
260
+
261
+ <div className="pb:mt-2 pb:max-w-[120px]">
262
+ <div
263
+ className={cn(
264
+ "pb:text-xs pb:font-bold pb:leading-tight",
265
+ isCurrent
266
+ ? "pb:text-gray-950"
267
+ : isComplete
268
+ ? "pb:text-gray-900"
269
+ : "pb:text-gray-600",
270
+ )}
271
+ >
272
+ {step.title}
273
+ </div>
274
+ {step.description && (
275
+ <div className="pb:text-[10px] pb:text-gray-600 pb:mt-0.5 pb:hidden sm:pb:block truncate">
276
+ {step.description}
277
+ </div>
278
+ )}
279
+ </div>
280
+ </div>
281
+
282
+ {!isLast && (
283
+ <div
284
+ className={cn(
285
+ "pb:flex-1 pb:h-0.5 pb:bg-gray-200 pb:mx-2 pb:transition-colors pb:duration-300 pb:-mt-6",
286
+ isComplete && colorStyle.line,
287
+ )}
288
+ />
289
+ )}
290
+ </li>
291
+ );
292
+ })}
293
+ </ol>
294
+ );
295
+ }
@@ -3,7 +3,7 @@ import { TabSelection } from "./TabSelection";
3
3
  import React, { useState } from "react";
4
4
 
5
5
  const meta: Meta<typeof TabSelection> = {
6
- title: "UI/TabSelection",
6
+ title: "Patterns/TabSelection",
7
7
  component: TabSelection,
8
8
  parameters: {
9
9
  layout: "centered",
@@ -1,4 +1,4 @@
1
- import { Button, Icon } from ".";
1
+ import { Button, Icon } from "..";
2
2
  import React from "react";
3
3
 
4
4
  export interface TabItem {
@@ -78,7 +78,7 @@ export function TabSelection({
78
78
  })}
79
79
  {onAddClick && (
80
80
  <Button
81
- variant="primary"
81
+ color="primary"
82
82
  size="sm"
83
83
  aria-label="Create New Tab"
84
84
  onClick={onAddClick}
@@ -0,0 +1,6 @@
1
+ export * from "./AuthFormUI";
2
+ export * from "./TabSelection";
3
+ export * from "./SearchInput";
4
+ export * from "./CopyButton";
5
+ export * from "./useClipboard";
6
+ export * from "./Stepper";
@@ -0,0 +1,75 @@
1
+ import { useState, useCallback, useRef, useEffect } from "react";
2
+
3
+ export interface UseClipboardOptions {
4
+ /**
5
+ * Duration in milliseconds for the `copied` state to remain `true`.
6
+ * @default 2000
7
+ */
8
+ timeout?: number;
9
+ /** Callback fired after successfully copying to clipboard. */
10
+ onSuccess?: (text: string) => void;
11
+ /** Callback fired if copying fails. */
12
+ onError?: (error: Error) => void;
13
+ }
14
+
15
+ export function useClipboard(options: UseClipboardOptions = {}) {
16
+ const { timeout = 2000, onSuccess, onError } = options;
17
+ const [copied, setCopied] = useState(false);
18
+ const [error, setError] = useState<Error | null>(null);
19
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
20
+
21
+ const copy = useCallback(
22
+ async (text: string): Promise<boolean> => {
23
+ if (timerRef.current) {
24
+ clearTimeout(timerRef.current);
25
+ }
26
+
27
+ try {
28
+ if (navigator?.clipboard?.writeText) {
29
+ await navigator.clipboard.writeText(text);
30
+ } else {
31
+ // Fallback for non-secure / legacy contexts
32
+ const textarea = document.createElement("textarea");
33
+ textarea.value = text;
34
+ textarea.style.position = "fixed";
35
+ textarea.style.opacity = "0";
36
+ document.body.appendChild(textarea);
37
+ textarea.focus();
38
+ textarea.select();
39
+ const successful = document.execCommand("copy");
40
+ document.body.removeChild(textarea);
41
+ if (!successful) {
42
+ throw new Error("execCommand copy failed");
43
+ }
44
+ }
45
+
46
+ setCopied(true);
47
+ setError(null);
48
+ onSuccess?.(text);
49
+
50
+ timerRef.current = setTimeout(() => {
51
+ setCopied(false);
52
+ }, timeout);
53
+
54
+ return true;
55
+ } catch (err) {
56
+ const copyError = err instanceof Error ? err : new Error(String(err));
57
+ setError(copyError);
58
+ setCopied(false);
59
+ onError?.(copyError);
60
+ return false;
61
+ }
62
+ },
63
+ [timeout, onSuccess, onError],
64
+ );
65
+
66
+ useEffect(() => {
67
+ return () => {
68
+ if (timerRef.current) {
69
+ clearTimeout(timerRef.current);
70
+ }
71
+ };
72
+ }, []);
73
+
74
+ return { copy, copied, error };
75
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Canonical Peerbots Brand Palette
3
+ */
4
+ export type BrandColor =
5
+ | "teal" // Primary Brand Teal (#5fc7cc)
6
+ | "pink" // Secondary Brand Pink (#e96c8a)
7
+ | "darkteal" // High-Contrast Dark Teal (#3f8588)
8
+ | "darkblue" // Deep Navy for Headings/Dark Surfaces (#2e3a59)
9
+ | "olive" // Earthy Accent (#82871f)
10
+ | "dark" // Dark Neutral / Near-Black (#1a1a1a)
11
+ | "light" // Light Neutral / Off-White (#f8f9fa)
12
+ | "white"; // Pure White (#ffffff)
13
+
14
+ /**
15
+ * Semantic Action & Status Intents
16
+ */
17
+ export type IntentColor =
18
+ | "primary" // High-emphasis main call-to-action (Electric Cyan #46d9d9)
19
+ | "neutral" // Neutral, low-friction secondary action (Gray)
20
+ | "success" // Positive confirmation (Green)
21
+ | "warning" // Cautionary alert (Yellow/Amber)
22
+ | "danger" // Destructive action / Error (Red)
23
+ | "info"; // Informational notice (Blue)
24
+
25
+ /**
26
+ * Composite Color Token for UI Components
27
+ */
28
+ export type ComponentColor = IntentColor | BrandColor;
29
+
30
+ /**
31
+ * Visual Fill Style / Treatment
32
+ */
33
+ export type FillVariant = "solid" | "soft" | "outline" | "ghost" | "link";
34
+
35
+ /**
36
+ * Surface & Container Styles (Cards, Dialogs, Panels)
37
+ */
38
+ export type SurfaceVariant = "surface" | "outline" | "glass" | "flat";
39
+
40
+ /**
41
+ * Standard Sizing Scale
42
+ */
43
+ export type ComponentSize = "xs" | "sm" | "md" | "lg" | "xl";
44
+
45
+ /**
46
+ * Standard Geometric Border Radii
47
+ */
48
+ export type ComponentRadius = "none" | "sm" | "md" | "lg" | "2xl" | "pill";
@@ -1,41 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react";
2
- import { Alert } from "./Alert";
3
- import React from "react";
4
-
5
- const meta: Meta<typeof Alert> = {
6
- title: "UI/Alert",
7
- component: Alert,
8
- parameters: {
9
- layout: "centered",
10
- },
11
- };
12
-
13
- export default meta;
14
-
15
- type Story = StoryObj<typeof Alert>;
16
-
17
- export const Default: Story = {
18
- args: {
19
- level: "Info",
20
- message: "This is a simple informational alert.",
21
- },
22
- };
23
-
24
- export const Variations: Story = {
25
- render: () => (
26
- <div className="pb:flex pb:flex-col pb:gap-4 pb:w-[600px] pb:bg-gray-100 pb:p-8">
27
- <Alert
28
- level="Error"
29
- message="Critical connection failure!"
30
- action={{ name: "Retry", callback: () => alert("Retrying...") }}
31
- />
32
- <Alert level="Warning" message="Your session is about to expire." />
33
- <Alert level="Success" message="Settings saved successfully." />
34
- <Alert
35
- level="Info"
36
- message="New firmware update available."
37
- action={{ name: "View", callback: () => alert("Viewing update...") }}
38
- />
39
- </div>
40
- ),
41
- };
package/src/ui/Alert.tsx DELETED
@@ -1,72 +0,0 @@
1
- import { useState } from "react";
2
- import { Button, Icon } from ".";
3
-
4
- export type AlertLevel = "Error" | "Warning" | "Success" | "Info";
5
-
6
- export interface AlertAction {
7
- name: string;
8
- callback: () => void;
9
- }
10
-
11
- export interface AlertUIProps {
12
- level: AlertLevel;
13
- message: React.ReactNode;
14
- action?: AlertAction;
15
- }
16
-
17
- export function Alert({ level, message, action }: AlertUIProps) {
18
- const [showAlert, setShowAlert] = useState(true);
19
-
20
- return (
21
- <div
22
- className={
23
- "pb:flex pb:w-full pb:p-4 pb:m-1 pb:text-sm pb:rounded-lg pb:bg-white pb:border pb:border-solid pb:transition-opacity pb:ease-in-out pb:delay-150 pb:duration-300 " +
24
- (showAlert ? " " : " pb:hidden")
25
- }
26
- >
27
- <span>
28
- {level === "Error" && (
29
- <Icon
30
- name="exclamationCircle"
31
- size="lg"
32
- className="pb:m-2"
33
- stroke="red"
34
- />
35
- )}
36
- {level === "Warning" && (
37
- <Icon
38
- name="exclamationTriangle"
39
- size="lg"
40
- className="pb:m-2"
41
- stroke="yellow"
42
- />
43
- )}
44
- {level === "Success" && (
45
- <Icon name="checkCircle" size="lg" className="pb:m-2" stroke="green" />
46
- )}
47
- {level === "Info" && (
48
- <Icon name="megaphone" size="lg" className="pb:m-2" />
49
- )}
50
- </span>
51
- <span className="pb:flex pb:flex-1 pb:ml-2">{message}</span>
52
- {action && (
53
- <Button
54
- onClick={() => {
55
- action.callback();
56
- setShowAlert(false);
57
- }}
58
- >
59
- {action.name}
60
- </Button>
61
- )}
62
- <span
63
- className="pb:cursor-pointer"
64
- onClick={() => {
65
- setShowAlert(false);
66
- }}
67
- >
68
- <Icon name="xCircle" size="lg" className="pb:m-1" />
69
- </span>
70
- </div>
71
- );
72
- }