@peerbots/core 0.1.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 (81) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/publish.yml +42 -0
  4. package/.github/workflows/storybook.yml +46 -0
  5. package/.storybook/main.ts +28 -0
  6. package/.storybook/preview.ts +22 -0
  7. package/README.md +9 -0
  8. package/dist/index.css +1 -0
  9. package/dist/index.d.mts +704 -0
  10. package/dist/index.d.ts +704 -0
  11. package/dist/index.js +5 -0
  12. package/dist/index.mjs +5 -0
  13. package/package.json +60 -0
  14. package/src/charts/DistributionBarChart.stories.tsx +41 -0
  15. package/src/charts/DistributionBarChart.tsx +170 -0
  16. package/src/charts/DistributionHistogram.stories.tsx +56 -0
  17. package/src/charts/DistributionHistogram.tsx +193 -0
  18. package/src/charts/index.ts +10 -0
  19. package/src/global.d.ts +1 -0
  20. package/src/helpers/SEO.tsx +41 -0
  21. package/src/index.ts +6 -0
  22. package/src/styles/theme.css +60 -0
  23. package/src/ui/Alert.stories.tsx +41 -0
  24. package/src/ui/Alert.tsx +72 -0
  25. package/src/ui/Anchor.stories.tsx +25 -0
  26. package/src/ui/Anchor.tsx +32 -0
  27. package/src/ui/AuthFormUI.stories.tsx +67 -0
  28. package/src/ui/AuthFormUI.tsx +217 -0
  29. package/src/ui/BasePanel.stories.tsx +36 -0
  30. package/src/ui/BasePanel.tsx +59 -0
  31. package/src/ui/Button.stories.tsx +108 -0
  32. package/src/ui/Button.tsx +121 -0
  33. package/src/ui/Checkbox.stories.tsx +61 -0
  34. package/src/ui/Checkbox.tsx +45 -0
  35. package/src/ui/Collapsible.stories.tsx +91 -0
  36. package/src/ui/Collapsible.tsx +52 -0
  37. package/src/ui/Colors.stories.tsx +67 -0
  38. package/src/ui/Dialog.stories.tsx +29 -0
  39. package/src/ui/Dialog.tsx +56 -0
  40. package/src/ui/Dropdown.tsx +66 -0
  41. package/src/ui/Field.stories.tsx +181 -0
  42. package/src/ui/Field.tsx +108 -0
  43. package/src/ui/Icon.stories.tsx +192 -0
  44. package/src/ui/Icon.tsx +42 -0
  45. package/src/ui/IconRegistry.tsx +189 -0
  46. package/src/ui/Input.stories.tsx +67 -0
  47. package/src/ui/Input.tsx +43 -0
  48. package/src/ui/Label.stories.tsx +42 -0
  49. package/src/ui/Label.tsx +26 -0
  50. package/src/ui/NumberField.stories.tsx +86 -0
  51. package/src/ui/NumberField.tsx +116 -0
  52. package/src/ui/Popover.tsx +42 -0
  53. package/src/ui/Select.stories.tsx +74 -0
  54. package/src/ui/Select.tsx +122 -0
  55. package/src/ui/Separator.stories.tsx +61 -0
  56. package/src/ui/Separator.tsx +28 -0
  57. package/src/ui/SettingsPanel.stories.tsx +83 -0
  58. package/src/ui/SettingsPanel.tsx +81 -0
  59. package/src/ui/Skeleton.stories.tsx +43 -0
  60. package/src/ui/Skeleton.tsx +15 -0
  61. package/src/ui/Slider.stories.tsx +140 -0
  62. package/src/ui/Slider.tsx +95 -0
  63. package/src/ui/SliderWithNumberField.stories.tsx +101 -0
  64. package/src/ui/SliderWithNumberField.tsx +88 -0
  65. package/src/ui/Switch.stories.tsx +81 -0
  66. package/src/ui/Switch.tsx +60 -0
  67. package/src/ui/TabRadio.stories.tsx +153 -0
  68. package/src/ui/TabRadio.tsx +68 -0
  69. package/src/ui/TabSelection.stories.tsx +44 -0
  70. package/src/ui/TabSelection.tsx +91 -0
  71. package/src/ui/TextArea.stories.tsx +64 -0
  72. package/src/ui/TextArea.tsx +24 -0
  73. package/src/ui/Tooltip.stories.tsx +84 -0
  74. package/src/ui/Tooltip.tsx +61 -0
  75. package/src/ui/Typography.stories.tsx +87 -0
  76. package/src/ui/Typography.tsx +80 -0
  77. package/src/ui/index.ts +28 -0
  78. package/src/ui/utils.ts +6 -0
  79. package/tsconfig.json +12 -0
  80. package/vitest.config.ts +36 -0
  81. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,193 @@
1
+ import {
2
+ VictoryChart,
3
+ VictoryAxis,
4
+ VictoryLabel,
5
+ VictoryLine,
6
+ VictoryHistogram,
7
+ VictoryTooltip,
8
+ VictoryVoronoiContainer,
9
+ } from "victory";
10
+
11
+ export interface DistributionHistogramData {
12
+ x: number;
13
+ text: string;
14
+ }
15
+
16
+ export interface DistributionHistogramProps {
17
+ data: DistributionHistogramData[];
18
+ alt: string;
19
+ label: string;
20
+ average?: number;
21
+ referenceLineValue?: number;
22
+ referenceLineLabel?: string;
23
+ chartWidth?: number;
24
+ chartHeight?: number;
25
+ }
26
+
27
+ export function DistributionHistogram({
28
+ data,
29
+ alt,
30
+ label,
31
+ average,
32
+ referenceLineValue,
33
+ referenceLineLabel = "Reference",
34
+ chartWidth = 600,
35
+ chartHeight = 400,
36
+ }: DistributionHistogramProps) {
37
+ if (data.length === 0) return null;
38
+
39
+ const binSize = 10;
40
+ const maxLength = Math.max(
41
+ ...data.map((r) => r.x),
42
+ referenceLineValue ?? 0,
43
+ binSize,
44
+ );
45
+ const maxX = Math.ceil(maxLength / binSize) * binSize + binSize;
46
+
47
+ const boundaries = [];
48
+ for (let i = 0; i <= maxX; i += binSize) {
49
+ boundaries.push(i);
50
+ }
51
+
52
+ const padding = { left: 60, right: 30, top: 20, bottom: 70 };
53
+
54
+ return (
55
+ <div className="mb-6" role="img" aria-label={alt}>
56
+ <label className="text-xs uppercase font-bold text-gray-400 block mb-2 px-1">
57
+ {label}
58
+ </label>
59
+ <VictoryChart
60
+ width={chartWidth}
61
+ height={chartHeight}
62
+ padding={padding}
63
+ containerComponent={<VictoryVoronoiContainer />}
64
+ >
65
+ <VictoryAxis
66
+ label="Length"
67
+ tickValues={boundaries}
68
+ tickFormat={(x: number | string) =>
69
+ typeof x === "number" ? Math.round(x) : x
70
+ }
71
+ style={{
72
+ axis: { stroke: "#cbd5e1" },
73
+ axisLabel: {
74
+ fontSize: 18,
75
+ padding: 45,
76
+ fill: "#334155",
77
+ fontWeight: "bold",
78
+ },
79
+ tickLabels: {
80
+ fontSize: 18,
81
+ padding: 5,
82
+ fill: "#334155",
83
+ angle: 45,
84
+ textAnchor: "start",
85
+ },
86
+ }}
87
+ />
88
+ <VictoryAxis
89
+ dependentAxis
90
+ label="Count"
91
+ tickFormat={(x: number | string) =>
92
+ typeof x === "number" ? Math.round(x) : x
93
+ }
94
+ style={{
95
+ axis: { stroke: "#cbd5e1" },
96
+ axisLabel: {
97
+ fontSize: 18,
98
+ padding: 40,
99
+ fill: "#334155",
100
+ fontWeight: "bold",
101
+ },
102
+ tickLabels: {
103
+ fontSize: 18,
104
+ padding: 5,
105
+ fill: "#334155",
106
+ },
107
+ grid: { stroke: "#f1f5f9" },
108
+ }}
109
+ />
110
+ <VictoryHistogram
111
+ data={data}
112
+ bins={boundaries}
113
+ x="x"
114
+ labels={({
115
+ datum,
116
+ }: {
117
+ datum?: {
118
+ y: number;
119
+ binnedData?: { text: string }[];
120
+ };
121
+ }) => {
122
+ if (!datum || datum.y === 0 || !datum.binnedData) return "";
123
+ const items = datum.binnedData.map((d: { text: string }) => d.text);
124
+ const displayItems = items.slice(0, 5);
125
+ let labelText = displayItems.join("\n");
126
+ if (items.length > 5)
127
+ labelText += `\n...and ${items.length - 5} more`;
128
+ return labelText;
129
+ }}
130
+ labelComponent={
131
+ <VictoryTooltip
132
+ style={{ fontSize: 18 }}
133
+ flyoutStyle={{ fill: "white", stroke: "#cbd5e1" }}
134
+ pointerLength={5}
135
+ cornerRadius={2}
136
+ constrainToVisibleArea
137
+ />
138
+ }
139
+ style={{
140
+ data: {
141
+ fill: "#46d9d9",
142
+ stroke: "#fff",
143
+ strokeWidth: 1,
144
+ cursor: "pointer",
145
+ },
146
+ }}
147
+ />
148
+ {average !== undefined && (
149
+ <VictoryLine
150
+ x={() => average}
151
+ style={{
152
+ data: {
153
+ stroke: "#9ca3af",
154
+ strokeWidth: 2,
155
+ strokeDasharray: "4, 4",
156
+ },
157
+ }}
158
+ labels={["Average"]}
159
+ labelComponent={
160
+ <VictoryLabel
161
+ y={45}
162
+ style={{ fill: "#475569", fontSize: 18, fontWeight: "bold" }}
163
+ backgroundStyle={{ fill: "white" }}
164
+ backgroundPadding={12}
165
+ />
166
+ }
167
+ />
168
+ )}
169
+ {referenceLineValue !== undefined && (
170
+ <VictoryLine
171
+ x={() => referenceLineValue}
172
+ labels={[referenceLineLabel]}
173
+ labelComponent={
174
+ <VictoryLabel
175
+ y={45}
176
+ style={{ fill: "#ef4444", fontSize: 18, fontWeight: "bold" }}
177
+ backgroundStyle={{ fill: "white" }}
178
+ backgroundPadding={12}
179
+ />
180
+ }
181
+ style={{
182
+ data: {
183
+ stroke: "#ef4444",
184
+ strokeWidth: 2,
185
+ strokeDasharray: "4, 4",
186
+ },
187
+ }}
188
+ />
189
+ )}
190
+ </VictoryChart>
191
+ </div>
192
+ );
193
+ }
@@ -0,0 +1,10 @@
1
+ export { DistributionBarChart } from "./DistributionBarChart";
2
+ export type {
3
+ DistributionBarChartProps,
4
+ DistributionBarData,
5
+ } from "./DistributionBarChart";
6
+ export { DistributionHistogram } from "./DistributionHistogram";
7
+ export type {
8
+ DistributionHistogramProps,
9
+ DistributionHistogramData,
10
+ } from "./DistributionHistogram";
@@ -0,0 +1 @@
1
+ declare module "*.css" {}
@@ -0,0 +1,41 @@
1
+ interface SEOProps {
2
+ title: string;
3
+ description?: string;
4
+ image?: string;
5
+ url?: string;
6
+ type?: string;
7
+ }
8
+
9
+ export function SEO({
10
+ title,
11
+ description = "Peerbots platform: social robots for everyone, powered by experts.",
12
+ image = "/peerbots-logo.png", // Default image if one exists, customizable
13
+ url,
14
+ type = "website",
15
+ }: SEOProps) {
16
+ const siteTitle = "Peerbots App";
17
+ const fullTitle = title === "Home" ? siteTitle : `${title} | ${siteTitle}`;
18
+ const currentUrl =
19
+ url || (typeof window !== "undefined" ? window.location.href : "");
20
+
21
+ return (
22
+ <>
23
+ <title>{fullTitle}</title>
24
+ <meta name="description" content={description} />
25
+
26
+ {/* Open Graph */}
27
+ <meta property="og:title" content={fullTitle} />
28
+ <meta property="og:description" content={description} />
29
+ <meta property="og:image" content={image} />
30
+ <meta property="og:url" content={currentUrl} />
31
+ <meta property="og:type" content={type} />
32
+ <meta property="og:site_name" content={siteTitle} />
33
+
34
+ {/* Twitter Card */}
35
+ <meta name="twitter:card" content="summary_large_image" />
36
+ <meta name="twitter:title" content={fullTitle} />
37
+ <meta name="twitter:description" content={description} />
38
+ <meta name="twitter:image" content={image} />
39
+ </>
40
+ );
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ // src/index.ts
2
+ import "./styles/theme.css"; // Ensure CSS is bundled
3
+
4
+ export * from "./ui";
5
+ export * from "./helpers/SEO";
6
+ export * from "./charts";
@@ -0,0 +1,60 @@
1
+ @import "tailwindcss";
2
+
3
+ @theme {
4
+ --color-primary: #46d9d9;
5
+ /* --color-old-dark-primary: #5fc7cc; */
6
+ --color-dark-primary: var(--color-teal-700);
7
+ --color-secondary: #e86e8a;
8
+ --color-accent: #d9e021;
9
+ --color-accent-hc: #c4cc24;
10
+ --color-accent-two: #4273ff;
11
+ --color-accent-two-hc: #516eb5;
12
+ --color-accent-three: #3f8588;
13
+ --color-danger: #e86e8a;
14
+ --color-light-bg: #d8e7eb;
15
+ --sidebar-bg: #f9ffff;
16
+
17
+ --background-image-gradient-radial: radial-gradient(var(--tw-gradient-stops));
18
+ --background-image-gradient-conic: conic-gradient(from 180deg at 50% 50%,
19
+ var(--tw-gradient-stops));
20
+
21
+ --font-sans: "Avenir", "Nunito", sans-serif;
22
+ }
23
+
24
+ @utility input-base {
25
+ @apply bg-primary/10 border border-b-2 border-gray-300 focus:border-gray-600 py-2 px-2;
26
+ }
27
+
28
+ @utility btn-primary {
29
+ @apply bg-primary hover:bg-dark-primary text-gray-900 font-bold py-1 px-2 my-1 mx-2 rounded-md disabled:bg-gray-400 disabled:hover:bg-gray-300 disabled:cursor-not-allowed cursor-pointer;
30
+ }
31
+
32
+ @utility btn-secondary {
33
+ @apply bg-gray-100 hover:bg-gray-200 text-gray-800 font-normal py-1 px-2 my-1 mx-2 rounded-md disabled:bg-gray-400 disabled:hover:bg-gray-300 disabled:cursor-not-allowed cursor-pointer;
34
+ }
35
+
36
+ @utility btn-danger {
37
+ @apply bg-danger hover:opacity-80 text-gray-900 font-bold py-1 px-2 my-1 mx-2 rounded-md disabled:bg-gray-400 disabled:hover:bg-gray-300 disabled:cursor-not-allowed cursor-pointer;
38
+ }
39
+
40
+ @utility range-primary {
41
+ @apply accent-primary bg-gray-100 rounded-md cursor-pointer;
42
+ }
43
+
44
+ .range-vertical {
45
+ writing-mode: vertical-lr;
46
+ }
47
+
48
+ .tooltip,
49
+ .tooltip-slow {
50
+ @apply absolute invisible opacity-0;
51
+ }
52
+
53
+ /* Using siblings so that hovering over the invisible tooltip doesn't trigger the tooltip */
54
+ .has-tooltip:hover+.tooltip {
55
+ @apply visible opacity-100 z-50 transition-opacity delay-300 ease-in-out;
56
+ }
57
+
58
+ .has-tooltip:hover+.tooltip-slow {
59
+ @apply visible opacity-100 z-50 transition-opacity delay-1000 ease-in-out;
60
+ }
@@ -0,0 +1,41 @@
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="flex flex-col gap-4 w-[600px] bg-gray-100 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
+ };
@@ -0,0 +1,72 @@
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
+ "flex w-full p-4 m-1 text-sm rounded-lg bg-white border border-solid transition-opacity ease-in-out delay-150 duration-300 " +
24
+ (showAlert ? " " : " hidden")
25
+ }
26
+ >
27
+ <span>
28
+ {level === "Error" && (
29
+ <Icon
30
+ name="exclamationCircle"
31
+ size="lg"
32
+ className="m-2"
33
+ stroke="red"
34
+ />
35
+ )}
36
+ {level === "Warning" && (
37
+ <Icon
38
+ name="exclamationTriangle"
39
+ size="lg"
40
+ className="m-2"
41
+ stroke="yellow"
42
+ />
43
+ )}
44
+ {level === "Success" && (
45
+ <Icon name="checkCircle" size="lg" className="m-2" stroke="green" />
46
+ )}
47
+ {level === "Info" && (
48
+ <Icon name="megaphone" size="lg" className="m-2" />
49
+ )}
50
+ </span>
51
+ <span className="flex flex-1 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="cursor-pointer"
64
+ onClick={() => {
65
+ setShowAlert(false);
66
+ }}
67
+ >
68
+ <Icon name="xCircle" size="lg" className="m-1" />
69
+ </span>
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,25 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Anchor } from "./Anchor";
3
+
4
+ const meta = {
5
+ title: "UI/Anchor",
6
+ component: Anchor,
7
+ parameters: {
8
+ layout: "centered",
9
+ },
10
+ tags: ["autodocs"],
11
+ argTypes: {
12
+ href: { control: "text" },
13
+ children: { control: "text" },
14
+ },
15
+ } satisfies Meta<typeof Anchor>;
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ export const Default: Story = {
21
+ args: {
22
+ href: "https://peerbots.org",
23
+ children: "Visit Peerbots",
24
+ },
25
+ };
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { cn } from "./utils";
4
+
5
+ export type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement>;
6
+
7
+ const Anchor = React.forwardRef<HTMLAnchorElement, AnchorProps>(
8
+ ({ className, href, target, ...props }, ref) => {
9
+ const isInternal = href && !href.startsWith("http") && !target;
10
+ const commonClass = cn(
11
+ "font-medium text-teal-700 underline underline-offset-4 hover:text-teal-900 cursor-pointer",
12
+ className,
13
+ );
14
+
15
+ if (isInternal) {
16
+ return <Link to={href} ref={ref} className={commonClass} {...props} />;
17
+ } else {
18
+ return (
19
+ <a
20
+ href={href}
21
+ target={target}
22
+ ref={ref}
23
+ className={commonClass}
24
+ {...props}
25
+ />
26
+ );
27
+ }
28
+ },
29
+ );
30
+ Anchor.displayName = "Anchor";
31
+
32
+ export { Anchor };
@@ -0,0 +1,67 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { AuthFormUI } from "./AuthFormUI";
3
+ import { useState } from "react";
4
+
5
+ const meta: Meta<typeof AuthFormUI> = {
6
+ title: "UI/AuthFormUI",
7
+ component: AuthFormUI,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ };
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof AuthFormUI>;
16
+
17
+ export const Default: Story = {
18
+ render: function Render() {
19
+ const [mode, setMode] = useState<
20
+ "signing up" | "signing in" | "resetting password"
21
+ >("signing in");
22
+ return (
23
+ <div className="w-[400px]">
24
+ <AuthFormUI
25
+ mode={mode}
26
+ onModeChange={setMode}
27
+ formAction={async () => {
28
+ await new Promise((resolve) => setTimeout(resolve, 1000));
29
+ }}
30
+ actionState={{ error: "", message: "" }}
31
+ onGoogleSignIn={() => alert("Google Sign In clicked")}
32
+ description="Sign in to simplify connecting to robots, and synchronize your work across devices."
33
+ />
34
+ </div>
35
+ );
36
+ },
37
+ };
38
+
39
+ export const Variations: Story = {
40
+ render: () => {
41
+ return (
42
+ <div className="flex flex-col gap-10 w-[400px]">
43
+ <AuthFormUI
44
+ mode="signing up"
45
+ onModeChange={() => {}}
46
+ formAction={async () => {}}
47
+ actionState={{ error: "", message: "" }}
48
+ onGoogleSignIn={() => {}}
49
+ description="Sign up description"
50
+ />
51
+ <AuthFormUI
52
+ mode="resetting password"
53
+ onModeChange={() => {}}
54
+ formAction={async () => {}}
55
+ actionState={{ error: "", message: "Email sent!" }}
56
+ />
57
+ <AuthFormUI
58
+ mode="signing in"
59
+ onModeChange={() => {}}
60
+ formAction={async () => {}}
61
+ actionState={{ error: "Invalid email or password", message: "" }}
62
+ description="Sign in description"
63
+ />
64
+ </div>
65
+ );
66
+ },
67
+ };