bsmnt 0.0.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/.changeset/2026-02-11-test-patch-bump.md +5 -0
- package/.changeset/README.md +10 -0
- package/.changeset/config.json +16 -0
- package/.cursor/rules/README.md +184 -0
- package/.cursor/rules/architecture.mdc +437 -0
- package/.cursor/rules/components.mdc +436 -0
- package/.cursor/rules/integrations.mdc +447 -0
- package/.cursor/rules/main.mdc +278 -0
- package/.cursor/rules/styling.mdc +433 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/workflows/.gitkeep +0 -0
- package/.github/workflows/ci.yml +37 -0
- package/.github/workflows/release.yml +54 -0
- package/.tldr/cache/call_graph.json +7 -0
- package/.tldr/languages.json +6 -0
- package/.tldr/status +1 -0
- package/.tldrignore +84 -0
- package/.vscode/extensions.json +20 -0
- package/.vscode/settings.json +98 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +138 -0
- package/README.md +176 -0
- package/bin/index.js +262 -0
- package/biome.json +44 -0
- package/bun.lock +496 -0
- package/changelog/04-02-26.md +86 -0
- package/changelog/05-02-26.md +101 -0
- package/changelog/09-02-26.md +83 -0
- package/docs/fix-studio-hydration.md +46 -0
- package/docs/plans/2026-01-29-sanity-smart-merge-design.md +196 -0
- package/docs/plans/2026-01-29-sanity-smart-merge-implementation.md +695 -0
- package/docs/sanity-setup-steps.md +199 -0
- package/integrations/basehub/README.md +3 -0
- package/integrations/sanity/app/api/draft-mode/disable/route.ts +7 -0
- package/integrations/sanity/app/api/draft-mode/enable/route.ts +21 -0
- package/integrations/sanity/app/api/revalidate/route.ts +37 -0
- package/integrations/sanity/app/layout.tsx +111 -0
- package/integrations/sanity/app/sitemap.ts +80 -0
- package/integrations/sanity/app/studio/[[...tool]]/page.tsx +8 -0
- package/integrations/sanity/app/studio/layout.tsx +7 -0
- package/integrations/sanity/components/ui/sanity-image/index.tsx +37 -0
- package/integrations/sanity/lib/integrations/README.md +58 -0
- package/integrations/sanity/lib/integrations/check-integration.ts +62 -0
- package/integrations/sanity/lib/integrations/sanity/README.md +144 -0
- package/integrations/sanity/lib/integrations/sanity/client.ts +30 -0
- package/integrations/sanity/lib/integrations/sanity/components/disable-draft-mode.tsx +29 -0
- package/integrations/sanity/lib/integrations/sanity/components/rich-text.tsx +73 -0
- package/integrations/sanity/lib/integrations/sanity/env.ts +38 -0
- package/integrations/sanity/lib/integrations/sanity/live/index.tsx +34 -0
- package/integrations/sanity/lib/integrations/sanity/queries.ts +99 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.cli.ts +20 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.config.ts +94 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.types.ts +337 -0
- package/integrations/sanity/lib/integrations/sanity/schema.json +1850 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/article.ts +132 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/example.ts +203 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/index.ts +37 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/link.ts +127 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/metadata.ts +68 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/navigation.ts +39 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/page.ts +77 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/richText.ts +59 -0
- package/integrations/sanity/lib/integrations/sanity/structure.ts +5 -0
- package/integrations/sanity/lib/integrations/sanity/utils/image.ts +11 -0
- package/integrations/sanity/lib/integrations/sanity/utils/link.ts +61 -0
- package/integrations/sanity/lib/scripts/copy-sanity-mcp.ts +23 -0
- package/integrations/sanity/lib/scripts/generate-page.ts +310 -0
- package/integrations/sanity/lib/utils/metadata.ts +190 -0
- package/layers/experiment/components/layout/header/index.tsx +58 -0
- package/layers/experiment/components/layout/navigation-menu.tsx +127 -0
- package/layers/experiment/lib/constants.ts +12 -0
- package/layers/webgl/app/page.tsx +10 -0
- package/layers/webgl/components/webgl/canvas/dynamic.tsx +34 -0
- package/layers/webgl/components/webgl/canvas/index.tsx +43 -0
- package/layers/webgl/components/webgl/components/scene/index.tsx +21 -0
- package/layers/webgpu/.gitkeep +0 -0
- package/package.json +44 -0
- package/plugins/README.md +21 -0
- package/plugins/no-anchor-element.grit +11 -0
- package/plugins/no-relative-parent-imports.grit +6 -0
- package/plugins/no-unnecessary-forwardref.grit +5 -0
- package/src/commands/add-integration.js +325 -0
- package/src/commands/create.js +415 -0
- package/src/commands/setup-sanity.js +426 -0
- package/src/commands/worktree.js +805 -0
- package/src/mergers/check-integration-merger.js +105 -0
- package/src/mergers/config.js +137 -0
- package/src/mergers/index.js +355 -0
- package/src/mergers/layout-merger.js +223 -0
- package/src/mergers/next-config-merger.js +63 -0
- package/src/mergers/sitemap-merger.js +121 -0
- package/tasks/prd-next-starter-dynamic-layers.md +184 -0
- package/tasks/prd.json +153 -0
- package/tasks/progress.txt +115 -0
- package/template-hooks/use-battery.ts +126 -0
- package/template-hooks/use-device-perf.ts +184 -0
- package/template-hooks/use-intersection-observer.ts +32 -0
- package/template-hooks/use-media.ts +33 -0
package/tasks/prd.json
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project": "basement-cli",
|
|
3
|
+
"branchName": "ralph/next-starter-dynamic-layers",
|
|
4
|
+
"description": "Replace 4 static templates with next-starter clone + dynamic technology layers to eliminate maintenance burden",
|
|
5
|
+
"userStories": [
|
|
6
|
+
{
|
|
7
|
+
"id": "US-001",
|
|
8
|
+
"title": "Create layer configuration system",
|
|
9
|
+
"description": "As a CLI maintainer, I want technology layers defined in a config file so that adding or modifying layers only requires changing one file.",
|
|
10
|
+
"acceptanceCriteria": [
|
|
11
|
+
"Add LAYER_CONFIG export to src/mergers/config.js alongside existing CMS_CONFIG",
|
|
12
|
+
"Each layer config defines: replaceFiles, additivePaths, dependencies, devDependencies",
|
|
13
|
+
"Add getLayerConfig(layer) helper function export",
|
|
14
|
+
"Config contains entries for webgl, webgpu, and experiment",
|
|
15
|
+
"webgpu is a deps-only layer (empty replaceFiles and additivePaths arrays)",
|
|
16
|
+
"webgl dependencies: @react-three/fiber ^9.5.0, @react-three/drei ^10.7.7, three ^0.182.0, @gsap/react ^2.0.0, @radix-ui/react-navigation-menu ^1.2.5, leva ^0.9.35, devDeps: @types/three ^0.182.0",
|
|
17
|
+
"webgpu dependencies: @react-three/fiber 10.0.0-alpha.2, @react-three/drei ^11.0.0-alpha.4, three ^0.182.0, leva ^0.9.35, lucide-react ^0.474.0, @radix-ui/react-navigation-menu ^1.2.5, devDeps: @types/three ^0.182.0",
|
|
18
|
+
"experiment dependencies: @react-three/fiber ^9.5.0, @react-three/drei ^10.7.7, three ^0.172.0, leva ^0.9.35, lucide-react ^0.474.0, @radix-ui/react-navigation-menu ^1.2.5, devDeps: @types/three ^0.182.0",
|
|
19
|
+
"Typecheck passes"
|
|
20
|
+
],
|
|
21
|
+
"priority": 1,
|
|
22
|
+
"passes": true,
|
|
23
|
+
"notes": ""
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "US-002",
|
|
27
|
+
"title": "Extract layer files from templates",
|
|
28
|
+
"description": "As a CLI maintainer, I need the unique files from each template extracted into a layers/ directory so they can be overlaid on next-starter.",
|
|
29
|
+
"acceptanceCriteria": [
|
|
30
|
+
"Create layers/webgl/app/page.tsx copied from template/webgl/app/page.tsx (the one that imports DynamicCanvas)",
|
|
31
|
+
"Create layers/webgl/components/webgl/canvas/dynamic.tsx from template/webgl/components/webgl/canvas/dynamic.tsx",
|
|
32
|
+
"Create layers/webgl/components/webgl/canvas/index.tsx from template/webgl/components/webgl/canvas/index.tsx",
|
|
33
|
+
"Create layers/webgl/components/webgl/components/scene/index.tsx from template/webgl/components/webgl/components/scene/index.tsx",
|
|
34
|
+
"Create layers/experiment/components/layout/header/index.tsx from template/experiment/components/layout/header/index.tsx",
|
|
35
|
+
"Create layers/experiment/components/layout/navigation-menu.tsx from template/experiment/components/layout/navigation-menu.tsx",
|
|
36
|
+
"Create layers/experiment/lib/constats.ts from template/experiment/lib/constats.ts",
|
|
37
|
+
"Create layers/experiment/lib/utils/cn.ts from template/experiment/lib/utils/cn.ts",
|
|
38
|
+
"Create empty layers/webgpu/ directory with a .gitkeep file",
|
|
39
|
+
"All copied files are byte-for-byte identical to their template source files",
|
|
40
|
+
"Typecheck passes"
|
|
41
|
+
],
|
|
42
|
+
"priority": 2,
|
|
43
|
+
"passes": true,
|
|
44
|
+
"notes": ""
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "US-003",
|
|
48
|
+
"title": "Implement layer injection function",
|
|
49
|
+
"description": "As the CLI system, I need to overlay layer-specific files on top of the next-starter base so that WebGL/WebGPU/Experiment projects get their unique components.",
|
|
50
|
+
"acceptanceCriteria": [
|
|
51
|
+
"Add injectLayer(targetDir, layer, spinner) function to src/mergers/index.js",
|
|
52
|
+
"Function resolves layer directory from CLI repo's layers/{type}/ path using __dirname",
|
|
53
|
+
"Function calls existing detectPathPrefix() to handle src/ directory structure",
|
|
54
|
+
"Function copies additive files from local layers/ directory (no tiged clone needed)",
|
|
55
|
+
"Function replaces files listed in replaceFiles config (overwrites base version with overwrite: true)",
|
|
56
|
+
"Function uses existing transformPath() for path prefix support",
|
|
57
|
+
"Function returns results object with replaced, copied, skipped, and failed arrays",
|
|
58
|
+
"When layer is 'default' or not found in config, function returns early with skipped result (no error)",
|
|
59
|
+
"Extend formatMergeResults() to display replaced entries with checkmark prefix",
|
|
60
|
+
"Typecheck passes"
|
|
61
|
+
],
|
|
62
|
+
"priority": 3,
|
|
63
|
+
"passes": true,
|
|
64
|
+
"notes": "Reuses existing detectPathPrefix() and transformPath() from src/mergers/index.js. Layer files are local (no network request)."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "US-004",
|
|
68
|
+
"title": "Change create.js to clone next-starter instead of template",
|
|
69
|
+
"description": "As a developer using the CLI, I want my project scaffolded from the latest next-starter repo so I always get the most up-to-date base.",
|
|
70
|
+
"acceptanceCriteria": [
|
|
71
|
+
"Change tiged source on line 124 of src/commands/create.js from 'github:basementstudio/basement-cli/template/${type}#${branch}' to 'github:basementstudio/next-starter#main'",
|
|
72
|
+
"Remove the BASEMENT_CLI_BRANCH env var usage for template (keep if used elsewhere)",
|
|
73
|
+
"Delete bun.lock file after clone if it exists (since dependencies will be modified)",
|
|
74
|
+
"Update download spinner text to say 'Downloading next-starter from GitHub...' instead of mentioning template type",
|
|
75
|
+
"Update error/troubleshooting message in catch block to reference basementstudio/next-starter instead of basementstudio/basement-cli/template/{type}",
|
|
76
|
+
"Typecheck passes"
|
|
77
|
+
],
|
|
78
|
+
"priority": 4,
|
|
79
|
+
"passes": true,
|
|
80
|
+
"notes": ""
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "US-005",
|
|
84
|
+
"title": "Wire layer injection into create flow",
|
|
85
|
+
"description": "As the CLI system, I need the layer injection step between template download and CMS integration so layers are applied at the right time.",
|
|
86
|
+
"acceptanceCriteria": [
|
|
87
|
+
"Add new step between template download (step 3) and CMS integration (step 3.5) in src/commands/create.js",
|
|
88
|
+
"Layer injection only runs when type !== 'default'",
|
|
89
|
+
"Import injectLayer and formatMergeResults from src/mergers/index.js",
|
|
90
|
+
"Show ora spinner with layer name during injection (e.g. 'Applying webgl layer...')",
|
|
91
|
+
"Display injection results to user using formatMergeResults",
|
|
92
|
+
"On failure, show warning (not hard error) and continue with project creation",
|
|
93
|
+
"Typecheck passes"
|
|
94
|
+
],
|
|
95
|
+
"priority": 5,
|
|
96
|
+
"passes": true,
|
|
97
|
+
"notes": "Must run BEFORE CMS integration because layers don't modify layout.tsx, and CMS mergers need the base layout intact."
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": "US-006",
|
|
101
|
+
"title": "Config-driven dependency injection in package.json hydration",
|
|
102
|
+
"description": "As the CLI system, I need package.json hydration to read layer dependencies from LAYER_CONFIG instead of being hardcoded.",
|
|
103
|
+
"acceptanceCriteria": [
|
|
104
|
+
"Import getLayerConfig from src/mergers/config.js in create.js",
|
|
105
|
+
"Read layer dependencies from LAYER_CONFIG and merge into package.json: pkg.dependencies = { ...pkg.dependencies, ...layerConfig.dependencies }",
|
|
106
|
+
"Also merge devDependencies from layer config",
|
|
107
|
+
"CMS dependency injection (Sanity/BaseHub) still works unchanged",
|
|
108
|
+
"Animation library injection (GSAP/Framer Motion) still works unchanged",
|
|
109
|
+
"Simplify package.json handling: next-starter uses package.json directly (not package.template.json), remove the rename logic",
|
|
110
|
+
"Project name still set to user-provided name, version still set to 0.1.0",
|
|
111
|
+
"Typecheck passes"
|
|
112
|
+
],
|
|
113
|
+
"priority": 6,
|
|
114
|
+
"passes": true,
|
|
115
|
+
"notes": "next-starter uses package.json not package.template.json, so the existsSync check for package.template.json can be simplified."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "US-007",
|
|
119
|
+
"title": "Delete template directory",
|
|
120
|
+
"description": "As a CLI maintainer, I want the template/ directory removed so there's no duplicated code to maintain.",
|
|
121
|
+
"acceptanceCriteria": [
|
|
122
|
+
"Delete template/default/ directory and all contents",
|
|
123
|
+
"Delete template/webgl/ directory and all contents",
|
|
124
|
+
"Delete template/webgpu/ directory and all contents",
|
|
125
|
+
"Delete template/experiment/ directory and all contents",
|
|
126
|
+
"Grep source code for any remaining references to 'template/' paths and remove them",
|
|
127
|
+
"Grep source code for any remaining references to 'basementstudio/basement-cli/template/' and remove them",
|
|
128
|
+
"Typecheck passes"
|
|
129
|
+
],
|
|
130
|
+
"priority": 7,
|
|
131
|
+
"passes": true,
|
|
132
|
+
"notes": ""
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "US-008",
|
|
136
|
+
"title": "End-to-end verification of all template types",
|
|
137
|
+
"description": "As a CLI maintainer, I need to verify all template + CMS + animation combinations produce working projects.",
|
|
138
|
+
"acceptanceCriteria": [
|
|
139
|
+
"Run npm link to install CLI locally",
|
|
140
|
+
"Run: basement -c test-default -d -no-cms -no-animation -claude -no-hooks — project creates successfully",
|
|
141
|
+
"Run: basement -c test-webgl -webgl -no-cms -no-animation -claude -no-hooks — project has R3F components in components/webgl/",
|
|
142
|
+
"Run: basement -c test-webgpu -webgpu -no-cms -no-animation -claude -no-hooks — project has WebGPU deps in package.json",
|
|
143
|
+
"Run: basement -c test-experiment -exp -no-cms -no-animation -claude -no-hooks — project has custom header with NavigationMenu",
|
|
144
|
+
"Run: basement -c test-sanity -webgl -sanity -gsap -claude -no-hooks — project has CMS + animation + layer deps",
|
|
145
|
+
"All generated projects pass bun install without errors",
|
|
146
|
+
"Typecheck passes"
|
|
147
|
+
],
|
|
148
|
+
"priority": 8,
|
|
149
|
+
"passes": true,
|
|
150
|
+
"notes": "This is the final validation step. Each test project should be cleaned up after verification."
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
## Codebase Patterns
|
|
2
|
+
- This is a plain JS (ESM) CLI project - no TypeScript, no typecheck command. "Typecheck passes" = no syntax errors.
|
|
3
|
+
- Config pattern: export const CONFIG object + getXConfig(name) helper that returns config or null
|
|
4
|
+
- src/mergers/config.js holds all configuration (CMS_CONFIG, LAYER_CONFIG)
|
|
5
|
+
- src/mergers/index.js holds injection/merge logic (injectIntegration for CMS, injectLayer for layers)
|
|
6
|
+
- src/commands/create.js is the main CLI flow
|
|
7
|
+
- next-starter uses package.json directly (no template rename needed)
|
|
8
|
+
- Layer files are local (in layers/ directory), CMS integration files are remote (fetched via tiged)
|
|
9
|
+
- ESM requires __dirname workaround: `path.dirname(fileURLToPath(import.meta.url))`
|
|
10
|
+
- CLI binary is basement (defined in package.json bin field)
|
|
11
|
+
- Dependency injection order in create.js: layer deps → CMS deps → animation deps
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 2026-02-09 - US-001
|
|
16
|
+
- Implemented LAYER_CONFIG export with webgl, webgpu, experiment entries
|
|
17
|
+
- Added getLayerConfig() helper function
|
|
18
|
+
- Each layer config has: replaceFiles, additivePaths, dependencies, devDependencies
|
|
19
|
+
- webgpu is deps-only (empty replaceFiles and additivePaths)
|
|
20
|
+
- Files changed: src/mergers/config.js
|
|
21
|
+
- **Learnings for future iterations:**
|
|
22
|
+
- The CLI project is plain JS ESM, verified by running `node --input-type=module -e "import {...}"`
|
|
23
|
+
- Config pattern follows existing CMS_CONFIG style - object + getter helper
|
|
24
|
+
- experiment uses three ^0.172.0 (not ^0.182.0 like webgl/webgpu) per PRD
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2026-02-09 - US-002
|
|
28
|
+
- Created layers/webgl/ with app/page.tsx and 3 webgl component files from template/webgl/
|
|
29
|
+
- Created layers/experiment/ with header, navigation-menu, constats.ts, cn.ts from template/experiment/
|
|
30
|
+
- Created layers/webgpu/.gitkeep (empty layer, deps-only)
|
|
31
|
+
- Verified all files are byte-for-byte identical using diff
|
|
32
|
+
- Files changed: 9 new files in layers/
|
|
33
|
+
- **Learnings for future iterations:**
|
|
34
|
+
- Layer files are local to CLI repo (no network clone needed, unlike CMS integrations)
|
|
35
|
+
- webgl is the only layer with a different app/page.tsx (imports DynamicCanvas)
|
|
36
|
+
- experiment has its own header and navigation-menu components
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2026-02-09 - US-003
|
|
40
|
+
- Added injectLayer(targetDir, layer, spinner) function to src/mergers/index.js
|
|
41
|
+
- Uses __dirname (via import.meta.url + fileURLToPath) to resolve layers/ directory
|
|
42
|
+
- Reuses detectPathPrefix() and transformPath() for src/ directory support
|
|
43
|
+
- Handles replaceFiles (overwrite: true) and additivePaths (overwrite: false) separately
|
|
44
|
+
- Returns results with replaced, copied, skipped, failed arrays
|
|
45
|
+
- Extended formatMergeResults() with defensive optional chaining for replaced/merged/copied/etc
|
|
46
|
+
- Files changed: src/mergers/index.js
|
|
47
|
+
- **Learnings for future iterations:**
|
|
48
|
+
- ESM doesn't have __dirname, must use: `const __dirname = path.dirname(fileURLToPath(import.meta.url))`
|
|
49
|
+
- Layer injection is purely local file copy (no network), unlike CMS integration which uses tiged
|
|
50
|
+
- formatMergeResults now handles both CMS results (merged) and layer results (replaced) gracefully
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2026-02-09 - US-004
|
|
54
|
+
- Changed tiged source from basement/template/${type} to basementstudio/next-starter#main
|
|
55
|
+
- Removed BASEMENT_CLI_BRANCH env var usage (was only used for template, not used elsewhere)
|
|
56
|
+
- Added bun.lock deletion after clone (since deps will be modified by layers/CMS/animation)
|
|
57
|
+
- Updated spinner text to "Downloading next-starter from GitHub..."
|
|
58
|
+
- Updated troubleshooting message to reference basementstudio/next-starter
|
|
59
|
+
- Kept dotenv import as it may be used by merger modules
|
|
60
|
+
- Files changed: src/commands/create.js
|
|
61
|
+
- **Learnings for future iterations:**
|
|
62
|
+
- dotenv is loaded in create.js but env vars are only used in merger modules (check-integration-merger.js)
|
|
63
|
+
- bun.lock must be deleted before modifying package.json deps, otherwise bun install may conflict
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 2026-02-09 - US-005
|
|
67
|
+
- Added layer injection step (3.2) between template download and CMS integration (3.5)
|
|
68
|
+
- Layer injection only runs when type !== 'default'
|
|
69
|
+
- Uses dynamic import for injectLayer and formatMergeResults from mergers/index.js
|
|
70
|
+
- Shows ora spinner with layer name during injection
|
|
71
|
+
- On failure, shows warning and continues (not a hard error)
|
|
72
|
+
- Files changed: src/commands/create.js
|
|
73
|
+
- **Learnings for future iterations:**
|
|
74
|
+
- Layer injection MUST happen before CMS integration to keep layout.tsx intact for CMS mergers
|
|
75
|
+
- Uses dynamic import (await import) consistent with existing CMS integration pattern
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 2026-02-09 - US-006
|
|
79
|
+
- Imported getLayerConfig from src/mergers/config.js at top of create.js
|
|
80
|
+
- Layer deps injected from LAYER_CONFIG before CMS and animation deps
|
|
81
|
+
- Simplified package.json handling: removed package.template.json logic (next-starter uses package.json directly)
|
|
82
|
+
- CMS and animation dependency injection unchanged
|
|
83
|
+
- Project name and version still set correctly
|
|
84
|
+
- Files changed: src/commands/create.js
|
|
85
|
+
- **Learnings for future iterations:**
|
|
86
|
+
- Dependency injection order matters: layer deps first, then CMS, then animation (later ones override earlier)
|
|
87
|
+
- next-starter uses package.json directly, no need for package.template.json rename logic
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 2026-02-09 - US-007
|
|
91
|
+
- Deleted template/default/, template/webgl/, template/webgpu/, template/experiment/ directories
|
|
92
|
+
- Removed entire template/ directory (281 files, -27,778 lines)
|
|
93
|
+
- Verified no remaining references to template/ paths in source code (src/)
|
|
94
|
+
- Updated CLAUDE.md to reflect new architecture (layers/ instead of template/)
|
|
95
|
+
- Files changed: template/ deleted, CLAUDE.md updated
|
|
96
|
+
- **Learnings for future iterations:**
|
|
97
|
+
- CLAUDE.md should be kept in sync with architecture changes
|
|
98
|
+
- Only documentation files (CLAUDE.md, PRD) had remaining references to template/ paths, not source code
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 2026-02-09 - US-008
|
|
102
|
+
- Installed CLI locally via npm link
|
|
103
|
+
- E2E test results (all passed):
|
|
104
|
+
- test-default: Created successfully, name=test-default, version=0.1.0, has next dep, no R3F deps
|
|
105
|
+
- test-webgl: Created with layer applied (replaced page.tsx, added 3 webgl components), R3F ^9.5.0, three ^0.182.0, @types/three ^0.182.0
|
|
106
|
+
- test-webgpu: Created with deps-only layer, R3F 10.0.0-alpha.2, drei ^11.0.0-alpha.4, lucide-react ^0.474.0
|
|
107
|
+
- test-experiment: Created with layer (header, navigation-menu, constats.ts, cn.ts), three ^0.172.0
|
|
108
|
+
- test-sanity: Combined webgl + sanity + gsap - all deps present (R3F, next-sanity, sanity, gsap, @gsap/react, sanity:extract script)
|
|
109
|
+
- All 4 projects pass bun install without errors
|
|
110
|
+
- CLI command is basement (not basement-starter as PRD mentions)
|
|
111
|
+
- **Learnings for future iterations:**
|
|
112
|
+
- Skills installation may fail without network (bunx skills add), but it's non-blocking
|
|
113
|
+
- The sanity integration triggers an interactive Sanity setup prompt after project creation
|
|
114
|
+
- bun install correctly resolves all dependency versions including alpha packages
|
|
115
|
+
---
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import isEqual from "lodash-es/isEqual";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
// misc
|
|
7
|
+
|
|
8
|
+
function on<T extends Window | Document | HTMLElement | EventTarget>(
|
|
9
|
+
obj: T | null,
|
|
10
|
+
|
|
11
|
+
...args:
|
|
12
|
+
| Parameters<T["addEventListener"]>
|
|
13
|
+
| [string, EventListenerOrEventListenerObject | null, ...unknown[]]
|
|
14
|
+
): void {
|
|
15
|
+
if (obj?.addEventListener) {
|
|
16
|
+
obj.addEventListener(
|
|
17
|
+
...(args as Parameters<HTMLElement["addEventListener"]>),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function off<T extends Window | Document | HTMLElement | EventTarget>(
|
|
23
|
+
obj: T | null,
|
|
24
|
+
...args:
|
|
25
|
+
| Parameters<T["removeEventListener"]>
|
|
26
|
+
| [string, EventListenerOrEventListenerObject | null, ...unknown[]]
|
|
27
|
+
): void {
|
|
28
|
+
if (obj?.removeEventListener) {
|
|
29
|
+
obj.removeEventListener(
|
|
30
|
+
...(args as Parameters<HTMLElement["removeEventListener"]>),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const isNavigator = typeof navigator !== "undefined";
|
|
36
|
+
|
|
37
|
+
// hook
|
|
38
|
+
|
|
39
|
+
export interface BatteryState {
|
|
40
|
+
charging: boolean;
|
|
41
|
+
chargingTime: number;
|
|
42
|
+
dischargingTime: number;
|
|
43
|
+
level: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface BatteryManager extends Readonly<BatteryState>, EventTarget {
|
|
47
|
+
onchargingchange: () => void;
|
|
48
|
+
onchargingtimechange: () => void;
|
|
49
|
+
ondischargingtimechange: () => void;
|
|
50
|
+
onlevelchange: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface NavigatorWithPossibleBattery extends Navigator {
|
|
54
|
+
getBattery?: () => Promise<BatteryManager>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type UseBatteryState =
|
|
58
|
+
| { isSupported: false } // Battery API is not supported
|
|
59
|
+
| { isSupported: true; fetched: false } // battery API supported but not fetched yet
|
|
60
|
+
| (BatteryState & { isSupported: true; fetched: true }); // battery API supported and fetched
|
|
61
|
+
|
|
62
|
+
const nav: NavigatorWithPossibleBattery | undefined = isNavigator
|
|
63
|
+
? navigator
|
|
64
|
+
: undefined;
|
|
65
|
+
const isBatteryApiSupported = nav && typeof nav.getBattery === "function";
|
|
66
|
+
|
|
67
|
+
function useBatteryMock(): UseBatteryState {
|
|
68
|
+
return { isSupported: false };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function useBatteryReal(): UseBatteryState {
|
|
72
|
+
const [state, setState] = useState<UseBatteryState>({
|
|
73
|
+
isSupported: true,
|
|
74
|
+
fetched: false,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
let isMounted = true;
|
|
79
|
+
let battery: BatteryManager | null = null;
|
|
80
|
+
|
|
81
|
+
const handleChange = (): void => {
|
|
82
|
+
if (!isMounted || !battery) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const newState: UseBatteryState = {
|
|
86
|
+
isSupported: true,
|
|
87
|
+
fetched: true,
|
|
88
|
+
level: battery.level,
|
|
89
|
+
charging: battery.charging,
|
|
90
|
+
dischargingTime: battery.dischargingTime,
|
|
91
|
+
chargingTime: battery.chargingTime,
|
|
92
|
+
};
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
94
|
+
!isEqual(state, newState) && setState(newState);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
nav?.getBattery?.().then((bat: BatteryManager) => {
|
|
98
|
+
if (!isMounted) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
battery = bat;
|
|
102
|
+
on(battery, "chargingchange", handleChange);
|
|
103
|
+
on(battery, "chargingtimechange", handleChange);
|
|
104
|
+
on(battery, "dischargingtimechange", handleChange);
|
|
105
|
+
on(battery, "levelchange", handleChange);
|
|
106
|
+
handleChange();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return () => {
|
|
110
|
+
isMounted = false;
|
|
111
|
+
if (battery) {
|
|
112
|
+
off(battery, "chargingchange", handleChange);
|
|
113
|
+
off(battery, "chargingtimechange", handleChange);
|
|
114
|
+
off(battery, "dischargingtimechange", handleChange);
|
|
115
|
+
off(battery, "levelchange", handleChange);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
return state;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const useBattery = isBatteryApiSupported
|
|
125
|
+
? useBatteryReal
|
|
126
|
+
: useBatteryMock;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { TierResult } from "detect-gpu";
|
|
4
|
+
import { memo, useEffect } from "react";
|
|
5
|
+
import { create } from "zustand";
|
|
6
|
+
import { useShallow } from "zustand/react/shallow";
|
|
7
|
+
|
|
8
|
+
import { Quality, useAppStore } from "@/stores/use-app-store";
|
|
9
|
+
|
|
10
|
+
import { useBattery } from "./use-battery";
|
|
11
|
+
|
|
12
|
+
export interface DeviceType {
|
|
13
|
+
browserName: string;
|
|
14
|
+
browserVersion: string;
|
|
15
|
+
deviceType: string;
|
|
16
|
+
engineName: string;
|
|
17
|
+
engineVersion: string;
|
|
18
|
+
fullBrowserVersion: string;
|
|
19
|
+
getUA: string;
|
|
20
|
+
isAndroid: boolean;
|
|
21
|
+
isBrowser: boolean;
|
|
22
|
+
isChrome: boolean;
|
|
23
|
+
isChromium: boolean;
|
|
24
|
+
isConsole: boolean;
|
|
25
|
+
isDesktop: boolean;
|
|
26
|
+
isEdge: boolean;
|
|
27
|
+
isEdgeChromium: boolean;
|
|
28
|
+
isElectron: boolean;
|
|
29
|
+
isEmbedded: boolean;
|
|
30
|
+
isFirefox: boolean;
|
|
31
|
+
isIE: boolean;
|
|
32
|
+
isIOS: boolean;
|
|
33
|
+
isIOS13: boolean;
|
|
34
|
+
isIPad13: boolean;
|
|
35
|
+
isIPhone13: boolean;
|
|
36
|
+
isIPod13: boolean;
|
|
37
|
+
isLegacyEdge: boolean;
|
|
38
|
+
isMIUI: boolean;
|
|
39
|
+
isMacOs: boolean;
|
|
40
|
+
isMobile: boolean;
|
|
41
|
+
isMobileOnly: boolean;
|
|
42
|
+
isMobileSafari: boolean;
|
|
43
|
+
isOpera: boolean;
|
|
44
|
+
isSafari: boolean;
|
|
45
|
+
isSamsungBrowser: boolean;
|
|
46
|
+
isSmartTV: boolean;
|
|
47
|
+
isTablet: boolean;
|
|
48
|
+
isWearable: boolean;
|
|
49
|
+
isWinPhone: boolean;
|
|
50
|
+
isWindows: boolean;
|
|
51
|
+
isYandex: boolean;
|
|
52
|
+
mobileModel: string;
|
|
53
|
+
mobileVendor: string;
|
|
54
|
+
osName: string;
|
|
55
|
+
osVersion: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface Battery {
|
|
59
|
+
charging: boolean;
|
|
60
|
+
level: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface UseDeviceStore {
|
|
64
|
+
device: DeviceType | null;
|
|
65
|
+
gpu: TierResult | null;
|
|
66
|
+
battery: Battery | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const useDevice = create<UseDeviceStore>(() => ({
|
|
70
|
+
device: null,
|
|
71
|
+
gpu: null,
|
|
72
|
+
battery: null,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
function useSyncDeviceScore(): void {
|
|
76
|
+
const deviceData = useDevice(useShallow((state) => state.device));
|
|
77
|
+
const gpuData = useDevice(useShallow((state) => state.gpu));
|
|
78
|
+
const batteryData = useDevice(useShallow((state) => state.battery));
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
let score = Quality.HIGH;
|
|
82
|
+
|
|
83
|
+
if (deviceData) {
|
|
84
|
+
if (deviceData.isMobile) {
|
|
85
|
+
score = Math.min(score, Quality.LOW);
|
|
86
|
+
} else if (deviceData.isTablet) {
|
|
87
|
+
score = Math.min(score, Quality.MEDIUM);
|
|
88
|
+
} else if (deviceData.isDesktop) {
|
|
89
|
+
score = Math.min(score, Quality.HIGH);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!batteryData) {
|
|
94
|
+
// not battery
|
|
95
|
+
score = Math.min(score, Quality.HIGH);
|
|
96
|
+
} else {
|
|
97
|
+
// has battery
|
|
98
|
+
if (batteryData.charging) {
|
|
99
|
+
if (batteryData.level > 0.3) {
|
|
100
|
+
// charging and high
|
|
101
|
+
score = Math.min(score, Quality.HIGH);
|
|
102
|
+
} else {
|
|
103
|
+
// charging but low
|
|
104
|
+
score = Math.min(score, Quality.LOW);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
if (batteryData.level > 0.5) {
|
|
108
|
+
// not charging but high
|
|
109
|
+
score = Math.min(score, Quality.HIGH);
|
|
110
|
+
} else {
|
|
111
|
+
// not charging and low
|
|
112
|
+
score = Math.min(score, Quality.LOW);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (gpuData) {
|
|
118
|
+
if (gpuData.tier === 3) {
|
|
119
|
+
score = Math.min(score, Quality.HIGH);
|
|
120
|
+
} else if (gpuData.tier === 2) {
|
|
121
|
+
score = Math.min(score, Quality.MEDIUM);
|
|
122
|
+
} else if (gpuData.tier === 1) {
|
|
123
|
+
score = Math.min(score, Quality.LOW);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
useAppStore.setState({
|
|
128
|
+
qualityConfig: score,
|
|
129
|
+
});
|
|
130
|
+
}, [deviceData, gpuData, batteryData]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// this hook will save the device information in the store
|
|
134
|
+
export const DeviceDetector = memo(function MemoDetector() {
|
|
135
|
+
const battery = useBattery();
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (battery.isSupported && battery.fetched) {
|
|
139
|
+
useDevice.setState({
|
|
140
|
+
battery: {
|
|
141
|
+
charging: battery.charging,
|
|
142
|
+
level: battery.level,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}, [battery]);
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (typeof window === "undefined") return;
|
|
150
|
+
const abortController = new AbortController();
|
|
151
|
+
const signal = abortController.signal;
|
|
152
|
+
|
|
153
|
+
void import("react-device-detect").then((module) => {
|
|
154
|
+
if (signal.aborted) return;
|
|
155
|
+
|
|
156
|
+
const newState = module.getSelectorsByUserAgent(
|
|
157
|
+
navigator.userAgent,
|
|
158
|
+
) as DeviceType;
|
|
159
|
+
|
|
160
|
+
useDevice.setState({
|
|
161
|
+
device: newState,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
void import("detect-gpu").then(({ getGPUTier }) => {
|
|
166
|
+
if (signal.aborted) return;
|
|
167
|
+
|
|
168
|
+
getGPUTier().then((gpu) => {
|
|
169
|
+
if (signal.aborted) return;
|
|
170
|
+
useDevice.setState({
|
|
171
|
+
gpu,
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
abortController.abort();
|
|
178
|
+
};
|
|
179
|
+
}, []);
|
|
180
|
+
|
|
181
|
+
useSyncDeviceScore();
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export const useIntersectionObserver = <T extends Element>(
|
|
4
|
+
options: IntersectionObserverInit & { triggerOnce?: boolean },
|
|
5
|
+
) => {
|
|
6
|
+
const ref = React.useRef<T>(null);
|
|
7
|
+
const [inView, setInView] = React.useState(false);
|
|
8
|
+
|
|
9
|
+
React.useEffect(() => {
|
|
10
|
+
const elementToObserve = ref.current;
|
|
11
|
+
if (!elementToObserve) return;
|
|
12
|
+
const handleObserve: IntersectionObserverCallback = ([element]) => {
|
|
13
|
+
if (element) {
|
|
14
|
+
setInView((p) => {
|
|
15
|
+
// trigger once?
|
|
16
|
+
if (options?.triggerOnce && p === true) return p;
|
|
17
|
+
else return element.isIntersecting;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const observer = new IntersectionObserver(handleObserve, options);
|
|
23
|
+
|
|
24
|
+
observer.observe(elementToObserve);
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
observer.disconnect();
|
|
28
|
+
};
|
|
29
|
+
}, [options]);
|
|
30
|
+
|
|
31
|
+
return [ref, inView] as const;
|
|
32
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { isApiSupported } from "@/utils";
|
|
4
|
+
|
|
5
|
+
export const useMedia = (mediaQuery: string, initialValue?: boolean) => {
|
|
6
|
+
const [isVerified, setIsVerified] = React.useState<boolean | undefined>(
|
|
7
|
+
initialValue,
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
if (!isApiSupported("matchMedia")) {
|
|
12
|
+
console.warn("matchMedia is not supported by your current browser");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const mediaQueryList = window.matchMedia(mediaQuery);
|
|
16
|
+
const changeHandler = () => setIsVerified(!!mediaQueryList.matches);
|
|
17
|
+
|
|
18
|
+
changeHandler();
|
|
19
|
+
if (typeof mediaQueryList.addEventListener === "function") {
|
|
20
|
+
mediaQueryList.addEventListener("change", changeHandler);
|
|
21
|
+
return () => {
|
|
22
|
+
mediaQueryList.removeEventListener("change", changeHandler);
|
|
23
|
+
};
|
|
24
|
+
} else if (typeof mediaQueryList.addListener === "function") {
|
|
25
|
+
mediaQueryList.addListener(changeHandler);
|
|
26
|
+
return () => {
|
|
27
|
+
mediaQueryList.removeListener(changeHandler);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}, [mediaQuery]);
|
|
31
|
+
|
|
32
|
+
return isVerified;
|
|
33
|
+
};
|