From b5ac5c3dd50d971a3781d40460145f729ed523e1 Mon Sep 17 00:00:00 2001 From: Vasanthdev2004 Date: Mon, 17 Aug 2026 22:00:09 +0530 Subject: [PATCH 01/22] chore: ignore the impeccable hook session cache hook.cache.json records per-session edit counts keyed by absolute paths on the machine that produced it, so it is local state rather than project config. config.json, which carries the shared detector settings, stays tracked. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index af192a4..e449e7d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ dist-ssr *.sw? .vercel .playwright-cli + +# Impeccable design-hook session cache (machine-local) +.impeccable/hook.cache.json From 9c86a1a3b1acf6a669bfe7aaa31753fe09d5b850 Mon Sep 17 00:00:00 2001 From: Vasanthdev2004 Date: Mon, 17 Aug 2026 22:00:18 +0530 Subject: [PATCH 02/22] chore(deps): add the redesign's UI dependencies and fix module resolution Adds @radix-ui/react-select (the sort menus), tw-animate-css (the enter/exit utilities Radix drives from data-[state]), github-slugger (heading anchors for docs search), motion, gsap, cobe, and the Magic UI / Aceternity runtime deps. tsconfig gains baseUrl and the "@/*" path mapping. Without it the shadcn CLI resolved "@/lib/utils" as a literal directory and wrote vendored components into a folder named "@" at the repo root. eslint.config.js exempts the vendored registry components from react-hooks/purity and react-hooks/set-state-in-effect. Those files are generated and `shadcn add` overwrites them, so the violations cannot be fixed in place. --- bun.lock | 131 ++++- eslint.config.js | 47 ++ package-lock.json | 1261 ++++++++++++++++++++++++++------------------- package.json | 17 +- tsconfig.json | 7 + 5 files changed, 895 insertions(+), 568 deletions(-) diff --git a/bun.lock b/bun.lock index c5d0f62..bdd3ecf 100644 --- a/bun.lock +++ b/bun.lock @@ -6,27 +6,36 @@ "name": "gitlawb-node-explorer", "dependencies": { "@expo-google-fonts/jetbrains-mono": "^0.4.1", - "@radix-ui/react-separator": "^1.1.9", - "@radix-ui/react-slot": "^1.2.5", - "@radix-ui/react-tabs": "^1.1.14", + "@radix-ui/react-accordion": "^1.2.20", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-slot": "^1.3.3", "@shuding/opentype.js": "^1.4.0-beta.0", + "@tailwindcss/postcss": "^4.3.3", + "@types/canvas-confetti": "^1.9.0", "@vercel/og": "^0.11.1", + "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cobe": "^0.6.4", "dompurify": "^3.4.11", "gemoji": "^8.1.0", - "lucide-react": "^1.17.0", + "gsap": "^3.15.0", + "lucide-react": "^1.27.0", "marked": "^18.0.5", "marked-alert": "^2.1.2", "marked-emoji": "^2.0.3", "marked-footnote": "^1.4.0", "marked-gfm-heading-id": "^4.1.4", "marked-highlight": "^2.2.4", + "motion": "^13.1.0", + "next-themes": "^0.4.6", + "postcss": "^8.5.23", "react": "^19.2.6", "react-dom": "^19.2.6", "react-router-dom": "^7.17.0", "shiki": "^4.3.0", "tailwind-merge": "^3.6.0", + "tailwind-variants": "^3.3.0", }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -50,6 +59,8 @@ }, }, "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="], "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="], @@ -204,37 +215,39 @@ "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.4", "", {}, "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ=="], + "@radix-ui/number": ["@radix-ui/number@1.1.3", "", {}, "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="], - "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.9", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-slot": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-zuSVi7ziP7uQRqc+yGxsKJfNkdyHv3ZKDaHe0gzg4dRgws96TPKWIiz84tVHP4GEcEl8bC0mdt17NkcxaJHmaQ=="], + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.20", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collapsible": "1.1.20", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg=="], - "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="], + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.20", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q=="], - "@radix-ui/react-context": ["@radix-ui/react-context@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.15", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA=="], - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA=="], + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="], - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA=="], + "@radix-ui/react-context": ["@radix-ui/react-context@1.2.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA=="], - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.6", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ=="], + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg=="], - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.5", "", { "dependencies": { "@radix-ui/react-slot": "1.2.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA=="], + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA=="], - "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-collection": "1.1.9", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-FvgPt1bRmg8Xt2QpF7NUZW3dE0ZQHGm41dAdgT2J2GJPoIXz+9Em3NobAxf4fupcxhgHu03E5CRiU2MWvObXyg=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.10", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw=="], - "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-gvgW+JV/Mbjj6darztTetnmElpQEzZrXpJvfj+dOxNAxiyHEAyUvEjjl4zxblvmjmKmi3jfPoy7ZdxzCuUBJSA=="], + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.10", "", { "dependencies": { "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="], + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.18", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA=="], - "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.4", "@radix-ui/react-context": "1.1.4", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-presence": "1.1.6", "@radix-ui/react-primitive": "2.1.5", "@radix-ui/react-roving-focus": "1.1.12", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-D5jwp9JNuwDeCw3CYD2Fz+sSHo0droQjC8u75dJHe4aWr5q6yBiXZU+hurXnKudRgEpUkD5TsI6bjHPo5ThUxA=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q=="], - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ=="], - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.3", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.3", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA=="], + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ=="], - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.3", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA=="], + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg=="], - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA=="], + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw=="], "@resvg/resvg-wasm": ["@resvg/resvg-wasm@2.4.0", "", {}, "sha512-C7c51Nn4yTxXFKvgh2txJFNweaVcfUPQxwEUFw4aWsCmfiBDJsTSwviIF8EcwjQ6k8bPyMWCl1vw4BdxE569Cg=="], @@ -318,10 +331,14 @@ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.3", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "postcss": "^8.5.16", "tailwindcss": "4.3.3" } }, "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.0", "", { "dependencies": { "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "tailwindcss": "4.3.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="], + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], @@ -410,6 +427,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001797", "", {}, "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w=="], + "canvas-confetti": ["canvas-confetti@1.9.4", "", {}, "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], @@ -422,6 +441,8 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cobe": ["cobe@0.6.5", "", { "dependencies": { "phenomenon": "^1.6.0" } }, "sha512-MA8bu81EFY6JjQpj+FovEuhyJ25khx2Q7Lh+ot/UkCJe5yKyDgzdc6u2lGZIOmsZTXK6Itg1i4lQZIJZbPWnAg=="], + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], @@ -520,6 +541,8 @@ "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + "framer-motion": ["framer-motion@13.1.0", "", { "dependencies": { "motion-dom": "^13.0.0", "motion-utils": "^13.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-QSZrF0Id3QGuHJ+OL+9PSY9pk86C8ERFalwAGSchzTm65+ZoGH/RM26lmEARLljcHj2lqhv0jZOOks+EI3COOw=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "gemoji": ["gemoji@8.1.0", "", {}, "sha512-HA4Gx59dw2+tn+UAa7XEV4ufUKI4fH1KgcbenVA9YKSj1QJTT0xh5Mwv5HMFNN3l2OtUe3ZIfuRwSyZS5pLIWw=="], @@ -534,6 +557,8 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "gsap": ["gsap@3.15.0", "", {}, "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], @@ -610,7 +635,7 @@ "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], - "lucide-react": ["lucide-react@1.17.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w=="], + "lucide-react": ["lucide-react@1.31.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -642,12 +667,20 @@ "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "motion": ["motion@13.1.0", "", { "dependencies": { "framer-motion": "^13.1.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-qtvscq59uCPdWnNW4SdSkrxR+BS/QYsa923bx7ocA+4p+ZGNbbVQwkSnG4aukB81QWjtl3AxX36plxNyZLmHCA=="], + + "motion-dom": ["motion-dom@13.0.0", "", { "dependencies": { "motion-utils": "^13.0.0" } }, "sha512-Xk+SJas70uMAUIApg+m3lZDShxI3LBFHq7mFGbBKoRXc2PVPDyAKmzN64Bbzt4CZdP/CItTiJxWtn4TA0v53Ng=="], + + "motion-utils": ["motion-utils@13.0.0", "", {}, "sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.12", "", { "bin": "bin/nanoid.cjs" }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + "node-releases": ["node-releases@2.0.47", "", {}, "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og=="], "obug": ["obug@2.1.3", "", {}, "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg=="], @@ -674,6 +707,8 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "phenomenon": ["phenomenon@1.6.0", "", {}, "sha512-7h9/fjPD3qNlgggzm88cY58l9sudZ6Ey+UmZsizfhtawO6E3srZQXywaNm2lBwT72TbpHYRPy7ytIHeBUD/G0A=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], @@ -682,7 +717,7 @@ "playwright-core": ["playwright-core@1.60.0", "", { "bin": "cli.js" }, "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="], - "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], @@ -746,6 +781,8 @@ "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], + "tailwind-variants": ["tailwind-variants@3.3.1", "", { "peerDependencies": { "tailwind-merge": ">=3.0.0", "tailwindcss": "*" }, "optionalPeers": ["tailwind-merge", "tailwindcss"] }, "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw=="], + "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], @@ -842,6 +879,12 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@tailwindcss/postcss/@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], + + "@tailwindcss/postcss/@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], + + "@tailwindcss/postcss/tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.8.2", "", { "bin": "bin/semver.js" }, "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ=="], @@ -849,5 +892,47 @@ "sharp/semver": ["semver@7.8.5", "", { "bin": "bin/semver.js" }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "vite/postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + + "@tailwindcss/postcss/@tailwindcss/node/enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], + + "vite/postcss/nanoid": ["nanoid@3.3.12", "", { "bin": "bin/nanoid.cjs" }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + + "@tailwindcss/postcss/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], } } diff --git a/eslint.config.js b/eslint.config.js index e8f5c94..1d762d6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -26,6 +26,53 @@ export default defineConfig([ 'react-refresh/only-export-components': 'off', }, }, + { + // Vendored registry components, added verbatim via `shadcn add` from the + // Magic UI and Aceternity registries. They are third-party sources, not + // ours to rewrite: they predate the React 19 compiler rules this project + // lints with (Math.random() during render for decorative jitter, + // setState inside effects to drive typing sequences). Scoped off here so + // the rules stay strict for code we actually author. Re-running + // `shadcn add` overwrites these files, so local fixes would not survive. + files: [ + 'src/components/ui/animated-beam.tsx', + 'src/components/ui/icon-cloud.tsx', + 'src/components/ui/globe.tsx', + 'src/components/ui/scroll-progress.tsx', + 'src/components/ui/animated-circular-progress-bar.tsx', + 'src/components/ui/orbiting-circles.tsx', + 'src/components/ui/animated-list.tsx', + 'src/components/ui/confetti.tsx', + 'src/components/ui/ripple-button.tsx', + 'src/components/ui/rainbow-button.tsx', + 'src/components/ui/pulsating-button.tsx', + 'src/components/ui/shiny-button.tsx', + 'src/components/ui/shimmer-button.tsx', + 'src/components/ui/ripple.tsx', + 'src/components/ui/marquee.tsx', + 'src/components/ui/magic-card.tsx', + 'src/components/ui/interactive-hover-button.tsx', + 'src/components/ui/aurora-text.tsx', + 'src/components/ui/animated-gradient-text.tsx', + 'src/components/ui/animated-theme-toggler.tsx', + 'src/components/ui/animated-shiny-text.tsx', + 'src/components/ui/bento-grid.tsx', + 'src/components/ui/blur-fade.tsx', + 'src/components/ui/border-beam.tsx', + 'src/components/ui/button.tsx', + 'src/components/ui/dot-pattern.tsx', + 'src/components/ui/file-tree.tsx', + 'src/components/ui/grid-pattern.tsx', + 'src/components/ui/number-ticker.tsx', + 'src/components/ui/scroll-area.tsx', + 'src/components/ui/terminal.tsx', + 'src/components/ui/tracing-beam.tsx', + ], + rules: { + 'react-hooks/purity': 'off', + 'react-hooks/set-state-in-effect': 'off', + }, + }, { // Vercel Functions run on Node and intentionally export HTTP handlers. files: ['api/**/*.{ts,tsx}'], diff --git a/package-lock.json b/package-lock.json index f74ec6f..79cd903 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,22 @@ "license": "MIT", "dependencies": { "@expo-google-fonts/jetbrains-mono": "^0.4.1", - "@heroui/react": "^3.2.2", - "@heroui/styles": "^3.2.2", + "@radix-ui/react-accordion": "^1.2.20", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-slot": "^1.3.3", "@shuding/opentype.js": "^1.4.0-beta.0", "@tailwindcss/postcss": "^4.3.3", + "@types/canvas-confetti": "^1.9.0", "@vercel/og": "^0.11.1", + "canvas-confetti": "^1.9.4", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cobe": "^0.6.4", "dompurify": "^3.4.11", "gemoji": "^8.1.0", + "github-slugger": "^2.0.0", + "gsap": "^3.15.0", "lucide-react": "^1.27.0", "marked": "^18.0.5", "marked-alert": "^2.1.2", @@ -25,13 +33,16 @@ "marked-footnote": "^1.4.0", "marked-gfm-heading-id": "^4.1.4", "marked-highlight": "^2.2.4", + "motion": "^13.1.0", + "next-themes": "^0.4.6", "postcss": "^8.5.23", "react": "^19.2.6", "react-dom": "^19.2.6", "react-router-dom": "^7.17.0", "shiki": "^4.3.0", "tailwind-merge": "^3.6.0", - "tailwind-variants": "^3.3.0" + "tailwind-variants": "^3.3.0", + "tw-animate-css": "^1.4.0" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -53,50 +64,6 @@ "vitest": "^4.1.9" } }, - "node_modules/@adobe/react-spectrum": { - "version": "3.47.2", - "resolved": "https://registry.npmjs.org/@adobe/react-spectrum/-/react-spectrum-3.47.2.tgz", - "integrity": "sha512-QxsE7bPBGpzwYofACF0RAL/Zs3p0u9HNvCDf9LEN87rEGFpkagIB+T+TSZ0xbl6BJ6z7S02m3zAFk3s9FoHJpQ==", - "license": "Apache-2.0", - "dependencies": { - "@internationalized/date": "^3.12.2", - "@react-types/shared": "^3.36.0", - "@spectrum-icons/ui": "^3.7.1", - "@spectrum-icons/workflow": "^4.3.1", - "@swc/helpers": "^0.5.0", - "client-only": "^0.0.1", - "clsx": "^2.0.0", - "react-aria": "3.50.0", - "react-aria-components": "1.19.0", - "react-stately": "3.48.0", - "react-transition-group": "^4.4.5", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@adobe/react-spectrum-ui": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@adobe/react-spectrum-ui/-/react-spectrum-ui-1.2.1.tgz", - "integrity": "sha512-wcrbEE2O/9WnEn6avBnaVRRx88S5PLFsPLr4wffzlbMfXeQsy+RMQwaJd3cbzrn18/j04Isit7f7Emfn0dhrJA==", - "license": "Apache-2.0", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@adobe/react-spectrum-workflow": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@adobe/react-spectrum-workflow/-/react-spectrum-workflow-2.3.5.tgz", - "integrity": "sha512-b53VIPwPWKb/T5gzE3qs+QlGP5gVrw/LnWV3xMksDU+CRl3rzOKUwxIGiZO8ICyYh1WiyqY4myGlPU/nAynBUg==", - "license": "Apache-2.0", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -352,15 +319,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -745,143 +703,43 @@ "integrity": "sha512-CslACrtMOcRwoWXCO7OMEI+9w3fukWSoBtvNz46OqPoogEuuoY0tkDY1O8sFumk8t0pC6Cx0Xr95O0TOQhpkug==", "license": "MIT AND OFL-1.1" }, - "node_modules/@formatjs/ecma402-abstract": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz", - "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==", - "license": "MIT", - "dependencies": { - "@formatjs/fast-memoize": "2.2.7", - "@formatjs/intl-localematcher": "0.6.2", - "decimal.js": "^10.4.3", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/fast-memoize": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", - "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz", - "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==", + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "@formatjs/icu-skeleton-parser": "1.8.16", - "tslib": "^2.8.0" + "@floating-ui/utils": "^0.2.12" } }, - "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.8.16", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz", - "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==", + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "tslib": "^2.8.0" - } - }, - "node_modules/@formatjs/intl-localematcher": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz", - "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@heroui/react": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@heroui/react/-/react-3.2.2.tgz", - "integrity": "sha512-TN8ccfHCRIxa9dGhWMaVSl5sGCKWH2E13z2NJ5QBZTFQCow8GogQzW3cn/dRz3YEdYt4B9zfH2N0oKEazVoKRA==", - "license": "MIT", - "dependencies": { - "@heroui/styles": "3.2.2", - "@radix-ui/react-avatar": "1.1.11", - "@react-aria/i18n": "3.13.1", - "@react-aria/ssr": "3.10.1", - "@react-aria/utils": "3.34.1", - "@react-stately/utils": "3.12.1", - "@react-types/color": "3.2.0", - "@react-types/shared": "3.36.0", - "input-otp": "1.4.2", - "react-aria": "^3.50.0", - "react-aria-components": "^1.19.0", - "tailwind-merge": "3.4.0", - "tailwind-variants": "3.2.2" - }, - "peerDependencies": { - "react": ">=19.0.0", - "react-dom": ">=19.0.0", - "tailwindcss": ">=4.0.0" + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, - "node_modules/@heroui/react/node_modules/tailwind-merge": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", - "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/@heroui/react/node_modules/tailwind-variants": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", - "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", - "license": "MIT", - "engines": { - "node": ">=16.x", - "pnpm": ">=7.x" - }, - "peerDependencies": { - "tailwind-merge": ">=3.0.0", - "tailwindcss": "*" - }, - "peerDependenciesMeta": { - "tailwind-merge": { - "optional": true - } - } - }, - "node_modules/@heroui/styles": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@heroui/styles/-/styles-3.2.2.tgz", - "integrity": "sha512-aDvmcF1bEtWMTg6xbbfv62PFhm2ldIoYIt1uFKyTXEwWH1VWbr+er4LvZojtddTbqA9t58rbE5y32xM8ei9gHg==", + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", "license": "MIT", "dependencies": { - "tailwind-variants": "3.2.2", - "tw-animate-css": "1.4.0" + "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { - "tailwindcss": ">=4.0.0" + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@heroui/styles/node_modules/tailwind-variants": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", - "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", - "license": "MIT", - "engines": { - "node": ">=16.x", - "pnpm": ">=7.x" - }, - "peerDependencies": { - "tailwind-merge": ">=3.0.0", - "tailwindcss": "*" - }, - "peerDependenciesMeta": { - "tailwind-merge": { - "optional": true - } - } + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" }, "node_modules/@humanfs/core": { "version": "0.19.2", @@ -1463,43 +1321,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@internationalized/date": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.2.tgz", - "integrity": "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, - "node_modules/@internationalized/message": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.10.tgz", - "integrity": "sha512-nc0Or6EdWHqZRcsXb6P9hBIpLsfSl/ILh0rk5h/OVBpzmhdExXtPy2cQtWsq8XKRBpRHwDNnAHt4OpolcB7dog==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0", - "intl-messageformat": "^10.1.0" - } - }, - "node_modules/@internationalized/number": { - "version": "3.6.7", - "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.7.tgz", - "integrity": "sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, - "node_modules/@internationalized/string": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.9.tgz", - "integrity": "sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1573,17 +1394,33 @@ "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", - "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==", + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.20.tgz", + "integrity": "sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==", "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.3", - "@radix-ui/react-primitive": "2.1.4", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collapsible": "1.1.20", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -1600,43 +1437,69 @@ } } }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", - "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.20.tgz", + "integrity": "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==", "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", - "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "node_modules/@radix-ui/react-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.4" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", @@ -1653,14 +1516,26 @@ } } }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -1671,10 +1546,10 @@ } } }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "node_modules/@radix-ui/react-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1686,10 +1561,37 @@ } } }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -1701,13 +1603,38 @@ } } }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", "license": "MIT", "dependencies": { - "use-sync-external-store": "^1.5.0" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -1719,151 +1646,348 @@ } } }, - "node_modules/@react-aria/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@react-aria/color/-/color-3.2.1.tgz", - "integrity": "sha512-v/pZh1yBNeayQ5Kio0xRIX0gBf6uu+KjCRW4QSydTyRpmhvk6sMzHT9i9/vIMdaDHaYHBXlXF+Tp/mvS9CG4Zw==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0", - "react-aria": "^3.48.0" + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-aria/i18n": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.13.1.tgz", - "integrity": "sha512-z56ZYcbfpNmMyiGLhyEjytpmEfoTlBaksk84q4kds3HvNkf7QWKj+DJVfVDrJX+c1LyuBsszLSX7yxJRiHsYKQ==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", "dependencies": { - "@internationalized/date": "^3.12.1", - "@internationalized/message": "^3.1.9", - "@internationalized/string": "^3.2.8", - "@swc/helpers": "^0.5.0", - "react-aria": "^3.48.0" + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-aria/ssr": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.10.1.tgz", - "integrity": "sha512-jn038/ZYmu6DpfXJ6r2U9zFFppjbc9wnApPJSCxao2RZVEqep4YyoniHSy8qv6V21/xyS4IV7W9a+X2jOjSuag==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0", - "react-aria": "^3.48.0" + "@radix-ui/react-use-layout-effect": "1.1.4" }, - "engines": { - "node": ">= 12" + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-aria/utils": { - "version": "3.34.1", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.34.1.tgz", - "integrity": "sha512-H6+rGZL+0f58bBNaUMfctEnT+NogqwAk+nHiB8sR3K+YlQ37GTuCijy2U/pPvQtFMS5mURrjZeBH5JNNXsx14A==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.18.tgz", + "integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0", - "react-aria": "^3.48.0", - "react-stately": "^3.46.0" + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-spectrum/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@react-spectrum/color/-/color-3.2.1.tgz", - "integrity": "sha512-FbB+GaVufsN+obNJmUAe8XGZTeJjEWvEf/k/7nxDxoDCBdg/o6eEPONAZyID5Dj1f8BZKNym8VURxDZx7IIyuQ==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-select": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", + "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", "dependencies": { - "@adobe/react-spectrum": "^3.47.0", - "@swc/helpers": "^0.5.0", - "react-stately": "^3.46.0" + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-spectrum/provider": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/@react-spectrum/provider/-/provider-3.11.1.tgz", - "integrity": "sha512-TsoNdVdmlQ7L+75ILq5Yb3+wp/I1AtIeat0o+Y+ZBxP+TtWpwT1ZtCB5l3cplFVzHzOpZlzO0VaDrDP9ElGYDw==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@adobe/react-spectrum": "^3.47.0", - "@swc/helpers": "^0.5.0" - }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-stately/color": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@react-stately/color/-/color-3.10.1.tgz", - "integrity": "sha512-v4YlLa/oRQpPa/M9Yq1e0Bl8Z7xzGZdNDAIbfIMZuiJtacaOKriPW+AOgjjT/Ab0fNX2YDbwIYvkiMOJBnTi5A==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0", - "react-stately": "^3.46.0" + "@radix-ui/rect": "1.1.3" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-stately/utils": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.12.1.tgz", - "integrity": "sha512-NqKfzrknpfwiewx7R2vk1P+CneClInPDsIhw15+jOcUYSEfej0nta4cJywuKQJ2gsPwqX/ojDNixedCve9FWGw==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0", - "react-stately": "^3.46.0" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@react-types/color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@react-types/color/-/color-3.2.0.tgz", - "integrity": "sha512-beV3vz80nzZ1EuYUM7296Kyi3AHcMrbQw0qub/9yzHWVTKKc5sy/e4dCMKcWL/ArkeAyc7jDOiui190RQ4l0Fw==", - "license": "Apache-2.0", + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", + "license": "MIT", "dependencies": { - "@react-aria/color": "^3.2.0", - "@react-spectrum/color": "^3.2.0", - "@react-stately/color": "^3.10.0" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { - "@react-spectrum/provider": "^3.0.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@react-types/shared": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.36.0.tgz", - "integrity": "sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ==", - "license": "Apache-2.0", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } + "node_modules/@radix-ui/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" }, "node_modules/@resvg/resvg-wasm": { "version": "2.4.0", @@ -2254,37 +2378,6 @@ "node": ">= 8.0.0" } }, - "node_modules/@spectrum-icons/ui": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.1.tgz", - "integrity": "sha512-veQymocUYo5OciXQajSailOdbWe+k6+2ehfF8D4d0V923D4xOUadtT253xXZ5vEQjPat6Kyp2WDKeQNjd7kL1w==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-ui": "1.2.1", - "@babel/runtime": "^7.24.4", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@spectrum-icons/workflow": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.1.tgz", - "integrity": "sha512-kDF+/EbFVyLGytotqqdYt4uSij4j/PQmDQO5km/C6DyzKjyuic3FnSBFinR+mA6oFv1OjMcLvrrDBqK3wbqRlA==", - "license": "Apache-2.0", - "dependencies": { - "@adobe/react-spectrum-workflow": "2.3.5", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "@adobe/react-spectrum": "^3.47.0", - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2292,15 +2385,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@swc/helpers": { - "version": "0.5.23", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", - "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/@tailwindcss/node": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", @@ -2857,6 +2941,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", + "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -3542,6 +3632,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -3582,11 +3682,17 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } }, "node_modules/clsx": { "version": "2.1.1", @@ -3597,6 +3703,15 @@ "node": ">=6" } }, + "node_modules/cobe": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cobe/-/cobe-0.6.5.tgz", + "integrity": "sha512-MA8bu81EFY6JjQpj+FovEuhyJ25khx2Q7Lh+ot/UkCJe5yKyDgzdc6u2lGZIOmsZTXK6Itg1i4lQZIJZbPWnAg==", + "license": "MIT", + "dependencies": { + "phenomenon": "^1.6.0" + } + }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", @@ -3707,6 +3822,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, "license": "MIT" }, "node_modules/data-urls": { @@ -3745,6 +3861,7 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, "license": "MIT" }, "node_modules/deep-is": { @@ -3772,6 +3889,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -3785,16 +3908,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/dompurify": { "version": "3.4.11", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", @@ -4180,6 +4293,29 @@ "dev": true, "license": "ISC" }, + "node_modules/framer-motion": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-13.1.0.tgz", + "integrity": "sha512-QSZrF0Id3QGuHJ+OL+9PSY9pk86C8ERFalwAGSchzTm65+ZoGH/RM26lmEARLljcHj2lqhv0jZOOks+EI3COOw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^13.0.0", + "motion-utils": "^13.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4215,6 +4351,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -4253,6 +4398,12 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/gsap": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/hast-util-to-html": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", @@ -4361,28 +4512,6 @@ "node": ">=0.8.19" } }, - "node_modules/input-otp": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", - "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/intl-messageformat": { - "version": "10.7.18", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz", - "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==", - "license": "BSD-3-Clause", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.6", - "@formatjs/fast-memoize": "2.2.7", - "@formatjs/icu-messageformat-parser": "2.11.4", - "tslib": "^2.8.0" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4433,6 +4562,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/jsdom": { @@ -4832,18 +4962,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5065,6 +5183,43 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/motion": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-13.1.0.tgz", + "integrity": "sha512-qtvscq59uCPdWnNW4SdSkrxR+BS/QYsa923bx7ocA+4p+ZGNbbVQwkSnG4aukB81QWjtl3AxX36plxNyZLmHCA==", + "license": "MIT", + "dependencies": { + "framer-motion": "^13.1.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-13.0.0.tgz", + "integrity": "sha512-Xk+SJas70uMAUIApg+m3lZDShxI3LBFHq7mFGbBKoRXc2PVPDyAKmzN64Bbzt4CZdP/CItTiJxWtn4TA0v53Ng==", + "license": "MIT", + "dependencies": { + "motion-utils": "^13.0.0" + } + }, + "node_modules/motion-utils": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-13.0.0.tgz", + "integrity": "sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5097,6 +5252,16 @@ "dev": true, "license": "MIT" }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-releases": { "version": "2.0.47", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", @@ -5107,15 +5272,6 @@ "node": ">=18" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/obug": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", @@ -5253,6 +5409,12 @@ "dev": true, "license": "MIT" }, + "node_modules/phenomenon": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/phenomenon/-/phenomenon-1.6.0.tgz", + "integrity": "sha512-7h9/fjPD3qNlgggzm88cY58l9sudZ6Ey+UmZsizfhtawO6E3srZQXywaNm2lBwT72TbpHYRPy7ytIHeBUD/G0A==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5363,17 +5525,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/property-information": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", @@ -5403,63 +5554,65 @@ "node": ">=0.10.0" } }, - "node_modules/react-aria": { - "version": "3.50.0", - "resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.50.0.tgz", - "integrity": "sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw==", - "license": "Apache-2.0", + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", "dependencies": { - "@internationalized/date": "^3.12.2", - "@internationalized/number": "^3.6.7", - "@internationalized/string": "^3.2.9", - "@react-types/shared": "^3.36.0", - "@swc/helpers": "^0.5.0", - "aria-hidden": "^1.2.3", - "clsx": "^2.0.0", - "react-stately": "3.48.0", - "use-sync-external-store": "^1.6.0" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "react": "^19.2.7" } }, - "node_modules/react-aria-components": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/react-aria-components/-/react-aria-components-1.19.0.tgz", - "integrity": "sha512-2smSS5nqJ8cGYMQezuUXveZm7eMyHCqTN6mDpylQBYLYbdF5dxCCuW1DHn1VKLe1DybSfPvX/cZtJlDmvFfn8A==", - "license": "Apache-2.0", + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", "dependencies": { - "@internationalized/date": "^3.12.2", - "@react-types/shared": "^3.36.0", - "@swc/helpers": "^0.5.0", - "client-only": "^0.0.1", - "react-aria": "3.50.0", - "react-stately": "3.48.0" + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" }, "peerDependencies": { - "react": "^19.2.7" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/react-router": { "version": "7.17.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", @@ -5498,37 +5651,26 @@ "react-dom": ">=18" } }, - "node_modules/react-stately": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.48.0.tgz", - "integrity": "sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA==", - "license": "Apache-2.0", + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", "dependencies": { - "@internationalized/date": "^3.12.2", - "@internationalized/number": "^3.6.7", - "@internationalized/string": "^3.2.9", - "@react-types/shared": "^3.36.0", - "@swc/helpers": "^0.5.0", - "use-sync-external-store": "^1.6.0" + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" + "engines": { + "node": ">=10" }, "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/regex": { @@ -5859,6 +6001,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "devOptional": true, "license": "MIT" }, "node_modules/tapable": { @@ -6195,13 +6338,47 @@ "punycode": "^2.1.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/vfile": { diff --git a/package.json b/package.json index c6e4e34..6b9b9ac 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,22 @@ }, "dependencies": { "@expo-google-fonts/jetbrains-mono": "^0.4.1", - "@heroui/react": "^3.2.2", - "@heroui/styles": "^3.2.2", + "@radix-ui/react-accordion": "^1.2.20", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-slot": "^1.3.3", "@shuding/opentype.js": "^1.4.0-beta.0", "@tailwindcss/postcss": "^4.3.3", + "@types/canvas-confetti": "^1.9.0", "@vercel/og": "^0.11.1", + "canvas-confetti": "^1.9.4", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cobe": "^0.6.4", "dompurify": "^3.4.11", "gemoji": "^8.1.0", + "github-slugger": "^2.0.0", + "gsap": "^3.15.0", "lucide-react": "^1.27.0", "marked": "^18.0.5", "marked-alert": "^2.1.2", @@ -34,13 +42,16 @@ "marked-footnote": "^1.4.0", "marked-gfm-heading-id": "^4.1.4", "marked-highlight": "^2.2.4", + "motion": "^13.1.0", + "next-themes": "^0.4.6", "postcss": "^8.5.23", "react": "^19.2.6", "react-dom": "^19.2.6", "react-router-dom": "^7.17.0", "shiki": "^4.3.0", "tailwind-merge": "^3.6.0", - "tailwind-variants": "^3.3.0" + "tailwind-variants": "^3.3.0", + "tw-animate-css": "^1.4.0" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/tsconfig.json b/tsconfig.json index a839ab9..29041c1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,13 @@ "extends": "./tsconfig.api.json", "files": [], "include": [], + // Mirrors tsconfig.app.json's paths so the shadcn CLI can resolve the "@/" + // alias when adding registry components; without it the CLI writes to a + // literal "@" directory. + "compilerOptions": { + "baseUrl": ".", + "paths": { "@/*": ["./src/*"] } + }, "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.api.json" }, From ec01cae6f21046d97977420b8c0a4d85ccaa5f74 Mon Sep 17 00:00:00 2001 From: Vasanthdev2004 Date: Mon, 17 Aug 2026 22:00:31 +0530 Subject: [PATCH 03/22] feat(ui): vendor the Magic UI and Aceternity components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installed through the shadcn CLI from the Magic UI and Aceternity registries. They are checked in rather than depended on, which is how those registries are meant to be used: the source is the deliverable. Two fixes applied on top of what the registry shipped, both silent failures: - The pulse keyframes declared `boxShadow:` — a JS property name in a CSS block. Browsers dropped the declaration, so PulsatingButton's glow did nothing. Four declarations corrected to `box-shadow`. - ScrollProgress hardcoded a purple/pink/orange gradient. This palette is monochrome and inverts with the theme, so the bar now paints the foreground. --- src/components/ui/animated-beam.tsx | 187 +++++++ .../ui/animated-circular-progress-bar.tsx | 108 ++++ src/components/ui/animated-gradient-text.tsx | 37 ++ src/components/ui/animated-list.tsx | 77 +++ src/components/ui/animated-shiny-text.tsx | 42 ++ src/components/ui/animated-theme-toggler.tsx | 328 ++++++++++++ src/components/ui/aurora-text.tsx | 41 ++ src/components/ui/bento-grid.tsx | 54 ++ src/components/ui/blur-fade.tsx | 92 ++++ src/components/ui/border-beam.tsx | 105 ++++ src/components/ui/button.tsx | 56 ++ src/components/ui/confetti.tsx | 146 +++++ src/components/ui/dot-pattern.tsx | 156 ++++++ src/components/ui/file-tree.tsx | 501 ++++++++++++++++++ src/components/ui/globe.tsx | 126 +++++ src/components/ui/grid-pattern.tsx | 70 +++ src/components/ui/icon-cloud.tsx | 375 +++++++++++++ .../ui/interactive-hover-button.tsx | 30 ++ src/components/ui/magic-card.tsx | 220 ++++++++ src/components/ui/marquee.tsx | 74 +++ src/components/ui/number-ticker.tsx | 72 +++ src/components/ui/orbiting-circles.tsx | 71 +++ src/components/ui/pulsating-button.tsx | 125 +++++ src/components/ui/rainbow-button.tsx | 61 +++ src/components/ui/ripple-button.tsx | 99 ++++ src/components/ui/ripple.tsx | 58 ++ src/components/ui/scroll-area.tsx | 46 ++ src/components/ui/scroll-progress.tsx | 34 ++ src/components/ui/shimmer-button.tsx | 96 ++++ src/components/ui/shiny-button.tsx | 72 +++ src/components/ui/terminal.tsx | 298 +++++++++++ src/components/ui/tracing-beam.tsx | 125 +++++ 32 files changed, 3982 insertions(+) create mode 100644 src/components/ui/animated-beam.tsx create mode 100644 src/components/ui/animated-circular-progress-bar.tsx create mode 100644 src/components/ui/animated-gradient-text.tsx create mode 100644 src/components/ui/animated-list.tsx create mode 100644 src/components/ui/animated-shiny-text.tsx create mode 100644 src/components/ui/animated-theme-toggler.tsx create mode 100644 src/components/ui/aurora-text.tsx create mode 100644 src/components/ui/bento-grid.tsx create mode 100644 src/components/ui/blur-fade.tsx create mode 100644 src/components/ui/border-beam.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/confetti.tsx create mode 100644 src/components/ui/dot-pattern.tsx create mode 100644 src/components/ui/file-tree.tsx create mode 100644 src/components/ui/globe.tsx create mode 100644 src/components/ui/grid-pattern.tsx create mode 100644 src/components/ui/icon-cloud.tsx create mode 100644 src/components/ui/interactive-hover-button.tsx create mode 100644 src/components/ui/magic-card.tsx create mode 100644 src/components/ui/marquee.tsx create mode 100644 src/components/ui/number-ticker.tsx create mode 100644 src/components/ui/orbiting-circles.tsx create mode 100644 src/components/ui/pulsating-button.tsx create mode 100644 src/components/ui/rainbow-button.tsx create mode 100644 src/components/ui/ripple-button.tsx create mode 100644 src/components/ui/ripple.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/scroll-progress.tsx create mode 100644 src/components/ui/shimmer-button.tsx create mode 100644 src/components/ui/shiny-button.tsx create mode 100644 src/components/ui/terminal.tsx create mode 100644 src/components/ui/tracing-beam.tsx diff --git a/src/components/ui/animated-beam.tsx b/src/components/ui/animated-beam.tsx new file mode 100644 index 0000000..4af8d97 --- /dev/null +++ b/src/components/ui/animated-beam.tsx @@ -0,0 +1,187 @@ +import { useEffect, useId, useState, type RefObject } from "react" +import { motion } from "motion/react" + +import { cn } from "@/lib/utils" + +export interface AnimatedBeamProps { + className?: string + containerRef: RefObject // Container ref + fromRef: RefObject + toRef: RefObject + curvature?: number + reverse?: boolean + pathColor?: string + pathWidth?: number + pathOpacity?: number + gradientStartColor?: string + gradientStopColor?: string + delay?: number + duration?: number + repeat?: number + repeatDelay?: number + startXOffset?: number + startYOffset?: number + endXOffset?: number + endYOffset?: number +} + +export const AnimatedBeam: React.FC = ({ + className, + containerRef, + fromRef, + toRef, + curvature = 0, + reverse = false, // Include the reverse prop + duration = 5, + delay = 0, + pathColor = "gray", + pathWidth = 2, + pathOpacity = 0.2, + gradientStartColor = "#ffaa40", + gradientStopColor = "#9c40ff", + repeat = Infinity, + repeatDelay = 0, + startXOffset = 0, + startYOffset = 0, + endXOffset = 0, + endYOffset = 0, +}) => { + const id = useId() + const [pathD, setPathD] = useState("") + const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 }) + + // Calculate the gradient coordinates based on the reverse prop + const gradientCoordinates = reverse + ? { + x1: ["90%", "-10%"], + x2: ["100%", "0%"], + y1: ["0%", "0%"], + y2: ["0%", "0%"], + } + : { + x1: ["10%", "110%"], + x2: ["0%", "100%"], + y1: ["0%", "0%"], + y2: ["0%", "0%"], + } + + useEffect(() => { + const updatePath = () => { + if (containerRef.current && fromRef.current && toRef.current) { + const containerRect = containerRef.current.getBoundingClientRect() + const rectA = fromRef.current.getBoundingClientRect() + const rectB = toRef.current.getBoundingClientRect() + + const svgWidth = containerRect.width + const svgHeight = containerRect.height + setSvgDimensions({ width: svgWidth, height: svgHeight }) + + const startX = + rectA.left - containerRect.left + rectA.width / 2 + startXOffset + const startY = + rectA.top - containerRect.top + rectA.height / 2 + startYOffset + const endX = + rectB.left - containerRect.left + rectB.width / 2 + endXOffset + const endY = + rectB.top - containerRect.top + rectB.height / 2 + endYOffset + + const controlY = startY - curvature + const d = `M ${startX},${startY} Q ${ + (startX + endX) / 2 + },${controlY} ${endX},${endY}` + setPathD(d) + } + } + + // Initialize ResizeObserver + const resizeObserver = new ResizeObserver(() => { + updatePath() + }) + + // Observe the container element + if (containerRef.current) { + resizeObserver.observe(containerRef.current) + } + + // Call the updatePath initially to set the initial path + updatePath() + + // Clean up the observer on component unmount + return () => { + resizeObserver.disconnect() + } + }, [ + containerRef, + fromRef, + toRef, + curvature, + startXOffset, + startYOffset, + endXOffset, + endYOffset, + ]) + + return ( + + + + + + + + + + + + + ) +} diff --git a/src/components/ui/animated-circular-progress-bar.tsx b/src/components/ui/animated-circular-progress-bar.tsx new file mode 100644 index 0000000..cb2eef0 --- /dev/null +++ b/src/components/ui/animated-circular-progress-bar.tsx @@ -0,0 +1,108 @@ +import { cn } from "@/lib/utils" + +interface AnimatedCircularProgressBarProps { + max?: number + min?: number + value: number + gaugePrimaryColor: string + gaugeSecondaryColor: string + className?: string +} + +export function AnimatedCircularProgressBar({ + max = 100, + min = 0, + value = 0, + gaugePrimaryColor, + gaugeSecondaryColor, + className, +}: AnimatedCircularProgressBarProps) { + const circumference = 2 * Math.PI * 45 + const percentPx = circumference / 100 + const currentPercent = Math.round(((value - min) / (max - min)) * 100) + + return ( +
+ + {currentPercent <= 90 && currentPercent >= 0 && ( + + )} + + + + {currentPercent} + +
+ ) +} diff --git a/src/components/ui/animated-gradient-text.tsx b/src/components/ui/animated-gradient-text.tsx new file mode 100644 index 0000000..39c6690 --- /dev/null +++ b/src/components/ui/animated-gradient-text.tsx @@ -0,0 +1,37 @@ +import { type ComponentPropsWithoutRef } from "react" + +import { cn } from "@/lib/utils" + +export interface AnimatedGradientTextProps extends ComponentPropsWithoutRef<"div"> { + speed?: number + colorFrom?: string + colorTo?: string +} + +export function AnimatedGradientText({ + children, + className, + speed = 1, + colorFrom = "#ffaa40", + colorTo = "#9c40ff", + ...props +}: AnimatedGradientTextProps) { + return ( + + {children} + + ) +} diff --git a/src/components/ui/animated-list.tsx b/src/components/ui/animated-list.tsx new file mode 100644 index 0000000..f2887d9 --- /dev/null +++ b/src/components/ui/animated-list.tsx @@ -0,0 +1,77 @@ +import React, { + useEffect, + useMemo, + useState, + type ComponentPropsWithoutRef, +} from "react" +import { AnimatePresence, motion, type MotionProps } from "motion/react" + +import { cn } from "@/lib/utils" + +export function AnimatedListItem({ children }: { children: React.ReactNode }) { + const animations: MotionProps = { + initial: { scale: 0, opacity: 0 }, + animate: { scale: 1, opacity: 1, originY: 0 }, + exit: { scale: 0, opacity: 0 }, + transition: { type: "spring", stiffness: 350, damping: 40 }, + } + + return ( + + {children} + + ) +} + +export interface AnimatedListProps extends ComponentPropsWithoutRef<"div"> { + children: React.ReactNode + delay?: number +} + +export const AnimatedList = React.memo( + ({ children, className, delay = 1000, ...props }: AnimatedListProps) => { + const [index, setIndex] = useState(0) + const childrenArray = useMemo( + () => React.Children.toArray(children), + [children] + ) + + useEffect(() => { + let timeout: ReturnType | null = null + + if (index < childrenArray.length - 1) { + timeout = setTimeout(() => { + setIndex((prevIndex) => (prevIndex + 1) % childrenArray.length) + }, delay) + } + + return () => { + if (timeout !== null) { + clearTimeout(timeout) + } + } + }, [index, delay, childrenArray.length]) + + const itemsToShow = useMemo(() => { + const result = childrenArray.slice(0, index + 1).reverse() + return result + }, [index, childrenArray]) + + return ( +
+ + {itemsToShow.map((item) => ( + + {item} + + ))} + +
+ ) + } +) + +AnimatedList.displayName = "AnimatedList" diff --git a/src/components/ui/animated-shiny-text.tsx b/src/components/ui/animated-shiny-text.tsx new file mode 100644 index 0000000..02b24c1 --- /dev/null +++ b/src/components/ui/animated-shiny-text.tsx @@ -0,0 +1,42 @@ +import { + type ComponentPropsWithoutRef, + type CSSProperties, + type FC, +} from "react" + +import { cn } from "@/lib/utils" + +export interface AnimatedShinyTextProps extends ComponentPropsWithoutRef<"span"> { + shimmerWidth?: number +} + +export const AnimatedShinyText: FC = ({ + children, + className, + shimmerWidth = 100, + ...props +}) => { + return ( + + {children} + + ) +} diff --git a/src/components/ui/animated-theme-toggler.tsx b/src/components/ui/animated-theme-toggler.tsx new file mode 100644 index 0000000..89f4154 --- /dev/null +++ b/src/components/ui/animated-theme-toggler.tsx @@ -0,0 +1,328 @@ +import { useCallback, useEffect, useRef, useState } from "react" +import { Moon, Sun } from "lucide-react" +import { flushSync } from "react-dom" + +import { cn } from "@/lib/utils" + +export type TransitionVariant = + | "circle" + | "square" + | "triangle" + | "diamond" + | "hexagon" + | "rectangle" + | "star" + +interface AnimatedThemeTogglerProps extends React.ComponentPropsWithoutRef<"button"> { + duration?: number + variant?: TransitionVariant + /** When true, the transition expands from the viewport center instead of the button center. */ + fromCenter?: boolean + /** + * Controlled theme value. When provided, the parent owns persistence + * (e.g. `next-themes`) and this component will not write to localStorage. + */ + theme?: "light" | "dark" + /** Called on toggle. Pair with `theme` for controlled usage. */ + onThemeChange?: (theme: "light" | "dark") => void +} + +function polygonCollapsed(point: string, vertexCount: number): string { + const pairs = Array.from({ length: vertexCount }, () => point).join(", ") + return `polygon(${pairs})` +} + +// All coordinates are percentages of the snapshot reference box: Chrome 150 +// renders absolute px clip-path coordinates on ::view-transition-new(root) +// unscaled on fractional display scales (e.g. Windows 150%) for the first +// transition after load, so px values land at the wrong position (#989). +function getThemeTransitionClipPaths( + variant: TransitionVariant, + cx: number, + cy: number, + maxRadius: number, + viewportWidth: number, + viewportHeight: number +): [string, string] { + const toX = (x: number) => `${(x / viewportWidth) * 100}%` + const toY = (y: number) => `${(y / viewportHeight) * 100}%` + const point = (x: number, y: number) => `${toX(x)} ${toY(y)}` + // circle() percentage radii resolve against hypot(w, h) / sqrt(2) of the reference box. + const toRadius = (r: number) => + `${(r / (Math.hypot(viewportWidth, viewportHeight) / Math.SQRT2)) * 100}%` + + switch (variant) { + case "circle": + return [ + `circle(0% at ${point(cx, cy)})`, + `circle(${toRadius(maxRadius)} at ${point(cx, cy)})`, + ] + case "square": { + const halfW = Math.max(cx, viewportWidth - cx) + const halfH = Math.max(cy, viewportHeight - cy) + const halfSide = Math.max(halfW, halfH) * 1.05 + const end = [ + point(cx - halfSide, cy - halfSide), + point(cx + halfSide, cy - halfSide), + point(cx + halfSide, cy + halfSide), + point(cx - halfSide, cy + halfSide), + ].join(", ") + return [polygonCollapsed(point(cx, cy), 4), `polygon(${end})`] + } + case "triangle": { + const scale = maxRadius * 2.2 + const dx = (Math.sqrt(3) / 2) * scale + const verts = [ + point(cx, cy - scale), + point(cx + dx, cy + 0.5 * scale), + point(cx - dx, cy + 0.5 * scale), + ].join(", ") + return [polygonCollapsed(point(cx, cy), 3), `polygon(${verts})`] + } + case "diamond": { + // Slightly larger than the view-transition circle radius so axis-aligned coverage matches the circle reveal. + const R = maxRadius * Math.SQRT2 + const end = [ + point(cx, cy - R), + point(cx + R, cy), + point(cx, cy + R), + point(cx - R, cy), + ].join(", ") + return [polygonCollapsed(point(cx, cy), 4), `polygon(${end})`] + } + case "hexagon": { + const R = maxRadius * Math.SQRT2 + const verts: string[] = [] + for (let i = 0; i < 6; i++) { + const a = -Math.PI / 2 + (i * Math.PI) / 3 + verts.push(point(cx + R * Math.cos(a), cy + R * Math.sin(a))) + } + return [ + polygonCollapsed(point(cx, cy), 6), + `polygon(${verts.join(", ")})`, + ] + } + case "rectangle": { + const halfW = Math.max(cx, viewportWidth - cx) + const halfH = Math.max(cy, viewportHeight - cy) + const end = [ + point(cx - halfW, cy - halfH), + point(cx + halfW, cy - halfH), + point(cx + halfW, cy + halfH), + point(cx - halfW, cy + halfH), + ].join(", ") + return [polygonCollapsed(point(cx, cy), 4), `polygon(${end})`] + } + case "star": { + // Small overscan so the last frames never leave a 1px seam before the transition group ends. + const R = maxRadius * Math.SQRT2 * 1.03 + const innerRatio = 0.42 + const starPolygon = (radius: number) => { + const verts: string[] = [] + for (let i = 0; i < 5; i++) { + const outerA = -Math.PI / 2 + (i * 2 * Math.PI) / 5 + verts.push( + point( + cx + radius * Math.cos(outerA), + cy + radius * Math.sin(outerA) + ) + ) + const innerA = outerA + Math.PI / 5 + verts.push( + point( + cx + radius * innerRatio * Math.cos(innerA), + cy + radius * innerRatio * Math.sin(innerA) + ) + ) + } + return `polygon(${verts.join(", ")})` + } + const startR = Math.max(2, R * 0.025) + return [starPolygon(startR), starPolygon(R)] + } + default: + return [ + `circle(0% at ${point(cx, cy)})`, + `circle(${toRadius(maxRadius)} at ${point(cx, cy)})`, + ] + } +} + +export const AnimatedThemeToggler = ({ + className, + duration = 400, + variant, + fromCenter = false, + theme, + onThemeChange, + ...props +}: AnimatedThemeTogglerProps) => { + const shape = variant ?? "circle" + const isControlled = theme !== undefined + const [internalIsDark, setInternalIsDark] = useState(false) + const isDark = isControlled ? theme === "dark" : internalIsDark + const buttonRef = useRef(null) + const isTransitioningRef = useRef(false) + const activeAnimRef = useRef(null) + + const cancelAnim = useCallback(() => { + activeAnimRef.current?.cancel() + activeAnimRef.current = null + }, []) + + useEffect(() => { + return () => { + cancelAnim() + const root = document.documentElement + if (root.dataset.magicuiThemeVt !== "active") return + delete root.dataset.magicuiThemeVt + root.style.removeProperty("--magicui-theme-toggle-vt-duration") + root.style.removeProperty("--magicui-theme-vt-clip-from") + } + }, [cancelAnim]) + + useEffect(() => { + if (isControlled) return + + const updateTheme = () => { + setInternalIsDark(document.documentElement.classList.contains("dark")) + } + + updateTheme() + + const observer = new MutationObserver(updateTheme) + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }) + + return () => observer.disconnect() + }, [isControlled]) + + const toggleTheme = useCallback(() => { + const button = buttonRef.current + if ( + !button || + isTransitioningRef.current || + document.documentElement.dataset.magicuiThemeVt === "active" + ) + return + + // innerWidth/innerHeight (not visualViewport): percentages must resolve + // against the snapshot reference box, which includes classic scrollbars. + const viewportWidth = window.innerWidth + const viewportHeight = window.innerHeight + + let x: number + let y: number + if (fromCenter) { + x = viewportWidth / 2 + y = viewportHeight / 2 + } else { + const { top, left, width, height } = button.getBoundingClientRect() + x = left + width / 2 + y = top + height / 2 + } + + const maxRadius = Math.hypot( + Math.max(x, viewportWidth - x), + Math.max(y, viewportHeight - y) + ) + + const applyTheme = () => { + const newTheme = !isDark + // Always toggle the class synchronously so the View Transitions API + // snapshots the new theme inside the startViewTransition callback. + document.documentElement.classList.toggle("dark") + if (isControlled) { + onThemeChange?.(newTheme ? "dark" : "light") + } else { + setInternalIsDark(newTheme) + localStorage.setItem("theme", newTheme ? "dark" : "light") + } + } + + if (typeof document.startViewTransition !== "function") { + applyTheme() + return + } + + const clipPath = getThemeTransitionClipPaths( + shape, + x, + y, + maxRadius, + viewportWidth, + viewportHeight + ) + + const root = document.documentElement + root.dataset.magicuiThemeVt = "active" + root.style.setProperty( + "--magicui-theme-toggle-vt-duration", + `${duration}ms` + ) + // Pin the collapsed clip-path via CSS so Firefox does not paint the new + // theme unclipped between snapshot and the ready.then() JS animation. + root.style.setProperty("--magicui-theme-vt-clip-from", clipPath[0]) + const cleanup = () => { + isTransitioningRef.current = false + delete root.dataset.magicuiThemeVt + root.style.removeProperty("--magicui-theme-toggle-vt-duration") + root.style.removeProperty("--magicui-theme-vt-clip-from") + cancelAnim() + } + + isTransitioningRef.current = true + const transition = document.startViewTransition(() => { + flushSync(applyTheme) + }) + if (typeof transition?.finished?.finally === "function") { + transition.finished.finally(cleanup).catch(() => {}) + } else { + cleanup() + } + + const ready = transition?.ready + if (ready && typeof ready.then === "function") { + ready + .then(() => { + const anim = document.documentElement.animate( + { + clipPath, + }, + { + duration, + // Star: linear avoids easing overshoot that fights polygon interpolation at t→1; VT group duration is synced above. + easing: shape === "star" ? "linear" : "ease-in-out", + fill: "forwards", + pseudoElement: "::view-transition-new(root)", + } + ) + activeAnimRef.current = anim + }) + .catch(() => {}) + } + }, [ + shape, + fromCenter, + duration, + isDark, + isControlled, + onThemeChange, + cancelAnim, + ]) + + return ( + + ) +} diff --git a/src/components/ui/aurora-text.tsx b/src/components/ui/aurora-text.tsx new file mode 100644 index 0000000..8be2c10 --- /dev/null +++ b/src/components/ui/aurora-text.tsx @@ -0,0 +1,41 @@ +import React, { memo } from "react" + +interface AuroraTextProps { + children: React.ReactNode + className?: string + colors?: string[] + speed?: number +} + +export const AuroraText = memo( + ({ + children, + className = "", + colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"], + speed = 1, + }: AuroraTextProps) => { + const gradientStyle = { + backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${ + colors[0] + })`, + WebkitBackgroundClip: "text", + WebkitTextFillColor: "transparent", + animationDuration: `${10 / speed}s`, + } + + return ( + + {children} + + + ) + } +) + +AuroraText.displayName = "AuroraText" diff --git a/src/components/ui/bento-grid.tsx b/src/components/ui/bento-grid.tsx new file mode 100644 index 0000000..cf6866b --- /dev/null +++ b/src/components/ui/bento-grid.tsx @@ -0,0 +1,54 @@ +import { cn } from "@/lib/utils"; + +export const BentoGrid = ({ + className, + children, +}: { + className?: string; + children?: React.ReactNode; +}) => { + return ( +
+ {children} +
+ ); +}; + +export const BentoGridItem = ({ + className, + title, + description, + header, + icon, +}: { + className?: string; + title?: string | React.ReactNode; + description?: string | React.ReactNode; + header?: React.ReactNode; + icon?: React.ReactNode; +}) => { + return ( +
+ {header} +
+ {icon} +
+ {title} +
+
+ {description} +
+
+
+ ); +}; diff --git a/src/components/ui/blur-fade.tsx b/src/components/ui/blur-fade.tsx new file mode 100644 index 0000000..045eb41 --- /dev/null +++ b/src/components/ui/blur-fade.tsx @@ -0,0 +1,92 @@ +import { useRef } from "react" +import { + AnimatePresence, + motion, + useInView, + type MotionProps, + type UseInViewOptions, + type Variants, +} from "motion/react" + +type MarginType = UseInViewOptions["margin"] + +interface BlurFadeProps extends MotionProps { + children: React.ReactNode + className?: string + variant?: { + hidden: { y: number } + visible: { y: number } + } + duration?: number + delay?: number + offset?: number + direction?: "up" | "down" | "left" | "right" + inView?: boolean + inViewMargin?: MarginType + blur?: string +} + +const getFilter = (v: Variants[string]) => + typeof v === "function" ? undefined : v.filter + +export function BlurFade({ + children, + className, + variant, + duration = 0.4, + delay = 0, + offset = 6, + direction = "down", + inView = false, + inViewMargin = "-50px", + blur = "6px", + ...props +}: BlurFadeProps) { + const ref = useRef(null) + const inViewResult = useInView(ref, { once: true, margin: inViewMargin }) + const isInView = !inView || inViewResult + const defaultVariants: Variants = { + hidden: { + [direction === "left" || direction === "right" ? "x" : "y"]: + direction === "right" || direction === "down" ? -offset : offset, + opacity: 0, + filter: `blur(${blur})`, + }, + visible: { + [direction === "left" || direction === "right" ? "x" : "y"]: 0, + opacity: 1, + filter: `blur(0px)`, + }, + } + const combinedVariants = variant ?? defaultVariants + + const hiddenFilter = getFilter(combinedVariants.hidden) + const visibleFilter = getFilter(combinedVariants.visible) + + const shouldTransitionFilter = + hiddenFilter != null && + visibleFilter != null && + hiddenFilter !== visibleFilter + + return ( + + + {children} + + + ) +} diff --git a/src/components/ui/border-beam.tsx b/src/components/ui/border-beam.tsx new file mode 100644 index 0000000..9a2e9a7 --- /dev/null +++ b/src/components/ui/border-beam.tsx @@ -0,0 +1,105 @@ +import { motion, type MotionStyle, type Transition } from "motion/react" + +import { cn } from "@/lib/utils" + +interface BorderBeamProps { + /** + * The size of the border beam. + */ + size?: number + /** + * The duration of the border beam. + */ + duration?: number + /** + * The delay of the border beam. + */ + delay?: number + /** + * The color of the border beam from. + */ + colorFrom?: string + /** + * The color of the border beam to. + */ + colorTo?: string + /** + * The motion transition of the border beam. + */ + transition?: Transition + /** + * The class name of the border beam. + */ + className?: string + /** + * The style of the border beam. + */ + style?: React.CSSProperties + /** + * Whether to reverse the animation direction. + */ + reverse?: boolean + /** + * The initial offset position (0-100). + */ + initialOffset?: number + /** + * The border width of the beam. + */ + borderWidth?: number +} + +export const BorderBeam = ({ + className, + size = 50, + delay = 0, + duration = 6, + colorFrom = "#ffaa40", + colorTo = "#9c40ff", + transition, + style, + reverse = false, + initialOffset = 0, + borderWidth = 1, +}: BorderBeamProps) => { + return ( +
+ +
+ ) +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..36496a2 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/components/ui/confetti.tsx b/src/components/ui/confetti.tsx new file mode 100644 index 0000000..bc0f290 --- /dev/null +++ b/src/components/ui/confetti.tsx @@ -0,0 +1,146 @@ +import type { ReactNode } from "react" +import React, { + createContext, + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, +} from "react" +import type { + GlobalOptions as ConfettiGlobalOptions, + CreateTypes as ConfettiInstance, + Options as ConfettiOptions, +} from "canvas-confetti" +import confetti from "canvas-confetti" + +import { Button } from "@/components/ui/button" + +export type ConfettiRef = { + fire: (options?: ConfettiOptions) => Promise | void +} + +type Props = React.ComponentPropsWithRef<"canvas"> & { + options?: ConfettiOptions + globalOptions?: ConfettiGlobalOptions + manualstart?: boolean + children?: ReactNode +} + +const ConfettiContext = createContext(null) + +const ConfettiComponent = forwardRef((props, ref) => { + const { + options, + globalOptions = { resize: true, useWorker: true }, + manualstart = false, + children, + className, + ...rest + } = props + + const canvasNodeRef = useRef(null) + const instanceRef = useRef(null) + const optionsRef = useRef(options) + const globalOptionsRef = useRef(globalOptions) + + useEffect(() => { + optionsRef.current = options + }, [options]) + + useEffect(() => { + globalOptionsRef.current = globalOptions + }, [globalOptions]) + + useEffect(() => { + if (canvasNodeRef.current && !instanceRef.current) { + instanceRef.current = confetti.create(canvasNodeRef.current, { + resize: true, + useWorker: true, + ...globalOptionsRef.current, + }) + } + + return () => { + instanceRef.current?.reset() + instanceRef.current = null + } + }, []) + + const fire = useCallback(async (opts: ConfettiOptions = {}) => { + try { + await instanceRef.current?.({ + ...optionsRef.current, + ...opts, + }) + } catch (error) { + console.error("Confetti error:", error) + } + }, []) + + const api = useMemo(() => ({ fire }), [fire]) + + useImperativeHandle(ref, () => api, [api]) + + useEffect(() => { + if (!manualstart) { + void fire() + } + }, [manualstart, fire]) + + return ( + + + {children} + + ) +}) + +ConfettiComponent.displayName = "Confetti" + +export const Confetti = ConfettiComponent + +export interface ConfettiButtonProps extends React.ComponentPropsWithoutRef< + typeof Button +> { + options?: ConfettiOptions & + ConfettiGlobalOptions & { canvas?: HTMLCanvasElement } +} + +export const ConfettiButton = forwardRef< + HTMLButtonElement, + ConfettiButtonProps +>(({ options, children, onClick, ...props }, ref) => { + const handleClick: ConfettiButtonProps["onClick"] = async (event) => { + try { + onClick?.(event) + if (event?.defaultPrevented) return + + const target = event?.currentTarget + if (target && "getBoundingClientRect" in target) { + const rect = target.getBoundingClientRect() + const origin = { + x: (rect.left + rect.width / 2) / window.innerWidth, + y: (rect.top + rect.height / 2) / window.innerHeight, + } + + await confetti({ + zIndex: 9999, + ...options, + origin, + }) + } + } catch (error) { + console.error("Confetti button error:", error) + } + } + + return ( + + ) +}) + +ConfettiButton.displayName = "ConfettiButton" diff --git a/src/components/ui/dot-pattern.tsx b/src/components/ui/dot-pattern.tsx new file mode 100644 index 0000000..eec6007 --- /dev/null +++ b/src/components/ui/dot-pattern.tsx @@ -0,0 +1,156 @@ +import React, { useEffect, useId, useRef, useState } from "react" +import { motion } from "motion/react" + +import { cn } from "@/lib/utils" + +/** + * DotPattern Component Props + * + * @param {number} [width=16] - The horizontal spacing between dots + * @param {number} [height=16] - The vertical spacing between dots + * @param {number} [x=0] - The x-offset of the entire pattern + * @param {number} [y=0] - The y-offset of the entire pattern + * @param {number} [cx=1] - The x-offset of individual dots + * @param {number} [cy=1] - The y-offset of individual dots + * @param {number} [cr=1] - The radius of each dot + * @param {string} [className] - Additional CSS classes to apply to the SVG container + * @param {boolean} [glow=false] - Whether dots should have a glowing animation effect + */ +interface DotPatternProps extends React.SVGProps { + width?: number + height?: number + x?: number + y?: number + cx?: number + cy?: number + cr?: number + className?: string + glow?: boolean + [key: string]: unknown +} + +/** + * DotPattern Component + * + * A React component that creates an animated or static dot pattern background using SVG. + * The pattern automatically adjusts to fill its container and can optionally display glowing dots. + * + * @component + * + * @see DotPatternProps for the props interface. + * + * @example + * // Basic usage + * + * + * // With glowing effect and custom spacing + * + * + * @notes + * - The component is client-side only ("use client") + * - Automatically responds to container size changes + * - When glow is enabled, dots will animate with random delays and durations + * - Uses Motion for animations + * - Dots color can be controlled via the text color utility classes + */ + +export function DotPattern({ + width = 16, + height = 16, + x = 0, + y = 0, + cx = 1, + cy = 1, + cr = 1, + className, + glow = false, + ...props +}: DotPatternProps) { + const id = useId() + const containerRef = useRef(null) + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) + + useEffect(() => { + const updateDimensions = () => { + if (containerRef.current) { + const { width, height } = containerRef.current.getBoundingClientRect() + setDimensions({ width, height }) + } + } + + updateDimensions() + window.addEventListener("resize", updateDimensions) + return () => window.removeEventListener("resize", updateDimensions) + }, []) + + const dots = Array.from( + { + length: + Math.ceil(dimensions.width / width) * + Math.ceil(dimensions.height / height), + }, + (_, i) => { + const col = i % Math.ceil(dimensions.width / width) + const row = Math.floor(i / Math.ceil(dimensions.width / width)) + return { + x: col * width + cx + x, + y: row * height + cy + y, + delay: Math.random() * 5, + duration: Math.random() * 3 + 2, + } + } + ) + + return ( + + ) +} diff --git a/src/components/ui/file-tree.tsx b/src/components/ui/file-tree.tsx new file mode 100644 index 0000000..b6173f9 --- /dev/null +++ b/src/components/ui/file-tree.tsx @@ -0,0 +1,501 @@ +import React, { + createContext, + forwardRef, + useCallback, + useContext, + useEffect, + useState, +} from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { FileIcon, FolderIcon, FolderOpenIcon } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" + +type TreeViewElement = { + id: string + name: string + type?: "file" | "folder" + isSelectable?: boolean + children?: TreeViewElement[] +} + +type TreeSortMode = + | "default" + | "none" + | ((a: TreeViewElement, b: TreeViewElement) => number) + +type TreeContextProps = { + selectedId: string | undefined + expandedItems: string[] | undefined + indicator: boolean + handleExpand: (id: string) => void + selectItem: (id: string) => void + setExpandedItems?: React.Dispatch> + openIcon?: React.ReactNode + closeIcon?: React.ReactNode + direction: "rtl" | "ltr" +} + +const TreeContext = createContext(null) + +const useTree = () => { + const context = useContext(TreeContext) + if (!context) { + throw new Error("useTree must be used within a TreeProvider") + } + return context +} + +type Direction = "rtl" | "ltr" | undefined + +const isFolderElement = (element: TreeViewElement) => { + if (element.type) { + return element.type === "folder" + } + + return Array.isArray(element.children) +} + +const mergeExpandedItems = ( + currentItems: string[] | undefined, + nextItems: string[] +) => [...new Set([...(currentItems ?? []), ...nextItems])] + +const treeCollator = new Intl.Collator("en", { + numeric: true, + sensitivity: "base", +}) + +const defaultTreeComparator = (a: TreeViewElement, b: TreeViewElement) => { + const aIsFolder = isFolderElement(a) + const bIsFolder = isFolderElement(b) + + if (aIsFolder !== bIsFolder) { + return aIsFolder ? -1 : 1 + } + + return treeCollator.compare(a.name, b.name) +} + +const getTreeComparator = (sort: TreeSortMode) => { + if (sort === "none") { + return undefined + } + + if (sort === "default") { + return defaultTreeComparator + } + + return sort +} + +const sortTreeElements = ( + elements: TreeViewElement[], + sort: TreeSortMode +): TreeViewElement[] => { + const comparator = getTreeComparator(sort) + + const nextElements = elements.map((element) => { + if (!Array.isArray(element.children)) { + return element + } + + return { + ...element, + children: sortTreeElements(element.children, sort), + } + }) + + if (!comparator) { + return nextElements + } + + return [...nextElements].sort(comparator) +} + +const renderTreeElements = ( + elements: TreeViewElement[], + sort: TreeSortMode +): React.ReactNode => + sortTreeElements(elements, sort).map((element) => { + if (isFolderElement(element)) { + return ( + + {Array.isArray(element.children) + ? renderTreeElements(element.children, sort) + : null} + + ) + } + + return ( + + {element.name} + + ) + }) + +type TreeViewProps = { + initialSelectedId?: string + indicator?: boolean + elements?: TreeViewElement[] + initialExpandedItems?: string[] + openIcon?: React.ReactNode + closeIcon?: React.ReactNode + sort?: TreeSortMode +} & Omit< + React.ComponentPropsWithoutRef, + "defaultValue" | "onValueChange" | "type" | "value" +> + +const Tree = forwardRef( + ( + { + className, + elements, + initialSelectedId, + initialExpandedItems, + children, + indicator = true, + openIcon, + closeIcon, + sort = "default", + dir, + ...props + }, + ref + ) => { + const [selectedId, setSelectedId] = useState( + initialSelectedId + ) + const [expandedItems, setExpandedItems] = useState( + initialExpandedItems + ) + + const selectItem = useCallback((id: string) => { + setSelectedId(id) + }, []) + + const handleExpand = useCallback((id: string) => { + setExpandedItems((prev) => { + if (prev?.includes(id)) { + return prev.filter((item) => item !== id) + } + return [...(prev ?? []), id] + }) + }, []) + + const expandSpecificTargetedElements = useCallback( + (elements?: TreeViewElement[], selectId?: string) => { + if (!elements || !selectId) return + const findParent = ( + currentElement: TreeViewElement, + currentPath: string[] = [] + ) => { + const isSelectable = currentElement.isSelectable ?? true + const newPath = [...currentPath, currentElement.id] + if (currentElement.id === selectId) { + if (isSelectable) { + setExpandedItems((prev) => mergeExpandedItems(prev, newPath)) + } else { + if (newPath.includes(currentElement.id)) { + newPath.pop() + setExpandedItems((prev) => mergeExpandedItems(prev, newPath)) + } + } + return + } + if ( + Array.isArray(currentElement.children) && + currentElement.children.length > 0 + ) { + currentElement.children.forEach((child) => { + findParent(child, newPath) + }) + } + } + elements.forEach((element) => { + findParent(element) + }) + }, + [] + ) + + useEffect(() => { + if (initialSelectedId) { + expandSpecificTargetedElements(elements, initialSelectedId) + } + }, [initialSelectedId, elements, expandSpecificTargetedElements]) + + const direction = dir === "rtl" ? "rtl" : "ltr" + const treeChildren = + children ?? (elements ? renderTreeElements(elements, sort) : null) + + return ( + +
+ + + {treeChildren} + + +
+
+ ) + } +) + +Tree.displayName = "Tree" + +const TreeIndicator = forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { direction } = useTree() + + return ( +
+ ) +}) + +TreeIndicator.displayName = "TreeIndicator" + +type FolderProps = { + expandedItems?: string[] + element: string + isSelectable?: boolean + isSelect?: boolean +} & React.ComponentPropsWithoutRef + +const Folder = forwardRef< + HTMLDivElement, + FolderProps & React.HTMLAttributes +>( + ( + { + className, + element, + value, + isSelectable = true, + isSelect, + children, + ...props + }, + ref + ) => { + const { + direction, + handleExpand, + expandedItems, + indicator, + selectedId, + selectItem, + openIcon, + closeIcon, + } = useTree() + const isSelected = isSelect ?? selectedId === value + + return ( + + { + selectItem(value) + handleExpand(value) + }} + > + {expandedItems?.includes(value) + ? (openIcon ?? ) + : (closeIcon ?? )} + {element} + + + {element && indicator && + + ) + } +) + +Folder.displayName = "Folder" + +const File = forwardRef< + HTMLButtonElement, + { + value: string + handleSelect?: (id: string) => void + isSelectable?: boolean + isSelect?: boolean + fileIcon?: React.ReactNode + } & React.ButtonHTMLAttributes +>( + ( + { + value, + className, + handleSelect, + onClick, + isSelectable = true, + isSelect, + fileIcon, + children, + ...props + }, + ref + ) => { + const { direction, selectedId, selectItem } = useTree() + const isSelected = isSelect ?? selectedId === value + return ( + + ) + } +) + +File.displayName = "File" + +const CollapseButton = forwardRef< + HTMLButtonElement, + { + elements: TreeViewElement[] + expandAll?: boolean + } & React.HTMLAttributes +>(({ className, elements, expandAll = false, children, ...props }, ref) => { + const { expandedItems, setExpandedItems } = useTree() + + const expendAllTree = useCallback((elements: TreeViewElement[]) => { + const expandedElementIds: string[] = [] + + const expandTree = (element: TreeViewElement) => { + const isSelectable = element.isSelectable ?? true + if (isSelectable && element.children && element.children.length > 0) { + expandedElementIds.push(element.id) + for (const child of element.children) { + expandTree(child) + } + } + } + + for (const element of elements) { + expandTree(element) + } + + return [...new Set(expandedElementIds)] + }, []) + + const closeAll = useCallback(() => { + setExpandedItems?.([]) + }, [setExpandedItems]) + + useEffect(() => { + if (expandAll) { + setExpandedItems?.(expendAllTree(elements)) + } + }, [expandAll, elements, expendAllTree, setExpandedItems]) + + return ( + + ) +}) + +CollapseButton.displayName = "CollapseButton" + +export { CollapseButton, File, Folder, Tree, type TreeViewElement } +export type { TreeSortMode } diff --git a/src/components/ui/globe.tsx b/src/components/ui/globe.tsx new file mode 100644 index 0000000..3c730fa --- /dev/null +++ b/src/components/ui/globe.tsx @@ -0,0 +1,126 @@ +import { useEffect, useRef } from "react" +import createGlobe, { type COBEOptions } from "cobe" +import { useMotionValue, useSpring } from "motion/react" + +import { cn } from "@/lib/utils" + +const MOVEMENT_DAMPING = 1400 + +const GLOBE_CONFIG: COBEOptions = { + width: 800, + height: 800, + onRender: () => {}, + devicePixelRatio: 2, + phi: 0, + theta: 0.3, + dark: 0, + diffuse: 0.4, + mapSamples: 16000, + mapBrightness: 1.2, + baseColor: [1, 1, 1], + markerColor: [251 / 255, 100 / 255, 21 / 255], + glowColor: [1, 1, 1], + markers: [ + { location: [14.5995, 120.9842], size: 0.03 }, + { location: [19.076, 72.8777], size: 0.1 }, + { location: [23.8103, 90.4125], size: 0.05 }, + { location: [30.0444, 31.2357], size: 0.07 }, + { location: [39.9042, 116.4074], size: 0.08 }, + { location: [-23.5505, -46.6333], size: 0.1 }, + { location: [19.4326, -99.1332], size: 0.1 }, + { location: [40.7128, -74.006], size: 0.1 }, + { location: [34.6937, 135.5022], size: 0.05 }, + { location: [41.0082, 28.9784], size: 0.06 }, + ], +} + +export function Globe({ + className, + config = GLOBE_CONFIG, +}: { + className?: string + config?: COBEOptions +}) { + const canvasRef = useRef(null) + const phiRef = useRef(0) + const widthRef = useRef(0) + const pointerInteracting = useRef(null) + const pointerInteractionMovement = useRef(0) + + const r = useMotionValue(0) + const rs = useSpring(r, { + mass: 1, + damping: 30, + stiffness: 100, + }) + + const updatePointerInteraction = (value: number | null) => { + pointerInteracting.current = value + if (canvasRef.current) { + canvasRef.current.style.cursor = value !== null ? "grabbing" : "grab" + } + } + + const updateMovement = (clientX: number) => { + if (pointerInteracting.current !== null) { + const delta = clientX - pointerInteracting.current + pointerInteractionMovement.current = delta + r.set(r.get() + delta / MOVEMENT_DAMPING) + } + } + + useEffect(() => { + const onResize = () => { + if (canvasRef.current) { + widthRef.current = canvasRef.current.offsetWidth + } + } + + window.addEventListener("resize", onResize) + onResize() + + const globe = createGlobe(canvasRef.current!, { + ...config, + width: widthRef.current * 2, + height: widthRef.current * 2, + onRender: (state) => { + if (!pointerInteracting.current) phiRef.current += 0.005 + state.phi = phiRef.current + rs.get() + state.width = widthRef.current * 2 + state.height = widthRef.current * 2 + }, + }) + + setTimeout(() => (canvasRef.current!.style.opacity = "1"), 0) + return () => { + globe.destroy() + window.removeEventListener("resize", onResize) + } + }, [rs, config]) + + return ( +
+ { + pointerInteracting.current = e.clientX + updatePointerInteraction(e.clientX) + }} + onPointerUp={() => updatePointerInteraction(null)} + onPointerOut={() => updatePointerInteraction(null)} + onMouseMove={(e) => updateMovement(e.clientX)} + onTouchMove={(e) => + e.touches[0] && updateMovement(e.touches[0].clientX) + } + /> +
+ ) +} diff --git a/src/components/ui/grid-pattern.tsx b/src/components/ui/grid-pattern.tsx new file mode 100644 index 0000000..a6d1839 --- /dev/null +++ b/src/components/ui/grid-pattern.tsx @@ -0,0 +1,70 @@ +import { useId } from "react" + +import { cn } from "@/lib/utils" + +interface GridPatternProps extends React.SVGProps { + width?: number + height?: number + x?: number + y?: number + squares?: Array<[x: number, y: number]> + strokeDasharray?: string + className?: string + [key: string]: unknown +} + +export function GridPattern({ + width = 40, + height = 40, + x = -1, + y = -1, + strokeDasharray = "0", + squares, + className, + ...props +}: GridPatternProps) { + const id = useId() + + return ( + + ) +} diff --git a/src/components/ui/icon-cloud.tsx b/src/components/ui/icon-cloud.tsx new file mode 100644 index 0000000..46c8a6b --- /dev/null +++ b/src/components/ui/icon-cloud.tsx @@ -0,0 +1,375 @@ +import React, { useEffect, useRef, useState } from "react" +import { Pause, Play } from "lucide-react" +import { renderToString } from "react-dom/server" + +import { Button } from "@/components/ui/button" + +interface Icon { + x: number + y: number + z: number + scale: number + opacity: number + id: number +} + +interface IconCloudProps { + icons?: React.ReactNode[] + images?: string[] + showControl?: boolean +} + +function easeOutCubic(t: number): number { + return 1 - Math.pow(1 - t, 3) +} + +export function IconCloud({ + icons, + images, + showControl = true, +}: IconCloudProps) { + const canvasRef = useRef(null) + const [iconPositions, setIconPositions] = useState([]) + const [isDragging, setIsDragging] = useState(false) + const [isPaused, setIsPaused] = useState(false) + const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 }) + const [mousePos, setMousePos] = useState({ x: 0, y: 0 }) + const [targetRotation, setTargetRotation] = useState<{ + x: number + y: number + startX: number + startY: number + distance: number + startTime: number + duration: number + } | null>(null) + const animationFrameRef = useRef(0) + const rotationRef = useRef({ x: 0, y: 0 }) + const iconCanvasesRef = useRef([]) + const imagesLoadedRef = useRef([]) + + // Pause animation if user prefers reduced motion + useEffect(() => { + const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)") + if (mediaQuery.matches) { + setIsPaused(true) + } + + const handleChange = (e: MediaQueryListEvent) => { + setIsPaused(e.matches) + } + + mediaQuery.addEventListener("change", handleChange) + return () => mediaQuery.removeEventListener("change", handleChange) + }, []) + + // Create icon canvases once when icons/images change + useEffect(() => { + if (!icons && !images) return + + const items = icons ?? images ?? [] + imagesLoadedRef.current = new Array(items.length).fill(false) + + const newIconCanvases = items.map((item, index) => { + const offscreen = document.createElement("canvas") + offscreen.width = 40 + offscreen.height = 40 + const offCtx = offscreen.getContext("2d") + + if (offCtx) { + if (images) { + // Handle image URLs directly + const img = new Image() + img.crossOrigin = "anonymous" + img.src = items[index] as string + img.onload = () => { + offCtx.clearRect(0, 0, offscreen.width, offscreen.height) + + // Create circular clipping path + offCtx.beginPath() + offCtx.arc(20, 20, 20, 0, Math.PI * 2) + offCtx.closePath() + offCtx.clip() + + // Draw the image + offCtx.drawImage(img, 0, 0, 40, 40) + + imagesLoadedRef.current[index] = true + } + } else { + // Handle SVG icons + offCtx.scale(0.4, 0.4) + const svgString = renderToString(item as React.ReactElement) + const img = new Image() + img.src = "data:image/svg+xml;base64," + btoa(svgString) + img.onload = () => { + offCtx.clearRect(0, 0, offscreen.width, offscreen.height) + offCtx.drawImage(img, 0, 0) + imagesLoadedRef.current[index] = true + } + } + } + return offscreen + }) + + iconCanvasesRef.current = newIconCanvases + }, [icons, images]) + + // Generate initial icon positions on a sphere + useEffect(() => { + const items = icons ?? images ?? [] + const newIcons: Icon[] = [] + const numIcons = items.length || 20 + + // Fibonacci sphere parameters + const offset = 2 / numIcons + const increment = Math.PI * (3 - Math.sqrt(5)) + + for (let i = 0; i < numIcons; i++) { + const y = i * offset - 1 + offset / 2 + const r = Math.sqrt(1 - y * y) + const phi = i * increment + + const x = Math.cos(phi) * r + const z = Math.sin(phi) * r + + newIcons.push({ + x: x * 100, + y: y * 100, + z: z * 100, + scale: 1, + opacity: 1, + id: i, + }) + } + setIconPositions(newIcons) + }, [icons, images]) + + // Handle mouse events + const handleMouseDown = (e: React.MouseEvent) => { + const rect = canvasRef.current?.getBoundingClientRect() + if (!rect || !canvasRef.current) return + + const x = e.clientX - rect.left + const y = e.clientY - rect.top + + const ctx = canvasRef.current.getContext("2d") + if (!ctx) return + + iconPositions.forEach((icon) => { + const cosX = Math.cos(rotationRef.current.x) + const sinX = Math.sin(rotationRef.current.x) + const cosY = Math.cos(rotationRef.current.y) + const sinY = Math.sin(rotationRef.current.y) + + const rotatedX = icon.x * cosY - icon.z * sinY + const rotatedZ = icon.x * sinY + icon.z * cosY + const rotatedY = icon.y * cosX + rotatedZ * sinX + + const screenX = canvasRef.current!.width / 2 + rotatedX + const screenY = canvasRef.current!.height / 2 + rotatedY + + const scale = (rotatedZ + 200) / 300 + const radius = 20 * scale + const dx = x - screenX + const dy = y - screenY + + if (dx * dx + dy * dy < radius * radius) { + const targetX = -Math.atan2( + icon.y, + Math.sqrt(icon.x * icon.x + icon.z * icon.z) + ) + const targetY = Math.atan2(icon.x, icon.z) + + const currentX = rotationRef.current.x + const currentY = rotationRef.current.y + const distance = Math.sqrt( + Math.pow(targetX - currentX, 2) + Math.pow(targetY - currentY, 2) + ) + + const duration = Math.min(2000, Math.max(800, distance * 1000)) + + setTargetRotation({ + x: targetX, + y: targetY, + startX: currentX, + startY: currentY, + distance, + startTime: performance.now(), + duration, + }) + return + } + }) + + setIsDragging(true) + setLastMousePos({ x: e.clientX, y: e.clientY }) + } + + const handleMouseMove = (e: React.MouseEvent) => { + const rect = canvasRef.current?.getBoundingClientRect() + if (rect) { + const x = e.clientX - rect.left + const y = e.clientY - rect.top + setMousePos({ x, y }) + } + + if (isDragging) { + const deltaX = e.clientX - lastMousePos.x + const deltaY = e.clientY - lastMousePos.y + + rotationRef.current = { + x: rotationRef.current.x + deltaY * 0.002, + y: rotationRef.current.y + deltaX * 0.002, + } + + setLastMousePos({ x: e.clientX, y: e.clientY }) + } + } + + const handleMouseUp = () => { + setIsDragging(false) + } + + // Animation and rendering + useEffect(() => { + const canvas = canvasRef.current + const ctx = canvas?.getContext("2d") + if (canvas && ctx) { + const animate = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height) + + const centerX = canvas.width / 2 + const centerY = canvas.height / 2 + const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY) + const dx = mousePos.x - centerX + const dy = mousePos.y - centerY + const distance = Math.sqrt(dx * dx + dy * dy) + const speed = 0.003 + (distance / maxDistance) * 0.01 + + if (targetRotation) { + const elapsed = performance.now() - targetRotation.startTime + const progress = Math.min(1, elapsed / targetRotation.duration) + const easedProgress = easeOutCubic(progress) + + rotationRef.current = { + x: + targetRotation.startX + + (targetRotation.x - targetRotation.startX) * easedProgress, + y: + targetRotation.startY + + (targetRotation.y - targetRotation.startY) * easedProgress, + } + + if (progress >= 1) { + setTargetRotation(null) + } + } else if (!isDragging && !isPaused) { + rotationRef.current = { + x: rotationRef.current.x + (dy / canvas.height) * speed, + y: rotationRef.current.y + (dx / canvas.width) * speed, + } + } + + iconPositions.forEach((icon, index) => { + const cosX = Math.cos(rotationRef.current.x) + const sinX = Math.sin(rotationRef.current.x) + const cosY = Math.cos(rotationRef.current.y) + const sinY = Math.sin(rotationRef.current.y) + + const rotatedX = icon.x * cosY - icon.z * sinY + const rotatedZ = icon.x * sinY + icon.z * cosY + const rotatedY = icon.y * cosX + rotatedZ * sinX + + const scale = (rotatedZ + 200) / 300 + const opacity = Math.max(0.2, Math.min(1, (rotatedZ + 150) / 200)) + + ctx.save() + ctx.translate( + canvas.width / 2 + rotatedX, + canvas.height / 2 + rotatedY + ) + ctx.scale(scale, scale) + ctx.globalAlpha = opacity + + if (icons || images) { + // Only try to render icons/images if they exist + if ( + iconCanvasesRef.current[index] && + imagesLoadedRef.current[index] + ) { + ctx.drawImage(iconCanvasesRef.current[index], -20, -20, 40, 40) + } + } else { + // Show numbered circles if no icons/images are provided + ctx.beginPath() + ctx.arc(0, 0, 20, 0, Math.PI * 2) + ctx.fillStyle = "#4444ff" + ctx.fill() + ctx.fillStyle = "white" + ctx.textAlign = "center" + ctx.textBaseline = "middle" + ctx.font = "16px Arial" + ctx.fillText(`${icon.id + 1}`, 0, 0) + } + + ctx.restore() + }) + + const hasPendingAssets = + Boolean(icons || images) && + !imagesLoadedRef.current.every((loaded) => loaded) + const shouldContinue = + !isPaused || isDragging || targetRotation !== null || hasPendingAssets + + if (shouldContinue) { + animationFrameRef.current = requestAnimationFrame(animate) + } + } + + animate() + } + + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } + } + }, [ + icons, + images, + iconPositions, + isDragging, + isPaused, + mousePos, + targetRotation, + ]) + + return ( +
+ + {showControl && ( + + )} +
+ ) +} diff --git a/src/components/ui/interactive-hover-button.tsx b/src/components/ui/interactive-hover-button.tsx new file mode 100644 index 0000000..6aab0d8 --- /dev/null +++ b/src/components/ui/interactive-hover-button.tsx @@ -0,0 +1,30 @@ +import { ArrowRight } from "lucide-react" + +import { cn } from "@/lib/utils" + +export function InteractiveHoverButton({ + children, + className, + ...props +}: React.ButtonHTMLAttributes) { + return ( + + ) +} diff --git a/src/components/ui/magic-card.tsx b/src/components/ui/magic-card.tsx new file mode 100644 index 0000000..68406ca --- /dev/null +++ b/src/components/ui/magic-card.tsx @@ -0,0 +1,220 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { + motion, + useMotionTemplate, + useMotionValue, + useSpring, +} from "motion/react" +import { useTheme } from "next-themes" + +import { cn } from "@/lib/utils" + +interface MagicCardBaseProps { + children?: React.ReactNode + className?: string + gradientSize?: number + gradientFrom?: string + gradientTo?: string +} + +interface MagicCardGradientProps extends MagicCardBaseProps { + mode?: "gradient" + + gradientColor?: string + gradientOpacity?: number + + glowFrom?: never + glowTo?: never + glowAngle?: never + glowSize?: never + glowBlur?: never + glowOpacity?: never +} + +interface MagicCardOrbProps extends MagicCardBaseProps { + mode: "orb" + + glowFrom?: string + glowTo?: string + glowAngle?: number + glowSize?: number + glowBlur?: number + glowOpacity?: number + + gradientColor?: never + gradientOpacity?: never +} + +type MagicCardProps = MagicCardGradientProps | MagicCardOrbProps +type ResetReason = "enter" | "leave" | "global" | "init" + +function isOrbMode(props: MagicCardProps): props is MagicCardOrbProps { + return props.mode === "orb" +} + +export function MagicCard(props: MagicCardProps) { + const { + children, + className, + gradientSize = 200, + gradientColor = "#262626", + gradientOpacity = 0.8, + gradientFrom = "#9E7AFF", + gradientTo = "#FE8BBB", + mode = "gradient", + } = props + + const glowFrom = isOrbMode(props) ? (props.glowFrom ?? "#ee4f27") : "#ee4f27" + const glowTo = isOrbMode(props) ? (props.glowTo ?? "#6b21ef") : "#6b21ef" + const glowAngle = isOrbMode(props) ? (props.glowAngle ?? 90) : 90 + const glowSize = isOrbMode(props) ? (props.glowSize ?? 420) : 420 + const glowBlur = isOrbMode(props) ? (props.glowBlur ?? 60) : 60 + const glowOpacity = isOrbMode(props) ? (props.glowOpacity ?? 0.9) : 0.9 + const { theme, systemTheme } = useTheme() + const [mounted, setMounted] = useState(false) + + useEffect(() => setMounted(true), []) + + const isDarkTheme = useMemo(() => { + if (!mounted) return true + const currentTheme = theme === "system" ? systemTheme : theme + return currentTheme === "dark" + }, [theme, systemTheme, mounted]) + + const mouseX = useMotionValue(-gradientSize) + const mouseY = useMotionValue(-gradientSize) + + const orbX = useSpring(mouseX, { stiffness: 250, damping: 30, mass: 0.6 }) + const orbY = useSpring(mouseY, { stiffness: 250, damping: 30, mass: 0.6 }) + const orbVisible = useSpring(0, { stiffness: 300, damping: 35 }) + + const modeRef = useRef(mode) + const glowOpacityRef = useRef(glowOpacity) + const gradientSizeRef = useRef(gradientSize) + + useEffect(() => { + modeRef.current = mode + }, [mode]) + + useEffect(() => { + glowOpacityRef.current = glowOpacity + }, [glowOpacity]) + + useEffect(() => { + gradientSizeRef.current = gradientSize + }, [gradientSize]) + + const reset = useCallback( + (reason: ResetReason = "leave") => { + const currentMode = modeRef.current + + if (currentMode === "orb") { + if (reason === "enter") orbVisible.set(glowOpacityRef.current) + else orbVisible.set(0) + return + } + + const off = -gradientSizeRef.current + mouseX.set(off) + mouseY.set(off) + }, + [mouseX, mouseY, orbVisible] + ) + + const handlePointerMove = useCallback( + (e: React.PointerEvent) => { + const rect = e.currentTarget.getBoundingClientRect() + mouseX.set(e.clientX - rect.left) + mouseY.set(e.clientY - rect.top) + }, + [mouseX, mouseY] + ) + + useEffect(() => { + reset("init") + }, [reset]) + + useEffect(() => { + const handleGlobalPointerOut = (e: PointerEvent) => { + if (!e.relatedTarget) reset("global") + } + const handleBlur = () => reset("global") + const handleVisibility = () => { + if (document.visibilityState !== "visible") reset("global") + } + + window.addEventListener("pointerout", handleGlobalPointerOut) + window.addEventListener("blur", handleBlur) + document.addEventListener("visibilitychange", handleVisibility) + + return () => { + window.removeEventListener("pointerout", handleGlobalPointerOut) + window.removeEventListener("blur", handleBlur) + document.removeEventListener("visibilitychange", handleVisibility) + } + }, [reset]) + + return ( + reset("leave")} + onPointerEnter={() => reset("enter")} + style={{ + background: useMotionTemplate` + linear-gradient(var(--color-background) 0 0) padding-box, + radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px, + ${gradientFrom}, + ${gradientTo}, + var(--color-border) 100% + ) border-box + `, + }} + > +
+ + {mode === "gradient" && ( + + )} + + {mode === "orb" && ( + + ) +} diff --git a/src/components/ui/marquee.tsx b/src/components/ui/marquee.tsx new file mode 100644 index 0000000..c86ab57 --- /dev/null +++ b/src/components/ui/marquee.tsx @@ -0,0 +1,74 @@ +import { type ComponentPropsWithoutRef } from "react" + +import { cn } from "@/lib/utils" + +interface MarqueeProps extends ComponentPropsWithoutRef<"div"> { + /** + * Optional CSS class name to apply custom styles + */ + className?: string + /** + * Whether to reverse the animation direction + * @default false + */ + reverse?: boolean + /** + * Whether to pause the animation on hover + * @default false + */ + pauseOnHover?: boolean + /** + * Content to be displayed in the marquee + */ + children: React.ReactNode + /** + * Whether to animate vertically instead of horizontally + * @default false + */ + vertical?: boolean + /** + * Number of times to repeat the content + * @default 4 + */ + repeat?: number +} + +export function Marquee({ + className, + reverse = false, + pauseOnHover = false, + children, + vertical = false, + repeat = 4, + ...props +}: MarqueeProps) { + return ( +
+ {Array(repeat) + .fill(0) + .map((_, i) => ( +
+ {children} +
+ ))} +
+ ) +} diff --git a/src/components/ui/number-ticker.tsx b/src/components/ui/number-ticker.tsx new file mode 100644 index 0000000..9c0049f --- /dev/null +++ b/src/components/ui/number-ticker.tsx @@ -0,0 +1,72 @@ +import { useEffect, useRef, type ComponentPropsWithoutRef } from "react" +import { useInView, useMotionValue, useSpring } from "motion/react" + +import { cn } from "@/lib/utils" + +interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> { + value: number + startValue?: number + direction?: "up" | "down" + delay?: number + decimalPlaces?: number +} + +export function NumberTicker({ + value, + startValue = 0, + direction = "up", + delay = 0, + className, + decimalPlaces = 0, + ...props +}: NumberTickerProps) { + const ref = useRef(null) + const motionValue = useMotionValue(direction === "down" ? value : startValue) + const springValue = useSpring(motionValue, { + damping: 60, + stiffness: 100, + }) + const isInView = useInView(ref, { once: true, margin: "0px" }) + + useEffect(() => { + let timer: ReturnType | null = null + + if (isInView) { + timer = setTimeout(() => { + motionValue.set(direction === "down" ? startValue : value) + }, delay * 1000) + } + + return () => { + if (timer !== null) { + clearTimeout(timer) + } + } + }, [motionValue, isInView, delay, value, direction, startValue]) + + useEffect( + () => + springValue.on("change", (latest) => { + if (ref.current) { + ref.current.textContent = Intl.NumberFormat("en-US", { + minimumFractionDigits: decimalPlaces, + maximumFractionDigits: decimalPlaces, + }).format(Number(latest.toFixed(decimalPlaces))) + } + }), + [springValue, decimalPlaces] + ) + + return ( + + {startValue} + + ) +} diff --git a/src/components/ui/orbiting-circles.tsx b/src/components/ui/orbiting-circles.tsx new file mode 100644 index 0000000..5cb8345 --- /dev/null +++ b/src/components/ui/orbiting-circles.tsx @@ -0,0 +1,71 @@ +import React from "react" + +import { cn } from "@/lib/utils" + +export interface OrbitingCirclesProps extends React.HTMLAttributes { + className?: string + children?: React.ReactNode + reverse?: boolean + duration?: number + delay?: number + radius?: number + path?: boolean + iconSize?: number + speed?: number +} + +export function OrbitingCircles({ + className, + children, + reverse, + duration = 20, + radius = 160, + path = true, + iconSize = 30, + speed = 1, + ...props +}: OrbitingCirclesProps) { + const calculatedDuration = duration / speed + return ( + <> + {path && ( + + + + )} + {React.Children.map(children, (child, index) => { + const angle = (360 / React.Children.count(children)) * index + return ( +
+ {child} +
+ ) + })} + + ) +} diff --git a/src/components/ui/pulsating-button.tsx b/src/components/ui/pulsating-button.tsx new file mode 100644 index 0000000..f088a3e --- /dev/null +++ b/src/components/ui/pulsating-button.tsx @@ -0,0 +1,125 @@ +import React, { useImperativeHandle, useLayoutEffect, useRef } from "react" + +import { cn } from "@/lib/utils" + +interface PulsatingButtonProps extends React.ButtonHTMLAttributes { + pulseColor?: string + duration?: string + distance?: string + variant?: "pulse" | "ripple" +} + +export const PulsatingButton = React.forwardRef< + HTMLButtonElement, + PulsatingButtonProps +>( + ( + { + className, + children, + pulseColor, + duration = "1.5s", + distance = "8px", + variant = "pulse", + ...props + }, + ref + ) => { + const innerRef = useRef(null) + useImperativeHandle(ref, () => innerRef.current!) + + useLayoutEffect(() => { + const button = innerRef.current + if (!button) return + + if (pulseColor) { + button.style.removeProperty("--bg") + return + } + + let animationFrameId = 0 + let currentBg = "" + + const updateBg = () => { + animationFrameId = 0 + + const nextBg = getComputedStyle(button).backgroundColor + if (nextBg === currentBg) return + + currentBg = nextBg + button.style.setProperty("--bg", nextBg) + } + + const scheduleBgUpdate = () => { + if (animationFrameId) return + animationFrameId = window.requestAnimationFrame(updateBg) + } + + updateBg() + + const themeObserver = new MutationObserver(scheduleBgUpdate) + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }) + + const buttonObserver = new MutationObserver(scheduleBgUpdate) + buttonObserver.observe(button, { + attributes: true, + }) + + const syncEvents = [ + "blur", + "focus", + "pointerenter", + "pointerleave", + ] as const + + for (const eventName of syncEvents) { + button.addEventListener(eventName, scheduleBgUpdate) + } + + return () => { + if (animationFrameId) { + window.cancelAnimationFrame(animationFrameId) + } + + themeObserver.disconnect() + buttonObserver.disconnect() + + for (const eventName of syncEvents) { + button.removeEventListener(eventName, scheduleBgUpdate) + } + } + }, [pulseColor]) + + return ( + + ) + } +) + +PulsatingButton.displayName = "PulsatingButton" diff --git a/src/components/ui/rainbow-button.tsx b/src/components/ui/rainbow-button.tsx new file mode 100644 index 0000000..4727300 --- /dev/null +++ b/src/components/ui/rainbow-button.tsx @@ -0,0 +1,61 @@ +import React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const rainbowButtonVariants = cva( + cn( + "relative cursor-pointer group transition-all animate-rainbow", + "inline-flex items-center justify-center gap-2 shrink-0", + "rounded-sm outline-none focus-visible:ring-[3px] aria-invalid:border-destructive", + "text-sm font-medium whitespace-nowrap", + "disabled:pointer-events-none disabled:opacity-50", + "[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0" + ), + { + variants: { + variant: { + default: + "border-0 bg-[linear-gradient(#121213,#121213),linear-gradient(#121213_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] bg-[length:200%] text-primary-foreground [background-clip:padding-box,border-box,border-box] [background-origin:border-box] [border:calc(0.125rem)_solid_transparent] before:absolute before:bottom-[-20%] before:left-1/2 before:z-0 before:h-1/5 before:w-3/5 before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] before:bg-[length:200%] before:[filter:blur(0.75rem)] dark:bg-[linear-gradient(#fff,#fff),linear-gradient(#fff_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))]", + outline: + "border border-input border-b-transparent bg-[linear-gradient(#ffffff,#ffffff),linear-gradient(#ffffff_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] bg-[length:200%] text-accent-foreground [background-clip:padding-box,border-box,border-box] [background-origin:border-box] before:absolute before:bottom-[-20%] before:left-1/2 before:z-0 before:h-1/5 before:w-3/5 before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] before:bg-[length:200%] before:[filter:blur(0.75rem)] dark:bg-[linear-gradient(#0a0a0a,#0a0a0a),linear-gradient(#0a0a0a_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))]", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-xl px-3 text-xs", + lg: "h-11 rounded-xl px-8", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +interface RainbowButtonProps + extends + React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const RainbowButton = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) + +RainbowButton.displayName = "RainbowButton" + +export { RainbowButton, rainbowButtonVariants, type RainbowButtonProps } diff --git a/src/components/ui/ripple-button.tsx b/src/components/ui/ripple-button.tsx new file mode 100644 index 0000000..faed828 --- /dev/null +++ b/src/components/ui/ripple-button.tsx @@ -0,0 +1,99 @@ +import React, { useEffect, useState, type MouseEvent } from "react" + +import { cn } from "@/lib/utils" + +interface RippleButtonProps extends React.ButtonHTMLAttributes { + rippleColor?: string + duration?: string +} + +export const RippleButton = React.forwardRef< + HTMLButtonElement, + RippleButtonProps +>( + ( + { + className, + children, + rippleColor = "#ffffff", + duration = "600ms", + onClick, + ...props + }, + ref + ) => { + const [buttonRipples, setButtonRipples] = useState< + Array<{ x: number; y: number; size: number; key: number }> + >([]) + + const handleClick = (event: MouseEvent) => { + createRipple(event) + onClick?.(event) + } + + const createRipple = (event: MouseEvent) => { + const button = event.currentTarget + const rect = button.getBoundingClientRect() + const size = Math.max(rect.width, rect.height) + const x = event.clientX - rect.left - size / 2 + const y = event.clientY - rect.top - size / 2 + + const newRipple = { x, y, size, key: Date.now() } + setButtonRipples((prevRipples) => [...prevRipples, newRipple]) + } + + useEffect(() => { + let timeout: ReturnType | null = null + + if (buttonRipples.length > 0) { + const lastRipple = buttonRipples[buttonRipples.length - 1] + timeout = setTimeout(() => { + setButtonRipples((prevRipples) => + prevRipples.filter((ripple) => ripple.key !== lastRipple.key) + ) + }, parseInt(duration)) + } + + return () => { + if (timeout !== null) { + clearTimeout(timeout) + } + } + }, [buttonRipples, duration]) + + return ( + + ) + } +) + +RippleButton.displayName = "RippleButton" diff --git a/src/components/ui/ripple.tsx b/src/components/ui/ripple.tsx new file mode 100644 index 0000000..bf166a1 --- /dev/null +++ b/src/components/ui/ripple.tsx @@ -0,0 +1,58 @@ +import React, { type ComponentPropsWithoutRef, type CSSProperties } from "react" + +import { cn } from "@/lib/utils" + +interface RippleProps extends ComponentPropsWithoutRef<"div"> { + mainCircleSize?: number + mainCircleOpacity?: number + numCircles?: number +} + +export const Ripple = React.memo(function Ripple({ + mainCircleSize = 210, + mainCircleOpacity = 0.24, + numCircles = 8, + className, + ...props +}: RippleProps) { + return ( +
+ {Array.from({ length: numCircles }, (_, i) => { + const size = mainCircleSize + i * 70 + const opacity = mainCircleOpacity - i * 0.03 + const animationDelay = `${i * 0.06}s` + const borderStyle = "solid" + + return ( +
+ ) + })} +
+ ) +}) + +Ripple.displayName = "Ripple" diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..cf253cf --- /dev/null +++ b/src/components/ui/scroll-area.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/src/components/ui/scroll-progress.tsx b/src/components/ui/scroll-progress.tsx new file mode 100644 index 0000000..f8aa5fb --- /dev/null +++ b/src/components/ui/scroll-progress.tsx @@ -0,0 +1,34 @@ +import { motion, useScroll, type MotionProps } from "motion/react" + +import { cn } from "@/lib/utils" + +interface ScrollProgressProps extends Omit< + React.HTMLAttributes, + keyof MotionProps +> { + ref?: React.Ref +} + +export function ScrollProgress({ + className, + ref, + ...props +}: ScrollProgressProps) { + const { scrollYProgress } = useScroll() + + return ( + + ) +} diff --git a/src/components/ui/shimmer-button.tsx b/src/components/ui/shimmer-button.tsx new file mode 100644 index 0000000..d675cc0 --- /dev/null +++ b/src/components/ui/shimmer-button.tsx @@ -0,0 +1,96 @@ +import React, { type ComponentPropsWithoutRef, type CSSProperties } from "react" + +import { cn } from "@/lib/utils" + +export interface ShimmerButtonProps extends ComponentPropsWithoutRef<"button"> { + shimmerColor?: string + shimmerSize?: string + borderRadius?: string + shimmerDuration?: string + background?: string + className?: string + children?: React.ReactNode +} + +export const ShimmerButton = React.forwardRef< + HTMLButtonElement, + ShimmerButtonProps +>( + ( + { + shimmerColor = "#ffffff", + shimmerSize = "0.05em", + shimmerDuration = "3s", + borderRadius = "100px", + background = "rgba(0, 0, 0, 1)", + className, + children, + ...props + }, + ref + ) => { + return ( + + ) + } +) + +ShimmerButton.displayName = "ShimmerButton" diff --git a/src/components/ui/shiny-button.tsx b/src/components/ui/shiny-button.tsx new file mode 100644 index 0000000..f54a602 --- /dev/null +++ b/src/components/ui/shiny-button.tsx @@ -0,0 +1,72 @@ +import React from "react" +import { motion, type MotionProps } from "motion/react" + +import { cn } from "@/lib/utils" + +const animationProps: MotionProps = { + initial: { "--x": "100%", scale: 0.8 }, + animate: { "--x": "-100%", scale: 1 }, + whileTap: { scale: 0.95 }, + transition: { + repeat: Infinity, + repeatType: "loop", + repeatDelay: 1, + type: "spring", + stiffness: 20, + damping: 15, + mass: 2, + scale: { + type: "spring", + stiffness: 200, + damping: 5, + mass: 0.5, + }, + }, +} + +interface ShinyButtonProps + extends + Omit, keyof MotionProps>, + MotionProps { + children: React.ReactNode + className?: string +} + +export const ShinyButton = React.forwardRef< + HTMLButtonElement, + ShinyButtonProps +>(({ children, className, ...props }, ref) => { + return ( + + + {children} + + + + ) +}) + +ShinyButton.displayName = "ShinyButton" diff --git a/src/components/ui/terminal.tsx b/src/components/ui/terminal.tsx new file mode 100644 index 0000000..00da5d6 --- /dev/null +++ b/src/components/ui/terminal.tsx @@ -0,0 +1,298 @@ +import { + Children, + createContext, + useContext, + useEffect, + useMemo, + useRef, + useState, + type ComponentType, + type RefAttributes, +} from "react" +import { + motion, + useInView, + type DOMMotionComponents, + type HTMLMotionProps, + type MotionProps, +} from "motion/react" + +import { cn } from "@/lib/utils" + +interface SequenceContextValue { + completeItem: (index: number) => void + activeIndex: number + sequenceStarted: boolean +} + +const SequenceContext = createContext(null) + +const useSequence = () => useContext(SequenceContext) + +const ItemIndexContext = createContext(null) +const useItemIndex = () => useContext(ItemIndexContext) + +const motionElements = { + article: motion.article, + div: motion.div, + h1: motion.h1, + h2: motion.h2, + h3: motion.h3, + h4: motion.h4, + h5: motion.h5, + h6: motion.h6, + li: motion.li, + p: motion.p, + section: motion.section, + span: motion.span, +} as const + +type MotionElementType = Extract< + keyof DOMMotionComponents, + keyof typeof motionElements +> +type TerminalTypingMotionComponent = ComponentType< + Omit, "ref"> & RefAttributes +> + +interface AnimatedSpanProps extends MotionProps { + children: React.ReactNode + delay?: number + className?: string + startOnView?: boolean +} + +export const AnimatedSpan = ({ + children, + delay = 0, + className, + startOnView = false, + ...props +}: AnimatedSpanProps) => { + const elementRef = useRef(null) + const isInView = useInView(elementRef as React.RefObject, { + amount: 0.3, + once: true, + }) + + const sequence = useSequence() + const itemIndex = useItemIndex() + const [hasStarted, setHasStarted] = useState(false) + useEffect(() => { + if (!sequence || itemIndex === null) return + if (!sequence.sequenceStarted) return + if (hasStarted) return + if (sequence.activeIndex === itemIndex) { + setHasStarted(true) + } + }, [sequence, hasStarted, itemIndex]) + + const shouldAnimate = sequence ? hasStarted : startOnView ? isInView : true + + return ( + { + if (!sequence) return + if (itemIndex === null) return + sequence.completeItem(itemIndex) + }} + {...props} + > + {children} + + ) +} + +interface TypingAnimationProps extends Omit { + children: string + className?: string + duration?: number + delay?: number + as?: MotionElementType + startOnView?: boolean +} + +export const TypingAnimation = ({ + children, + className, + duration = 60, + delay = 0, + as: Component = "span", + startOnView = true, + ...props +}: TypingAnimationProps) => { + if (typeof children !== "string") { + throw new Error("TypingAnimation: children must be a string. Received:") + } + + const MotionComponent = motionElements[ + Component + ] as TerminalTypingMotionComponent + + const [displayedText, setDisplayedText] = useState("") + const [started, setStarted] = useState(false) + const elementRef = useRef(null) + const isInView = useInView(elementRef as React.RefObject, { + amount: 0.3, + once: true, + }) + + const sequence = useSequence() + const itemIndex = useItemIndex() + const hasSequence = sequence !== null + const sequenceStarted = sequence?.sequenceStarted ?? false + const sequenceActiveIndex = sequence?.activeIndex ?? null + const sequenceCompleteItemRef = useRef< + SequenceContextValue["completeItem"] | null + >(null) + const sequenceItemIndexRef = useRef(null) + + useEffect(() => { + sequenceCompleteItemRef.current = sequence?.completeItem ?? null + sequenceItemIndexRef.current = itemIndex + }, [sequence?.completeItem, itemIndex]) + + useEffect(() => { + let startTimeout: ReturnType | null = null + + if (hasSequence && itemIndex !== null) { + if (sequenceStarted && !started && sequenceActiveIndex === itemIndex) { + setStarted(true) + } + } else if (!startOnView || isInView) { + startTimeout = setTimeout(() => setStarted(true), delay) + } + + return () => { + if (startTimeout !== null) { + clearTimeout(startTimeout) + } + } + }, [ + delay, + startOnView, + isInView, + started, + hasSequence, + sequenceActiveIndex, + sequenceStarted, + itemIndex, + ]) + + useEffect(() => { + let typingEffect: ReturnType | null = null + + if (started) { + let i = 0 + typingEffect = setInterval(() => { + if (i < children.length) { + setDisplayedText(children.substring(0, i + 1)) + i++ + } else { + if (typingEffect !== null) { + clearInterval(typingEffect) + } + const completeItem = sequenceCompleteItemRef.current + const currentItemIndex = sequenceItemIndexRef.current + if (completeItem && currentItemIndex !== null) { + completeItem(currentItemIndex) + } + } + }, duration) + } + + return () => { + if (typingEffect !== null) { + clearInterval(typingEffect) + } + } + }, [children, duration, started]) + + return ( + + {displayedText} + + ) +} + +interface TerminalProps { + children: React.ReactNode + className?: string + sequence?: boolean + startOnView?: boolean +} + +export const Terminal = ({ + children, + className, + sequence = true, + startOnView = true, +}: TerminalProps) => { + const containerRef = useRef(null) + const isInView = useInView(containerRef as React.RefObject, { + amount: 0.3, + once: true, + }) + + const [activeIndex, setActiveIndex] = useState(0) + const sequenceHasStarted = sequence ? !startOnView || isInView : false + + const contextValue = useMemo(() => { + if (!sequence) return null + return { + completeItem: (index: number) => { + setActiveIndex((current) => (index === current ? current + 1 : current)) + }, + activeIndex, + sequenceStarted: sequenceHasStarted, + } + }, [sequence, activeIndex, sequenceHasStarted]) + + const wrappedChildren = useMemo(() => { + if (!sequence) return children + const array = Children.toArray(children) + return array.map((child, index) => ( + + {child as React.ReactNode} + + )) + }, [children, sequence]) + + const content = ( +
+
+
+
+
+
+
+
+
+        {wrappedChildren}
+      
+
+ ) + + if (!sequence) return content + + return ( + + {content} + + ) +} diff --git a/src/components/ui/tracing-beam.tsx b/src/components/ui/tracing-beam.tsx new file mode 100644 index 0000000..1b94e66 --- /dev/null +++ b/src/components/ui/tracing-beam.tsx @@ -0,0 +1,125 @@ +"use client"; +import React, { useEffect, useRef, useState } from "react"; +import { + motion, + useTransform, + useScroll, + useSpring, +} from "motion/react"; +import { cn } from "@/lib/utils"; + +export const TracingBeam = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + const ref = useRef(null); + const { scrollYProgress } = useScroll({ + target: ref, + offset: ["start start", "end start"], + }); + + const contentRef = useRef(null); + const [svgHeight, setSvgHeight] = useState(0); + + useEffect(() => { + if (contentRef.current) { + setSvgHeight(contentRef.current.offsetHeight); + } + }, []); + + const y1 = useSpring( + useTransform(scrollYProgress, [0, 0.8], [50, svgHeight]), + { + stiffness: 500, + damping: 90, + }, + ); + const y2 = useSpring( + useTransform(scrollYProgress, [0, 1], [50, svgHeight - 200]), + { + stiffness: 500, + damping: 90, + }, + ); + + return ( + +
+ 0 + ? "none" + : "rgba(0, 0, 0, 0.24) 0px 3px 8px", + }} + className="border-netural-200 ml-[27px] flex h-4 w-4 items-center justify-center rounded-full border shadow-sm" + > + 0 ? "white" : "#10b981", + borderColor: scrollYProgress.get() > 0 ? "white" : "#059669", + }} + className="h-2 w-2 rounded-full border border-neutral-300 bg-white" + /> + + +
+
{children}
+
+ ); +}; From e8a7e09ede9820165190abaffdaacf2611d05e70 Mon Sep 17 00:00:00 2001 From: Vasanthdev2004 Date: Mon, 17 Aug 2026 22:00:46 +0530 Subject: [PATCH 04/22] feat(theme): rebuild the token system on gitlawb.com's identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Colours are extracted from the live gitlawb.com rather than approximated: pure #000 ground, pure #fff text, Geist Mono throughout. The over-scoped theme customizer is removed in favour of a single dark/light toggle. Notable fixes folded in here, each of which broke something silently: - The `*` border-color reset sat outside any cascade layer, so it beat every layered border-color utility regardless of specificity. `border-success`, `border-danger`, and `border-transparent` all painted the same faint white. Moved into @layer base. - Syntax-highlight .tok-* colours existed only for dark, leaving near-white glyphs on the light ground. Both sets now defined. - The light palette had been mirrored from dark, but alpha compositing is not symmetric: black at 45% over white is 1.75:1 where white at 45% over black is 10:1. The light ramp is retuned, and --success moved to #00712f, which clears 4.5:1 on both the page and its own tint. - AuroraText and AnimatedGradientText painted through background-clip with a hardcoded ramp, so they rendered invisible in light mode with nothing for a contrast check to see. The ramp is now themed. Radius: rules and list hairlines stay square, controls take a full round to match the search field, and floating panels take 12px. useIsDark subscribes to the class on via useSyncExternalStore, because canvas components (the globe, the icon cloud) bake colours into textures and image URLs and must re-render on a theme flip. useTheme cannot serve that — it holds its own state per call site, so a second caller never hears the toggle. useTheme also forces a full style recalculation after the flip. Chromium does not reliably invalidate `color` on existing elements when an inherited custom property changes on the root: measured after one toggle, 76 of 236 .text-muted elements kept the previous theme's colour while a freshly created element with the same class in the same parent resolved correctly. --- index.html | 30 +- src/components/ui/ThemeCustomizer.tsx | 400 ------------------ src/components/ui/ThemeToggle.tsx | 39 +- src/hooks/useIsDark.ts | 26 ++ src/hooks/useTheme.ts | 40 +- src/index.css | 567 +++++++++++++++++++++++--- 6 files changed, 634 insertions(+), 468 deletions(-) delete mode 100644 src/components/ui/ThemeCustomizer.tsx create mode 100644 src/hooks/useIsDark.ts diff --git a/index.html b/index.html index c548a52..fda2eb2 100644 --- a/index.html +++ b/index.html @@ -25,12 +25,16 @@ - - + + +
diff --git a/src/components/ui/ThemeCustomizer.tsx b/src/components/ui/ThemeCustomizer.tsx deleted file mode 100644 index ada781a..0000000 --- a/src/components/ui/ThemeCustomizer.tsx +++ /dev/null @@ -1,400 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { Palette } from 'lucide-react'; -import { Button, Popover, PopoverTrigger, PopoverContent } from '@heroui/react'; -import { cn } from '../../lib/utils'; - -type Accent = - | 'purple' - | 'blue' - | 'indigo' - | 'teal' - | 'green' - | 'orange' - | 'pink' - | 'red' - | 'amber' - | 'violet'; - -type Base = 'neutral' | 'slate' | 'warm' | 'olive' | 'sand'; - -type Font = - | 'jetbrains-mono' - | 'inter' - | 'ibm-plex-mono' - | 'fira-code' - | 'geist' - | 'space-grotesk' - | 'source-code-pro' - | 'dm-mono' - | 'jakarta-sans' - | 'onest'; - -type Rad = 'none' | 'sm' | 'md' | 'lg'; - -interface ThemeSettings { - accent: Accent; - base: Base; - font: Font; - radius: Rad; -} - -interface AccentDef { - oklch: string; - oklchForeground: string; - css: string; - cssForeground: string; -} - -const ACCENTS: Record = { - purple: { - oklch: 'oklch(78% 0.16 295.38)', - oklchForeground: 'oklch(15% 0.03 295.38)', - css: 'hsl(295.38, 100%, 70%)', - cssForeground: 'hsl(295.38, 100%, 5%)', - }, - blue: { - oklch: 'oklch(70% 0.18 260)', - oklchForeground: 'oklch(15% 0.03 260)', - css: 'hsl(220, 100%, 60%)', - cssForeground: 'hsl(220, 100%, 5%)', - }, - indigo: { - oklch: 'oklch(65% 0.19 275)', - oklchForeground: 'oklch(15% 0.03 275)', - css: 'hsl(245, 90%, 60%)', - cssForeground: 'hsl(245, 90%, 5%)', - }, - teal: { - oklch: 'oklch(75% 0.14 195)', - oklchForeground: 'oklch(15% 0.03 195)', - css: 'hsl(180, 80%, 50%)', - cssForeground: 'hsl(180, 80%, 5%)', - }, - green: { - oklch: 'oklch(73% 0.19 155.8)', - oklchForeground: 'oklch(15% 0.03 155.8)', - css: 'hsl(150, 80%, 50%)', - cssForeground: 'hsl(150, 80%, 5%)', - }, - orange: { - oklch: 'oklch(78% 0.159 77.32)', - oklchForeground: 'oklch(15% 0.03 77.32)', - css: 'hsl(30, 100%, 55%)', - cssForeground: 'hsl(30, 100%, 5%)', - }, - pink: { - oklch: 'oklch(75% 0.18 340)', - oklchForeground: 'oklch(15% 0.03 340)', - css: 'hsl(330, 100%, 65%)', - cssForeground: 'hsl(330, 100%, 5%)', - }, - red: { - oklch: 'oklch(65% 0.23 25.74)', - oklchForeground: 'oklch(99% 0 0)', - css: 'hsl(0, 85%, 55%)', - cssForeground: 'hsl(0, 85%, 95%)', - }, - amber: { - oklch: 'oklch(82% 0.14 82)', - oklchForeground: 'oklch(15% 0.03 82)', - css: 'hsl(45, 100%, 55%)', - cssForeground: 'hsl(45, 100%, 5%)', - }, - violet: { - oklch: 'oklch(68% 0.2 305)', - oklchForeground: 'oklch(99% 0 0)', - css: 'hsl(280, 95%, 60%)', - cssForeground: 'hsl(280, 95%, 95%)', - }, -}; - -const ACCENT_DISPLAY: Record = { - purple: '#b87ce0', - blue: '#5898f0', - indigo: '#6366f1', - teal: '#2dd4bf', - green: '#4ade80', - orange: '#fb923c', - pink: '#f472b6', - red: '#ef4444', - amber: '#fbbf24', - violet: '#a78bfa', -}; - -type BaseDef = { hue: number; label: string }; - -const BASES: Record = { - neutral: { hue: 285.823, label: '#8c8c9e' }, - slate: { hue: 260, label: '#7878a0' }, - warm: { hue: 70, label: '#9e9e78' }, - olive: { hue: 120, label: '#8c9e8c' }, - sand: { hue: 45, label: '#9e948c' }, -}; - -interface FontDef { - family: string; - googleFonts: string; - category: 'sans' | 'mono'; - weights: string; -} - -const FONTS: Record = { - 'jetbrains-mono': { - family: "'JetBrains Mono', monospace", - googleFonts: 'JetBrains+Mono:wght@400;500;600;700;800', - category: 'mono', - weights: '400,500,600,700,800', - }, - 'inter': { - family: "'Inter', system-ui, sans-serif", - googleFonts: 'Inter:wght@400;500;600;700;800', - category: 'sans', - weights: '400,500,600,700,800', - }, - 'ibm-plex-mono': { - family: "'IBM Plex Mono', monospace", - googleFonts: 'IBM+Plex+Mono:wght@400;500;600;700', - category: 'mono', - weights: '400,500,600,700', - }, - 'fira-code': { - family: "'Fira Code', monospace", - googleFonts: 'Fira+Code:wght@400;500;600;700', - category: 'mono', - weights: '400,500,600,700', - }, - 'geist': { - family: "'Geist', sans-serif", - googleFonts: 'Geist:wght@400;500;600;700;800', - category: 'sans', - weights: '400,500,600,700,800', - }, - 'space-grotesk': { - family: "'Space Grotesk', sans-serif", - googleFonts: 'Space+Grotesk:wght@400;500;600;700', - category: 'sans', - weights: '400,500,600,700', - }, - 'source-code-pro': { - family: "'Source Code Pro', monospace", - googleFonts: 'Source+Code+Pro:wght@400;500;600;700;800', - category: 'mono', - weights: '400,500,600,700,800', - }, - 'dm-mono': { - family: "'DM Mono', monospace", - googleFonts: 'DM+Mono:wght@400;500', - category: 'mono', - weights: '400,500', - }, - 'jakarta-sans': { - family: "'Plus Jakarta Sans', sans-serif", - googleFonts: 'Plus+Jakarta+Sans:wght@400;500;600;700;800', - category: 'sans', - weights: '400,500,600,700,800', - }, - 'onest': { - family: "'Onest', sans-serif", - googleFonts: 'Onest:wght@400;500;600;700;800', - category: 'sans', - weights: '400,500,600,700,800', - }, -}; - -const RADIUS: Record = { - none: '0px', - sm: '0.25rem', - md: '0.5rem', - lg: '0.75rem', -}; - -const STORAGE_KEY = 'nxe-theme'; - -const OLD_FONT_MAP: Record = { - mono: 'jetbrains-mono', - sans: 'inter', -}; - -function load(): ThemeSettings { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (raw) { - const parsed: Record = JSON.parse(raw); - const a = parsed.accent as string; - const b = parsed.base as string; - const f = parsed.font as string; - const r = parsed.radius as string; - return { - accent: ACCENTS[a as Accent] ? (a as Accent) : 'purple', - base: BASES[b as Base] ? (b as Base) : 'neutral', - font: FONTS[f as Font] - ? (f as Font) - : OLD_FONT_MAP[f] && FONTS[OLD_FONT_MAP[f]] - ? OLD_FONT_MAP[f] - : 'jetbrains-mono', - radius: RADIUS[r as Rad] ? (r as Rad) : 'md', - }; - } - } catch { /* ignore */ } - return { accent: 'purple', base: 'neutral', font: 'jetbrains-mono', radius: 'md' }; -} - -const loadedFonts = new Set(); - -function loadFont(font: Font) { - const def = FONTS[font]; - if (loadedFonts.has(def.googleFonts)) return; - loadedFonts.add(def.googleFonts); - - const existing = document.querySelector('#nxe-font-link'); - if (existing) { - const current = existing.href; - const next = `https://fonts.googleapis.com/css2?family=${def.googleFonts}&display=swap`; - if (current !== next) { - existing.href = next; - } - } -} - -function apply(settings: ThemeSettings) { - const isDark = document.documentElement.classList.contains('dark'); - const root = document.documentElement.style; - - const accentDef = ACCENTS[settings.accent]; - const accent = isDark ? accentDef.oklch : accentDef.oklch; - const accentFg = isDark ? accentDef.oklchForeground : accentDef.oklchForeground; - - root.setProperty('--accent', accent); - root.setProperty('--accent-foreground', accentFg); - root.setProperty('--focus', accent); - root.setProperty('--link', accent); - - const baseDef = BASES[settings.base]; - const bgLight = `oklch(97.02% 0.0015 ${baseDef.hue})`; - const bgDark = `oklch(12% 0.005 ${baseDef.hue})`; - root.setProperty('--background', isDark ? bgDark : bgLight); - - const r = RADIUS[settings.radius]; - root.setProperty('--radius', r); - root.setProperty('--field-radius', settings.radius === 'none' ? '0px' : `calc(${r} * 1.5)`); - - loadFont(settings.font); - root.setProperty('--font-sans', FONTS[settings.font].family); - - localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); -} - -const accentKeys: Accent[] = Object.keys(ACCENTS) as Accent[]; -const baseKeys: Base[] = Object.keys(BASES) as Base[]; -const fontKeys: Font[] = Object.keys(FONTS) as Font[]; -const radiusKeys: Rad[] = ['none', 'sm', 'md', 'lg']; - -export function ThemeCustomizer() { - const [settings, setSettings] = useState(load); - useEffect(() => { - apply(settings); - }, [settings]); - - useEffect(() => { - const mo = new MutationObserver(() => apply(settings)); - mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); - return () => mo.disconnect(); - }, [settings]); - - useEffect(() => { - if (!document.querySelector('#nxe-font-link')) { - const link = document.createElement('link'); - link.id = 'nxe-font-link'; - link.rel = 'stylesheet'; - link.href = `https://fonts.googleapis.com/css2?family=${FONTS[settings.font].googleFonts}&display=swap`; - document.head.appendChild(link); - } - }, []); - - const update = useCallback((partial: Partial) => { - setSettings(s => ({ ...s, ...partial })); - }, []); - - return ( - - - - - -
-
- accent -
- {accentKeys.map(key => ( -
-
- -
- base -
- {baseKeys.map(key => ( -
-
- -
- font -
- {fontKeys.map(key => ( - - ))} -
-
- -
- radius -
- {radiusKeys.map(key => ( - - ))} -
-
-
-
-
- ); -} diff --git a/src/components/ui/ThemeToggle.tsx b/src/components/ui/ThemeToggle.tsx index b534dde..8fdcf4f 100644 --- a/src/components/ui/ThemeToggle.tsx +++ b/src/components/ui/ThemeToggle.tsx @@ -1,19 +1,36 @@ -import { Button } from '@heroui/react'; -import { Sun, Moon } from 'lucide-react'; +import { AnimatedThemeToggler } from './animated-theme-toggler'; import { useTheme } from '../../hooks/useTheme'; +/** + * Theme switch. + * + * Magic UI's AnimatedThemeToggler drives the visual: it wipes the new theme in + * over the old one with the View Transitions API, and falls back to an instant + * swap where that API is missing. + * + * It runs controlled — the toggler flips the `dark` class synchronously so the + * transition snapshots the new theme, then hands the value back here, and + * useTheme stays the single owner of `data-theme` and the stored preference. + * Letting it manage its own state would have it write the pre-redesign + * `theme` key, which is exactly the stale value we retired. + */ export function ThemeToggle() { - const { theme, toggle } = useTheme(); + const { theme, setTheme } = useTheme(); return ( - + // No border and no box. `border border-transparent` does not work here: + // the global `*` reset sets border-color, so a transparent border still + // paints as a 1px frame. The icon is sized down from the component's + // 24px default, which was cramped inside a 32px hit area. + className="inline-flex h-8 w-8 items-center justify-center + text-muted transition-colors hover:text-foreground + [&_svg]:size-[17px]" + /> ); } diff --git a/src/hooks/useIsDark.ts b/src/hooks/useIsDark.ts new file mode 100644 index 0000000..cc9a6f4 --- /dev/null +++ b/src/hooks/useIsDark.ts @@ -0,0 +1,26 @@ +import { useSyncExternalStore } from 'react'; + +/** + * Whether the dark theme is currently applied. + * + * Canvas-based components (the globe, the icon cloud) bake colours into + * textures and image URLs, so they need to re-render when the theme flips. + * `useTheme` cannot serve that: it holds its own useState per call site, so a + * second caller would track its own copy and never hear about the toggle. + * This subscribes to the class on , which is the single source of truth + * every theme path already writes to. + */ +function subscribe(onChange: () => void): () => void { + const observer = new MutationObserver(onChange); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'], + }); + return () => observer.disconnect(); +} + +const getSnapshot = () => document.documentElement.classList.contains('dark'); + +export function useIsDark(): boolean { + return useSyncExternalStore(subscribe, getSnapshot, () => true); +} diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts index 45a37f4..b62499d 100644 --- a/src/hooks/useTheme.ts +++ b/src/hooks/useTheme.ts @@ -2,12 +2,23 @@ import { useState, useEffect } from 'react'; type Theme = 'dark' | 'light'; +/** + * The stored preference is versioned. + * + * The previous design defaulted to dark and persisted `theme: "dark"` for every + * visitor. This design is printed on light stock, with dark as the counterfoil. + * Reading the old key would hand every returning visitor the counterfoil and + * hide the redesign from exactly the people who had seen the old one — so the + * legacy value is deliberately not inherited. A visitor who wants dark picks it + * again once, and that choice is stored under the new key. + */ +const KEY = 'gl-theme'; + function getInitialTheme(): Theme { - if (typeof window === 'undefined') return 'dark'; - const stored = localStorage.getItem('theme') as Theme | null; + if (typeof window === 'undefined') return 'light'; + const stored = localStorage.getItem(KEY) as Theme | null; if (stored === 'dark' || stored === 'light') return stored; - // Dark is the default theme (matches the anti-FOUC script in index.html). - return 'dark'; + return 'light'; } export function useTheme() { @@ -17,10 +28,27 @@ export function useTheme() { const root = document.documentElement; root.classList.toggle('dark', theme === 'dark'); root.setAttribute('data-theme', theme); - localStorage.setItem('theme', theme); + localStorage.setItem(KEY, theme); + // Retire the pre-redesign key so it cannot resurface later. + localStorage.removeItem('theme'); + + // Force a full style recalculation. + // + // Every themed colour resolves through an inherited custom property + // (`.text-muted { color: var(--fg-muted) }`), and Chromium does not + // reliably invalidate `color` on elements that already exist when such a + // property changes on the root. Measured after a toggle: 76 of 236 + // `.text-muted` elements kept the previous theme's colour — white text on + // the white ground — while a freshly created element with the same class + // in the same parent resolved correctly. Detaching and reattaching the + // document element forces the engine to recompute the whole tree. + const display = root.style.display; + root.style.display = 'none'; + void root.offsetHeight; + root.style.display = display; }, [theme]); const toggle = () => setTheme(t => (t === 'dark' ? 'light' : 'dark')); - return { theme, toggle }; + return { theme, toggle, setTheme }; } diff --git a/src/index.css b/src/index.css index e34dfaa..7ef666b 100644 --- a/src/index.css +++ b/src/index.css @@ -1,11 +1,323 @@ @import "tailwindcss"; -@import "@heroui/styles"; +/* Enter/exit utilities (animate-in, fade-in-0, zoom-in-95, slide-in-from-*). + Radix drives them from data-[state] on the popup; without this import those + class names compile to nothing and the menu appears with no transition. */ +@import "tw-animate-css"; @custom-variant dark (&:where(.dark, .dark *)); -*, *::before, *::after { - box-sizing: border-box; - border-color: var(--color-border); +/* ───────────────────────────────────────────────────────────────────────── + gitlawb.com's identity, applied. + + Extracted from the live site rather than approximated: pure #000 ground, + pure #fff text, Geist Mono throughout, zero border radius. Hierarchy is + built from WHITE AT ALPHA (0.8 / 0.7 / 0.55 / 0.45 / 0.3 / 0.2 / 0.1), not + from a grey ramp — that is what keeps the surface reading as monochrome + rather than as a dark theme with grey furniture. + + There is no brand accent: links on gitlawb.com are plain white. The single + colour in the system is #05df72, and it is reserved strictly for liveness — + the site uses it on "live" chips and nowhere else. Spending it on anything + that is not a live signal breaks the one meaning it carries. + + Dark is primary here because the source identity is a black site; light is + its exact inversion, fully specified. + ───────────────────────────────────────────────────────────────────────── */ + +:root, +[data-theme="light"] { + --canvas: #ffffff; + --canvas-inset: #f7f7f7; + --canvas-subtle: #fafafa; + --canvas-raised: #ffffff; + + --fg: #000000; + --fg-muted: rgba(0, 0, 0, 0.82); /* 4.57:1 on white */ + --fg-subtle: rgba(0, 0, 0, 0.62); /* 2.44:1 — decoration only */ + + --line: rgba(0, 0, 0, 0.2); + --line-muted: rgba(0, 0, 0, 0.1); + + /* Monochrome: the "accent" is the foreground itself. */ + --accent: #000000; + --accent-hover: rgba(0, 0, 0, 0.7); + --accent-fg: #ffffff; + --accent-subtle: rgba(0, 0, 0, 0.06); + + --success: #00712f; /* 6.17:1 on white, 5.4:1 on the /10 tint the status + chips sit on. #00a63e read 3.22:1 and failed both. */ + --danger: #e7000b; + --attention: #a65f00; + --done: #7008e7; + + --primary: #000000; + --primary-foreground: #ffffff; + + /* Gradient ramp for text painted via background-clip. Must invert with the + theme: a hardcoded ramp renders white-on-white in light mode, with the + text fully invisible and no error anywhere. */ + --ramp-1: #000000; + --ramp-2: rgba(0, 0, 0, 0.80); + --ramp-3: #000000; + --ramp-4: rgba(0, 0, 0, 0.72); /* lightest stop still 3.18:1 for display text */ + + /* Radius. + The identity draws its rules and list hairlines square, and those stay + square — rounding a table row or a section rule looks wrong. Controls are + the exception: a control at zero radius reads as a hard box, and this page + carries a lot of them side by side. `--radius-control` is a full round, to + match the search field, which is the one control that always had it. + Panels that float above the page take `--radius`. */ + --radius: 12px; + --radius-sm: 8px; + --radius-control: 9999px; +} + +.dark, +[data-theme="dark"] { + --canvas: #000000; + --canvas-inset: #000000; + --canvas-subtle: rgba(255, 255, 255, 0.04); + --canvas-raised: #000000; + + --fg: #ffffff; + --fg-muted: rgba(255, 255, 255, 0.7); + --fg-subtle: rgba(255, 255, 255, 0.45); + + --line: rgba(255, 255, 255, 0.2); + --line-muted: rgba(255, 255, 255, 0.1); + + --accent: #ffffff; + --accent-hover: rgba(255, 255, 255, 0.7); + --accent-fg: #000000; + --accent-subtle: rgba(255, 255, 255, 0.08); + + /* The one colour in the system, and only for liveness. */ + --success: #05df72; + --danger: #ff6467; + --attention: #ffb900; + --done: #a684ff; + + --primary: #ffffff; + --primary-foreground: #000000; + + --ramp-1: #ffffff; + --ramp-2: rgba(255, 255, 255, 0.55); + --ramp-3: #ffffff; + --ramp-4: rgba(255, 255, 255, 0.35); +} + +@theme inline { + --color-background: var(--canvas); + --color-canvas-inset: var(--canvas-inset); + --color-surface: var(--canvas-raised); + --color-surface-secondary: var(--canvas-subtle); + --color-surface-tertiary: var(--canvas-inset); + + --color-foreground: var(--fg); + --color-muted: var(--fg-muted); + --color-subtle: var(--fg-subtle); + + --color-border: var(--line); + --color-separator: var(--line-muted); + --color-rule: var(--line); + + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-fg); + --color-accent-subtle: var(--accent-subtle); + + --color-success: var(--success); + --color-danger: var(--danger); + --color-warning: var(--attention); + --color-attention: var(--attention); + --color-done: var(--done); + + /* shadcn base tokens. + Registry components (Magic UI, Aceternity, shadcn's own button) are written + against this vocabulary — bg-primary, text-primary-foreground, ring-ring, + border-input. Without them those utilities resolve to nothing and fail + silently: an unstyled colour is not an error, it is just invisible text on + an invisible fill. Mapped onto this project's palette. */ + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--canvas-subtle); + --color-secondary-foreground: var(--fg); + --color-destructive: var(--danger); + --color-destructive-foreground: #ffffff; + --color-muted-foreground: var(--fg-muted); + --color-card: var(--canvas-raised); + --color-card-foreground: var(--fg); + --color-popover: var(--canvas-raised); + --color-popover-foreground: var(--fg); + --color-input: var(--line); + --color-ring: var(--accent); + + /* Retired aesthetic tokens, mapped onto the conventional palette so no + surface is left unstyled while components migrate. */ + --color-guilloche: var(--accent); + --color-seal: var(--danger); + --color-void: var(--done); + --color-plate: var(--canvas-raised); + --color-stock: var(--canvas); + --color-intaglio: var(--fg); + + /* gitlawb.com sets Geist Mono as the interface face, not just the code face. + Keeping that is most of what makes the two sites read as one product. */ + --font-sans: "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, + Consolas, monospace; + --font-display: "Geist Mono", ui-monospace, monospace; + --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, + Consolas, monospace; + --font-prose: "Geist", ui-sans-serif, system-ui, sans-serif; + --animate-shiny-text: shiny-text 8s infinite; + @keyframes shiny-text { + 0%, 90%, 100% { + background-position: calc(-100% - var(--shiny-width)) 0; + } + 30%, 60% { + background-position: calc(100% + var(--shiny-width)) 0; + } + } + --animate-shimmer-slide: shimmer-slide var(--speed) ease-in-out infinite alternate; + --animate-spin-around: spin-around calc(var(--speed) * 2) infinite linear +; + @keyframes shimmer-slide { + to { + transform: translate(calc(100cqw - 100%), 0); + } + } + @keyframes spin-around { + 0% { + transform: translateZ(0) rotate(0); + } + 15%, 35% { + transform: translateZ(0) rotate(90deg); + } + 65%, 85% { + transform: translateZ(0) rotate(270deg); + } + 100% { + transform: translateZ(0) rotate(360deg); + } + } + --animate-aurora: aurora 8s ease-in-out infinite alternate; + @keyframes aurora { + 0% { + background-position: 0% 50%; + transform: rotate(-5deg) scale(0.9); + } + 25% { + background-position: 50% 100%; + transform: rotate(5deg) scale(1.1); + } + 50% { + background-position: 100% 50%; + transform: rotate(-3deg) scale(0.95); + } + 75% { + background-position: 50% 0%; + transform: rotate(3deg) scale(1.05); + } + 100% { + background-position: 0% 50%; + transform: rotate(-5deg) scale(0.9); + } + } + --animate-ripple: ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite; + @keyframes ripple { + 0%, 100% { + transform: translate(-50%, -50%) scale(1); + } + 50% { + transform: translate(-50%, -50%) scale(0.9); + } + } + --animate-marquee: marquee var(--duration) infinite linear; + --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite; + @keyframes marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(calc(-100% - var(--gap))); + } + } + @keyframes marquee-vertical { + from { + transform: translateY(0); + } + to { + transform: translateY(calc(-100% - var(--gap))); + } + } + --animate-gradient: gradient 8s linear infinite; + @keyframes gradient { + to { + background-position: var(--bg-size, 300%) 0; + } + } + --animate-pulse: pulse var(--duration) ease-out infinite; + --animate-pulse-ripple: pulse-ripple var(--duration) cubic-bezier(0.16, 1, 0.3, 1) infinite; + @keyframes pulse { + 0%, 100% { + box-shadow: 0 0 0 0 var(--pulse-color, oklch(from var(--bg) l c h / 0.5)); + } + 50% { + box-shadow: 0 0 0 var(--distance) var(--pulse-color, oklch(from var(--bg) l c h / 0.5)); + } + } + @keyframes pulse-ripple { + 0% { + box-shadow: 0 0 0 0 oklch(from var(--pulse-color, var(--bg)) l c h / 1); + } + 100% { + box-shadow: 0 0 0 var(--distance) oklch(from var(--pulse-color, var(--bg)) l c h / 0); + } + } + --animate-rippling: rippling var(--duration) ease-out; + @keyframes rippling { + 0% { + opacity: 1; + } + 100% { + transform: scale(2); + opacity: 0; + } + } + --animate-rainbow: rainbow var(--speed, 2s) infinite linear; + --color-color-5: var(--color-5); + --color-color-4: var(--color-4); + --color-color-3: var(--color-3); + --color-color-2: var(--color-2); + --color-color-1: var(--color-1); + @keyframes rainbow { + 0% { + background-position: 0%; + } + 100% { + background-position: 200%; + } + } + --animate-orbit: orbit calc(var(--duration)*1s) linear infinite; + @keyframes orbit { + 0% { + transform: rotate(calc(var(--angle) * 1deg)) translateY(calc(var(--radius) * 1px)) rotate(calc(var(--angle) * -1deg)); + } + 100% { + transform: rotate(calc(var(--angle) * 1deg + 360deg)) translateY(calc(var(--radius) * 1px)) rotate(calc((var(--angle) * -1deg) - 360deg)); + } + }} + +/* This reset MUST live in @layer base. Unlayered CSS wins over layered CSS in + the cascade regardless of specificity, so an unlayered `*` rule silently + overrode every border-color utility Tailwind emits — border-success, + border-danger and even border-transparent all resolved to the default + border colour, with no error anywhere. */ +@layer base { + *, *::before, *::after { + box-sizing: border-box; + border-color: var(--color-border); + } } html, body { @@ -16,101 +328,262 @@ html, body { html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - text-rendering: optimizeLegibility; } body { background-color: var(--color-background); color: var(--color-foreground); - font-family: var(--font-sans, 'JetBrains Mono', monospace); + font-family: var(--font-sans); + font-size: 13.5px; + line-height: 1.6; + letter-spacing: -0.01em; } #root { min-height: 100vh; } +/* ── Browser surfaces ─────────────────────────────────────────────────── */ + ::selection { - background: color-mix(in oklch, var(--color-accent) 28%, transparent); + background: color-mix(in oklab, var(--color-accent) 32%, transparent); } +:root { + caret-color: var(--color-accent); + accent-color: var(--color-accent); + scrollbar-color: var(--color-border) transparent; + scrollbar-width: thin; + color-scheme: light; + /* Monochrome sweep — gitlawb.com has no multi-hue gradient anywhere. */ + --color-1: rgba(0, 0, 0, 0.85); + --color-2: rgba(0, 0, 0, 0.35); + --color-3: rgba(0, 0, 0, 0.65); + --color-4: rgba(0, 0, 0, 0.15); + --color-5: rgba(0, 0, 0, 0.5); +} +.dark { + color-scheme: dark; + --color-1: rgba(255, 255, 255, 0.9); + --color-2: rgba(255, 255, 255, 0.35); + --color-3: rgba(255, 255, 255, 0.7); + --color-4: rgba(255, 255, 255, 0.15); + --color-5: rgba(255, 255, 255, 0.55); +} + +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { + background: var(--color-border); + border: 2px solid var(--color-background); +} +::-webkit-scrollbar-thumb:hover { background: var(--color-muted); } + +:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: -1px; +} + +a { text-underline-offset: 0.2em; } + +.tabular { font-variant-numeric: tabular-nums; } + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* Section caption used above dense field groups. Not an eyebrow: it labels a + data region, the way a column header labels a column. */ +.section-label { + font-size: 12px; + font-weight: 600; + color: var(--color-muted); +} + +/* ── Markdown ───────────────────────────────────────────────────────────── + README/doc body copy, set to forge conventions. */ + .markdown-gl { color: var(--color-foreground); - font-size: 14px; - line-height: 1.7; + font-family: var(--font-prose); + font-size: 15px; + line-height: 1.6; word-wrap: break-word; } -.markdown-gl h1, .markdown-gl h2, .markdown-gl h3, .markdown-gl h4 { +.markdown-gl h1, .markdown-gl h2, .markdown-gl h3, +.markdown-gl h4, .markdown-gl h5, .markdown-gl h6 { margin-top: 24px; - margin-bottom: 12px; - font-weight: 700; - line-height: 1.3; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; color: var(--color-foreground); } -.markdown-gl h1 { font-size: 1.6em; border-bottom: 1px solid var(--color-separator); padding-bottom: 8px; } -.markdown-gl h2 { font-size: 1.3em; border-bottom: 1px solid var(--color-separator); padding-bottom: 6px; } -.markdown-gl h3 { font-size: 1.1em; } -.markdown-gl p, .markdown-gl ul, .markdown-gl ol, .markdown-gl dl, .markdown-gl blockquote, -.markdown-gl pre, .markdown-gl table, .markdown-gl hr { margin-top: 0; margin-bottom: 16px; } +.markdown-gl h1 { font-size: 2em; border-bottom: 1px solid var(--color-border); padding-bottom: 0.3em; } +.markdown-gl h2 { font-size: 1.5em; border-bottom: 1px solid var(--color-border); padding-bottom: 0.3em; } +.markdown-gl h3 { font-size: 1.25em; } +.markdown-gl h4 { font-size: 1em; } +.markdown-gl p, .markdown-gl ul, .markdown-gl ol, .markdown-gl dl, +.markdown-gl blockquote, .markdown-gl pre, .markdown-gl table, .markdown-gl hr { + margin-top: 0; + margin-bottom: 16px; +} .markdown-gl ul, .markdown-gl ol { padding-left: 2em; } .markdown-gl li { margin-top: 0.25em; } .markdown-gl li > p { margin-bottom: 0; } .markdown-gl a { color: var(--color-accent); text-decoration: none; } .markdown-gl a:hover { text-decoration: underline; } .markdown-gl code { - padding: 2px 6px; - border-radius: 4px; - font-size: 0.875em; - background: var(--color-surface-secondary); - color: var(--color-accent); + padding: 0.2em 0.4em; + margin: 0; + font-family: var(--font-mono); + font-size: 85%; + white-space: break-spaces; + background: color-mix(in oklab, var(--color-muted) 18%, transparent); + border-radius: var(--radius-sm); } .markdown-gl pre { padding: 16px; - border-radius: 6px; - overflow-x: auto; - background: var(--color-surface); - border: 1px solid var(--color-border); + overflow: auto; + font-size: 85%; + line-height: 1.45; + background: var(--color-surface-secondary); + border: 1px solid var(--color-separator); + border-radius: var(--radius); } .markdown-gl pre code { padding: 0; background: transparent; + border: 0; + font-size: 100%; + white-space: pre; +} + +/* ── Documentation fences ───────────────────────────────────────────────── + DocArticle wraps each fence in a
carrying a header that names the + language and offers a copy button. The header and the code share one frame, + so the fence reads as a single object rather than a bar sitting on a box. */ +.doc-fence { + margin: 0 0 16px; + border: 1px solid var(--color-separator); + border-radius: var(--radius); + overflow: hidden; + background: var(--color-surface-secondary); +} +.doc-fence-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 6px 8px 6px 14px; + border-bottom: 1px solid var(--color-separator); +} +.doc-fence-lang { + font-size: 11.5px; + letter-spacing: 0.02em; + color: var(--color-muted); +} +.doc-fence-copy { + padding: 3px 10px; + border: 0; + border-radius: var(--radius-control); + background: transparent; + font: inherit; + font-size: 11.5px; + color: var(--color-muted); + cursor: pointer; + transition: color 120ms, background-color 120ms; +} +.doc-fence-copy:hover { color: var(--color-foreground); + background: color-mix(in oklab, var(--color-muted) 14%, transparent); +} +.doc-fence-copy:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 1px; +} +/* The fence inside the figure has already been framed by the figure. */ +.markdown-gl .doc-fence pre { + margin: 0; + border: 0; border-radius: 0; + background: transparent; } + +/* ── Heading anchors ────────────────────────────────────────────────────── + Shown on hover or keyboard focus; always reachable, never decorative + clutter while reading. */ +.doc-anchor { + margin-left: 0.4em; + color: var(--color-muted); + text-decoration: none; + opacity: 0; + transition: opacity 120ms; +} +.markdown-gl h2:hover .doc-anchor, +.markdown-gl h3:hover .doc-anchor, +.doc-anchor:focus-visible { + opacity: 1; +} +.doc-anchor:hover { color: var(--color-accent); } .markdown-gl blockquote { padding: 0 1em; - border-left: 3px solid var(--color-separator); color: var(--color-muted); + border-left: 0.25em solid var(--color-border); } .markdown-gl table { display: block; - overflow-x: auto; + width: max-content; + max-width: 100%; + overflow: auto; border-collapse: collapse; - width: 100%; } .markdown-gl table th, .markdown-gl table td { - padding: 8px 12px; + padding: 6px 13px; border: 1px solid var(--color-border); - text-align: left; } -.markdown-gl table th { - font-weight: 600; - background: var(--color-surface-secondary); -} -.markdown-gl tr:nth-child(even) { background: var(--color-surface); } +.markdown-gl table th { font-weight: 600; background: var(--color-surface-secondary); } +.markdown-gl table tr:nth-child(2n) { background: var(--color-surface-secondary); } .markdown-gl hr { - height: 0; + height: 0.25em; + padding: 0; + margin: 24px 0; + background: var(--color-separator); border: 0; - border-top: 1px solid var(--color-separator); } .markdown-gl img { max-width: 100%; box-sizing: border-box; } .markdown-gl .task-list-item { list-style: none; } .markdown-gl .task-list-item input { margin: 0 0.2em 0.25em -1.4em; vertical-align: middle; } -.tok-kw { color: #60a5fa; } -.tok-str { color: #4ade80; } -.tok-type { color: #facc15; } -.tok-num { color: #facc15; } -.tok-cmt { color: #777778; font-style: italic; } -.tok-fn { color: #f0f0f1; } -.tok-prop { color: #d0d0d1; } -.tok-punc { color: #909091; } +/* ── Syntax tokens ──────────────────────────────────────────────────────── + Both themes specified. The pre-redesign build shipped a single hardcoded + dark set, which painted near-white glyphs onto the light canvas. */ + +.tok-kw { color: #cf222e; } +.tok-str { color: #0a3069; } +.tok-type { color: #953800; } +.tok-num { color: #0550ae; } +.tok-cmt { color: #59636e; } +.tok-fn { color: #8250df; } +.tok-prop { color: #0550ae; } +.tok-punc { color: #1f2328; } + +.dark .tok-kw { color: #ff7b72; } +.dark .tok-str { color: #a5d6ff; } +.dark .tok-type { color: #ffa657; } +.dark .tok-num { color: #79c0ff; } +.dark .tok-cmt { color: #9198a1; } +.dark .tok-fn { color: #d2a8ff; } +.dark .tok-prop { color: #79c0ff; } +.dark .tok-punc { color: #f0f6fc; } +/* Theme switch (Magic UI AnimatedThemeToggler): the component drives the + clip-path wipe itself, so the browser's default cross-fade on these + pseudo-elements is disabled to keep the two from fighting. */ +::view-transition-old(root), +::view-transition-new(root) { + animation: none; + mix-blend-mode: normal; +} \ No newline at end of file From bbf28ad0816ed7392059927d179006030410e131 Mon Sep 17 00:00:00 2001 From: Vasanthdev2004 Date: Mon, 17 Aug 2026 22:00:58 +0530 Subject: [PATCH 05/22] feat(ui): frameless control kit, and drop the native select At zero radius a bordered control reads as a hard box, and a listing page puts hundreds of them on screen at once. Buttons are frameless by default, fields are ruled with a single underline, and a frame or a fill is reserved for the control that has to stand apart. SearchField defines the one search shape every listing page uses, so there are no longer four slightly different ones. Dropdown replaces the native onChange(e.target.value)} + placeholder={placeholder} + autoComplete="off" + spellCheck="false" + className="h-10 w-full rounded-full pl-9 pr-3" + /> +
+ ); +} diff --git a/src/components/register/Tabs.tsx b/src/components/register/Tabs.tsx new file mode 100644 index 0000000..7e8292d --- /dev/null +++ b/src/components/register/Tabs.tsx @@ -0,0 +1,153 @@ +import { + createContext, + useContext, + useId, + useRef, + type ReactNode, + type KeyboardEvent, +} from 'react'; +import { cn } from '../../lib/utils'; + +/** + * Tabs, underline-indicator style. + * + * Arrow-key roving and correct tab/tabpanel wiring are kept from the kit this + * replaces. + */ + +interface TabsCtx { + selected: string; + select: (id: string) => void; + baseId: string; +} + +const Ctx = createContext(null); + +function useTabs(): TabsCtx { + const ctx = useContext(Ctx); + if (!ctx) throw new Error('Tabs subcomponent used outside '); + return ctx; +} + +interface TabsProps { + selectedKey: string; + onSelectionChange: (key: string) => void; + children: ReactNode; +} + +export function Tabs({ selectedKey, onSelectionChange, children }: TabsProps) { + const baseId = useId(); + return ( + + {children} + + ); +} + +function ListContainer({ children, className }: { children: ReactNode; className?: string }) { + return
{children}
; +} + +function List({ children, ...rest }: { children: ReactNode; 'aria-label'?: string }) { + const listRef = useRef(null); + + // Roving arrow keys across the tab strip, wrapping at both ends. + const onKeyDown = (e: KeyboardEvent) => { + if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return; + const tabs = Array.from( + listRef.current?.querySelectorAll('[role="tab"]') ?? [], + ); + const i = tabs.findIndex(t => t === document.activeElement); + if (i === -1) return; + e.preventDefault(); + const next = e.key === 'ArrowRight' ? (i + 1) % tabs.length : (i - 1 + tabs.length) % tabs.length; + tabs[next].focus(); + tabs[next].click(); + }; + + return ( +
+ {children} +
+ ); +} + +function Tab({ + id, + children, + className, +}: { + id: string; + children: ReactNode; + className?: string; +}) { + const { selected, select, baseId } = useTabs(); + const active = selected === id; + return ( + + ); +} + +/** Kept for call-site compatibility; the indicator is drawn by Tab itself. */ +function Indicator() { + return null; +} + +function Panel({ + id, + children, + className, +}: { + id: string; + children: ReactNode; + className?: string; +}) { + const { selected, baseId } = useTabs(); + if (selected !== id) return null; + return ( +
+ {children} +
+ ); +} + +Tabs.ListContainer = ListContainer; +Tabs.List = List; +Tabs.Tab = Tab; +Tabs.Indicator = Indicator; +Tabs.Panel = Panel; diff --git a/src/components/register/controls.tsx b/src/components/register/controls.tsx new file mode 100644 index 0000000..435055b --- /dev/null +++ b/src/components/register/controls.tsx @@ -0,0 +1,215 @@ +import type { + ButtonHTMLAttributes, + InputHTMLAttributes, + ReactNode, +} from 'react'; +import { cn } from '../../lib/utils'; + +/** + * Controls. + * + * Rules and list hairlines are drawn square; controls are not. A control at + * zero radius reads as a hard box, and a listing page puts many of them side by + * side, so they take `--radius-control` — a full round, matching the search + * field. Buttons stay frameless by default, and a border or a fill is reserved + * for the rare control that must stand apart. + * + * `onPress` / `isDisabled` are accepted alongside `onClick` / `disabled` so + * call sites migrating off the old kit keep working. + */ + +const focus = + 'focus:outline-none focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-[-1px]'; + +/* ── Input ────────────────────────────────────────────────────────────── */ +export interface InputProps + extends Omit, 'size'> { + variant?: string; +} + +export function Input({ className, variant: _variant, ...rest }: InputProps) { + void _variant; + return ( + + ); +} + +/* ── Button ───────────────────────────────────────────────────────────── */ +export interface ButtonProps + extends Omit, 'onClick'> { + children?: ReactNode; + onClick?: ButtonHTMLAttributes['onClick']; + onPress?: () => void | Promise; + isDisabled?: boolean; + variant?: 'primary' | 'secondary' | 'tertiary' | 'danger' | string; + size?: 'sm' | 'md' | string; + fullWidth?: boolean; + isIconOnly?: boolean; +} + +export function Button({ + children, + className, + variant = 'secondary', + size = 'md', + fullWidth, + isIconOnly, + onClick, + onPress, + isDisabled, + disabled, + type = 'button', + ...rest +}: ButtonProps) { + const off = isDisabled ?? disabled; + + // A bordered control at list density is hundreds of frames on one page, which + // is what made the surface feel boxy. The default is frameless: quiet text + // that lifts on hover. A frame is reserved for `outline`, and a fill for the + // one primary action on a view. + const tones: Record = { + primary: 'bg-foreground text-background font-medium hover:opacity-90', + secondary: 'text-muted hover:text-foreground hover:bg-surface-secondary', + tertiary: 'text-muted hover:text-foreground', + outline: 'text-foreground border border-border hover:border-foreground/40', + danger: 'text-danger hover:bg-danger hover:text-background', + }; + + return ( + + ); +} + +/* ── Select ─────────────────────────────────────────────────────────────── + Removed. The native control paints its option list as an operating-system + popup that cannot take this page's colours or its zero-radius identity, and + styling the closed control did nothing to the opened one. Use `Dropdown` + (components/ui/Dropdown.tsx), which renders the list in the page. */ + +/* ── Kbd ──────────────────────────────────────────────────────────────── */ +export function Kbd({ children, className }: { children: ReactNode; className?: string }) { + return ( + + {children} + + ); +} + +Kbd.Content = function KbdContent({ children }: { children: ReactNode }) { + return <>{children}; +}; + +/* ── Skeleton ─────────────────────────────────────────────────────────── */ +export function Skeleton({ className }: { className?: string }) { + return ( +