Returning to the page via browser Back (e.g. from the /download
redirect's external target) restored it from the bfcache in Firefox:
the JS heap thaws but the WebGL context is discarded, so the galaxy
canvas came back blank and the rAF-driven instruments looked
unmounted. Chrome restores GPU state, which is why it never showed.
The pre-paint head script now attaches a pageshow listener that
reloads on e.persisted — the listener lives inside the frozen page,
so it fires on the restore itself. The intro is seen-flagged by
then and the page is statically prerendered, so the reload is cheap.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
The per-arm conveyor offset was in absolute cycle units, so it
resonated whenever it landed near a multiple of the slot spacing:
at 51 cards (9 slots/arm, spacing .111) the .13 offset was one slot
+ 17%, radially aligning every arm's ladder into six-fold rings —
the starfish. 64 cards (spacing .0909) just happened to dodge it.
The stagger is now measured in slot gaps (divided by slotsPerArm), so
it can never ring-align at any card count or viewport scale, and the
default moves to 0.38 — the golden-ratio conjugate — which lands the
six arms at low-discrepancy phases within the gap.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
With 17 clips on the CDN, 51 cards lands the round-robin deal at
exactly three cards per clip (down from ~4.6 repeats at 64). The
neighbor-gap collision sizing grows the cards to keep the same screen
fill, and smaller viewports still scale the count down from the same
Tuned @ Mpx reference.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
The CDN holds 17 clips but the wall only dealt the first 14. Verified
015-017 return 200 (018+ 404). At the tuned 64-card count this drops
simultaneous repeats per clip from ~4.6 to ~3.8; the least-recently-
shown rebirth reassignment spreads what remains.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
The max-width content container carried the site's border-x rails, so
two vertical hairlines cut through the immersive spiral/wordmark stage.
The poster now runs edge to edge; the structural rail-and-tick language
begins at the announce strip (the fold line), which keeps its border-x.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
The #1916 restyling wrote `<LoaderCircleIcon spin>` in five spots, but
raw lucide-react icons have no `spin` prop — the attribute fell through
to the SVG (React DOM warning) and the spinner never animated. The two
spots inside ImagePromptRow were corrected during the services/main
merge resolution; this covers the remaining three files with the same
animate-spin class main uses. Also drops the unused DynamicIcon import
in create-video (pre-existing on both lineages).
`tsc --noEmit -p apps/artcraft-webapp/tsconfig.app.json` is now clean.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
The design branch forked from the old monorepo main at f3442f3146;
main then advanced through #1910 (last-viewed badge), #1911 (iOS
upload fix — uploading queue stores previewUrl, not File), #1912,
#1699 (keybinds), the auth-page video update (976f2ae757, Vimeo
iframe → muted <video>), and the modal freeze fix (ed57c6de47,
auto-merged) before the artcraft/artcraft-services split.
Seven files conflicted where main's functional work crossed the
branch's #1913/#1916 webapp restyling. Resolution rule: main's
functionality, the design branch's styling —
- auth-showcase: main's <video> element, squared corners kept
- ImagePromptRow (webapp + libs): main's previewUrl refactor; kept
square corners/hairline borders. The branch's `spin` prop was a
latent bug (raw lucide icons have no such prop — no animation and
a React DOM warning), so main's animate-spin class wins.
- GalleryCard/GalleryRow: main's lastViewed ring + badge woven into
the branch's squared-corner classes (badge pill squared to match)
- old-site app.tsx: union of both new routes (landingp0 + creator)
- TimelineClipRow: main's enableClipTransition undo-step flow
superseded the branch's setClipTransitionEasing(DEFAULT_EASING)
artcraft-website-next is untouched by the incoming side. Webapp
typechecks clean after the follow-up spinner fixes.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
Proofing-pass deployment plumbing. The app referenced seven marketing
routes it doesn't serve yet (/download, /pricing, /tutorials, /news,
/faq, /press-kit, /support) — every one now 307s to the shipping Vite
site, which this app already depends on staying live (MEDIA_BASE
feature footage). Drop a route from LEGACY_ROUTES as its page migrates.
netlify.toml follows the sibling apps' discovery pattern with base
frontend/apps/artcraft-website-next (standalone lockfile, not the Nx
workspace) and the Next runtime plugin, which also carries the
/cdn-media and /ext-media proxy rewrites into production.
turbopack.root and outputFileTracingRoot are pinned to the app dir —
Next was guessing the Nx workspace's lockfile as the project root,
which breaks output file tracing on serverless deploys.
Claude-Session: https://claude.ai/code/session_01YBoaBEt3gynzTwogBFtn4B
Opening a generated image/video and quickly clicking the backdrop to
close left the whole page unclickable, with no visible overlay to
explain why.
**Cause:** `Dialog.Overlay`/`Dialog.Content` are `forceMount`ed so
react-spring owns the leave animation. On close `Dialog.Root`'s `open`
flips false while the layer is still mounted, and Radix derives
`disableOutsidePointerEvents` from that flag, so its cleanup never
restores the `pointer-events: none` it set on `<body>`. Closing slowly
masked it; closing fast interrupts the leave spring so the clearing
render never comes.
**Fix:** `unregisterModal` now schedules `releaseStrandedBodyLock`,
clearing a lock nothing still owns. Bails if a modal is still registered
or a real open Radix dialog exists.
Adds a new creator spotlight landing page to the artcraft-website under
`/creators/`.
### Changes
- New spotlight page: hero with fanned reel triptych, credits marquee,
work grid, craft section, and closing CTA, all in the existing
landing-page design language
- Reel cards use locally hosted thumbnails (`public/images/creators/`)
and link out to the posts on Instagram, no embeds
- Primary "Create Now" CTA routes subscribers to the webapp and everyone
else to `/pricing`
- SEO wired up: `PAGE_META` entry for the prerender step, `_redirects`
rules plus short 301 aliases for the route
Recreate restored reference videos/audios from the prompt record with duration: 0. The cost quote forwards that as a zero input-duration hint, and the backend deliberately prices a zero/unknown input at the worst-case 30-second billing maximum (Seedance 2.5 bills reference-video input seconds), so a recreated generation quoted far above the original
charge, e.g. a 1080p 10s output with ~5s of input quotes 548 credits, but 1096 with the failsafe.
Measure each restored reference's duration from its CDN copy while building the recreate payload, the same way the library "Send to prompt" path probes durations. Unmeasurable files keep 0 (worst-case quote as before; generation always bills from its own server-side measurement).
Add Billing tab to webapp settings
- New "Billing" tab in the settings modal, between Account and API Keys
- Shows your current plan (Free/Basic/Pro/Max)
- Subscribers get a "Manage Plan" button that opens the Stripe portal,
same as the pricing page, plus a "View Plans" link for switching
- Free users get a "View Plans" button to the pricing page
- Handles signed-out and loading states like the other tabs
## Summary
Adds a unified, remappable keyboard-shortcut system
(`@storyteller/keybinds`) across the webapp and desktop app, replacing
the scattered hardcoded `keydown` listeners in the 3D editor, 2D editor,
and moodboard with one registry, two presets (**Gamer** = today's
defaults, **Blender-like**), a Settings → Keybinds pane, and a hold-Ctrl
/ pinnable cheatsheet on every editor surface.
This PR was approved in June and then sat behind ~195 commits on `main`
(timeline, build/record modes, lucide migration). It has been merged
with current `main` and extended to cover the features that landed
since. **Nothing changes by default** — the Gamer preset reproduces
every existing key exactly.
## Architecture
- **Registry of `ActionDef`s** keyed by surface-namespaced ids
(`pagescene.timeline.playPause`), bound to physical `event.code`.
Resolution: user override → preset delta → base bindings. Persisted in
the `artcraft-keybinds` zustand store.
- **Five surfaces**: `global`, `pagescene` (3D), `pagedraw` (2D),
`moodboard` (canvas), `moodboard-grid` (board library / inspector /
presentation).
- **Declared availability** — `ActionDef.when(ctx)` gates decide whether
a binding is live for the current editor state (`sceneMode`, `encoding`,
`timelineExpanded`, `timelineSelection`, …). An unavailable binding
falls through *without consuming the event*, which is what lets Space /
arrows / Delete / Escape be shared by context-exclusive actions. A
sampled-context checker (`actionsCoAvailable`) proves the exclusivity in
tests and drives the conflict UI in Settings.
- **Global surface is a pure trigger layer**: features register behavior
with `useGlobalAction(id, handler)`; a window capture-phase dispatcher
fires above the modal key-swallow, and an unregistered binding is inert
per host. First inhabitant: Ctrl/Cmd+B sidebar. This is the intended
home for the upcoming privacy-blur toggle (one `ActionDef` + one binding
+ one `useGlobalAction` call).
- Cross-surface conflict detection (global ↔ everything, both
directions); preset-integrity tests guard every preset against
same-surface collisions.
- **Video editor keeps its own keybinds system** — deliberate non-goal.
## What's in it
**3D editor (pagescene)**
- Timeline actions: Space play/pause, ←/→ frame step or keyframe/strip
nudge, Home/End, `I` add keyframe (Blender parity), Shift+T expand,
Delete/Backspace on a valid timeline selection (exact complement of
scene-object delete).
- Record mode: Tab toggles build/record, `C` capture still, `R` record
video (bare letters — Firefox eats Ctrl+Shift+S/E), Escape cancels an
in-flight encode (overlay Cancel button too). Record mode is immutable:
all scene-mutating actions gate off, Escape no longer drops out of the
render cam.
- **Per-edit timeline undo** — every timeline edit is one Ctrl+Z step
(`TimelineEditAction` with whole-timeline snapshots; drags and the
easing popover coalesce into single steps). Save now only moves the
Cancel baseline; Cancel is itself undoable.
- The old "Shortcuts" modal is replaced by the live cheatsheet, pinnable
from the same button.
- Dead code removed: `MouseControls.onkeydown`, `OnboardingHelper`, the
hardcoded `Help` view, three unused pagedraw hotkey hooks.
**2D editor (pagedraw)**
- Tool hotkeys 1–5 with live key badges on the toolbar icons; the shape
key cycles rectangle → circle → triangle (submenu-aware).
- Ctrl+= / Ctrl+- (and numpad) resize the active tool. The eraser now
has its own size (was sharing the brush's) and its own slider popout;
also fixes the eraser button never highlighting.
**Moodboard**
- Canvas keys unchanged (now registry-backed), plus Ctrl+0 reset
viewport.
- Grid: rate 0–5 / `P` pick / Delete / Escape, inspector and
presentation ←/→/Space/Esc — all rebindable. Escape now closes the
inspector; arrows no longer navigate while typing in the tag field.
**Settings & cheatsheet**
- Settings → Keybinds: preset picker, name/key search, per-action rebind
with conflict warnings, per-section reset, and a bottom "danger zone"
reset-all with a two-step confirm (2 s lockout + visible 5 s
auto-disarm).
- Cheatsheet: hold Ctrl (⌘) 3 s to peek; **General → "Keep shortcut
cheatsheet open"** makes it stick until Esc / click-outside; the 3D
"Shortcuts" button pins it.
## Testing
- `@storyteller/keybinds` unit tests (29): resolution layering,
overrides, per-surface/global reset, conflict detection incl.
global↔surface, matcher semantics, and preset integrity across all
presets.
- Builds green: `keybinds`, `ui-pagescene`, `ui-pagedraw`,
`ui-moodboard`, `ui-settings-modal`, `artcraft-webapp`, `artcraft`
(desktop).
- Manual QA pass completed on the webapp for every area above (settings
pane, cheatsheets, Blender modal transforms, timeline/record keys,
pagedraw toolbar, global surface, reset UX, pinned cheatsheet, timeline
undo, Escape contexts).
## Notes for review
- The keybinds store is unversioned-migration-free on purpose — the PR
was never merged, so there's no persisted shape to migrate from.
- Space-hold pan (moodboard) and held WASD camera movement stay as
gestures, resolved from the same store but outside the discrete-action
dispatch.
---------
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Tuner (see TUNER.md): a draggable, collapsible dev panel rendering every
group registered via defineTunables() as slider + number-input rows.
- Live: physics/light/material/halo values are read per frame; formation
values trigger a debounced re-sample
- Values persist to localStorage; modified labels tint accent; Copy puts a
JSON snapshot on the clipboard for baking back into defaults; Reset clears
- Shown in dev builds or with ?tuner=1; drag position and section collapse
state persist
- Policy (documented): every new tweakable constant registers with the tuner
by default
- Hero groups: Formation, Physics, Cursor & Light, Material, Halo
Formation: the hex lattice read too uniform. Balls are now placed by
multi-pass dart throwing over the glyph mask — big balls claim spots first,
smaller ones settle into leftover gaps, random depth within the slab, 3D
overlap rejection via spatial hash — a faked settled-pile impression, not a
physics solve. Stroke-margin test keeps big balls inside the letterforms so
the type stays crisp.
Drag and drop didn't work while the prompt focus mode was open - the
modal sat on top of the inline promptbox's drop zone, so files just did
nothing (webapp and desktop).
### Changes
- `usePromptBoxDrop` now exposes `fullscreenDropZoneProps` - same
handlers/routing, second ref so the Tauri hit-test follows the
focus-mode modal while it's open
- `PromptFullscreenModal` (shared lib + webapp copy) takes optional
`dropZoneProps` / `dropOverlay` and renders the drop zone + hover
overlay on its panel
- Image / Video / Audio boxes + webapp `PromptBox` pass their existing
drop wiring through - one overlay element now serves both the inline box
and focus mode
- No new drop logic anywhere: same MIME routing, accept/reject toasts,
keyframe handling, and paste behavior as dropping on the inline box
On iOS, adding more than 2 image prompts can blow past the browser's
memory limit and force-reload the page, wiping your prompts. We were
storing every uploaded image as a full-res base64 string just to show a
56px thumbnail.
- Thumbnails are now small ~512px previews instead of full-res data URLs
(memory per image goes from ~100MB+ to under 1MB)
- Tapping a thumbnail still shows the original full-res image
- Nothing changes for the backend — uploads and generation still use the
original file + media token
- Fixed the flicker when an upload finishes: the spinner card now turns
into the image card in place, instead of a new card popping in next to
it
- Small cleanups along the way: leaked object URLs in the old prompt
row, and upload spinners that never actually spun
Applies to webapp (desktop + mobile), website, and the desktop app since
they share the upload code.
- Ball sizes now vary organically: smooth spatial value noise drifts size
clusters across the word, white noise roughens per ball; the neighbor cap
keeps every draw collision-safe
- Radial halo billboard tracks the cursor BEHIND the text plane, so balls
silhouette against the glow (additive in dark, soft accent wash in light)
- Cursor point light switched to physical inverse-square falloff (decay 2):
balls near the cursor are genuinely brighter; the light dims to 30% when
the pointer leaves so the resting state stays moody
- Dark mode is an eclipse rig: near-black lacquer balls (clearcoat physical
material), almost no front light, cool key + counter rim from behind, and
no environment map — scene.environment floods instanced materials at full
strength regardless of envMapIntensity, which was washing the whole word
to gray; light mode keeps the procedural room reflections
The hero brand mark is now the signature interactive moment: 'artcraft'
rasterized per-glyph (manual advances so no ligature shaping), sampled onto a
jittered close-packed lattice 2-3 layers deep, and rendered as instanced 3D
spheres under a perspective camera fitted so world units equal CSS pixels at
the text plane.
- Cursor is both a point light and a push force (radial + swirl, quadratic
falloff, toward-viewer pop); balls scatter, then rejoin on home springs
- Per-ball radii capped at half the nearest-neighbor gap, plus a 3D
spatial-hash collision pass, so spheres never clip through each other
- Mount plays an assemble: balls fly in from a scattered shell
- Idle: light wanders a slow Lissajous path; balls breathe subtly
- Fallbacks: static display-type wordmark for SSR/crawlers, reduced motion,
and WebGL failure; theme-aware colors via shared token watcher (extracted
to lib/theme-colors, now also used by the blocking/render comparator)
- Tagline 'Controllable AI for artists.' remains the crawlable h1
Standalone Next 16 app at frontend/apps/artcraft-website-next (excluded from
the npm workspace like artcraft-dashboard; React 19 + R3F 9 + GSAP + Lenis +
Tailwind 4). Branched from design/prototype0; the Marathon-style navbar is
ported to next/link with theme-aware tokens plus a light/dark toggle.
- Design system: hairline-grid 'instrument panel' language; light (blueprint
paper) + dark (viewport terminal) themes via CSS tokens, system-pref default
with persisted override applied pre-paint
- Landing: hero, capability ticker, feature grid (real product footage),
ownership, made-with (click-to-load YouTube), CTA, footer — all crawlable
SSR HTML; GSAP reveals are progressive enhancement only
- 3D hero: interactive blocking↔render comparator — wireframe R3F set clipped
over the product reel by a cursor-driven scan line; degrades to plain video
on reduced-motion/coarse-pointer/WebGL failure
- SEO: metadata + OG, sitemap.ts, robots.ts, SoftwareApplication JSON-LD