create-react-adam 0.2.2 → 0.3.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.
@@ -17,7 +17,7 @@ jobs:
17
17
  - name: Setup Node.js
18
18
  uses: actions/setup-node@v6
19
19
  with:
20
- node-version: "24.11.1"
20
+ node-version: "24.18.0"
21
21
  cache: "npm"
22
22
 
23
23
  - name: Install dependencies
@@ -36,7 +36,7 @@ jobs:
36
36
  run: npm run test:e2e
37
37
 
38
38
  - name: Upload Playwright report
39
- uses: actions/upload-artifact@v5
39
+ uses: actions/upload-artifact@v6
40
40
  if: always()
41
41
  with:
42
42
  name: playwright-report
@@ -44,7 +44,7 @@ jobs:
44
44
  retention-days: 30
45
45
 
46
46
  - name: Upload Allure results
47
- uses: actions/upload-artifact@v5
47
+ uses: actions/upload-artifact@v6
48
48
  if: always()
49
49
  with:
50
50
  name: allure-results
@@ -0,0 +1,33 @@
1
+ name: Lighthouse CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ lighthouse:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v6
16
+
17
+ - name: Setup Node.js
18
+ uses: actions/setup-node@v6
19
+ with:
20
+ node-version: "24.18.0"
21
+ cache: "npm"
22
+
23
+ - name: Install dependencies
24
+ run: npm install
25
+
26
+ - name: Build
27
+ run: npm run build
28
+
29
+ - name: Run Lighthouse CI
30
+ uses: treosh/lighthouse-ci-action@v12
31
+ with:
32
+ configPath: ./lighthouserc.json
33
+ uploadArtifacts: true
package/template/.nvmrc CHANGED
@@ -1,2 +1,2 @@
1
- 24.11.1
1
+ 24.18.0
2
2
 
@@ -1,6 +1,6 @@
1
- # **PROJECT_NAME**
1
+ # {{PROJECT_NAME}}
2
2
 
3
- A React application created with create-react-adam.
3
+ A React application created with [create-react-adam](https://github.com/adamjsturge/create-react-adam).
4
4
 
5
5
  ## Getting Started
6
6
 
@@ -12,14 +12,6 @@ This will start the development server at `http://localhost:5173`.
12
12
 
13
13
  ## Available Scripts
14
14
 
15
- ### `npm run itDoesNotWork`
16
-
17
- Runs basic troubleshooting checks:
18
-
19
- - Installs dependencies
20
- - Checks if Vite dev server is running
21
- - Provides helpful diagnostic information
22
-
23
15
  ### `npm run dev`
24
16
 
25
17
  Starts the development server with hot module replacement.
@@ -32,49 +24,159 @@ Builds the app for production. The build artifacts will be stored in the `dist/`
32
24
 
33
25
  Previews the production build locally.
34
26
 
35
- ### `npm run lint`
27
+ ### `npm run lint` / `npm run lint:check`
28
+
29
+ Runs ESLint with auto-fix / check-only (for CI).
30
+
31
+ ### `npm run format` / `npm run format:check`
32
+
33
+ Formats all files with Prettier (organizes imports, sorts Tailwind classes) / check-only (for CI).
36
34
 
37
- Runs ESLint to check for code quality issues.
35
+ ### `npm run setFormatToPrecommitHook`
38
36
 
39
- ### `npm run format`
37
+ Installs a git pre-commit hook that formats and lints staged files. If this
38
+ project was scaffolded outside an existing repository, the hook was already
39
+ installed for you.
40
40
 
41
- Formats all files with Prettier, organizing imports and sorting Tailwind classes.
41
+ ### `npm run test:e2e` (if E2E was included)
42
42
 
43
- ## Tech Stack
43
+ Runs Playwright E2E tests from the `e2e/` directory. `test:e2e:ui` opens
44
+ Playwright UI mode and `test:e2e:report` generates an Allure report.
44
45
 
45
- - **React** - UI library
46
- - **TypeScript** - Type safety
47
- - **Vite** - Build tool and dev server
48
- - **Wouter** - Lightweight routing
49
- - **Tailwind CSS** - Utility-first CSS framework
50
- - **ESLint & Prettier** - Code quality and formatting
46
+ ### `npm run itDoesNotWork`
47
+
48
+ Runs basic troubleshooting checks: installs dependencies, checks whether the
49
+ Vite dev server is running, and prints diagnostic information.
51
50
 
52
51
  ## Project Structure
53
52
 
54
53
  ```
55
54
  src/
56
- ├── pages/ # Page components
57
- ├── Home/index.tsx
58
- │ └── About/index.tsx
59
- ├── App.tsx # Main app with routes
60
- ├── main.tsx # Entry point
61
- └── app.css # Global styles (Tailwind imports)
55
+ ├── pages/ # Route pages (Home, About, NotFound)
56
+ ├── components/ # Reusable components
57
+ ├── types/ # Shared TypeScript types
58
+ ├── utils/ # Utility functions (if included)
59
+ ├── App.tsx # Routing, skip link, <main> landmark
60
+ ├── main.tsx # Entry point
61
+ └── app.css # Tailwind theme + base styles
62
62
  ```
63
63
 
64
+ Routing uses [Wouter](https://github.com/molefrog/wouter). Pages are
65
+ code-split with `React.lazy`, and each page sets its own `document.title`.
66
+
64
67
  ## Adding New Pages
65
68
 
66
69
  1. Create a new component in `src/pages/`
67
- 2. Import and add a route in `src/App.tsx`
68
-
69
- Example:
70
+ 2. Add a lazy import and route in `src/App.tsx`
70
71
 
71
72
  ```tsx
72
- import NewPage from "./pages/NewPage";
73
+ const NewPage = lazy(() => import("./pages/NewPage"));
73
74
 
74
75
  // In the Switch component:
75
76
  <Route path="/new" component={NewPage} />;
76
77
  ```
77
78
 
79
+ ## Performance, SEO, and Accessibility
80
+
81
+ - **Fonts**: Inter is self-hosted from `public/fonts/` (variable `woff2`,
82
+ `font-display: swap`, preloaded in `index.html`). No third-party requests.
83
+ - **SEO**: `index.html` ships meta description, Open Graph, and Twitter card
84
+ tags — update them as your app takes shape. `public/robots.txt` is included.
85
+ - **Accessibility**: a skip-to-content link, `<main>` landmark, and visible
86
+ `:focus-visible` styles are wired into `App.tsx` / `app.css`.
87
+
88
+ ## ESLint: prefer-webp-images (if included)
89
+
90
+ A local rule in `eslint-rules/prefer-webp-images.js` errors on references to
91
+ `.png`, `.jpg`, `.jpeg`, `.gif`, or `.bmp` images in imports, JSX attributes,
92
+ and string literals. Convert images to WebP at
93
+ [tools.sturge.dev/webp](https://tools.sturge.dev/webp).
94
+
95
+ For the rare image that must stay in another format:
96
+
97
+ ```tsx
98
+ // eslint-disable-next-line adam/prefer-webp-images
99
+ import legacyLogo from "./legacy-logo.png";
100
+ ```
101
+
102
+ ## Utilities (if included)
103
+
104
+ ### `classNames`
105
+
106
+ Joins class names, skipping falsy values, with clsx-style conditional objects:
107
+
108
+ ```tsx
109
+ import classNames from "./utils/classNames";
110
+
111
+ classNames("btn", isActive && "btn-active", { hidden: !isOpen });
112
+ // => "btn btn-active hidden" depending on the conditions
113
+ ```
114
+
115
+ ### `storage` and `useReactPersist`
116
+
117
+ `localStorage` wrapper with JSON serialization and optional expiry, plus a
118
+ `useState`-compatible hook that persists across reloads:
119
+
120
+ ```tsx
121
+ import storage, { useReactPersist } from "./utils/Storage";
122
+
123
+ storage.save("token", "abc123", { secondsTillExpiry: 3600 });
124
+ storage.load("token", null);
125
+
126
+ const [count, setCount] = useReactPersist("counter", 0);
127
+ ```
128
+
129
+ ### `useUrlState`
130
+
131
+ `useState`-compatible hook that syncs a value with a URL query parameter.
132
+ Supports string and number values (the type follows the default value):
133
+
134
+ ```tsx
135
+ import { useUrlState } from "./utils/useUrlState";
136
+
137
+ const [page, setPage] = useUrlState("page", 1);
138
+ const [tab, setTab] = useUrlState("tab", "overview");
139
+ ```
140
+
141
+ ### `useInternetConnected`
142
+
143
+ Runs a callback when the browser is online and every time the connection
144
+ comes back:
145
+
146
+ ```tsx
147
+ import { useInternetConnected } from "./utils/Internet";
148
+
149
+ useInternetConnected(() => refetch(), [refetch]);
150
+ ```
151
+
152
+ ### `safeTimeout`
153
+
154
+ Drop-in `setTimeout` replacement that clamps delays above the 32-bit signed
155
+ integer maximum (~24.8 days) instead of letting them overflow and fire
156
+ immediately:
157
+
158
+ ```tsx
159
+ import { safeTimeout } from "./utils/helpers";
160
+
161
+ safeTimeout(() => expireSession(), thirtyDaysInMs);
162
+ ```
163
+
164
+ ## CI Workflows
165
+
166
+ - **Code Checks** (`.github/workflows/check.yml`): type checking, formatting,
167
+ linting, and a production build on every push and PR to `main`.
168
+ - **E2E Tests** (`.github/workflows/e2e.yml`, if included): Playwright tests
169
+ with report artifacts.
170
+ - **Lighthouse CI** (`.github/workflows/lighthouse.yml`, if included): audits
171
+ the production build and fails below accessibility ≥ 95, SEO ≥ 95, and
172
+ performance ≥ 85. Budgets live in `lighthouserc.json`.
173
+
174
+ ## Dependency Management
175
+
176
+ Dependencies are pinned to exact versions and `.npmrc` sets
177
+ `save-exact=true`, so future installs stay pinned. The repo is
178
+ Renovate-friendly.
179
+
78
180
  ## Learn More
79
181
 
80
182
  - [React Documentation](https://react.dev)
@@ -0,0 +1,13 @@
1
+ # Dependencies
2
+ node_modules
3
+
4
+ # Test results
5
+ test-results
6
+ playwright-report
7
+ allure-results
8
+ allure-report
9
+ playwright/.cache
10
+
11
+ # Logs
12
+ *.log
13
+
@@ -9,7 +9,7 @@
9
9
  "test:e2e:report": "allure generate ./allure-results --clean && allure open"
10
10
  },
11
11
  "devDependencies": {
12
- "@playwright/test": "1.57.0",
13
- "allure-playwright": "3.4.2"
12
+ "@playwright/test": "1.61.1",
13
+ "allure-playwright": "3.10.2"
14
14
  }
15
15
  }
@@ -0,0 +1,66 @@
1
+ const NON_WEBP_IMAGE_PATTERN = /\.(png|jpe?g|gif|bmp)$/i;
2
+
3
+ /**
4
+ * Flags references to non-WebP raster images (.png, .jpg, .jpeg, .gif, .bmp)
5
+ * in imports, JSX attributes, and string literals. WebP/AVIF/SVG are allowed.
6
+ */
7
+ function findNonWebpImage(value) {
8
+ if (typeof value !== "string") {
9
+ return null;
10
+ }
11
+
12
+ // Handle srcSet-style values ("a.png 1x, b.png 2x") and Vite suffixes
13
+ // ("./a.png?url") by checking each path-like token.
14
+ for (const token of value.split(/[\s,]+/)) {
15
+ const path = token.split(/[?#]/)[0];
16
+ if (NON_WEBP_IMAGE_PATTERN.test(path)) {
17
+ return token;
18
+ }
19
+ }
20
+
21
+ return null;
22
+ }
23
+
24
+ const preferWebpImages = {
25
+ meta: {
26
+ type: "suggestion",
27
+ docs: {
28
+ description:
29
+ "Prefer WebP images over PNG/JPEG/GIF/BMP for smaller file sizes and faster page loads",
30
+ },
31
+ messages: {
32
+ preferWebp:
33
+ 'Image "{{path}}" is not WebP. Convert it at https://tools.sturge.dev/webp',
34
+ },
35
+ schema: [],
36
+ },
37
+ create(context) {
38
+ function check(node, value) {
39
+ const match = findNonWebpImage(value);
40
+ if (match) {
41
+ context.report({
42
+ node,
43
+ messageId: "preferWebp",
44
+ data: { path: match },
45
+ });
46
+ }
47
+ }
48
+
49
+ return {
50
+ Literal(node) {
51
+ check(node, node.value);
52
+ },
53
+ TemplateLiteral(node) {
54
+ if (node.quasis.length === 1) {
55
+ check(node, node.quasis[0].value.cooked);
56
+ }
57
+ },
58
+ };
59
+ },
60
+ };
61
+
62
+ export default {
63
+ rules: {
64
+ "prefer-webp-images": preferWebpImages,
65
+ },
66
+ };
@@ -8,6 +8,7 @@ import reactHooks from "eslint-plugin-react-hooks";
8
8
  import reactRefresh from "eslint-plugin-react-refresh";
9
9
  import unicorn from "eslint-plugin-unicorn";
10
10
  import tseslint from "typescript-eslint";
11
+ import adam from "./eslint-rules/prefer-webp-images.js";
11
12
 
12
13
  export default tseslint.config({
13
14
  extends: [
@@ -25,6 +26,7 @@ export default tseslint.config({
25
26
  "jsx-a11y": jsxA11y,
26
27
  "react-hooks": reactHooks,
27
28
  "react-refresh": reactRefresh,
29
+ adam,
28
30
  },
29
31
  languageOptions: {
30
32
  parserOptions: {
@@ -48,6 +50,7 @@ export default tseslint.config({
48
50
  "warn",
49
51
  { allowConstantExport: true },
50
52
  ],
53
+ "adam/prefer-webp-images": "error",
51
54
  },
52
55
  files: ["**/*.{ts,tsx}"],
53
56
  ignores: ["dist"],
@@ -0,0 +1,54 @@
1
+ import eslint from "@eslint/js";
2
+ import prettier from "eslint-config-prettier";
3
+ import deMorgan from "eslint-plugin-de-morgan";
4
+ import importPlugin from "eslint-plugin-import";
5
+ import jsxA11y from "eslint-plugin-jsx-a11y";
6
+ import promisePlugin from "eslint-plugin-promise";
7
+ import reactHooks from "eslint-plugin-react-hooks";
8
+ import reactRefresh from "eslint-plugin-react-refresh";
9
+ import unicorn from "eslint-plugin-unicorn";
10
+ import tseslint from "typescript-eslint";
11
+
12
+ export default tseslint.config({
13
+ extends: [
14
+ eslint.configs.recommended,
15
+ ...tseslint.configs.recommended,
16
+ ...tseslint.configs.strict,
17
+ prettier,
18
+ promisePlugin.configs["flat/recommended"],
19
+ importPlugin.flatConfigs.recommended,
20
+ importPlugin.flatConfigs.typescript,
21
+ unicorn.configs["unopinionated"],
22
+ deMorgan.configs.recommended,
23
+ ],
24
+ plugins: {
25
+ "jsx-a11y": jsxA11y,
26
+ "react-hooks": reactHooks,
27
+ "react-refresh": reactRefresh,
28
+ },
29
+ languageOptions: {
30
+ parserOptions: {
31
+ ecmaFeatures: {
32
+ jsx: true,
33
+ },
34
+ },
35
+ },
36
+ rules: {
37
+ "@typescript-eslint/no-unused-vars": [
38
+ "error",
39
+ {
40
+ argsIgnorePattern: "^_",
41
+ varsIgnorePattern: "^_",
42
+ caughtErrorsIgnorePattern: "^_",
43
+ },
44
+ ],
45
+ ...jsxA11y.flatConfigs.strict.rules,
46
+ ...reactHooks.configs.recommended.rules,
47
+ "react-refresh/only-export-components": [
48
+ "warn",
49
+ { allowConstantExport: true },
50
+ ],
51
+ },
52
+ files: ["**/*.{ts,tsx}"],
53
+ ignores: ["dist"],
54
+ });
@@ -3,6 +3,25 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta
7
+ name="description"
8
+ content="__PROJECT_NAME__ — a React app built with create-react-adam"
9
+ />
10
+ <meta name="theme-color" content="#2563eb" />
11
+ <meta property="og:title" content="__PROJECT_NAME__" />
12
+ <meta
13
+ property="og:description"
14
+ content="__PROJECT_NAME__ — a React app built with create-react-adam"
15
+ />
16
+ <meta property="og:type" content="website" />
17
+ <meta name="twitter:card" content="summary" />
18
+ <link
19
+ rel="preload"
20
+ href="/fonts/InterVariable.woff2"
21
+ as="font"
22
+ type="font/woff2"
23
+ crossorigin
24
+ />
6
25
  <title>__PROJECT_NAME__</title>
7
26
  </head>
8
27
  <body>
@@ -0,0 +1,17 @@
1
+ {
2
+ "ci": {
3
+ "collect": {
4
+ "startServerCommand": "npm run preview",
5
+ "startServerReadyPattern": "Local",
6
+ "url": ["http://localhost:4173/"],
7
+ "numberOfRuns": 3
8
+ },
9
+ "assert": {
10
+ "assertions": {
11
+ "categories:accessibility": ["error", { "minScore": 0.95 }],
12
+ "categories:seo": ["error", { "minScore": 0.95 }],
13
+ "categories:performance": ["error", { "minScore": 0.85 }]
14
+ }
15
+ }
16
+ }
17
+ }
package/template/npmrc ADDED
@@ -0,0 +1,2 @@
1
+ save-exact=true
2
+
@@ -19,32 +19,32 @@
19
19
  "test:e2e:report": "cd e2e && allure generate ./allure-results --clean && allure open"
20
20
  },
21
21
  "dependencies": {
22
- "react": "19.2.0",
23
- "react-dom": "19.2.0",
24
- "react-icons": "5.5.0",
25
- "wouter": "3.8.0"
22
+ "react": "19.2.7",
23
+ "react-dom": "19.2.7",
24
+ "react-icons": "5.7.0",
25
+ "wouter": "3.10.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@eslint/js": "9.39.1",
29
- "@tailwindcss/vite": "4.1.17",
30
- "@types/react": "19.2.7",
28
+ "@eslint/js": "9.39.4",
29
+ "@tailwindcss/vite": "4.3.2",
30
+ "@types/react": "19.2.17",
31
31
  "@types/react-dom": "19.2.3",
32
- "@vitejs/plugin-react": "5.1.1",
33
- "eslint": "9.39.1",
32
+ "@vitejs/plugin-react": "5.2.0",
33
+ "eslint": "9.39.4",
34
34
  "eslint-config-prettier": "10.1.8",
35
- "eslint-plugin-de-morgan": "2.0.0",
35
+ "eslint-plugin-de-morgan": "2.1.2",
36
36
  "eslint-plugin-import": "2.32.0",
37
37
  "eslint-plugin-jsx-a11y": "6.10.2",
38
- "eslint-plugin-promise": "7.2.1",
39
- "eslint-plugin-react-hooks": "7.0.1",
40
- "eslint-plugin-react-refresh": "0.4.24",
38
+ "eslint-plugin-promise": "7.3.0",
39
+ "eslint-plugin-react-hooks": "7.1.1",
40
+ "eslint-plugin-react-refresh": "0.5.3",
41
41
  "eslint-plugin-unicorn": "62.0.0",
42
- "prettier": "3.6.2",
42
+ "prettier": "3.9.5",
43
43
  "prettier-plugin-organize-imports": "4.3.0",
44
- "prettier-plugin-tailwindcss": "0.7.1",
45
- "tailwindcss": "4.1.17",
44
+ "prettier-plugin-tailwindcss": "0.8.0",
45
+ "tailwindcss": "4.3.2",
46
46
  "typescript": "5.9.3",
47
- "typescript-eslint": "8.48.0",
48
- "vite": "7.2.4"
47
+ "typescript-eslint": "8.63.0",
48
+ "vite": "7.3.6"
49
49
  }
50
50
  }
@@ -0,0 +1,5 @@
1
+ User-agent: *
2
+ Allow: /
3
+
4
+ # Add your sitemap URL once deployed:
5
+ # Sitemap: https://example.com/sitemap.xml
@@ -1,17 +1,31 @@
1
+ import { lazy, Suspense } from "react";
1
2
  import { Redirect, Route, Switch } from "wouter";
2
- import About from "./pages/About";
3
- import Home from "./pages/Home";
4
- import NotFound from "./pages/NotFound";
3
+
4
+ const Home = lazy(() => import("./pages/Home"));
5
+ const About = lazy(() => import("./pages/About"));
6
+ const NotFound = lazy(() => import("./pages/NotFound"));
5
7
 
6
8
  const App = () => (
7
- <Switch>
8
- <Route path="/" component={Home} />
9
- <Route path="/about" component={About} />
10
- <Route path="/home">
11
- <Redirect to="/" />
12
- </Route>
13
- <Route component={NotFound} />
14
- </Switch>
9
+ <>
10
+ <a
11
+ href="#main-content"
12
+ className="bg-brand-primary text-brand-white sr-only z-50 rounded-lg px-4 py-2 focus:not-sr-only focus:fixed focus:top-4 focus:left-4"
13
+ >
14
+ Skip to main content
15
+ </a>
16
+ <main id="main-content">
17
+ <Suspense fallback={null}>
18
+ <Switch>
19
+ <Route path="/" component={Home} />
20
+ <Route path="/about" component={About} />
21
+ <Route path="/home">
22
+ <Redirect to="/" />
23
+ </Route>
24
+ <Route component={NotFound} />
25
+ </Switch>
26
+ </Suspense>
27
+ </main>
28
+ </>
15
29
  );
16
30
 
17
31
  export default App;
@@ -1,19 +1,30 @@
1
1
  @import "tailwindcss";
2
2
 
3
+ @font-face {
4
+ font-family: "Inter";
5
+ font-style: normal;
6
+ font-weight: 100 900;
7
+ font-display: swap;
8
+ src: url("/fonts/InterVariable.woff2") format("woff2");
9
+ }
10
+
3
11
  @theme {
4
- --font-sans: "Inter", sans-serif;
12
+ --font-sans:
13
+ "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
5
14
 
6
- --color-brand-primary: #3b82f6;
7
- --color-brand-primaryHover: #2563eb;
8
- --color-brand-primaryActive: #1d4ed8;
15
+ /* Brand colors are chosen to meet WCAG AA contrast (>= 4.5:1) with white
16
+ text; keep that in mind when changing them. */
17
+ --color-brand-primary: #2563eb;
18
+ --color-brand-primaryHover: #1d4ed8;
19
+ --color-brand-primaryActive: #1e40af;
9
20
 
10
- --color-brand-secondary: #8b5cf6;
11
- --color-brand-secondaryHover: #7c3aed;
12
- --color-brand-secondaryActive: #6d28d9;
21
+ --color-brand-secondary: #7c3aed;
22
+ --color-brand-secondaryHover: #6d28d9;
23
+ --color-brand-secondaryActive: #5b21b6;
13
24
 
14
- --color-brand-success: #22c55e;
25
+ --color-brand-success: #15803d;
15
26
  --color-brand-warning: #f59e0b;
16
- --color-brand-danger: #ef4444;
27
+ --color-brand-danger: #dc2626;
17
28
 
18
29
  --color-brand-black: #0f172a;
19
30
  --color-brand-white: #ffffff;
@@ -27,3 +38,10 @@
27
38
 
28
39
  --color-brand-disabled: #94a3b8;
29
40
  }
41
+
42
+ @layer base {
43
+ :focus-visible {
44
+ outline: 2px solid var(--color-brand-primary);
45
+ outline-offset: 2px;
46
+ }
47
+ }
@@ -0,0 +1,27 @@
1
+ import { ComponentPropsWithoutRef } from "react";
2
+ import { ButtonVariant } from "../types/ui";
3
+
4
+ interface ButtonProps extends ComponentPropsWithoutRef<"button"> {
5
+ variant?: ButtonVariant;
6
+ }
7
+
8
+ const variantClasses: Record<ButtonVariant, string> = {
9
+ primary: "bg-brand-primary hover:bg-brand-primaryHover",
10
+ success: "bg-brand-success hover:bg-brand-success/90",
11
+ danger: "bg-brand-danger hover:bg-brand-danger/90",
12
+ };
13
+
14
+ const Button = ({ variant = "primary", className, ...props }: ButtonProps) => (
15
+ <button
16
+ className={[
17
+ "text-brand-white rounded-lg px-4 py-2 transition-colors",
18
+ variantClasses[variant],
19
+ className,
20
+ ]
21
+ .filter(Boolean)
22
+ .join(" ")}
23
+ {...props}
24
+ />
25
+ );
26
+
27
+ export default Button;