@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,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
|
+
}
|
|
@@ -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
|
-
|
|
81
|
+
color="primary"
|
|
82
82
|
size="sm"
|
|
83
83
|
aria-label="Create New Tab"
|
|
84
84
|
onClick={onAddClick}
|
|
@@ -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
|
+
}
|
package/src/ui/types.ts
ADDED
|
@@ -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";
|
package/src/ui/Alert.stories.tsx
DELETED
|
@@ -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
|
-
}
|