Skip to content

Commit 8029cf9

Browse files
cards demo: share a card as a link whose preview is the card (#1016)
## 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. - Share panel, in the stage under the card: Copy link, Share on X (opens the composer with the link), Save image, Save video (a five second spin, encoded in the browser with WebCodecs). - Style row: Template (the card on the Figma share template, with a dot grid in the middle column) or Hand (the card in one of five photographed hands; a Skin row picks). Backdrops: Light, Dark, Brand (the card's color, or the dominant color of its art), or a custom color. - Share page at `/c/{slug}`: a live 3D card the visitor can turn, with **Design yours** and **View in Playground** going to `docs.lightspark.com/cards/demo`. Crawlers get `og:image` pointing at the rendered still (2400 x 1256), and the title `{Brand} Card — Lightspark Cards Playground`. - Storage: Vercel Blob for the stills, video, and uploaded logo and art; Upstash Redis for the records. Both already connected to the `grid-cards-demo` project with per-environment prefixes (`cards/preview/...`, `cards/production/...`). Locally the store is the filesystem (`.shares/`, gitignored). - API routes under `/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. - A share stores only image URLs of its own (root-relative presets, or files in this deployment's Blob store); anything else is dropped. Assets are set only by the upload route. - `cards.lightspark.com/` redirects to `docs.lightspark.com/cards/demo` (the iframe's `?embed=true` load is exempt). - Card numbers, expiry, and CVV are generated on each issue (Luhn-valid Visa PAN) instead of hardcoded. - Docs embed snippet: the iframe passes `c` and `edit` through from the docs page URL, and `allow` gains `web-share`. Dot grid lattice math moved to `src/lib/dotLattice.ts`, shared by the stage backdrop and the share template. ## Infra state - `cards.lightspark.com` CNAME added by PE ([ops#4134](https://github.com/lightsparkdev/ops/pull/4134)), attached to the `grid-cards-demo` Vercel project, certificate issued. - Production env: `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). - Created a share on the preview: `POST /api/shares` and both file uploads returned 200, the files landed under `cards/preview/`, the crawler view of `/c/{slug}` carries the correct `og:*` and `twitter:*` tags, and the `og:image` on Blob is 2400 x 1256 WebP. - Share page loaded on desktop Chromium and mobile WebKit with no console errors; both buttons resolve to the docs URL with `?c={slug}`. - Bugbot ran on the branch earlier; findings addressed. Greptile's three findings (partial updates, late upload cap, external image URLs) fixed in 57c579c and verified locally with curl: 413 on a 9 MB declared length and on a streamed 5 MB body; external and data: image URLs nulled on create and patch; an update issues GET, uploads, then PATCH last. - Docs preview (`ramps-pat-cards-export-share.mintlify.site/cards/demo?c=…`) loads the shared design in the iframe (brand and color restored). - After merge: confirm the `/` redirect on `cards.lightspark.com`, create a share from `docs.lightspark.com/cards/demo`, and post it to X to see the card. --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b67647e commit 8029cf9

73 files changed

Lines changed: 7237 additions & 142 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎components/grid-cards-demo/.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ certs/
1212
# in src/apps/shared/icons/sfSymbolPaths.ts, not these SVGs.
1313
public/assets/symbols/
1414
.tmp-sf-symbols-*/
15+
# Local share store (dev): cards shared from localhost land here.
16+
.shares/

‎components/grid-cards-demo/README.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ The port is pinned to 4002 (4000 is the wallet demo) so the docs page can embed
3030
while you preview the docs with `make mint`. The docs page targets `localhost:4002` when served
3131
locally.
3232

33+
### Share
34+
35+
Share (under the card) parks the card in a frame on the stage, renders it to a still in the browser, and
36+
makes a link whose preview is that card; Save video renders a spin the visitor keeps. The Style
37+
row's Hand puts the real card in one of five photographed hands (a Skin row picks; see
38+
`scripts/hand-assets.py` to add one from a masked photo). Locally, shares are written to `.shares/` (gitignored) and served
39+
by the app itself: the link is `http://localhost:4002/c/{slug}`, and `?preview=unfurl` on it mocks
40+
the X, Slack, and iMessage previews. The team layer ("For a customer") is always unlocked in dev;
41+
in production it needs `SHARE_TEAM_KEY` and a visit to `/api/team?key=…`. Dev hook:
42+
`__cardExport.still('post', 'dark')` opens a still; `__cardExport.video('dark')` downloads the spin.
43+
Smoke test: with the dev server up, `node scripts/share-smoke.mjs [chromium|webkit]` drives every
44+
share flow and fails on console errors, 404s, a card off its slot, a hand swap with an empty frame,
45+
or an empty export. See the 2026-09-15 entry in `APPROACH.md`.
46+
3347
## Deploy
3448

3549
Deploy to Vercel as its own project. `vercel.json` sets an `ignoreCommand` so a commit only

‎components/grid-cards-demo/next.config.mjs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ const nextConfig = {
1919
path.resolve(__dirname, 'node_modules'),
2020
],
2121
},
22+
async redirects() {
23+
return [
24+
{
25+
// Someone typing the share domain itself lands in the docs, where the
26+
// playground lives; the docs' iframe loads `/` with `?embed=true`.
27+
source: '/',
28+
has: [{ type: 'host', value: 'cards.lightspark.com' }],
29+
missing: [{ type: 'query', key: 'embed' }],
30+
destination: 'https://docs.lightspark.com/cards/demo',
31+
permanent: false,
32+
},
33+
];
34+
},
2235
async headers() {
2336
return [
2437
{

0 commit comments

Comments
 (0)