bsmnt 0.0.2 → 0.1.1
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/.github/workflows/release.yml +2 -0
- package/CHANGELOG.md +31 -0
- package/CLAUDE.md +42 -24
- package/README.md +33 -12
- package/biome.json +1 -0
- package/bun.lock +2 -1
- package/docs/architecture.drawio +250 -0
- package/docs/architecture.mermaid +85 -0
- package/package.json +42 -42
- package/{bin → packages/cli/bin}/index.js +28 -31
- package/packages/cli/package.json +16 -0
- package/{src → packages/cli/src}/commands/add-integration.js +22 -37
- package/{src → packages/cli/src}/commands/create.js +125 -131
- package/packages/create-basement-app/integrations/sanity/config.js +46 -0
- package/{src → packages/create-basement-app/integrations/sanity}/mergers/check-integration-merger.js +1 -1
- package/{src → packages/create-basement-app/integrations/sanity}/mergers/layout-merger.js +1 -1
- package/{src → packages/create-basement-app/integrations/sanity}/mergers/sitemap-merger.js +1 -1
- package/packages/create-basement-app/package.json +10 -0
- package/packages/create-basement-app/src/configs/animations.js +28 -0
- package/packages/create-basement-app/src/index.js +15 -0
- package/packages/create-basement-app/src/mergers/check-integration-merger.js +105 -0
- package/{src → packages/create-basement-app/src}/mergers/config.js +5 -33
- package/{src → packages/create-basement-app/src}/mergers/index.js +89 -98
- package/packages/create-basement-app/src/mergers/layout-merger.js +223 -0
- package/packages/create-basement-app/src/mergers/sitemap-merger.js +121 -0
- package/packages/create-basement-app/template-hooks/config.js +38 -0
- package/packages/create-basement-app/templates/default/.biome/plugins/README.md +21 -0
- package/packages/create-basement-app/templates/default/.biome/plugins/no-anchor-element.grit +12 -0
- package/packages/create-basement-app/templates/default/.biome/plugins/no-relative-parent-imports.grit +10 -0
- package/packages/create-basement-app/templates/default/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
- package/packages/create-basement-app/templates/default/.cursor/rules/README.md +184 -0
- package/packages/create-basement-app/templates/default/.cursor/rules/architecture.mdc +437 -0
- package/packages/create-basement-app/templates/default/.cursor/rules/components.mdc +436 -0
- package/packages/create-basement-app/templates/default/.cursor/rules/integrations.mdc +447 -0
- package/packages/create-basement-app/templates/default/.cursor/rules/main.mdc +278 -0
- package/packages/create-basement-app/templates/default/.cursor/rules/styling.mdc +433 -0
- package/packages/create-basement-app/templates/default/.editorconfig +40 -0
- package/packages/create-basement-app/templates/default/.env.example +81 -0
- package/packages/create-basement-app/templates/default/.gitattributes +19 -0
- package/packages/create-basement-app/templates/default/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/packages/create-basement-app/templates/default/.github/workflows/lighthouse-to-slack.yml +136 -0
- package/packages/create-basement-app/templates/default/.vscode/extensions.json +20 -0
- package/packages/create-basement-app/templates/default/.vscode/settings.json +105 -0
- package/packages/create-basement-app/templates/default/README.md +221 -0
- package/packages/create-basement-app/templates/default/_gitignore +67 -0
- package/packages/create-basement-app/templates/default/app/favicon.ico +0 -0
- package/packages/create-basement-app/templates/default/app/layout.tsx +104 -0
- package/packages/create-basement-app/templates/default/app/page.tsx +275 -0
- package/packages/create-basement-app/templates/default/app/robots.ts +15 -0
- package/packages/create-basement-app/templates/default/app/sitemap.ts +16 -0
- package/packages/create-basement-app/templates/default/biome.json +250 -0
- package/packages/create-basement-app/templates/default/components/basement.svg +1 -0
- package/packages/create-basement-app/templates/default/components/layout/footer/index.tsx +27 -0
- package/packages/create-basement-app/templates/default/components/layout/header/index.tsx +11 -0
- package/packages/create-basement-app/templates/default/components/layout/theme/index.tsx +66 -0
- package/packages/create-basement-app/templates/default/components/layout/wrapper/index.tsx +65 -0
- package/packages/create-basement-app/templates/default/components/ui/README.md +77 -0
- package/packages/create-basement-app/templates/default/components/ui/image/README.md +37 -0
- package/packages/create-basement-app/templates/default/components/ui/image/index.tsx +224 -0
- package/packages/create-basement-app/templates/default/components/ui/link/index.tsx +146 -0
- package/packages/create-basement-app/templates/default/lib/README.md +33 -0
- package/packages/create-basement-app/templates/default/lib/hooks/index.ts +12 -0
- package/packages/create-basement-app/templates/default/lib/hooks/use-device-detection.ts +81 -0
- package/packages/create-basement-app/templates/default/lib/hooks/use-media-breakpoint.ts +15 -0
- package/packages/create-basement-app/templates/default/lib/hooks/use-prefetch.ts +74 -0
- package/packages/create-basement-app/templates/default/lib/scripts/dev.ts +52 -0
- package/packages/create-basement-app/templates/default/lib/scripts/generate-component.ts +322 -0
- package/packages/create-basement-app/templates/default/lib/scripts/generate-page.ts +193 -0
- package/packages/create-basement-app/templates/default/lib/scripts/generate.ts +79 -0
- package/packages/create-basement-app/templates/default/lib/scripts/utils.ts +246 -0
- package/packages/create-basement-app/templates/default/lib/store/app.ts +11 -0
- package/packages/create-basement-app/templates/default/lib/store/index.ts +11 -0
- package/packages/create-basement-app/templates/default/lib/styles/README.md +64 -0
- package/packages/create-basement-app/templates/default/lib/styles/cn.ts +7 -0
- package/packages/create-basement-app/templates/default/lib/styles/colors.ts +63 -0
- package/packages/create-basement-app/templates/default/lib/styles/config.ts +34 -0
- package/packages/create-basement-app/templates/default/lib/styles/css/global.css +85 -0
- package/packages/create-basement-app/templates/default/lib/styles/css/index.css +6 -0
- package/packages/create-basement-app/templates/default/lib/styles/css/reset.css +166 -0
- package/packages/create-basement-app/templates/default/lib/styles/css/root.css +68 -0
- package/packages/create-basement-app/templates/default/lib/styles/css/tailwind.css +132 -0
- package/packages/create-basement-app/templates/default/lib/styles/easings.ts +21 -0
- package/packages/create-basement-app/templates/default/lib/styles/fonts.ts +28 -0
- package/packages/create-basement-app/templates/default/lib/styles/index.ts +12 -0
- package/packages/create-basement-app/templates/default/lib/styles/layout.mjs +27 -0
- package/packages/create-basement-app/templates/default/lib/styles/scripts/README.md +29 -0
- package/packages/create-basement-app/templates/default/lib/styles/scripts/generate-root.ts +57 -0
- package/packages/create-basement-app/templates/default/lib/styles/scripts/generate-tailwind.ts +162 -0
- package/packages/create-basement-app/templates/default/lib/styles/scripts/postcss-functions.mjs +168 -0
- package/packages/create-basement-app/templates/default/lib/styles/scripts/setup-styles.ts +24 -0
- package/packages/create-basement-app/templates/default/lib/styles/scripts/utils.ts +20 -0
- package/packages/create-basement-app/templates/default/lib/styles/typography.ts +36 -0
- package/packages/create-basement-app/templates/default/lib/utils/README.md +40 -0
- package/packages/create-basement-app/templates/default/lib/utils/css.d.ts +21 -0
- package/packages/create-basement-app/templates/default/lib/utils/easings.ts +240 -0
- package/packages/create-basement-app/templates/default/lib/utils/fetch.ts +84 -0
- package/packages/create-basement-app/templates/default/lib/utils/math.test.ts +221 -0
- package/packages/create-basement-app/templates/default/lib/utils/math.ts +236 -0
- package/packages/create-basement-app/templates/default/lib/utils/metadata.ts +126 -0
- package/packages/create-basement-app/templates/default/lib/utils/strings.test.ts +166 -0
- package/packages/create-basement-app/templates/default/lib/utils/strings.ts +246 -0
- package/packages/create-basement-app/templates/default/lib/utils/types.d.ts +15 -0
- package/packages/create-basement-app/templates/default/lib/utils/viewport.test.ts +256 -0
- package/packages/create-basement-app/templates/default/lib/utils/viewport.ts +193 -0
- package/packages/create-basement-app/templates/default/next.config.ts +142 -0
- package/packages/create-basement-app/templates/default/package.json +62 -0
- package/packages/create-basement-app/templates/default/postcss.config.mjs +42 -0
- package/packages/create-basement-app/templates/default/public/fonts/geist/Geist-Mono.woff2 +0 -0
- package/packages/create-basement-app/templates/default/tsconfig.json +43 -0
- package/packages/create-basement-app/templates/experiment/.biome/plugins/README.md +21 -0
- package/packages/create-basement-app/templates/experiment/.biome/plugins/no-anchor-element.grit +12 -0
- package/packages/create-basement-app/templates/experiment/.biome/plugins/no-relative-parent-imports.grit +10 -0
- package/packages/create-basement-app/templates/experiment/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
- package/packages/create-basement-app/templates/experiment/.cursor/rules/README.md +184 -0
- package/packages/create-basement-app/templates/experiment/.cursor/rules/architecture.mdc +437 -0
- package/packages/create-basement-app/templates/experiment/.cursor/rules/components.mdc +436 -0
- package/packages/create-basement-app/templates/experiment/.cursor/rules/integrations.mdc +447 -0
- package/packages/create-basement-app/templates/experiment/.cursor/rules/main.mdc +278 -0
- package/packages/create-basement-app/templates/experiment/.cursor/rules/styling.mdc +433 -0
- package/packages/create-basement-app/templates/experiment/.editorconfig +40 -0
- package/packages/create-basement-app/templates/experiment/.env.example +81 -0
- package/packages/create-basement-app/templates/experiment/.gitattributes +19 -0
- package/packages/create-basement-app/templates/experiment/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/packages/create-basement-app/templates/experiment/.github/workflows/lighthouse-to-slack.yml +136 -0
- package/packages/create-basement-app/templates/experiment/.vscode/extensions.json +20 -0
- package/packages/create-basement-app/templates/experiment/.vscode/settings.json +105 -0
- package/packages/create-basement-app/templates/experiment/README.md +221 -0
- package/packages/create-basement-app/templates/experiment/_gitignore +67 -0
- package/packages/create-basement-app/templates/experiment/app/favicon.ico +0 -0
- package/packages/create-basement-app/templates/experiment/app/layout.tsx +104 -0
- package/packages/create-basement-app/templates/experiment/app/page.tsx +275 -0
- package/packages/create-basement-app/templates/experiment/app/robots.ts +15 -0
- package/packages/create-basement-app/templates/experiment/app/sitemap.ts +16 -0
- package/packages/create-basement-app/templates/experiment/biome.json +250 -0
- package/packages/create-basement-app/templates/experiment/components/basement.svg +1 -0
- package/packages/create-basement-app/templates/experiment/components/layout/footer/index.tsx +27 -0
- package/packages/create-basement-app/templates/experiment/components/layout/header/index.tsx +58 -0
- package/packages/create-basement-app/templates/experiment/components/layout/navigation-menu.tsx +127 -0
- package/packages/create-basement-app/templates/experiment/components/layout/theme/index.tsx +66 -0
- package/packages/create-basement-app/templates/experiment/components/layout/wrapper/index.tsx +65 -0
- package/packages/create-basement-app/templates/experiment/components/ui/README.md +77 -0
- package/packages/create-basement-app/templates/experiment/components/ui/image/README.md +37 -0
- package/packages/create-basement-app/templates/experiment/components/ui/image/index.tsx +224 -0
- package/packages/create-basement-app/templates/experiment/components/ui/link/index.tsx +146 -0
- package/packages/create-basement-app/templates/experiment/lib/README.md +33 -0
- package/packages/create-basement-app/templates/experiment/lib/constants.ts +12 -0
- package/packages/create-basement-app/templates/experiment/lib/hooks/index.ts +12 -0
- package/packages/create-basement-app/templates/experiment/lib/hooks/use-device-detection.ts +81 -0
- package/packages/create-basement-app/templates/experiment/lib/hooks/use-media-breakpoint.ts +15 -0
- package/packages/create-basement-app/templates/experiment/lib/hooks/use-prefetch.ts +74 -0
- package/packages/create-basement-app/templates/experiment/lib/integrations/.gitkeep +0 -0
- package/packages/create-basement-app/templates/experiment/lib/scripts/dev.ts +52 -0
- package/packages/create-basement-app/templates/experiment/lib/scripts/generate-component.ts +322 -0
- package/packages/create-basement-app/templates/experiment/lib/scripts/generate-page.ts +193 -0
- package/packages/create-basement-app/templates/experiment/lib/scripts/generate.ts +79 -0
- package/packages/create-basement-app/templates/experiment/lib/scripts/utils.ts +246 -0
- package/packages/create-basement-app/templates/experiment/lib/store/app.ts +11 -0
- package/packages/create-basement-app/templates/experiment/lib/store/index.ts +11 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/README.md +64 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/cn.ts +7 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/colors.ts +63 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/config.ts +34 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/css/global.css +85 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/css/index.css +6 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/css/reset.css +166 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/css/root.css +68 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/css/tailwind.css +132 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/easings.ts +21 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/fonts.ts +28 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/index.ts +12 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/layout.mjs +27 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/scripts/README.md +29 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/scripts/generate-root.ts +57 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/scripts/generate-tailwind.ts +162 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/scripts/postcss-functions.mjs +168 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/scripts/setup-styles.ts +24 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/scripts/utils.ts +20 -0
- package/packages/create-basement-app/templates/experiment/lib/styles/typography.ts +36 -0
- package/packages/create-basement-app/templates/experiment/lib/utils/README.md +40 -0
- package/packages/create-basement-app/templates/experiment/lib/utils/css.d.ts +21 -0
- package/packages/create-basement-app/templates/experiment/lib/utils/easings.ts +240 -0
- package/packages/create-basement-app/templates/experiment/lib/utils/fetch.ts +84 -0
- package/packages/create-basement-app/templates/experiment/lib/utils/math.test.ts +221 -0
- package/packages/create-basement-app/templates/experiment/lib/utils/math.ts +236 -0
- package/packages/create-basement-app/templates/experiment/lib/utils/metadata.ts +126 -0
- package/packages/create-basement-app/templates/experiment/lib/utils/strings.test.ts +166 -0
- package/packages/create-basement-app/templates/experiment/lib/utils/strings.ts +246 -0
- package/packages/create-basement-app/templates/experiment/lib/utils/types.d.ts +15 -0
- package/packages/create-basement-app/templates/experiment/lib/utils/viewport.test.ts +256 -0
- package/packages/create-basement-app/templates/experiment/lib/utils/viewport.ts +193 -0
- package/packages/create-basement-app/templates/experiment/next.config.ts +142 -0
- package/packages/create-basement-app/templates/experiment/package.json +69 -0
- package/packages/create-basement-app/templates/experiment/postcss.config.mjs +42 -0
- package/packages/create-basement-app/templates/experiment/public/fonts/geist/Geist-Mono.woff2 +0 -0
- package/packages/create-basement-app/templates/experiment/tsconfig.json +43 -0
- package/packages/create-basement-app/templates/webgl/.biome/plugins/README.md +21 -0
- package/packages/create-basement-app/templates/webgl/.biome/plugins/no-anchor-element.grit +12 -0
- package/packages/create-basement-app/templates/webgl/.biome/plugins/no-relative-parent-imports.grit +10 -0
- package/packages/create-basement-app/templates/webgl/.biome/plugins/no-unnecessary-forwardref.grit +9 -0
- package/packages/create-basement-app/templates/webgl/.cursor/rules/README.md +184 -0
- package/packages/create-basement-app/templates/webgl/.cursor/rules/architecture.mdc +437 -0
- package/packages/create-basement-app/templates/webgl/.cursor/rules/components.mdc +436 -0
- package/packages/create-basement-app/templates/webgl/.cursor/rules/integrations.mdc +447 -0
- package/packages/create-basement-app/templates/webgl/.cursor/rules/main.mdc +278 -0
- package/packages/create-basement-app/templates/webgl/.cursor/rules/styling.mdc +433 -0
- package/packages/create-basement-app/templates/webgl/.editorconfig +40 -0
- package/packages/create-basement-app/templates/webgl/.env.example +81 -0
- package/packages/create-basement-app/templates/webgl/.gitattributes +19 -0
- package/packages/create-basement-app/templates/webgl/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/packages/create-basement-app/templates/webgl/.github/workflows/lighthouse-to-slack.yml +136 -0
- package/packages/create-basement-app/templates/webgl/.vscode/extensions.json +20 -0
- package/packages/create-basement-app/templates/webgl/.vscode/settings.json +105 -0
- package/packages/create-basement-app/templates/webgl/README.md +221 -0
- package/packages/create-basement-app/templates/webgl/_gitignore +67 -0
- package/packages/create-basement-app/templates/webgl/app/favicon.ico +0 -0
- package/packages/create-basement-app/templates/webgl/app/layout.tsx +104 -0
- package/packages/create-basement-app/templates/webgl/app/page.tsx +10 -0
- package/packages/create-basement-app/templates/webgl/app/robots.ts +15 -0
- package/packages/create-basement-app/templates/webgl/app/sitemap.ts +16 -0
- package/packages/create-basement-app/templates/webgl/biome.json +250 -0
- package/packages/create-basement-app/templates/webgl/components/basement.svg +1 -0
- package/packages/create-basement-app/templates/webgl/components/layout/footer/index.tsx +27 -0
- package/packages/create-basement-app/templates/webgl/components/layout/header/index.tsx +11 -0
- package/packages/create-basement-app/templates/webgl/components/layout/theme/index.tsx +66 -0
- package/packages/create-basement-app/templates/webgl/components/layout/wrapper/index.tsx +65 -0
- package/packages/create-basement-app/templates/webgl/components/ui/README.md +77 -0
- package/packages/create-basement-app/templates/webgl/components/ui/image/README.md +37 -0
- package/packages/create-basement-app/templates/webgl/components/ui/image/index.tsx +224 -0
- package/packages/create-basement-app/templates/webgl/components/ui/link/index.tsx +146 -0
- package/packages/create-basement-app/templates/webgl/components/webgl/canvas/dynamic.tsx +34 -0
- package/packages/create-basement-app/templates/webgl/components/webgl/canvas/index.tsx +38 -0
- package/packages/create-basement-app/templates/webgl/components/webgl/components/scene/index.tsx +29 -0
- package/packages/create-basement-app/templates/webgl/lib/README.md +33 -0
- package/packages/create-basement-app/templates/webgl/lib/hooks/index.ts +12 -0
- package/packages/create-basement-app/templates/webgl/lib/hooks/use-device-detection.ts +81 -0
- package/packages/create-basement-app/templates/webgl/lib/hooks/use-media-breakpoint.ts +15 -0
- package/packages/create-basement-app/templates/webgl/lib/hooks/use-prefetch.ts +74 -0
- package/packages/create-basement-app/templates/webgl/lib/integrations/.gitkeep +0 -0
- package/packages/create-basement-app/templates/webgl/lib/renderer.ts +7 -0
- package/packages/create-basement-app/templates/webgl/lib/scripts/dev.ts +52 -0
- package/packages/create-basement-app/templates/webgl/lib/scripts/generate-component.ts +322 -0
- package/packages/create-basement-app/templates/webgl/lib/scripts/generate-page.ts +193 -0
- package/packages/create-basement-app/templates/webgl/lib/scripts/generate.ts +79 -0
- package/packages/create-basement-app/templates/webgl/lib/scripts/utils.ts +246 -0
- package/packages/create-basement-app/templates/webgl/lib/store/app.ts +11 -0
- package/packages/create-basement-app/templates/webgl/lib/store/index.ts +11 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/README.md +64 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/cn.ts +7 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/colors.ts +63 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/config.ts +34 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/css/global.css +85 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/css/index.css +6 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/css/reset.css +166 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/css/root.css +68 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/css/tailwind.css +132 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/easings.ts +21 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/fonts.ts +28 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/index.ts +12 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/layout.mjs +27 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/scripts/README.md +29 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/scripts/generate-root.ts +57 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/scripts/generate-tailwind.ts +162 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/scripts/postcss-functions.mjs +168 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/scripts/setup-styles.ts +24 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/scripts/utils.ts +20 -0
- package/packages/create-basement-app/templates/webgl/lib/styles/typography.ts +36 -0
- package/packages/create-basement-app/templates/webgl/lib/utils/README.md +40 -0
- package/packages/create-basement-app/templates/webgl/lib/utils/css.d.ts +21 -0
- package/packages/create-basement-app/templates/webgl/lib/utils/easings.ts +240 -0
- package/packages/create-basement-app/templates/webgl/lib/utils/fetch.ts +84 -0
- package/packages/create-basement-app/templates/webgl/lib/utils/math.test.ts +221 -0
- package/packages/create-basement-app/templates/webgl/lib/utils/math.ts +236 -0
- package/packages/create-basement-app/templates/webgl/lib/utils/metadata.ts +126 -0
- package/packages/create-basement-app/templates/webgl/lib/utils/strings.test.ts +166 -0
- package/packages/create-basement-app/templates/webgl/lib/utils/strings.ts +246 -0
- package/packages/create-basement-app/templates/webgl/lib/utils/types.d.ts +15 -0
- package/packages/create-basement-app/templates/webgl/lib/utils/viewport.test.ts +256 -0
- package/packages/create-basement-app/templates/webgl/lib/utils/viewport.ts +193 -0
- package/packages/create-basement-app/templates/webgl/next.config.ts +142 -0
- package/packages/create-basement-app/templates/webgl/package.json +68 -0
- package/packages/create-basement-app/templates/webgl/postcss.config.mjs +42 -0
- package/packages/create-basement-app/templates/webgl/public/fonts/geist/Geist-Mono.woff2 +0 -0
- package/packages/create-basement-app/templates/webgl/tsconfig.json +43 -0
- package/tasks/.last-branch +1 -0
- package/tasks/CLAUDE.md +104 -0
- package/tasks/archive/2026-02-09-next-starter-dynamic-layers/prd.json +153 -0
- package/tasks/archive/2026-02-09-next-starter-dynamic-layers/progress.txt +115 -0
- package/tasks/prd-project-restructure.md +375 -0
- package/tasks/prd.json +227 -91
- package/tasks/progress.txt +281 -87
- package/tasks/ralph.sh +113 -0
- package/integrations/basehub/README.md +0 -3
- package/layers/experiment/components/layout/header/index.tsx +0 -58
- package/layers/experiment/components/layout/navigation-menu.tsx +0 -127
- package/layers/experiment/lib/constants.ts +0 -12
- package/layers/webgl/app/page.tsx +0 -10
- package/layers/webgl/components/webgl/canvas/dynamic.tsx +0 -34
- package/layers/webgl/components/webgl/canvas/index.tsx +0 -43
- package/layers/webgl/components/webgl/components/scene/index.tsx +0 -21
- package/src/mergers/next-config-merger.js +0 -63
- /package/{src → packages/cli/src}/commands/setup-sanity.js +0 -0
- /package/{src → packages/cli/src}/commands/worktree.js +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/app/api/draft-mode/disable/route.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/app/api/draft-mode/enable/route.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/app/api/revalidate/route.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/app/layout.tsx +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/app/sitemap.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/app/studio/[[...tool]]/page.tsx +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/app/studio/layout.tsx +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/components/ui/sanity-image/index.tsx +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/README.md +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/check-integration.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/README.md +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/client.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/components/disable-draft-mode.tsx +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/components/rich-text.tsx +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/env.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/live/index.tsx +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/queries.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/sanity.cli.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/sanity.config.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/sanity.types.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/schema.json +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/schemas/article.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/schemas/example.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/schemas/index.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/schemas/link.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/schemas/metadata.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/schemas/navigation.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/schemas/page.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/schemas/richText.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/structure.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/utils/image.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/integrations/sanity/utils/link.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/scripts/copy-sanity-mcp.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/scripts/generate-page.ts +0 -0
- /package/{integrations/sanity → packages/create-basement-app/integrations/sanity/files}/lib/utils/metadata.ts +0 -0
- /package/{plugins → packages/create-basement-app/plugins}/README.md +0 -0
- /package/{plugins → packages/create-basement-app/plugins}/no-anchor-element.grit +0 -0
- /package/{plugins → packages/create-basement-app/plugins}/no-relative-parent-imports.grit +0 -0
- /package/{plugins → packages/create-basement-app/plugins}/no-unnecessary-forwardref.grit +0 -0
- /package/{template-hooks → packages/create-basement-app/template-hooks}/use-battery.ts +0 -0
- /package/{template-hooks → packages/create-basement-app/template-hooks}/use-device-perf.ts +0 -0
- /package/{template-hooks → packages/create-basement-app/template-hooks}/use-intersection-observer.ts +0 -0
- /package/{template-hooks → packages/create-basement-app/template-hooks}/use-media.ts +0 -0
- /package/{layers/webgpu → packages/create-basement-app/templates/default/lib/integrations}/.gitkeep +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for string utilities
|
|
3
|
+
*
|
|
4
|
+
* Run with: bun test lib/utils/strings.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "bun:test"
|
|
8
|
+
import {
|
|
9
|
+
capitalizeFirstLetter,
|
|
10
|
+
convertToCamelCase,
|
|
11
|
+
isEmptyArray,
|
|
12
|
+
isEmptyObject,
|
|
13
|
+
numberWithCommas,
|
|
14
|
+
slugify,
|
|
15
|
+
twoDigits,
|
|
16
|
+
} from "./strings"
|
|
17
|
+
|
|
18
|
+
describe("slugify", () => {
|
|
19
|
+
it("should convert text to URL-friendly format", () => {
|
|
20
|
+
expect(slugify("Hello World")).toBe("hello-world")
|
|
21
|
+
expect(slugify("Hello World!")).toBe("hello-world")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("should handle special characters", () => {
|
|
25
|
+
expect(slugify("Café & Restaurant")).toBe("cafe-restaurant")
|
|
26
|
+
expect(slugify("Rock & Roll")).toBe("rock-roll")
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("should handle multiple spaces and dashes", () => {
|
|
30
|
+
expect(slugify("Hello World")).toBe("hello-world")
|
|
31
|
+
expect(slugify("Hello--World")).toBe("hello-world")
|
|
32
|
+
expect(slugify("Hello - World")).toBe("hello-world")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("should trim whitespace", () => {
|
|
36
|
+
expect(slugify(" Hello World ")).toBe("hello-world")
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("should handle numbers", () => {
|
|
40
|
+
expect(slugify("Product 123")).toBe("product-123")
|
|
41
|
+
expect(slugify("2024 Review")).toBe("2024-review")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("should handle objects with toString", () => {
|
|
45
|
+
const obj = { toString: () => "Custom Object" }
|
|
46
|
+
expect(slugify(obj)).toBe("custom-object")
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe("convertToCamelCase", () => {
|
|
51
|
+
it("should convert first character to lowercase", () => {
|
|
52
|
+
expect(convertToCamelCase("HelloWorld")).toBe("helloWorld")
|
|
53
|
+
expect(convertToCamelCase("MyComponent")).toBe("myComponent")
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("should handle already camelCase strings", () => {
|
|
57
|
+
expect(convertToCamelCase("helloWorld")).toBe("helloWorld")
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("should handle single characters", () => {
|
|
61
|
+
expect(convertToCamelCase("A")).toBe("a")
|
|
62
|
+
expect(convertToCamelCase("a")).toBe("a")
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("should handle empty strings", () => {
|
|
66
|
+
expect(convertToCamelCase("")).toBe("")
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe("capitalizeFirstLetter", () => {
|
|
71
|
+
it("should capitalize the first letter", () => {
|
|
72
|
+
expect(capitalizeFirstLetter("hello")).toBe("Hello")
|
|
73
|
+
expect(capitalizeFirstLetter("world")).toBe("World")
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("should handle already capitalized strings", () => {
|
|
77
|
+
expect(capitalizeFirstLetter("Hello")).toBe("Hello")
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("should handle single characters", () => {
|
|
81
|
+
expect(capitalizeFirstLetter("a")).toBe("A")
|
|
82
|
+
expect(capitalizeFirstLetter("A")).toBe("A")
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("should handle empty strings", () => {
|
|
86
|
+
expect(capitalizeFirstLetter("")).toBe("")
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe("twoDigits", () => {
|
|
91
|
+
it("should pad single digit numbers with zero", () => {
|
|
92
|
+
expect(twoDigits(0)).toBe("00")
|
|
93
|
+
expect(twoDigits(5)).toBe("05")
|
|
94
|
+
expect(twoDigits(9)).toBe("09")
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("should not pad two-digit numbers", () => {
|
|
98
|
+
expect(twoDigits(10)).toBe("10")
|
|
99
|
+
expect(twoDigits(23)).toBe("23")
|
|
100
|
+
expect(twoDigits(99)).toBe("99")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("should handle numbers greater than 99", () => {
|
|
104
|
+
expect(twoDigits(100)).toBe("100")
|
|
105
|
+
expect(twoDigits(999)).toBe("999")
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe("numberWithCommas", () => {
|
|
110
|
+
it("should add commas as thousands separators", () => {
|
|
111
|
+
expect(numberWithCommas(1000)).toBe("1,000")
|
|
112
|
+
expect(numberWithCommas(1234)).toBe("1,234")
|
|
113
|
+
expect(numberWithCommas(1234567)).toBe("1,234,567")
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it("should not add commas to numbers less than 1000", () => {
|
|
117
|
+
expect(numberWithCommas(999)).toBe("999")
|
|
118
|
+
expect(numberWithCommas(1)).toBe("1")
|
|
119
|
+
expect(numberWithCommas(0)).toBe("0")
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("should handle objects with toString", () => {
|
|
123
|
+
const obj = { toString: () => "1234567" }
|
|
124
|
+
expect(numberWithCommas(obj)).toBe("1,234,567")
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe("isEmptyObject", () => {
|
|
129
|
+
it("should return true for empty objects", () => {
|
|
130
|
+
expect(isEmptyObject({})).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it("should return false for non-empty objects", () => {
|
|
134
|
+
expect(isEmptyObject({ a: 1 })).toBe(false)
|
|
135
|
+
expect(isEmptyObject({ key: "value" })).toBe(false)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it("should return true for null/undefined", () => {
|
|
139
|
+
expect(isEmptyObject(null as unknown as Record<string, unknown>)).toBe(true)
|
|
140
|
+
expect(isEmptyObject(undefined as unknown as Record<string, unknown>)).toBe(
|
|
141
|
+
true
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe("isEmptyArray", () => {
|
|
147
|
+
it("should return true for empty arrays", () => {
|
|
148
|
+
expect(isEmptyArray([])).toBe(true)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it("should return false for non-empty arrays", () => {
|
|
152
|
+
expect(isEmptyArray([1, 2, 3])).toBe(false)
|
|
153
|
+
expect(isEmptyArray(["a"])).toBe(false)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it("should return false for non-arrays (strings)", () => {
|
|
157
|
+
// Note: strings are not arrays, so Array.isArray returns false
|
|
158
|
+
// The function returns false for non-arrays that have a truthy value
|
|
159
|
+
expect(isEmptyArray("test")).toBe(false)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it("should return true for null/undefined", () => {
|
|
163
|
+
expect(isEmptyArray(null as unknown as unknown[])).toBe(true)
|
|
164
|
+
expect(isEmptyArray(undefined as unknown as unknown[])).toBe(true)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String & Object Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for string manipulation, object operations, and refs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Ref } from "react"
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// STRING UTILITIES
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Converts text to URL-friendly slug format.
|
|
15
|
+
*
|
|
16
|
+
* @param text - Text to convert (must have toString method)
|
|
17
|
+
* @returns URL-safe slug string
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* slugify('Hello World!') // 'hello-world'
|
|
22
|
+
* slugify('Café & Restaurant') // 'cafe-restaurant'
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function slugify(text: { toString: () => string }) {
|
|
26
|
+
return text
|
|
27
|
+
.toString()
|
|
28
|
+
.normalize("NFKD")
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.trim()
|
|
31
|
+
.replace(/\s+/g, "-")
|
|
32
|
+
.replace(/[^\w-]+/g, "")
|
|
33
|
+
.replace(/--+/g, "-")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Converts first character to lowercase (camelCase format).
|
|
38
|
+
*
|
|
39
|
+
* @param inputString - String to convert
|
|
40
|
+
* @returns String with lowercase first character
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* convertToCamelCase('HelloWorld') // 'helloWorld'
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function convertToCamelCase(inputString: string) {
|
|
48
|
+
return inputString.charAt(0).toLowerCase() + inputString.slice(1)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Capitalizes the first letter of a string.
|
|
53
|
+
*
|
|
54
|
+
* @param inputString - String to capitalize
|
|
55
|
+
* @returns String with capitalized first letter
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* capitalizeFirstLetter('hello') // 'Hello'
|
|
60
|
+
* capitalizeFirstLetter('world') // 'World'
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function capitalizeFirstLetter(inputString: string) {
|
|
64
|
+
return inputString.charAt(0).toUpperCase() + inputString.slice(1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Formats a number as a two-digit string (padding with zero).
|
|
69
|
+
*
|
|
70
|
+
* @param number - Number to format
|
|
71
|
+
* @returns Two-digit string representation
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* twoDigits(5) // '05'
|
|
76
|
+
* twoDigits(23) // '23'
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function twoDigits(number: number) {
|
|
80
|
+
return number > 9 ? `${number}` : `0${number}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Adds commas as thousands separators to numbers.
|
|
85
|
+
*
|
|
86
|
+
* @param x - Number to format (must have toString method)
|
|
87
|
+
* @returns String with comma separators
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* numberWithCommas(1234) // '1,234'
|
|
92
|
+
* numberWithCommas(1234567) // '1,234,567'
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function numberWithCommas(x: { toString: () => string }) {
|
|
96
|
+
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// ARRAY & OBJECT UTILITIES
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
export function checkIsArray<T>(value: T): T extends unknown[] ? T[0] : T {
|
|
104
|
+
return (Array.isArray(value) ? value[0] : value) as T extends unknown[]
|
|
105
|
+
? T[0]
|
|
106
|
+
: T
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Checks if an object is empty (has no enumerable properties).
|
|
111
|
+
*
|
|
112
|
+
* @param obj - Object to check
|
|
113
|
+
* @returns True if object is empty or null/undefined
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* isEmptyObject({}) // true
|
|
118
|
+
* isEmptyObject({ a: 1 }) // false
|
|
119
|
+
* isEmptyObject(null) // true
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export function isEmptyObject(obj: Record<string, unknown>) {
|
|
123
|
+
if (!obj) return true
|
|
124
|
+
return Object.keys(obj).length === 0
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Checks if an array is empty or if the input is not an array.
|
|
129
|
+
*
|
|
130
|
+
* @param arr - Array or value to check
|
|
131
|
+
* @returns True if empty array or not an array
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* isEmptyArray([]) // true
|
|
136
|
+
* isEmptyArray([1, 2]) // false
|
|
137
|
+
* isEmptyArray('test') // true (not an array)
|
|
138
|
+
* isEmptyArray(null) // true
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function isEmptyArray(arr: string | unknown[]) {
|
|
142
|
+
if (!arr) return true
|
|
143
|
+
return Array.isArray(arr) && arr.length === 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function arraytoObject(array: Record<string, unknown>[]) {
|
|
147
|
+
return array.reduce((acc: Record<string, unknown>, currentObj) => {
|
|
148
|
+
const key = Object.keys(currentObj)[0]
|
|
149
|
+
if (key !== undefined) {
|
|
150
|
+
acc[key] = currentObj[key]
|
|
151
|
+
}
|
|
152
|
+
return acc
|
|
153
|
+
}, {})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function shortenObjectKeys(
|
|
157
|
+
obj: Record<string, unknown>,
|
|
158
|
+
keyword: string
|
|
159
|
+
) {
|
|
160
|
+
const regex = new RegExp(`[^]+${keyword}(.*)`)
|
|
161
|
+
|
|
162
|
+
for (const key in obj) {
|
|
163
|
+
const match = key.match(regex)
|
|
164
|
+
|
|
165
|
+
if (match?.[1]) {
|
|
166
|
+
const newKey = convertToCamelCase(match[1])
|
|
167
|
+
obj[newKey] = obj[key]
|
|
168
|
+
delete obj[key]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return obj
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function filterObjectKeys(
|
|
176
|
+
obj: { [x: string]: unknown },
|
|
177
|
+
keyword: string
|
|
178
|
+
) {
|
|
179
|
+
const newObj: { [x: string]: unknown } = {}
|
|
180
|
+
|
|
181
|
+
for (const key in obj) {
|
|
182
|
+
const match = key.includes(keyword)
|
|
183
|
+
|
|
184
|
+
if (match) {
|
|
185
|
+
newObj[key] = obj[key]
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return newObj
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function iterableObject(
|
|
193
|
+
obj: { [s: string]: unknown } | ArrayLike<unknown>
|
|
194
|
+
) {
|
|
195
|
+
return Object.entries(obj).map(([, value]) => {
|
|
196
|
+
return value
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// =============================================================================
|
|
201
|
+
// REF UTILITIES
|
|
202
|
+
// =============================================================================
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Merges multiple React refs into a single ref callback.
|
|
206
|
+
*
|
|
207
|
+
* Useful when you need to assign multiple refs to the same element,
|
|
208
|
+
* such as combining a local ref with a forwarded ref.
|
|
209
|
+
*
|
|
210
|
+
* @param refs - Refs to merge (functions and ref objects supported)
|
|
211
|
+
* @returns A ref callback that updates all provided refs
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```tsx
|
|
215
|
+
* // Component that needs both local and forwarded ref
|
|
216
|
+
* function MyComponent({ ref }: { ref: React.Ref<HTMLDivElement> }) {
|
|
217
|
+
* const localRef = useRef<HTMLDivElement>(null)
|
|
218
|
+
* const mergedRef = mergeRefs(localRef, ref)
|
|
219
|
+
*
|
|
220
|
+
* return <div ref={mergedRef}>Content</div>
|
|
221
|
+
* }
|
|
222
|
+
*
|
|
223
|
+
* // Forward ref pattern
|
|
224
|
+
* const ForwardedComponent = forwardRef(MyComponent)
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
export function mergeRefs<T>(...refs: (Ref<T> | undefined)[]): Ref<T> {
|
|
228
|
+
return (value) => {
|
|
229
|
+
const cleanups = refs.reduce<VoidFunction[]>((accumulator, ref) => {
|
|
230
|
+
if (typeof ref === "function") {
|
|
231
|
+
const cleanup = ref(value)
|
|
232
|
+
if (typeof cleanup === "function") {
|
|
233
|
+
accumulator.push(cleanup)
|
|
234
|
+
}
|
|
235
|
+
} else if (ref) {
|
|
236
|
+
ref.current = value
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return accumulator
|
|
240
|
+
}, [])
|
|
241
|
+
|
|
242
|
+
return () => {
|
|
243
|
+
for (const cleanup of cleanups) cleanup()
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Type augmentations and module declarations
|
|
2
|
+
|
|
3
|
+
// React CSS custom properties support
|
|
4
|
+
import "react"
|
|
5
|
+
|
|
6
|
+
declare module "react" {
|
|
7
|
+
interface CSSProperties {
|
|
8
|
+
[key: `--${string}`]: string | number
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Global window extensions
|
|
13
|
+
declare global {
|
|
14
|
+
interface Window {}
|
|
15
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for viewport utilities
|
|
3
|
+
*
|
|
4
|
+
* Run with: bun test lib/utils/viewport.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "bun:test"
|
|
8
|
+
import { toem, torem, tovw } from "./viewport"
|
|
9
|
+
|
|
10
|
+
// Helper to check vw values with floating point tolerance
|
|
11
|
+
function expectVw(actual: string, expectedVw: number) {
|
|
12
|
+
const match = actual.match(/^([\d.]+)vw$/)
|
|
13
|
+
if (!match?.[1]) {
|
|
14
|
+
throw new Error(`Expected vw format, got: ${actual}`)
|
|
15
|
+
}
|
|
16
|
+
const actualVw = Number.parseFloat(match[1])
|
|
17
|
+
expect(actualVw).toBeCloseTo(expectedVw, 4)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Helper to check max() values with floating point tolerance
|
|
21
|
+
function expectMaxVw(actual: string, expectedMin: number, expectedVw: number) {
|
|
22
|
+
const match = actual.match(/^max\((\d+)px, ([\d.]+)vw\)$/)
|
|
23
|
+
if (!(match?.[1] && match[2])) {
|
|
24
|
+
throw new Error(`Expected max() format, got: ${actual}`)
|
|
25
|
+
}
|
|
26
|
+
const actualMin = Number.parseFloat(match[1])
|
|
27
|
+
const actualVw = Number.parseFloat(match[2])
|
|
28
|
+
expect(actualMin).toBe(expectedMin)
|
|
29
|
+
expect(actualVw).toBeCloseTo(expectedVw, 4)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("tovw", () => {
|
|
33
|
+
describe("basic conversion (default desktop context)", () => {
|
|
34
|
+
it("should convert pixels to vw with default desktop context (1440px)", () => {
|
|
35
|
+
expectVw(tovw(100), 6.9444)
|
|
36
|
+
expect(tovw(1440)).toBe("100vw")
|
|
37
|
+
expect(tovw(720)).toBe("50vw")
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("should handle zero", () => {
|
|
41
|
+
expect(tovw(0)).toBe("0")
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe("with minimum value", () => {
|
|
46
|
+
it("should apply minimum value using max()", () => {
|
|
47
|
+
expectMaxVw(tovw(100, 50), 50, 6.9444)
|
|
48
|
+
expectMaxVw(tovw(200, 100), 100, 13.8889)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("should handle zero target with min", () => {
|
|
52
|
+
expect(tovw(0, 50)).toBe("0")
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe("with context identifier", () => {
|
|
57
|
+
it("should use mobile context (375px)", () => {
|
|
58
|
+
expectVw(tovw(16, "mobile"), 4.2667)
|
|
59
|
+
expect(tovw(375, "mobile")).toBe("100vw")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("should use desktop-large context (1920px)", () => {
|
|
63
|
+
expectVw(tovw(100, "desktop-large"), 5.2083)
|
|
64
|
+
expect(tovw(1920, "desktop-large")).toBe("100vw")
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("should use tablet-lg context (1024px)", () => {
|
|
68
|
+
expectVw(tovw(100, "tablet-lg"), 9.7656)
|
|
69
|
+
expect(tovw(1024, "tablet-lg")).toBe("100vw")
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it("should use tablet context (620px)", () => {
|
|
73
|
+
expectVw(tovw(100, "tablet"), 16.129)
|
|
74
|
+
expect(tovw(620, "tablet")).toBe("100vw")
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("should use desktop context explicitly", () => {
|
|
78
|
+
expectVw(tovw(100, "desktop"), 6.9444)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe("with custom numeric context", () => {
|
|
83
|
+
it("should use custom pixel context as third parameter", () => {
|
|
84
|
+
// Numeric contexts must be passed as third parameter (with undefined min)
|
|
85
|
+
// Using type assertion to test the implementation behavior
|
|
86
|
+
expectVw(tovw(100, 1920), 5.2083)
|
|
87
|
+
expectVw(tovw(100, 800), 12.5)
|
|
88
|
+
expectVw(tovw(100, 2000), 5)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("should treat numeric second parameter as min, not context", () => {
|
|
92
|
+
// When second param is a number, it's treated as min, not context
|
|
93
|
+
expectMaxVw(tovw(100, 1920), 1920, 6.9444)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe("with both min and context", () => {
|
|
98
|
+
it("should apply min and context together", () => {
|
|
99
|
+
expectMaxVw(tovw(100, 50, "mobile"), 50, 26.6667)
|
|
100
|
+
expectMaxVw(tovw(200, 100, "desktop-large"), 100, 10.4167)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("should use numeric context with min", () => {
|
|
104
|
+
expectMaxVw(tovw(100, 50, 1920), 50, 5.2083)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe("edge cases", () => {
|
|
109
|
+
it("should handle very small values", () => {
|
|
110
|
+
expectVw(tovw(1), 0.0694)
|
|
111
|
+
expectVw(tovw(0.5), 0.0347)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("should handle very large values", () => {
|
|
115
|
+
expectVw(tovw(5000), 347.2222)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("should handle decimal values", () => {
|
|
119
|
+
expectVw(tovw(16.5), 1.1458)
|
|
120
|
+
expectVw(tovw(16.5, "mobile"), 4.4)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe("torem", () => {
|
|
126
|
+
describe("basic conversion (default 16px context)", () => {
|
|
127
|
+
it("should convert pixels to rem with default context", () => {
|
|
128
|
+
expect(torem(16)).toBe("1rem")
|
|
129
|
+
expect(torem(24)).toBe("1.5rem")
|
|
130
|
+
expect(torem(32)).toBe("2rem")
|
|
131
|
+
expect(torem(8)).toBe("0.5rem")
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("should handle zero", () => {
|
|
135
|
+
expect(torem(0)).toBe("0")
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe("with custom context", () => {
|
|
140
|
+
it("should use custom base font size", () => {
|
|
141
|
+
// Using toBeCloseTo for floating point comparison
|
|
142
|
+
const result1 = torem(18, 14)
|
|
143
|
+
const match1 = result1.match(/^([\d.]+)rem$/)
|
|
144
|
+
expect(Number.parseFloat(match1?.[1] || "0")).toBeCloseTo(
|
|
145
|
+
1.2857142857142858,
|
|
146
|
+
4
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
expect(torem(20, 10)).toBe("2rem")
|
|
150
|
+
expect(torem(24, 12)).toBe("2rem")
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it("should handle zero with custom context", () => {
|
|
154
|
+
expect(torem(0, 14)).toBe("0")
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe("edge cases", () => {
|
|
159
|
+
it("should handle decimal values", () => {
|
|
160
|
+
const result1 = torem(16.5)
|
|
161
|
+
const match1 = result1.match(/^([\d.]+)rem$/)
|
|
162
|
+
// 16.5 / 16 = 1.03125, rounded to 4 decimals = 1.0313
|
|
163
|
+
expect(Number.parseFloat(match1?.[1] || "0")).toBeCloseTo(1.0313, 4)
|
|
164
|
+
|
|
165
|
+
const result2 = torem(18.5, 14)
|
|
166
|
+
const match2 = result2.match(/^([\d.]+)rem$/)
|
|
167
|
+
// 18.5 / 14 = 1.3214285714285714, rounded to 4 decimals = 1.3214
|
|
168
|
+
expect(Number.parseFloat(match2?.[1] || "0")).toBeCloseTo(1.3214, 4)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("should handle very small values", () => {
|
|
172
|
+
const result1 = torem(1)
|
|
173
|
+
const match1 = result1.match(/^([\d.]+)rem$/)
|
|
174
|
+
expect(Number.parseFloat(match1?.[1] || "0")).toBeCloseTo(0.0625, 4)
|
|
175
|
+
|
|
176
|
+
const result2 = torem(0.5)
|
|
177
|
+
const match2 = result2.match(/^([\d.]+)rem$/)
|
|
178
|
+
// 0.5 / 16 = 0.03125, rounded to 4 decimals = 0.0313
|
|
179
|
+
expect(Number.parseFloat(match2?.[1] || "0")).toBeCloseTo(0.0313, 4)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it("should handle very large values", () => {
|
|
183
|
+
expect(torem(100)).toBe("6.25rem")
|
|
184
|
+
expect(torem(200)).toBe("12.5rem")
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe("toem", () => {
|
|
190
|
+
describe("basic conversion", () => {
|
|
191
|
+
it("should convert pixels to em", () => {
|
|
192
|
+
expect(toem(24, 16)).toBe("1.5em")
|
|
193
|
+
expect(toem(16, 16)).toBe("1em")
|
|
194
|
+
expect(toem(32, 16)).toBe("2em")
|
|
195
|
+
expect(toem(8, 16)).toBe("0.5em")
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it("should handle zero", () => {
|
|
199
|
+
expect(toem(0, 16)).toBe("0")
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe("with different contexts", () => {
|
|
204
|
+
it("should use custom context size", () => {
|
|
205
|
+
const result1 = toem(18, 14)
|
|
206
|
+
const match1 = result1.match(/^([\d.]+)em$/)
|
|
207
|
+
expect(Number.parseFloat(match1?.[1] || "0")).toBeCloseTo(
|
|
208
|
+
1.2857142857142858,
|
|
209
|
+
4
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
expect(toem(20, 10)).toBe("2em")
|
|
213
|
+
expect(toem(24, 12)).toBe("2em")
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it("should handle zero with different contexts", () => {
|
|
217
|
+
expect(toem(0, 14)).toBe("0")
|
|
218
|
+
expect(toem(0, 20)).toBe("0")
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe("edge cases", () => {
|
|
223
|
+
it("should handle decimal values", () => {
|
|
224
|
+
const result1 = toem(16.5, 16)
|
|
225
|
+
const match1 = result1.match(/^([\d.]+)em$/)
|
|
226
|
+
// 16.5 / 16 = 1.03125, rounded to 4 decimals = 1.0313
|
|
227
|
+
expect(Number.parseFloat(match1?.[1] || "0")).toBeCloseTo(1.0313, 4)
|
|
228
|
+
|
|
229
|
+
const result2 = toem(18.5, 14)
|
|
230
|
+
const match2 = result2.match(/^([\d.]+)em$/)
|
|
231
|
+
// 18.5 / 14 = 1.3214285714285714, rounded to 4 decimals = 1.3214
|
|
232
|
+
expect(Number.parseFloat(match2?.[1] || "0")).toBeCloseTo(1.3214, 4)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it("should handle very small values", () => {
|
|
236
|
+
const result1 = toem(1, 16)
|
|
237
|
+
const match1 = result1.match(/^([\d.]+)em$/)
|
|
238
|
+
expect(Number.parseFloat(match1?.[1] || "0")).toBeCloseTo(0.0625, 4)
|
|
239
|
+
|
|
240
|
+
const result2 = toem(0.5, 16)
|
|
241
|
+
const match2 = result2.match(/^([\d.]+)em$/)
|
|
242
|
+
// 0.5 / 16 = 0.03125, rounded to 4 decimals = 0.0313
|
|
243
|
+
expect(Number.parseFloat(match2?.[1] || "0")).toBeCloseTo(0.0313, 4)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it("should handle very large values", () => {
|
|
247
|
+
expect(toem(100, 16)).toBe("6.25em")
|
|
248
|
+
expect(toem(200, 16)).toBe("12.5em")
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it("should handle context larger than target", () => {
|
|
252
|
+
expect(toem(8, 16)).toBe("0.5em")
|
|
253
|
+
expect(toem(4, 16)).toBe("0.25em")
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
})
|