@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
@@ -13,6 +13,14 @@
13
13
  --sidebar-bg: #f9ffff;
14
14
  --color-sidebar-bg: #f9ffff;
15
15
 
16
+ --color-peerbots-pink: #e96c8a;
17
+ --color-peerbots-teal: #5fc7cc;
18
+ --color-peerbots-darkteal: #0f766e;
19
+ --color-peerbots-darkblue: #2e3a59;
20
+ --color-peerbots-olive: #82871f;
21
+ --color-peerbots-dark: #1a1a1a;
22
+ --color-peerbots-light: #f8f9fa;
23
+
16
24
  --background-image-gradient-radial: radial-gradient(var(--tw-gradient-stops));
17
25
  --background-image-gradient-conic: conic-gradient(from 180deg at 50% 50%,
18
26
  var(--tw-gradient-stops));
@@ -0,0 +1,56 @@
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: "Feedback/Alert",
7
+ component: Alert,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ tags: ["autodocs"],
12
+ argTypes: {
13
+ color: {
14
+ control: "select",
15
+ options: ["info", "success", "warning", "danger", "neutral"],
16
+ },
17
+ variant: {
18
+ control: "select",
19
+ options: ["soft", "outline", "solid"],
20
+ },
21
+ title: { control: "text" },
22
+ message: { control: "text" },
23
+ },
24
+ };
25
+
26
+ export default meta;
27
+ type Story = StoryObj<typeof Alert>;
28
+
29
+ export const Default: Story = {
30
+ args: {
31
+ color: "info",
32
+ variant: "outline",
33
+ title: "System Update",
34
+ message: "A new robot face configuration has been synchronized to your controller.",
35
+ className: "pb:w-[480px]",
36
+ },
37
+ };
38
+
39
+ export const ColorsAndVariants: Story = {
40
+ render: () => (
41
+ <div className="pb:flex pb:flex-col pb:gap-4 pb:w-[520px]">
42
+ <Alert color="info" variant="outline" title="Info Banner (Outline)" message="New feature release available." />
43
+ <Alert color="success" variant="soft" title="Session Connected" message="Controller paired with robot face." />
44
+ <Alert color="warning" variant="soft" title="Battery Low" message="Robot head battery is below 15%." />
45
+ <Alert
46
+ color="danger"
47
+ variant="soft"
48
+ title="Connection Failed"
49
+ message="Unable to reach the WebSocket server."
50
+ action={{ name: "Retry", callback: () => console.log("Retrying...") }}
51
+ />
52
+ <Alert color="danger" variant="outline" title="Outline Danger" message="Action cannot be undone." />
53
+ <Alert color="success" variant="solid" title="Solid Success" message="Changes published successfully." />
54
+ </div>
55
+ ),
56
+ };
@@ -0,0 +1,168 @@
1
+ import React, { useState } from "react";
2
+ import { Button } from "../forms/Button";
3
+ import { Icon } from "../foundations/Icon";
4
+ import { cn } from "../utils";
5
+ import { ComponentRadius, FillVariant } from "../types";
6
+
7
+ export type AlertLevel = "Error" | "Warning" | "Success" | "Info";
8
+ export type AlertColor = "danger" | "warning" | "success" | "info" | "neutral";
9
+
10
+ export interface AlertAction {
11
+ name: string;
12
+ callback: () => void;
13
+ }
14
+
15
+ export interface AlertUIProps {
16
+ /** Alert semantic color */
17
+ color?: AlertColor;
18
+ /** Legacy level prop for backward compatibility */
19
+ level?: AlertLevel;
20
+ /** Visual style variant */
21
+ variant?: "soft" | "outline" | "solid";
22
+ /** Optional alert title */
23
+ title?: React.ReactNode;
24
+ /** Alert message body */
25
+ message?: React.ReactNode;
26
+ /** Children as alternative to message */
27
+ children?: React.ReactNode;
28
+ /** Optional action button */
29
+ action?: AlertAction;
30
+ /** Callback fired when alert is dismissed */
31
+ onClose?: () => void;
32
+ /** Border radius scale */
33
+ radius?: ComponentRadius;
34
+ className?: string;
35
+ }
36
+
37
+ const levelColorMap: Record<AlertLevel, AlertColor> = {
38
+ Error: "danger",
39
+ Warning: "warning",
40
+ Success: "success",
41
+ Info: "info",
42
+ };
43
+
44
+ const iconMap: Record<AlertColor, { name: any; colorClass: string }> = {
45
+ danger: { name: "exclamationCircle", colorClass: "pb:text-red-700" },
46
+ warning: { name: "exclamationTriangle", colorClass: "pb:text-amber-800" },
47
+ success: { name: "checkCircle", colorClass: "pb:text-emerald-800" },
48
+ info: { name: "megaphone", colorClass: "pb:text-teal-800" },
49
+ neutral: { name: "questionMarkCircle", colorClass: "pb:text-gray-700" },
50
+ };
51
+
52
+ const variantColorClasses: Record<string, Record<AlertColor, string>> = {
53
+ soft: {
54
+ danger: "pb:bg-red-50 pb:border-red-200 pb:text-red-900",
55
+ warning: "pb:bg-amber-50 pb:border-amber-200 pb:text-amber-900",
56
+ success: "pb:bg-emerald-50 pb:border-emerald-200 pb:text-emerald-900",
57
+ info: "pb:bg-teal-50 pb:border-teal-200 pb:text-teal-900",
58
+ neutral: "pb:bg-gray-50 pb:border-gray-200 pb:text-gray-900",
59
+ },
60
+ outline: {
61
+ danger: "pb:bg-red-50/40 pb:border-2 pb:border-red-400 pb:text-red-950",
62
+ warning: "pb:bg-amber-50/40 pb:border-2 pb:border-amber-400 pb:text-amber-950",
63
+ success: "pb:bg-emerald-50/40 pb:border-2 pb:border-emerald-500 pb:text-emerald-950",
64
+ info: "pb:bg-teal-50/40 pb:border-2 pb:border-teal-500 pb:text-teal-950",
65
+ neutral: "pb:bg-gray-50/40 pb:border-2 pb:border-gray-300 pb:text-gray-950",
66
+ },
67
+ solid: {
68
+ danger: "pb:bg-red-700 pb:border-transparent pb:text-white",
69
+ warning: "pb:bg-amber-300 pb:border-transparent pb:text-gray-950 pb:font-medium",
70
+ success: "pb:bg-emerald-700 pb:border-transparent pb:text-white",
71
+ info: "pb:bg-teal-700 pb:border-transparent pb:text-white",
72
+ neutral: "pb:bg-gray-900 pb:border-transparent pb:text-white",
73
+ },
74
+ };
75
+
76
+ const radiusClasses: Record<ComponentRadius, string> = {
77
+ none: "pb:rounded-none",
78
+ sm: "pb:rounded-md",
79
+ md: "pb:rounded-xl",
80
+ lg: "pb:rounded-2xl",
81
+ "2xl": "pb:rounded-2xl",
82
+ pill: "pb:rounded-full",
83
+ };
84
+
85
+ export function Alert({
86
+ color,
87
+ level,
88
+ variant = "soft",
89
+ title,
90
+ message,
91
+ children,
92
+ action,
93
+ onClose,
94
+ radius = "2xl",
95
+ className,
96
+ }: AlertUIProps) {
97
+ const [visible, setVisible] = useState(true);
98
+
99
+ if (!visible) return null;
100
+
101
+ const resolvedColor: AlertColor =
102
+ color || (level ? levelColorMap[level] : "info") || "info";
103
+
104
+ const { name: iconName, colorClass } = iconMap[resolvedColor];
105
+ const colorStyle = variantColorClasses[variant]?.[resolvedColor] || variantColorClasses.soft.info;
106
+
107
+ const handleDismiss = () => {
108
+ setVisible(false);
109
+ onClose?.();
110
+ };
111
+
112
+ const body = children || message;
113
+
114
+ return (
115
+ <div
116
+ role="alert"
117
+ className={cn(
118
+ "pb:flex pb:w-full pb:items-start pb:gap-3.5 pb:p-4.5 pb:border pb:shadow-xs pb:transition-all pb:duration-200",
119
+ radiusClasses[radius] || radiusClasses["2xl"],
120
+ colorStyle,
121
+ className,
122
+ )}
123
+ >
124
+ <div className="pb:shrink-0 pb:pt-0.5">
125
+ <Icon
126
+ name={iconName}
127
+ className={cn(
128
+ "pb:w-5 pb:h-5",
129
+ variant === "solid" ? "pb:text-current" : colorClass,
130
+ )}
131
+ />
132
+ </div>
133
+
134
+ <div className="pb:flex-1 pb:min-w-0 pb:text-sm pb:leading-relaxed">
135
+ {title && (
136
+ <div className="pb:font-bold pb:text-base pb:mb-1 pb:leading-snug">
137
+ {title}
138
+ </div>
139
+ )}
140
+ <div className={cn(title && "pb:opacity-90")}>{body}</div>
141
+ </div>
142
+
143
+ {action && (
144
+ <Button
145
+ size="sm"
146
+ variant={variant === "solid" ? "outline" : "solid"}
147
+ color={resolvedColor === "danger" ? "danger" : "neutral"}
148
+ onClick={() => {
149
+ action.callback();
150
+ handleDismiss();
151
+ }}
152
+ className="pb:shrink-0 pb:self-center"
153
+ >
154
+ {action.name}
155
+ </Button>
156
+ )}
157
+
158
+ <button
159
+ type="button"
160
+ onClick={handleDismiss}
161
+ aria-label="Dismiss alert"
162
+ className="pb:shrink-0 pb:p-1 pb:rounded-lg pb:opacity-70 pb:hover:opacity-100 pb:transition-opacity pb:cursor-pointer pb:self-start pb:-mr-1"
163
+ >
164
+ <Icon name="xCircle" className="pb:w-5 pb:h-5" />
165
+ </button>
166
+ </div>
167
+ );
168
+ }
@@ -1,9 +1,9 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { Dialog, DialogTitle, DialogDescription, DialogClose } from "./Dialog";
3
- import { Button } from "./Button";
3
+ import { Button } from "../forms/Button";
4
4
 
5
5
  const meta: Meta<typeof Dialog> = {
6
- title: "UI/Dialog",
6
+ title: "Feedback/Dialog",
7
7
  component: Dialog,
8
8
  parameters: {
9
9
  layout: "centered",
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Dialog as BaseDialog } from "@base-ui/react";
3
- import { cn } from "./utils";
3
+ import { cn } from "../utils";
4
4
 
5
5
  export interface DialogProps {
6
6
  open?: boolean;
@@ -34,7 +34,7 @@ export function Dialog({
34
34
  <BaseDialog.Backdrop className="pb:fixed pb:inset-0 pb:z-50 pb:bg-black/40 pb:transition-all pb:duration-200 data-[state=open]:pb:animate-in data-[state=closed]:pb:animate-out pb:data-[state=closed]:fade-out-0 pb:data-[state=open]:fade-in-0" />
35
35
  <BaseDialog.Popup
36
36
  className={cn(
37
- "pb:fixed pb:left-[50%] pb:top-[50%] pb:z-50 pb:grid pb:w-auto pb:max-w-[90vw] pb:translate-x-[-50%] pb:translate-y-[-50%] pb:gap-4 pb:border pb:bg-white pb:p-6 pb:shadow-lg pb:duration-200 data-[state=open]:pb:animate-in data-[state=closed]:pb:animate-out data-[state=closed]:pb:fade-out-0 data-[state=open]:pb:fade-in-0 data-[state=closed]:pb:zoom-out-95 data-[state=open]:pb:zoom-in-95 data-[state=closed]:pb:slide-out-to-left-1/2 data-[state=closed]:pb:slide-out-to-top-[48%] data-[state=open]:pb:slide-in-from-left-1/2 data-[state=open]:pb:slide-in-from-top-[48%] sm:rounded-lg",
37
+ "pb:fixed pb:left-[50%] pb:top-[50%] pb:z-50 pb:grid pb:w-auto pb:max-w-[90vw] pb:translate-x-[-50%] pb:translate-y-[-50%] pb:gap-4 pb:border pb:border-gray-100 pb:bg-white pb:p-6 pb:shadow-2xl pb:duration-200 data-[state=open]:pb:animate-in data-[state=closed]:pb:animate-out data-[state=closed]:pb:fade-out-0 data-[state=open]:pb:fade-in-0 data-[state=closed]:pb:zoom-out-95 data-[state=open]:pb:zoom-in-95 data-[state=closed]:pb:slide-out-to-left-1/2 data-[state=closed]:pb:slide-out-to-top-[48%] data-[state=open]:pb:slide-in-from-left-1/2 data-[state=open]:pb:slide-in-from-top-[48%] pb:rounded-2xl",
38
38
  className,
39
39
  )}
40
40
  >
@@ -0,0 +1,57 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { EmptyState } from "./EmptyState";
3
+
4
+ const meta: Meta<typeof EmptyState> = {
5
+ title: "Feedback/EmptyState",
6
+ component: EmptyState,
7
+ };
8
+
9
+ export default meta;
10
+ type Story = StoryObj<typeof EmptyState>;
11
+
12
+ export const Default: Story = {
13
+ args: {
14
+ title: "No Items Found",
15
+ description: "You haven't created any items yet. Get started by creating your first item.",
16
+ icon: "document",
17
+ primaryAction: {
18
+ label: "Create Item",
19
+ icon: "plus",
20
+ onClick: () => alert("Create clicked"),
21
+ },
22
+ },
23
+ };
24
+
25
+ export const Variations: Story = {
26
+ render: () => (
27
+ <div className="pb:flex pb:flex-col pb:gap-8 pb:p-4">
28
+ <div>
29
+ <span className="pb:text-xs pb:font-bold pb:text-gray-500 pb:block pb:mb-2">With Dual Actions</span>
30
+ <EmptyState
31
+ title="No Content Collections"
32
+ description="You can create and save interactions or acquire content from the marketplace."
33
+ icon="squares"
34
+ primaryAction={{
35
+ label: "Create Interactions",
36
+ variant: "primary",
37
+ icon: "plus",
38
+ onClick: () => {},
39
+ }}
40
+ secondaryAction={{
41
+ label: "Browse Marketplace",
42
+ variant: "secondary",
43
+ icon: "externalLink",
44
+ onClick: () => {},
45
+ }}
46
+ />
47
+ </div>
48
+ <div>
49
+ <span className="pb:text-xs pb:font-bold pb:text-gray-500 pb:block pb:mb-2">Simple Text Only (No Icon, No Actions)</span>
50
+ <EmptyState
51
+ title="Nothing to display"
52
+ description="Your search did not match any templates."
53
+ />
54
+ </div>
55
+ </div>
56
+ ),
57
+ };
@@ -0,0 +1,123 @@
1
+ import React from "react";
2
+ import { Button } from "../forms/Button";
3
+ import { Icon, IconProps } from "../foundations/Icon";
4
+ import { Heading, Text } from "../foundations/Typography";
5
+ import { cn } from "../utils";
6
+ import { ComponentColor, FillVariant } from "../types";
7
+
8
+ export interface EmptyStateAction {
9
+ label: React.ReactNode;
10
+ onClick?: () => void;
11
+ render?: React.ReactElement;
12
+ variant?: FillVariant;
13
+ color?: ComponentColor;
14
+ icon?: IconProps["name"] | React.ReactNode;
15
+ }
16
+
17
+ export interface EmptyStateProps {
18
+ title: React.ReactNode;
19
+ description?: React.ReactNode;
20
+ icon?: IconProps["name"] | React.ReactNode;
21
+ primaryAction?: EmptyStateAction;
22
+ secondaryAction?: EmptyStateAction;
23
+ children?: React.ReactNode;
24
+ className?: string;
25
+ }
26
+
27
+ export function EmptyState({
28
+ title,
29
+ description,
30
+ icon,
31
+ primaryAction,
32
+ secondaryAction,
33
+ children,
34
+ className,
35
+ }: EmptyStateProps) {
36
+ const renderIcon = () => {
37
+ if (!icon) return null;
38
+ if (typeof icon === "string") {
39
+ return (
40
+ <Icon
41
+ name={icon as IconProps["name"]}
42
+ className="pb:w-16 pb:h-16 pb:mx-auto pb:text-gray-400 pb:mb-4"
43
+ />
44
+ );
45
+ }
46
+ return <div className="pb:flex pb:justify-center pb:mb-4">{icon}</div>;
47
+ };
48
+
49
+ return (
50
+ <div
51
+ className={cn(
52
+ "pb:flex pb:flex-col pb:items-center pb:justify-center pb:p-8 pb:text-center pb:rounded-2xl pb:border pb:border-dashed pb:border-gray-200 pb:bg-white/50",
53
+ className,
54
+ )}
55
+ >
56
+ {renderIcon()}
57
+
58
+ {typeof title === "string" ? (
59
+ <Heading
60
+ level={3}
61
+ className="pb:font-bold pb:text-gray-800 pb:mb-2"
62
+ >
63
+ {title}
64
+ </Heading>
65
+ ) : (
66
+ title
67
+ )}
68
+
69
+ {description &&
70
+ (typeof description === "string" ? (
71
+ <Text
72
+ color="muted"
73
+ className="pb:max-w-md pb:mb-6"
74
+ >
75
+ {description}
76
+ </Text>
77
+ ) : (
78
+ <div className="pb:text-gray-600 pb:max-w-md pb:mb-6">{description}</div>
79
+ ))}
80
+
81
+ {(primaryAction || secondaryAction) && (
82
+ <div className="pb:flex pb:flex-wrap pb:gap-3 pb:justify-center pb:items-center">
83
+ {primaryAction && (
84
+ <Button
85
+ variant={primaryAction.variant || "solid"}
86
+ color={primaryAction.color || "primary"}
87
+ onClick={primaryAction.onClick}
88
+ render={primaryAction.render}
89
+ leftIcon={
90
+ typeof primaryAction.icon === "string" ? (
91
+ <Icon name={primaryAction.icon as IconProps["name"]} />
92
+ ) : (
93
+ primaryAction.icon
94
+ )
95
+ }
96
+ >
97
+ {primaryAction.label}
98
+ </Button>
99
+ )}
100
+ {secondaryAction && (
101
+ <Button
102
+ variant={secondaryAction.variant || "solid"}
103
+ color={secondaryAction.color || "neutral"}
104
+ onClick={secondaryAction.onClick}
105
+ render={secondaryAction.render}
106
+ leftIcon={
107
+ typeof secondaryAction.icon === "string" ? (
108
+ <Icon name={secondaryAction.icon as IconProps["name"]} />
109
+ ) : (
110
+ secondaryAction.icon
111
+ )
112
+ }
113
+ >
114
+ {secondaryAction.label}
115
+ </Button>
116
+ )}
117
+ </div>
118
+ )}
119
+
120
+ {children}
121
+ </div>
122
+ );
123
+ }
@@ -0,0 +1,130 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Popover } from "./Popover";
3
+ import { Button } from "../forms/Button";
4
+ import { Input } from "../forms/Input";
5
+ import { Text } from "../foundations/Typography";
6
+ import { Icon } from "../foundations/Icon";
7
+ import React from "react";
8
+
9
+ const meta: Meta<typeof Popover> = {
10
+ title: "Feedback/Popover",
11
+ component: Popover,
12
+ parameters: {
13
+ layout: "centered",
14
+ },
15
+ tags: ["autodocs"],
16
+ argTypes: {
17
+ side: {
18
+ control: "select",
19
+ options: ["top", "bottom", "left", "right"],
20
+ },
21
+ align: {
22
+ control: "select",
23
+ options: ["start", "center", "end"],
24
+ },
25
+ variant: {
26
+ control: "select",
27
+ options: ["surface", "glass", "outline", "flat"],
28
+ },
29
+ },
30
+ };
31
+
32
+ export default meta;
33
+ type Story = StoryObj<typeof Popover>;
34
+
35
+ export const Default: Story = {
36
+ args: {
37
+ title: "Quick Settings",
38
+ trigger: <Button color="teal">Open Popover</Button>,
39
+ children: (
40
+ <div className="pb:space-y-3 pb:py-1">
41
+ <Text size="xs" color="muted">
42
+ Adjust the active robot connection stream rate.
43
+ </Text>
44
+ <Input placeholder="Rate (Hz)" defaultValue="60" />
45
+ <Button size="sm" color="teal" className="pb:w-full">
46
+ Save Preset
47
+ </Button>
48
+ </div>
49
+ ),
50
+ side: "bottom",
51
+ align: "center",
52
+ },
53
+ };
54
+
55
+ export const ExportActionsPopover: Story = {
56
+ render: () => (
57
+ <Popover
58
+ title="Export CAD Configuration"
59
+ trigger={
60
+ <Button color="neutral" leftIcon={<Icon name="arrowDownTray" />}>
61
+ Export
62
+ </Button>
63
+ }
64
+ side="bottom"
65
+ align="end"
66
+ >
67
+ <div className="pb:flex pb:flex-col pb:gap-2 pb:w-64 pb:py-1">
68
+ <button
69
+ type="button"
70
+ className="pb:flex pb:items-center pb:gap-2.5 pb:p-2 pb:rounded-xl pb:hover:bg-gray-100 pb:text-left pb:cursor-pointer pb:transition-colors"
71
+ >
72
+ <div className="pb:w-7 pb:h-7 pb:rounded-lg pb:bg-peerbots-teal/15 pb:text-peerbots-darkteal pb:flex pb:items-center pb:justify-center">
73
+ <Icon name="code" className="pb:w-4 pb:h-4" />
74
+ </div>
75
+ <div>
76
+ <div className="pb:text-xs pb:font-bold pb:text-gray-900">Download JSON</div>
77
+ <div className="pb:text-[10px] pb:text-gray-500">Raw parameter payload</div>
78
+ </div>
79
+ </button>
80
+
81
+ <button
82
+ type="button"
83
+ className="pb:flex pb:items-center pb:gap-2.5 pb:p-2 pb:rounded-xl pb:hover:bg-gray-100 pb:text-left pb:cursor-pointer pb:transition-colors"
84
+ >
85
+ <div className="pb:w-7 pb:h-7 pb:rounded-lg pb:bg-peerbots-pink/15 pb:text-peerbots-pink pb:flex pb:items-center pb:justify-center">
86
+ <Icon name="cube" className="pb:w-4 pb:h-4" />
87
+ </div>
88
+ <div>
89
+ <div className="pb:text-xs pb:font-bold pb:text-gray-900">Export 3MF / STL</div>
90
+ <div className="pb:text-[10px] pb:text-gray-500">3D printable mesh parts</div>
91
+ </div>
92
+ </button>
93
+ </div>
94
+ </Popover>
95
+ ),
96
+ };
97
+
98
+ export const GlassPopoverInContext: Story = {
99
+ render: () => (
100
+ <div className="pb:relative pb:w-[500px] pb:h-72 pb:rounded-3xl pb:bg-gradient-to-br pb:from-slate-900 pb:via-indigo-950 pb:to-teal-950 pb:flex pb:items-center pb:justify-center pb:overflow-hidden pb:p-6">
101
+ <div className="pb:text-center pb:text-white/60 pb:text-xs pb:space-y-1">
102
+ <div>3D Robot Viewport Stage</div>
103
+ </div>
104
+
105
+ <div className="pb:absolute pb:top-6 pb:right-6">
106
+ <Popover
107
+ title="Viewport HUD"
108
+ variant="glass"
109
+ trigger={
110
+ <Button variant="outline" color="teal" size="sm">
111
+ Camera HUD
112
+ </Button>
113
+ }
114
+ side="bottom"
115
+ align="end"
116
+ >
117
+ <div className="pb:space-y-2 pb:w-56 pb:py-1">
118
+ <Text size="xs" color="muted">
119
+ Translucent glass popover floating over 3D canvas.
120
+ </Text>
121
+ <div className="pb:flex pb:gap-2">
122
+ <Button size="xs" color="teal" className="pb:flex-1">Reset</Button>
123
+ <Button size="xs" color="neutral" className="pb:flex-1">Snap</Button>
124
+ </div>
125
+ </div>
126
+ </Popover>
127
+ </div>
128
+ </div>
129
+ ),
130
+ };