create-starbase 5.0.0 → 5.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 (35) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +56 -0
  3. package/package.json +3 -3
  4. package/template/.claude/commands/update-deps.md +74 -0
  5. package/template/CLAUDE.md +21 -2
  6. package/template/README.md +2 -1
  7. package/template/eslint.config.js +51 -1
  8. package/template/index.html +4 -4
  9. package/template/package.json +11 -5
  10. package/template/public/favicon.svg +10 -0
  11. package/template/src/lib/theme/base.css +1 -6
  12. package/template/src/lib/theme/tailwind.css +12 -24
  13. package/template/src/lib/utils/cn.ts +2 -2
  14. package/template/src/lib/utils/darkMode.ts +1 -1
  15. package/template/src/main.tsx +3 -6
  16. package/template/src/routes/__root.tsx +3 -8
  17. package/template/src/routes/index.tsx +42 -15
  18. package/template/src/routes/liftoff.tsx +105 -9
  19. package/template/src/ui/atoms/Button.tsx +7 -17
  20. package/template/src/ui/atoms/Code.tsx +14 -12
  21. package/template/src/ui/atoms/Link.tsx +1 -1
  22. package/template/src/ui/atoms/index.ts +4 -0
  23. package/template/src/ui/molecules/DarkModeToggle.tsx +7 -3
  24. package/template/src/ui/molecules/FeatureCard.tsx +41 -0
  25. package/template/src/ui/molecules/Stargazers.tsx +34 -8
  26. package/template/src/ui/molecules/index.ts +3 -0
  27. package/template/src/ui/organisms/SiteHeader.tsx +43 -0
  28. package/template/src/ui/organisms/index.ts +1 -0
  29. package/template/src/ui/templates/SiteLayout.tsx +26 -0
  30. package/template/src/ui/templates/index.ts +1 -0
  31. package/template/tsconfig.app.json +4 -0
  32. package/template/vite.config.ts +2 -2
  33. package/template/src/ui/molecules/PageHeader.tsx +0 -35
  34. package/template/src/ui/organisms/.gitkeep +0 -0
  35. package/template/src/ui/templates/.gitkeep +0 -0
package/README.md CHANGED
@@ -69,8 +69,9 @@ Starbase ships with custom [Claude Code commands](https://docs.anthropic.com/en/
69
69
 
70
70
  - **`/audit`** -- Scans the codebase for drift against CLAUDE.md conventions. Raw color values bypassing the token system, components at the wrong atomic level, accessibility regressions, import violations. Architecture enforcement, automated.
71
71
  - **`/review`** -- Reviews the current branch's changes against CLAUDE.md. Like a PR review from someone who actually read the style guide.
72
+ - **`/update-deps`** -- Runs `npm outdated`, categorizes updates into safe / Vite-aligned / major-breaking tiers, bumps what's safe, holds what isn't, and verifies with build + lint. Scaffolds a fresh `create-vite` template to check alignment -- no hardcoded version lists to maintain.
72
73
 
73
- More commands are on the launchpad. The goal is a suite of tools that handle the mechanical parts of maintaining consistency so you can focus on building.
74
+ The goal is a suite of tools that handle the mechanical parts of maintaining consistency so you can focus on building.
74
75
 
75
76
  ## Liftoff
76
77
 
package/dist/index.js ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const SKIP_FILES = ['node_modules', '.npmignore'];
8
+ function copyDir(src, dest) {
9
+ fs.mkdirSync(dest, { recursive: true });
10
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
11
+ const srcPath = path.join(src, entry.name);
12
+ const destPath = path.join(dest, entry.name);
13
+ if (SKIP_FILES.includes(entry.name)) {
14
+ continue;
15
+ }
16
+ if (entry.isDirectory()) {
17
+ copyDir(srcPath, destPath);
18
+ }
19
+ else {
20
+ fs.copyFileSync(srcPath, destPath);
21
+ }
22
+ }
23
+ }
24
+ function main() {
25
+ const args = process.argv.slice(2);
26
+ const projectName = args[0];
27
+ if (!projectName) {
28
+ console.error('Error: Please specify a project name.');
29
+ console.error(' npm create starbase <project-name>');
30
+ process.exit(1);
31
+ }
32
+ const targetDir = path.resolve(process.cwd(), projectName);
33
+ if (fs.existsSync(targetDir)) {
34
+ console.error(`Error: Directory "${projectName}" already exists.`);
35
+ process.exit(1);
36
+ }
37
+ const templateDir = path.resolve(__dirname, '..', 'template');
38
+ if (!fs.existsSync(templateDir)) {
39
+ console.error('Error: Template directory not found.');
40
+ process.exit(1);
41
+ }
42
+ console.log(`Creating project in ${targetDir}...`);
43
+ copyDir(templateDir, targetDir);
44
+ const packageJsonPath = path.join(targetDir, 'package.json');
45
+ if (fs.existsSync(packageJsonPath)) {
46
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
47
+ packageJson.name = projectName;
48
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
49
+ }
50
+ console.log('\nDone! Next steps:\n');
51
+ console.log(` cd ${projectName}`);
52
+ console.log(' npm install');
53
+ console.log(' npm run dev');
54
+ console.log();
55
+ }
56
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-starbase",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "type": "module",
5
5
  "author": "Brian Staruk <brian@staruk.net>",
6
6
  "contributors": [
@@ -39,7 +39,7 @@
39
39
  "devDependencies": {
40
40
  "@commitlint/cli": "^20.4.1",
41
41
  "@commitlint/config-conventional": "^20.4.1",
42
- "@types/node": "^20.11.0",
42
+ "@types/node": "^24.10.1",
43
43
  "husky": "^9.1.7",
44
44
  "lint-staged": "^16.2.7",
45
45
  "prettier": "^3.8.1",
@@ -49,6 +49,6 @@
49
49
  "*.{js,jsx,ts,tsx,css,md,json}": "prettier --write"
50
50
  },
51
51
  "engines": {
52
- "node": ">=20"
52
+ "node": ">=24"
53
53
  }
54
54
  }
@@ -0,0 +1,74 @@
1
+ Update project dependencies safely and methodically.
2
+
3
+ Focus: $ARGUMENTS
4
+
5
+ If a focus was provided, narrow the update to matching packages. Otherwise, evaluate all dependencies.
6
+
7
+ ## Step 1: Inventory
8
+
9
+ Run `npm outdated` and list every package with an available update. For each, note the current version, wanted version, and latest version.
10
+
11
+ ## Step 2: Categorize
12
+
13
+ Sort every outdated package into one of three tiers:
14
+
15
+ ### Safe
16
+
17
+ Patch and minor bumps with no breaking changes. These can be bumped immediately.
18
+
19
+ ### Hold — Vite alignment
20
+
21
+ Packages that `create-vite` pins to a specific major version in its `react-ts` template. To check the current Vite-aligned versions, scaffold a throwaway project:
22
+
23
+ ```
24
+ npm create vite@latest /tmp/vite-check -- --template react-ts
25
+ cat /tmp/vite-check/package.json
26
+ rm -rf /tmp/vite-check
27
+ ```
28
+
29
+ Compare the major versions of shared packages — at minimum check these:
30
+
31
+ - `vite`
32
+ - `typescript`
33
+ - `eslint`
34
+ - `@eslint/js`
35
+ - `globals`
36
+ - `eslint-plugin-react-hooks`
37
+ - `eslint-plugin-react-refresh`
38
+ - `@vitejs/plugin-react`
39
+ - `@types/react`
40
+ - `@types/react-dom`
41
+
42
+ If a package's latest version jumps to a higher major than what `create-vite` ships, **hold it** and note the Vite-aligned version. If `create-vite` has moved to a newer major and this project hasn't yet, flag it as a potential upgrade to investigate.
43
+
44
+ ### Hold — major/breaking
45
+
46
+ Any remaining major version bump not covered by Vite alignment. These require investigation before bumping. Do a quick web search to check the changelog or migration guide and note what changed.
47
+
48
+ ### Special case: `@types/node`
49
+
50
+ `@types/node` should match the major version in the `engines` field of `package.json` (currently `>=24`, so `@types/node` major should be `24`). Do not bump it to a higher major just because one exists.
51
+
52
+ ## Step 3: Apply safe updates
53
+
54
+ For all packages in the **Safe** tier:
55
+
56
+ 1. Update the version ranges in `package.json`
57
+ 2. Run `nvm use` (`.nvmrc` exists in this directory)
58
+ 3. Run `npm install`
59
+ 4. Run `npm run build`
60
+ 5. Run `npm run lint`
61
+
62
+ If build or lint fails, investigate and fix. If the failure is caused by a dependency update, move that package to the Hold tier and revert its bump.
63
+
64
+ ## Step 4: Report
65
+
66
+ Output a summary with three sections:
67
+
68
+ **Bumped** — what was updated and to which version
69
+
70
+ **Held — Vite alignment** — what was held and the Vite-aligned version vs. latest
71
+
72
+ **Held — major/breaking** — what was held and why (brief note on what the major change involves)
73
+
74
+ If everything is already up to date, say so.
@@ -23,7 +23,7 @@ Inclusivity and accessibility are more important than any other aspect of the us
23
23
 
24
24
  - **Always use aliased paths** — never relative imports
25
25
  - `from 'utils'` not `from '../../lib/utils/darkMode'`
26
- - `from 'atoms/Button'` not `from '../ui/atoms/Button'`
26
+ - `from 'atoms'` not `from '../ui/atoms/Button'`
27
27
  - If a relative path seems necessary, that means a new alias is needed — update both `tsconfig.app.json` `paths` and `vite.config.ts` `resolve.alias`
28
28
  - See `tsconfig.app.json` `paths` for the current list of aliases
29
29
 
@@ -65,7 +65,25 @@ Components follow [Atomic Design](https://atomicdesign.bradfrost.com/chapter-2/)
65
65
 
66
66
  Reference: [Atomic Design by Brad Frost](https://atomicdesign.bradfrost.com/table-of-contents/)
67
67
 
68
- - Theme prefix: `sb-` (starbase) for all color tokens
68
+ ### Component Barrel Exports
69
+
70
+ Each component level has a barrel file (`index.ts`) that re-exports all components. Every new component must be added to its level's barrel.
71
+
72
+ Components import from **lower-level barrels** for cross-level dependencies. For same-level siblings, use **direct paths**. This avoids circular dependencies.
73
+
74
+ ```tsx
75
+ // Molecule importing atoms (lower level barrel — safe)
76
+ import { Button, Input, Label } from 'atoms';
77
+
78
+ // Atom importing a sibling atom (same level — use direct path)
79
+ import { Button } from 'atoms/Button';
80
+
81
+ // Route file importing from atoms and molecules (barrel — safe)
82
+ import { Code, RouterLink } from 'atoms';
83
+ import { PageHeader } from 'molecules';
84
+ ```
85
+
86
+ The rule: **never import from your own level's barrel** — it re-exports you, creating a circular dependency.
69
87
 
70
88
  ## Libraries
71
89
 
@@ -85,6 +103,7 @@ When adding a new library, always do a fresh web search for the latest docs, cha
85
103
 
86
104
  ## Color Token System
87
105
 
106
+ - Theme prefix: `sb-` (starbase) for all color tokens
88
107
  - CSS variables defined in `src/lib/theme/tailwind.css`
89
108
  - Dark mode: `.dark` class on `<html>`, with flash-prevention script in `index.html`
90
109
  - Cookie: `theme-preference` with values `light`, `dark`, or absent (system default)
@@ -28,7 +28,7 @@ src/
28
28
  routes/ # TanStack Router file-based routes
29
29
  ```
30
30
 
31
- Components follow [Atomic Design](https://atomicdesign.bradfrost.com/chapter-2/). Imports use path aliases -- `from 'atoms/Button'`, not relative paths.
31
+ Components follow [Atomic Design](https://atomicdesign.bradfrost.com/chapter-2/). Imports use path aliases and barrel files -- `from 'atoms'`, not relative paths. See CLAUDE.md for the full convention.
32
32
 
33
33
  ## CLAUDE.md
34
34
 
@@ -38,6 +38,7 @@ Start here. It's the source of truth for all conventions, patterns, and architec
38
38
 
39
39
  - **`/audit`** -- Scan the codebase for drift against CLAUDE.md conventions
40
40
  - **`/review`** -- Review current branch changes against CLAUDE.md
41
+ - **`/update-deps`** -- Update dependencies safely with Vite-alignment awareness
41
42
 
42
43
  ## Learn more
43
44
 
@@ -1,9 +1,11 @@
1
1
  import js from '@eslint/js';
2
- import globals from 'globals';
2
+ import tanstackQuery from '@tanstack/eslint-plugin-query';
3
+ import importPlugin from 'eslint-plugin-import';
3
4
  import reactDom from 'eslint-plugin-react-dom';
4
5
  import reactHooks from 'eslint-plugin-react-hooks';
5
6
  import reactRefresh from 'eslint-plugin-react-refresh';
6
7
  import reactX from 'eslint-plugin-react-x';
8
+ import globals from 'globals';
7
9
  import tseslint from 'typescript-eslint';
8
10
  import { defineConfig, globalIgnores } from 'eslint/config';
9
11
 
@@ -18,10 +20,58 @@ export default defineConfig([
18
20
  reactDom.configs.recommended,
19
21
  reactHooks.configs.flat.recommended,
20
22
  reactRefresh.configs.vite,
23
+ tanstackQuery.configs['flat/recommended'],
21
24
  ],
25
+ plugins: {
26
+ import: importPlugin,
27
+ },
22
28
  languageOptions: {
23
29
  ecmaVersion: 2020,
24
30
  globals: globals.browser,
25
31
  },
32
+ settings: {
33
+ 'import/resolver': {
34
+ node: true,
35
+ },
36
+ },
37
+ rules: {
38
+ 'import/no-duplicates': 'warn',
39
+ 'import/order': [
40
+ 'warn',
41
+ {
42
+ groups: [
43
+ 'builtin',
44
+ 'external',
45
+ 'internal',
46
+ 'parent',
47
+ 'sibling',
48
+ 'index',
49
+ ],
50
+ pathGroups: [
51
+ {
52
+ pattern: 'react',
53
+ group: 'external',
54
+ position: 'before',
55
+ },
56
+ {
57
+ pattern: '{queries,utils}',
58
+ group: 'internal',
59
+ position: 'before',
60
+ },
61
+ {
62
+ pattern: '{atoms,molecules,organisms,templates}{,/**}',
63
+ group: 'internal',
64
+ position: 'after',
65
+ },
66
+ ],
67
+ pathGroupsExcludedImportTypes: ['react'],
68
+ 'newlines-between': 'never',
69
+ alphabetize: {
70
+ order: 'asc',
71
+ caseInsensitive: true,
72
+ },
73
+ },
74
+ ],
75
+ },
26
76
  },
27
77
  ]);
@@ -2,11 +2,11 @@
2
2
  <html lang="en" style="background-color: #f5f5f4">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Starbase</title>
7
+ <title>starbase.dev</title>
8
8
  <script>
9
- // Inline script to prevent dark mode flash
9
+ // Prevent dark mode flash keep colors in sync with --sb-surface in tailwind.css
10
10
  (function () {
11
11
  const COOKIE_NAME = 'theme-preference';
12
12
  const DARK_CLASS = 'dark';
@@ -34,7 +34,7 @@
34
34
  // Apply dark class only if dark mode
35
35
  if (theme === 'dark') {
36
36
  document.documentElement.classList.add(DARK_CLASS);
37
- document.documentElement.style.backgroundColor = '#09090b';
37
+ document.documentElement.style.backgroundColor = '#18181b';
38
38
  }
39
39
  })();
40
40
  </script>
@@ -3,11 +3,15 @@
3
3
  "private": true,
4
4
  "version": "0.0.0",
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": ">=24"
8
+ },
6
9
  "scripts": {
7
10
  "dev": "vite",
8
11
  "build": "tsc -b && vite build",
9
12
  "format": "prettier --write .",
10
13
  "lint": "eslint .",
14
+ "lint:fix": "eslint --fix .",
11
15
  "preview": "vite preview"
12
16
  },
13
17
  "dependencies": {
@@ -16,8 +20,8 @@
16
20
  "@fontsource/roboto-mono": "^5.2.8",
17
21
  "@tanstack/react-query": "^5.90.20",
18
22
  "@tanstack/react-query-devtools": "^5.91.3",
19
- "@tanstack/react-router": "^1.158.1",
20
- "@tanstack/react-router-devtools": "^1.158.1",
23
+ "@tanstack/react-router": "^1.158.4",
24
+ "@tanstack/react-router-devtools": "^1.158.4",
21
25
  "clsx": "^2.1.1",
22
26
  "js-cookie": "^3.0.5",
23
27
  "motion": "^12.33.0",
@@ -30,17 +34,19 @@
30
34
  "devDependencies": {
31
35
  "@eslint/js": "^9.39.1",
32
36
  "@tailwindcss/vite": "^4.1.18",
33
- "@tanstack/router-plugin": "^1.158.1",
37
+ "@tanstack/eslint-plugin-query": "^5.91.4",
38
+ "@tanstack/router-plugin": "^1.158.4",
34
39
  "@types/js-cookie": "^3.0.6",
35
40
  "@types/node": "^24.10.1",
36
41
  "@types/react": "^19.2.5",
37
42
  "@types/react-dom": "^19.2.3",
38
43
  "@vitejs/plugin-react": "^5.1.1",
39
44
  "eslint": "^9.39.1",
40
- "eslint-plugin-react-dom": "^2.11.0",
45
+ "eslint-plugin-import": "^2.32.0",
46
+ "eslint-plugin-react-dom": "^2.12.1",
41
47
  "eslint-plugin-react-hooks": "^7.0.1",
42
48
  "eslint-plugin-react-refresh": "^0.4.24",
43
- "eslint-plugin-react-x": "^2.11.0",
49
+ "eslint-plugin-react-x": "^2.12.1",
44
50
  "globals": "^16.5.0",
45
51
  "prettier": "^3.8.1",
46
52
  "tailwindcss": "^4.1.18",
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 576">
2
+ <path
3
+ transform="translate(288,288) translate(-288,-256)"
4
+ d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"
5
+ fill="#ef4444"
6
+ stroke="#000"
7
+ stroke-width="30"
8
+ stroke-linejoin="round"
9
+ />
10
+ </svg>
@@ -10,8 +10,7 @@
10
10
  h1,
11
11
  h2,
12
12
  h3,
13
- h4,
14
- h5 {
13
+ h4 {
15
14
  @apply font-display font-medium;
16
15
  }
17
16
 
@@ -30,8 +29,4 @@
30
29
  h4 {
31
30
  @apply text-h4;
32
31
  }
33
-
34
- h5 {
35
- @apply text-h5;
36
- }
37
32
  }
@@ -22,33 +22,26 @@
22
22
  --text-h1--letter-spacing: -0.01em;
23
23
 
24
24
  /* Text Variant: h2 */
25
- /* 20px24px */
26
- --text-h2: clamp(1.25rem, 1.1rem + 0.45vw, 1.5rem);
25
+ /* 18px20px */
26
+ --text-h2: clamp(1.125rem, 1.075rem + 0.15vw, 1.25rem);
27
27
  --text-h2--font-weight: 600;
28
- --text-h2--line-height: 1.2;
28
+ --text-h2--line-height: 1.25;
29
29
  --text-h2--letter-spacing: -0.005em;
30
30
 
31
31
  /* Text Variant: h3 */
32
- /* 18px22px */
33
- --text-h3: clamp(1.125rem, 1rem + 0.4vw, 1.375rem);
32
+ /* 17px18px */
33
+ --text-h3: clamp(1.0625rem, 1.037rem + 0.098vw, 1.125rem);
34
34
  --text-h3--font-weight: 600;
35
- --text-h3--line-height: 1.25;
36
- --text-h3--letter-spacing: -0.005em;
35
+ --text-h3--line-height: 1.3;
36
+ --text-h3--letter-spacing: 0;
37
37
 
38
38
  /* Text Variant: h4 */
39
- /* 17px20px */
40
- --text-h4: clamp(1.0625rem, 0.975rem + 0.275vw, 1.25rem);
39
+ /* 16px17px */
40
+ --text-h4: clamp(1rem, 0.974rem + 0.098vw, 1.0625rem);
41
41
  --text-h4--font-weight: 600;
42
- --text-h4--line-height: 1.3;
42
+ --text-h4--line-height: 1.35;
43
43
  --text-h4--letter-spacing: 0;
44
44
 
45
- /* Text Variant: h5 */
46
- /* 16px → 18px */
47
- --text-h5: clamp(1rem, 0.95rem + 0.15vw, 1.125rem);
48
- --text-h5--font-weight: 500;
49
- --text-h5--line-height: 1.35;
50
- --text-h5--letter-spacing: 0;
51
-
52
45
  /* Text Variant: base - 16px to 17px */
53
46
  --text-base: clamp(1rem, 0.974rem + 0.098vw, 1.063rem);
54
47
  --text-base--font-weight: 400;
@@ -61,14 +54,9 @@
61
54
  --text-sm--line-height: 1.5;
62
55
  --text-sm--letter-spacing: 0.005em;
63
56
 
64
- /* Text Variant: xs - 14px to 15px */
65
- --text-xs: clamp(0.875rem, 0.849rem + 0.098vw, 0.9375rem);
66
- --text-xs--font-weight: 400;
67
- --text-xs--line-height: 1.45;
68
- --text-xs--letter-spacing: 0.01em;
69
-
70
57
  /* Spacing (padding, margins, height, width, etc) */
71
- --spacing-wrapper-page-full-x: clamp(0.75rem, 0.25rem + 1.5vw, 1rem);
58
+ --spacing-page: 64rem;
59
+ --spacing-page-x: clamp(0.75rem, 0.25rem + 1.5vw, 1rem);
72
60
 
73
61
  /* List Style Types */
74
62
  --list-style-type-circle: circle;
@@ -4,8 +4,8 @@ import { extendTailwindMerge } from 'tailwind-merge';
4
4
  const twMerge = extendTailwindMerge({
5
5
  extend: {
6
6
  theme: {
7
- spacing: ['wrapper-page-full-x'],
8
- text: ['h1', 'h2', 'h3', 'h4', 'h5', 'base', 'sm', 'xs'],
7
+ spacing: ['page', 'page-x'],
8
+ text: ['h1', 'h2', 'h3', 'h4', 'base', 'sm'],
9
9
  },
10
10
  classGroups: {
11
11
  'list-style-type': [{ list: ['circle', 'roman'] }],
@@ -36,7 +36,7 @@ export function getEffectiveTheme(
36
36
  export function applyTheme(theme: 'light' | 'dark'): void {
37
37
  if (theme === 'dark') {
38
38
  document.documentElement.classList.add(DARK_CLASS);
39
- document.documentElement.style.backgroundColor = 'var(--sb-canvas)';
39
+ document.documentElement.style.backgroundColor = 'var(--sb-surface)';
40
40
  } else {
41
41
  document.documentElement.classList.remove(DARK_CLASS);
42
42
  document.documentElement.style.backgroundColor = 'var(--sb-surface)';
@@ -1,8 +1,4 @@
1
1
  import { StrictMode } from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
- import { RouterProvider, createRouter } from '@tanstack/react-router';
5
-
6
2
  import '@fontsource/bricolage-grotesque/400.css';
7
3
  import '@fontsource/bricolage-grotesque/500.css';
8
4
  import '@fontsource/bricolage-grotesque/600.css';
@@ -10,9 +6,10 @@ import '@fontsource/nunito/400.css';
10
6
  import '@fontsource/nunito/500.css';
11
7
  import '@fontsource/nunito/600.css';
12
8
  import '@fontsource/roboto-mono/400.css';
13
-
9
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
10
+ import { RouterProvider, createRouter } from '@tanstack/react-router';
11
+ import { createRoot } from 'react-dom/client';
14
12
  import './lib/theme/app.css';
15
-
16
13
  import { routeTree } from './routeTree.gen';
17
14
 
18
15
  const queryClient = new QueryClient();
@@ -5,8 +5,7 @@ import {
5
5
  createRootRouteWithContext,
6
6
  Outlet,
7
7
  } from '@tanstack/react-router';
8
- import { DarkModeToggle } from 'molecules/DarkModeToggle';
9
- import { Stargazers } from 'molecules/Stargazers';
8
+ import { SiteLayout } from 'templates';
10
9
 
11
10
  export interface RouterContext {
12
11
  queryClient: QueryClient;
@@ -35,13 +34,9 @@ export const Route = createRootRouteWithContext<RouterContext>()({
35
34
  component: () => (
36
35
  <>
37
36
  <HeadContent />
38
- <main className="flex min-h-screen flex-col items-center justify-center gap-6 p-8">
37
+ <SiteLayout>
39
38
  <Outlet />
40
- <footer className="flex items-center gap-3">
41
- <Stargazers />
42
- <DarkModeToggle />
43
- </footer>
44
- </main>
39
+ </SiteLayout>
45
40
  <TanStackRouterDevtools />
46
41
  <ReactQueryDevtools />
47
42
  </>
@@ -1,29 +1,56 @@
1
1
  import { createFileRoute } from '@tanstack/react-router';
2
- import { Code } from 'atoms/Code';
3
- import { RouterLink } from 'atoms/Link';
4
- import { PageHeader } from 'molecules/PageHeader';
2
+ import { motion, useReducedMotion } from 'motion/react';
3
+ import { Code, StarbaseLogo } from 'atoms';
5
4
 
6
5
  export const Route = createFileRoute('/')({
7
6
  component: Index,
8
7
  head: () => ({
9
- meta: [{ title: 'Starbase' }],
8
+ meta: [
9
+ { title: 'Opinionated React Starter for Claude Code | starbase.dev' },
10
+ {
11
+ name: 'description',
12
+ content:
13
+ 'A launchpad for modern React apps, optimized for Claude Code. Built on Vite, TypeScript, Tailwind CSS, TanStack, and atomic design.',
14
+ },
15
+ ],
10
16
  }),
11
17
  });
12
18
 
13
19
  function Index() {
14
- return (
15
- <div className="flex flex-col items-center gap-6">
16
- <PageHeader title="Starbase" />
17
- <p className="text-sb-fg-subtle max-w-md text-center text-balance">
18
- A launchpad for modern React apps, built on Vite, TypeScript, Tailwind
19
- CSS, TanStack Router, and TanStack Query. Start your mission today:
20
- </p>
20
+ const prefersReducedMotion = useReducedMotion();
21
21
 
22
- <Code>npm create starbase@latest</Code>
22
+ return (
23
+ <div className="flex flex-1 flex-col items-center justify-center gap-10">
24
+ <div className="flex flex-col items-center gap-3">
25
+ <div className="flex flex-col items-center gap-4">
26
+ <motion.div
27
+ initial={prefersReducedMotion ? false : { y: -20, rotate: 12 }}
28
+ animate={{ y: 0, rotate: [16, 8, 14, 10, 12] }}
29
+ transition={
30
+ prefersReducedMotion
31
+ ? { duration: 0 }
32
+ : { duration: 0.5, ease: 'easeOut' }
33
+ }
34
+ whileHover={
35
+ prefersReducedMotion
36
+ ? undefined
37
+ : { rotate: [12, 4, 20, 6, 18, 9, 15, 12] }
38
+ }
39
+ >
40
+ <StarbaseLogo className="size-12" />
41
+ </motion.div>
42
+ <h1 className="text-sb-fg-title">Starbase</h1>
43
+ </div>
44
+ <p className="text-sb-fg-subtle max-w-md text-center text-balance">
45
+ A launchpad for modern React apps, optimized for Claude Code. Built on
46
+ Vite, TypeScript, Tailwind CSS, and TanStack.
47
+ </p>
48
+ </div>
23
49
 
24
- <RouterLink to="/liftoff" className="text-sm">
25
- Ready for liftoff?
26
- </RouterLink>
50
+ <div className="flex flex-col items-center gap-3">
51
+ <h2 className="text-sb-fg">Start your mission today:</h2>
52
+ <Code>npm create starbase@latest</Code>
53
+ </div>
27
54
  </div>
28
55
  );
29
56
  }
@@ -1,22 +1,118 @@
1
1
  import { createFileRoute } from '@tanstack/react-router';
2
- import { RouterLink } from 'atoms/Link';
3
- import { PageHeader } from 'molecules/PageHeader';
2
+ import {
3
+ LuAtom,
4
+ LuContrast,
5
+ LuLayers,
6
+ LuPaintbrush,
7
+ LuRocket,
8
+ LuRoute,
9
+ LuSearch,
10
+ LuSparkles,
11
+ LuSquareActivity,
12
+ } from 'react-icons/lu';
13
+ import { Link } from 'atoms';
14
+ import { FeatureCard } from 'molecules';
4
15
 
5
16
  export const Route = createFileRoute('/liftoff')({
6
17
  component: Liftoff,
7
18
  head: () => ({
8
- meta: [{ title: 'Liftoff — Starbase' }],
19
+ meta: [
20
+ { title: "What's On Board | starbase.dev" },
21
+ {
22
+ name: 'description',
23
+ content:
24
+ "What's included in Starbase: Claude Code integration, React 19, TanStack Router & Query, Tailwind CSS, dark mode theming, atomic design, and AA 2.2 a11y.",
25
+ },
26
+ ],
9
27
  }),
10
28
  });
11
29
 
30
+ const features = [
31
+ {
32
+ icon: LuSparkles,
33
+ title: 'Claude Code',
34
+ description:
35
+ 'Ships with CLAUDE.md conventions, project memory, and custom commands. Ready for AI-assisted development from the start.',
36
+ },
37
+ {
38
+ icon: LuAtom,
39
+ title: 'React 19 + Vite',
40
+ description:
41
+ 'Latest React with Vite for instant HMR, fast builds, and a modern developer experience.',
42
+ },
43
+ {
44
+ icon: LuRoute,
45
+ title: 'TanStack Router',
46
+ description:
47
+ 'File-based routing with type-safe navigation, preloading on intent, and automatic code splitting.',
48
+ },
49
+ {
50
+ icon: LuSquareActivity,
51
+ title: 'TanStack Query',
52
+ description:
53
+ 'Declarative data fetching with caching, background updates, and stale-while-revalidate. See the stargazer count above for a live example.',
54
+ },
55
+ {
56
+ icon: LuPaintbrush,
57
+ title: 'Tailwind CSS',
58
+ description:
59
+ 'Utility-first styling with custom theme tokens, responsive typography, and a semantic color system.',
60
+ },
61
+ {
62
+ icon: LuContrast,
63
+ title: 'Theming',
64
+ description:
65
+ 'Dark and light modes with semantic color tokens, system preference detection, and zero-flash on load.',
66
+ },
67
+ {
68
+ icon: LuLayers,
69
+ title: 'Atomic Design',
70
+ description:
71
+ 'Components organized as atoms, molecules, organisms, and templates, scaling from buttons to full page layouts.',
72
+ },
73
+ {
74
+ icon: LuSearch,
75
+ title: 'Accessibility',
76
+ description:
77
+ 'WCAG 2.2 AA baseline with semantic HTML, keyboard navigation, focus indicators, and skip-to-content.',
78
+ },
79
+ ] as const;
80
+
12
81
  function Liftoff() {
13
82
  return (
14
- <div className="flex flex-col items-center gap-6">
15
- <PageHeader title="Liftoff" />
16
- <p className="text-sb-fg-subtle">More details coming soon.</p>
17
- <RouterLink to="/" className="text-sm">
18
- Back to home
19
- </RouterLink>
83
+ <div className="flex w-full max-w-3xl flex-col items-center gap-10 sm:gap-14">
84
+ <div className="flex flex-col items-center gap-4">
85
+ <h1 className="text-sb-fg-title">
86
+ Ready for liftoff!{' '}
87
+ <LuRocket
88
+ className="inline size-[1em] align-middle"
89
+ aria-hidden="true"
90
+ />
91
+ </h1>
92
+ <p className="max-w-lg text-center text-balance text-sb-fg-subtle">
93
+ An opinionated React starter, built with Claude Code in mind. Just
94
+ enough structure to ship fast without starting from scratch.
95
+ </p>
96
+ </div>
97
+
98
+ <h2 className="sr-only">Features</h2>
99
+ <ul className="grid w-full grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-5">
100
+ {features.map((feature, i) => (
101
+ <FeatureCard key={feature.title} index={i} {...feature} />
102
+ ))}
103
+ </ul>
104
+
105
+ <p className="pb-4 text-center text-sm text-sb-fg-subtle">
106
+ Full documentation and source on{' '}
107
+ <Link
108
+ href="https://github.com/bstaruk/starbase"
109
+ target="_blank"
110
+ rel="noopener noreferrer"
111
+ >
112
+ GitHub
113
+ <span className="sr-only"> (opens in new tab)</span>
114
+ </Link>
115
+ </p>
20
116
  </div>
21
117
  );
22
118
  }
@@ -8,21 +8,18 @@ type ButtonSize = 'sm' | 'md' | 'lg';
8
8
 
9
9
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
10
10
  variant?: ButtonVariant;
11
- iconOnly?: boolean;
12
11
  size?: ButtonSize;
13
12
  ref?: React.Ref<HTMLButtonElement>;
14
13
  }
15
14
 
16
15
  interface ButtonLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
17
16
  variant?: ButtonVariant;
18
- iconOnly?: boolean;
19
17
  size?: ButtonSize;
20
18
  ref?: React.Ref<HTMLAnchorElement>;
21
19
  }
22
20
 
23
21
  const makeButtonClasses = (
24
22
  variant?: ButtonVariant,
25
- iconOnly?: boolean,
26
23
  size: ButtonSize = 'md',
27
24
  className?: string,
28
25
  ) =>
@@ -30,18 +27,13 @@ const makeButtonClasses = (
30
27
  // Base styles
31
28
  'inline-flex items-center justify-center',
32
29
  'font-sans font-semibold rounded-md border border-transparent outline-none focus-visible:outline-sb-action cursor-pointer',
33
- 'transition-all duration-150 ease-out',
30
+ 'motion-safe:transition-all motion-safe:duration-150 ease-out',
34
31
  'disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none',
35
32
  {
36
- /* iconOnly sizes (square buttons) */
37
- 'shrink-0 p-0 size-8': iconOnly && size === 'sm',
38
- 'shrink-0 p-0 size-9': iconOnly && size === 'md',
39
- 'shrink-0 p-0 size-11 rounded-lg': iconOnly && size === 'lg',
40
-
41
- /* text button sizes */
42
- 'text-sm px-3 py-1.5': !iconOnly && size === 'sm',
43
- 'text-base px-4 py-2': !iconOnly && size === 'md',
44
- 'text-lg px-5 py-2.5': !iconOnly && size === 'lg',
33
+ /* Size variants */
34
+ 'text-sm px-3 py-1.5': size === 'sm',
35
+ 'text-base px-4 py-2': size === 'md',
36
+ 'text-lg px-5 py-2.5': size === 'lg',
45
37
 
46
38
  /* Variant: anchor */
47
39
  'bg-sb-anchor border-sb-anchor text-sb-surface-raised shadow-sm is-active:bg-sb-anchor-active is-active:border-sb-anchor-active is-active:shadow-md':
@@ -61,7 +53,6 @@ export const ButtonLink = ({
61
53
  children,
62
54
  className,
63
55
  variant = 'anchor',
64
- iconOnly,
65
56
  size,
66
57
  ref,
67
58
  ...rest
@@ -70,7 +61,7 @@ export const ButtonLink = ({
70
61
  <a
71
62
  {...rest}
72
63
  ref={ref}
73
- className={makeButtonClasses(variant, iconOnly, size, className)}
64
+ className={makeButtonClasses(variant, size, className)}
74
65
  >
75
66
  {children}
76
67
  </a>
@@ -81,7 +72,6 @@ export const Button = ({
81
72
  children,
82
73
  className,
83
74
  variant = 'anchor',
84
- iconOnly,
85
75
  size,
86
76
  type = 'button',
87
77
  ref,
@@ -92,7 +82,7 @@ export const Button = ({
92
82
  {...rest}
93
83
  ref={ref}
94
84
  type={type}
95
- className={makeButtonClasses(variant, iconOnly, size, className)}
85
+ className={makeButtonClasses(variant, size, className)}
96
86
  >
97
87
  {children}
98
88
  </button>
@@ -1,12 +1,12 @@
1
1
  import { type HTMLAttributes, type Ref, useEffect, useState } from 'react';
2
+ import { LuCheck, LuCopy } from 'react-icons/lu';
2
3
  import { useCopyToClipboard } from 'usehooks-ts';
3
- import { LuCopy, LuCheck } from 'react-icons/lu';
4
- import { Button } from 'atoms/Button';
5
4
  import { cn } from 'utils';
5
+ import { Button } from 'atoms/Button';
6
6
 
7
- export interface CodeProps extends HTMLAttributes<HTMLElement> {
7
+ export interface CodeProps extends HTMLAttributes<HTMLDivElement> {
8
8
  children: string;
9
- ref?: Ref<HTMLElement>;
9
+ ref?: Ref<HTMLDivElement>;
10
10
  }
11
11
 
12
12
  export const Code = ({ children, className, ref, ...rest }: CodeProps) => {
@@ -24,29 +24,31 @@ export const Code = ({ children, className, ref, ...rest }: CodeProps) => {
24
24
  };
25
25
 
26
26
  return (
27
- <code
27
+ <div
28
28
  {...rest}
29
29
  ref={ref}
30
30
  className={cn(
31
- 'inline-flex items-center gap-2 font-mono',
31
+ 'inline-flex items-center gap-2',
32
32
  'bg-sb-canvas text-sb-fg px-4 py-2 rounded-lg',
33
33
  className,
34
34
  )}
35
35
  >
36
- {children}
36
+ <code className="font-mono">{children}</code>
37
37
  <Button
38
38
  variant="ghost"
39
- iconOnly
40
39
  size="sm"
41
40
  onClick={handleCopy}
42
- aria-label={copied ? 'Copied' : 'Copy to clipboard'}
41
+ aria-label="Copy to clipboard"
43
42
  >
44
43
  {copied ? (
45
- <LuCheck className="size-4" />
44
+ <LuCheck className="size-4" aria-hidden="true" />
46
45
  ) : (
47
- <LuCopy className="size-4" />
46
+ <LuCopy className="size-4" aria-hidden="true" />
48
47
  )}
49
48
  </Button>
50
- </code>
49
+ <span role="status" className="sr-only">
50
+ {copied ? 'Copied to clipboard' : ''}
51
+ </span>
52
+ </div>
51
53
  );
52
54
  };
@@ -20,7 +20,7 @@ type LinkButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
20
20
 
21
21
  const makeLinkClasses = (variant?: LinkVariant, className?: string) => {
22
22
  return cn(
23
- 'outline-none focus-visible:outline-sb-action transition-colors duration-100',
23
+ 'outline-none focus-visible:outline-sb-action motion-safe:transition-colors motion-safe:duration-100',
24
24
  'underline decoration-current/30 underline-offset-4',
25
25
  'hover:decoration-current focus-visible:decoration-current',
26
26
  {
@@ -0,0 +1,4 @@
1
+ export * from './Button';
2
+ export * from './Code';
3
+ export * from './Link';
4
+ export * from './StarbaseLogo';
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useState } from 'react';
2
2
  import { LuMoon, LuSun } from 'react-icons/lu';
3
- import { Button } from 'atoms/Button';
4
3
  import { darkMode } from 'utils';
4
+ import { Button } from 'atoms';
5
5
 
6
6
  export function DarkModeToggle() {
7
7
  const [isDark, setIsDark] = useState(
@@ -18,12 +18,16 @@ export function DarkModeToggle() {
18
18
  return (
19
19
  <Button
20
20
  variant="ghost"
21
- iconOnly
22
21
  size="sm"
22
+ className="p-2"
23
23
  onClick={toggle}
24
24
  aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
25
25
  >
26
- {isDark ? <LuSun size={16} /> : <LuMoon size={16} />}
26
+ {isDark ? (
27
+ <LuSun size={16} aria-hidden="true" />
28
+ ) : (
29
+ <LuMoon size={16} aria-hidden="true" />
30
+ )}
27
31
  </Button>
28
32
  );
29
33
  }
@@ -0,0 +1,41 @@
1
+ import type { ComponentType, SVGProps } from 'react';
2
+ import { motion, useReducedMotion } from 'motion/react';
3
+
4
+ interface FeatureCardProps {
5
+ icon: ComponentType<SVGProps<SVGSVGElement>>;
6
+ title: string;
7
+ description: string;
8
+ index?: number;
9
+ }
10
+
11
+ export function FeatureCard({
12
+ icon: Icon,
13
+ title,
14
+ description,
15
+ index = 0,
16
+ }: FeatureCardProps) {
17
+ const prefersReducedMotion = useReducedMotion();
18
+
19
+ return (
20
+ <motion.li
21
+ initial={prefersReducedMotion ? false : { opacity: 0, y: 12 }}
22
+ animate={{ opacity: 1, y: 0 }}
23
+ transition={
24
+ prefersReducedMotion
25
+ ? { duration: 0 }
26
+ : {
27
+ duration: 0.4,
28
+ delay: 0.1 + index * 0.06,
29
+ ease: 'easeOut',
30
+ }
31
+ }
32
+ className="rounded-xl border border-sb-divider bg-sb-surface-raised p-5"
33
+ >
34
+ <div className="mb-3 flex items-center gap-3">
35
+ <Icon className="size-5 shrink-0 text-sb-action" aria-hidden="true" />
36
+ <h3 className="text-sb-fg-title">{title}</h3>
37
+ </div>
38
+ <p className="text-sm text-sb-fg-subtle">{description}</p>
39
+ </motion.li>
40
+ );
41
+ }
@@ -1,24 +1,50 @@
1
1
  import { useQuery } from '@tanstack/react-query';
2
+ import { AnimatePresence, motion, useReducedMotion } from 'motion/react';
3
+ import { LuGithub } from 'react-icons/lu';
2
4
  import { github } from 'queries';
3
- import { Link } from 'atoms/Link';
5
+ import { ButtonLink } from 'atoms';
4
6
 
5
7
  export function Stargazers() {
6
8
  const { data } = useQuery({
7
9
  ...github.starbaseRepoQueryOptions(),
8
10
  retry: false,
9
11
  });
12
+ const prefersReducedMotion = useReducedMotion();
10
13
 
11
14
  return (
12
- <Link
15
+ <ButtonLink
13
16
  href="https://github.com/bstaruk/starbase"
14
17
  target="_blank"
15
18
  rel="noopener noreferrer"
16
- variant="fg-subtle"
19
+ variant="ghost"
20
+ size="sm"
21
+ className="p-2"
22
+ aria-label={
23
+ data
24
+ ? `${data.stargazers_count.toLocaleString()} stargazers on GitHub (opens in new tab)`
25
+ : 'Starbase on GitHub (opens in new tab)'
26
+ }
17
27
  >
18
- {data
19
- ? `${data.stargazers_count.toLocaleString()} stargazers on GitHub`
20
- : 'find us on GitHub'}
21
- <span className="sr-only"> (opens in new tab)</span>
22
- </Link>
28
+ <AnimatePresence mode="popLayout">
29
+ {data && (
30
+ <motion.span
31
+ key="count"
32
+ initial={
33
+ prefersReducedMotion ? false : { opacity: 0, filter: 'blur(4px)' }
34
+ }
35
+ animate={{ opacity: 1, filter: 'blur(0px)' }}
36
+ transition={
37
+ prefersReducedMotion
38
+ ? { duration: 0 }
39
+ : { duration: 0.4, ease: 'easeOut' }
40
+ }
41
+ className="mr-1 text-xs font-semibold tabular-nums"
42
+ >
43
+ {data.stargazers_count.toLocaleString()}
44
+ </motion.span>
45
+ )}
46
+ </AnimatePresence>
47
+ <LuGithub size={16} aria-hidden="true" />
48
+ </ButtonLink>
23
49
  );
24
50
  }
@@ -0,0 +1,3 @@
1
+ export * from './DarkModeToggle';
2
+ export * from './FeatureCard';
3
+ export * from './Stargazers';
@@ -0,0 +1,43 @@
1
+ import { RouterLink, StarbaseLogo } from 'atoms';
2
+ import { DarkModeToggle, Stargazers } from 'molecules';
3
+
4
+ const navLinkClasses = 'font-display text-sm font-medium px-0.5';
5
+
6
+ const activeProps = {
7
+ className: 'text-sb-fg-title',
8
+ 'aria-current': 'page' as const,
9
+ };
10
+
11
+ export function SiteHeader() {
12
+ return (
13
+ <header className="flex items-center py-3">
14
+ <nav aria-label="Main" className="flex items-center gap-4 sm:gap-5">
15
+ <RouterLink
16
+ to="/"
17
+ variant="fg-subtle"
18
+ className={`${navLinkClasses} flex items-center gap-2 font-semibold tracking-tight`}
19
+ activeProps={activeProps}
20
+ >
21
+ <StarbaseLogo className="size-5 shrink-0" />
22
+ Starbase
23
+ </RouterLink>
24
+
25
+ <span aria-hidden="true" className="h-4 w-px bg-sb-divider" />
26
+
27
+ <RouterLink
28
+ to="/liftoff"
29
+ variant="fg-subtle"
30
+ className={navLinkClasses}
31
+ activeProps={activeProps}
32
+ >
33
+ Liftoff
34
+ </RouterLink>
35
+ </nav>
36
+
37
+ <div className="ml-auto flex items-center gap-1">
38
+ <Stargazers />
39
+ <DarkModeToggle />
40
+ </div>
41
+ </header>
42
+ );
43
+ }
@@ -0,0 +1 @@
1
+ export * from './SiteHeader';
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from 'react';
2
+ import { SiteHeader } from 'organisms';
3
+
4
+ interface SiteLayoutProps {
5
+ children: ReactNode;
6
+ }
7
+
8
+ export function SiteLayout({ children }: SiteLayoutProps) {
9
+ return (
10
+ <div className="mx-auto flex min-h-screen w-full max-w-page flex-col bg-sb-surface px-page-x">
11
+ <a
12
+ href="#main-content"
13
+ className="sr-only focus:not-sr-only focus:absolute focus:left-page-x focus:top-3 focus:z-50 focus:rounded-md focus:bg-sb-surface-raised focus:px-3 focus:py-1.5 focus:text-sm focus:font-medium focus:text-sb-fg focus:shadow-sm focus:ring-1 focus:ring-sb-divider focus:outline-none"
14
+ >
15
+ Skip to content
16
+ </a>
17
+ <SiteHeader />
18
+ <main
19
+ id="main-content"
20
+ className="flex flex-1 flex-col items-center py-6 sm:py-10"
21
+ >
22
+ {children}
23
+ </main>
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1 @@
1
+ export * from './SiteLayout';
@@ -29,9 +29,13 @@
29
29
  "paths": {
30
30
  "queries": ["src/lib/queries/index.ts"],
31
31
  "utils": ["src/lib/utils/index.ts"],
32
+ "atoms": ["src/ui/atoms/index.ts"],
32
33
  "atoms/*": ["src/ui/atoms/*"],
34
+ "molecules": ["src/ui/molecules/index.ts"],
33
35
  "molecules/*": ["src/ui/molecules/*"],
36
+ "organisms": ["src/ui/organisms/index.ts"],
34
37
  "organisms/*": ["src/ui/organisms/*"],
38
+ "templates": ["src/ui/templates/index.ts"],
35
39
  "templates/*": ["src/ui/templates/*"]
36
40
  }
37
41
  },
@@ -1,8 +1,8 @@
1
1
  import path from 'path';
2
- import { defineConfig } from 'vite';
2
+ import tailwindcss from '@tailwindcss/vite';
3
3
  import { tanstackRouter } from '@tanstack/router-plugin/vite';
4
4
  import react from '@vitejs/plugin-react';
5
- import tailwindcss from '@tailwindcss/vite';
5
+ import { defineConfig } from 'vite';
6
6
 
7
7
  // https://vite.dev/config/
8
8
  export default defineConfig({
@@ -1,35 +0,0 @@
1
- import { motion, useReducedMotion } from 'motion/react';
2
- import { useLocation } from '@tanstack/react-router';
3
- import { StarbaseLogo } from 'atoms/StarbaseLogo';
4
-
5
- interface PageHeaderProps {
6
- title: string;
7
- }
8
-
9
- export function PageHeader({ title }: PageHeaderProps) {
10
- const { pathname } = useLocation();
11
- const prefersReducedMotion = useReducedMotion();
12
-
13
- return (
14
- <header className="flex flex-col items-center gap-4">
15
- <motion.div
16
- key={pathname}
17
- initial={prefersReducedMotion ? false : { y: -20, rotate: 12 }}
18
- animate={{ y: 0, rotate: [16, 8, 14, 10, 12] }}
19
- transition={
20
- prefersReducedMotion
21
- ? { duration: 0 }
22
- : { duration: 0.5, ease: 'easeOut' }
23
- }
24
- whileHover={
25
- prefersReducedMotion
26
- ? undefined
27
- : { rotate: [12, 4, 20, 6, 18, 9, 15, 12] }
28
- }
29
- >
30
- <StarbaseLogo className="size-12" />
31
- </motion.div>
32
- <h1 className="text-sb-fg-title">{title}</h1>
33
- </header>
34
- );
35
- }
File without changes
File without changes