system-one-design-system 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.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # Initial Design System Components
2
+
3
+ This package contains initial React design-system components with tests.
4
+
5
+ ## Implemented
6
+
7
+ - `Button` component (React + TypeScript)
8
+ - Variants: `primary`, `outline`, `plain`
9
+ - Sizes: `xs`, `sm`, `base`, `lg`, `xl`
10
+ - Icon positions: `left`, `right`, `icon-only`
11
+ - Disabled behavior and keyboard focus styles
12
+
13
+ ## Testing
14
+
15
+ - Test stack: `Vitest` + `@testing-library/react` + `jsdom`
16
+ - Run tests:
17
+
18
+ ```bash
19
+ npm install
20
+ npm test
21
+ ```
22
+
23
+ ## Notes
24
+
25
+ - This is an initial implementation based on extracted Figma structure and the existing token architecture.
26
+ - Tailwind utility class names are used directly in component class strings; integrate with your app's Tailwind config/preset from `design-tokens` for full token fidelity.
package/index.html ADDED
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>System One Design System</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "system-one-design-system",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview",
9
+ "test": "vitest run",
10
+ "test:watch": "vitest"
11
+ },
12
+ "dependencies": {
13
+ "react": "^19.1.0",
14
+ "react-dom": "^19.1.0"
15
+ },
16
+ "devDependencies": {
17
+ "@testing-library/jest-dom": "^6.6.3",
18
+ "@testing-library/react": "^16.2.0",
19
+ "@testing-library/user-event": "^14.6.1",
20
+ "@types/react": "^19.1.2",
21
+ "@types/react-dom": "^19.1.2",
22
+ "jsdom": "^26.1.0",
23
+ "typescript": "^5.8.3",
24
+ "vite": "^6.3.5",
25
+ "@vitejs/plugin-react": "^4.3.4",
26
+ "vitest": "^3.1.1"
27
+ }
28
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,48 @@
1
+ import { Button } from "./components/Button";
2
+
3
+ const variants = ["primary", "outline", "plain"] as const;
4
+ const sizes = ["xs", "sm", "base", "lg", "xl"] as const;
5
+
6
+ function DotIcon() {
7
+ return (
8
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
9
+ <circle cx="8" cy="8" r="6" fill="currentColor" />
10
+ </svg>
11
+ );
12
+ }
13
+
14
+ export default function App() {
15
+ return (
16
+ <main style={{ padding: 24, fontFamily: "Inter, system-ui, sans-serif" }}>
17
+ <h1 style={{ marginBottom: 8 }}>System One Buttons</h1>
18
+ <p style={{ marginTop: 0, marginBottom: 24, color: "#4A5565" }}>
19
+ Initial React implementation aligned to the Figma button matrix.
20
+ </p>
21
+
22
+ {variants.map((variant) => (
23
+ <section key={variant} style={{ marginBottom: 20 }}>
24
+ <h2 style={{ fontSize: 16, marginBottom: 12, textTransform: "capitalize" }}>{variant}</h2>
25
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 10, marginBottom: 10 }}>
26
+ {sizes.map((size) => (
27
+ <Button key={`${variant}-${size}`} variant={variant} size={size}>
28
+ {variant} {size}
29
+ </Button>
30
+ ))}
31
+ </div>
32
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 10 }}>
33
+ <Button variant={variant} size="base" icon={<DotIcon />} iconPosition="left">
34
+ Left icon
35
+ </Button>
36
+ <Button variant={variant} size="base" icon={<DotIcon />} iconPosition="right">
37
+ Right icon
38
+ </Button>
39
+ <Button variant={variant} size="base" icon={<DotIcon />} iconPosition="icon-only" aria-label="Icon only" />
40
+ <Button variant={variant} size="base" disabled>
41
+ Disabled
42
+ </Button>
43
+ </div>
44
+ </section>
45
+ ))}
46
+ </main>
47
+ );
48
+ }
@@ -0,0 +1,48 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import { Button } from "./Button";
4
+
5
+ function TestIcon() {
6
+ return <svg data-testid="test-icon" width="16" height="16" />;
7
+ }
8
+
9
+ describe("Button", () => {
10
+ it("renders label text", () => {
11
+ render(<Button>Click me</Button>);
12
+ expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
13
+ });
14
+
15
+ it("supports variant classes", () => {
16
+ render(<Button variant="outline">Outline</Button>);
17
+ const button = screen.getByRole("button", { name: "Outline" });
18
+ expect(button.className).toContain("border-brand-primary-600");
19
+ });
20
+
21
+ it("supports left icon", () => {
22
+ render(
23
+ <Button icon={<TestIcon />} iconPosition="left">
24
+ Save
25
+ </Button>
26
+ );
27
+ expect(screen.getByTestId("test-icon")).toBeInTheDocument();
28
+ expect(screen.getByRole("button", { name: "Save" })).toBeInTheDocument();
29
+ });
30
+
31
+ it("supports icon-only mode", () => {
32
+ render(<Button icon={<TestIcon />} iconPosition="icon-only" aria-label="Icon Action" />);
33
+ expect(screen.getByRole("button", { name: "Icon Action" })).toBeInTheDocument();
34
+ expect(screen.getByTestId("test-icon")).toBeInTheDocument();
35
+ });
36
+
37
+ it("does not call onClick when disabled", async () => {
38
+ const user = userEvent.setup();
39
+ const onClick = vi.fn();
40
+ render(
41
+ <Button disabled onClick={onClick}>
42
+ Disabled
43
+ </Button>
44
+ );
45
+ await user.click(screen.getByRole("button", { name: "Disabled" }));
46
+ expect(onClick).not.toHaveBeenCalled();
47
+ });
48
+ });
@@ -0,0 +1,114 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/cn";
3
+
4
+ export type ButtonVariant = "primary" | "outline" | "plain";
5
+ export type ButtonSize = "xs" | "sm" | "base" | "lg" | "xl";
6
+ export type IconPosition = "left" | "right" | "icon-only";
7
+
8
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
9
+ variant?: ButtonVariant;
10
+ size?: ButtonSize;
11
+ icon?: React.ReactNode;
12
+ iconPosition?: IconPosition;
13
+ }
14
+
15
+ const sizeClasses: Record<ButtonSize, string> = {
16
+ xs: "h-8 px-3 text-xs gap-1.5 rounded-sm",
17
+ sm: "h-9 px-3.5 text-sm gap-2 rounded-default",
18
+ base: "h-10 px-4 text-sm gap-2 rounded-default",
19
+ lg: "h-11 px-5 text-base gap-2.5 rounded-md",
20
+ xl: "h-12 px-6 text-base gap-2.5 rounded-md"
21
+ };
22
+
23
+ const iconOnlySizeClasses: Record<ButtonSize, string> = {
24
+ xs: "h-8 w-8 rounded-sm",
25
+ sm: "h-9 w-9 rounded-default",
26
+ base: "h-10 w-10 rounded-default",
27
+ lg: "h-11 w-11 rounded-md",
28
+ xl: "h-12 w-12 rounded-md"
29
+ };
30
+
31
+ const variantClasses: Record<ButtonVariant, string> = {
32
+ primary:
33
+ "bg-brand-primary-600 text-white border border-brand-primary-600 hover:bg-brand-primary-700 active:bg-brand-primary-800 disabled:bg-brand-primary-100 disabled:text-brand-primary-400 disabled:border-brand-primary-100",
34
+ outline:
35
+ "bg-transparent text-brand-primary-600 border border-brand-primary-600 hover:bg-brand-primary-100 active:bg-brand-primary-200 disabled:text-brand-primary-300 disabled:border-brand-primary-200",
36
+ plain:
37
+ "bg-transparent text-brand-primary-600 border border-transparent hover:bg-brand-primary-100 active:bg-brand-primary-200 disabled:text-brand-primary-300"
38
+ };
39
+
40
+ const sizeStyles: Record<ButtonSize, React.CSSProperties> = {
41
+ xs: { height: 32, padding: "0 12px", fontSize: 12, borderRadius: 4, gap: 6 },
42
+ sm: { height: 36, padding: "0 14px", fontSize: 14, borderRadius: 6, gap: 8 },
43
+ base: { height: 40, padding: "0 16px", fontSize: 14, borderRadius: 6, gap: 8 },
44
+ lg: { height: 44, padding: "0 20px", fontSize: 16, borderRadius: 8, gap: 10 },
45
+ xl: { height: 48, padding: "0 24px", fontSize: 16, borderRadius: 8, gap: 10 }
46
+ };
47
+
48
+ const iconOnlySizeStyles: Record<ButtonSize, React.CSSProperties> = {
49
+ xs: { height: 32, width: 32, borderRadius: 2 },
50
+ sm: { height: 36, width: 36, borderRadius: 4 },
51
+ base: { height: 40, width: 40, borderRadius: 4 },
52
+ lg: { height: 44, width: 44, borderRadius: 6 },
53
+ xl: { height: 48, width: 48, borderRadius: 6 }
54
+ };
55
+
56
+ const variantStyles: Record<ButtonVariant, React.CSSProperties> = {
57
+ primary: { background: "#0A6EE7", color: "#FFFFFF", border: "1px solid #0A6EE7" },
58
+ outline: { background: "transparent", color: "#0A6EE7", border: "1px solid #0A6EE7" },
59
+ plain: { background: "transparent", color: "#0A6EE7", border: "1px solid transparent" }
60
+ };
61
+
62
+ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
63
+ {
64
+ className,
65
+ variant = "primary",
66
+ size = "base",
67
+ icon,
68
+ iconPosition = "left",
69
+ children,
70
+ disabled,
71
+ type = "button",
72
+ style,
73
+ ...props
74
+ },
75
+ ref
76
+ ) {
77
+ const iconOnly = iconPosition === "icon-only";
78
+ const baseStyle: React.CSSProperties = {
79
+ display: "inline-flex",
80
+ alignItems: "center",
81
+ justifyContent: "center",
82
+ cursor: disabled ? "not-allowed" : "pointer",
83
+ opacity: disabled ? 0.55 : 1,
84
+ fontWeight: 500,
85
+ transition: "all 120ms ease-in-out"
86
+ };
87
+ const resolvedStyle = {
88
+ ...baseStyle,
89
+ ...variantStyles[variant],
90
+ ...(iconOnly ? iconOnlySizeStyles[size] : sizeStyles[size]),
91
+ ...style
92
+ } satisfies React.CSSProperties;
93
+
94
+ return (
95
+ <button
96
+ ref={ref}
97
+ type={type}
98
+ disabled={disabled}
99
+ className={cn(
100
+ "inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary-400 focus-visible:ring-offset-2 disabled:cursor-not-allowed",
101
+ variantClasses[variant],
102
+ iconOnly ? iconOnlySizeClasses[size] : sizeClasses[size],
103
+ className
104
+ )}
105
+ style={resolvedStyle}
106
+ {...props}
107
+ >
108
+ {icon && iconPosition === "left" ? <span aria-hidden="true">{icon}</span> : null}
109
+ {!iconOnly ? <span>{children}</span> : null}
110
+ {icon && iconPosition === "right" ? <span aria-hidden="true">{icon}</span> : null}
111
+ {icon && iconOnly ? <span aria-hidden="true">{icon}</span> : null}
112
+ </button>
113
+ );
114
+ });
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Button } from "./components/Button";
2
+ export type { ButtonProps, ButtonSize, ButtonVariant, IconPosition } from "./components/Button";
package/src/lib/cn.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function cn(...classes: Array<string | undefined | false | null>): string {
2
+ return classes.filter(Boolean).join(" ");
3
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import App from "./App";
4
+
5
+ createRoot(document.getElementById("root")!).render(
6
+ <React.StrictMode>
7
+ <App />
8
+ </React.StrictMode>
9
+ );
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom/vitest";
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "types": ["vitest/globals", "@testing-library/jest-dom"],
10
+ "baseUrl": ".",
11
+ "paths": {
12
+ "@/*": ["src/*"]
13
+ }
14
+ },
15
+ "include": ["src", "vitest.config.ts"]
16
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()]
6
+ });
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "jsdom",
6
+ globals: true,
7
+ setupFiles: "./src/test/setup.ts"
8
+ }
9
+ });