diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b357b9..c40f5fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: node-version: '22.12.0' - name: Install pnpm - run: npm install --global pnpm@11.21.0 + run: npm install --global pnpm@10.34.5 - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.prettierignore b/.prettierignore index 857335b..625450d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ legacy/ LICENSE public/favicon.svg public/robots.txt +public/sitemap.xml src/features/studio/defaultStudioDocument.json src/features/export/standaloneEngine.generated.ts pnpm-lock.yaml diff --git a/README.md b/README.md index 9230fee..819f9d0 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Bible Strong Avatar Lab is a browser-based authoring studio for procedural 2D avatars. It combines 3D-inspired geometry with SVG rendering so you can construct a character, define its neutral appearance, create expressions, compose reusable animations, and export the result without depending on the Studio UI. +Website: [bible-strong.app](https://bible-strong.app) · Source: [GitHub](https://github.com/smontlouis/bible-strong-avatar-lab) + The application runs entirely in the browser. Projects are stored locally and can be moved between browsers with JSON export/import; no account or backend is required. ## What you can do @@ -64,7 +66,7 @@ The JSON project file contains the complete current document: avatars, base and ### Requirements - Node.js 22.12 or newer -- pnpm 11.21 (declared by the repository's `packageManager` field) +- pnpm 10.34 (declared by the repository's `packageManager` field) ### Install and run @@ -116,6 +118,8 @@ pnpm preview The application is a client-only Vite site with no server runtime or environment variables. The build uses relative asset paths, so the contents of `dist/` can be hosted at a domain root or a subpath, including a GitHub Pages project URL. Any static host that serves `index.html` can deploy it. +Vercel Web Analytics and Speed Insights are integrated in the React entry point. Enable both products in the Vercel project dashboard, then redeploy so their collection routes become available. + ## Technical overview - **React 19** coordinates the editor UI and durable application state. diff --git a/index.html b/index.html index 9d28d74..ae2169b 100644 --- a/index.html +++ b/index.html @@ -10,20 +10,36 @@ content="Create procedural 2D avatars, expressions and animations in your browser, then export reusable React, JavaScript, SVG and PNG assets." /> + + + - + + + + + + + + diff --git a/package.json b/package.json index ef88cf4..e9ba1d5 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,19 @@ "name": "bible-strong-avatar-lab", "version": "0.1.0", "description": "A browser-based studio for designing procedural 2D avatars, expressions and reusable animations.", + "author": "Stéphane Montlouis-Calixte", + "homepage": "https://bible-strong.app", + "repository": { + "type": "git", + "url": "git+https://github.com/smontlouis/bible-strong-avatar-lab.git" + }, + "bugs": { + "url": "https://github.com/smontlouis/bible-strong-avatar-lab/issues" + }, "license": "AGPL-3.0-only", "private": true, "type": "module", - "packageManager": "pnpm@11.21.0", + "packageManager": "pnpm@10.34.5", "engines": { "node": ">=22.12.0" }, @@ -24,6 +33,8 @@ }, "dependencies": { "@base-ui/react": "^1.7.0", + "@vercel/analytics": "^2.0.1", + "@vercel/speed-insights": "^2.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.31.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b6ddbb..08f796c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: '@base-ui/react': specifier: ^1.7.0 version: 1.7.0(@types/react@19.2.18)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@vercel/analytics': + specifier: ^2.0.1 + version: 2.0.1(react@19.2.3) + '@vercel/speed-insights': + specifier: ^2.0.0 + version: 2.0.0(react@19.2.3) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -440,6 +446,61 @@ packages: '@types/react@19.2.18': resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + '@vercel/analytics@2.0.1': + resolution: {integrity: sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==} + peerDependencies: + '@remix-run/react': ^2 + '@sveltejs/kit': ^1 || ^2 + next: '>= 13' + nuxt: '>= 3' + react: ^18 || ^19 || ^19.0.0-rc + svelte: '>= 4' + vue: ^3 + vue-router: ^4 + peerDependenciesMeta: + '@remix-run/react': + optional: true + '@sveltejs/kit': + optional: true + next: + optional: true + nuxt: + optional: true + react: + optional: true + svelte: + optional: true + vue: + optional: true + vue-router: + optional: true + + '@vercel/speed-insights@2.0.0': + resolution: {integrity: sha512-jwkNcrTeafWxjmWq4AHBaptSqZiJkYU5adLC9QBSqeim0GcqDMgN5Ievh8OG1rJ6W3A4l1oiP7qr9CWxGuzu3w==} + peerDependencies: + '@sveltejs/kit': ^1 || ^2 + next: '>= 13' + nuxt: '>= 3' + react: ^18 || ^19 || ^19.0.0-rc + svelte: '>= 4' + vue: ^3 + vue-router: ^4 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + next: + optional: true + nuxt: + optional: true + react: + optional: true + svelte: + optional: true + vue: + optional: true + vue-router: + optional: true + '@vitejs/plugin-react@6.0.5': resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1298,6 +1359,14 @@ snapshots: dependencies: csstype: 3.2.3 + '@vercel/analytics@2.0.1(react@19.2.3)': + optionalDependencies: + react: 19.2.3 + + '@vercel/speed-insights@2.0.0(react@19.2.3)': + optionalDependencies: + react: 19.2.3 + '@vitejs/plugin-react@6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.2.4)(vite@8.2.1(@types/node@24.13.3)(jiti@2.7.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.2.1(@types/node@24.13.3)(jiti@2.7.0))': dependencies: '@rolldown/pluginutils': 1.0.1 diff --git a/public/og-image.png b/public/og-image.png new file mode 100644 index 0000000..9b1d6fc Binary files /dev/null and b/public/og-image.png differ diff --git a/public/robots.txt b/public/robots.txt index c2a49f4..4a480bb 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,2 +1,4 @@ User-agent: * Allow: / + +Sitemap: https://bible-strong.app/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..6a7e808 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,6 @@ + + + + https://bible-strong.app/ + + diff --git a/src/app/styles.css b/src/app/styles.css index 9aff0c3..60c3251 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -222,6 +222,32 @@ input { font-size: 15px; line-height: 1; } +.source-link { + display: inline-flex; + align-items: center; + gap: 5px; + color: #f6f7f8; + font-size: 10px; + font-weight: 750; + text-decoration: none; +} +.source-link:hover { + color: white; +} +.source-link:focus-visible { + border-radius: 5px; + outline: 2px solid #7895ee; + outline-offset: 2px; +} +.source-link svg { + width: 14px; + height: 14px; +} +.identity-divider { + align-self: stretch; + width: 1px; + background: rgb(255 255 255 / 14%); +} .language-picker [data-slot='select-trigger'] { width: auto; height: 24px; @@ -1317,7 +1343,7 @@ p { min-width: 0; flex-direction: column; gap: 3px; - border-radius: 12px; + border-radius: 999px; color: #7b828e; font-size: 9px; font-weight: 750; @@ -2386,6 +2412,9 @@ p { .stage-identity .language-picker [data-slot='select-trigger'] { min-width: 68px; } + .stage-identity .source-link > span { + display: none; + } .avatar-wrap { width: min(42dvh, 78vw, 340px); } diff --git a/src/features/studio/components/AvatarDrawer.tsx b/src/features/studio/components/AvatarDrawer.tsx index 044b7bc..2c88dd1 100644 --- a/src/features/studio/components/AvatarDrawer.tsx +++ b/src/features/studio/components/AvatarDrawer.tsx @@ -1,5 +1,5 @@ import { Copy, Pencil, Plus, Trash2 } from 'lucide-react' -import { motion } from 'motion/react' +import { motion, useDragControls } from 'motion/react' import { Button } from '@/components/ui/button' import { @@ -9,7 +9,7 @@ import { ContextMenuSeparator, ContextMenuTrigger, } from '@/components/ui/context-menu' -import { DrawerClose, DrawerContent, DrawerDescription, DrawerTitle } from '@/components/ui/drawer' +import { DrawerContent, DrawerDescription, DrawerTitle } from '@/components/ui/drawer' import { defaultAvatarEyes } from '@/features/avatar/avatars' import { ExpressionPreview } from '@/features/avatar/components/ExpressionWorkspace' import { defaultExpression } from '@/features/avatar/presets' @@ -22,6 +22,7 @@ export function AvatarDrawer({ controller: StudioController onOpenChange: (open: boolean) => void }) { + const drawerDragControls = useDragControls() const { activateAvatar, activeAvatarId, @@ -45,10 +46,30 @@ export function AvatarDrawer({ } = controller return ( - - + { + if (info.offset.y > 70 || info.velocity.y > 600) onOpenChange(false) + }} + /> + } + > +
Avatars diff --git a/src/features/studio/components/StudioIdentity.tsx b/src/features/studio/components/StudioIdentity.tsx index 93b4a6e..0578137 100644 --- a/src/features/studio/components/StudioIdentity.tsx +++ b/src/features/studio/components/StudioIdentity.tsx @@ -1,3 +1,5 @@ +import { Code2 } from 'lucide-react' + import { Select, SelectContent, @@ -20,6 +22,17 @@ export function StudioIdentity({ className = '', language, setLanguage, t }: Stu Bible Strong Avatar Lab
+ + +