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.
- package/README.md +2 -1
- package/dist/index.js +56 -0
- package/package.json +3 -3
- package/template/.claude/commands/update-deps.md +74 -0
- package/template/CLAUDE.md +21 -2
- package/template/README.md +2 -1
- package/template/eslint.config.js +51 -1
- package/template/index.html +4 -4
- package/template/package.json +11 -5
- package/template/public/favicon.svg +10 -0
- package/template/src/lib/theme/base.css +1 -6
- package/template/src/lib/theme/tailwind.css +12 -24
- package/template/src/lib/utils/cn.ts +2 -2
- package/template/src/lib/utils/darkMode.ts +1 -1
- package/template/src/main.tsx +3 -6
- package/template/src/routes/__root.tsx +3 -8
- package/template/src/routes/index.tsx +42 -15
- package/template/src/routes/liftoff.tsx +105 -9
- package/template/src/ui/atoms/Button.tsx +7 -17
- package/template/src/ui/atoms/Code.tsx +14 -12
- package/template/src/ui/atoms/Link.tsx +1 -1
- package/template/src/ui/atoms/index.ts +4 -0
- package/template/src/ui/molecules/DarkModeToggle.tsx +7 -3
- package/template/src/ui/molecules/FeatureCard.tsx +41 -0
- package/template/src/ui/molecules/Stargazers.tsx +34 -8
- package/template/src/ui/molecules/index.ts +3 -0
- package/template/src/ui/organisms/SiteHeader.tsx +43 -0
- package/template/src/ui/organisms/index.ts +1 -0
- package/template/src/ui/templates/SiteLayout.tsx +26 -0
- package/template/src/ui/templates/index.ts +1 -0
- package/template/tsconfig.app.json +4 -0
- package/template/vite.config.ts +2 -2
- package/template/src/ui/molecules/PageHeader.tsx +0 -35
- package/template/src/ui/organisms/.gitkeep +0 -0
- 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
|
-
|
|
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.
|
|
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": "^
|
|
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": ">=
|
|
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.
|
package/template/CLAUDE.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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)
|
package/template/README.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
]);
|
package/template/index.html
CHANGED
|
@@ -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="/
|
|
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>
|
|
7
|
+
<title>starbase.dev</title>
|
|
8
8
|
<script>
|
|
9
|
-
//
|
|
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 = '#
|
|
37
|
+
document.documentElement.style.backgroundColor = '#18181b';
|
|
38
38
|
}
|
|
39
39
|
})();
|
|
40
40
|
</script>
|
package/template/package.json
CHANGED
|
@@ -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.
|
|
20
|
-
"@tanstack/react-router-devtools": "^1.158.
|
|
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/
|
|
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-
|
|
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.
|
|
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>
|
|
@@ -22,33 +22,26 @@
|
|
|
22
22
|
--text-h1--letter-spacing: -0.01em;
|
|
23
23
|
|
|
24
24
|
/* Text Variant: h2 */
|
|
25
|
-
/*
|
|
26
|
-
--text-h2: clamp(1.
|
|
25
|
+
/* 18px → 20px */
|
|
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.
|
|
28
|
+
--text-h2--line-height: 1.25;
|
|
29
29
|
--text-h2--letter-spacing: -0.005em;
|
|
30
30
|
|
|
31
31
|
/* Text Variant: h3 */
|
|
32
|
-
/*
|
|
33
|
-
--text-h3: clamp(1.
|
|
32
|
+
/* 17px → 18px */
|
|
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.
|
|
36
|
-
--text-h3--letter-spacing:
|
|
35
|
+
--text-h3--line-height: 1.3;
|
|
36
|
+
--text-h3--letter-spacing: 0;
|
|
37
37
|
|
|
38
38
|
/* Text Variant: h4 */
|
|
39
|
-
/*
|
|
40
|
-
--text-h4: clamp(
|
|
39
|
+
/* 16px → 17px */
|
|
40
|
+
--text-h4: clamp(1rem, 0.974rem + 0.098vw, 1.0625rem);
|
|
41
41
|
--text-h4--font-weight: 600;
|
|
42
|
-
--text-h4--line-height: 1.
|
|
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-
|
|
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: ['
|
|
8
|
-
text: ['h1', 'h2', 'h3', 'h4', '
|
|
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-
|
|
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)';
|
package/template/src/main.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
<
|
|
37
|
+
<SiteLayout>
|
|
39
38
|
<Outlet />
|
|
40
|
-
|
|
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 {
|
|
3
|
-
import {
|
|
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: [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
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 {
|
|
3
|
-
|
|
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: [
|
|
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-
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
/*
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
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,
|
|
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,
|
|
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<
|
|
7
|
+
export interface CodeProps extends HTMLAttributes<HTMLDivElement> {
|
|
8
8
|
children: string;
|
|
9
|
-
ref?: Ref<
|
|
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
|
-
<
|
|
27
|
+
<div
|
|
28
28
|
{...rest}
|
|
29
29
|
ref={ref}
|
|
30
30
|
className={cn(
|
|
31
|
-
'inline-flex items-center gap-2
|
|
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=
|
|
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
|
-
|
|
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
|
{
|
|
@@ -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 ?
|
|
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 {
|
|
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
|
-
<
|
|
15
|
+
<ButtonLink
|
|
13
16
|
href="https://github.com/bstaruk/starbase"
|
|
14
17
|
target="_blank"
|
|
15
18
|
rel="noopener noreferrer"
|
|
16
|
-
variant="
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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,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
|
},
|
package/template/vite.config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import
|
|
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
|
|
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
|