create-omg 0.4.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/index.mjs +100 -0
- package/dist/template/AGENTS.md +49 -0
- package/dist/template/README.md +18 -0
- package/dist/template/_gitignore +24 -0
- package/dist/template/bun.lock +898 -0
- package/dist/template/eslint.config.js +23 -0
- package/dist/template/functions/.gitkeep +0 -0
- package/dist/template/index.html +20 -0
- package/dist/template/package.json +48 -0
- package/dist/template/public/favicon.svg +1 -0
- package/dist/template/public/icons/apple-touch-icon.png +0 -0
- package/dist/template/public/icons/pwa-192x192.png +0 -0
- package/dist/template/public/icons/pwa-512x512-maskable.png +0 -0
- package/dist/template/public/icons/pwa-512x512.png +0 -0
- package/dist/template/public/icons.svg +24 -0
- package/dist/template/schema.ts +5 -0
- package/dist/template/server/db.ts +103 -0
- package/dist/template/src/App.tsx +128 -0
- package/dist/template/src/components/ui/animated-number.tsx +102 -0
- package/dist/template/src/components/ui/animated-text.tsx +109 -0
- package/dist/template/src/components/ui/bottom-nav.tsx +131 -0
- package/dist/template/src/components/ui/button.tsx +84 -0
- package/dist/template/src/components/ui/card.tsx +70 -0
- package/dist/template/src/components/ui/otp-input.tsx +179 -0
- package/dist/template/src/components/ui/scroll-affordance.tsx +106 -0
- package/dist/template/src/components/ui/sound.ts +182 -0
- package/dist/template/src/components/ui/transitions.tsx +419 -0
- package/dist/template/src/components/ui/vibes-ui-styles.ts +388 -0
- package/dist/template/src/db.drizzle.ts +5 -0
- package/dist/template/src/index.css +1 -0
- package/dist/template/src/lib/utils.ts +6 -0
- package/dist/template/src/main.tsx +10 -0
- package/dist/template/tsconfig.app.json +30 -0
- package/dist/template/tsconfig.json +10 -0
- package/dist/template/tsconfig.node.json +24 -0
- package/dist/template/vite.config.ts +23 -0
- package/package.json +37 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
7
|
+
|
|
8
|
+
export default defineConfig([
|
|
9
|
+
globalIgnores(['dist']),
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
extends: [
|
|
13
|
+
js.configs.recommended,
|
|
14
|
+
tseslint.configs.recommended,
|
|
15
|
+
reactHooks.configs.flat.recommended,
|
|
16
|
+
reactRefresh.configs.vite,
|
|
17
|
+
],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: 2020,
|
|
20
|
+
globals: globals.browser,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
])
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<!-- Own both color schemes so Android browsers (Chrome auto-dark, Samsung
|
|
6
|
+
Internet "Web page darkening") don't run their force-dark pass on top
|
|
7
|
+
of the app's own theme and produce a muddy double-darkened result. -->
|
|
8
|
+
<meta name="color-scheme" content="light dark" />
|
|
9
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
10
|
+
<meta
|
|
11
|
+
name="viewport"
|
|
12
|
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover"
|
|
13
|
+
/>
|
|
14
|
+
<title>react-ts</title>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div id="root"></div>
|
|
18
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-ts",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "mkdir -p $HOME/.bun/bin && ln -sf $(command -v bun) $HOME/.bun/bin/node && PATH=$HOME/.bun/bin:$PATH bun ./node_modules/vite-plus/dist/bin.js dev --host 0.0.0.0",
|
|
8
|
+
"build": "mkdir -p $HOME/.bun/bin && ln -sf $(command -v bun) $HOME/.bun/bin/node && PATH=$HOME/.bun/bin:$PATH bun ./node_modules/vite-plus/dist/bin.js build",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@omg-dev/ai": "^0.4.0",
|
|
14
|
+
"@omg-dev/auth": "^0.4.0",
|
|
15
|
+
"@omg-dev/db-collection": "^0.4.0",
|
|
16
|
+
"@omg-dev/media": "^0.4.0",
|
|
17
|
+
"@omg-dev/pwa": "^0.4.0",
|
|
18
|
+
"@omg-dev/schema": "^0.4.0",
|
|
19
|
+
"@omg-dev/sdk": "^0.4.0",
|
|
20
|
+
"@omg-dev/server": "^0.4.0",
|
|
21
|
+
"better-drizzle": "^0.1.1",
|
|
22
|
+
"clsx": "^2.1.1",
|
|
23
|
+
"drizzle-orm": "^0.45.2",
|
|
24
|
+
"lucide-react": "^1.16.0",
|
|
25
|
+
"react": "^19.2.4",
|
|
26
|
+
"react-dom": "^19.2.4",
|
|
27
|
+
"tailwind-merge": "^3.5.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@eslint/js": "^9.39.4",
|
|
31
|
+
"@tailwindcss/vite": "^4.1.0",
|
|
32
|
+
"@types/node": "^24.12.2",
|
|
33
|
+
"@types/react": "^19.2.14",
|
|
34
|
+
"@types/react-dom": "^19.2.3",
|
|
35
|
+
"@typescript/native": "npm:typescript@~7.0.2",
|
|
36
|
+
"@omg-dev/vite-plugin": "^0.4.0",
|
|
37
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
38
|
+
"eslint": "^9.39.4",
|
|
39
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
40
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
41
|
+
"globals": "^17.4.0",
|
|
42
|
+
"tailwindcss": "^4.1.0",
|
|
43
|
+
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
44
|
+
"typescript-eslint": "^8.58.0",
|
|
45
|
+
"vite": "^8.0.4",
|
|
46
|
+
"vite-plus": "0.1.21"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
|
3
|
+
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
|
4
|
+
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
|
5
|
+
</symbol>
|
|
6
|
+
<symbol id="discord-icon" viewBox="0 0 20 19">
|
|
7
|
+
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
|
8
|
+
</symbol>
|
|
9
|
+
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
|
10
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
|
11
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
|
12
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
|
13
|
+
</symbol>
|
|
14
|
+
<symbol id="github-icon" viewBox="0 0 19 19">
|
|
15
|
+
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
|
16
|
+
</symbol>
|
|
17
|
+
<symbol id="social-icon" viewBox="0 0 20 20">
|
|
18
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
|
19
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
|
20
|
+
</symbol>
|
|
21
|
+
<symbol id="x-icon" viewBox="0 0 19 19">
|
|
22
|
+
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
|
23
|
+
</symbol>
|
|
24
|
+
</svg>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { better } from "better-drizzle"
|
|
2
|
+
import { drizzle } from "drizzle-orm/bun-sqlite"
|
|
3
|
+
import { db } from "@omg-dev/server"
|
|
4
|
+
import { drizzleSchema } from "../src/db.drizzle"
|
|
5
|
+
|
|
6
|
+
type BetterClient = ReturnType<typeof createBetterClient>
|
|
7
|
+
type ReadMethod =
|
|
8
|
+
| "count"
|
|
9
|
+
| "exists"
|
|
10
|
+
| "findFirst"
|
|
11
|
+
| "findMany"
|
|
12
|
+
| "findOne"
|
|
13
|
+
| "findUnique"
|
|
14
|
+
| "paginate"
|
|
15
|
+
type ReadOnlyDelegate<T> = Pick<T, Extract<keyof T, ReadMethod>>
|
|
16
|
+
type ReadOnlyBetterClient<T> = {
|
|
17
|
+
readonly [K in keyof T as K extends string
|
|
18
|
+
? K extends `$${string}` | "repository" | "transaction"
|
|
19
|
+
? never
|
|
20
|
+
: T[K] extends object
|
|
21
|
+
? K
|
|
22
|
+
: never
|
|
23
|
+
: never]: ReadOnlyDelegate<T[K]>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const READ_METHODS = new Set<string>([
|
|
27
|
+
"count",
|
|
28
|
+
"exists",
|
|
29
|
+
"findFirst",
|
|
30
|
+
"findMany",
|
|
31
|
+
"findOne",
|
|
32
|
+
"findUnique",
|
|
33
|
+
"paginate",
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
let drizzleClient: ReturnType<typeof createDrizzleClient> | null = null
|
|
37
|
+
let betterClient: ReturnType<typeof createBetterClient> | null = null
|
|
38
|
+
let betterReadClient: ReadOnlyBetterClient<BetterClient> | null = null
|
|
39
|
+
|
|
40
|
+
function createDrizzleClient() {
|
|
41
|
+
return drizzle(db.raw(), { schema: drizzleSchema })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createBetterClient() {
|
|
45
|
+
return better(getDrizzleDb(), { schema: drizzleSchema })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getDrizzleDb() {
|
|
49
|
+
drizzleClient ??= createDrizzleClient()
|
|
50
|
+
return drizzleClient
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getBetterDb() {
|
|
54
|
+
betterClient ??= createBetterClient()
|
|
55
|
+
return betterClient
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readOnlyDelegate(delegate: Record<string, unknown>, tableName: string) {
|
|
59
|
+
return new Proxy({}, {
|
|
60
|
+
get(_target, prop) {
|
|
61
|
+
if (typeof prop !== "string") return undefined
|
|
62
|
+
if (!READ_METHODS.has(prop)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`[vibes] Better Drizzle helper is read-only. Use @omg-dev/server db or auto-CRUD for ${tableName}.${prop} mutations so auth scoping, timestamps, and live updates stay consistent.`,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const value = delegate[prop]
|
|
69
|
+
return typeof value === "function" ? value.bind(delegate) : value
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createBetterReadClient(): ReadOnlyBetterClient<BetterClient> {
|
|
75
|
+
const client = getBetterDb() as Record<string, unknown>
|
|
76
|
+
const delegates = new Map<string, object>()
|
|
77
|
+
|
|
78
|
+
return new Proxy({}, {
|
|
79
|
+
get(_target, prop) {
|
|
80
|
+
if (typeof prop !== "string") return undefined
|
|
81
|
+
if (prop.startsWith("$") || prop === "repository" || prop === "transaction") {
|
|
82
|
+
throw new Error(`[vibes] Better Drizzle helper only exposes table read delegates.`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const value = client[prop]
|
|
86
|
+
if (!value || typeof value !== "object") return undefined
|
|
87
|
+
|
|
88
|
+
let delegate = delegates.get(prop)
|
|
89
|
+
if (!delegate) {
|
|
90
|
+
delegate = readOnlyDelegate(value as Record<string, unknown>, prop)
|
|
91
|
+
delegates.set(prop, delegate)
|
|
92
|
+
}
|
|
93
|
+
return delegate
|
|
94
|
+
},
|
|
95
|
+
}) as ReadOnlyBetterClient<BetterClient>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getBetterReadDb() {
|
|
99
|
+
betterReadClient ??= createBetterReadClient()
|
|
100
|
+
return betterReadClient
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { drizzleSchema }
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import {
|
|
3
|
+
Bot,
|
|
4
|
+
Lightbulb,
|
|
5
|
+
Database,
|
|
6
|
+
Home,
|
|
7
|
+
ImagePlus,
|
|
8
|
+
KeyRound,
|
|
9
|
+
Mic,
|
|
10
|
+
User,
|
|
11
|
+
Zap,
|
|
12
|
+
} from "lucide-react"
|
|
13
|
+
import { AnimatedNumber } from "@/components/ui/animated-number"
|
|
14
|
+
import { AnimatedText } from "@/components/ui/animated-text"
|
|
15
|
+
import { ScrollAffordance } from "@/components/ui/scroll-affordance"
|
|
16
|
+
import { Card } from "@/components/ui/card"
|
|
17
|
+
import { BottomNav } from "@/components/ui/bottom-nav"
|
|
18
|
+
|
|
19
|
+
const verbs = ["build", "ship", "iterate", "launch", "design"]
|
|
20
|
+
|
|
21
|
+
const ideas = [
|
|
22
|
+
{
|
|
23
|
+
title: "Add sign-in",
|
|
24
|
+
prompt: "Add sign-in with an email code",
|
|
25
|
+
icon: KeyRound,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
title: "Save data",
|
|
29
|
+
prompt: "Add a posts collection and a form to create them",
|
|
30
|
+
icon: Database,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: "Add AI chat",
|
|
34
|
+
prompt: "Add a chat box that streams from Claude",
|
|
35
|
+
icon: Bot,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
title: "Server endpoint",
|
|
39
|
+
prompt: "Add an API route that returns JSON",
|
|
40
|
+
icon: Zap,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: "Upload files",
|
|
44
|
+
prompt: "Let users upload an avatar image and show it on their profile",
|
|
45
|
+
icon: ImagePlus,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: "Add voice",
|
|
49
|
+
prompt: "Add a button that reads the page summary aloud with generated speech",
|
|
50
|
+
icon: Mic,
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
// The vendored bottom tab bar (src/components/ui/bottom-nav.tsx) is a
|
|
55
|
+
// controlled component — drive `active`/`onSelect` from state or your router.
|
|
56
|
+
const tabs = [
|
|
57
|
+
{ id: "home", label: "Home", Icon: Home },
|
|
58
|
+
{ id: "ideas", label: "Ideas", Icon: Lightbulb },
|
|
59
|
+
{ id: "you", label: "You", Icon: User },
|
|
60
|
+
] as const
|
|
61
|
+
|
|
62
|
+
function App() {
|
|
63
|
+
// Taste-by-default delight primitives, vendored as editable source under
|
|
64
|
+
// src/components/ui/. The count rolls up from 0 on first paint
|
|
65
|
+
// (<AnimatedNumber>), the headline verb flips on a timer (<AnimatedText>),
|
|
66
|
+
// and <ScrollAffordance> is a self-aware "more below" cue that shows only
|
|
67
|
+
// when the page overflows. All zero-dep and themed from your shadcn
|
|
68
|
+
// variables — open the files and restyle freely.
|
|
69
|
+
const [count, setCount] = useState(0)
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const id = requestAnimationFrame(() => setCount(ideas.length))
|
|
72
|
+
return () => cancelAnimationFrame(id)
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
const [verbIndex, setVerbIndex] = useState(0)
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const id = setInterval(
|
|
78
|
+
() => setVerbIndex((i) => (i + 1) % verbs.length),
|
|
79
|
+
2200,
|
|
80
|
+
)
|
|
81
|
+
return () => clearInterval(id)
|
|
82
|
+
}, [])
|
|
83
|
+
|
|
84
|
+
const [tab, setTab] = useState<(typeof tabs)[number]["id"]>("home")
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<main className="min-h-svh bg-background text-foreground flex flex-col items-center justify-center px-6 pt-12 pb-28">
|
|
88
|
+
<div className="w-full max-w-xl">
|
|
89
|
+
<header className="mb-8 text-center">
|
|
90
|
+
<h1 className="text-2xl font-semibold tracking-tight inline-flex items-baseline gap-2 flex-wrap justify-center">
|
|
91
|
+
<span>What do you want to</span>
|
|
92
|
+
<AnimatedText value={verbs[verbIndex]} className="text-primary" />
|
|
93
|
+
</h1>
|
|
94
|
+
<p className="text-sm text-muted-foreground mt-2">
|
|
95
|
+
<AnimatedNumber value={count} className="font-medium text-foreground" />{" "}
|
|
96
|
+
ways to start.
|
|
97
|
+
</p>
|
|
98
|
+
</header>
|
|
99
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
100
|
+
{ideas.map(({ title, prompt, icon: Icon }) => (
|
|
101
|
+
<Card
|
|
102
|
+
key={title}
|
|
103
|
+
className="gap-2 p-4 transition-colors hover:bg-accent/40"
|
|
104
|
+
>
|
|
105
|
+
<Icon
|
|
106
|
+
className="size-5 text-muted-foreground mb-1"
|
|
107
|
+
aria-hidden="true"
|
|
108
|
+
/>
|
|
109
|
+
<h2 className="text-sm font-medium">{title}</h2>
|
|
110
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
111
|
+
{prompt}
|
|
112
|
+
</p>
|
|
113
|
+
</Card>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
<div className="mt-10 flex justify-center">
|
|
117
|
+
<ScrollAffordance label="More" />
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* The omg home-screen bottom bar, now vendored. Swap these tabs for
|
|
122
|
+
your app's sections and wire onSelect to your router. */}
|
|
123
|
+
<BottomNav tabs={tabs} active={tab} onSelect={setTab} />
|
|
124
|
+
</main>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default App
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// <AnimatedNumber> — odometer rolling digits (the number half of slot-text;
|
|
2
|
+
// its sibling <AnimatedText> rolls arbitrary letters).
|
|
3
|
+
//
|
|
4
|
+
// Vendored, editable source (was @omg-dev/ui). Any number that changes deserves
|
|
5
|
+
// to *move*: counts, prices, stats. Self-contained — no Tailwind utility
|
|
6
|
+
// classes, no CSS import beyond the injected `.vui-*` stylesheet, no runtime
|
|
7
|
+
// dependency beyond React. Themes itself from your shadcn CSS variables
|
|
8
|
+
// (--foreground) with light/dark fallbacks and honors prefers-reduced-motion.
|
|
9
|
+
|
|
10
|
+
import { useEffect, type CSSProperties } from "react"
|
|
11
|
+
import { ensureStyles } from "@/components/ui/vibes-ui-styles"
|
|
12
|
+
|
|
13
|
+
const DIGITS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
|
14
|
+
|
|
15
|
+
export type AnimatedNumberProps = {
|
|
16
|
+
/** The value to display. Changing it rolls the digits to the new number. */
|
|
17
|
+
value: number
|
|
18
|
+
/**
|
|
19
|
+
* Format the raw value into the string that gets rendered. Defaults to the
|
|
20
|
+
* host locale's grouped integer (e.g. `1,234`). Any digit in the output
|
|
21
|
+
* animates; everything else (separators, currency, units) stays static.
|
|
22
|
+
*/
|
|
23
|
+
format?: (value: number) => string
|
|
24
|
+
className?: string
|
|
25
|
+
style?: CSSProperties
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Rolling-digit number. Each digit is a vertical reel of 0–9 that translates
|
|
30
|
+
* to the target digit; CSS handles the motion, so re-rendering with a new
|
|
31
|
+
* `value` is all it takes to animate. Significant digits cascade in slightly
|
|
32
|
+
* after the small ones for an odometer feel.
|
|
33
|
+
*/
|
|
34
|
+
export function AnimatedNumber({
|
|
35
|
+
value,
|
|
36
|
+
format = defaultFormat,
|
|
37
|
+
className = "",
|
|
38
|
+
style,
|
|
39
|
+
}: AnimatedNumberProps) {
|
|
40
|
+
useEffect(ensureStyles, [])
|
|
41
|
+
|
|
42
|
+
const text = format(value)
|
|
43
|
+
const chars = Array.from(text)
|
|
44
|
+
// Stagger from the least-significant character so the ripple travels left.
|
|
45
|
+
const lastIndex = chars.length - 1
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<span
|
|
49
|
+
className={`vui vui-num ${className}`.trim()}
|
|
50
|
+
style={style}
|
|
51
|
+
role="text"
|
|
52
|
+
aria-label={text}
|
|
53
|
+
>
|
|
54
|
+
{chars.map((char, i) => {
|
|
55
|
+
if (!isDigit(char)) {
|
|
56
|
+
return (
|
|
57
|
+
<span key={i} className="vui-num-sym" aria-hidden="true">
|
|
58
|
+
{char}
|
|
59
|
+
</span>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
const digit = char.charCodeAt(0) - 48
|
|
63
|
+
const fromRight = lastIndex - i
|
|
64
|
+
return (
|
|
65
|
+
<Reel key={i} digit={digit} delay={Math.min(fromRight, 6) * 0.05} />
|
|
66
|
+
)
|
|
67
|
+
})}
|
|
68
|
+
</span>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function Reel({ digit, delay }: { digit: number; delay: number }) {
|
|
73
|
+
return (
|
|
74
|
+
<span className="vui-num-reel" aria-hidden="true">
|
|
75
|
+
<span
|
|
76
|
+
className="vui-num-track"
|
|
77
|
+
style={{
|
|
78
|
+
transform: `translateY(${-digit}em)`,
|
|
79
|
+
transitionDelay: `${delay}s`,
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{DIGITS.map((d) => (
|
|
83
|
+
<span key={d} className="vui-num-cell">
|
|
84
|
+
{d}
|
|
85
|
+
</span>
|
|
86
|
+
))}
|
|
87
|
+
</span>
|
|
88
|
+
</span>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isDigit(char: string) {
|
|
93
|
+
return char.length === 1 && char >= "0" && char <= "9"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function defaultFormat(value: number): string {
|
|
97
|
+
try {
|
|
98
|
+
return new Intl.NumberFormat().format(value)
|
|
99
|
+
} catch {
|
|
100
|
+
return String(value)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// <AnimatedText> — split-flap / slot-text rolling letters.
|
|
2
|
+
//
|
|
3
|
+
// The text half of the slot-text effect (its sibling <AnimatedNumber> rolls
|
|
4
|
+
// digits). Any word that changes deserves to *move*: a rotating headline verb,
|
|
5
|
+
// a status that flips, a name that resolves. Each character position is a
|
|
6
|
+
// vertical reel of glyphs that translates to the target letter — like a
|
|
7
|
+
// split-flap departure board. Self-contained — no Tailwind utility classes, no
|
|
8
|
+
// CSS import beyond the injected `.vui-*` stylesheet, no runtime dependency
|
|
9
|
+
// beyond React. Themes itself from your shadcn CSS variables (--foreground)
|
|
10
|
+
// with light/dark fallbacks and honors prefers-reduced-motion.
|
|
11
|
+
|
|
12
|
+
import { useEffect, useMemo, type CSSProperties } from "react"
|
|
13
|
+
import { ensureStyles } from "@/components/ui/vibes-ui-styles"
|
|
14
|
+
|
|
15
|
+
// The reel alphabet — every glyph a character can roll *through*. Space sits
|
|
16
|
+
// first so blanks rest at the top of the reel. Any character in `value` that
|
|
17
|
+
// isn't here renders static (no reel), the way <AnimatedNumber> leaves
|
|
18
|
+
// separators and currency symbols still.
|
|
19
|
+
const ALPHABET = Array.from(
|
|
20
|
+
" ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?'-&@",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export type AnimatedTextProps = {
|
|
24
|
+
/** The text to display. Changing it rolls each letter to its new glyph. */
|
|
25
|
+
value: string
|
|
26
|
+
/**
|
|
27
|
+
* Override the glyphs a reel can roll through. Defaults to spaces, letters,
|
|
28
|
+
* digits and common punctuation. A tighter set (e.g. just `"ABC…Z"`) makes
|
|
29
|
+
* the roll shorter and the board feel snappier.
|
|
30
|
+
*/
|
|
31
|
+
alphabet?: string
|
|
32
|
+
className?: string
|
|
33
|
+
style?: CSSProperties
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Rolling-letter text. Each character is a vertical reel of the alphabet that
|
|
38
|
+
* translates to the target glyph; CSS handles the motion, so re-rendering with
|
|
39
|
+
* a new `value` is all it takes to animate. Characters reveal left-to-right so
|
|
40
|
+
* the roll reads like the word is being dealt out.
|
|
41
|
+
*/
|
|
42
|
+
export function AnimatedText({
|
|
43
|
+
value,
|
|
44
|
+
alphabet,
|
|
45
|
+
className = "",
|
|
46
|
+
style,
|
|
47
|
+
}: AnimatedTextProps) {
|
|
48
|
+
useEffect(ensureStyles, [])
|
|
49
|
+
|
|
50
|
+
// Resolve the reel alphabet and a glyph→row lookup once per alphabet.
|
|
51
|
+
const { set, index } = useMemo(() => {
|
|
52
|
+
const chars = alphabet ? Array.from(alphabet) : ALPHABET
|
|
53
|
+
return { set: chars, index: new Map(chars.map((c, i) => [c, i])) }
|
|
54
|
+
}, [alphabet])
|
|
55
|
+
|
|
56
|
+
const chars = Array.from(value)
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<span
|
|
60
|
+
className={`vui vui-text ${className}`.trim()}
|
|
61
|
+
style={style}
|
|
62
|
+
role="text"
|
|
63
|
+
aria-label={value}
|
|
64
|
+
>
|
|
65
|
+
{chars.map((char, i) => {
|
|
66
|
+
const row = index.get(char)
|
|
67
|
+
if (row === undefined) {
|
|
68
|
+
return (
|
|
69
|
+
<span key={i} className="vui-text-sym" aria-hidden="true">
|
|
70
|
+
{char}
|
|
71
|
+
</span>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
// Reveal left-to-right; cap the stagger so long strings still settle.
|
|
75
|
+
return (
|
|
76
|
+
<Reel key={i} row={row} set={set} delay={Math.min(i, 8) * 0.06} />
|
|
77
|
+
)
|
|
78
|
+
})}
|
|
79
|
+
</span>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function Reel({
|
|
84
|
+
row,
|
|
85
|
+
set,
|
|
86
|
+
delay,
|
|
87
|
+
}: {
|
|
88
|
+
row: number
|
|
89
|
+
set: string[]
|
|
90
|
+
delay: number
|
|
91
|
+
}) {
|
|
92
|
+
return (
|
|
93
|
+
<span className="vui-text-reel" aria-hidden="true">
|
|
94
|
+
<span
|
|
95
|
+
className="vui-text-track"
|
|
96
|
+
style={{
|
|
97
|
+
transform: `translateY(${-row}em)`,
|
|
98
|
+
transitionDelay: `${delay}s`,
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{set.map((c, i) => (
|
|
102
|
+
<span key={i} className="vui-text-cell">
|
|
103
|
+
{c === " " ? " " : c}
|
|
104
|
+
</span>
|
|
105
|
+
))}
|
|
106
|
+
</span>
|
|
107
|
+
</span>
|
|
108
|
+
)
|
|
109
|
+
}
|