From acf541d256e628359bdb16b0b6b2bacb21a818b9 Mon Sep 17 00:00:00 2001 From: Local Admin Date: Mon, 7 Sep 2026 05:12:58 -0700 Subject: [PATCH 1/7] Initial commot of grok built app --- .grok/app-env.json | 6 + .grok/brand-refs/favicon-16-x8.png | Bin 0 -> 433 bytes .grok/brand-refs/favicon-16.png | Bin 0 -> 159 bytes .grok/brand-refs/favicon-preview.html | 13 + .grok/brand-refs/favicon-preview.png | Bin 0 -> 1393 bytes .grok/favicon-16.png | Bin 0 -> 508 bytes .grok/favicon-32.png | Bin 0 -> 1026 bytes .grok/preview.log | 15 + .grok/references/browser-qa.md | 173 + .grok/references/data-and-auth.md | 100 + .grok/references/deploy-target.md | 40 + .grok/references/generated-art.md | 67 + .grok/references/hibernate-revive.md | 44 + .grok/references/scaffold.md | 80 + .grok/skills/auth/SKILL.md | 110 + .grok/skills/auth/references/grok-identity.md | 64 + .grok/skills/auth/references/per-user-data.md | 49 + .../auth/references/prewired-and-env.md | 54 + .grok/skills/auth/references/session-ui.md | 133 + .../skills/auth/references/sign-in-methods.md | 55 + .grok/skills/auth/references/wiring.md | 71 + .grok/skills/building-games/SKILL.md | 213 + .../building-games/references/3d-libs.md | 64 + .../references/ai-pathfinding.md | 122 + .../skills/building-games/references/audio.md | 129 + .../building-games/references/babylon.md | 202 + .../references/collision-physics.md | 122 + .../references/ecs-architecture.md | 111 + .../references/game-feel-juice.md | 110 + .../references/genres/board-card-chess.md | 89 + .../references/genres/endless-runner.md | 83 + .../building-games/references/genres/fps.md | 105 + .../references/genres/platformer-2d.md | 97 + .../references/genres/puzzle-match3-tetris.md | 131 + .../references/genres/racing-kart.md | 106 + .../references/genres/topdown-twin-stick.md | 97 + .../references/genres/tower-defense.md | 84 + .../references/genres/voxel-minecraft.md | 89 + .../skills/building-games/references/input.md | 139 + .../building-games/references/phaser.md | 277 + .../references/procedural-generation.md | 141 + .../references/save-persistence.md | 108 + .../references/threejs-foundational.md | 148 + .grok/skills/controls/SKILL.md | 274 + .grok/skills/design-ui/SKILL.md | 122 + .../skills/design-ui/references/animations.md | 379 + .../design-ui/references/performance.md | 88 + .../skills/design-ui/references/refined-ui.md | 384 + .grok/skills/design-ui/references/surfaces.md | 247 + .../skills/design-ui/references/typography.md | 123 + .grok/skills/game-animation-frames/SKILL.md | 84 + .grok/skills/game-asset-core/SKILL.md | 69 + .../game-character-consistency/SKILL.md | 69 + .grok/skills/game-tilesets/SKILL.md | 65 + .grok/skills/game-ui-icons/SKILL.md | 56 + .grok/skills/generate2dmap/LICENSE | 21 + .grok/skills/generate2dmap/SKILL.md | 120 + .grok/skills/generate2dmap/SOURCE.md | 6 + .../generate2dmap/references/deliverables.md | 101 + .../references/layered-map-contract.md | 209 + .../references/map-strategies.md | 341 + .../references/object-production-gate.md | 24 + .../generate2dmap/references/pipeline.md | 149 + .../references/prop-pack-contract.md | 174 + .../references/side-scroll-stages.md | 29 + .../scripts/compose_layered_preview.py | 157 + .../scripts/extract_prop_pack.py | 369 + .grok/skills/generate2dsprite/LICENSE | 21 + .grok/skills/generate2dsprite/SKILL.md | 313 + .grok/skills/generate2dsprite/SOURCE.md | 6 + .../generate2dsprite/references/modes.md | 124 + .../references/prompt-rules.md | 315 + .../scripts/generate2dsprite.py | 1031 ++ .../scripts/make_layout_guide.py | 109 + .grok/skills/imagine/SKILL.md | 201 + .grok/skills/multiplayer-p2p/SKILL.md | 111 + .../references/react-binding.md | 144 + .../references/signaling-relay.md | 269 + .grok/skills/neon/SKILL.md | 157 + .grok/skills/og/SKILL.md | 102 + .grok/skills/og/references/brand-pass.md | 53 + .grok/skills/og/references/custom-card.md | 107 + .../skills/og/references/favicon-and-icons.md | 48 + .../skills/og/references/og-type-contract.md | 18 + .../skills/og/references/placeholder-card.md | 21 + .grok/skills/og/references/x-banner.md | 41 + .grok/skills/threejs/SKILL.md | 67 + .grok/skills/threejs/references/llms-full.txt | 2780 +++ .grok/skills/video2dsprite/LICENSE | 21 + .grok/skills/video2dsprite/SKILL.md | 236 + .grok/skills/video2dsprite/SOURCE.md | 6 + .../video2dsprite/references/pipeline.md | 90 + .../video2dsprite/references/prompt-rules.md | 63 + .../video2dsprite/scripts/video2dsprite.py | 490 + .grok/skills/xai-api/SKILL.md | 152 + .grok/status | 1 + .node_modules.lock | 0 .prettierrc | 6 + .project_id | 1 + .vercel/output/config.json | 23 + .../functions/__server.func/.vc-config.json | 7 + .../__server.func/_chunks/ssr-renderer.mjs | 26 + .../_libs/@floating-ui/core+[...].mjs | 671 + .../_libs/@floating-ui/dom+[...].mjs | 649 + .../_libs/@floating-ui/react-dom+[...].mjs | 856 + .../@radix-ui/react-collection+[...].mjs | 823 + .../_libs/@radix-ui/react-dialog+[...].mjs | 1907 ++ .../_libs/@radix-ui/react-popper+[...].mjs | 276 + .../_libs/@radix-ui/react-select+[...].mjs | 912 + .../_libs/@tanstack/react-router+[...].mjs | 14545 ++++++++++++++++ .../_libs/@tanstack/router-core+[...].mjs | 3636 ++++ .../_libs/class-variance-authority+clsx.mjs | 69 + .../__server.func/_libs/h3+rou3+srvx.mjs | 654 + .../__server.func/_libs/h3-v2+rou3.mjs | 291 + .../__server.func/_libs/lucide-react.mjs | 280 + .../__server.func/_libs/radix-ui__number.mjs | 12 + .../_libs/radix-ui__primitive.mjs | 44 + .../_libs/radix-ui__react-direction.mjs | 19 + .../_libs/radix-ui__react-label.mjs | 25 + .../_libs/radix-ui__react-scroll-area.mjs | 663 + .../_libs/radix-ui__react-separator.mjs | 35 + .../_libs/radix-ui__react-slider.mjs | 559 + .../_libs/radix-ui__react-switch.mjs | 190 + .../_libs/radix-ui__react-tooltip.mjs | 495 + .../__server.func/_libs/tailwind-merge.mjs | 3380 ++++ .../__server.func/_libs/tanstack__history.mjs | 393 + .../functions/__server.func/_libs/zod.mjs | 5017 ++++++ .../functions/__server.func/_libs/zustand.mjs | 48 + .../functions/__server.func/_runtime.mjs | 35 + .../_ssr/empty-plugin-adapters-D9UWiqvJ.mjs | 5 + .../__server.func/_ssr/router-DU4M9C_n.mjs | 396 + .../__server.func/_ssr/routes-BVnYdpl0.mjs | 2796 +++ .../functions/__server.func/_ssr/ssr.mjs | 1642 ++ .../__server.func/_ssr/start-5Z2QO8AU.mjs | 4 + .../_tanstack-start-manifest_v-Wibokl5V.mjs | 20 + .../output/functions/__server.func/index.mjs | 655 + .../functions/__server.func/package.json | 9 + .vercel/output/nitro.json | 26 + .vercel/output/static/__grok/icon-180.png | Bin 0 -> 2100 bytes .../assets/homescreen/glass-puzzle.svg | 13 + .../install/assets/homescreen/glass-share.svg | 10 + .../install/assets/homescreen/logo-grok.svg | 6 + .../install/assets/homescreen/ob-ipad.png | Bin 0 -> 101843 bytes .../install/assets/homescreen/ob-phone.png | Bin 0 -> 103612 bytes .../__grok/install/assets/homescreen/plus.svg | 6 + .../output/static/__grok/install/styles.css | 375 + .../output/static/assets/index-uWZOSZRu.js | 60 + .../output/static/assets/routes-DpXahYWx.js | 50 + .../output/static/assets/styles-HHokZl6q.css | 2 + .vercel/output/static/brand/ICCery-logo.svg | 47 + .vercel/output/static/brand/app-icon.svg | 40 + .vercel/output/static/brand/icon.png | Bin 0 -> 16879 bytes .vercel/output/static/favicon.svg | 8 + .vercel/output/static/og.jpg | Bin 0 -> 76220 bytes AGENTS.md | 351 + artifacts/browsed_files/ce5c7a2235626032.text | 617 + eslint.config.mjs | 47 + macos/.gitea/workflows/build-macos.yml | 287 + macos/.gitignore | 6 + macos/Bridging-Header.h | 6 + macos/Info.plist | 102 + macos/LICENCE.md | 9 + macos/README.md | 143 + .../AppIcon.appiconset/Contents.json | 20 + .../AppIcon.appiconset/icon_1024.png | Bin 0 -> 16879 bytes macos/Resources/Assets.xcassets/Contents.json | 6 + macos/Resources/ICCery-logo.svg | 47 + macos/Resources/app-icon.svg | 40 + macos/Resources/dmg/background.png | Bin 0 -> 9018 bytes macos/Resources/dmg/background.svg | 78 + macos/Resources/sample.targetjob | 22 + macos/SPEC.md | 617 + macos/Scripts/build_macos.sh | 55 + macos/Scripts/build_universal.sh | 18 + macos/Scripts/create_dmg.sh | 41 + macos/Scripts/lib.sh | 39 + macos/Scripts/make_universal.sh | 38 + macos/Scripts/package_app.sh | 27 + macos/Scripts/package_release.sh | 55 + macos/Scripts/vendor_bundle.sh | 46 + macos/Sources/AppDelegate.swift | 161 + macos/Sources/AppError.swift | 35 + .../Controllers/PreviewWindowController.swift | 251 + macos/Sources/Logging.swift | 85 + macos/Sources/Models/Geometry.swift | 175 + macos/Sources/Models/PixelIntegrity.swift | 78 + macos/Sources/Models/TargetJob.swift | 183 + macos/Sources/Models/TargetPage.swift | 154 + macos/Sources/Printing/CUPSManager.swift | 236 + macos/Sources/Printing/ColorMatching.swift | 93 + macos/Sources/Printing/PrintEngine.swift | 131 + macos/Sources/Views/DropZoneView.swift | 77 + macos/Sources/Views/InspectorView.swift | 253 + macos/Sources/Views/TargetCanvasView.swift | 207 + .../Sources/Views/ThumbnailSidebarView.swift | 81 + macos/Sources/main.swift | 9 + macos/TargetPrint.entitlements | 17 + macos/TargetPrint.xcodeproj/project.pbxproj | 483 + .../xcschemes/TargetPrint.xcscheme | 88 + macos/Tests/AirPrintTests.swift | 78 + macos/Tests/GeometryTests.swift | 63 + macos/Tests/TargetJobTests.swift | 91 + macos/VENDOR.md | 36 + migrations/auth/0001_auth.sql | 67 + package-lock.json | 7391 ++++++++ package.json | 97 + public/__grok/icon-180.png | Bin 0 -> 2100 bytes .../assets/homescreen/glass-puzzle.svg | 13 + .../install/assets/homescreen/glass-share.svg | 10 + .../install/assets/homescreen/logo-grok.svg | 6 + .../install/assets/homescreen/ob-ipad.png | Bin 0 -> 101843 bytes .../install/assets/homescreen/ob-phone.png | Bin 0 -> 103612 bytes .../__grok/install/assets/homescreen/plus.svg | 6 + public/__grok/install/styles.css | 375 + public/brand/ICCery-logo.svg | 47 + public/brand/app-icon.svg | 40 + public/brand/icon.png | Bin 0 -> 16879 bytes public/favicon.svg | 7 + public/og.jpg | Bin 0 -> 54230 bytes screenshots/.gitkeep | 0 screenshots/after-load.png | Bin 0 -> 63376 bytes screenshots/airprint.png | Bin 0 -> 64660 bytes screenshots/app-builder-built-mobile.png | Bin 0 -> 23369 bytes screenshots/app-builder-built.json | 38 + screenshots/app-builder-built.png | Bin 0 -> 75567 bytes screenshots/app-builder-preview-mobile.png | Bin 0 -> 23365 bytes screenshots/app-builder-preview.json | 36 + screenshots/app-builder-preview.png | Bin 0 -> 75590 bytes screenshots/diagnostics.png | Bin 0 -> 92339 bytes screenshots/page-2.png | Bin 0 -> 62214 bytes screenshots/print-panel.png | Bin 0 -> 70054 bytes scripts/app-env-plugin.mjs | 34 + scripts/brand-check.mjs | 227 + scripts/brand-check.test.mjs | 373 + scripts/browser-guard.mjs | 47 + scripts/browser-smoke-verdict.mjs | 128 + scripts/browser-smoke-verdict.test.mjs | 416 + scripts/browser-smoke.mjs | 177 + scripts/check-auth-invariant.mjs | 109 + scripts/check-auth-invariant.test.mjs | 110 + scripts/grok-pwa-plugin.mjs | 190 + scripts/grok-pwa-plugin.test.mjs | 506 + scripts/grok-pwa-shared.d.mts | 80 + scripts/grok-pwa-shared.mjs | 527 + scripts/install-page.html | 147 + scripts/migrate.mjs | 90 + scripts/migration-plan.mjs | 46 + scripts/migration-plan.test.mjs | 90 + scripts/preview-thumbnail.mjs | 42 + scripts/preview.mjs | 343 + scripts/preview.test.mjs | 257 + scripts/sign-out-plan.mjs | 160 + scripts/sign-out-plan.test.mjs | 248 + scripts/with-app-env.mjs | 130 + scripts/with-app-env.test.mjs | 128 + scripts/write-atomic.mjs | 94 + scripts/write-atomic.test.mjs | 201 + server/middleware/grok-pwa.ts | 111 + server/virtual-grok-og-identity.d.ts | 13 + src/components/app-workspace.tsx | 283 + src/components/diagnostics-drawer.tsx | 213 + src/components/inspector-panel.tsx | 272 + src/components/preview-host-bridge.tsx | 26 + src/components/print-panel.tsx | 187 + src/components/target-canvas.tsx | 223 + src/components/thumbnail-sidebar.tsx | 82 + src/components/ui/badge.tsx | 27 + src/components/ui/button.tsx | 55 + src/components/ui/dialog.tsx | 91 + src/components/ui/input.tsx | 19 + src/components/ui/label.tsx | 15 + src/components/ui/scroll-area.tsx | 23 + src/components/ui/select.tsx | 73 + src/components/ui/separator.tsx | 22 + src/components/ui/sheet.tsx | 45 + src/components/ui/slider.tsx | 20 + src/components/ui/switch.tsx | 24 + src/components/ui/tooltip.tsx | 26 + src/lib/app-data/app-data.test.ts | 498 + src/lib/app-data/client.server.ts | 427 + src/lib/app-data/errors.ts | 78 + src/lib/app-data/index.ts | 21 + src/lib/app-data/login.ts | 33 + src/lib/app-data/readiness-schedule.test.ts | 36 + src/lib/app-data/readiness-schedule.ts | 18 + src/lib/app-data/readiness.ts | 10 + src/lib/app-data/server-only.ts | 11 + src/lib/app-data/types.ts | 50 + src/lib/app-data/use-connector-readiness.ts | 120 + src/lib/auth/client.ts | 236 + src/lib/auth/email-password.ts | 10 + src/lib/auth/gate-identity.server.ts | 206 + src/lib/auth/gate-identity.test.ts | 495 + src/lib/auth/gate-session-marker.ts | 18 + src/lib/auth/gate-session.server.ts | 339 + src/lib/auth/gates.tsx | 132 + src/lib/auth/isolation.server.ts | 52 + src/lib/auth/middleware.ts | 47 + src/lib/auth/pglite-dialect.ts | 138 + src/lib/auth/popup.server.ts | 178 + src/lib/auth/preview.ts | 32 + src/lib/auth/provider.tsx | 15 + src/lib/auth/providers.ts | 31 + src/lib/auth/server.ts | 261 + src/lib/auth/sign-in-gate.test.ts | 30 + src/lib/auth/sign-in-gate.ts | 13 + src/lib/auth/use-current-user.ts | 83 + src/lib/auth/verify.server.ts | 97 + src/lib/db.ts | 238 + src/lib/env.server.ts | 14 + src/lib/error-component.tsx | 29 + src/lib/multiplayer/index.ts | 9 + src/lib/multiplayer/p2p.ts | 570 + src/lib/og/site.json | 6 + src/lib/preview-embedder-origin.ts | 56 + src/lib/preview-host-bridge.ts | 307 + src/lib/targetprint/geometry.ts | 177 + src/lib/targetprint/image-load.ts | 133 + src/lib/targetprint/integrity.ts | 106 + src/lib/targetprint/job.ts | 149 + src/lib/targetprint/printers.ts | 114 + src/lib/targetprint/store.ts | 299 + src/lib/targetprint/targets.ts | 224 + src/lib/targetprint/tiff.ts | 277 + src/lib/targetprint/types.ts | 92 + src/lib/utils.ts | 6 + src/routeTree.gen.ts | 68 + src/router.tsx | 7 + src/routes/__root.tsx | 45 + src/routes/index.tsx | 8 + src/styles.css | 90 + startup.sh | 10 + tsconfig.json | 36 + vite.config.ts | 183 + 334 files changed, 84948 insertions(+) create mode 100644 .grok/app-env.json create mode 100644 .grok/brand-refs/favicon-16-x8.png create mode 100644 .grok/brand-refs/favicon-16.png create mode 100644 .grok/brand-refs/favicon-preview.html create mode 100644 .grok/brand-refs/favicon-preview.png create mode 100644 .grok/favicon-16.png create mode 100644 .grok/favicon-32.png create mode 100644 .grok/preview.log create mode 100644 .grok/references/browser-qa.md create mode 100644 .grok/references/data-and-auth.md create mode 100644 .grok/references/deploy-target.md create mode 100644 .grok/references/generated-art.md create mode 100644 .grok/references/hibernate-revive.md create mode 100644 .grok/references/scaffold.md create mode 100644 .grok/skills/auth/SKILL.md create mode 100644 .grok/skills/auth/references/grok-identity.md create mode 100644 .grok/skills/auth/references/per-user-data.md create mode 100644 .grok/skills/auth/references/prewired-and-env.md create mode 100644 .grok/skills/auth/references/session-ui.md create mode 100644 .grok/skills/auth/references/sign-in-methods.md create mode 100644 .grok/skills/auth/references/wiring.md create mode 100644 .grok/skills/building-games/SKILL.md create mode 100644 .grok/skills/building-games/references/3d-libs.md create mode 100644 .grok/skills/building-games/references/ai-pathfinding.md create mode 100644 .grok/skills/building-games/references/audio.md create mode 100644 .grok/skills/building-games/references/babylon.md create mode 100644 .grok/skills/building-games/references/collision-physics.md create mode 100644 .grok/skills/building-games/references/ecs-architecture.md create mode 100644 .grok/skills/building-games/references/game-feel-juice.md create mode 100644 .grok/skills/building-games/references/genres/board-card-chess.md create mode 100644 .grok/skills/building-games/references/genres/endless-runner.md create mode 100644 .grok/skills/building-games/references/genres/fps.md create mode 100644 .grok/skills/building-games/references/genres/platformer-2d.md create mode 100644 .grok/skills/building-games/references/genres/puzzle-match3-tetris.md create mode 100644 .grok/skills/building-games/references/genres/racing-kart.md create mode 100644 .grok/skills/building-games/references/genres/topdown-twin-stick.md create mode 100644 .grok/skills/building-games/references/genres/tower-defense.md create mode 100644 .grok/skills/building-games/references/genres/voxel-minecraft.md create mode 100644 .grok/skills/building-games/references/input.md create mode 100644 .grok/skills/building-games/references/phaser.md create mode 100644 .grok/skills/building-games/references/procedural-generation.md create mode 100644 .grok/skills/building-games/references/save-persistence.md create mode 100644 .grok/skills/building-games/references/threejs-foundational.md create mode 100644 .grok/skills/controls/SKILL.md create mode 100644 .grok/skills/design-ui/SKILL.md create mode 100644 .grok/skills/design-ui/references/animations.md create mode 100644 .grok/skills/design-ui/references/performance.md create mode 100644 .grok/skills/design-ui/references/refined-ui.md create mode 100644 .grok/skills/design-ui/references/surfaces.md create mode 100644 .grok/skills/design-ui/references/typography.md create mode 100644 .grok/skills/game-animation-frames/SKILL.md create mode 100644 .grok/skills/game-asset-core/SKILL.md create mode 100644 .grok/skills/game-character-consistency/SKILL.md create mode 100644 .grok/skills/game-tilesets/SKILL.md create mode 100644 .grok/skills/game-ui-icons/SKILL.md create mode 100644 .grok/skills/generate2dmap/LICENSE create mode 100644 .grok/skills/generate2dmap/SKILL.md create mode 100644 .grok/skills/generate2dmap/SOURCE.md create mode 100644 .grok/skills/generate2dmap/references/deliverables.md create mode 100644 .grok/skills/generate2dmap/references/layered-map-contract.md create mode 100644 .grok/skills/generate2dmap/references/map-strategies.md create mode 100644 .grok/skills/generate2dmap/references/object-production-gate.md create mode 100644 .grok/skills/generate2dmap/references/pipeline.md create mode 100644 .grok/skills/generate2dmap/references/prop-pack-contract.md create mode 100644 .grok/skills/generate2dmap/references/side-scroll-stages.md create mode 100644 .grok/skills/generate2dmap/scripts/compose_layered_preview.py create mode 100644 .grok/skills/generate2dmap/scripts/extract_prop_pack.py create mode 100644 .grok/skills/generate2dsprite/LICENSE create mode 100644 .grok/skills/generate2dsprite/SKILL.md create mode 100644 .grok/skills/generate2dsprite/SOURCE.md create mode 100644 .grok/skills/generate2dsprite/references/modes.md create mode 100644 .grok/skills/generate2dsprite/references/prompt-rules.md create mode 100644 .grok/skills/generate2dsprite/scripts/generate2dsprite.py create mode 100644 .grok/skills/generate2dsprite/scripts/make_layout_guide.py create mode 100644 .grok/skills/imagine/SKILL.md create mode 100644 .grok/skills/multiplayer-p2p/SKILL.md create mode 100644 .grok/skills/multiplayer-p2p/references/react-binding.md create mode 100644 .grok/skills/multiplayer-p2p/references/signaling-relay.md create mode 100644 .grok/skills/neon/SKILL.md create mode 100644 .grok/skills/og/SKILL.md create mode 100644 .grok/skills/og/references/brand-pass.md create mode 100644 .grok/skills/og/references/custom-card.md create mode 100644 .grok/skills/og/references/favicon-and-icons.md create mode 100644 .grok/skills/og/references/og-type-contract.md create mode 100644 .grok/skills/og/references/placeholder-card.md create mode 100644 .grok/skills/og/references/x-banner.md create mode 100644 .grok/skills/threejs/SKILL.md create mode 100644 .grok/skills/threejs/references/llms-full.txt create mode 100644 .grok/skills/video2dsprite/LICENSE create mode 100644 .grok/skills/video2dsprite/SKILL.md create mode 100644 .grok/skills/video2dsprite/SOURCE.md create mode 100644 .grok/skills/video2dsprite/references/pipeline.md create mode 100644 .grok/skills/video2dsprite/references/prompt-rules.md create mode 100644 .grok/skills/video2dsprite/scripts/video2dsprite.py create mode 100644 .grok/skills/xai-api/SKILL.md create mode 100644 .grok/status create mode 100644 .node_modules.lock create mode 100644 .prettierrc create mode 100644 .project_id create mode 100644 .vercel/output/config.json create mode 100644 .vercel/output/functions/__server.func/.vc-config.json create mode 100644 .vercel/output/functions/__server.func/_chunks/ssr-renderer.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/@floating-ui/core+[...].mjs create mode 100644 .vercel/output/functions/__server.func/_libs/@floating-ui/dom+[...].mjs create mode 100644 .vercel/output/functions/__server.func/_libs/@floating-ui/react-dom+[...].mjs create mode 100644 .vercel/output/functions/__server.func/_libs/@radix-ui/react-collection+[...].mjs create mode 100644 .vercel/output/functions/__server.func/_libs/@radix-ui/react-dialog+[...].mjs create mode 100644 .vercel/output/functions/__server.func/_libs/@radix-ui/react-popper+[...].mjs create mode 100644 .vercel/output/functions/__server.func/_libs/@radix-ui/react-select+[...].mjs create mode 100644 .vercel/output/functions/__server.func/_libs/@tanstack/react-router+[...].mjs create mode 100644 .vercel/output/functions/__server.func/_libs/@tanstack/router-core+[...].mjs create mode 100644 .vercel/output/functions/__server.func/_libs/class-variance-authority+clsx.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/h3+rou3+srvx.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/h3-v2+rou3.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/lucide-react.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/radix-ui__number.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/radix-ui__primitive.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/radix-ui__react-direction.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/radix-ui__react-label.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/radix-ui__react-scroll-area.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/radix-ui__react-separator.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/radix-ui__react-slider.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/radix-ui__react-switch.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/radix-ui__react-tooltip.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/tailwind-merge.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/tanstack__history.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/zod.mjs create mode 100644 .vercel/output/functions/__server.func/_libs/zustand.mjs create mode 100644 .vercel/output/functions/__server.func/_runtime.mjs create mode 100644 .vercel/output/functions/__server.func/_ssr/empty-plugin-adapters-D9UWiqvJ.mjs create mode 100644 .vercel/output/functions/__server.func/_ssr/router-DU4M9C_n.mjs create mode 100644 .vercel/output/functions/__server.func/_ssr/routes-BVnYdpl0.mjs create mode 100644 .vercel/output/functions/__server.func/_ssr/ssr.mjs create mode 100644 .vercel/output/functions/__server.func/_ssr/start-5Z2QO8AU.mjs create mode 100644 .vercel/output/functions/__server.func/_tanstack-start-manifest_v-Wibokl5V.mjs create mode 100644 .vercel/output/functions/__server.func/index.mjs create mode 100644 .vercel/output/functions/__server.func/package.json create mode 100644 .vercel/output/nitro.json create mode 100644 .vercel/output/static/__grok/icon-180.png create mode 100644 .vercel/output/static/__grok/install/assets/homescreen/glass-puzzle.svg create mode 100644 .vercel/output/static/__grok/install/assets/homescreen/glass-share.svg create mode 100644 .vercel/output/static/__grok/install/assets/homescreen/logo-grok.svg create mode 100644 .vercel/output/static/__grok/install/assets/homescreen/ob-ipad.png create mode 100644 .vercel/output/static/__grok/install/assets/homescreen/ob-phone.png create mode 100644 .vercel/output/static/__grok/install/assets/homescreen/plus.svg create mode 100644 .vercel/output/static/__grok/install/styles.css create mode 100644 .vercel/output/static/assets/index-uWZOSZRu.js create mode 100644 .vercel/output/static/assets/routes-DpXahYWx.js create mode 100644 .vercel/output/static/assets/styles-HHokZl6q.css create mode 100644 .vercel/output/static/brand/ICCery-logo.svg create mode 100644 .vercel/output/static/brand/app-icon.svg create mode 100644 .vercel/output/static/brand/icon.png create mode 100644 .vercel/output/static/favicon.svg create mode 100644 .vercel/output/static/og.jpg create mode 100644 AGENTS.md create mode 100644 artifacts/browsed_files/ce5c7a2235626032.text create mode 100644 eslint.config.mjs create mode 100644 macos/.gitea/workflows/build-macos.yml create mode 100644 macos/.gitignore create mode 100644 macos/Bridging-Header.h create mode 100644 macos/Info.plist create mode 100644 macos/LICENCE.md create mode 100644 macos/README.md create mode 100644 macos/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 macos/Resources/Assets.xcassets/AppIcon.appiconset/icon_1024.png create mode 100644 macos/Resources/Assets.xcassets/Contents.json create mode 100644 macos/Resources/ICCery-logo.svg create mode 100644 macos/Resources/app-icon.svg create mode 100644 macos/Resources/dmg/background.png create mode 100644 macos/Resources/dmg/background.svg create mode 100644 macos/Resources/sample.targetjob create mode 100644 macos/SPEC.md create mode 100644 macos/Scripts/build_macos.sh create mode 100644 macos/Scripts/build_universal.sh create mode 100644 macos/Scripts/create_dmg.sh create mode 100644 macos/Scripts/lib.sh create mode 100644 macos/Scripts/make_universal.sh create mode 100644 macos/Scripts/package_app.sh create mode 100644 macos/Scripts/package_release.sh create mode 100644 macos/Scripts/vendor_bundle.sh create mode 100644 macos/Sources/AppDelegate.swift create mode 100644 macos/Sources/AppError.swift create mode 100644 macos/Sources/Controllers/PreviewWindowController.swift create mode 100644 macos/Sources/Logging.swift create mode 100644 macos/Sources/Models/Geometry.swift create mode 100644 macos/Sources/Models/PixelIntegrity.swift create mode 100644 macos/Sources/Models/TargetJob.swift create mode 100644 macos/Sources/Models/TargetPage.swift create mode 100644 macos/Sources/Printing/CUPSManager.swift create mode 100644 macos/Sources/Printing/ColorMatching.swift create mode 100644 macos/Sources/Printing/PrintEngine.swift create mode 100644 macos/Sources/Views/DropZoneView.swift create mode 100644 macos/Sources/Views/InspectorView.swift create mode 100644 macos/Sources/Views/TargetCanvasView.swift create mode 100644 macos/Sources/Views/ThumbnailSidebarView.swift create mode 100644 macos/Sources/main.swift create mode 100644 macos/TargetPrint.entitlements create mode 100644 macos/TargetPrint.xcodeproj/project.pbxproj create mode 100644 macos/TargetPrint.xcodeproj/xcshareddata/xcschemes/TargetPrint.xcscheme create mode 100644 macos/Tests/AirPrintTests.swift create mode 100644 macos/Tests/GeometryTests.swift create mode 100644 macos/Tests/TargetJobTests.swift create mode 100644 macos/VENDOR.md create mode 100644 migrations/auth/0001_auth.sql create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/__grok/icon-180.png create mode 100644 public/__grok/install/assets/homescreen/glass-puzzle.svg create mode 100644 public/__grok/install/assets/homescreen/glass-share.svg create mode 100644 public/__grok/install/assets/homescreen/logo-grok.svg create mode 100644 public/__grok/install/assets/homescreen/ob-ipad.png create mode 100644 public/__grok/install/assets/homescreen/ob-phone.png create mode 100644 public/__grok/install/assets/homescreen/plus.svg create mode 100644 public/__grok/install/styles.css create mode 100644 public/brand/ICCery-logo.svg create mode 100644 public/brand/app-icon.svg create mode 100644 public/brand/icon.png create mode 100644 public/favicon.svg create mode 100644 public/og.jpg create mode 100644 screenshots/.gitkeep create mode 100644 screenshots/after-load.png create mode 100644 screenshots/airprint.png create mode 100644 screenshots/app-builder-built-mobile.png create mode 100644 screenshots/app-builder-built.json create mode 100644 screenshots/app-builder-built.png create mode 100644 screenshots/app-builder-preview-mobile.png create mode 100644 screenshots/app-builder-preview.json create mode 100644 screenshots/app-builder-preview.png create mode 100644 screenshots/diagnostics.png create mode 100644 screenshots/page-2.png create mode 100644 screenshots/print-panel.png create mode 100644 scripts/app-env-plugin.mjs create mode 100644 scripts/brand-check.mjs create mode 100644 scripts/brand-check.test.mjs create mode 100644 scripts/browser-guard.mjs create mode 100644 scripts/browser-smoke-verdict.mjs create mode 100644 scripts/browser-smoke-verdict.test.mjs create mode 100644 scripts/browser-smoke.mjs create mode 100644 scripts/check-auth-invariant.mjs create mode 100644 scripts/check-auth-invariant.test.mjs create mode 100644 scripts/grok-pwa-plugin.mjs create mode 100644 scripts/grok-pwa-plugin.test.mjs create mode 100644 scripts/grok-pwa-shared.d.mts create mode 100644 scripts/grok-pwa-shared.mjs create mode 100644 scripts/install-page.html create mode 100644 scripts/migrate.mjs create mode 100644 scripts/migration-plan.mjs create mode 100644 scripts/migration-plan.test.mjs create mode 100644 scripts/preview-thumbnail.mjs create mode 100644 scripts/preview.mjs create mode 100644 scripts/preview.test.mjs create mode 100644 scripts/sign-out-plan.mjs create mode 100644 scripts/sign-out-plan.test.mjs create mode 100644 scripts/with-app-env.mjs create mode 100644 scripts/with-app-env.test.mjs create mode 100644 scripts/write-atomic.mjs create mode 100644 scripts/write-atomic.test.mjs create mode 100644 server/middleware/grok-pwa.ts create mode 100644 server/virtual-grok-og-identity.d.ts create mode 100644 src/components/app-workspace.tsx create mode 100644 src/components/diagnostics-drawer.tsx create mode 100644 src/components/inspector-panel.tsx create mode 100644 src/components/preview-host-bridge.tsx create mode 100644 src/components/print-panel.tsx create mode 100644 src/components/target-canvas.tsx create mode 100644 src/components/thumbnail-sidebar.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/separator.tsx create mode 100644 src/components/ui/sheet.tsx create mode 100644 src/components/ui/slider.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/components/ui/tooltip.tsx create mode 100644 src/lib/app-data/app-data.test.ts create mode 100644 src/lib/app-data/client.server.ts create mode 100644 src/lib/app-data/errors.ts create mode 100644 src/lib/app-data/index.ts create mode 100644 src/lib/app-data/login.ts create mode 100644 src/lib/app-data/readiness-schedule.test.ts create mode 100644 src/lib/app-data/readiness-schedule.ts create mode 100644 src/lib/app-data/readiness.ts create mode 100644 src/lib/app-data/server-only.ts create mode 100644 src/lib/app-data/types.ts create mode 100644 src/lib/app-data/use-connector-readiness.ts create mode 100644 src/lib/auth/client.ts create mode 100644 src/lib/auth/email-password.ts create mode 100644 src/lib/auth/gate-identity.server.ts create mode 100644 src/lib/auth/gate-identity.test.ts create mode 100644 src/lib/auth/gate-session-marker.ts create mode 100644 src/lib/auth/gate-session.server.ts create mode 100644 src/lib/auth/gates.tsx create mode 100644 src/lib/auth/isolation.server.ts create mode 100644 src/lib/auth/middleware.ts create mode 100644 src/lib/auth/pglite-dialect.ts create mode 100644 src/lib/auth/popup.server.ts create mode 100644 src/lib/auth/preview.ts create mode 100644 src/lib/auth/provider.tsx create mode 100644 src/lib/auth/providers.ts create mode 100644 src/lib/auth/server.ts create mode 100644 src/lib/auth/sign-in-gate.test.ts create mode 100644 src/lib/auth/sign-in-gate.ts create mode 100644 src/lib/auth/use-current-user.ts create mode 100644 src/lib/auth/verify.server.ts create mode 100644 src/lib/db.ts create mode 100644 src/lib/env.server.ts create mode 100644 src/lib/error-component.tsx create mode 100644 src/lib/multiplayer/index.ts create mode 100644 src/lib/multiplayer/p2p.ts create mode 100644 src/lib/og/site.json create mode 100644 src/lib/preview-embedder-origin.ts create mode 100644 src/lib/preview-host-bridge.ts create mode 100644 src/lib/targetprint/geometry.ts create mode 100644 src/lib/targetprint/image-load.ts create mode 100644 src/lib/targetprint/integrity.ts create mode 100644 src/lib/targetprint/job.ts create mode 100644 src/lib/targetprint/printers.ts create mode 100644 src/lib/targetprint/store.ts create mode 100644 src/lib/targetprint/targets.ts create mode 100644 src/lib/targetprint/tiff.ts create mode 100644 src/lib/targetprint/types.ts create mode 100644 src/lib/utils.ts create mode 100644 src/routeTree.gen.ts create mode 100644 src/router.tsx create mode 100644 src/routes/__root.tsx create mode 100644 src/routes/index.tsx create mode 100644 src/styles.css create mode 100644 startup.sh create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.grok/app-env.json b/.grok/app-env.json new file mode 100644 index 0000000..bf303ce --- /dev/null +++ b/.grok/app-env.json @@ -0,0 +1,6 @@ +{ + "VITE_AUTH_ENABLED": "false", + "deploy": { + "database": false + } +} diff --git a/.grok/brand-refs/favicon-16-x8.png b/.grok/brand-refs/favicon-16-x8.png new file mode 100644 index 0000000000000000000000000000000000000000..1ec41a9ea36674a9d614140738be9d882073a3ac GIT binary patch literal 433 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V65mdKI;Vst0K}_|W&i*H literal 0 HcmV?d00001 diff --git a/.grok/brand-refs/favicon-16.png b/.grok/brand-refs/favicon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..31f3fc21b3fa064b928dd27e057886846b5b0937 GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|A)YRdAr*7(T-wNaz=5On;q)gP zO85(Po)=lGZ`j76GC}QZB+H@Nsjc;j77QVxdhh1VtVm2V-u?XT&3RiEuauQhTD0o( z!48g+n)<_B2Nf2DeK;I%#mK<$|G$%>^kks|HNH@*M> literal 0 HcmV?d00001 diff --git a/.grok/brand-refs/favicon-preview.html b/.grok/brand-refs/favicon-preview.html new file mode 100644 index 0000000..652956c --- /dev/null +++ b/.grok/brand-refs/favicon-preview.html @@ -0,0 +1,13 @@ + + + +
+
+
+
+
diff --git a/.grok/brand-refs/favicon-preview.png b/.grok/brand-refs/favicon-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..bb0e7e46e68642480667e0c49d353d41cce8b62e GIT binary patch literal 1393 zcmZuxeKgYx82&Y1Yh96IMP)|w8Lndqxs1`~EA3+8j_$HVv###0R4ktzjGA0h;hI?1 zM7oIPqZFMH8Jo&CuAn%}*Bn~*}*R`ZM$V!8E^xd{826`#@r zvE@@!OCWBaMN9fz(CU7zY2jphpRLcW1}8c9e$(YN>?gVP|6l_qK6x z2<8&UYnQEInC_v3dDWg;e|=YF-c2TxnR3qB&d$!+xjJ8<49|wG!OP=Ljlb$Ayk6ig zcSysetE=}{#J6tkd9*7Up~@x^vgHfzo{e>e(^o}D&X+9~44d*=T3V8l9uE%nzo{Z* zw-Z4-NW*{d0%MVzT&U>?F^B>sRgEJ=lkH^Lr<#H%RhDbXbBA#wta~$QOy+V*O3F16 z82x*UAr6L2ztroEFl_pjFx%PL@NxLZ*!sIm;Vi8{dB#+m#L>*n%--(4AhzpKPm$x5 z#Ytn(@$jD*AU5hY%}?84dmltgy!)M z6?Wbi0p&|{+^GES2@6S4{k!}+HX`moWC;9pNWG}<1G-?A6MMay!@q$;!W)HjQKu8v z|Dk73tDfk4M-p%FLmCG1Z0RwNkgXdYzT$L)ssYKUB~dP)B~IROL8rkmhzm#|42vXj zx(EGPdxHi|9=wGYU(=@#u?M?#Br;ptgj13@$`<2u!I8fZ71fViDXdU=(%ly(J;aL8 z&l<|;YgDV_h zJT=EwS<_~tC%?E^8?kN;HSzS*{E> zQ%2b|avEs~>xs>Dx>Vpkt_k*Hh@(!{{^C8aXbp@k3>9e|0v^awpccQch+r~m$QM4P z8i2J3>aU>5Fa?PBPW{)F-z;0AU~JC@N8}t$MSh(0MdY^G9lQW4U^8LAQK{gu+TpDy zSyv(Nyb-5@0{!lZK(F42jrIBhKu>8W0N|_dSy#^FN#;CEc0-No4%cvt7eBT1PxIBZ z6E!-T6G7}d;)e0u7!;(8QYuP5*5NZRqXvfZV^GkYP(5*_rb81?_3Imh!)qBe;)aKG z*hNju%W3VR6R@~GC$Ren+E@piJcTlOk8r4Qv94P#$=i1mPbEHsV@7*peh%}V|F5L~ qPoYn003erNt8Tch2u{^6iKzsj3!GTWSS2gP2Ee-eqN`j_CjSF|>zVHW literal 0 HcmV?d00001 diff --git a/.grok/favicon-16.png b/.grok/favicon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..ecded0c6933233e4f0de414f9cd1fd5c37fe2b9d GIT binary patch literal 508 zcmVJSyQS(cS+DhARaBB(V~%su?LivP@GxiebqzonAW7 z+cQ1MuBxge2*Pgv_WOzC4@h!J!-IW%-WrbMp->n^6fFQBp+s?`yUmheYJx2l?HG)v z(Q!^wW4+VqV)YDHFc=H-vu@9?#rm1TfypfAahFZz$5rljJebetTUy~3((tW~mFL$t zo9zpfd8Dngy$!>#UEzKvOMzGpruNq61PACzZ#RoZqkUMcq;_nA1Gh$fN{V&0HC~_B zaBqUX#Y&38iAE{^AQFwFcWG{hv%0)cUE2^0hT^fwJdoM?{vubZj!(`K9P|bw5eV$^ zH#5V_rF+0$tGgq`n1jRN6iXEBkwK+QT!`bO5*`O~Nf)aVM&b5&Qc5B*GStuCTE|cn z28SRBm6{(4hXo?pwN{_bA&#;#dPiH!;`|(RKy`Rzet2^IxRXDmFCWx)exYG9%HgfP yyrN_P$#uQ|00030|H%uX3IG5A21!IgR09C%FrH%q2Nya30000$52PS@bKfayWoq0PmJG;bP6m4WHp@D2M zi1??nZLHZWYe3rCJKElN;9RvC2P|tr@O40hsR(`txI(0%p`*i{@AE62m6y`@(u;nZ>hAgspWpcqshrB6&5tNZQs6;&ez*BEAxq&sR@aM z5y@>NlA!_9+sl1wO4QxchiqyG+S-(qq(ldMJ5(|{I<~a5tgo*N_|W%Z5|Mb|00Y(5 z);ETP$IQ;oDx1QHBaukV&CWor4}Kfsow;!Q1gE&997>AcmfPAnxt#L^Dk$MNp32S? zVrB(H6dM(O%?j@y_aA?bBMMs3Hcc7mw@0v%$)}lKPCau)YJ?N z@K@!4L?ZFHPLK;iqcD@E3@|MsIu3Fk$59~@>jt)L8(8mO2Z!*VReWVQUX zSvFSkDp`TUK4WQVfhq(y0u-zW(=x!fk#>1`1(Iw2ek^M%v|q{20ayV0j0zlLnxlMh z;G^ov!qiOoH~*?#gB^h$X=G%;ihc#fI%o< z*D8#S46DlDAZv}x)Pkj80u3X&eG~)&;Zt`fC_=!Uc&-DVzo4phAp$3MN+begraNF=|KQqm!J;d3+6lIc1+8=2m_cTZ+|GS69d_#Y-d zmd?$oae%CDGrz8(335Kysj;@q=b{Idy6)Q8y_*Vz4b`Qn^{Se>e5}I5zHlfM%G0cL z8!JnBp-41Mr_)x6c$%(5lUKEObY8g{gxR~Wugt~8#gej05CL;j6I7_BMV1jVS$=X} zeqqqfu!V&M1Pf{|eeik^1P6lK4OF^43CV0-v preview +> node scripts/with-app-env.mjs vite preview + + + ╭────────── [Build Info] ─────────────╮ + │ │ + │ - Build Directory: .vercel/output │ + │ - Date: 9/7/2026, 10:13:24 AM │ + │ - Nitro Version: 3.0.260610-beta │ + │ - Nitro Preset: vercel │ + │ │ + ╰─────────────────────────────────────╯ + + ➜ Local: http://127.0.0.1:8081/ diff --git a/.grok/references/browser-qa.md b/.grok/references/browser-qa.md new file mode 100644 index 0000000..15457e5 --- /dev/null +++ b/.grok/references/browser-qa.md @@ -0,0 +1,173 @@ +# Browser QA (agent-driven only; the user is not your QA) + +`AGENTS.md` § "Execution loop" states the mandatory pass. This is the menu of +capabilities and the depth judgment around it. + +Everything here runs **in the sandbox** against `http://127.0.0.1:8080` — it is +**not** the user's Grok chat tab. Use whatever browser capability you have +**yourself**, so quality beats curl-only. + +1. **Grok browser / computer-use / MCP browser tools**, if listed — open + `http://127.0.0.1:8080`, glance at the UI, screenshot if supported. +2. **`web_fetch`** on that URL for an HTML-only check. +3. **Playwright helper (preinstalled)** — one run loads desktop **and** mobile, + screenshots both, and prints a JSON verdict. + +```bash +mkdir -p /workspace/screenshots +node scripts/browser-smoke.mjs http://127.0.0.1:8080/ /workspace/screenshots/app-builder-preview.png +# Writes app-builder-preview.png (desktop), -mobile.png, and .json (verdict). +# Then Read BOTH PNGs in one batched read if you have an image tool, and iterate if either looks wrong. +``` + +One run audits desktop (1280×800) **and** mobile (390×844): two PNGs (mobile +gets a `-mobile` suffix) plus a JSON verdict (per-viewport console/page errors, +body text, horizontal overflow) on stdout and next to the PNG (`.json`). It +writes under `/workspace/screenshots/` by default; pass an explicit path only +for a different name **under that directory**. + +4. **`agent-browser` (preinstalled)** — the interactive pass: click, type, hold + keys, read state back out. § "Interactive QA" below; it is also what the + `controls` §5c self-test runs. + +## Built output + +Serve the build with `npm run preview:restart` (loopback `127.0.0.1:8081`) and +reuse the dev verdict as a baseline — the JSON reports `divergesFromBaseline`, so +you only re-read the built screenshots when it flags. + +```bash +npm run preview:restart # built output on 127.0.0.1:8081 — QA only, never the live preview +node scripts/browser-smoke.mjs http://127.0.0.1:8081/ /workspace/screenshots/app-builder-built.png --baseline /workspace/screenshots/app-builder-preview.json +npm run preview:stop # frees :8081 when the built-output QA is done +``` + +`preview:restart` (`scripts/preview.mjs`) kills whatever holds `:8081` — the +port's owner, whoever started it — then serves the current build in the +background and returns once it answers. Use it instead of bare +`npm run preview`: `vite preview` is strictPort, so a preview left running from +an earlier build both fails a plain start and keeps serving stale output. + +## Interactive QA — `agent-browser` + +```bash +agent-browser open http://127.0.0.1:8080/ +agent-browser snapshot -i # a11y tree with @e1 refs +agent-browser click @e1 # or: find text "Start" click +agent-browser fill "#email" you@example.com # type for keystrokes +agent-browser press Enter # a tap; holding: see "Keys" +agent-browser wait --fn "window.__ready === true" # or: wait 500 (ms) +agent-browser eval "document.querySelectorAll('.card').length" +agent-browser console # or: errors (page errors) +agent-browser screenshot /workspace/screenshots/step.png +agent-browser close +``` + +**Any multi-verb flow goes in one `batch`.** Each invocation is a tool call of +its own, so a 10-verb flow costs ten round trips; `batch` runs the flow in one. +Reach for it by default and drop to single commands only for a one-off probe. +It takes the commands as a JSON array of string arrays on stdin, which also +sidesteps the shell quoting a flow with quotes in its `eval` would need: + +```bash +agent-browser batch --bail <<'JSON' +[["open","http://127.0.0.1:8080/"], + ["find","text","Add note","click"], + ["fill","#title","Grocery list"], + ["press","Enter"], + ["wait","300"], + ["eval","if (![...document.querySelectorAll('li')].some(n => n.textContent.includes('Grocery list'))) throw Error('note did not appear in the list')"], + ["screenshot","/workspace/screenshots/note-added.png"]] +JSON +``` + +That is the shape for any app: drive the thing a user would do, then assert the +result with a throwing `eval`. It exits 0 when the note lands and 1 when it does +not, so one tool call is the whole verdict. No `close` — the page is still there +for the next check. + +`--bail` stops at the first failing command; without it the later ones still +run. Either way a failed step exits non-zero, so a batch is a verdict. That is +how to keep an interactive pass cheap — one tool call instead of one per verb. +`controls` §5c is the worked example. + +**`open` once.** The page outlives the command: a later invocation, batched or +not, keeps the DOM and any `window` state you set. A second `open` re-navigates +and **discards that state**, so starting every batch with one both costs a page +load and throws away the state you just built. Open at the start of a pass and +again only after a rebuild, or to reset deliberately. + +**Match the depth of the pass to the app.** A page that only renders is done +once the smoke screenshots look right — the interactive verbs are for apps with +something to drive. Verify each behaviour once; re-run a check after you change +the code it covers, not otherwise. + +**A `✓` from `open` is not "the page loaded".** An HTTP error page reports +`✓` too (a 404 as `✓ Error response`), so assert on page content — a `find`, a +throwing `eval` — rather than trusting the tick. + +The image build asserts the core verbs stay in the CLI's subcommand list. +**Their arguments and flags are not checked** — they are upstream's, and +upstream ships ~17 releases a month, so a manual here would be stale on arrival: +if a command rejects what you typed, `agent-browser --help` decides, not +this file. + +**`@ref`s go stale.** A `@e1` is valid only for the `snapshot` that produced +it, so re-snapshot after any DOM change — navigation, a click that re-renders, +a route change — rather than reusing an old ref. It is the most common misuse. + +**`find text` is case-sensitive.** It matches a substring of an element's +trimmed text — `find text "Play" click` hits `Play now` — but `start` will not +find `Start`. Copy the label from `snapshot -i`; guessing it costs a failed +command and a re-snapshot. + +**Keys.** `press ` sends a real `event.code` (`press d` → `KeyD`) and is +the only single-key command `--help` documents; raw keystrokes at the current +focus, with no selector, are `keyboard type `. `keydown`/`keyup` exist but +are undocumented and send **`event.code === ""`**, so a game that tracks +`event.code` never sees them — do not build a check on either. Hold keys from +inside the page instead: the game's own probe (`controls` +§5b), or dispatch the event yourself, on `document.body` with `bubbles: true` +so listeners on `document` or `window` see it — dispatching on `window` itself +does not reach a `document` listener: + +```bash +agent-browser eval "document.body.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyA', key: 'a', bubbles: true }))" +``` + +**Falsy is not failure — throw.** `eval "false"` exits **0**; `eval "if (!cond) +throw Error('why')"` exits **1**. Throwing is what turns a check into a verdict +instead of a line of output you have to read yourself; `controls` §5c is the +worked example. Anything past one expression goes in an IIFE — +`eval "(() => { … })()"`, the form the image build asserts and the only one you +can re-run (a bare `const x = …` fails the second time with "already +declared") — or through `eval --stdin` with a heredoc, or `eval -b `, +when the quoting gets awkward. `eval` awaits a promise, so an `async` IIFE +works and its rejection still exits 1: that is how you time something in page +time rather than across command round-trips. + +**Output modes.** Plain output is a line per command (`eval "1+1"` prints `2`) +and is the default for a reason: `--json` wraps each one in a repeated +`lifecycle` block, measured at ~1 KB for three commands whichever three, so it +is worth it only when you are parsing. Same for +snapshots: prefer `snapshot -i | grep -i ""` or `snapshot -s "#sel"`, +and keep a bare `snapshot` for a page you cannot otherwise navigate. + +**Operating notes** (conventions, not enforcement): loopback targets and +screenshots under `/workspace/screenshots/`, the same rules as everywhere else +here; a `--session ` per flow if you ever run two at once; if a command +hangs, `agent-browser close` and retry. + +### Fallback + +`agent-browser` is the tool for interactive QA. If it fails **twice on the same +step** — daemon wedged, a command it does not support — fall back to a short +Playwright script (still installed) and say so in one line of your summary. + +## How deep to go + +Depth **beyond the mandatory smoke pass + screenshot read** is your judgment: a +landing page usually needs nothing more than that pass. For a game with WASD / +vehicles / flight, still verify control signs (A left / D right from a chase +cam) per `.grok/skills/controls/SKILL.md` — you don't have to play end-to-end, +but inverted A/D must not ship. diff --git a/.grok/references/data-and-auth.md b/.grok/references/data-and-auth.md new file mode 100644 index 0000000..de2208f --- /dev/null +++ b/.grok/references/data-and-auth.md @@ -0,0 +1,100 @@ +# Data & auth — implementation + +Read this **after** `AGENTS.md` §0.5 has already said the app needs a database +and/or sign-in. The decision (auth OFF by default, the closed trigger list, "no +migrations / no `@/lib/db` unless triggered") lives in `AGENTS.md`, not here. + +Full guides + snippets: the **`neon` skill** (database) and the **`auth` skill** +(sign-in), under `.grok/skills/`. + +## Database (`@/lib/db`, server-only) + +- `const sql = await getSql()` from `@/lib/db`. Use it **only** inside + `createServerFn` handlers / server loaders. +- Dual-mode: a regular Postgres driver (node-postgres, `pg`) when `DATABASE_URL` + is set, else a local **PGLite** fallback — so the preview always renders. +- In preview, PGLite **bootstraps at server start** (`ensureDbReady`) once the + app has migrations. Do not remove that. +- A deployed app is provisioned a real database when it ships `migrations/*.sql` + or sign-in. An app that needs one without either says so with + `"deploy": {"database": true}` in `.grok/app-env.json` — see the `neon` skill. + +## Migrations + +- `migrations/*.sql` is the single schema source: applied to **Neon on deploy** + (`npm run build` runs `npm run db:migrate`, so Vercel ships with the schema + ready) and to the **PGLite** preview automatically on startup. +- Add the app's tables as ordered files (`migrations/0002_*.sql`), not inline. +- An app that needs no database adds no `.sql` file, and then no migration runs + anywhere. +- The Better Auth schema sits outside that scope in `migrations/auth/` (neither + applier descends into it); the `auth` skill's "Turning sign-in on" copies it + up. Do not edit it. Applied files are keyed by **basename**, so a database + that already has `0001_auth.sql` will not re-run it. + +## Server functions + +`createServerFn` with input via `.validator()` — the current API on the +installed version (`.inputValidator()` is deprecated). Examples in the `neon` +and `auth` skills. + +## Auth wiring (only once §0.5 says accounts) + +- The app runs its **own** Better Auth at `/api/auth/*` and federates to the + shared Grok auth broker for **Google** and **X**. The only other supported + method is this app's own **email/password** (local Better Auth, off by + default — enable only via `src/lib/auth/email-password.ts`; **never rewrite** + `src/lib/auth/server.ts`). No other social providers, magic links, passkeys, + or OTP/phone. +- Two routes: `src/routes/api/auth/$.ts` (mounts Better Auth at `/api/auth/*`) + and `src/routes/login.tsx` (provider buttons via `signIn(providerId)`). Copy + the snippets from the `auth` skill. +- The live-preview popup at `/auth/popup` is served by the template Vite plugin + (`vite.config.ts` → `popup.server.ts`); `AGENTS.md` § "First scaffold" states + the rule about not adding a React route there. +- Read the user with `useCurrentUser()` (`@/lib/auth/use-current-user`) and gate + UI with `SignedIn` / `SignedOut` / `UserButton` (`@/lib/auth/gates`). +- Sign-in is **real even in the live preview** — it federates via a baked shared + preview client — so a visitor is signed out until they sign in. Build real + sign-in; do **NOT** scaffold demo/mock/hardcoded users. +- **Authorize every server function** with `authMiddleware` + (`@/lib/auth/middleware`): `createServerFn().middleware([authMiddleware])` + hands the handler a **verified** `context.userId` (resolved from the + same-origin session; throws when signed out). Scope **every** query by that + `user_id`. Never trust a client-sent id. + +## Turning sign-in on (at scaffold or later) + +`.grok/app-env.json` (`{"VITE_AUTH_ENABLED": "false"}`) is the switch, read by +`npm run dev` / `build` / `preview` alike through `scripts/with-app-env.mjs` — +which is why Vite is never started outside those scripts. Follow the `auth` +skill's **"Turning sign-in on"** (flag, schema, then routes) in that order: the +routes alone render the disabled branch. `npm run check:auth`, run against a +live dev server, fails when that server and the next build disagree about the +flag (exit 0 agree, 1 diverged, 2 could not observe). + +## Env + +On deploy the platform injects `DATABASE_URL` + per-app auth creds; live preview +needs neither (baked preview client, PGLite fallback). Deployed behind the gate, +signed-in Grok viewers get the app session automatically from `x-grok-identity` +(see the `auth` skill — `references/grok-identity.md`); the broker federation +covers anonymous viewers and no-gate contexts. + +## HARD RULE — connector / AppData API (backend only) + +**Never call connector or AppData APIs from frontend code.** + +| Allowed | Forbidden | +| --- | --- | +| `createServerFn({ method: "POST" }).handler` that dynamic-imports `@/lib/app-data/client.server` and calls `callTool` | Importing `@/lib/app-data/client.server` from a route component, `useEffect`, event handler, or any client module | +| UI calling that **server function** only | Browser `fetch("/__gate/app-data/…")`, `fetch` to the connectors host, or any direct CallTool from the client | +| Types/constants from `@/lib/app-data` (no network) | Putting `x-connector-access-token`, connector JWTs, or gate secrets in client state, props, or `VITE_*` env | + +**Flow (required):** browser → **this app's** `createServerFn` → **app backend** +SDK → public connectors host (`connectors.grok.me`, **auth required**) → gate. +Unauthenticated hits on the connectors host redirect to gate OIDC sign-in. + +If you need Drive / Gmail / calendar / connector data, load the **`app-data` +skill** (`.grok/skills/app-data/SKILL.md`) and follow it exactly. Do not invent +a client-side connector client. diff --git a/.grok/references/deploy-target.md b/.grok/references/deploy-target.md new file mode 100644 index 0000000..68ea777 --- /dev/null +++ b/.grok/references/deploy-target.md @@ -0,0 +1,40 @@ +# Build & deploy target + +You never trigger the deploy yourself, **but the app you build is eventually +deployed to Vercel** by the platform — so your output must build cleanly under +Vercel's process. `npm run build` must succeed and emit valid output, and code +that works under `npm run dev` but breaks a production / SSR build is a bug. +Watch for dev-only deps, server-only Node APIs run at import time, runtime +filesystem writes, and hard-coded ports / hosts / secrets. + +## A passing `npm run build` does not mean the deployed app renders + +The most common blank-deploy failure is +`Failed to load module script … MIME type "text/html"`: the built `index.html` +requests JS assets that 404 in prod, so the server returns the HTML fallback +(wrong MIME) and the page is blank. Fix the asset base path / build output so +`/assets/*` resolve, and ensure the SPA/SSR fallback doesn't shadow real asset +requests — then re-verify the served build renders. + +If you edited source after kicking off the build, re-run `npm run build` first, +then `npm run preview:restart` — it frees `:8081` before serving, so you never +smoke the previous build's output. + +## What `vite.config.ts` already does + +The workspace ships a ready `vite.config.ts` and `tsconfig.json` — don't +recreate them, and don't import a vendored `vite-tanstack-config` preset. The +config: + +- binds the dev port `0.0.0.0:8080`; +- pins `vite preview` to loopback `127.0.0.1:8081`, so the built output can + never be picked up as the user's live preview; +- gates `nitro({ preset: "vercel" })` on `command === "build" || isPreview`, so + it never runs in dev — left on in dev, nitro opens a second dev-server port, + which breaks the single-port 8080 live preview — but still serves the built + output under `vite preview`; +- mounts `grokPwaPlugin()`. + +If you edit it, preserve both port contracts, the build/preview-gated nitro +plugin **including its `serverDir: "./server"` option** (without it the deployed +app loses the Home Screen install page), and `grokPwaPlugin()`. diff --git a/.grok/references/generated-art.md b/.grok/references/generated-art.md new file mode 100644 index 0000000..c36134d --- /dev/null +++ b/.grok/references/generated-art.md @@ -0,0 +1,67 @@ +# Generated art (2D only) + +`AGENTS.md` § "Skills" states the availability rule: only call `imagine_*` tools +when they appear in your available tools list, and ship CSS / SVG / emoji / +canvas / geometric-WebGL art when they don't. This file is the pipeline detail +for when they are listed. + +## Illustration + +When the product needs illustration (heroes, empty states, textures, icons), +generate **2D** assets via the image tools — follow the **`imagine`** skill +(`imagine_text_to_image` / `imagine_image_to_image` / `imagine_text_to_video` / +`imagine_image_to_video` path-based stack; show results with `render_file`). +Image tools do **not** create 3D models; use geometry/glTF for interactive 3D +(`building-games`). + +## Game art + +- **Doctrine, not the pipeline:** for any game sprites, sheets, animations, + tiles, or UI art, load **`game-asset-core`** plus the matching specialist — + **`game-animation-frames`** (motion / loop laws), **`game-tilesets`** + (seamless tiles / transitions), **`game-character-consistency`** (turnarounds + / variants), **`game-ui-icons`** (HUD / buttons / icon sets). These cover + engine-ready defaults, blind verify, and retry discipline — **not** a + substitute for the pipeline skills below, and not a substitute for + implementing the app. +- **2D sprites / animation sheets** (characters, walk cycles, attacks, + projectiles, FX, props): run **`generate2dsprite`** — solid **`#FF00FF`** + magenta sheets + local chroma postprocess scripts. That magenta key is + **required** by the processor; do not invent a different "keyable" color on + this path. Layer `game-asset-core` (+ `game-animation-frames` / + `game-character-consistency`) for QC. +- **Exception — abstract/geometric games** (tetris, snake, pong, breakout, and + the like) are correctly rendered procedurally. Generated sprite sheets there + are a quality regression: do **not** invoke image generation for them even + when gen tools are listed. +- **2D maps / levels / prop packs** (top-down RPG, side-scroller stages, layered + maps, collision zones): follow **`generate2dmap`**. Default engine target is + browser (`raw_canvas` / Phaser), not Godot/Unity. Tileable ground/walls → also + `game-tilesets` for seamlessness checks. +- **Denser motion from video** (optional, Grok-only): **`video2dsprite`** — + `imagine_image_to_video` → ffmpeg → magenta chroma scripts. Prefer + `generate2dsprite` for crisp production sheets, and use `video2dsprite` rather + than ad-hoc ffmpeg for the sandbox execution path. + +## Share card / app identity + +A custom share card is the default: open the **`og`** skill and produce a custom +`public/og.jpg` from the app's own art before you finish. + +- It covers games of **every** kind and rendering tech (a DOM tic-tac-toe is + still a game), whimsical apps, creative tools, and brand-forward pages. Only + plain utilities (converters, CRUD trackers, admin dashboards) keep the + `og.grok.me` placeholder, and the favicon alone never satisfies this. +- Custom art is `public/og.jpg` **and** `"card": "custom"` in + `src/lib/og/site.json`. Bake infers custom from the file when the flag is + missing, but `brand-check` still requires the field. +- Games also set `"type": "x:game"` (X presents the unfurl as a game card) and + ship `public/x-banner.jpg` (50:11, 1200×264). `twitter:card=summary_large_image` + is layout, not the game signal. +- Title defaults to the host slug; `src/lib/og/site.json` is only needed when + the display name is not the slug, or the app is a game. +- The tags come from the PWA injector (`scripts/grok-pwa-shared.mjs`), which + overwrites anything in `__root.tsx`. Live preview emits the same tags (via + `X-Forwarded-Host`), and identity is baked at `vite build` so published Nitro + can inject without reading the workspace filesystem. +- Applies at build time, publish or not. diff --git a/.grok/references/hibernate-revive.md b/.grok/references/hibernate-revive.md new file mode 100644 index 0000000..62f9f1a --- /dev/null +++ b/.grok/references/hibernate-revive.md @@ -0,0 +1,44 @@ +# Hibernate, revive, and follow-up turns + +`AGENTS.md` § "`/workspace/startup.sh`" holds the rules for the file itself. +This is the surrounding lifecycle behaviour. + +## Session shapes you may land in + +- **Fresh `/workspace`** — template + `node_modules` only, no app routes, no + `startup.sh`. Scaffold, then write `startup.sh`. +- **Hibernate / revive (snapshot restore)** — the platform re-runs + `/workspace/startup.sh` if it exists. Everything else in the workspace comes + back as it was, including the app source. Your job on every turn is to leave + that file able to bring the preview back on its own. +- **Reboot / recreate** — may wipe app files back to the template. Re-scaffold + and **restore `startup.sh`** before verifying the preview. + +A revive with no `startup.sh` leaves nothing listening on `:8080`, so the user +sees an empty preview pane. + +## A `startup.sh` that satisfies the rules + +```sh +#!/bin/sh +set -eu +cd /workspace +# :8081 is QA-only — a revive must never inherit a stale built-output preview. +# Called directly, not via npm: no node_modules needed, so nothing to wait for. +node scripts/preview.mjs stop || true +if curl -sf -o /dev/null --max-time 2 http://127.0.0.1:8080/; then + exit 0 +fi +npm run dev >>/tmp/app-startup.log 2>&1 & +``` + +## Follow-up turns (multi-turn continuity) + +- Edit in place. Do not re-scaffold unless files were wiped or the change is too + big to patch cleanly. +- Vite HMR pushes source edits to the preview instantly. Restart the dev server + **only** for `vite.config` / dependency changes — and update `startup.sh` in + the same turn if the restart command changed. +- Killing the dev server blanks the user's preview mid-session. +- After edits, re-run the smoke pass; re-read the screenshots only when the edit + touched visible UI. diff --git a/.grok/references/scaffold.md b/.grok/references/scaffold.md new file mode 100644 index 0000000..3da9bcc --- /dev/null +++ b/.grok/references/scaffold.md @@ -0,0 +1,80 @@ +# First-scaffold snippets + +Copy-paste bodies for the four entry files `AGENTS.md` § "First scaffold" +requires. They match the **installed** TanStack Start: `src/router.tsx` is +resolved by a **named `getRouter` export**, and older +`createRouter`-default-export / `app/`-directory conventions are rejected by the +plugin. + +```tsx +// src/router.tsx +import { createRouter } from "@tanstack/react-router"; +import { AppErrorComponent } from "@/lib/error-component"; +import { routeTree } from "./routeTree.gen"; // generated on first dev/build + +export function getRouter() { + return createRouter({ routeTree, defaultErrorComponent: AppErrorComponent }); +} +``` + +```tsx +// src/routes/__root.tsx — the document shell +import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router"; +import { AuthProvider } from "@/lib/auth/provider"; +import { PreviewHostBridge } from "@/components/preview-host-bridge"; +import appCss from "../styles.css?url"; + +const APP_NAME = "My App"; + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: "utf-8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + { title: APP_NAME }, + { name: "theme-color", content: "#000000" }, + ], + links: [ + { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" }, + { rel: "stylesheet", href: appCss }, + { rel: "manifest", href: "/__grok/manifest.webmanifest" }, + { rel: "apple-touch-icon", href: "/__grok/icon-180.png" }, + ], + }), + component: () => ( + + + + + + {/* Keep this bridge — lets the Grok preview chrome drive the app; noops when not embedded. */} + + + + + + + + ), +}); +``` + +```tsx +// src/routes/index.tsx +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ component: Home }); + +function Home() { + return
Hello
; +} +``` + +```css +/* src/styles.css */ +@import "tailwindcss"; + +@layer base { + button:not(:disabled), [role="button"]:not(:disabled) { cursor: pointer; } +} +``` diff --git a/.grok/skills/auth/SKILL.md b/.grok/skills/auth/SKILL.md new file mode 100644 index 0000000..bb8e99f --- /dev/null +++ b/.grok/skills/auth/SKILL.md @@ -0,0 +1,110 @@ +--- +name: auth +description: > + Add user accounts and sign-in to this TanStack Start app. Use when the app + needs authentication, sign-in, user accounts, protected routes, or per-user + data. Triggers on "auth", "login", "log in", "sign in", "sign up", "account", + "users", "authentication", "protected", "who is logged in", "current user", + "per-user". +metadata: + short-description: "Auth via the Grok broker (Google, X) or local email/password — no other methods supported" +user-invocable: false +--- + +# Auth + +This app runs its **own** [Better Auth](https://better-auth.com) at +`/api/auth/*`, federating to the shared **Grok auth broker** (`auth.grok.me`) +via the `genericOAuth` plugin. This template wires **Google** and **X**. + +**Supported sign-in methods — use ONLY these three; nothing else is supported: +Google, X, and email/password.** No other social/OAuth provider (GitHub, Apple, +Discord, …), no magic links, passkeys, OTP, phone/SMS, or anonymous sign-in. Do +not add entries to `GROK_PROVIDERS`. Method detail and the email/password switch +(edit **only** `src/lib/auth/email-password.ts`): `references/sign-in-methods.md`. +**Exception — gate viewers are signed in already; NEVER render login/re-auth +buttons for them. Connector / app-data apps: ONLY gate "Continue with Grok", +no Google/X buttons**: `references/grok-identity.md`. + +**Sign-in is OFF by default** — the template ships `.grok/app-env.json` with +`{"VITE_AUTH_ENABLED": "false"}`, so only add accounts when the ask calls for +them (AGENTS.md §0.5). Switching it on is "Turning sign-in on" below. + +**Once on, sign-in is REAL — including in the sandbox live preview.** Do **NOT** +scaffold demo/mock/hardcoded users. Preview: popup + baked preview client; +deployed: per-app client + `DATABASE_URL` + zero-click gate sign-in +(`references/prewired-and-env.md`). + +**While OFF** (`VITE_AUTH_ENABLED=false`) a **dev user** is returned so a +non-auth app renders without a signed-in visitor — dev and preview only. +Deployed, the flag is the platform's (always `"true"`), so `requireUserId` +rejects every visitor — auth-off apps use neither it nor `authMiddleware`. + +Everything is **preinstalled and pre-wired in `src/lib/auth/`** — do not +`npm install` anything; `better-auth` is the only auth package (never +`@neondatabase/*`, `@stackframe/*`, or `@clerk/*`). **Do not edit or rewrite any +file under `src/lib/auth/`** — `server.ts` least of all — except +`email-password.ts` for its one flag. Per-file map: +`references/prewired-and-env.md`. + +**`/auth/popup` is already handled by the template Vite plugin** +(`vite.config.ts` → `popup.server.ts`): it never paints the React app. **Do NOT +create `src/routes/auth/popup.tsx`** (or any React page / client OAuth at that +path) — that shows the full app inside the popup, the common failure mode. + +**Never write a `.env` / `.env.local` / `.env.example`** in this sandbox: live +preview needs **zero** env configuration and a deployed app gets its vars +injected by the platform. The knobs that exist are in +`references/prewired-and-env.md` — never expose a non-`VITE_` var to the client. + +`migrations/auth/0001_auth.sql` is the Better Auth schema — **do not edit**. It +sits outside the globbed `migrations/` directory (neither applier descends), so +it is not applied to apps without sign-in; "Turning sign-in on" copies it up. + +## Turning sign-in on + +Do all of this — the routes alone render the disabled branch: + +1. **Flag:** delete the `VITE_AUTH_ENABLED` key from `.grok/app-env.json` and + **restart the dev server**. Vite reads env at startup, so HMR will not pick + it up. `npm run dev`, `npm run build` and `npm run preview` all read that + file through `scripts/with-app-env.mjs`, so preview and the built output flip + together — never start Vite directly. +2. **Schema:** `cp migrations/auth/0001_auth.sql migrations/0001_auth.sql`, then + restart so it applies. It is tracked by basename in `_migrations`, so a + database that already has it will not re-run it. +3. **Routes:** add `src/routes/api/auth/$.ts` + `src/routes/login.tsx` — copy + both from `references/wiring.md` (the catch-all API route is what makes + `/api/auth/*` and the broker callback work). +4. **Sign out:** a login with no way out is not done — render `` + from `@/lib/auth/gates` (wires `signOut()`; hides sign-out for gate sessions). +5. **Existing data:** wrap the app's server functions in `authMiddleware` (an + auth-off app must not have been using it — see the `neon` skill). Rows from + before sign-in existed are **development data**: drop and recreate them + unless the user says otherwise — don't hand them to whoever signs in first. + +## Building on it once it's on + +- **Sign in / out:** `signIn(providerId)` and `signOut()` from + `@/lib/auth/client`; `GROK_PROVIDERS` renders the buttons. The popup, + bearer-token hand-off, and request attachment are internal — leave them alone. + Prefer `` (it handles the pending and failure states); `signOut()` + rejects when deployed if the server never confirms — catch it. Never + `authClient.signOut()`: it leaves the preview bearer token attached to every + later request, so the visitor stays signed in. +- **Reading the user:** `useCurrentUser()` is display-only (`null` means + *loading OR signed out*, so never redirect on it alone); guard on + `useCurrentUserState()`'s `isPending` instead. Gates (`SignedIn`, `SignedOut`, + `SignInGate`, `RedirectToSignIn`, `UserButton`) live in `@/lib/auth/gates`. + CTA hard rules, skeleton, and cookie-SSR zero-flash: `references/session-ui.md`. +- **Per-user data (mandatory):** every server function that touches per-user data + must use the prewired `authMiddleware` and scope every read **and** write to + `context.userId` — a Postgres driver has full DB access, so nothing else limits + the query. Keep `user_id` columns `TEXT`; never trust a client-supplied user id; + signed out, the middleware throws `UnauthorizedError` (401). Code and + disabled-mode semantics: `references/per-user-data.md`. +- **Security model:** headless broker, `__Host-` cookies + `trustedOrigins`, and + Fetch-Metadata sibling isolation are already wired — never weaken them to make + an error go away (`references/sign-in-methods.md` covers the model and the + "Invalid origin" fix). + diff --git a/.grok/skills/auth/references/grok-identity.md b/.grok/skills/auth/references/grok-identity.md new file mode 100644 index 0000000..3d5701a --- /dev/null +++ b/.grok/skills/auth/references/grok-identity.md @@ -0,0 +1,64 @@ +# Sign in with Grok (deployed apps — zero clicks) + +Behind the edge gate, every proxied request from a signed-in Grok viewer +carries an unforgeable `x-grok-identity` JWT (EdDSA, minted per request; the +gate strips any client-supplied copy). The pre-wired `gateIdentitySessions` +plugin (`src/lib/auth/gate-session.server.ts`) verifies it against the gate's +JWKS (`/__gate/identity-key`, via `src/lib/auth/gate-identity.server.ts`) and, +when the app has no session yet, materializes the Better Auth session for that +viewer automatically — no sign-in button, no redirect, no broker round-trip. +`useSession` / `useCurrentUser` simply return the Grok user. + +The broker OAuth flow is the **fallback** for anonymous/public viewers and for +contexts without the gate. The live preview gets the same zero-click identity +from the in-VM preview proxy at `http://127.0.0.1:6014` (preview tokens carry +audience `preview`); the popup mechanism remains the fallback there. + +## Never render sign-in or re-auth UI to a gate viewer + +A gate-authenticated viewer is **already signed in**, and the gate refreshes +connector tokens on every proxied request — **the platform has no re-auth +concept**. Never render "Re-auth with Grok", "Sign in again", "Refresh +session", or a standing "Continue with Grok" button: sign-in UI may appear +only in the `app-data` skill's `login` error state, after a connector call +actually returned `loginRequired: true`. + +The live preview gets the same zero-click session, so a sign-in button visible +to the owner in the preview indicates a bug (a CTA rendered while the session +check was still pending, or rendered in reaction to a data error) — fix it, +don't restyle it. For any fallback sign-in surface use `` from +`@/lib/auth/gates`: it renders sign-in UI only after the session check resolved +to no user, never during loading and never from a data error. + +Sign-out is also a no-op for a gate session — the next request re-materializes +it from `x-grok-identity`, an instant sign-back-in loop. `` +already hides its sign-out control for gate sessions; never build a custom +sign-out (or any sign-out route/handler) for gate viewers. + +## Files (pre-wired — do not edit) + +| File | Role | +|---|---| +| `gate-identity.server.ts` | Verifies the gate's `x-grok-identity` viewer JWT (EdDSA vs the gate JWKS; fail-closed). Server-only. | +| `gate-session.server.ts` | Better Auth plugin that turns a verified gate identity into the app session with zero clicks. Already registered in `server.ts`. | + +## Env (deployer-injected) + +| Var | Scope | Meaning | +|---|---|---| +| `GROK_PROJECT_ID` | server | deployed apps: enables "Sign in with Grok" (`x-grok-identity` audience check `app:`) | +| `GROK_GATE_ORIGIN` | server | gate public origin override (JWKS + issuer pin); unset → preview mode (no `GROK_PROJECT_ID`) defaults to the in-VM proxy `http://127.0.0.1:6014` (audience `preview`), deployed mode derives it from the inbound host | + +Deployed behavior: gate-authenticated viewers are signed in automatically from +`x-grok-identity`; the deployer also injects a per-app broker client + +`DATABASE_URL`, so the fallback sign-in persists identities in Postgres. + +## Connector / app-data apps: gate sign-in only + +When the `app-data` skill applies, the login page offers ONLY "Continue with +Grok" via the gate sign-in — the zero-click `x-grok-identity` session above, or +the gate-built `loginUrl` returned by a connector call. Do not wire Google/X +buttons for these apps: a broker login can mint an identity that is not the +gate viewer the connector data belongs to. The three-method rule applies to +apps without connector data. Still no new `GROK_PROVIDERS` entries, still never +edit `src/lib/auth/`. diff --git a/.grok/skills/auth/references/per-user-data.md b/.grok/skills/auth/references/per-user-data.md new file mode 100644 index 0000000..3fa8bfd --- /dev/null +++ b/.grok/skills/auth/references/per-user-data.md @@ -0,0 +1,49 @@ +# Per-user data (server-side — mandatory) + +Pair auth with the DB (see the `neon` skill). A regular Postgres driver has full +DB access, so **every** server function that touches per-user data must verify +the caller and scope rows to them. Use the prewired **`authMiddleware`**: it +resolves the same-origin session to a verified `context.userId` (and rejects +scripted cross-site/sibling requests) — no token threading: + +```ts +import { createServerFn } from "@tanstack/react-start"; +import { getSql } from "@/lib/db"; +import { authMiddleware } from "@/lib/auth/middleware"; + +export const listTodos = createServerFn({ method: "GET" }) + .middleware([authMiddleware]) + .handler(async ({ context }) => { + const sql = await getSql(); + // Type the row shape — a server fn's return must be provably serializable. + return sql<{ id: number; title: string; done: boolean }>`select id, title, done from todos where user_id = ${context.userId} order by id desc`; + }); + +// Inputs go through `.validator()` (the current API); the client passes `{ data }`: +export const addTodo = createServerFn({ method: "POST" }) + .validator((title: string) => title.trim()) + .middleware([authMiddleware]) + .handler(async ({ context, data: title }) => { + if (!title) return; + const sql = await getSql(); + await sql`insert into todos (user_id, title) values (${context.userId}, ${title})`; + }); +// mutations must scope writes too: `... where id = ${id} and user_id = ${context.userId}` +``` + +Call these from **client code** (effects, event handlers, React Query) — that's +where `Sec-Fetch-Site: same-origin` holds: + +```ts +useEffect(() => { listTodos().then(setTodos).catch(() => setTodos([])); }, []); +``` + +Semantics: signed out → the middleware throws `UnauthorizedError` (message +`"Unauthorized"`, `status` 401 — match it to send the visitor to sign-in), in the +live preview too (real auth). With auth disabled (`VITE_AUTH_ENABLED=false`) it +resolves the dev user (`"dev-user"`) in dev and preview only — the deployed flag +comes from the deployer (today always `"true"`), so deployed it rejects every +visitor — which is why an app without sign-in must not use the middleware at all +(see the `neon` skill). Keep `user_id` columns +`TEXT` (Better Auth uses text ids; the disabled dev user is `'dev-user'`). Never +trust a client-supplied user id — only the middleware / `requireUserId()` result. diff --git a/.grok/skills/auth/references/prewired-and-env.md b/.grok/skills/auth/references/prewired-and-env.md new file mode 100644 index 0000000..411024e --- /dev/null +++ b/.grok/skills/auth/references/prewired-and-env.md @@ -0,0 +1,54 @@ +# What's pre-wired, and the env vars + +## `src/lib/auth/` + +| File | Use it for | +|---|---| +| `client.ts` | Browser client. `signIn(providerId)`, `signOut()`, `authEnabled`, `GROK_PROVIDERS`. | +| `server.ts` | The Better Auth instance (server-only). **Do not edit or rewrite.** Import only from `/api/auth/$`. | +| `email-password.ts` | **Only** place to enable local email/password (`emailAndPasswordEnabled = true`). | +| `popup.server.ts` | Live-preview popup handler (server-only). Already wired by the Vite plugin — do not create a route for it. | +| `providers.ts` | `GROK_PROVIDERS` — the fixed broker upstream list (Google and X only; don't add others). | +| `use-current-user.ts` | `useCurrentUser()` / `useCurrentUserState()` React hooks. | +| `gates.tsx` | `SignedIn`, `SignedOut`, `RedirectToSignIn`, `UserButton`. | +| `middleware.ts` | `authMiddleware` for server functions → verified `context.userId`. | +| `verify.server.ts` | `requireUserId()` / `getSessionUser()` (server-only) for manual wiring. | + +## How each mode gets its credentials + +- **Live preview** (`*.grok-sandbox.com`): the app is an embedded iframe, so + sign-in opens a **popup** (a top-level redirect to the broker can't work inside + the iframe) and federates via a baked shared **preview client** + (`src/lib/auth/preview.ts`). The handler 302s straight to the broker/upstream + login; on return the popup posts the session bearer back in a tiny HTML page. + Sessions (and email/password users) persist in the app's embedded PGLite DB — + the SAME DB as app data — and, since the iframe's cookies are partitioned, + ride that bearer token; all of it lives in `src/lib/auth`. + Restarting the preview resets the DB. +- **Deployed**: the deployer injects a per-app client + `DATABASE_URL`, so + sign-in persists identities in Postgres. + +## Env vars — do **not** create a `.env` file + +**Never write a `.env` / `.env.local` / `.env.example` for auth (or anything +else) in this sandbox.** Live preview sign-in works out of the box with **zero** +env configuration: the server falls back to the baked preview client in +`src/lib/auth/preview.ts`, derives the `*.grok-sandbox.com` origin per-request, +mints a process-stable session secret, and persists sessions in embedded +PGLite. Deployed apps get `GROK_AUTH_*` / `BETTER_AUTH_*` / `DATABASE_URL` +injected by the platform — still not something you write into a file. + +Optional process-env knobs (platform / rare overrides only — **do not** put +these in a file you create): + +| Var | Where | Purpose | +|---|---|---| +| `VITE_AUTH_ENABLED` | client | `"false"` in the shipped `.grok/app-env.json` (dev user); drop the key to turn sign-in ON. Only client-visible auth flag | +| `BETTER_AUTH_URL` | server | app's own public origin; unset in preview (origin is derived per-request) | +| `BETTER_AUTH_SECRET` | server | signs this app's own sessions (process-stable fallback in preview; survives HMR) | +| `GROK_AUTH_ISSUER` | server | the shared broker (defaults to `https://auth.grok.me`) | +| `GROK_AUTH_CLIENT_ID` / `GROK_AUTH_CLIENT_SECRET` | server | per-app client (falls back to the preview client) | +| `DATABASE_URL` | server | when deployed, Better Auth persists here (preview persists to the embedded PGLite — same DB as app data) | + +Never expose a non-`VITE_` var to the client. The preview client id/secret live +server-only in `src/lib/auth/preview.ts`. diff --git a/.grok/skills/auth/references/session-ui.md b/.grok/skills/auth/references/session-ui.md new file mode 100644 index 0000000..c07df42 --- /dev/null +++ b/.grok/skills/auth/references/session-ui.md @@ -0,0 +1,133 @@ +# Reading the user, protecting routes, and preventing flicker + +## When sign-in UI may render (hard rules) + +- **Connector / owner-data apps: never gate the data fetch behind a session.** + When the data comes from the app's connector grants, the fetch runs on the + gate-injected connector token — a chain that does not involve the app + session. Fetch first; use the session for personalization only (greeting, + per-user rows). Requiring sign-in before fetching adds a click that changes + nothing. +- **A sign-in CTA may render only after the session check has RESOLVED to no + user** — never while `isPending` is true (no CTA flash on load), and never + as a reaction to a data error (the `app-data` skill's error mapping owns + those states; only its `login` kind ever shows "Continue with Grok"). +- **In preview, gate identity signs the owner in with zero clicks** — a + visible sign-in button for the owner in the preview is a bug to fix, not a + style choice (`grok-identity.md`). +- **Use `` from `@/lib/auth/gates`** — `{ children, fallback? }`: + nothing while pending, `children` when signed in, `fallback` (or the + standard provider buttons) only once signed out is known. Do not hand-roll + sign-in CTAs from `useCurrentUser()`. + +## Reading the user / protecting routes + +`@/lib/auth/use-current-user` (with auth on these reflect the REAL session, so a +preview visitor is signed out until they sign in): + +- `useCurrentUser()` → `AppUser | null` — for display. `null` means *loading OR + signed out*, so never redirect on it alone. +- `useCurrentUserState()` → `{ user, isPending }` — for guards: wait for + `isPending` to clear before treating `user: null` as signed out, or a hard + reload bounces signed-in users to sign-in. + +**State components** from `@/lib/auth/gates`: `SignedIn`, `SignedOut`, +`SignInGate` (`{ children, fallback? }` — nothing while pending, `children` +signed in, `fallback` or the standard provider buttons only once signed out is +known; prefer it over hand-rolled sign-in CTAs), `RedirectToSignIn`, +`UserButton`. (When auth is disabled via `VITE_AUTH_ENABLED=false` they apply +dev-user semantics so a non-auth app still renders.) + +```tsx +import { useCurrentUser, useCurrentUserState } from "@/lib/auth/use-current-user"; +import { RedirectToSignIn, SignedIn, SignedOut, UserButton } from "@/lib/auth/gates"; + +function Navbar() { + const user = useCurrentUser(); // display only — null may just mean "loading" + return ( + <> + {user?.displayName ?? "Guest"} + Sign in + + + ); +} + +function AccountPage() { + const { user, isPending } = useCurrentUserState(); + if (isPending) return null; // session still resolving + if (!user) return ; // client-side Navigate — not window.location + return

Welcome, {user.displayName}

; +} +``` + +Sign out with `` or `signOut()` from `@/lib/auth/client`. + +Session loading is the **same in live preview and when deployed**: wait for +`isPending` from `useCurrentUserState()` (backed by `/api/auth/get-session`). +The only live-preview difference is **how sign-in starts** (popup + bearer hand-off +instead of a full-page OAuth redirect) — not how guests vs signed-in users are +detected. Prefer `` (TanStack ``) over +`window.location.href = "/login"` so a signed-out redirect does not full-reload +the SPA. + +## Preventing auth flicker + +`useSession()` resolves on the client, so a naive UI flashes signed-out → +signed-in on load. Rules: + +1. **Gate on `isPending`, not `user` alone — and render a same-sized skeleton.** + Showing the SAME placeholder while `isPending` (server render + first client + paint) makes it one clean swap (skeleton → content) with no flash and no SSR + hydration mismatch. Don't return `null` in a slot that then grows — reserve the + space: + + ```tsx + import { useCurrentUserState } from "@/lib/auth/use-current-user"; + import { UserButton } from "@/lib/auth/gates"; + + function AuthSlot() { + const { user, isPending } = useCurrentUserState(); + if (isPending) return
; + return user ? : Sign in; + } + ``` + +2. **Guard at a layout boundary** (nav / page shell), not in leaf components that + mount/unmount — `useSession` is one shared store, so keep one stable consumer + per region instead of re-gating everywhere. + +3. **Zero-flash when deployed: SSR the session from the cookie.** On a deployed + app (and top-level navigations) the session cookie is same-origin, so the server + already knows the user on the first request — resolve it in the root route and + render the authed shell immediately: + + ```tsx + // src/routes/__root.tsx (excerpt) + import { createServerFn } from "@tanstack/react-start"; + import { createRootRoute } from "@tanstack/react-router"; + + const fetchSessionUser = createServerFn({ method: "GET" }).handler(async () => { + // Cookie path only — works when deployed / on top-level loads. + const { getSessionUser } = await import("@/lib/auth/verify.server"); + const u = await getSessionUser(); + return u ? { id: u.id, email: u.email } : null; + }); + + export const Route = createRootRoute({ + beforeLoad: async () => ({ sessionUser: await fetchSessionUser() }), + // Merge into the existing root — keep the existing head(). + // component: prefer `sessionUser` for the FIRST paint when deployed, then + // `useCurrentUserState()` for live in-page updates. + }); + ``` + + Sign-in/out navigate, so `beforeLoad` re-runs and the context stays fresh; call + `router.invalidate()` if you change auth state without navigating. + + In live preview the session often rides a bearer after popup sign-in, so cookie + SSR may still return null until the client `useSession()` runs with the bearer + attached — still gate on `isPending`, same as when deployed. + +The template already enables Better Auth's `session.cookieCache`, so `/get-session` +answers from a cookie when one is present (no DB round-trip). diff --git a/.grok/skills/auth/references/sign-in-methods.md b/.grok/skills/auth/references/sign-in-methods.md new file mode 100644 index 0000000..3ffc658 --- /dev/null +++ b/.grok/skills/auth/references/sign-in-methods.md @@ -0,0 +1,55 @@ +# Supported sign-in methods (Google, X, email/password — and nothing else) + +Use **only** these three — no other method is supported: + +- **Google** and **X** — federated through the Grok broker (pre-wired here). The + broker federates these two upstreams and nothing else, so do **not** add entries + to `GROK_PROVIDERS` beyond them (the broker rejects an unknown `idp`). +- **Email + password** — this app's OWN Better Auth, persisted in your database + (never the broker, never mocked). Better Auth is DB-backed in BOTH modes — real + Postgres when deployed and the embedded PGLite in the sandbox preview — so + email/password accounts are stored and survive across requests, **in preview + too**. It's off by default. Enable it by editing **only** + `src/lib/auth/email-password.ts`: + + ```ts + // src/lib/auth/email-password.ts + export const emailAndPasswordEnabled = true; // was false + ``` + + **Do not edit or rewrite `src/lib/auth/server.ts`** (or any other file under + `src/lib/auth/` except `email-password.ts` for this flag). That file is + pre-wired; "fixing" it by regenerating Better Auth config breaks live-preview + sign-in. + + The pre-applied schema already has the `account.password` column — no migration + needed. Then build sign-up / sign-in forms with `authClient.signUp.email(...)` + and `authClient.signIn.email(...)` from `@/lib/auth/client`. + + **Do not** add `emailAndPassword` as a plugin entry (that is a syntax/type + error). **Do not** invent a new Better Auth config. + + If sign-up/sign-in returns **"Invalid origin"**, do **not** disable CSRF and + do **not** edit `server.ts`. The template's `trustedOrigins` already covers + `*.grok-sandbox.com` and local loopback on port 8080 (`localhost` / + `127.0.0.1` / `[::1]`). Open the app at one of those origins (not a random + host/port). + +Do **NOT** add or use anything else: no other social / OAuth providers (GitHub, +Apple, Discord, Microsoft, Facebook, …), and no magic links, passkeys, one-time +codes / OTP, phone / SMS, or anonymous sign-in. + +## Security model (already handled — don't undo it) + +- **Headless broker**: the broker offers the upstream sign-in methods + and holds their shared secrets; this app only holds its own per-app client + id/secret and names the upstream it wants via each provider's `idp` hint. The + broker forwards straight to Google/X. Users never see the broker. +- **`__Host-` cookies + `trustedOrigins`**: a sibling `*.grok.me` app can't toss a + `Domain=.grok.me` cookie, and Better Auth rejects cross-origin `/api/auth` + calls. +- **Sibling isolation**: `authMiddleware` rejects scripted cross-site/same-site + requests (Fetch-Metadata), so a sibling can't ride this app's session cookie + into its server functions. +- The upstream Google/X tokens live only on the broker; this app only ever gets a + broker-issued identity and mints its own local session. diff --git a/.grok/skills/auth/references/wiring.md b/.grok/skills/auth/references/wiring.md new file mode 100644 index 0000000..c2331be --- /dev/null +++ b/.grok/skills/auth/references/wiring.md @@ -0,0 +1,71 @@ +# Wiring the routes (do this once) + +**Live-preview popup is PRE-WIRED — do not create it.** +`signIn` opens `/auth/popup`; the template Vite plugin +(`authPopupPlugin` in `vite.config.ts`) serves it via `popup.server.ts`. +**Never** add `src/routes/auth/popup.tsx` (or any React page / client OAuth at +that path). Doing so loads the full app shell in the popup ("the app opened +instead of Google") — that is always wrong. + +**1. Mount Better Auth** — create the catch-all API route (this is what makes +`/api/auth/*` work; the broker's OAuth callback lands here): + +```ts +// src/routes/api/auth/$.ts +import { createFileRoute } from "@tanstack/react-router"; +import { auth } from "@/lib/auth/server"; + +export const Route = createFileRoute("/api/auth/$")({ + server: { + handlers: { + GET: ({ request }) => auth.handler(request), + POST: ({ request }) => auth.handler(request), + }, + }, +}); +``` + +**2. Add a sign-in page** — buttons that kick off the broker flow. Import from +`@/lib/auth/client`. With the flag on, `authEnabled` is true in preview and +deployed, so the buttons show and work in the live preview; the `else` branch +shows while auth is still disabled (`VITE_AUTH_ENABLED=false`): + +```tsx +// src/routes/login.tsx +import { createFileRoute } from "@tanstack/react-router"; +import { GROK_PROVIDERS, authEnabled, signIn } from "@/lib/auth/client"; + +export const Route = createFileRoute("/login")({ component: Login }); + +function Login() { + return ( +
+
+

Sign in

+ {authEnabled ? ( + GROK_PROVIDERS.map((p) => ( + + )) + ) : ( +

Sign-in is disabled.

+ )} +
+
+ ); +} +``` + +`RedirectToSignIn` sends signed-out users to `/login` by default (override with +``). Style the page however you like — see +the `design-ui` skill. + +That's it — call `signIn(providerId)` from your sign-in buttons. The popup, +bearer-token hand-off, and request attachment are all inside `src/lib/auth` + +the Vite plugin; leave them alone. diff --git a/.grok/skills/building-games/SKILL.md b/.grok/skills/building-games/SKILL.md new file mode 100644 index 0000000..0c6b3cf --- /dev/null +++ b/.grok/skills/building-games/SKILL.md @@ -0,0 +1,213 @@ +--- +name: building-games +description: > + Build browser games and interactive/canvas/3D experiences in this TanStack + Start + React app. Use for any game, simulation, or WebGL/Canvas experience — + 2D or 3D, single-player. Covers the game loop & timing, 3D orientation/camera + conventions, collision, performance, assets, audio, save, game feel, and + per-genre playbooks. For WASD / vehicle / flight **input signs and inverted + A/D**, open the **`controls`** skill — do not rely on this file or racing-kart + alone. Triggers on "game", "minecraft", "fps", "platformer", "racing", + "tetris", "snake", "shooter", "3d", "three.js", "canvas", "voxel", "physics". +metadata: + short-description: "Browser games: loop, 3D orientation, camera, perf, assets, genres" +user-invocable: false +--- + +# Building Games + +Build a **playable, correct** browser game — not a static screenshot. A game is +just a React route with a `` (or `` for R3F) plus DOM overlay UI. +Style the overlay (start screen, HUD, menus) with the **`design-ui`** skill; this +skill owns the gameplay loop and world. + +**Controls / inverted A/D:** open **`.grok/skills/controls/SKILL.md`** **before** +writing WASD, steering, or flight input. Vehicle/flight demos often ship with +A/D flipped if you only read this file or a single genre playbook. + +**Scope note — single-player, bots, or small P2P co-op:** +- Ship **single-player** or **single-player + AI/bots** by default. +- **2–8 player co-op / casual realtime** (shared cursors, party games, casual + action among friends) is supported — use the **`multiplayer-p2p` skill** + (WebRTC mesh, signaled at `/api/rtc`). Read its trust model first. +- P2P is the only supported multiplayer right now. Do not half-build sockets + that can’t connect. + +**References (load on demand):** +- **`controls` skill** (`../controls/`) — **required** for movement/steer/flight: + player-visible A/D, inverted-steer anti-pattern, flight ailerons, mandatory + self-test + `window.__controlsTest`. Not optional for vehicles/planes. +- `references/threejs-foundational.md` — the deep 3D/loop/perf reference. Read for 3D. +- `references/3d-libs.md` — three.js + @react-three/fiber + drei + rapier usage. +- **`threejs` skill** (`../threejs/`) — official full Three.js + TSL API dump + (`llms-full.txt`). Load for advanced materials/shaders/WebGPU/loaders; not for + simple 2D canvas games. +- `references/babylon.md` — Babylon.js, the batteries-included 3D engine alternative. +- `references/phaser.md` — Phaser 3, the default engine for 2D games. +- `references/ecs-architecture.md` — entity-component-system structure for larger games. +- `references/genres/*.md` — per-genre playbooks (fps, platformer-2d, racing-kart, + puzzle-match3-tetris, voxel-minecraft, endless-runner, topdown-twin-stick, + tower-defense, board-card-chess). Genre files **do not** replace **`controls`**. +- `references/game-feel-juice.md`, `input.md`, `audio.md`, `collision-physics.md`, + `save-persistence.md`, `procedural-generation.md`, `ai-pathfinding.md`. +- **`game-asset-core`** (+ `game-animation-frames` / `game-tilesets` / + `game-character-consistency` / `game-ui-icons`) — engine-ready 2D art defaults + and verification when generating sprites, sheets, tiles, or UI (see §6). + +Pick the specific genre/topic reference for the build; this file is the universal +core for loop/world. **Input signs → `controls`.** **2D game art → `game-asset-core`.** + +--- + +## 1. Game loop & timing (the #1 correctness issue) + +- Drive the loop with the engine's RAF loop (`renderer.setAnimationLoop`, R3F + `useFrame`, or `requestAnimationFrame` for 2D canvas). **Never** `setInterval`/ + `setTimeout`/`Date.now()` for game timing. +- **Scale ALL movement/animation by delta time** (seconds) so speed is frame-rate + independent (60Hz vs 144Hz). Compute delta **once per frame** and reuse it. + - three.js: use `THREE.Timer` (not `Clock` — `Clock.getDelta()` returns ~0 on a + second call in the same frame, a classic freeze bug). +- **Cap delta** (`min(delta, 0.1)`) so a backgrounded tab doesn't teleport things. +- **Fixed timestep for physics/gameplay:** accumulate delta and step simulation at + a fixed rate (e.g. 1/60) while rendering at display rate — prevents tunneling and + non-determinism. + +## 2. Controls (delegate to the `controls` skill) + +**Open `.grok/skills/controls/SKILL.md` before implementing any WASD / steer / +flight code.** That skill is the source of truth for: + +- Player-visible **A = left / D = right** (chase cam, while moving forward) +- Why **`KeyA → steer−` + `yaw += steer * +rate` inverts** (most common bug) +- Vehicle vs FPS (strafe ≠ steer), fixed-wing ailerons, heli/drone notes +- Mandatory self-test + `window.__controlsTest` probe + +Do **not** treat `genres/racing-kart.md` as the only place steer signs live — +planes, jetskis, and mechs never open it. + +**Short reminder (full detail in `controls`):** + +``` +// Vehicle yaw body (chase cam): A must increase yaw with this basis +forward = (-sin(yaw), 0, -cos(yaw)) +// KeyA → steer = +1; yaw += steer * turnRate * speedFactor * dt +// WRONG (ships inverted): KeyA → steer = -1; yaw += steer * turnRate * dt +``` + +- **Pointer lock:** mouse-look only — implement WASD yourself; click-to-play + overlay; dismiss on lock. +- Track keys with held state + **dt**; unify devices via `references/input.md`. +- **Finish:** run the `controls` skill checklist. Screenshot-only is insufficient + for vehicles/flight. + +## 3. 3D orientation & world objects (the "sideways/backwards" bugs) + +- three.js is **right-handed, +Y up**: +X right, +Y up, +Z toward viewer. **Meshes + face +Z; cameras look −Z** (the classic "camera backwards" gotcha). +- Primitives like `Cone`/`Cylinder` point **+Y** by default → rotate to align a + tip with forward (`geo.rotateX(Math.PI/2)`). +- **Orienting a mesh to face `forward`** (meshes face **+Z**): simplest correct way + is `mesh.lookAt(mesh.position.clone().add(forward))`. To build the basis by hand, + set the **+Z column to `forward`** and choose the x-axis that keeps it a *proper* + right-handed rotation (`det = +1`): + `xAxis = normalize(cross(up, forward))`, then `makeBasis(xAxis, up, forward)`. + Note this is `cross(up, forward)` — **not** the movement `right = cross(forward, up)` + from §2. Targeting +Z (instead of a camera's −Z) flips the x-axis sign so that + `xAxis × up = forward`; the frame stays right-handed (no mirroring). Do **not** use + `makeBasis(xAxis, up, -forward)` for a mesh — that targets −forward (the *camera* + convention) so the mesh faces **backwards**. +- **Orienting a camera** to look along `forward` (cameras look **−Z**): set the +Z + column to `-forward` — `xAxis = normalize(cross(forward, up))`, then + `makeBasis(xAxis, up, -forward)` (or just `camera.lookAt(target)`). +- Keep a consistent world `up` so objects stay upright; only rotate flat primitives + to stand up. +- Verify glTF import orientation; debug with `AxesHelper`/`ArrowHelper`. Upright + self-test: characters stand on the ground plane, not lying/sunk. + +## 4. Camera must agree with movement + +- Keep a **dedicated `moveForward`/`moveRight`** for movement, computed once and + never mutated by camera code (aliasing a shared temp vector makes camera and + movement disagree — a real repro bug). +- Third-person follow: `desired = playerPos + up*height + moveForward*(-followDist)`; + lerp the camera toward it (use exp-based smoothing, delta-scaled), `lookAt(player)`. +- Isolate-the-layer debug order: (1) keys register → (2) movement signs correct → + (3) camera agrees. Fix in that order. + +## 5. Performance + +- **Minimize draw calls** (`renderer.info.render.calls`, target <100): share + materials, `InstancedMesh`/`BatchedMesh` for repeated objects, atlases. +- **Dispose GPU resources yourself** (`geometry/material/texture.dispose()`) on + level change — three.js does not GC them. **Pool** bullets/enemies/particles. +- No per-frame allocations (reuse temp vectors). Compress textures; LOD for distance. + +## 6. Assets (avoid the generated-photo trap) + +- **Interactive 3D elements** (weapon viewmodels, characters, props, projectiles) + → build from **3D geometry / glTF**, not a generated image. A flat photorealistic + JPG of a gun-in-hands used as an FPS viewmodel looks wrong, can't animate, and + (JPG has **no alpha**) renders as a black box. Parent a real 3D viewmodel to the + camera as an overlay render layer. +- Reserve image generation for **flat 2D** assets only (textures, sky/menu + backgrounds, 2D sprites, UI art). **Never** use a generated photo as a 3D mesh, + viewmodel, or character substitute — build those in 3D geometry / glTF. + Set `crossOrigin="anonymous"` on images drawn to canvas/textures. + See the **`imagine`** skill (2D only — image tools cannot produce real 3D). +- **Engine-ready game art doctrine** → open **`game-asset-core`** + (`../game-asset-core/`) for defaults + blind verify + retry discipline, then the + matching specialist: **`game-animation-frames`** (loop / motion laws), + **`game-tilesets`**, **`game-character-consistency`**, **`game-ui-icons`**. + These are **QC/doctrine**, not the export pipeline. Do not ship stick-figure + placeholders when real art is expected. +- **2D game sprites / animation sheets** → run **`generate2dsprite`** + (`.grok/skills/generate2dsprite/SKILL.md`): solid **`#FF00FF`** magenta + `imagine_text_to_image` sheets + chroma postprocess scripts (magenta is required for the + processor). Wire transparent PNGs/GIFs into Canvas/Phaser. Still apply + **`game-asset-core`** (+ animation/character specialists when relevant). +- **2D maps / levels / prop packs** → open **`generate2dmap`** + (`.grok/skills/generate2dmap/SKILL.md`). Prefer foundation-only base + separate + props/collision for playable maps. Browser default: `raw_canvas` / Phaser. + Tileable ground/walls → also **`game-tilesets`** for 2×2 seam checks. +- **Optional denser locomotion** → run **`video2dsprite`** (Grok + `imagine_image_to_video` + sandbox scripts; magenta base). Prefer `generate2dsprite` + for crisp production heroes. Use **`game-animation-frames`** for loop/flip-test + laws; prefer **`video2dsprite`** over ad-hoc ffmpeg-only harvest in this + sandbox. + +## 7. Audio, save, feel +- **Audio**: unlock `AudioContext` on the first user gesture (tap-to-start) or iOS + is silent; re-resume on `visibilitychange`. (`references/audio.md`) +- **Save**: `localStorage`/IndexedDB with a `version` field + migrations. +- **Juice**: screen shake, hit-stop, eased tweens, particles — cheap, huge + perceived-quality lift. Keep presentation separate from simulation. + +## 8. Mobile +- Distinguish canvas buffer size from CSS size; respect `devicePixelRatio`. +- `touch-action: none`, letterbox-fit to a base resolution, handle orientation. +- Touch controls (virtual joystick + action buttons), ≥44px targets. + +--- + +## Stack / engine choice +- **3D → three.js**, ideally via **@react-three/fiber + drei** (fits the React + app; drei gives pointer-lock/controls/loaders) + **@react-three/rapier** for + physics/character controllers. See `references/3d-libs.md`. +- **2D →** native Canvas 2D is enough for snake/tetris/flappy/platformer; reach for + Phaser only when the genre needs it. +- These game deps are **not preinstalled** — `npm install` them (and make sure they + land in `package.json` so the Vercel build has them). + +## Finish criteria (before "done") +- Loads with **no console errors**; visible gameplay (not a blank canvas). +- **`controls` skill self-test passed** (A = left / D = right from chase cam + while moving forward; flip one sign if inverted). Not screenshot-only. +- 3D upright & camera-agrees self-tests pass (§3, §4). +- Runs on mobile viewport with touch controls. +- Production build (`npm run build`) renders the built output, not just dev. +- **Share / X card:** open the **`og`** skill — custom `public/og.jpg` **and** + `"type": "x:game"` in `src/lib/og/site.json`. X uses `og:type="x:game"` + to present the unfurl as a game card; do not use `twitter:card` or invent + `x:type` for this. `browser-smoke` / `brand-check` warn when canvas apps omit + the `site.json` field. diff --git a/.grok/skills/building-games/references/3d-libs.md b/.grok/skills/building-games/references/3d-libs.md new file mode 100644 index 0000000..d3bf27a --- /dev/null +++ b/.grok/skills/building-games/references/3d-libs.md @@ -0,0 +1,64 @@ +# 3D libraries for this stack (three.js + React Three Fiber + drei + rapier) + +The app is React/TanStack. The idiomatic way to do 3D here is **React Three +Fiber (R3F)** — write three.js as React components inside a `` on a +route. It deploys like any other page. + +> These are **not preinstalled**. Install and confirm they land in `package.json`: +> `npm i three @react-three/fiber @react-three/drei @react-three/rapier` +> (+ `npm i -D @types/three`). If the deploy build has no three, the game is blank. + +## When to use what +- **three.js** — the engine. Raw three is fine for a self-contained canvas; prefer + R3F when the game has React UI/state around it. +- **@react-three/fiber** — renders three as React; `useFrame((state, delta) => …)` + is your game loop (delta is seconds — scale movement by it, see SKILL §1). +- **@react-three/drei** — helpers so you don't hand-roll bug-prone code: + ``, `useKeyboardControls`, ``, + `useGLTF`, ``, ``, ``. +- **@react-three/rapier** — physics + **character controller** (capsule + + autostep + snap-to-ground). Use for FPS/platformer movement and collisions + instead of hand-rolled raycasts. ``, ``, `useRapier`. + +## Minimal R3F shape +```tsx +import { Canvas, useFrame } from "@react-three/fiber"; +import { PointerLockControls, useKeyboardControls } from "@react-three/drei"; + +function Player() { + useFrame((_, delta) => { + const d = Math.min(delta, 0.1); // cap delta (SKILL §1) + // move with a dedicated moveForward/moveRight basis (SKILL §2/§4) + }); + return null; +} + +export default function Game() { + return ( + + {/* scene */} + {/* mouse-look ONLY — implement WASD yourself */} + + + ); +} +``` + +## Gotchas (map to the SKILL universals) +- **Controls:** drei `PointerLockControls` is mouse-look only — WASD is yours + (SKILL §2). Gate `.lock()` behind a "click to play" overlay and dismiss it on lock. +- **Orientation:** meshes face +Z, camera looks −Z; right-handed +Y up (SKILL §3). +- **Delta:** `useFrame` delta is seconds; cap it; fixed-step physics via rapier's + own stepping. Don't read `THREE.Clock.getDelta()` twice a frame. +- **Perf:** ``/`InstancedMesh` for repeats; dispose on unmount (R3F + auto-disposes objects it created, but not manual textures/loaders — dispose those). + Set `dpr={[1, 2]}` to cap retina cost. +- **Mobile:** R3F sets pixel ratio via `dpr`; still add touch controls + `touch-action:none`. + +## Alternatives +- **Babylon.js** (`references/babylon.md`) — batteries-included 3D (Havok physics, + inspector, SceneOptimizer). Viable if you want an all-in-one engine, but it's a + separate paradigm from React; default to three/R3F for consistency with the app. + +Sources: R3F docs (https://r3f.docs.pmnd.rs), drei (https://github.com/pmndrs/drei), +react-three-rapier (https://github.com/pmndrs/react-three-rapier), three.js docs. diff --git a/.grok/skills/building-games/references/ai-pathfinding.md b/.grok/skills/building-games/references/ai-pathfinding.md new file mode 100644 index 0000000..80232c6 --- /dev/null +++ b/.grok/skills/building-games/references/ai-pathfinding.md @@ -0,0 +1,122 @@ +# Game AI & Pathfinding (A* on grids, steering, FSM, behavior trees, navmesh) + +Consolidated from Red Blob Games (the canonical A*/grid reference), Craig Reynolds' steering work, and Game AI Pro / behavior-tree canon (see Sources). Focus: what an AI builder needs so enemies/agents **move sensibly and don't jitter, get stuck, or tank the frame rate.** + +--- + +## 1. Pathfinding: A* on a grid (the default) + +**A\*** finds the shortest path by expanding the node that minimizes `f = g + h`, where `g` = cost so far and `h` = heuristic estimate to goal. It's Dijkstra + a goal-directed heuristic. + +Rules: +- **Heuristic must be admissible** (never overestimate true cost) or A* may return a non-optimal path. + - **4-directional grid → Manhattan distance** `|dx|+|dy|`. + - **8-directional grid → octile/Chebyshev distance** (diagonal cost ≈ 1.414). Using Manhattan on an 8-dir grid overestimates → wrong paths; using Euclidean on a 4-dir grid underestimates → slow. +- **Use a binary heap / priority queue** for the open set. A linear scan of the open set is the #1 A* perf bug — turns O(E log V) into O(V²) and stutters on big maps. +- **Track a closed set / best-g per node** so you don't reprocess nodes; update `g` if a cheaper path to a node is found. +- **Weighted tiles:** give terrain costs (mud=5, road=1) via `g`; A* naturally routes around expensive terrain. +- **Diagonal corner-cutting:** forbid diagonal moves that clip through two wall corners, or agents visually cut through walls. +- **Reconstruct the path** by following `cameFrom` parents from goal back to start, then reverse. + +Related tools (Red Blob Games): +- **Breadth-First Search / Dijkstra maps ("flow fields"):** when *many* agents chase *one* goal (tower defense, RTS), compute one BFS/Dijkstra field from the goal and have every agent follow the gradient — far cheaper than one A* per agent. +- **Greedy Best-First** is faster but not optimal; A* is the balanced default. +- **JPS (Jump Point Search)** optimizes A* on uniform-cost grids. + +Libraries: **PathFinding.js** (grid A*/JPS/BFS, easy), or roll your own for control. + +**Path-following gotchas:** smooth the raw grid path (string-pulling / line-of-sight simplification) so agents don't zig-zag along cell centers; recompute paths sparingly (on target move / periodically, not every frame); handle "no path exists" explicitly. + +--- + +## 2. Steering behaviors (smooth local movement) + +A* gives *where* to go; **steering** (Craig Reynolds) makes agents *move there naturally* by accumulating steering forces on velocity: +- **Seek / Arrive** — move toward a target; Arrive decelerates within a slowing radius (prevents overshoot/orbit). +- **Flee / Evade** — away from a threat. +- **Pursuit** — seek the target's *predicted future* position. +- **Wander** — smooth random roaming (jitter a point on a circle ahead), not teleporting random targets. +- **Obstacle avoidance** — steer around obstacles detected by a look-ahead feeler. +- **Separation / Alignment / Cohesion** = **Flocking (boids)** for crowds/schools/swarms. +- **Path following** — follow a computed A* path smoothly rather than snapping cell-to-cell. + +Rules: **combine forces** (weighted sum or priority), **clamp to max force and max speed**, apply with **delta time**. Steering handles the smooth "how"; A*/flow fields handle the strategic "where." Use both together — A* alone looks robotic; steering alone gets stuck on walls. + +--- + +## 3. Finite State Machines (FSM) — decision-making for simple agents + +- Agent is in exactly one **state** (Idle, Patrol, Chase, Attack, Flee); **transitions** fire on conditions (saw player → Chase; lost player → Patrol; low HP → Flee). +- Each state has `enter()`, `update(dt)`, `exit()`. Keep transition logic centralized/table-driven. +- Great for enemies with a handful of behaviors. **Downside:** transitions explode combinatorially (n states → up to n² transitions) — becomes spaghetti past ~6–8 states. +- **Hierarchical FSMs** (states containing sub-states) tame some of that. + +--- + +## 4. Behavior Trees (BT) — scalable decision-making + +When FSMs get unwieldy, use a **behavior tree**: a tree of nodes evaluated each tick, each returning **Success / Failure / Running**. +- **Composites:** **Sequence** (run children in order, fail-fast — "AND"), **Selector/Fallback** (try children until one succeeds — "OR"), **Parallel**. +- **Decorators:** Inverter, Repeat, Cooldown, Succeeder, condition guards. +- **Leaves:** Actions (MoveTo, Attack, Reload) and Conditions (IsPlayerVisible, HasAmmo). +- **`Running` is the key concept:** long actions (walking a path) return `Running` and resume next tick — don't block. +- Far more **modular/reusable/authorable** than FSMs; the industry standard for complex NPCs. Consider a lib (behaviortree.js) or a small hand-rolled implementation. +- **Utility AI / GOAP** are alternatives for emergent/goal-driven behavior at higher complexity. + +**Rule:** simple enemies → FSM; complex/multi-goal NPCs → behavior tree. Don't build a BT framework for a single 3-state slime. + +--- + +## 5. Navmesh basics (3D / open spaces) + +- Grids are natural for tile games; for **open 3D/continuous** worlds, a **navigation mesh** (convex polygons covering walkable surfaces) is more efficient and gives smoother paths than a dense grid. +- Run A* over the polygon graph (portal edges), then **string-pull ("funnel algorithm")** through polygon portals for a smooth, natural path. +- In JS: **recast-navigation-js** (WASM port of industry-standard Recast/Detour) bakes navmeshes from level geometry and provides crowd/agent avoidance; three-pathfinding for a simpler Three.js navmesh follower. +- Navmeshes handle multi-level geometry (ramps, bridges) that flat grids can't; support off-mesh links (jumps, ladders). + +--- + +## 6. Performance & correctness rules +- **Don't A* every agent every frame.** Cache paths; recompute on target movement or a throttled interval; **time-slice** pathfinding across frames or use a **web worker** for big searches so the main thread doesn't hitch. +- **Flow field / Dijkstra map** when many agents share one goal — one computation for all. +- **Binary heap** open set, **typed arrays** for large grids, reuse buffers (no per-search allocation churn). +- **Run AI on a fixed timestep** (decoupled from render) for determinism/netcode; often at a lower rate than render (e.g. 10–20Hz for decisions) with steering interpolating between. +- **Stagger** AI updates across agents (LOD-AI): distant/off-screen agents think less often. +- Handle **dynamic obstacles**: mark blocked cells / re-path when the map changes; use local steering avoidance for moving agents. + +--- + +## 7. Bug-prevention checklist +- **Non-admissible heuristic** (Euclidean on 4-dir, Manhattan on 8-dir) → non-optimal or slow paths; match heuristic to movement. +- **Linear open-set scan** → severe stutter on big maps; use a binary heap. +- **A* per agent per frame** → frame drops; cache/throttle/flow-field/worker. +- **Following raw grid path** → robotic zig-zag; smooth/string-pull and add steering. +- **No Arrive/slowing radius** → agents overshoot and orbit the target. +- **Corner-cutting diagonals** → agents clip through wall corners; forbid unsafe diagonals. +- **FSM sprawl** → unmaintainable transitions; switch to a behavior tree. +- **Blocking actions in a BT** → tree stalls; return `Running` for long actions. +- **Ignoring "no path"** → crashes/agents freeze; handle unreachable goals explicitly. +- **Not re-pathing on map change** → agents walk into new walls; invalidate cached paths. +- **AI tied to render frame rate** → non-deterministic/uneven; fixed timestep, staggered. + +--- + +## Defaults to apply +- **Default AI stack:** **A\* on a grid** (binary-heap open set, movement-matched heuristic, weighted tiles) for "where," **steering behaviors** (seek/arrive/avoid/separation) for smooth "how," and an **FSM for simple enemies / behavior tree for complex NPCs** for "what to do." +- **Use flow fields (BFS/Dijkstra map) when many agents chase one target** (tower defense/RTS) — big perf win over per-agent A*. +- **For 3D/open worlds, generate navmeshes via recast-navigation-js**; smooth paths with the funnel algorithm. +- **Bake in the perf guardrails**: cache paths, throttle/time-slice/worker pathfinding, run AI on a fixed lower-rate tick with steering interpolation, and stagger distant agents. Always handle "no path found." + +--- + +## Sources +- Red Blob Games — Introduction to A* (canonical, interactive): https://www.redblobgames.com/pathfinding/a-star/introduction.html ; A* Implementation Guide: https://www.redblobgames.com/pathfinding/a-star/implementation.html +- Red Blob Games — Grids/heuristics & "Pathfinding for Tower Defense" (flow fields): https://www.redblobgames.com/pathfinding/tower-defense/ ; grid pathfinding tricks: https://www.redblobgames.com/pathfinding/grids/algorithms.html +- Craig Reynolds — "Steering Behaviors For Autonomous Characters": https://www.red3d.com/cwr/steer/ ; Boids: https://www.red3d.com/cwr/boids/ +- Amit Patel / Red Blob — heuristics guide: https://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html +- PathFinding.js (grid A*/JPS/BFS): https://github.com/qiao/PathFinding.js +- Jump Point Search (Harabor & Grastien): https://harablog.wordpress.com/2011/09/07/jump-point-search/ +- Behavior Trees — Chris Simpson, "Behavior trees for AI: How they work": https://www.gamedeveloper.com/programming/behavior-trees-for-ai-how-they-work ; behaviortree.js: https://github.com/Calamari/BehaviorTree.js +- Game Programming Patterns — State (FSM): https://gameprogrammingpatterns.com/state.html +- Recast/Detour navmesh: https://github.com/recastnavigation/recastnavigation ; recast-navigation-js: https://github.com/isaac-mason/recast-navigation-js ; three-pathfinding: https://github.com/donmccurdy/three-pathfinding +- GOAP (Jeff Orkin, F.E.A.R.): https://alumni.media.mit.edu/~jorkin/goap.html diff --git a/.grok/skills/building-games/references/audio.md b/.grok/skills/building-games/references/audio.md new file mode 100644 index 0000000..ca86a7f --- /dev/null +++ b/.grok/skills/building-games/references/audio.md @@ -0,0 +1,129 @@ +# Game Audio in the Browser (Web Audio API, mobile unlock, Howler.js, gain buses, spatial, latency) + +Consolidated from MDN Web Audio docs and Howler.js docs (see Sources). Focus: what an AI builder needs so audio **actually plays on mobile, stays low-latency, and mixes cleanly** — the #1 audio bug in browser games is "no sound on iOS." + +--- + +## 1. The autoplay unlock — the single most important rule + +Browsers block audio until the user interacts with the page. An `AudioContext` created on page load starts in the **`"suspended"`** state and will silently play nothing until resumed. + +**Rule: create/resume the AudioContext (or unlock your audio lib) from inside the FIRST real user gesture** (`click`/`touchend`/`keydown`), and call `resume()` **synchronously** in that handler's call stack (iOS Safari is strict — an `await` before `resume()` can break the gesture chain). + +```js +let audioCtx; +function unlockAudio() { + if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + if (audioCtx.state === 'suspended') audioCtx.resume(); // call synchronously in the gesture +} +// Attach once; remove after first success. `pointerdown`/`touchend`/`keydown` all count. +window.addEventListener('pointerdown', unlockAudio, { once: true }); +window.addEventListener('keydown', unlockAudio, { once: true }); +``` + +- Show a "tap to start" / "click to play" screen so the first gesture reliably unlocks audio (and any pointer lock / fullscreen you need). Games should not expect audio before the player clicks in. +- Also **resume on `visibilitychange`/focus** — contexts can get re-suspended when a tab is backgrounded on mobile. +- Listen to the `statechange` event if you need to reflect mute/paused UI. + +--- + +## 2. Web Audio API vs `