diff --git a/.api-test-output/github-activity_2025-12-23T17-03-23-718Z.json b/.api-test-output/github-activity_2025-12-23T17-03-23-718Z.json deleted file mode 100644 index 03325161..00000000 --- a/.api-test-output/github-activity_2025-12-23T17-03-23-718Z.json +++ /dev/null @@ -1,485 +0,0 @@ -{ - "endpoint": { - "name": "GitHub Activity", - "method": "GET", - "path": "/api/github/activity", - "url": "http://localhost:3000/api/github/activity" - }, - "metadata": { - "status": 200, - "duration": 11, - "size": 15232, - "timestamp": "2025-12-23T17:03:23.717Z" - }, - "response": [ - { - "id": "6951291471", - "timestamp": "2025-12-23T16:40:38Z", - "repository": "remcostoeten/remcostoeten.nl", - "url": "https://github.com/remcostoeten/remcostoeten.nl", - "isPrivate": false, - "type": "create", - "title": "deleted a branch", - "description": "branch: codex/update-terms-and-privacy-content-dd4c81", - "payload": { - "ref": "codex/update-terms-and-privacy-content-dd4c81", - "ref_type": "branch", - "full_ref": "refs/heads/codex/update-terms-and-privacy-content-dd4c81", - "pusher_type": "user" - } - }, - { - "id": "5472404561", - "timestamp": "2025-12-23T08:49:40Z", - "repository": "remcostoeten/remcostoeten.nl", - "url": "https://github.com/remcostoeten/remcostoeten.nl", - "isPrivate": false, - "type": "pr", - "title": "closed PR #41", - "description": "", - "payload": { - "action": "closed", - "number": 41, - "pull_request": { - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/pulls/41", - "id": 3125401074, - "number": 41, - "head": { - "ref": "codex/update-terms-and-privacy-content-dd4c81", - "sha": "d4c9e41aa833ac5b479a680fafa58de6a62a1bfe", - "repo": { - "id": 1017613680, - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl", - "name": "remcostoeten.nl" - } - }, - "base": { - "ref": "master", - "sha": "7a345e238865f31708e69c3851f06d25945ea4e3", - "repo": { - "id": 1017613680, - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl", - "name": "remcostoeten.nl" - } - } - } - } - }, - { - "id": "5469758520", - "timestamp": "2025-12-23T14:09:53Z", - "repository": "remcostoeten/remcostoeten.nl", - "url": "https://github.com/remcostoeten/remcostoeten.nl/pull/43#issuecomment-3686782829", - "isPrivate": false, - "type": "issue", - "title": "commented on PR #43", - "description": "#### 📝 Changed routes:", - "payload": { - "action": "created", - "issue": { - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/issues/43", - "repository_url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl", - "labels_url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/issues/43/labels{/name}", - "comments_url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/issues/43/comments", - "events_url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/issues/43/events", - "html_url": "https://github.com/remcostoeten/remcostoeten.nl/pull/43", - "id": 3757497687, - "node_id": "PR_kwDOPKeNcM66VgL9", - "number": 43, - "title": "Add SSR projects showcase", - "user": { - "login": "remcostoeten", - "id": 57683378, - "node_id": "MDQ6VXNlcjU3NjgzMzc4", - "avatar_url": "https://avatars.githubusercontent.com/u/57683378?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/remcostoeten", - "html_url": "https://github.com/remcostoeten", - "followers_url": "https://api.github.com/users/remcostoeten/followers", - "following_url": "https://api.github.com/users/remcostoeten/following{/other_user}", - "gists_url": "https://api.github.com/users/remcostoeten/gists{/gist_id}", - "starred_url": "https://api.github.com/users/remcostoeten/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/remcostoeten/subscriptions", - "organizations_url": "https://api.github.com/users/remcostoeten/orgs", - "repos_url": "https://api.github.com/users/remcostoeten/repos", - "events_url": "https://api.github.com/users/remcostoeten/events{/privacy}", - "received_events_url": "https://api.github.com/users/remcostoeten/received_events", - "type": "User", - "user_view_type": "public", - "site_admin": false - }, - "labels": [ - { - "id": 9875953871, - "node_id": "LA_kwDOPKeNcM8AAAACTKcYzw", - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/labels/codex", - "name": "codex", - "color": "ededed", - "default": false, - "description": null - } - ], - "state": "open", - "locked": false, - "assignee": null, - "assignees": [], - "milestone": null, - "comments": 3, - "created_at": "2025-12-23T14:06:44Z", - "updated_at": "2025-12-23T14:10:57Z", - "closed_at": null, - "active_lock_reason": null, - "draft": false, - "pull_request": { - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/pulls/43", - "html_url": "https://github.com/remcostoeten/remcostoeten.nl/pull/43", - "diff_url": "https://github.com/remcostoeten/remcostoeten.nl/pull/43.diff", - "patch_url": "https://github.com/remcostoeten/remcostoeten.nl/pull/43.patch", - "merged_at": null - }, - "body": "## Summary\n- add SSR-first projects overview and detail routes with premium dark UI, filtering, and framer-motion transitions\n- centralize project registry, metadata, and GitHub query utility with sandbox-ready media frames and structured data\n- expand sitemap entries and harden sign-in button provider guards\n\n## Testing\n- `DATABASE_URL=postgres://local/local@localhost:5432/local BETTER_AUTH_URL=http://localhost:3000 BETTER_AUTH_SECRET=12345678901234567890123456789012 npm run build`\n\n\n------\n[Codex Task](https://chatgpt.com/codex/tasks/task_e_694a562aaf90832799cbd778a3b7a492)\n\n\n\n## Summary by CodeRabbit\n\n* **New Features**\n * Added a Projects section with searchable project showcase pages featuring detailed project views, navigation between projects, and GitHub repository metadata display.\n * Added project filtering capabilities by category, status, year, and sorting options.\n * Added media preview support for projects (images and videos).\n * Added interactive sandbox preview panels for eligible projects.\n * Added project pages to the site sitemap.\n\n* **Bug Fixes**\n * Improved provider validation checks in the sign-in button component.\n\n✏️ Tip: You can customize this high-level summary in your review settings.\n\n", - "reactions": { - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/issues/43/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "timeline_url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/issues/43/timeline", - "performed_via_github_app": null, - "state_reason": null - }, - "comment": { - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/issues/comments/3686782829", - "html_url": "https://github.com/remcostoeten/remcostoeten.nl/pull/43#issuecomment-3686782829", - "issue_url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/issues/43", - "id": 3686782829, - "node_id": "IC_kwDOPKeNcM7bv9dt", - "user": { - "login": "remcostoeten", - "id": 57683378, - "node_id": "MDQ6VXNlcjU3NjgzMzc4", - "avatar_url": "https://avatars.githubusercontent.com/u/57683378?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/remcostoeten", - "html_url": "https://github.com/remcostoeten", - "followers_url": "https://api.github.com/users/remcostoeten/followers", - "following_url": "https://api.github.com/users/remcostoeten/following{/other_user}", - "gists_url": "https://api.github.com/users/remcostoeten/gists{/gist_id}", - "starred_url": "https://api.github.com/users/remcostoeten/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/remcostoeten/subscriptions", - "organizations_url": "https://api.github.com/users/remcostoeten/orgs", - "repos_url": "https://api.github.com/users/remcostoeten/repos", - "events_url": "https://api.github.com/users/remcostoeten/events{/privacy}", - "received_events_url": "https://api.github.com/users/remcostoeten/received_events", - "type": "User", - "user_view_type": "public", - "site_admin": false - }, - "created_at": "2025-12-23T14:09:53Z", - "updated_at": "2025-12-23T14:09:53Z", - "body": "#### 📝 Changed routes:\n\n\n| [**/projects/\\[slug\\]**](https://remcostoeten-4dtrw8t6t-remcostoetens-projects.vercel.app/projects/[slug]) | [**/projects**](https://remcostoeten-4dtrw8t6t-remcostoetens-projects.vercel.app/projects) |\n|:-:|:-:|\n|\"Screenshot
(view full size) |\"Screenshot
(view full size) |\n\n\n\n\n\n\n\nCommit 715ce3110ec0222fb525228d5e1e3f216bcc29ac (https://remcostoeten-4dtrw8t6t-remcostoetens-projects.vercel.app).", - "reactions": { - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/issues/comments/3686782829/reactions", - "total_count": 0, - "+1": 0, - "-1": 0, - "laugh": 0, - "hooray": 0, - "confused": 0, - "heart": 0, - "rocket": 0, - "eyes": 0 - }, - "performed_via_github_app": null - } - } - }, - { - "id": "5469700121", - "timestamp": "2025-12-23T14:06:44Z", - "repository": "remcostoeten/remcostoeten.nl", - "url": "https://github.com/remcostoeten/remcostoeten.nl", - "isPrivate": false, - "type": "pr", - "title": "worked on PR #43", - "description": "", - "payload": { - "action": "labeled", - "number": 43, - "pull_request": { - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/pulls/43", - "id": 3126199037, - "number": 43, - "head": { - "ref": "codex/implement-projects-showcase-system", - "sha": "715ce3110ec0222fb525228d5e1e3f216bcc29ac", - "repo": { - "id": 1017613680, - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl", - "name": "remcostoeten.nl" - } - }, - "base": { - "ref": "master", - "sha": "56b28479d7ced5cbd4bea71f999e74328f955881", - "repo": { - "id": 1017613680, - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl", - "name": "remcostoeten.nl" - } - } - }, - "label": { - "id": 9875953871, - "node_id": "LA_kwDOPKeNcM8AAAACTKcYzw", - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/labels/codex", - "name": "codex", - "color": "ededed", - "default": false, - "description": null - }, - "labels": [ - { - "id": 9875953871, - "node_id": "LA_kwDOPKeNcM8AAAACTKcYzw", - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/labels/codex", - "name": "codex", - "color": "ededed", - "default": false, - "description": null - } - ] - } - }, - { - "id": "5469699292", - "timestamp": "2025-12-23T14:06:44Z", - "repository": "remcostoeten/remcostoeten.nl", - "url": "https://github.com/remcostoeten/remcostoeten.nl", - "isPrivate": false, - "type": "pr", - "title": "opened PR #43", - "description": "", - "payload": { - "action": "opened", - "number": 43, - "pull_request": { - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/pulls/43", - "id": 3126199037, - "number": 43, - "head": { - "ref": "codex/implement-projects-showcase-system", - "sha": "715ce3110ec0222fb525228d5e1e3f216bcc29ac", - "repo": { - "id": 1017613680, - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl", - "name": "remcostoeten.nl" - } - }, - "base": { - "ref": "master", - "sha": "56b28479d7ced5cbd4bea71f999e74328f955881", - "repo": { - "id": 1017613680, - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl", - "name": "remcostoeten.nl" - } - } - } - } - }, - { - "id": "6947901523", - "timestamp": "2025-12-23T14:06:42Z", - "repository": "remcostoeten/remcostoeten.nl", - "url": "https://github.com/remcostoeten/remcostoeten.nl/tree/codex/implement-projects-showcase-system", - "isPrivate": false, - "type": "create", - "title": "created a branch", - "description": "Branch: codex/implement-projects-showcase-system", - "payload": { - "ref": "codex/implement-projects-showcase-system", - "ref_type": "branch", - "full_ref": "refs/heads/codex/implement-projects-showcase-system", - "master_branch": "master", - "description": "Currently rebuilding @ v2. v1 (monorepo + design) can be found at the branch `v1` and is still the current site at remcostoeten.nl", - "pusher_type": "user" - } - }, - { - "id": "5469695784", - "timestamp": "2025-12-23T14:06:32Z", - "repository": "remcostoeten/skriuw", - "url": "https://github.com/remcostoeten/skriuw", - "isPrivate": false, - "type": "pr", - "title": "worked on PR #62", - "description": "", - "payload": { - "action": "labeled", - "number": 62, - "pull_request": { - "url": "https://api.github.com/repos/remcostoeten/skriuw/pulls/62", - "id": 3126198313, - "number": 62, - "head": { - "ref": "codex/add-public/private-notes-feature", - "sha": "2a17f02d288ea86eff65abf718b02e4ece9d484e", - "repo": { - "id": 1086615687, - "url": "https://api.github.com/repos/remcostoeten/skriuw", - "name": "skriuw" - } - }, - "base": { - "ref": "daddy", - "sha": "8549892965a8e9da1a5321bc4c60d7f338f13c8d", - "repo": { - "id": 1086615687, - "url": "https://api.github.com/repos/remcostoeten/skriuw", - "name": "skriuw" - } - } - }, - "label": { - "id": 9606472625, - "node_id": "LA_kwDOQMRwh88AAAACPJcjsQ", - "url": "https://api.github.com/repos/remcostoeten/skriuw/labels/codex", - "name": "codex", - "color": "ededed", - "default": false, - "description": null - }, - "labels": [ - { - "id": 9606472625, - "node_id": "LA_kwDOQMRwh88AAAACPJcjsQ", - "url": "https://api.github.com/repos/remcostoeten/skriuw/labels/codex", - "name": "codex", - "color": "ededed", - "default": false, - "description": null - } - ] - } - }, - { - "id": "5469695527", - "timestamp": "2025-12-23T14:06:32Z", - "repository": "remcostoeten/skriuw", - "url": "https://github.com/remcostoeten/skriuw", - "isPrivate": false, - "type": "pr", - "title": "opened PR #62", - "description": "", - "payload": { - "action": "opened", - "number": 62, - "pull_request": { - "url": "https://api.github.com/repos/remcostoeten/skriuw/pulls/62", - "id": 3126198313, - "number": 62, - "head": { - "ref": "codex/add-public/private-notes-feature", - "sha": "2a17f02d288ea86eff65abf718b02e4ece9d484e", - "repo": { - "id": 1086615687, - "url": "https://api.github.com/repos/remcostoeten/skriuw", - "name": "skriuw" - } - }, - "base": { - "ref": "daddy", - "sha": "8549892965a8e9da1a5321bc4c60d7f338f13c8d", - "repo": { - "id": 1086615687, - "url": "https://api.github.com/repos/remcostoeten/skriuw", - "name": "skriuw" - } - } - } - } - }, - { - "id": "6947896942", - "timestamp": "2025-12-23T14:06:30Z", - "repository": "remcostoeten/skriuw", - "url": "https://github.com/remcostoeten/skriuw/tree/codex/add-public/private-notes-feature", - "isPrivate": false, - "type": "create", - "title": "created a branch", - "description": "Branch: codex/add-public/private-notes-feature", - "payload": { - "ref": "codex/add-public/private-notes-feature", - "ref_type": "branch", - "full_ref": "refs/heads/codex/add-public/private-notes-feature", - "master_branch": "daddy", - "description": "110,120,130!! 🔥 Blazingly fast note & todo manager for Web, Linux, OSX and Windows. Minimalism at base, maximalism at... when you opt in. Cloud, or local, managed AI or BYOK.", - "pusher_type": "user" - } - }, - { - "id": "5464412485", - "timestamp": "2025-12-23T08:59:27Z", - "repository": "remcostoeten/remcostoeten.nl", - "url": "https://github.com/remcostoeten/remcostoeten.nl", - "isPrivate": false, - "type": "pr", - "title": "worked on PR #42", - "description": "", - "payload": { - "action": "labeled", - "number": 42, - "pull_request": { - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/pulls/42", - "id": 3125429488, - "number": 42, - "head": { - "ref": "codex/add-animated-number-component-to-homepage", - "sha": "c899ec8778dd3921691848675dd716bb5cb5ef7f", - "repo": { - "id": 1017613680, - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl", - "name": "remcostoeten.nl" - } - }, - "base": { - "ref": "master", - "sha": "56b28479d7ced5cbd4bea71f999e74328f955881", - "repo": { - "id": 1017613680, - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl", - "name": "remcostoeten.nl" - } - } - }, - "label": { - "id": 9875953871, - "node_id": "LA_kwDOPKeNcM8AAAACTKcYzw", - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/labels/codex", - "name": "codex", - "color": "ededed", - "default": false, - "description": null - }, - "labels": [ - { - "id": 9875953871, - "node_id": "LA_kwDOPKeNcM8AAAACTKcYzw", - "url": "https://api.github.com/repos/remcostoeten/remcostoeten.nl/labels/codex", - "name": "codex", - "color": "ededed", - "default": false, - "description": null - } - ] - } - } - ] -} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index e5c36dca..00000000 --- a/.prettierignore +++ /dev/null @@ -1,13 +0,0 @@ -node_modules -.next -out -build -dist -.cache -.vercel -.turbo -*.lock -package-lock.json -bun.lock -yarn.lock -pnpm-lock.yaml diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 92ef2f12..00000000 --- a/.prettierrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "useTabs": true, - "tabWidth": 4, - "singleQuote": true, - "semi": false, - "trailingComma": "none", - "endOfLine": "lf", - "printWidth": 80, - "arrowParens": "avoid" -} diff --git a/bun.lock b/bun.lock index 0f015aff..8449d724 100644 --- a/bun.lock +++ b/bun.lock @@ -6,9 +6,14 @@ "dependencies": { "@neondatabase/serverless": "^1.0.2", "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@t3-oss/env-nextjs": "^0.13.10", "@tailwindcss/postcss": "^4.1.18", @@ -22,7 +27,7 @@ "clsx": "^2.1.1", "drizzle-orm": "^0.45.1", "framer-motion": "^12.23.25", - "geist": "1.2.2", + "geist": "^1.7.0", "lucide-react": "^0.562.0", "next": "16", "next-mdx-remote": "^5.0.0", @@ -33,6 +38,7 @@ "react-dom": "19", "react-icons": "^5.5.0", "react-markdown": "^10.1.0", + "react-resizable-panels": "^4.6.2", "react-syntax-highlighter": "^16.1.0", "rehype-mdx-code-props": "^3.0.1", "remark-gfm": "^4.0.1", @@ -52,6 +58,8 @@ "@types/react-dom": "19", "@types/react-syntax-highlighter": "^15.5.13", "drizzle-kit": "^0.31.8", + "oxlint": "^1.43.0", + "prettier": "^3.8.1", }, }, }, @@ -132,6 +140,14 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -242,6 +258,22 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.39.0", "", {}, "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg=="], + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.43.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C/GhObv/pQZg34NOzB6Mk8x0wc9AKj8fXzJF8ZRKTsBPyHusC6AZ6bba0QG0TUufw1KWuD0j++oebQfWeiFXNw=="], + + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.43.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-4NjfUtEEH8ewRQ2KlZGmm6DyrvypMdHwBnQT92vD0dLScNOQzr0V9O8Ua4IWXdeCNl/XMVhAV3h4/3YEYern5A=="], + + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-75tf1HvwdZ3ebk83yMbSB+moAEWK98mYqpXiaFAi6Zshie7r+Cx5PLXZFUEqkscenoZ+fcNXakHxfn94V6nf1g=="], + + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-BHV4fb36T2p/7bpA9fiJ5ayt7oJbiYX10nklW5arYp4l9/9yG/FQC5J4G1evzbJ/YbipF9UH0vYBAm5xbqGrvw=="], + + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1l3nvnzWWse1YHibzZ4HQXdF/ibfbKZhp9IguElni3bBqEyPEyurzZ0ikWynDxKGXqZa+UNXTFuU1NRVX1RJ3g=="], + + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+jNYgLGRFTJxJuaSOZJBwlYo5M0TWRw0+3y5MHOL4ArrIdHyCthg6r4RbVWrsR1qUfUE1VSSHQ2bfbC99RXqMg=="], + + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.43.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-dvs1C/HCjCyGTURMagiHprsOvVTT3omDiSzi5Qw0D4QFJ1pEaNlfBhVnOUYgUfS6O7Mcmj4+G+sidRsQcWQ/kA=="], + + "@oxlint/win32-x64": ["@oxlint/win32-x64@1.43.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bSuItSU8mTSDsvmmLTepTdCL2FkJI6dwt9tot/k0EmiYF+ArRzmsl4lXVLssJNRV5lJEc5IViyTrh7oiwrjUqA=="], + "@posthog/core": ["@posthog/core@1.10.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-Xk3JQ+cdychsvftrV3G9ZrN9W329lbyFW0pGJXFGKFQf8qr4upw2SgNg9BVorjSrfhoXZRnJGt/uNF4nGFBL5A=="], "@posthog/types": ["@posthog/types@1.328.0", "", {}, "sha512-T8RPYpgPxwtAMqtfj8+3VSw3Xca2hIYb3BN3ohPAZhN8eQOtkC00jEKy+bOcniy1BJCbbhpmGtxqQCpzr4/EXQ=="], @@ -270,6 +302,8 @@ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.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-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], @@ -290,6 +324,12 @@ "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.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-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.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-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "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-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], @@ -300,10 +340,16 @@ "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.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-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.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-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], + "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "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-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "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" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "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-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], @@ -316,6 +362,16 @@ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.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-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], @@ -502,7 +558,7 @@ "framer-motion": ["framer-motion@12.26.2", "", { "dependencies": { "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA=="], - "geist": ["geist@1.2.2", "", { "peerDependencies": { "next": ">=13.2.0 <15" } }, "sha512-uRDrxhvdnPwWJmh+K5+/5LXSKwvJzaYCl9tDXgiBi4hj7hB4K7+n/WLcvJMFs5btvyn0r9OSwCd1s6CmqAsxEw=="], + "geist": ["geist@1.7.0", "", { "peerDependencies": { "next": ">=13.2.0" } }, "sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], @@ -704,6 +760,8 @@ "next-mdx-remote": ["next-mdx-remote@5.0.0", "", { "dependencies": { "@babel/code-frame": "^7.23.5", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", "unist-util-remove": "^3.1.0", "vfile": "^6.0.1", "vfile-matter": "^5.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-RNNbqRpK9/dcIFZs/esQhuLA8jANqlH694yqoDBK8hkVdJUndzzGmnPHa2nyi90N4Z9VmzuSWNRpr5ItT3M7xQ=="], + "oxlint": ["oxlint@1.43.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.43.0", "@oxlint/darwin-x64": "1.43.0", "@oxlint/linux-arm64-gnu": "1.43.0", "@oxlint/linux-arm64-musl": "1.43.0", "@oxlint/linux-x64-gnu": "1.43.0", "@oxlint/linux-x64-musl": "1.43.0", "@oxlint/win32-arm64": "1.43.0", "@oxlint/win32-x64": "1.43.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.11.2" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-xiqTCsKZch+R61DPCjyqUVP2MhkQlRRYxLRBeBDi+dtQJ90MOgdcjIktvDCgXz0bgtx94EQzHEndsizZjMX2OA=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -734,6 +792,8 @@ "preact": ["preact@10.28.2", "", {}, "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA=="], + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], @@ -754,6 +814,8 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + "react-resizable-panels": ["react-resizable-panels@4.6.2", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-d6hyD6s7ewNAI+oINrZznR/08GUyAszrowXouUDztePEn/tQ2z/LEI2qRvrizYBe3TpgBi0cCjc10pXTTOc4jw=="], + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], "react-syntax-highlighter": ["react-syntax-highlighter@16.1.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", "refractor": "^5.0.0" }, "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg=="], @@ -898,8 +960,14 @@ "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "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" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "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" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "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" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "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" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.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-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], diff --git a/components.json b/components.json deleted file mode 100644 index da378fc0..00000000 --- a/components.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "tailwind.config.ts", - "css": "src/app/global.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "iconLibrary": "lucide", - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "registries": {} -} diff --git a/depcheck-results.json b/depcheck-results.json deleted file mode 100644 index 2c59117e..00000000 --- a/depcheck-results.json +++ /dev/null @@ -1,661 +0,0 @@ -{ - "dependencies": [ - "@t3-oss/env-nextjs", - "@tailwindcss/postcss", - "@tailwindcss/typography", - "postcss", - "react-markdown", - "rehype-mdx-code-props", - "tailwindcss", - "tailwindcss-animate", - "tw-animate-css" - ], - "devDependencies": [], - "missing": { - "db": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/auth.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/comments.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/reactions.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/test-db.ts" - ], - "env": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/auth.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/db/connection.ts" - ], - "@t3-oss/env-core": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/env.ts" - ], - "schema": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/db/connection.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/test-db.ts" - ], - "web-vitals": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/dev/web-vitals-reporter.tsx" - ] - }, - "using": { - "drizzle-kit": [ - "/home/remco-stoeten/dev/remcostoeten.nl/drizzle.config.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/package.json", - "/home/remco-stoeten/dev/remcostoeten.nl/src/proxy.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/auth.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/auth-client.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/auth/[...all]/route.ts" - ], - "next": [ - "/home/remco-stoeten/dev/remcostoeten.nl/package.json", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/DevWidget.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/RoutesSection.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/proxy.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/utils/is-admin.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/auth.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/auth-client.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/protection.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/optimized-page-transition.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/seo/structured-data.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/search/search-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/posthog-provider.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/providers.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/breadcrumbs.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/footer.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/page-transition.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/featured-projects.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/activity-hover-card.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/section.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/home/hero.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/collapsible-media.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/comment-section.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/mdx.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/post-view.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/posts-client.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/table-of-contents.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/topics-sidebar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/comments/admin-comments.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/blogs/blog-list.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/not-found.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/sitemap.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/og/route.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/sync/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/token/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/refresh/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/recent/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/now-playing/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/callback/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/auth-url/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/repo/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/events/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/contributions/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/commits/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/activity/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/example/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/auth/providers/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/auth/[...all]/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/activity/combined/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/terms/terms-content.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/privacy/privacy-content.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/legal/language-utils.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/legal/legal-header.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/dev/spotify/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/opengraph-image.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/twitter-image.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/opengraph-image.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/twitter-image.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/[topic]/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/[...slug]/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(auth)/auth/callback/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(admin)/admin/layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/analytics.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/auth.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/comments.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/contact.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/reactions.ts" - ], - "react": [ - "/home/remco-stoeten/dev/remcostoeten.nl/package.json", - "/home/remco-stoeten/dev/remcostoeten.nl/package.json", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/DevWidget.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/AuthSection.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/RoutesSection.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/SettingsPopover.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/SystemInfoSection.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/proxy.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/utils/is-admin.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/auth.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/auth-client.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/protection.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/hooks/use-admin.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/hooks/use-combined-activity.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/hooks/use-github.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/hooks/use-repo-details.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/hooks/use-spotify-playback.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/hooks/use-vim-command.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/animations.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/brand-icons.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/resume-drawer.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/theme-switch.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/vim-status-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/badge.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/button.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/card.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/collapsible.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/heading.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/input.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/notice.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/optimized-layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/optimized-page-transition.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/scroll-area.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/section.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/separator.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/stagger-system.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/table.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/tabs.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/skeletons/graph-skeleton.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/skeletons/section-skeletons.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/effects/animated-number.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/effects/ouder-auth-glow.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/code-block/CodeBlock.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/code-block/language-icons.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/seo/structured-data.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/search/search-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/posthog-provider.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/providers.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/query-client-provider.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/breadcrumbs.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/footer.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/page-transition.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/featured-projects.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/language-stats.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/tech-stack-cloud.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/activity-feed.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/activity-hover-card.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/contribution-graph.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/hover-wrappers.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/section.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/transition-debugger.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/home/hero.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/dev/web-vitals-reporter.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/demo/posthog-demo.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/contact/contact-popover.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/contact/email-autocomplete.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/collapsible-media.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/comment-section.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/demo-popover.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/mdx.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/post-view.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/posts-client.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/reaction-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/sign-in-button.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/spotify-api-explorer.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/spotify-env-generator.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/table-of-contents.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/topics-sidebar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/auth/oauth-modal.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/auth/vim-auth-provider.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/metrics/user-metrics.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/contact/contact-overview.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/comments/admin-comments.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/blogs/blog-list.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/Test.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/not-found.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/sitemap.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/og/route.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/sync/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/token/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/refresh/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/recent/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/now-playing/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/callback/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/auth-url/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/repo/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/events/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/contributions/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/commits/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/activity/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/example/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/auth/providers/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/auth/[...all]/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/activity/combined/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/terms/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/terms/terms-content.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/privacy/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/privacy/privacy-content.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/legal/language-utils.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/legal/legal-header.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/dev/spotify/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/opengraph-image.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/twitter-image.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/opengraph-image.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/twitter-image.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/[topic]/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/[...slug]/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(auth)/auth/callback/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(admin)/admin/layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(admin)/admin/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/analytics.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/auth.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/comments.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/contact.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/reactions.ts" - ], - "react-dom": [ - "/home/remco-stoeten/dev/remcostoeten.nl/package.json", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/DevWidget.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/RoutesSection.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/SettingsPopover.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/SystemInfoSection.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/proxy.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/utils/is-admin.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/auth.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/auth-client.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/protection.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/animations.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/resume-drawer.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/vim-status-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/collapsible.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/optimized-layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/optimized-page-transition.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/scroll-area.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/separator.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/stagger-system.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/tabs.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/skeletons/graph-skeleton.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/effects/animated-number.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/code-block/CodeBlock.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/seo/structured-data.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/search/search-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/posthog-provider.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/providers.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/breadcrumbs.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/footer.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/page-transition.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/featured-projects.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/language-stats.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/tech-stack-cloud.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/activity-feed.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/activity-hover-card.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/contribution-graph.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/section.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/transition-debugger.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/home/hero.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/contact/contact-popover.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/collapsible-media.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/comment-section.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/demo-popover.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/mdx.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/post-view.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/posts-client.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/reaction-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/table-of-contents.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/topics-sidebar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/auth/oauth-modal.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/auth/vim-auth-provider.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/comments/admin-comments.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/blogs/blog-list.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/not-found.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/sitemap.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/og/route.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/sync/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/token/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/refresh/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/recent/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/now-playing/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/callback/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/spotify/auth-url/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/repo/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/events/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/contributions/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/commits/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/github/activity/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/example/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/auth/providers/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/auth/[...all]/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/activity/combined/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/terms/terms-content.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/privacy/privacy-content.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/legal/language-utils.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/legal/legal-header.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/dev/spotify/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/opengraph-image.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/twitter-image.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/opengraph-image.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/twitter-image.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/[topic]/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/[...slug]/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(auth)/auth/callback/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(admin)/admin/layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/analytics.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/auth.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/comments.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/contact.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/reactions.ts" - ], - "@types/react": [ - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/DevWidget.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/hooks/use-admin.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/hooks/use-spotify-playback.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/hooks/use-vim-command.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/core/global.d.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/animations.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/brand-icons.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/resume-drawer.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/theme-switch.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/vim-status-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/badge.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/button.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/card.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/collapsible.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/heading.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/input.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/notice.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/optimized-layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/optimized-page-transition.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/scroll-area.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/section.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/separator.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/stagger-system.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/table.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/tabs.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/effects/animated-number.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/effects/ouder-auth-glow.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/code-block/CodeBlock.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/search/search-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/posthog-provider.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/providers.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/query-client-provider.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/breadcrumbs.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/footer.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/page-transition.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/featured-projects.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/language-stats.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/tech-stack-cloud.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/activity-feed.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/activity-hover-card.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/contribution-graph.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/hover-wrappers.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/transition-debugger.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/dev/web-vitals-reporter.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/demo/posthog-demo.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/contact/contact-popover.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/contact/email-autocomplete.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/collapsible-media.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/comment-section.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/demo-popover.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/mdx.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/post-view.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/posts-client.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/reaction-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/sign-in-button.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/spotify-api-explorer.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/spotify-env-generator.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/table-of-contents.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/auth/oauth-modal.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/auth/vim-auth-provider.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/metrics/user-metrics.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/contact/contact-overview.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/comments/admin-comments.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/blogs/blog-list.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/Test.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/terms/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/terms/terms-content.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/privacy/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/privacy/privacy-content.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/legal/legal-header.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/dev/spotify/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(auth)/auth/callback/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(admin)/admin/layout.tsx" - ], - "framer-motion": [ - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/DevWidget.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/SettingsPopover.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/SystemInfoSection.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/animations.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/vim-status-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/optimized-layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/optimized-page-transition.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/stagger-system.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/skeletons/graph-skeleton.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/effects/animated-number.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/code-block/CodeBlock.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/page-transition.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/featured-projects.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/language-stats.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/tech-stack-cloud.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/activity-feed.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/activity-hover-card.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/contribution-graph.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/transition-debugger.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/contact/contact-popover.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/collapsible-media.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/comment-section.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/demo-popover.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/reaction-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/auth/oauth-modal.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/auth/vim-auth-provider.tsx" - ], - "lucide-react": [ - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/DevWidget.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/AuthSection.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/RoutesSection.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/SettingsPopover.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/tools/dev-menu/components/sections/SystemInfoSection.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/resume-drawer.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/heading.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/notice.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/section.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/skeletons/section-skeletons.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/code-block/CodeBlock.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/search/search-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/breadcrumbs.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/footer.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/featured-projects.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/language-stats.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/tech-stack-cloud.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/activity-feed.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/transition-debugger.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/contact/contact-popover.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/collapsible-media.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/comment-section.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/post-view.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/posts-client.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/sign-in-button.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/spotify-api-explorer.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/spotify-env-generator.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/table-of-contents.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/topics-sidebar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/auth/oauth-modal.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/metrics/user-metrics.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/contact/contact-overview.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/comments/admin-comments.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/admin/blogs/blog-list.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/not-found.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/terms/terms-content.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/privacy/privacy-content.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/legal/legal-header.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/topics/[topic]/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(admin)/admin/layout.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(admin)/admin/page.tsx" - ], - "better-auth": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/proxy.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/auth.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/auth-client.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/auth/[...all]/route.ts" - ], - "drizzle-orm": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/proxy.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/auth.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/services/activity-sync.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/db/auth-schema.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/db/connection.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/db/helpers.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/db/schema.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/auth-client.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/protection.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/api/auth/[...all]/route.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/[...slug]/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/admin.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/analytics.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/comments.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/contact.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/reactions.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/scripts/api.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/scripts/check-db.mjs" - ], - "@types/node": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/utils/utils.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/analytics.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/scripts/api.ts" - ], - "db": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/auth.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/comments.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/reactions.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/test-db.ts" - ], - "env": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/auth.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/db/connection.ts" - ], - "@t3-oss/env-core": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/env.ts" - ], - "zod": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/env.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/contact.ts" - ], - "typescript": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/env.ts" - ], - "@neondatabase/serverless": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/services/activity-sync.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/db/auth-schema.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/db/connection.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/db/helpers.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/db/schema.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/protection.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/(marketing)/blog/[...slug]/page.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/admin.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/analytics.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/comments.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/contact.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/reactions.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/scripts/api.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/scripts/check-db.mjs" - ], - "schema": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/server/db/connection.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/test-db.ts" - ], - "unist-util-visit": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/rehype-extract-code-meta.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/remark-demo-plugin.ts" - ], - "clsx": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/utils.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/code-block/CodeBlock.tsx" - ], - "tailwind-merge": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/lib/utils.ts" - ], - "@tanstack/react-query": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/hooks/use-combined-activity.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/hooks/use-github.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/hooks/use-repo-details.ts", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/query-client-provider.tsx" - ], - "vaul": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/resume-drawer.tsx" - ], - "class-variance-authority": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/badge.tsx" - ], - "@radix-ui/react-slot": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/button.tsx" - ], - "@radix-ui/react-collapsible": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/collapsible.tsx" - ], - "@types/react-dom": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/collapsible.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/scroll-area.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/separator.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/tabs.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/activity-hover-card.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/activity/contribution-graph.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/table-of-contents.tsx" - ], - "@radix-ui/react-scroll-area": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/scroll-area.tsx" - ], - "@radix-ui/react-separator": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/separator.tsx" - ], - "@radix-ui/react-tabs": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/tabs.tsx" - ], - "react-syntax-highlighter": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/code-block/CodeBlock.tsx" - ], - "@types/react-syntax-highlighter": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/code-block/CodeBlock.tsx" - ], - "react-icons": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/ui/code-block/language-icons.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/language-stats.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/landing/tech-stack-cloud.tsx" - ], - "posthog-js": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/search/search-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/posthog-provider.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/demo/posthog-demo.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/comment-section.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/reaction-bar.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/sign-in-button.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/auth/oauth-modal.tsx" - ], - "sonner": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/providers.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/layout/footer.tsx", - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/contact/contact-popover.tsx" - ], - "@vercel/analytics": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/providers.tsx" - ], - "@vercel/speed-insights": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/providers/providers.tsx" - ], - "web-vitals": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/dev/web-vitals-reporter.tsx" - ], - "next-mdx-remote": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/mdx.tsx" - ], - "remark-gfm": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/components/blog/mdx.tsx" - ], - "geist": ["/home/remco-stoeten/dev/remcostoeten.nl/src/app/layout.tsx"], - "posthog-node": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/app/posthog.ts" - ], - "uuid": [ - "/home/remco-stoeten/dev/remcostoeten.nl/src/actions/contact.ts" - ] - }, - "invalidFiles": {}, - "invalidDirs": {} -} diff --git a/next.config.mjs b/next.config.mjs index b7ec7c33..b6d62d83 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -53,6 +53,9 @@ const nextConfig = { }, compress: true, productionBrowserSourceMaps: false, + turbopack: { + root: process.cwd() + }, experimental: { optimizePackageImports: [ 'lucide-react', diff --git a/package.json b/package.json index a9a6fbb7..30c3cfdd 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "private": true, "scripts": { - "dev": "next dev", - "build": "next build --webpack", - "build:fast": "NEXT_TELEMETRY_DISABLED=1 FAST_BUILD=true next build --webpack", + "dev": "next --bun dev", + "build": "next --bun build", + "build:fast": "NEXT_TELEMETRY_DISABLED=1 FAST_BUILD=true next build", "ogbuild": "next build", "start": "next start", "db:generate": "drizzle-kit generate", @@ -21,9 +21,14 @@ "dependencies": { "@neondatabase/serverless": "^1.0.2", "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@t3-oss/env-nextjs": "^0.13.10", "@tailwindcss/postcss": "^4.1.18", @@ -37,7 +42,7 @@ "clsx": "^2.1.1", "drizzle-orm": "^0.45.1", "framer-motion": "^12.23.25", - "geist": "1.2.2", + "geist": "^1.7.0", "lucide-react": "^0.562.0", "next": "16", "next-mdx-remote": "^5.0.0", @@ -48,6 +53,7 @@ "react-dom": "19", "react-icons": "^5.5.0", "react-markdown": "^10.1.0", + "react-resizable-panels": "^4.6.2", "react-syntax-highlighter": "^16.1.0", "rehype-mdx-code-props": "^3.0.1", "remark-gfm": "^4.0.1", diff --git a/prompt.md b/prompt.md deleted file mode 100644 index b1a200c4..00000000 --- a/prompt.md +++ /dev/null @@ -1,73 +0,0 @@ -# Prompt for Design Agent - -**Role:** Expert UI Engineer & Designer -**Task:** Create a new "Snippet Store Module" & Redesign the existing Playground. -**Context:** Personal Portfolio Website -**Placement:** Render this new section either immediately **above** or **below** the "Featured Projects" section on the homepage. - ---- - -### 1. Scope & Purpose - -This is a **personal portfolio website**. -The goal of this new "Sandbox/Playground" feature is to serve as a **showcase for technical explorations** that don't fit into full "Projects". - -**What we are storing/displaying:** - -- **Snippets:** Reusable code patterns, hooks, or quick utilities. -- **UI Experiments:** Animation trials, component prototypes, or visual designs. -- **Packages/CLI Tools:** Small libraries or scripts. - -Think of it as a "Digital Garden" or "Store" where visitors can browse these smaller artifacts. It needs to feel like a premium, curated collection. - -### 2. Functional Requirements - -- **Detail View:** Users must be able to click on any item (snippet, experiment, etc.) to open a **detailed view** (modal or separate page) to see the code, live preview, and description. -- **Visuals:** High-quality, "wow" factor design. -- **Location:** Integrate this seamlessly on the homepage, keeping the "Featured Projects" section in mind (place it above or below it). - -### 3. Critical Constraint: Animation Physics - -**Do not reinvent the interaction physics.** -You must **strictly preserve the exact sorting, filtering, and layout animation physics** of existing playground. The feel of switching tabs and reordering items must remain identical to the current implementation. - -**Use these exact Framer Motion configurations:** - -1. **Easing Curve:** - - ```typescript - const bezier = [0.16, 1, 0.3, 1] as const // Smooth elastic-like bezier - ``` - -2. **Card Animations (Entry/Exit/Layout):** - - Apply `layout` prop to the container and items for smooth reordering. - - Use `AnimatePresence` with `mode="popLayout"`. - - ```tsx - - ``` - -3. **Filter Tab Animation:** - - Use `layoutId` for the active tab indicator. - ```tsx - - ``` - -### 4. Technical Constraints - -- **Stack:** React, Tailwind CSS, Framer Motion. -- **Reference Code:** See `src/app/(marketing)/playground/content.tsx` for the current logic and data structure. Ensure feature parity (filtering, selection). diff --git a/src/app/(marketing)/page.tsx b/src/app/(marketing)/page.tsx index b7bf1f4b..92bfaafc 100644 --- a/src/app/(marketing)/page.tsx +++ b/src/app/(marketing)/page.tsx @@ -2,6 +2,8 @@ import { Intro } from '@/components/home/hero' import { Section } from '@/components/ui/section' import { homeMetadata } from '@/core/metadata' import nextDynamic from 'next/dynamic' +import { DeferredRender } from '@/components/ui/deferred-render' +import { ProjectShowcaseSkeleton } from '@/components/projects/components/project-showcase-skeleton' import { ActivitySectionSkeleton, TechStackSkeleton, @@ -14,7 +16,7 @@ const ActivitySection = nextDynamic( import('@/components/landing/activity/section').then(m => ({ default: m.ActivitySection })), - { loading: () => } + { loading: () => , ssr: false } ) const TechStackCloud = nextDynamic( @@ -44,7 +46,10 @@ const ProjectShowcase = nextDynamic( import('@/components/projects').then(m => ({ default: m.ProjectShowcase })), - { loading: () =>
} + { + loading: () => , + ssr: false + } ) const Playground = nextDynamic( @@ -52,11 +57,10 @@ const Playground = nextDynamic( import('@/components/landing/playground').then(m => ({ default: m.Playground })), - { loading: () =>
} + { loading: () =>
, ssr: false } ) export const revalidate = 60 -export const dynamic = 'force-dynamic' export { homeMetadata as metadata } @@ -85,15 +89,36 @@ export default function Page() { - + } + > + + -
- -
+ + + + } + > +
+ +
+
- + + } + > + + - +
diff --git a/src/app/(marketing)/playground/page.tsx b/src/app/(marketing)/playground/page.tsx index 656b669d..155ad9eb 100644 --- a/src/app/(marketing)/playground/page.tsx +++ b/src/app/(marketing)/playground/page.tsx @@ -1,17 +1,212 @@ -import { Metadata } from 'next' -import { Suspense } from 'react' -import { PlaygroundView } from '@/views/playground-view' - -export const metadata: Metadata = { - title: 'Playground', - description: - 'Components, utilities, and experiments. Small artifacts that do not warrant their own repositories.' +"use client" + +import { useState } from "react" +import Link from "next/link" +import { ArrowLeft, Layers, Code, Package, Terminal } from "lucide-react" +import { + COMPONENT_REGISTRY, + CATEGORIES, + getComponentsByCategory, +} from "@/components/component-studio/playground/component-registry" +import { PlaygroundLayout } from "@/components/component-studio/playground/playground-layout" +import { cn } from "@/lib/utils" +import { GooeyFilter } from "@/components/ui/gooey-toggle" + +const CATEGORY_ICONS: Record = { + Layers, + Code, + Package, + Terminal, } +type ViewState = + | { level: "categories" } + | { level: "components"; category: string } + | { level: "studio"; slug: string } + export default function PlaygroundPage() { - return ( - - - - ) + const [view, setView] = useState({ level: "categories" }) + + // --- Level 1: Category Overview --- + if (view.level === "categories") { + return ( +
+
+
+ + ~ + + / + playground +
+

Playground

+

+ A collection of components, utilities, and experiments. These artifacts are intentionally small or experimental and do not warrant their own repositories. Browse, copy, and adapt as needed. +

+
+ +
+ {CATEGORIES.map((cat) => { + const Icon = CATEGORY_ICONS[cat.icon] || Layers + const count = getComponentsByCategory(cat.slug).length + const isDisabled = cat.disabled || count === 0 + + return ( + + ) + })} +
+
+ ) + } + + // --- Level 2: Components in Category --- + if (view.level === "components") { + const category = CATEGORIES.find((c) => c.slug === view.category) + const components = getComponentsByCategory(view.category) + + return ( +
+
+
+ + ~ + + / + + / + {category?.label} +
+
+ +
+

{category?.label}

+

{category?.description}

+
+
+
+ + {/* SVG Filter for GooeyToggle preview */} + + +
+ {components.map((comp) => { + const Component = comp.component + const previewProps = comp.previewProps ?? {} + + return ( + + ) + })} +
+
+ ) + } + + // --- Level 3: Full Studio --- + if (view.level === "studio") { + const component = COMPONENT_REGISTRY.find((c) => c.slug === view.slug) + const category = component ? CATEGORIES.find((c) => c.slug === component.category) : null + + if (!component) { + return ( +
+ Component not found +
+ ) + } + + return ( +
+ {/* Breadcrumb Bar */} +
+ + ~ + + / + + / + + / + {component.name} +
+ + {/* Studio */} +
+ +
+
+ ) + } + + return null } diff --git a/src/app/api/projects/git-metrics/route.ts b/src/app/api/projects/git-metrics/route.ts new file mode 100644 index 00000000..accfba2b --- /dev/null +++ b/src/app/api/projects/git-metrics/route.ts @@ -0,0 +1,59 @@ +import { NextResponse } from 'next/server' +import { unstable_cache } from 'next/cache' +import { asc, eq } from 'drizzle-orm' +import { db } from 'db' +import { projects } from 'schema' +import type { IGitMetrics } from '@/components/projects/types' +import { fetchGitMetrics } from '@/components/projects/server/github' + +export const revalidate = 3600 + +const getProjectGitMetrics = unstable_cache( + async () => { + const dbProjects = await db + .select({ + title: projects.title, + gitUrl: projects.gitUrl + }) + .from(projects) + .where(eq(projects.hidden, false)) + .orderBy(asc(projects.idx)) + + const metricsByProject: Record = {} + + await Promise.all( + dbProjects.map(async project => { + if (!project.gitUrl) return + const metrics = await fetchGitMetrics(project.gitUrl) + if (metrics) { + metricsByProject[project.title] = metrics + } + }) + ) + + return metricsByProject + }, + ['project-git-metrics'], + { revalidate: 3600, tags: ['projects', 'github'] } +) + +export async function GET() { + try { + const data = await getProjectGitMetrics() + return NextResponse.json(data, { + headers: { + 'Cache-Control': + 'public, s-maxage=3600, stale-while-revalidate=86400' + } + }) + } catch (error) { + console.error( + '[api/projects/git-metrics] Failed to load project git metrics:', + error + ) + return NextResponse.json( + { error: 'Failed to load git metrics' }, + { status: 500 } + ) + } +} diff --git a/src/app/api/projects/showcase/route.ts b/src/app/api/projects/showcase/route.ts new file mode 100644 index 00000000..2d4e8e81 --- /dev/null +++ b/src/app/api/projects/showcase/route.ts @@ -0,0 +1,67 @@ +import { NextResponse } from 'next/server' +import { unstable_cache } from 'next/cache' +import { asc, eq } from 'drizzle-orm' +import { db } from 'db' +import { projects } from 'schema' +import type { IProject, TPreview } from '@/components/projects/types' + +export const revalidate = 300 + +function mapDbProjectToProject(dbProject: typeof projects.$inferSelect): IProject { + const preview: TPreview = dbProject.demoUrl + ? { + type: 'iframe', + url: dbProject.demoUrl, + embedUrl: dbProject.demoBox ?? undefined + } + : { type: 'none' } + + return { + name: dbProject.title, + description: dbProject.desc, + additionalDescription: dbProject.additionalDesc ?? undefined, + type: dbProject.native ? 'desktop' : 'utility', + status: 'active', + github: dbProject.gitUrl ?? '', + tech: dbProject.labels, + preview, + spotlight: dbProject.featured, + defaultOpen: dbProject.defaultOpen, + showIndicatorOnScroll: dbProject.showIndicator + } +} + +const getProjectShowcaseData = unstable_cache( + async () => { + const dbProjects = await db + .select() + .from(projects) + .where(eq(projects.hidden, false)) + .orderBy(asc(projects.idx)) + + const allProjects = dbProjects.map(mapDbProjectToProject) + return { + featured: allProjects.filter(project => project.spotlight), + other: allProjects.filter(project => !project.spotlight) + } + }, + ['project-showcase-data'], + { revalidate: 300, tags: ['projects'] } +) + +export async function GET() { + try { + const data = await getProjectShowcaseData() + return NextResponse.json(data, { + headers: { + 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=86400' + } + }) + } catch (error) { + console.error('[api/projects/showcase] Failed to load projects:', error) + return NextResponse.json( + { error: 'Failed to load project showcase data' }, + { status: 500 } + ) + } +} diff --git a/src/app/fonts.ts b/src/app/fonts.ts new file mode 100644 index 00000000..59dbb4d9 --- /dev/null +++ b/src/app/fonts.ts @@ -0,0 +1,23 @@ +import { GeistSans } from 'geist/font/sans' +import { GeistMono } from 'geist/font/mono' +import { Press_Start_2P } from 'next/font/google' + +// Toggle this to true to enable the pixel font +export const ENABLE_PIXEL_FONT = true + +const pixelFont = Press_Start_2P({ + weight: '400', + subsets: ['latin'], + variable: '--font-pixel' +}) + +export function getBodyFonts() { + if (ENABLE_PIXEL_FONT) { + // Return pixel font variable and className + // We remove font-sans so pixel font takes precedence via its className + return `${pixelFont.variable} ${pixelFont.className}` + } + + // Default behavior: Geist variables + font-sans utility + return `${GeistSans.variable} ${GeistMono.variable} font-sans` +} diff --git a/src/components/auth/vim-auth-provider.tsx b/src/components/auth/vim-auth-provider.tsx index 84bf286e..288ca7c1 100644 --- a/src/components/auth/vim-auth-provider.tsx +++ b/src/components/auth/vim-auth-provider.tsx @@ -1,6 +1,12 @@ 'use client' -import { useEffect, useState } from 'react' +import { + useEffect, + useState, + createContext, + useContext, + useCallback +} from 'react' import { signOut, useSession } from '@/lib/auth-client' import { VimStatusBar } from '@/components/vim-status-bar' import { OAuthModal } from '@/components/auth/oauth-modal' @@ -9,33 +15,54 @@ import { OuterAuthGlow } from '../ui/effects/ouder-auth-glow' import { useBlogFilter } from '@/hooks/use-blog-filter' const ALLOWED_GITHUB_USERNAME = 'remcostoeten' +type VimAuthContextType = { + openAuthModal: () => void +} + type Props = { children: React.ReactNode } +export const VimAuthContext = createContext( + undefined +) + +export function useVimAuth() { + const context = useContext(VimAuthContext) + if (context === undefined) { + throw new Error('useVimAuth must be used within a VimAuthProvider') + } + return context +} + export function VimAuthProvider({ children }: Props) { const { data: session } = useSession() const [showOAuthModal, setShowOAuthModal] = useState(false) const { command: backgroundCommand, clearCommand: clearBackgroundCommand } = useVimCommand() - const handleCommand = async (command: string) => { - const cmd = command.toLowerCase().trim() + const openAuthModal = useCallback(() => setShowOAuthModal(true), []) - if ((cmd === 'signin' || cmd === 'login') && !session) { - // Show the modal - setShowOAuthModal(true) - } else if ((cmd === 'signout' || cmd === 'logout') && session) { - await signOut() - } - } + const handleCommand = useCallback( + async (command: string) => { + const cmd = command.toLowerCase().trim() + + if ((cmd === 'signin' || cmd === 'login') && !session) { + // Show the modal + setShowOAuthModal(true) + } else if ((cmd === 'signout' || cmd === 'logout') && session) { + await signOut() + } + }, + [session] + ) useEffect(() => { if (backgroundCommand) { handleCommand(backgroundCommand) clearBackgroundCommand() } - }, [backgroundCommand, clearBackgroundCommand]) + }, [backgroundCommand, clearBackgroundCommand, handleCommand]) useEffect(() => { if (session?.user) { @@ -52,7 +79,7 @@ export function VimAuthProvider({ children }: Props) { }, [session]) return ( - <> + {children} @@ -63,6 +90,6 @@ export function VimAuthProvider({ children }: Props) { /> {session?.user && } - + ) } diff --git a/src/components/component-studio/lib/icons.tsx b/src/components/component-studio/lib/icons.tsx new file mode 100644 index 00000000..2ac9fba4 --- /dev/null +++ b/src/components/component-studio/lib/icons.tsx @@ -0,0 +1,223 @@ +import React from "react" + +function PrismIcon() { + return ( + + + + + + + ) +} + +function OrbitIcon() { + return ( + + + + + + + + ) +} + +function HexagonIcon() { + return ( + + + + + ) +} + +function WavelengthIcon() { + return ( + + + + + + ) +} + +function FlameIcon() { + return ( + + + + ) +} + +function ScanIcon() { + return ( + + + + + + + + + + + + ) +} + +function CrystalIcon() { + return ( + + + + + + ) +} + +function SignalIcon() { + return ( + + + + + ) +} + +function PulseRingIcon() { + return ( + + + + + ) +} + +function SpinnerIcon() { + return ( + + + + + + + + + + + ) +} + +// --- Icon Registry --- + +export const ICON_REGISTRY: Record React.ReactNode> = { + prism: () => , + orbit: () => , + hexagon: () => , + wavelength: () => , + flame: () => , + scan: () => , + crystal: () => , + signal: () => , + "pulse-ring": () => , + spinner: () => , +} + +export function getIconByKey(key: string | null): React.ReactNode | undefined { + if (!key) return undefined + const factory = ICON_REGISTRY[key] + return factory ? factory() : undefined +} + +export function getIconKeys(): string[] { + return Object.keys(ICON_REGISTRY) +} diff --git a/src/components/component-studio/lib/jsx-utils.ts b/src/components/component-studio/lib/jsx-utils.ts new file mode 100644 index 00000000..bc20823b --- /dev/null +++ b/src/components/component-studio/lib/jsx-utils.ts @@ -0,0 +1,95 @@ +import type { PropSchema, BehaviorOption } from "@/components/component-studio/playground/types" + +export function formatPropValue(value: unknown, propType: PropSchema["type"] | "behavior"): string { + if (value === null || value === undefined) return "null" + + if (typeof value === "string") { + return `"${value}"` + } + if (typeof value === "boolean") { + return value ? "true" : "false" + } + if (typeof value === "number") { + return String(value) + } + return JSON.stringify(value) +} + +function isDefaultValue( + value: unknown, + schema: PropSchema | BehaviorOption | undefined +): boolean { + if (!schema) return false + return value === schema.defaultValue +} + +export function generateComponentJsx( + componentName: string, + props: Record, + behaviors: Record, + registration: { props: PropSchema[]; behaviors?: BehaviorOption[] } +): string { + const propEntries: string[] = [] + + // Add props that differ from defaults + for (const propSchema of registration.props) { + const value = props[propSchema.name] + if (!isDefaultValue(value, propSchema) && value !== "" && value !== null) { + const formattedValue = formatPropValue(value, propSchema.type) + + // For booleans that are true, use shorthand + if (propSchema.type === "boolean" && value === true) { + propEntries.push(` ${propSchema.name}`) + } else if (propSchema.type === "string") { + propEntries.push(` ${propSchema.name}=${formattedValue}`) + } else { + propEntries.push(` ${propSchema.name}={${formattedValue}}`) + } + } + } + + // Add behaviors that differ from defaults + for (const behaviorSchema of registration.behaviors ?? []) { + const value = behaviors[behaviorSchema.name] + if (!isDefaultValue(value, behaviorSchema) && value !== "" && value !== null) { + const formattedValue = formatPropValue(value, "behavior") + + if (behaviorSchema.type === "boolean" && value === true) { + propEntries.push(` ${behaviorSchema.name}`) + } else if (behaviorSchema.type === "string") { + propEntries.push(` ${behaviorSchema.name}=${formattedValue}`) + } else { + propEntries.push(` ${behaviorSchema.name}={${formattedValue}}`) + } + } + } + + // Always include required props + for (const propSchema of registration.props) { + if (propSchema.required) { + const value = props[propSchema.name] + if (value === undefined) continue // Skip if required prop is missing (shouldn't happen in playground) + + const formattedValue = formatPropValue(value, propSchema.type) + const propLine = + propSchema.type === "string" + ? ` ${propSchema.name}=${formattedValue}` + : ` ${propSchema.name}={${formattedValue}}` + + if (!propEntries.some((p) => p.trim().startsWith(propSchema.name))) { + propEntries.unshift(propLine) + } + } + } + + if (propEntries.length === 0) { + return `<${componentName} />` + } + + if (propEntries.length === 1) { + const prop = propEntries[0].trim() + return `<${componentName} ${prop} />` + } + + return `<${componentName}\n${propEntries.join("\n")}\n/>` +} diff --git a/src/components/component-studio/playground/animation-studio/animation-export.tsx b/src/components/component-studio/playground/animation-studio/animation-export.tsx new file mode 100644 index 00000000..a67b6ae1 --- /dev/null +++ b/src/components/component-studio/playground/animation-studio/animation-export.tsx @@ -0,0 +1,120 @@ +"use client" + +import { useMemo } from "react" +import { Button } from "@/components/ui/button" +import { Copy } from "lucide-react" +import { toast } from "sonner" +import type { AnimationDirection, AnimationFillMode } from "../types" + +interface AnimationExportProps { + name: string + keyframes: Record> + bezierValue: [number, number, number, number] + duration: number + iterationCount: string | number + direction: AnimationDirection + fillMode: AnimationFillMode + delayBetweenMs: number + repeatCount: number +} + +export function AnimationExport({ + name, + keyframes, + bezierValue, + duration, + iterationCount, + direction, + fillMode, + delayBetweenMs, + repeatCount, +}: AnimationExportProps) { + const [x1, y1, x2, y2] = bezierValue + + const effectiveIterCount = repeatCount > 0 ? repeatCount : iterationCount + const iterStr = + effectiveIterCount === "infinite" ? "infinite" : String(effectiveIterCount) + + const buildAnimShorthand = () => { + const parts = [name, `${duration}ms`, `cubic-bezier(${x1}, ${y1}, ${x2}, ${y2})`] + if (delayBetweenMs > 0) parts.push(`${delayBetweenMs}ms`) + if (iterStr !== "1") parts.push(iterStr) + if (direction !== "normal") parts.push(direction) + if (fillMode !== "none") parts.push(fillMode) + return parts.join(" ") + } + + const cssOutput = useMemo(() => { + const frames = Object.entries(keyframes) + .map( + ([offset, props]) => + ` ${offset} {\n${Object.entries(props) + .map(([k, v]) => ` ${k}: ${v};`) + .join("\n")}\n }` + ) + .join("\n") + + return `@keyframes ${name} {\n${frames}\n}\n\n.animate-${name} {\n animation: ${buildAnimShorthand()};\n}` + }, [name, keyframes, x1, y1, x2, y2, duration, iterStr, direction, fillMode, delayBetweenMs]) + + const tailwindOutput = useMemo(() => { + const kfEntries = Object.entries(keyframes) + .map( + ([offset, props]) => + ` '${offset}': {\n${Object.entries(props) + .map(([k, v]) => ` ${k}: '${v}',`) + .join("\n")}\n },` + ) + .join("\n") + + return `// tailwind.config.ts \u2192 theme.extend\nkeyframes: {\n '${name}': {\n${kfEntries}\n },\n},\nanimation: {\n '${name}': '${buildAnimShorthand()}',\n},` + }, [name, keyframes, x1, y1, x2, y2, duration, iterStr, direction, fillMode, delayBetweenMs]) + + const reactOutput = useMemo(() => { + return `// React inline style\nconst style: React.CSSProperties = {\n animation: '${buildAnimShorthand()}',\n};` + }, [name, duration, x1, y1, x2, y2, iterStr, direction, fillMode, delayBetweenMs]) + + const copy = (text: string) => { + navigator.clipboard.writeText(text) + toast("Copied to clipboard") + } + + return ( +
+ copy(cssOutput)} /> + copy(tailwindOutput)} /> + copy(reactOutput)} /> +
+ ) +} + +function ExportBlock({ + label, + content, + onCopy, +}: { + label: string + content: string + onCopy: () => void +}) { + return ( +
+
+ + {label} + + +
+
+        {content}
+      
+
+ ) +} diff --git a/src/components/component-studio/playground/animation-studio/animation-studio.tsx b/src/components/component-studio/playground/animation-studio/animation-studio.tsx new file mode 100644 index 00000000..53984e4e --- /dev/null +++ b/src/components/component-studio/playground/animation-studio/animation-studio.tsx @@ -0,0 +1,362 @@ +"use client" + +import { useCallback, useMemo, type Dispatch } from "react" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Separator } from "@/components/ui/separator" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import type { + AnimationSchema, + AnimationDirection, + AnimationFillMode, + PlaygroundAction, +} from "../types" +import { BezierEditor } from "./bezier-editor" +import { Timeline } from "./timeline" +import { PlaybackControls } from "./playback-controls" +import { KeyframeEditor, AddKeyframeButton } from "./keyframe-editor" +import { AnimationExport } from "./animation-export" +import { useAnimationPlayback } from "@/hooks/use-animation-playback" + +interface AnimationStudioProps { + animations: AnimationSchema[] + activeAnimation: string | null + bezierValue: [number, number, number, number] + duration: number + progress: number + speed: number + isPlaying: boolean + loop: boolean + repeatCount: number + delayBetweenMs: number + currentRun: number + isInDelay: boolean + selectedKeyframe: string | null + editedKeyframes: Record> | null + direction: AnimationDirection + fillMode: AnimationFillMode + timelineZoom: number + dispatch: Dispatch +} + +const DIRECTION_OPTIONS: { value: AnimationDirection; label: string }[] = [ + { value: "normal", label: "Normal" }, + { value: "reverse", label: "Reverse" }, + { value: "alternate", label: "Alternate" }, + { value: "alternate-reverse", label: "Alt-Reverse" }, +] + +const FILL_MODE_OPTIONS: { value: AnimationFillMode; label: string }[] = [ + { value: "none", label: "None" }, + { value: "forwards", label: "Forwards" }, + { value: "backwards", label: "Backwards" }, + { value: "both", label: "Both" }, +] + +export function AnimationStudio({ + animations, + activeAnimation, + bezierValue, + duration, + progress, + speed, + isPlaying, + loop, + repeatCount, + delayBetweenMs, + currentRun, + isInDelay, + selectedKeyframe, + editedKeyframes, + direction, + fillMode, + timelineZoom, + dispatch, +}: AnimationStudioProps) { + const currentAnim = useMemo( + () => animations.find((a) => a.name === activeAnimation) ?? null, + [animations, activeAnimation] + ) + + const keyframes = useMemo( + () => editedKeyframes ?? currentAnim?.keyframes ?? {}, + [editedKeyframes, currentAnim] + ) + + const keyframeOffsets = useMemo(() => Object.keys(keyframes), [keyframes]) + + const onProgressChange = useCallback( + (value: number) => dispatch({ type: "SET_PROGRESS", value }), + [dispatch] + ) + const onPlayingChange = useCallback( + (value: boolean) => dispatch({ type: "SET_PLAYING", value }), + [dispatch] + ) + const onCurrentRunChange = useCallback( + (value: number) => dispatch({ type: "SET_CURRENT_RUN", value }), + [dispatch] + ) + const onInDelayChange = useCallback( + (value: boolean) => dispatch({ type: "SET_IN_DELAY", value }), + [dispatch] + ) + + const { + stepForward, + stepBackward, + stepByPercent, + toggle, + reset, + goToPercent, + totalRuns, + } = useAnimationPlayback({ + duration, + speed, + isPlaying, + loop, + progress, + repeatCount, + delayBetweenMs, + currentRun, + isInDelay, + onProgressChange, + onPlayingChange, + onCurrentRunChange, + onInDelayChange, + }) + + const selectedKeyframeProps = selectedKeyframe + ? keyframes[selectedKeyframe] + : null + + return ( +
+ {/* Animation selector + duration */} +
+
+ + +
+ +
+ +
+ { + const v = parseInt(e.target.value, 10) + if (!isNaN(v) && v > 0) + dispatch({ type: "SET_DURATION", value: v }) + }} + min={10} + max={30000} + step={10} + className="h-8 w-20 rounded-none border-border/50 bg-card/20 text-sm font-mono" + /> + ms +
+
+
+ + {currentAnim && ( + <> + {/* Direction + Fill Mode */} +
+
+ + +
+
+ + +
+
+ + + + {/* Bezier curve editor */} +
+ + dispatch({ type: "SET_BEZIER", value: v })} + size={180} + /> +
+ + + + {/* Timeline */} +
+ + + dispatch({ type: "SET_SELECTED_KEYFRAME", value: v }) + } + zoom={timelineZoom} + onZoomChange={(v) => + dispatch({ type: "SET_TIMELINE_ZOOM", value: v }) + } + duration={duration} + currentRun={currentRun} + totalRuns={totalRuns} + isInDelay={isInDelay} + /> +
+ + {/* Playback controls */} + dispatch({ type: "SET_SPEED", value: v })} + onLoopChange={(v) => dispatch({ type: "SET_LOOP", value: v })} + onRepeatCountChange={(v) => + dispatch({ type: "SET_REPEAT_COUNT", value: v }) + } + onDelayBetweenChange={(v) => + dispatch({ type: "SET_DELAY_BETWEEN", value: v }) + } + onGoToPercent={goToPercent} + onReset={reset} + /> + + + + {/* Keyframe editor */} + {selectedKeyframe && selectedKeyframeProps && ( + 2} + onChange={(property, value) => + dispatch({ + type: "SET_KEYFRAME_PROPERTY", + offset: selectedKeyframe, + property, + value, + }) + } + onAddProperty={(property, value) => + dispatch({ + type: "SET_KEYFRAME_PROPERTY", + offset: selectedKeyframe, + property, + value, + }) + } + onRemoveKeyframe={() => + dispatch({ type: "REMOVE_KEYFRAME", offset: selectedKeyframe }) + } + /> + )} + + {/* Add keyframe */} + + dispatch({ type: "ADD_KEYFRAME", offset, properties }) + } + /> + + + + {/* Export */} + + + )} +
+ ) +} diff --git a/src/components/component-studio/playground/animation-studio/bezier-editor.tsx b/src/components/component-studio/playground/animation-studio/bezier-editor.tsx new file mode 100644 index 00000000..d82e4aa9 --- /dev/null +++ b/src/components/component-studio/playground/animation-studio/bezier-editor.tsx @@ -0,0 +1,230 @@ +"use client" + +import { useCallback, useRef, useState } from "react" +import { cn } from "@/lib/utils" + +interface BezierEditorProps { + value: [number, number, number, number] + onChange: (value: [number, number, number, number]) => void + size?: number +} + +const PRESETS: { label: string; value: [number, number, number, number] }[] = [ + { label: "linear", value: [0, 0, 1, 1] }, + { label: "ease", value: [0.25, 0.1, 0.25, 1] }, + { label: "ease-in", value: [0.42, 0, 1, 1] }, + { label: "ease-out", value: [0, 0, 0.58, 1] }, + { label: "ease-in-out", value: [0.42, 0, 0.58, 1] }, + { label: "poppy", value: [0.16, 1, 0.3, 1] }, + { label: "bounce", value: [0.34, 1.56, 0.64, 1] }, + { label: "snap", value: [0.5, 0, 0, 1] }, +] + +export function BezierEditor({ + value, + onChange, + size = 200, +}: BezierEditorProps) { + const svgRef = useRef(null) + const [activeHandle, setActiveHandle] = useState<1 | 2 | null>(null) + const [x1, y1, x2, y2] = value + + // Convert CSS bezier values to SVG coordinates + // CSS: (0,0) is bottom-left, (1,1) is top-right + // SVG: (0,0) is top-left, (size,size) is bottom-right + const toSvg = (cx: number, cy: number): [number, number] => [ + cx * size, + (1 - cy) * size, + ] + + const fromSvg = (sx: number, sy: number): [number, number] => [ + Math.round((sx / size) * 100) / 100, + Math.round(((1 - sy / size)) * 100) / 100, + ] + + const [p1x, p1y] = toSvg(x1, y1) + const [p2x, p2y] = toSvg(x2, y2) + + const handlePointerDown = useCallback( + (handle: 1 | 2) => (e: React.PointerEvent) => { + e.preventDefault() + ;(e.target as SVGElement).setPointerCapture(e.pointerId) + setActiveHandle(handle) + }, + [] + ) + + const handlePointerMove = useCallback( + (e: React.PointerEvent) => { + if (!activeHandle || !svgRef.current) return + + const rect = svgRef.current.getBoundingClientRect() + const sx = Math.max(0, Math.min(size, e.clientX - rect.left)) + // Allow y overshoot for bounce curves (clamp between -0.5 and 1.5 in CSS space) + const rawSy = e.clientY - rect.top + const sy = Math.max(-size * 0.5, Math.min(size * 1.5, rawSy)) + + const [cx, cy] = fromSvg(sx, sy) + + if (activeHandle === 1) { + onChange([cx, cy, x2, y2]) + } else { + onChange([x1, y1, cx, cy]) + } + }, + [activeHandle, size, x1, y1, x2, y2, onChange, fromSvg] + ) + + const handlePointerUp = useCallback(() => { + setActiveHandle(null) + }, []) + + const bezierString = `cubic-bezier(${x1}, ${y1}, ${x2}, ${y2})` + + return ( +
+ + {/* Grid lines */} + {[0.25, 0.5, 0.75].map((t) => ( + + + + + ))} + + {/* Linear reference */} + + + {/* Control arms */} + + + + {/* Bezier curve */} + + + {/* Start/end points */} + + + + {/* Control point 1 */} + + + {/* Control point 2 */} + + + + {/* Bezier value display */} +
+ + {bezierString} + + +
+ + {/* Presets */} +
+ {PRESETS.map((preset) => { + const isActive = + preset.value[0] === x1 && + preset.value[1] === y1 && + preset.value[2] === x2 && + preset.value[3] === y2 + return ( + + ) + })} +
+
+ ) +} diff --git a/src/components/component-studio/playground/animation-studio/keyframe-editor.tsx b/src/components/component-studio/playground/animation-studio/keyframe-editor.tsx new file mode 100644 index 00000000..60d07221 --- /dev/null +++ b/src/components/component-studio/playground/animation-studio/keyframe-editor.tsx @@ -0,0 +1,186 @@ +"use client" + +import { useState } from "react" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Button } from "@/components/ui/button" +import { Plus, Trash2, X } from "lucide-react" + +interface KeyframeEditorProps { + offset: string + properties: Record + canDelete: boolean + onChange: (property: string, value: string) => void + onAddProperty: (property: string, value: string) => void + onRemoveKeyframe: () => void +} + +export function KeyframeEditor({ + offset, + properties, + canDelete, + onChange, + onAddProperty, + onRemoveKeyframe, +}: KeyframeEditorProps) { + const [newProp, setNewProp] = useState("") + const [newValue, setNewValue] = useState("") + + const handleAdd = () => { + if (newProp.trim() && newValue.trim()) { + onAddProperty(newProp.trim(), newValue.trim()) + setNewProp("") + setNewValue("") + } + } + + return ( +
+
+ + Keyframe: {offset} + + {canDelete && ( + + )} +
+ +
+ {Object.entries(properties).map(([prop, value]) => ( +
+ + onChange(prop, e.target.value)} + className="h-7 flex-1 rounded-none border-border/40 bg-card/10 text-[11px] font-mono" + /> +
+ ))} +
+ + {/* Add new property */} +
+ setNewProp(e.target.value)} + placeholder="property" + className="h-6 flex-1 rounded-none border-border/30 bg-card/5 text-[10px] font-mono" + /> + setNewValue(e.target.value)} + placeholder="value" + onKeyDown={(e) => e.key === "Enter" && handleAdd()} + className="h-6 flex-1 rounded-none border-border/30 bg-card/5 text-[10px] font-mono" + /> + +
+
+ ) +} + +// Standalone "Add Keyframe" component +interface AddKeyframeProps { + onAdd: (offset: string, properties: Record) => void +} + +export function AddKeyframeButton({ onAdd }: AddKeyframeProps) { + const [isOpen, setIsOpen] = useState(false) + const [offset, setOffset] = useState("") + const [property, setProperty] = useState("opacity") + const [value, setValue] = useState("1") + + const handleAdd = () => { + const pct = parseInt(offset, 10) + if (isNaN(pct) || pct < 0 || pct > 100) return + onAdd(`${pct}%`, { [property]: value }) + setIsOpen(false) + setOffset("") + } + + if (!isOpen) { + return ( + + ) + } + + return ( +
+
+ + New Keyframe + + +
+
+ setOffset(e.target.value)} + placeholder="0-100" + min={0} + max={100} + className="h-6 w-16 rounded-none border-border/30 bg-card/5 text-[10px] font-mono" + /> + % + setProperty(e.target.value)} + placeholder="property" + className="h-6 flex-1 rounded-none border-border/30 bg-card/5 text-[10px] font-mono" + /> + setValue(e.target.value)} + placeholder="value" + className="h-6 flex-1 rounded-none border-border/30 bg-card/5 text-[10px] font-mono" + /> + +
+
+ ) +} diff --git a/src/components/component-studio/playground/animation-studio/playback-controls.tsx b/src/components/component-studio/playground/animation-studio/playback-controls.tsx new file mode 100644 index 00000000..03b936e6 --- /dev/null +++ b/src/components/component-studio/playground/animation-studio/playback-controls.tsx @@ -0,0 +1,302 @@ +"use client" + +import { + Play, + Pause, + SkipBack, + SkipForward, + Rewind, + FastForward, + Repeat, + RotateCcw, + ChevronsLeft, + ChevronsRight, +} from "lucide-react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Slider } from "@/components/ui/slider" +import { cn } from "@/lib/utils" + +interface PlaybackControlsProps { + isPlaying: boolean + speed: number // 1-1000 (percent) + loop: boolean + repeatCount: number // 0 = use loop, N = play N times + delayBetweenMs: number // ms between runs + progress: number + onToggle: () => void + onStepForward: (frames?: number) => void + onStepBackward: (frames?: number) => void + onStepByPercent: (pct: number) => void + onSpeedChange: (speed: number) => void + onLoopChange: (loop: boolean) => void + onRepeatCountChange: (count: number) => void + onDelayBetweenChange: (ms: number) => void + onGoToPercent: (pct: number) => void + onReset: () => void +} + +export function PlaybackControls({ + isPlaying, + speed, + loop, + repeatCount, + delayBetweenMs, + progress, + onToggle, + onStepForward, + onStepBackward, + onStepByPercent, + onSpeedChange, + onLoopChange, + onRepeatCountChange, + onDelayBetweenChange, + onGoToPercent, + onReset, +}: PlaybackControlsProps) { + return ( +
+ {/* Transport controls */} +
+ + + + + + + + + + + + + + + + +
+ + +
+ + {/* Precision jump */} +
+ + { + const v = parseFloat(e.target.value) + if (!isNaN(v)) onGoToPercent(v) + }} + min={0} + max={100} + step={0.01} + className="h-6 w-20 rounded-none border-border/40 bg-card/10 text-[10px] font-mono" + /> + % + +
+ + + + + +
+ + {/* Speed control - continuous % */} +
+
+ +
+ { + const v = parseInt(e.target.value, 10) + if (!isNaN(v) && v >= 1 && v <= 1000) onSpeedChange(v) + }} + min={1} + max={1000} + step={1} + className="h-6 w-16 rounded-none border-border/40 bg-card/10 text-[10px] font-mono text-right" + /> + % +
+
+ onSpeedChange(v)} + min={1} + max={500} + step={1} + className="w-full" + /> +
+ 1% + 100% + 500% +
+
+ + {/* Repeat & Delay */} +
+
+ + { + const v = parseInt(e.target.value, 10) + if (!isNaN(v) && v >= 0) onRepeatCountChange(v) + }} + min={0} + max={999} + step={1} + className="h-7 rounded-none border-border/40 bg-card/10 text-[11px] font-mono" + placeholder="0 = loop" + /> + + 0 = {loop ? "infinite" : "play once"} + +
+ +
+ +
+ { + const v = parseInt(e.target.value, 10) + if (!isNaN(v) && v >= 0) onDelayBetweenChange(v) + }} + min={0} + max={30000} + step={100} + className="h-7 flex-1 rounded-none border-border/40 bg-card/10 text-[11px] font-mono" + /> + ms +
+ + {delayBetweenMs > 0 + ? `${(delayBetweenMs / 1000).toFixed(1)}s hold at end` + : "No delay"} + +
+
+
+ ) +} diff --git a/src/components/component-studio/playground/animation-studio/timeline.tsx b/src/components/component-studio/playground/animation-studio/timeline.tsx new file mode 100644 index 00000000..2ee7e7d0 --- /dev/null +++ b/src/components/component-studio/playground/animation-studio/timeline.tsx @@ -0,0 +1,254 @@ +"use client" + +import { useCallback, useRef, useEffect } from "react" +import { cn } from "@/lib/utils" +import { ZoomIn, ZoomOut } from "lucide-react" +import { Button } from "@/components/ui/button" + +interface TimelineProps { + progress: number + onProgressChange: (progress: number) => void + keyframeOffsets: string[] + selectedKeyframe: string | null + onKeyframeSelect: (offset: string | null) => void + zoom: number + onZoomChange: (zoom: number) => void + duration: number + currentRun: number + totalRuns: number + isInDelay: boolean +} + +function parseOffset(offset: string): number { + // "0%" -> 0, "50%" -> 50, "100%" -> 100, "0%, 100%" -> 0 (take first) + const match = offset.match(/(\d+)%/) + return match ? parseInt(match[1], 10) : 0 +} + +export function Timeline({ + progress, + onProgressChange, + keyframeOffsets, + selectedKeyframe, + onKeyframeSelect, + zoom, + onZoomChange, + duration, + currentRun, + totalRuns, + isInDelay, +}: TimelineProps) { + const scrollRef = useRef(null) + const trackRef = useRef(null) + const isDragging = useRef(false) + + // Generate tick marks based on zoom level + const tickInterval = zoom >= 4 ? 1 : zoom >= 2 ? 2 : 5 + const ticks: number[] = [] + for (let i = 0; i <= 100; i += tickInterval) { + ticks.push(i) + } + + // Major tick marks (always show labels on these) + const majorInterval = zoom >= 4 ? 5 : zoom >= 2 ? 10 : 25 + + const handleTrackInteraction = useCallback( + (e: React.PointerEvent) => { + if (!trackRef.current) return + const rect = trackRef.current.getBoundingClientRect() + const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)) + onProgressChange(Math.round(x * 10000) / 100) // 0.01% precision + }, + [onProgressChange] + ) + + const handlePointerDown = useCallback( + (e: React.PointerEvent) => { + isDragging.current = true + ;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId) + handleTrackInteraction(e) + }, + [handleTrackInteraction] + ) + + const handlePointerMove = useCallback( + (e: React.PointerEvent) => { + if (!isDragging.current) return + handleTrackInteraction(e) + }, + [handleTrackInteraction] + ) + + const handlePointerUp = useCallback(() => { + isDragging.current = false + }, []) + + // Scroll wheel on track = precision scrub (not page scroll) + useEffect(() => { + const el = scrollRef.current + if (!el) return + const handler = (e: WheelEvent) => { + if (e.ctrlKey || e.metaKey) { + // Ctrl+wheel = zoom + e.preventDefault() + const delta = e.deltaY > 0 ? -0.5 : 0.5 + onZoomChange(Math.max(1, Math.min(10, zoom + delta))) + } else { + // Regular wheel = scrub + e.preventDefault() + const scrubAmount = e.deltaY > 0 ? 0.5 : -0.5 + onProgressChange(Math.max(0, Math.min(100, progress + scrubAmount))) + } + } + el.addEventListener("wheel", handler, { passive: false }) + return () => el.removeEventListener("wheel", handler) + }, [zoom, progress, onZoomChange, onProgressChange]) + + // Keep playhead visible during playback + useEffect(() => { + if (!scrollRef.current || !trackRef.current) return + const scroll = scrollRef.current + const track = trackRef.current + const trackWidth = track.offsetWidth + const playheadX = (progress / 100) * trackWidth + const scrollLeft = scroll.scrollLeft + const viewWidth = scroll.offsetWidth + + if (playheadX < scrollLeft + 40 || playheadX > scrollLeft + viewWidth - 40) { + scroll.scrollLeft = playheadX - viewWidth / 2 + } + }, [progress]) + + // Deduplicate offsets and parse positions + const uniqueOffsets = [...new Set(keyframeOffsets)] + const keyframePositions = uniqueOffsets.map((offset) => ({ + offset, + position: parseOffset(offset), + })) + + const msAtProgress = ((progress / 100) * duration).toFixed(0) + + return ( +
+ {/* Zoom controls + run indicator */} +
+
+ + + {zoom.toFixed(1)}x + + +
+ +
+ {totalRuns > 1 && totalRuns < Infinity && ( + + Run {currentRun + 1}/{totalRuns} + + )} + {isInDelay && ( + Delay... + )} +
+
+ + {/* Scrollable track container */} +
+ {/* Track */} +
+ {/* Progress fill */} +
+ + {/* Tick marks */} + {ticks.map((tick) => { + const isMajor = tick % majorInterval === 0 + return ( +
+
+ {isMajor && ( + + {tick} + + )} +
+ ) + })} + + {/* Keyframe diamonds */} + {keyframePositions.map(({ offset, position }) => ( +
+ + {/* Time readout */} +
+ + {progress.toFixed(2)}% · {msAtProgress}ms + + {selectedKeyframe && ( + + Keyframe: {selectedKeyframe} + + )} +
+
+ ) +} diff --git a/src/components/component-studio/playground/behavior-editor.tsx b/src/components/component-studio/playground/behavior-editor.tsx new file mode 100644 index 00000000..920810de --- /dev/null +++ b/src/components/component-studio/playground/behavior-editor.tsx @@ -0,0 +1,126 @@ +"use client" + +import { Label } from "@/components/ui/label" +import type { BehaviorOption } from "./types" +import { StringControl } from "./prop-controls/string-control" +import { BooleanControl } from "./prop-controls/boolean-control" +import { EnumControl } from "./prop-controls/enum-control" +import { NumberControl } from "./prop-controls/number-control" + +interface BehaviorEditorProps { + schema: BehaviorOption[] + behaviorValues: Record + propValues: Record + onChange: (name: string, value: unknown) => void +} + +function isBehaviorVisible( + behavior: BehaviorOption, + behaviorValues: Record, + propValues: Record +): boolean { + if (!behavior.dependsOn) return true + + const source = behavior.dependsOn.source ?? "behavior" + const values = source === "prop" ? propValues : behaviorValues + const depValue = values[behavior.dependsOn.behavior] + + if ("notValue" in behavior.dependsOn && behavior.dependsOn.notValue !== undefined) { + return depValue !== behavior.dependsOn.notValue + } + + if ("value" in behavior.dependsOn && behavior.dependsOn.value !== undefined) { + return depValue === behavior.dependsOn.value + } + + return true +} + +function BehaviorControl({ + schema, + value, + onChange, +}: { + schema: BehaviorOption + value: unknown + onChange: (value: unknown) => void +}) { + switch (schema.type) { + case "string": + return ( + + ) + case "boolean": + return ( + + ) + case "enum": + return ( + + ) + case "number": + return ( + + ) + default: + return null + } +} + +export function BehaviorEditor({ + schema, + behaviorValues, + propValues, + onChange, +}: BehaviorEditorProps) { + const visibleBehaviors = schema.filter((b) => + isBehaviorVisible(b, behaviorValues, propValues) + ) + + return ( +
+ {visibleBehaviors.length === 0 && ( +

+ No behaviors configured for this component. +

+ )} + {visibleBehaviors.map((behavior) => ( +
+ + {behavior.description && ( +

+ {behavior.description} +

+ )} + onChange(behavior.name, v)} + /> +
+ ))} +
+ ) +} diff --git a/src/components/component-studio/playground/code-export-panel.tsx b/src/components/component-studio/playground/code-export-panel.tsx new file mode 100644 index 00000000..6ac18a72 --- /dev/null +++ b/src/components/component-studio/playground/code-export-panel.tsx @@ -0,0 +1,139 @@ +"use client" + +import { useMemo, useState, useCallback } from "react" +import { Check, Copy, Clipboard } from "lucide-react" +import { Button } from "@/components/ui/button" +import type { ComponentRegistration } from "./types" +import { cn } from "@/lib/utils" +import { generateComponentJsx } from "@/components/component-studio/lib/jsx-utils" + +interface CodeExportPanelProps { + registration: ComponentRegistration + props: Record + behaviors: Record +} + + + +function CopyButton({ text, label }: { text: string; label: string }) { + const [copied, setCopied] = useState(false) + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch (err) { + console.error("Failed to copy:", err) + } + }, [text]) + + return ( + + ) +} + +import Prism from "prismjs" +import "prismjs/components/prism-jsx" +import "prismjs/themes/prism-tomorrow.css" // or another dark theme + +function CodeBlock({ code, className }: { code: string; className?: string }) { + const highlightedCode = useMemo(() => { + return Prism.highlight(code, Prism.languages.jsx, "jsx") + }, [code]) + + return ( +
+      
+    
+ ) +} + +export function CodeExportPanel({ + registration, + props, + behaviors, +}: CodeExportPanelProps) { + // Generate the configured usage JSX + const configuredCode = useMemo(() => { + return generateComponentJsx(registration.name, props, behaviors, registration) + }, [registration, props, behaviors]) + + // Generate the base component usage + const baseCode = useMemo(() => { + const componentName = registration.name + const requiredProps = registration.props.filter(p => p.required) + + if (requiredProps.length === 0) { + return `import { ${componentName} } from "@/components/ui/${registration.slug}"\n\n<${componentName} />` + } + + const propsStr = requiredProps + .map(p => { + if (p.type === "string") { + return `${p.name}="..."` + } + return `${p.name}={...}` + }) + .join(" ") + + return `import { ${componentName} } from "@/components/ui/${registration.slug}"\n\n<${componentName} ${propsStr} />` + }, [registration]) + + return ( +
+ {/* Configured Usage */} +
+
+

+ Current Configuration +

+ +
+ +

+ Shows props that differ from defaults. Update props to see changes here. +

+
+ + {/* Base Component */} +
+
+

+ Base Component +

+ +
+ +

+ Minimal usage with import statement. +

+
+
+ ) +} diff --git a/src/components/component-studio/playground/component-preview.tsx b/src/components/component-studio/playground/component-preview.tsx new file mode 100644 index 00000000..9754e8d2 --- /dev/null +++ b/src/components/component-studio/playground/component-preview.tsx @@ -0,0 +1,71 @@ +"use client" + +import React, { useMemo } from "react" +import { Separator } from "@/components/ui/separator" +import type { ComponentRegistration } from "./types" +import { getIconByKey } from "@/components/component-studio/lib/icons" + +interface ComponentPreviewProps { + registration: ComponentRegistration + props: Record + behaviors: Record + animationStyle?: React.CSSProperties + animationKeyframesCSS?: string +} + +export function ComponentPreview({ + registration, + props, + behaviors, + animationStyle, + animationKeyframesCSS, +}: ComponentPreviewProps) { + const Component = registration.component + const Extras = registration.previewExtras + + const resolvedProps = useMemo(() => { + const merged = { ...props, ...behaviors } + if (merged.icon && typeof merged.icon === "string") { + merged.icon = getIconByKey(merged.icon as string) + } + return merged + }, [props, behaviors]) + + const variantProp = registration.props.find( + (p) => p.name === "variant" && p.type === "enum" + ) + + return ( +
+ {Extras && } + {animationKeyframesCSS && ( +