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 +26 -0
- package/index.html +12 -0
- package/package.json +28 -0
- package/src/App.tsx +48 -0
- package/src/components/Button.test.tsx +48 -0
- package/src/components/Button.tsx +114 -0
- package/src/index.ts +2 -0
- package/src/lib/cn.ts +3 -0
- package/src/main.tsx +9 -0
- package/src/test/setup.ts +1 -0
- package/tsconfig.json +16 -0
- package/vite.config.ts +6 -0
- package/vitest.config.ts +9 -0
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
package/src/lib/cn.ts
ADDED
package/src/main.tsx
ADDED
|
@@ -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