From 6dc1df528bad48626edc38f282340551ab4a07d9 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 30 Jun 2026 15:39:37 +0530 Subject: [PATCH 1/7] =?UTF-8?q?docs(spec):=20desktop=20workbench=20?= =?UTF-8?q?=E2=80=94=20workspace/project=20explorer=20design?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../2026-06-30-desktop-workbench-design.md | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-30-desktop-workbench-design.md diff --git a/docs/superpowers/specs/2026-06-30-desktop-workbench-design.md b/docs/superpowers/specs/2026-06-30-desktop-workbench-design.md new file mode 100644 index 0000000..58f4161 --- /dev/null +++ b/docs/superpowers/specs/2026-06-30-desktop-workbench-design.md @@ -0,0 +1,153 @@ +# MotionCode Desktop Workbench — Workspace/Project Explorer + +Date: 2026-06-30 +Status: Approved (design), pending implementation plan + +## Goal + +Turn the authed MotionCode app into a desktop-style workbench. Workspaces are +**folders** in a persistent left explorer tree; each workspace holds many +**projects**. Selecting a project opens the **Analyze Studio** directly in the +main pane, so analysis happens right there without leaving the screen. + +This extends the already-approved Analyze Studio spec +(`2026-06-30-analyze-studio-design.md`): that spec established the persistent +sidebar + editor/live-preview studio; this spec makes the sidebar a +workspace→project explorer and the explorer the primary navigation. + +## Decisions (locked) + +- **Approach A — persistent layout + real routes.** A shared Next.js layout + renders the desktop chrome (icon rail + explorer tree) and stays mounted while + the main pane swaps via routes. Keeps deep-linkable/shareable URLs, + server-fetched tree data, and reuses `AnalyzeStudio`. (Rejected B: single + client workbench — loses routing, deep-link/refresh, makes the studio a large + client island.) +- **Nesting:** two levels only — workspace (folder) → projects. No sub-folders + inside a workspace in this iteration. +- **Tree is the primary nav.** It replaces the current product nav + (Analyze/Dashboard/Projects/Workspaces list) as the main way to move around. +- **Main pane on project select = Analyze Studio directly** (full-bleed). + +## Architecture (3 parts) + +### 1. `Workbench` desktop shell + +Evolve `components/dashboard/app-shell.tsx` into a three-column desktop shell: + +- **Icon rail** (~56px, fixed): MotionCode mark at top; icon buttons for the + *global, non-per-project* destinations — Dashboard, Account, Billing, Sign + out. Hover tooltips. Active state on current section. +- **Explorer column** (~260px, collapsible): the workspace→project tree (below). + Collapsible to give the studio more room; collapsed state persists per session. +- **Main pane**: routed children. Full-bleed (no max-width/padding) when the + studio owns it; padded otherwise. + +Keeps a props API compatible with today's `AppShell` (`active`, `userEmail`, +`children`, `bleed`) so existing pages migrate with minimal churn. + +### 2. Explorer tree — `components/app/explorer/` + +- `ExplorerTree.tsx` — client component. Receives server-fetched + `{ workspaces, projectsByWorkspace }` seed; renders folder rows. +- `WorkspaceNode.tsx` — chevron + folder icon + name. Expanding reveals nested + project rows. Hover actions: `+` (new project in this workspace), rename. +- `ProjectNode.tsx` — nested row; click navigates to the project's studio route. +- `InlineCreate.tsx` — inline text input for creating a workspace (top of tree) + or a project (under a workspace). Replaces the current full-page/standalone + `CreateWorkspaceForm` flow. Submits to existing APIs. +- **Selection + expansion live in the URL** (active route segment = selected + project/workspace; expanded set derived from / persisted alongside it) so + refresh and deep-links restore tree state. +- **Empty state:** zero workspaces → inline "Create your first workspace" prompt + in place of the tree body. + +### 3. Main pane states (routed) + +- **No selection** (`/app` or workbench root) → light home overview: recent + projects, prompt to pick/create a workspace. +- **Workspace selected** (`/workspaces/[id]`) → slimmed workspace summary + (members, project count, settings) rendered into the main pane. +- **Project selected** (`/projects/[id]`) → `AnalyzeStudio` full-bleed + (editor + live preview), reusing `components/app/studio/AnalyzeStudio.tsx`. + +## Routing / layout + +- New route group `app/(workbench)/layout.tsx` renders `Workbench` (chrome + + explorer) once and wraps the workspaces / projects / analyze routes. Next.js + layouts stay mounted across child navigation → the tree does not reload, which + is what produces the desktop feel. +- Tree seed data is fetched in the layout (server) via the existing + `getDashboardData` shape (`workspaces`, `projects` with `workspace_id`). + Projects already carry `workspace_id`, so grouping is a pure transform — no + schema change for nesting. + +## Creation bug fix ("Failed to create workspace") + +Observed: creating a workspace from `/workspaces` returns the generic "Failed to +create workspace." + +Root-cause path: `createWorkspaceWithSupabase` inserts only +`{ name, owner_id, slug }`. `owner_id` is a FK to `public.profiles(id)`, and the +INSERT RLS policy requires `owner_id = auth.uid() AND plan_tier = 'free'` +(`plan_tier` defaults to `'free'`, so that clause passes). The most probable +failure is a **missing `profiles` row** for the current user → FK violation. The +handler swallows the real Postgres error into a generic string +(`toErrorResponse` → `INTERNAL_ERROR`), which hides the cause. + +Fix (to confirm against the live error during build): +1. Stop swallowing — log/surface the actual Supabase error code+message + server-side so the true cause is visible. +2. Ensure a `profiles` row exists for the authenticated user before/at first + write (ensure-profile helper, or verify the auth.users→profiles trigger is + present in this environment; add a migration if missing). +3. Confirm the fix by creating a workspace end to end and seeing it land in the + tree. + +## Files + +``` +components/app/Workbench.tsx (new desktop shell; from app-shell.tsx) +components/app/explorer/ExplorerTree.tsx (new) +components/app/explorer/WorkspaceNode.tsx (new) +components/app/explorer/ProjectNode.tsx (new) +components/app/explorer/InlineCreate.tsx (new) +app/(workbench)/layout.tsx (new persistent layout) +app/workspaces/page.tsx (slim → main-pane overview) +app/workspaces/[workspaceId]/page.tsx (slim → main-pane workspace summary) +app/projects/[projectId]/page.tsx (render AnalyzeStudio full-bleed) +app/api/workspaces/handler.ts (stop swallowing error; profiles ensure) +components/app/studio/AnalyzeStudio.tsx (reused as-is) +supabase/migrations/ (only if profiles trigger missing) +``` + +## Data flow + +Layout (server) → `{ workspaces, projects }` → group projects by `workspace_id` +→ `ExplorerTree` seed. Expand workspace → show its projects. Click project → +route to `/projects/[id]` → layout stays mounted, main pane renders +`AnalyzeStudio`. Inline create → POST existing workspace/project APIs → +`router.refresh()` re-seeds the tree. + +## Error handling + +- Create workspace/project failure → inline error on the create input + real + cause logged server-side (no more silent generic message). +- Workspace/project not found or no access → main pane shows not-found/forbidden + state; tree stays usable. +- Studio errors handled by the existing Analyze Studio spec (iframe sandbox, + console bridge). + +## Testing + +- **Unit:** group-projects-by-workspace transform; inline-create slug/validation; + empty-tree state. +- **E2E:** create workspace → appears in tree → add project under it → expand → + select project → studio renders → deep-link to `/projects/[id]` restores + expanded tree with the project selected. + +## Out of scope (this iteration) + +- Sub-folders inside a workspace. +- Drag-and-drop reordering / moving projects between workspaces. +- Multi-select / bulk actions. From e7dbfce447ba92bb7512705421285b8565d02333 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 30 Jun 2026 22:03:13 +0530 Subject: [PATCH 2/7] docs(spec): paid-gate workbench + read-only free studio Co-Authored-By: Claude Opus 4.8 --- .../projects/[projectId]/page.tsx | 0 .../[projectId]/versions/[versionId]/page.tsx | 0 app/{ => (workbench)}/projects/page.tsx | 0 .../workspaces/[workspaceId]/page.tsx | 0 app/{ => (workbench)}/workspaces/page.tsx | 0 ...6-30-paid-gating-readonly-studio-design.md | 102 ++++++++++++++++++ 6 files changed, 102 insertions(+) rename app/{ => (workbench)}/projects/[projectId]/page.tsx (100%) rename app/{ => (workbench)}/projects/[projectId]/versions/[versionId]/page.tsx (100%) rename app/{ => (workbench)}/projects/page.tsx (100%) rename app/{ => (workbench)}/workspaces/[workspaceId]/page.tsx (100%) rename app/{ => (workbench)}/workspaces/page.tsx (100%) create mode 100644 docs/superpowers/specs/2026-06-30-paid-gating-readonly-studio-design.md diff --git a/app/projects/[projectId]/page.tsx b/app/(workbench)/projects/[projectId]/page.tsx similarity index 100% rename from app/projects/[projectId]/page.tsx rename to app/(workbench)/projects/[projectId]/page.tsx diff --git a/app/projects/[projectId]/versions/[versionId]/page.tsx b/app/(workbench)/projects/[projectId]/versions/[versionId]/page.tsx similarity index 100% rename from app/projects/[projectId]/versions/[versionId]/page.tsx rename to app/(workbench)/projects/[projectId]/versions/[versionId]/page.tsx diff --git a/app/projects/page.tsx b/app/(workbench)/projects/page.tsx similarity index 100% rename from app/projects/page.tsx rename to app/(workbench)/projects/page.tsx diff --git a/app/workspaces/[workspaceId]/page.tsx b/app/(workbench)/workspaces/[workspaceId]/page.tsx similarity index 100% rename from app/workspaces/[workspaceId]/page.tsx rename to app/(workbench)/workspaces/[workspaceId]/page.tsx diff --git a/app/workspaces/page.tsx b/app/(workbench)/workspaces/page.tsx similarity index 100% rename from app/workspaces/page.tsx rename to app/(workbench)/workspaces/page.tsx diff --git a/docs/superpowers/specs/2026-06-30-paid-gating-readonly-studio-design.md b/docs/superpowers/specs/2026-06-30-paid-gating-readonly-studio-design.md new file mode 100644 index 0000000..ca7bc16 --- /dev/null +++ b/docs/superpowers/specs/2026-06-30-paid-gating-readonly-studio-design.md @@ -0,0 +1,102 @@ +# MotionCode — Paid Gating + Read-Only Free Studio + +Date: 2026-06-30 +Status: Approved, in implementation + +## Goal +Lock the management surfaces (Dashboard, Projects, Workspaces) behind a paid plan, +and turn the Analyze studio into a read-only experience for free users: they can +analyze, see the live preview, and copy code — nothing else. Paid users keep the +full editable studio. + +## Decisions (locked) +- **Free tier surface:** Analyze + live Preview + Copy code. Read-only. + - No code editing, no Run / Format / Reset, **no Download**. + - Spec & audit drawer (`MotionSpecPanel`) fields are read-only for free. + - Framework tabs (CSS/GSAP/Framer/Spring), preview Replay, and "New analysis" + stay available (they don't mutate or persist anything). +- **Paid tier:** unchanged full editable studio (edit, Run, Format, Reset, Copy, + Download, editable spec). +- **Locked pages → in-place upgrade gate** (not a silent redirect), rendered inside + the existing shell. Applies to all viewable paid pages, including detail pages. +- **Nav:** locked items stay visible with a lock badge (advertises the upgrade). +- `/onboarding` keeps its existing `paidOnly` redirect (it is a flow, not a surface). + +## A. Page gating + upgrade gate + +`requireDashboardUser(next, { paidOnly: true })` currently **redirects** free users to +`/app`. Switch viewable paid pages to render a gate in place. + +- **New** `components/app/UpgradeGate.tsx` (server component): branded + "Upgrade to unlock {feature}" panel — headline, 2–3 benefit bullets, primary CTA + → `/pricing`, secondary → `/app`. Brand tokens (`--accent`, mono type), matches + studio styling. +- **New helper** in `app/dashboard/data.ts`: `resolvePlanGate(userId)` → + `{ isPaid: boolean; planTier: PlanTier }`, thin wrapper over `getEntitlementSummary`. +- **Pages** (`force-dynamic` already set): + - `/projects` (index) — add gate (currently ungated) + - `/workspaces` (index) — add gate (currently ungated) + - `/dashboard` — swap `paidOnly` redirect → gate + - `/projects/[projectId]` — swap → gate + - `/projects/[projectId]/versions/[versionId]` — swap → gate + - `/workspaces/[workspaceId]` — swap → gate + - Pattern: + ```ts + const user = await requireDashboardUser("/projects"); + const { isPaid } = await resolvePlanGate(user.id); + if (!isPaid) return ; + ``` + - `/onboarding` — unchanged (`paidOnly` redirect retained). + +## B. Nav lock badges (plan-aware) + +- `components/dashboard/app-shell.tsx`: optional `planTier?: PlanTier` prop + (default `"free"`). For free users, Dashboard/Projects/Workspaces items render a + trailing `Lock` icon (lucide); Analyze stays open. Links still navigate (land on + the gate). `/app/page.tsx` and `/dashboard` pass the resolved `planTier`. +- `components/app/Workbench.tsx`: rail exposes only Analyze + Dashboard, so Dashboard + is its sole lockable item. Add `planTier?: PlanTier` prop and lock the Dashboard + rail link for free; `(workbench)/layout.tsx` resolves `planTier` once + (via `getEntitlementSummary`) and passes it in. + +## C. Read-only studio for free + +Thread `editable: boolean` (`planTier !== "free"`) down: +`AppShell` → `AnalyzeStudio` → `EditorPane` / `MotionSpecPanel` → `CodeMirrorEditor`. + +- **`CodeMirrorEditor`**: new `editable` prop. When false, add + `EditorState.readOnly.of(true)` and `EditorView.editable.of(false)` to the + extensions (and into the mount). Preview still runs the original generated code, + so the animation plays. +- **`EditorPane`**: when `!editable`, render only the Copy button (plus framework + tabs + file label). Hide Format, Reset, Run, **Download**. Show a subtle + "Read-only · Upgrade to edit & export" hint linking to `/pricing`. +- **`AnalyzeStudio`**: pass `editable` through; when not editable, `dirty` is always + false, edit handlers are inert, and the spec drawer is read-only. +- **`MotionSpecPanel`**: accept `editable` (default true); when false, fields render + as static values (no inputs), matching the read-only intent. +- **`AppShell` (`components/app/AppShell.tsx`)**: derive `editable = userPlan !== "free"` + and pass to `AnalyzeStudio`. + +## Data flow +`/app` server page resolves `summary.planTier` → `AppShell(initialPlanTier)` → +`editable = planTier !== "free"` → studio renders editable or read-only. Locked pages +resolve `isPaid` server-side and short-circuit to ``. + +## Error handling +- Gate is a pure render; no data fetch beyond `getEntitlementSummary` (already used). +- Read-only CodeMirror cannot dispatch user edits; external value sync (tab switch) + still works via programmatic dispatch. + +## Testing +- Unit: `resolvePlanGate` returns isPaid for paid tiers, false for free. +- Unit: `CodeMirrorEditor` mounts non-editable when `editable={false}` (no contentEditable). +- Unit: `EditorPane` hides Format/Reset/Run/Download and keeps Copy when `!editable`. +- Update existing tests that assert `paidOnly` redirect on `/dashboard` etc. to assert + gate render. + +## Phasing +1. `resolvePlanGate` + `UpgradeGate` + convert paid pages (A). +2. Nav lock badges in both shells (B). +3. Read-only thread through studio (C). +4. Tests + `npm run build` (production) green. From 8260b680820a1c8c8c52c0838f85aba023c6b437 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 1 Jul 2026 02:14:26 +0530 Subject: [PATCH 3/7] chore: foundation for workbench, gating, and live plan sync Add the supporting layer the new surfaces build on: - lib/preview (preview doc builder + console bridge) and lib/workbench/tree - analysis usage reserve/release recorder in lib/server/audit - migrations: release_analysis_usage_event RPC (quota rollback on failure) and realtime publication for live plan-tier sync - CodeMirror/codemirror deps, brand + auth assets, theme tokens Co-Authored-By: Claude Opus 4.8 --- app/globals.css | 118 ++++++++ .../specs/2026-06-30-analyze-studio-design.md | 67 +++++ eslint.config.mjs | 1 + lib/preview/buildPreviewDoc.ts | 256 ++++++++++++++++++ lib/preview/consoleBridge.ts | 50 ++++ lib/preview/types.ts | 52 ++++ lib/server/audit.ts | 23 ++ lib/workbench/tree.ts | 34 +++ package-lock.json | 212 +++++++++++++++ package.json | 8 + public/auth/motioncode-figure.png | Bin 0 -> 16719 bytes public/brand/logo-120.png | Bin 0 -> 10965 bytes public/brand/logo-512.png | Bin 0 -> 59965 bytes public/brand/logo.svg | 34 +++ public/vendor/gsap.min.js | 11 + ...add_release_analysis_usage_reservation.sql | 48 ++++ ...260701020000_enable_realtime_plan_sync.sql | 35 +++ 17 files changed, 949 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-30-analyze-studio-design.md create mode 100644 lib/preview/buildPreviewDoc.ts create mode 100644 lib/preview/consoleBridge.ts create mode 100644 lib/preview/types.ts create mode 100644 lib/workbench/tree.ts create mode 100644 public/auth/motioncode-figure.png create mode 100644 public/brand/logo-120.png create mode 100644 public/brand/logo-512.png create mode 100644 public/brand/logo.svg create mode 100644 public/vendor/gsap.min.js create mode 100644 supabase/migrations/20260630154500_add_release_analysis_usage_reservation.sql create mode 100644 supabase/migrations/20260701020000_enable_realtime_plan_sync.sql diff --git a/app/globals.css b/app/globals.css index d3fd037..e4624ab 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2303,6 +2303,102 @@ body::after { transform: translateY(-1px); } +/* Razorpay checkout button sits in the same slot as the ghost CTA. */ +.motioncode-pricing-checkout { + position: relative; + z-index: 1; + margin-top: auto; +} + +/* The standalone /pricing page has no JS hover tracking like the landing + section, so mirror the lift with a pure-CSS :hover, scoped to this grid. */ +.motioncode-pricing-grid[data-static="true"] .motioncode-pricing-card:hover { + transform: translateY(-8px); + border-color: rgba(216, 207, 188, 0.58); + box-shadow: + 0 38px 90px rgba(0, 0, 0, 0.48), + 0 0 42px rgba(0, 255, 136, 0.07), + inset 0 1px 0 rgba(255, 251, 244, 0.12); +} + +.motioncode-pricing-grid[data-static="true"] .motioncode-pricing-card:hover::before { + opacity: 1; +} + +.motioncode-pricing-fineprint { + margin: 28px 0 0; + color: rgba(216, 207, 188, 0.5); + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.02em; +} + +/* Standalone /pricing page: tighter rhythm + balanced two-column header so the + hero doesn't read lopsided, and the cards don't carry the landing section's + tall reserved header slot. */ +.pricing-page { + padding-top: 84px; + padding-bottom: 104px; +} + +.pricing-page-head { + display: grid; + grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); + align-items: end; + gap: 40px; + margin-bottom: 52px; +} + +.pricing-page-head-lede { + min-width: 0; +} + +.pricing-page-title { + margin: 16px 0 0; + color: var(--text); + font-family: var(--font-mono); + font-size: clamp(34px, 4.2vw, 52px); + line-height: 1.03; + letter-spacing: -0.01em; + text-wrap: balance; +} + +.pricing-page-sub { + margin: 0 0 6px; + max-width: 48ch; + color: rgba(216, 207, 188, 0.72); + font-size: 15px; + line-height: 1.8; +} + +.motioncode-pricing-grid[data-static="true"] .motioncode-pricing-card-top { + min-height: 84px; +} + +@media (max-width: 900px) { + .pricing-page { + padding-top: 64px; + padding-bottom: 80px; + } + + .pricing-page-head { + grid-template-columns: 1fr; + align-items: start; + gap: 16px; + margin-bottom: 36px; + } + + .pricing-page-sub { + margin-bottom: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + .motioncode-pricing-grid[data-static="true"] .motioncode-pricing-card:hover { + transform: none; + } +} + .motioncode-final-cta { position: relative; display: flex; @@ -6659,3 +6755,25 @@ body::after { width: 100%; } } + +/* Sign-in dialog: a quiet hover-in over the live motion field. */ +@keyframes loginDialogIn { + from { + opacity: 0; + transform: translateY(10px) scale(0.985); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.login-dialog { + animation: loginDialogIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +@media (prefers-reduced-motion: reduce) { + .login-dialog { + animation: none; + } +} diff --git a/docs/superpowers/specs/2026-06-30-analyze-studio-design.md b/docs/superpowers/specs/2026-06-30-analyze-studio-design.md new file mode 100644 index 0000000..1bb068b --- /dev/null +++ b/docs/superpowers/specs/2026-06-30-analyze-studio-design.md @@ -0,0 +1,67 @@ +# MotionCode Workspace Redesign — Analyze Studio + +Date: 2026-06-30 +Status: Approved, in implementation + +## Goal +One cohesive, navigable authed workspace with a persistent left sidebar, and an +Analyze page rebuilt as a split-resizable **editor / live-preview studio** where +the generated animation actually plays right after analysis. + +## Decisions (locked) +- Scope: unified manageable workspace; easy nav between Workspaces/Projects/Analyze; "see the final output". +- Preview engine: sandboxed-iframe execution (production-grade; code actually runs). +- Editor: editable + Run + Console (like the playground reference). +- Navigation: persistent left-sidebar shell across the authed app. +- Build approach: react-resizable-panels + CodeMirror 6 + from-scratch iframe runtime. + +## Architecture (3 layers) +1. **WorkspaceShell** — evolves `components/dashboard/app-shell.tsx` into a left + sidebar; keeps props API (`active`, `userEmail`, `children`). Used by all authed + pages including Analyze. +2. **AnalyzeStudio** — `react-resizable-panels` horizontal split inside the shell. + - Left: EditorPane — toolbar (framework tabs CSS/GSAP/Framer/Spring · Format · Run · + Reset · Copy/Download) + CodeMirror 6 themed to brand. Editable per-tab state. + - Right: PreviewPane — Preview | Console tabs, live iframe stage, `READY · {ms}` status strip. + - Upload + frame controls in compact rail; pre-analysis shows ProcessCanvas, on `done` + the studio takes over. Spec/Scorecard relocate to a collapsible drawer. +3. **PreviewRuntime** — sandboxed `