cards demo: share a card as a link whose preview is the card - #1016
Conversation
Share (under the floating card, and beside Reset on the Design divider) opens a sheet with the picture the link will show, a format (Post, Square, Card only), and a backdrop. Copy link makes the share: the record first, then the stills, then a 5 s spin video at 1080p60 in the background. - CardExporter renders the stage's own mesh and studio through an export camera into offscreen targets, borrowing the WebGL context between frames; OutputPass tone maps; HoloDove exposes its view update. - Spin video: fixed-step frames, WebCodecs H.264, mp4-muxer. - ShareStore interface with a local folder store (.shares/, gitignored); route handlers under /api/shares; team gate for named (pitch) shares, edit tokens, view counts. - Share page at /c/[slug] with server-rendered og/twitter meta, og:video for iMessage, Open in the playground; ?preview=unfurl mocks X, Slack, and iMessage. - ?c= deep link loads a shared design; the docs embed forwards c and edit and allows web-share; painters load images with CORS. Phase 1 of the plan: local only, no Vercel store, no website rewrite yet. Co-authored-by: Cursor <cursoragent@cursor.com>
…ialog The still and the video now sit on the og-image template (light, dark, and a Brand surface from the card's color or its art). The sheet is Origin's Dialog with the square preview, the sidebar's rows for Backdrop and Link, and Origin buttons: Copy link, Post to X, Download image, Download video. Header, close control, format tabs, the customer option, and the idle status line are gone. Co-authored-by: Cursor <cursoragent@cursor.com>
… tile actions Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Inside a dialog scaling in, the screen rects are all smaller alike; the ring was placed from them and landed short. Divide by the row's scale and read the row once more after the entrance. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This reverts commit bb2dc03.
Co-authored-by: Cursor <cursoragent@cursor.com>
…er is Reset alone again Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The dialog goes. Share brings a panel up in the middle of the stage: the share template in DOM with an empty slot, which the one card flies into and is posed in; the Backdrop and Pose rows and the tiles stagger in under it as the panel grows. The card is turned by hand right there (CardMotion.free: a release stops at any angle) or from the Pose row (setPose). Stills render from the live pose. The Share button reads Close while the panel is up; closing flies the card back. Co-authored-by: Cursor <cursoragent@cursor.com>
The controls were mounting one after another under Motion layout animations, each reflowing the panel and re-centering it: a jump per arrival. Now the panel is anchored where its grown height will sit, one height animation opens the room downward, and the always-mounted rows and tiles rise in inside the clip on their own delays. The stage button morphs its label (torph) and turns its glyph from the share arrow to the cross. Co-authored-by: Cursor <cursoragent@cursor.com>
… is done Co-authored-by: Cursor <cursoragent@cursor.com>
…wn work The output target was an sRGB framebuffer, so the pass's sRGB values were encoded again on write and every export came out lifted (a black card gray). Plain bytes now; the export matches the stage pixel for pixel. The progress bar and status lines go: a working tile turns its glyph into a spinner and morphs its label (Making link…, Rendering…); only errors are written out. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…g with it Co-authored-by: Cursor <cursoragent@cursor.com>
…; Share button after the intro Save video renders on its own (the link, if any, gets the same file attached later, or on Copy link without a second render). Save image and Save video morph to Saved with a check for a moment. A tile's glyph changes through a blur and a scale rather than a cut. Cancel aborts a render and forgets the ask. The rows and tiles come down into place. The stage's Share button arrives out of a blur once the intro has played. Co-authored-by: Cursor <cursoragent@cursor.com>
…no hover tilt in the frame Co-authored-by: Cursor <cursoragent@cursor.com>
…the Share button and sinks back Motion read 'auto' once at the start of the growth and the content settled a pixel or two after (fonts, the swatch ring), so the end of the animation was a jump. The target is the content's layout height now, kept current by a ResizeObserver. The panel enters scaled about its bottom edge and risen 36px, and leaves the same way, toward the button. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
|
Mintlify's snippet processing fails to parse the export when the block carries // comments (Could not parse expression with acorn), and the component then renders nothing. The passthrough is written plainly, with no comments in the file. Co-authored-by: Cursor <cursoragent@cursor.com>
…ped as they stream, updates publish last
- cleanDesign nulls a logo or art source that is not a root-relative path
or a file in this deployment's Blob store (BLOB_STORE_ID), so a share
cannot point visitors' browsers at another server.
- PATCH /api/shares/{id} no longer accepts assets; only the upload route
sets them. clearAsset, which used that, is gone.
- The upload route refuses a declared length over the role's cap before
reading, and cuts a stream off as it crosses it. JSON bodies are capped
at 64 KB.
- The video role is removed: nothing uploads a video (attachVideo was
unused), and 40 MB could not pass a route handler anyway.
- Updating a share uploads the files and stills first and patches the
record last, so a failure part way leaves the public link as it was.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
@greptileai review the latest commits: the three findings are addressed in 57c579c (owned-only image URLs, streamed upload cap, record patched last on update) and the docs embed snippet changed in fabeedc. |
…ther The upload route only stores a file and returns its URL. The client uploads the brand images and both stills, then publishes the design and every URL in a single PATCH (URLs checked to be ours). A failure part way leaves the public record as it was. The Vercel store deletes replaced files when the record moves off them, not at upload time. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@greptileai the update is now atomic (7a7f365): uploads only store files, and a single PATCH publishes the design and all asset URLs together. Replaced Blob files are deleted when the record moves off them. |
…s are conditional writes
- ownedFileUrl binds a URL to the share being patched: the local store's
/api/shares/{id}/files/ or the Blob store's cards/{env}/{id}/{role}-…;
a share being created (no files yet) may only reference presets. So a
share cannot adopt another share's file and have the store delete it.
- The client re-uploads a stored image it does not own (a design opened
from someone's link and shared afresh) instead of referencing it.
- VercelStore.update writes through a Lua script conditional on the
record's version (cards:{env}:ver:{id}) and retries a lost race with
backoff, so two tabs publishing at once cannot leave the record pointing
at a file the other transition deleted. Deletion is limited to files
under the share's own Blob prefix.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
@greptileai 4132f8b: asset and design URLs are now bound to the authenticated share's own path (another share's file is refused with 400), deletion only touches files under the share's own prefix, and the record write is a version-conditional Lua script with retry, so concurrent publishes serialize. Verified locally: 20 concurrent PATCHes leave a consistent record. |
…the conditional write Co-authored-by: Cursor <cursoragent@cursor.com>
|
@greptileai a823473: the record and its version are now read in a single MGET, so the version checked by the Lua write is the one the record was read at. |
|
Confirmed. |
|
@greptileai review |
1 similar comment
|
@greptileai review |
…on dark systems (#1017) ## What changed Three fixes for the cards share page, found after #1016 went live. - The card rendered nearly black on iOS. The studio environment maps were `Float32` `DataTexture`s, and iOS Safari has no `OES_texture_float_linear`, so the PMREM could not filter them and sampled black; the card was lit by the one directional lamp alone. The maps are now uploaded as half floats (`DataUtils.toHalfFloat`), which every WebGL2 implementation filters. The desktop picture is unchanged. - Grey bands above and below the page. Mobile Safari colors its status bar and tab bar from the document background, which was the app body's surface (`#1a1a1a` in dark) rather than the page's (`#111111`). The share page's `html` and `body` now take its surface, and the page exports its own `themeColor` (the root layout's `theme-color` metas moved to a `viewport` export so the page-level one replaces them). - White flash on a dark system (desktop). The page's surface and ink were inline styles from a React state that started light and flipped in an effect after hydration. They are now CSS custom properties keyed on `html[data-theme]`, which the root layout's boot script sets before the first paint, so the server's HTML paints in the right colors. The GL reads the theme through `useSyncExternalStore` (client snapshot during hydration, re-rendered before paint), and follows a theme switch. ## Test plan - iOS 27 Simulator (iPhone 18 Pro), Safari, against the local dev server: the green Buchi card is lit and legible in dark mode; status and tab bar areas sample as the page color in both light (`#f8f8f7`) and dark (`#111111`). - Desktop Chromium: the playground stage in light and dark looks as before the half-float change. - CDP screencast of the page load in headless Chromium with `colorScheme: dark`: production shows a light frame before hydration; the branch shows only dark frames. SSR HTML carries no inline surface style. - `curl` of `/` and `/c/{slug}` shows the app's and the page's `theme-color` tags respectively, no duplicates. --------- Co-authored-by: Cursor <cursoragent@cursor.com>
What changed
The cards playground gets a Share panel. The card parks in a frame on the stage, renders to a still in the browser, and gets a link at
cards.lightspark.com/c/{slug}whose preview is that card./c/{slug}: a live 3D card the visitor can turn, with Design yours and View in Playground going todocs.lightspark.com/cards/demo. Crawlers getog:imagepointing at the rendered still (2400 x 1256), and the title{Brand} Card — Lightspark Cards Playground.grid-cards-demoproject with per-environment prefixes (cards/preview/...,cards/production/...). Locally the store is the filesystem (.shares/, gitignored)./api/shares: create, get, patch (with an edit token), file upload, view count, slug check. Rate limits per IP: 40 creates and 400 uploads per hour. Uploads are capped at 4 MB per file, enforced while the body streams; JSON bodies at 64 KB. Uploaded rasters are downscaled to 2048 px and re-encoded as WebP on the client before upload.cards.lightspark.com/redirects todocs.lightspark.com/cards/demo(the iframe's?embed=trueload is exempt).candeditthrough from the docs page URL, andallowgainsweb-share.Dot grid lattice math moved to
src/lib/dotLattice.ts, shared by the stage backdrop and the share template.Infra state
cards.lightspark.comCNAME added by PE (ops#4134), attached to thegrid-cards-demoVercel project, certificate issued.NEXT_PUBLIC_SHARE_ORIGIN=https://cards.lightspark.com/c. Preview deployments link to their own host and to the matching Mintlify preview.Test plan
node scripts/share-smoke.mjs chromium|webkit <url>against the branch's Vercel preview: both pass (template and hand alignment, five hand swaps, backdrops, custom color drag, poses, Save image download, Save video start and cancel, Copy link creating a share, a flow dismissing the panel).POST /api/sharesand both file uploads returned 200, the files landed undercards/preview/, the crawler view of/c/{slug}carries the correctog:*andtwitter:*tags, and theog:imageon Blob is 2400 x 1256 WebP.?c={slug}.ramps-pat-cards-export-share.mintlify.site/cards/demo?c=…) loads the shared design in the iframe (brand and color restored)./redirect oncards.lightspark.com, create a share fromdocs.lightspark.com/cards/demo, and post it to X to see the card.