@danielthurau/atlas-labs-codex 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 +138 -0
- package/components/react/Badge/Badge.module.css +38 -0
- package/components/react/Badge/Badge.stories.tsx +79 -0
- package/components/react/Badge/Badge.tsx +42 -0
- package/components/react/Badge/index.ts +2 -0
- package/components/react/Button/Button.module.css +118 -0
- package/components/react/Button/Button.stories.tsx +108 -0
- package/components/react/Button/Button.tsx +73 -0
- package/components/react/Button/index.ts +2 -0
- package/components/react/Card/Card.module.css +59 -0
- package/components/react/Card/Card.stories.tsx +114 -0
- package/components/react/Card/Card.tsx +79 -0
- package/components/react/Card/index.ts +11 -0
- package/components/react/Input/Input.module.css +63 -0
- package/components/react/Input/Input.stories.tsx +88 -0
- package/components/react/Input/Input.tsx +50 -0
- package/components/react/Input/index.ts +2 -0
- package/components/react/Modal/Modal.module.css +103 -0
- package/components/react/Modal/Modal.stories.tsx +119 -0
- package/components/react/Modal/Modal.tsx +75 -0
- package/components/react/Modal/index.ts +2 -0
- package/components/react/RefreshButton/RefreshButton.module.css +202 -0
- package/components/react/RefreshButton/RefreshButton.stories.tsx +43 -0
- package/components/react/RefreshButton/RefreshButton.tsx +222 -0
- package/components/react/RefreshButton/index.ts +2 -0
- package/components/react/Tabs/Tabs.module.css +58 -0
- package/components/react/Tabs/Tabs.stories.tsx +101 -0
- package/components/react/Tabs/Tabs.tsx +62 -0
- package/components/react/Tabs/index.ts +2 -0
- package/components/react/Toast/Toast.module.css +145 -0
- package/components/react/Toast/Toast.stories.tsx +143 -0
- package/components/react/Toast/Toast.tsx +123 -0
- package/components/react/Toast/index.ts +2 -0
- package/components/react/index.ts +31 -0
- package/lib/index.ts +7 -0
- package/lib/utils.ts +32 -0
- package/package.json +95 -0
- package/public/fonts/MartianMono-OFL.txt +93 -0
- package/public/fonts/MartianMono-VariableFont_wdth,wght.ttf +0 -0
- package/themes/css/base.css +142 -0
- package/themes/css/theme-dark.css +48 -0
- package/themes/css/theme-light.css +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Personal Design Codex
|
|
2
|
+
|
|
3
|
+
A private repository that acts as a single source of truth for all design-related decisions. This codex centralizes typography rules, color palettes, spacing systems, and UI components in one place.
|
|
4
|
+
|
|
5
|
+
## Philosophy
|
|
6
|
+
|
|
7
|
+
This is a **personal craftsman repository**—a tidy workshop where design decisions are explicit, documented, and reusable by your future self.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Using in Other Projects
|
|
12
|
+
|
|
13
|
+
Install from npm:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @atlas-labs/design-codex
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then import components, styles, and utilities:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
// Import components
|
|
23
|
+
import { Button, Card, Input, Badge, RefreshButton } from '@atlas-labs/design-codex';
|
|
24
|
+
|
|
25
|
+
// Import utilities
|
|
26
|
+
import { formatRelativeTime, clsx } from '@atlas-labs/design-codex/lib';
|
|
27
|
+
|
|
28
|
+
// Import CSS (in your layout.tsx or entry point)
|
|
29
|
+
import '@atlas-labs/design-codex/themes/css/base.css';
|
|
30
|
+
import '@atlas-labs/design-codex/themes/css/theme-light.css';
|
|
31
|
+
import '@atlas-labs/design-codex/themes/css/theme-dark.css';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Font Setup
|
|
35
|
+
|
|
36
|
+
Copy the Martian Mono font to your project's public folder, or load it in your CSS:
|
|
37
|
+
|
|
38
|
+
```css
|
|
39
|
+
@font-face {
|
|
40
|
+
font-family: 'Martian Mono';
|
|
41
|
+
src: url('/fonts/MartianMono-VariableFont_wdth,wght.ttf') format('truetype');
|
|
42
|
+
font-weight: 100 800;
|
|
43
|
+
font-display: swap;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Development
|
|
50
|
+
|
|
51
|
+
### Prerequisites
|
|
52
|
+
|
|
53
|
+
**Node.js 18+** is required. Check your version:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
node --version # Should be v18.x or higher
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
If you need to upgrade, use [nvm](https://github.com/nvm-sh/nvm):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
nvm install 20
|
|
63
|
+
nvm use 20
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Quick Start
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Install dependencies
|
|
70
|
+
npm install
|
|
71
|
+
|
|
72
|
+
# Run the playground
|
|
73
|
+
npm run dev
|
|
74
|
+
|
|
75
|
+
# Run Storybook
|
|
76
|
+
npm run storybook
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Structure
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
├── codex/ # Design documentation
|
|
83
|
+
│ ├── principles.md # Core design principles
|
|
84
|
+
│ ├── typography.md # Type system
|
|
85
|
+
│ ├── color.md # Color strategy
|
|
86
|
+
│ ├── spacing.md # Spacing scale
|
|
87
|
+
│ ├── components.md # Component guidelines
|
|
88
|
+
│ └── patterns/ # UI pattern recipes
|
|
89
|
+
├── themes/css/ # CSS custom properties
|
|
90
|
+
│ ├── base.css # Primitives & component tokens
|
|
91
|
+
│ ├── theme-light.css # Light mode
|
|
92
|
+
│ └── theme-dark.css # Dark mode
|
|
93
|
+
├── components/react/ # React component library
|
|
94
|
+
│ ├── Button/
|
|
95
|
+
│ ├── Input/
|
|
96
|
+
│ ├── Card/
|
|
97
|
+
│ ├── Badge/
|
|
98
|
+
│ ├── Modal/
|
|
99
|
+
│ ├── Toast/
|
|
100
|
+
│ ├── Tabs/
|
|
101
|
+
│ └── RefreshButton/ # AWS-style auto-refresh button
|
|
102
|
+
├── lib/ # Shared utilities
|
|
103
|
+
│ ├── index.ts # Exports all utilities
|
|
104
|
+
│ └── utils.ts # formatRelativeTime, clsx
|
|
105
|
+
├── app/ # Next.js playground
|
|
106
|
+
├── public/fonts/ # Martian Mono font files
|
|
107
|
+
└── .storybook/ # Storybook configuration
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Design Tokens
|
|
111
|
+
|
|
112
|
+
All visual values are defined as CSS custom properties in `themes/css/`. The token architecture follows three layers:
|
|
113
|
+
|
|
114
|
+
1. **Primitive tokens** — Raw values (colors, spacing units)
|
|
115
|
+
2. **Semantic tokens** — Meaningful mappings (`--color-text-primary`)
|
|
116
|
+
3. **Component tokens** — Component-specific overrides (`--button-radius`)
|
|
117
|
+
|
|
118
|
+
## Themes
|
|
119
|
+
|
|
120
|
+
The codex supports light and dark themes via CSS custom properties. Theme switching uses:
|
|
121
|
+
|
|
122
|
+
```html
|
|
123
|
+
<html data-theme="light">
|
|
124
|
+
<!-- or -->
|
|
125
|
+
<html data-theme="dark">
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Components
|
|
129
|
+
|
|
130
|
+
All components:
|
|
131
|
+
- Use tokens exclusively (no hardcoded values)
|
|
132
|
+
- Are built on Radix UI primitives for accessibility
|
|
133
|
+
- Encode taste, not flexibility
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
Private repository. Personal use only.
|
|
138
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
.badge {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
padding: var(--badge-padding-y) var(--badge-padding-x);
|
|
6
|
+
font-size: var(--badge-font-size);
|
|
7
|
+
font-weight: var(--font-medium);
|
|
8
|
+
line-height: 1;
|
|
9
|
+
border-radius: var(--badge-radius);
|
|
10
|
+
white-space: nowrap;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Variants */
|
|
14
|
+
.default {
|
|
15
|
+
background-color: var(--color-bg-subtle);
|
|
16
|
+
color: var(--color-text-secondary);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.primary {
|
|
20
|
+
background-color: var(--color-intent-primary-bg);
|
|
21
|
+
color: var(--color-intent-primary);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.success {
|
|
25
|
+
background-color: var(--color-intent-success-bg);
|
|
26
|
+
color: var(--color-intent-success);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.warning {
|
|
30
|
+
background-color: var(--color-intent-warning-bg);
|
|
31
|
+
color: var(--color-intent-warning);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.danger {
|
|
35
|
+
background-color: var(--color-intent-danger-bg);
|
|
36
|
+
color: var(--color-intent-danger);
|
|
37
|
+
}
|
|
38
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Badge } from "./Badge";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Badge> = {
|
|
5
|
+
title: "Components/Badge",
|
|
6
|
+
component: Badge,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "centered",
|
|
9
|
+
},
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
argTypes: {
|
|
12
|
+
variant: {
|
|
13
|
+
control: "select",
|
|
14
|
+
options: ["default", "primary", "success", "warning", "danger"],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
type Story = StoryObj<typeof meta>;
|
|
21
|
+
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
children: "Default",
|
|
25
|
+
variant: "default",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Primary: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
children: "Primary",
|
|
32
|
+
variant: "primary",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Success: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
children: "Success",
|
|
39
|
+
variant: "success",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Warning: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
children: "Warning",
|
|
46
|
+
variant: "warning",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const Danger: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
children: "Danger",
|
|
53
|
+
variant: "danger",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const AllVariants: Story = {
|
|
58
|
+
render: () => (
|
|
59
|
+
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
|
60
|
+
<Badge variant="default">Default</Badge>
|
|
61
|
+
<Badge variant="primary">Primary</Badge>
|
|
62
|
+
<Badge variant="success">Success</Badge>
|
|
63
|
+
<Badge variant="warning">Warning</Badge>
|
|
64
|
+
<Badge variant="danger">Danger</Badge>
|
|
65
|
+
</div>
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const StatusBadges: Story = {
|
|
70
|
+
render: () => (
|
|
71
|
+
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
|
72
|
+
<Badge variant="success">Active</Badge>
|
|
73
|
+
<Badge variant="warning">Pending</Badge>
|
|
74
|
+
<Badge variant="danger">Failed</Badge>
|
|
75
|
+
<Badge variant="default">Draft</Badge>
|
|
76
|
+
</div>
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { clsx } from "clsx";
|
|
4
|
+
import styles from "./Badge.module.css";
|
|
5
|
+
|
|
6
|
+
const badgeVariants = cva(styles.badge, {
|
|
7
|
+
variants: {
|
|
8
|
+
variant: {
|
|
9
|
+
default: styles.default,
|
|
10
|
+
primary: styles.primary,
|
|
11
|
+
success: styles.success,
|
|
12
|
+
warning: styles.warning,
|
|
13
|
+
danger: styles.danger,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: {
|
|
17
|
+
variant: "default",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export interface BadgeProps
|
|
22
|
+
extends HTMLAttributes<HTMLSpanElement>,
|
|
23
|
+
VariantProps<typeof badgeVariants> {
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
|
28
|
+
({ className, variant, children, ...props }, ref) => {
|
|
29
|
+
return (
|
|
30
|
+
<span
|
|
31
|
+
ref={ref}
|
|
32
|
+
className={clsx(badgeVariants({ variant }), className)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</span>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
Badge.displayName = "Badge";
|
|
42
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
.button {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
gap: var(--space-2);
|
|
6
|
+
font-weight: var(--button-font-weight);
|
|
7
|
+
border-radius: var(--button-radius);
|
|
8
|
+
transition: all 150ms ease;
|
|
9
|
+
cursor: pointer;
|
|
10
|
+
border: none;
|
|
11
|
+
text-decoration: none;
|
|
12
|
+
white-space: nowrap;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.button:focus-visible {
|
|
16
|
+
outline: none;
|
|
17
|
+
box-shadow: var(--focus-ring);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.button:disabled {
|
|
21
|
+
opacity: 0.5;
|
|
22
|
+
cursor: not-allowed;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Variants */
|
|
26
|
+
.primary {
|
|
27
|
+
background-color: var(--color-intent-primary);
|
|
28
|
+
color: var(--color-text-inverse);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.primary:hover:not(:disabled) {
|
|
32
|
+
background-color: var(--color-intent-primary-hover);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.primary:active:not(:disabled) {
|
|
36
|
+
transform: scale(0.98);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.secondary {
|
|
40
|
+
background-color: var(--color-bg-surface);
|
|
41
|
+
color: var(--color-text-primary);
|
|
42
|
+
border: 1px solid var(--color-border-default);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.secondary:hover:not(:disabled) {
|
|
46
|
+
background-color: var(--color-bg-subtle);
|
|
47
|
+
border-color: var(--color-border-strong);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.secondary:active:not(:disabled) {
|
|
51
|
+
transform: scale(0.98);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.ghost {
|
|
55
|
+
background-color: transparent;
|
|
56
|
+
color: var(--color-text-primary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.ghost:hover:not(:disabled) {
|
|
60
|
+
background-color: var(--color-bg-subtle);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ghost:active:not(:disabled) {
|
|
64
|
+
background-color: var(--color-bg-muted);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.danger {
|
|
68
|
+
background-color: var(--color-intent-danger);
|
|
69
|
+
color: var(--color-white);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.danger:hover:not(:disabled) {
|
|
73
|
+
background-color: var(--color-intent-danger-hover);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.danger:active:not(:disabled) {
|
|
77
|
+
transform: scale(0.98);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Sizes */
|
|
81
|
+
.sm {
|
|
82
|
+
height: 32px;
|
|
83
|
+
padding: 0 var(--space-3);
|
|
84
|
+
font-size: var(--text-sm);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.md {
|
|
88
|
+
height: 40px;
|
|
89
|
+
padding: 0 var(--button-padding-x);
|
|
90
|
+
font-size: var(--text-sm);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.lg {
|
|
94
|
+
height: 48px;
|
|
95
|
+
padding: 0 var(--space-6);
|
|
96
|
+
font-size: var(--text-base);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Loading State */
|
|
100
|
+
.spinner {
|
|
101
|
+
width: 16px;
|
|
102
|
+
height: 16px;
|
|
103
|
+
border: 2px solid currentColor;
|
|
104
|
+
border-right-color: transparent;
|
|
105
|
+
border-radius: 50%;
|
|
106
|
+
animation: spin 0.6s linear infinite;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.loadingText {
|
|
110
|
+
opacity: 0.7;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@keyframes spin {
|
|
114
|
+
to {
|
|
115
|
+
transform: rotate(360deg);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Button } from "./Button";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Button> = {
|
|
5
|
+
title: "Components/Button",
|
|
6
|
+
component: Button,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "centered",
|
|
9
|
+
},
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
argTypes: {
|
|
12
|
+
variant: {
|
|
13
|
+
control: "select",
|
|
14
|
+
options: ["primary", "secondary", "ghost", "danger"],
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
control: "select",
|
|
18
|
+
options: ["sm", "md", "lg"],
|
|
19
|
+
},
|
|
20
|
+
loading: {
|
|
21
|
+
control: "boolean",
|
|
22
|
+
},
|
|
23
|
+
disabled: {
|
|
24
|
+
control: "boolean",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default meta;
|
|
30
|
+
type Story = StoryObj<typeof meta>;
|
|
31
|
+
|
|
32
|
+
export const Primary: Story = {
|
|
33
|
+
args: {
|
|
34
|
+
children: "Primary Button",
|
|
35
|
+
variant: "primary",
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Secondary: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
children: "Secondary Button",
|
|
42
|
+
variant: "secondary",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Ghost: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
children: "Ghost Button",
|
|
49
|
+
variant: "ghost",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Danger: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
children: "Delete",
|
|
56
|
+
variant: "danger",
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const Small: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
children: "Small",
|
|
63
|
+
size: "sm",
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const Large: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
children: "Large Button",
|
|
70
|
+
size: "lg",
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const Loading: Story = {
|
|
75
|
+
args: {
|
|
76
|
+
children: "Loading...",
|
|
77
|
+
loading: true,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const Disabled: Story = {
|
|
82
|
+
args: {
|
|
83
|
+
children: "Disabled",
|
|
84
|
+
disabled: true,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const AllVariants: Story = {
|
|
89
|
+
render: () => (
|
|
90
|
+
<div style={{ display: "flex", gap: "12px", flexWrap: "wrap" }}>
|
|
91
|
+
<Button variant="primary">Primary</Button>
|
|
92
|
+
<Button variant="secondary">Secondary</Button>
|
|
93
|
+
<Button variant="ghost">Ghost</Button>
|
|
94
|
+
<Button variant="danger">Danger</Button>
|
|
95
|
+
</div>
|
|
96
|
+
),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const AllSizes: Story = {
|
|
100
|
+
render: () => (
|
|
101
|
+
<div style={{ display: "flex", gap: "12px", alignItems: "center" }}>
|
|
102
|
+
<Button size="sm">Small</Button>
|
|
103
|
+
<Button size="md">Medium</Button>
|
|
104
|
+
<Button size="lg">Large</Button>
|
|
105
|
+
</div>
|
|
106
|
+
),
|
|
107
|
+
};
|
|
108
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { clsx } from "clsx";
|
|
5
|
+
import styles from "./Button.module.css";
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(styles.button, {
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
primary: styles.primary,
|
|
11
|
+
secondary: styles.secondary,
|
|
12
|
+
ghost: styles.ghost,
|
|
13
|
+
danger: styles.danger,
|
|
14
|
+
},
|
|
15
|
+
size: {
|
|
16
|
+
sm: styles.sm,
|
|
17
|
+
md: styles.md,
|
|
18
|
+
lg: styles.lg,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
variant: "primary",
|
|
23
|
+
size: "md",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export interface ButtonProps
|
|
28
|
+
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
|
29
|
+
VariantProps<typeof buttonVariants> {
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
loading?: boolean;
|
|
32
|
+
asChild?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
36
|
+
(
|
|
37
|
+
{
|
|
38
|
+
className,
|
|
39
|
+
variant,
|
|
40
|
+
size,
|
|
41
|
+
loading,
|
|
42
|
+
disabled,
|
|
43
|
+
asChild = false,
|
|
44
|
+
children,
|
|
45
|
+
...props
|
|
46
|
+
},
|
|
47
|
+
ref
|
|
48
|
+
) => {
|
|
49
|
+
const Comp = asChild ? Slot : "button";
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Comp
|
|
53
|
+
className={clsx(buttonVariants({ variant, size }), className)}
|
|
54
|
+
ref={ref}
|
|
55
|
+
disabled={disabled || loading}
|
|
56
|
+
aria-disabled={disabled || loading}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
{loading ? (
|
|
60
|
+
<>
|
|
61
|
+
<span className={styles.spinner} aria-hidden="true" />
|
|
62
|
+
<span className={styles.loadingText}>{children}</span>
|
|
63
|
+
</>
|
|
64
|
+
) : (
|
|
65
|
+
children
|
|
66
|
+
)}
|
|
67
|
+
</Comp>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
Button.displayName = "Button";
|
|
73
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
.card {
|
|
2
|
+
background-color: var(--color-bg-surface);
|
|
3
|
+
border: 1px solid var(--color-border-default);
|
|
4
|
+
border-radius: var(--card-radius);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.elevated {
|
|
8
|
+
border-color: transparent;
|
|
9
|
+
box-shadow: var(--shadow-md);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Padding Variants */
|
|
13
|
+
.padding-none {
|
|
14
|
+
padding: 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.padding-sm {
|
|
18
|
+
padding: var(--space-3);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.padding-md {
|
|
22
|
+
padding: var(--card-padding);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.padding-lg {
|
|
26
|
+
padding: var(--space-6);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Subcomponents */
|
|
30
|
+
.header {
|
|
31
|
+
padding: var(--card-padding);
|
|
32
|
+
border-bottom: 1px solid var(--color-border-subtle);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.header:first-child {
|
|
36
|
+
border-top-left-radius: var(--card-radius);
|
|
37
|
+
border-top-right-radius: var(--card-radius);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.content {
|
|
41
|
+
padding: var(--card-padding);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.footer {
|
|
45
|
+
padding: var(--card-padding);
|
|
46
|
+
border-top: 1px solid var(--color-border-subtle);
|
|
47
|
+
background-color: var(--color-bg-subtle);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.footer:last-child {
|
|
51
|
+
border-bottom-left-radius: var(--card-radius);
|
|
52
|
+
border-bottom-right-radius: var(--card-radius);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* When using subcomponents, remove padding from card itself */
|
|
56
|
+
.card:has(.header, .content, .footer) {
|
|
57
|
+
padding: 0;
|
|
58
|
+
}
|
|
59
|
+
|