Skip to content

Showcase packs

A developer who has a finished set of 12 stickers and needs it to appear on the landing page: in the showcase, the kiosk mock-up, the sticker ribbon or the share image.

  • Only two kinds of faces are allowed: the product owner’s own stickers and fictional demo people whose portraits were generated. Stickers of any real third person are never used.
  • A pack is one source folder, public/stickers/_src/<pack>/sticker_1.png … sticker_12.png, plus one entry in src/content/packs.ts. Components are not touched.
  • npm run demo-packs copies finished demo packs from a sibling checkout of the product repository. npm run sticker-gap clears enclosed white holes. npm run assets writes WebP at 192 and 384 px, outfit cards at 256 px and the share-image collage.
  • Source folders are git-ignored. The generated WebP files and the collage are committed.
  • The showcase is built for exactly six demo people and a 3 + 3 kiosk. Replacing a person is routine. Adding a seventh means changing tests, the audit and the layout.
  • Nothing in this pipeline generates an image or calls a paid service. It only copies, resizes and re-encodes files that already exist.

These come from the repository’s CLAUDE.md and are partly enforced by src/content/packs.test.ts.

Rule Enforced by
Faces on the page belong to the product owner or to fictional demo people only. Never a real third party: no event guests, no clients, no colleagues, no stock photos of real people convention — nothing can test whose face a file shows
The owner’s pack appears in the hero and nowhere else packs.test.ts; the audit checks the hero’s final pack
The showcase, the “Why” chat, the ribbon and the share image use demo people only packs.test.ts for showcase and ribbon; the audit for the showcase
The landing repository takes finished files from disk. It never runs product code, never reads the product’s .env, never calls an AI provider convention
Outfit ids are the product’s own catalogue ids (bot/catalog.py in the product repository) packs.test.ts ties outfits to kiosk cards

If you are unsure whether a face is allowed, it is not.

flowchart LR
    A["Product checkout: out/landing_demo"] -- "npm run demo-packs" --> B["public/stickers/_src/PACK"]
    B -- "npm run sticker-gap" --> B
    B -- "npm run assets" --> C["public/stickers/PACK/N-192.webp and N-384.webp"]
    B -- "npm run assets" --> D["src/assets/og/people.png"]
    E["public/outfits/_src"] -- "npm run assets" --> F["public/outfits/OUTFIT.webp"]
    C --> G["src/content/packs.ts"]
    F --> G
    G --> H["Hero, showcase, kiosk, ribbon, chat, burst"]
Location In git Content
public/stickers/_src/<pack>/sticker_1..12.png no source PNGs with alpha
public/outfits/_src/<outfit>.webp (also .png, .jpg) no outfit preview sources
public/stickers/<pack>/<n>-192.webp, <n>-384.webp yes what the page loads
public/outfits/<outfit>.webp yes kiosk card pictures
src/assets/og/people.png yes collage for the share image
Whose pack Folder name Example
The owner the outfit id u1
A demo person <person>_<outfit> man_m1, woman3_u2

packDir() in packs.ts builds the same names, so the folder and the config entry must agree.

scripts/demo-packs.mjs brings finished demo packs in.

  1. Looks for ../ai-sticker-pack/out/landing_demo — a checkout of the product repository next to the landing checkout. If the folder is missing it prints a message and exits with code 0.
  2. Takes every sub-folder whose name matches <man|woman><optional digits>_<outfit>, for example man2_u4. Folders such as _archive and _portraits do not match and are skipped.
  3. Skips a folder that does not hold exactly 12 files named sticker_<number>.png (a pack still being made).
  4. Copies each file to public/stickers/_src/<name>/, renaming sticker_01.png to sticker_1.png.
Terminal window
npm run demo-packs -- --list # only report what is there
npm run demo-packs # copy every complete pack

It is read-only on the product side: files are copied, nothing there is run or written.

The product’s die-cut fills any hole that is closed on every side with white — for example the inside of an “OK” ring, or the gap between a raised hand and the face. On a white chat background nobody sees it. On the landing page’s graphite it is a white blob.

scripts/sticker-gap.mjs makes named holes transparent on the copy in _src. The original in the product checkout is never touched, and the outer white die-cut edge is left alone.

Which hole to clear is named by hand with a seed pixel, not guessed: the whites of the eyes are enclosed white regions too.

Command What it does
npm run sticker-gap -- --scan=<pack>,<pack> For every sticker of those packs, tints and numbers each enclosed white region of at least 300 px on graphite and prints its seed. Writes shots/sticker-gap/scan-<pack>.png
npm run sticker-gap -- --list Prints every enclosed white region of the files already named in the script’s FIXES list
npm run sticker-gap Clears the regions named in FIXES, overwrites the file in _src, and writes before/after pictures to shots/sticker-gap/

Procedure for a new pack:

  1. npm run sticker-gap -- --scan=<pack>
  2. Open shots/sticker-gap/scan-<pack>.png. Decide by eye which numbered regions are holes and which are eyes or highlights.
  3. Add one entry per file to the FIXES array in the script: { file: "<pack>/sticker_<n>.png", seeds: [[x, y], …] }.
  4. npm run sticker-gap, then check the _before_after.png pictures.

Gaps that are open to the background along the outline, such as spaces between curls, are background already and are not touched. A limb cut off by the frame edge cannot be fixed this way; that pack has to be regenerated in the product.

scripts/assets.mjs turns sources into what the page loads. It processes every folder present in _src, and only those.

Input Output Processing
public/stickers/_src/<pack>/sticker_<n>.png public/stickers/<pack>/<n>-192.webp and <n>-384.webp resized to a square with fit: contain on a transparent background; WebP quality 82, alpha quality 95, effort 6
public/outfits/_src/<id>.webp, .png, .jpg or .jpeg public/outfits/<id>.webp 256 × 256 with fit: cover; WebP quality 80, effort 6
six named source stickers src/assets/og/people.png see “The OG collage” below

A folder with other than 12 stickers produces a warning, not an error. The test catches the missing files later.

Sizes are fixed by the type StickerSize = 192 | 384 in packs.ts. next/image then serves AVIF or WebP at the width each placement needs (see Landing quality tooling).

src/content/packs.ts is everything the page knows about people, outfits and packs.

Export Shape Meaning
OUTFITS ["m1", "u4", "u2", "u1", "u3", "u5"] the product’s outfit catalogue. Each id needs a preview card and a label in every dictionary
KIOSK_ROWS two objects { row, outfits } the kiosk mock-up, top row first. row is "men" or "women"; outfits is the three outfit ids on that row, left to right
PEOPLE ["owner", "man", "man2", "man3", "woman", "woman2", "woman3"] every person id
PERSON_ROW Record<PersonId, KioskRow> which kiosk row each person’s outfit sits on
HERO_PACK { person: "owner", outfit: "u1" } the pack the hero assembles, and the outfit the scene’s guest picks
SHOWCASE_PACKS six objects { person, outfit } the showcase, in the order of its face switch
PACKS [HERO_PACK, ...SHOWCASE_PACKS] every pack the page loads; this is what the file-existence test walks
STICKERS_PER_PACK 12
AVATAR_STICKER 1 (0-based: the second sticker, the wave) the sticker each round face is cut from
RIBBON_PER_ROW 16 stickers per ribbon row
RIBBON_ROWS two generated rows the sticker ribbon; computed, not written by hand

A SHOWCASE_PACKS entry has two fields:

Field Type Rule
person a DemoPerson — any PersonId except "owner" unique in the list
outfit an OutfitId must be on the kiosk row that PERSON_ROW gives this person; no other showcase person on that row may wear it

A KIOSK_ROWS entry:

Field Type Rule
row "men" or "women" rows are in the order men, women
outfits three OutfitId values no outfit on the kiosk twice; all six outfits used

Each kiosk card stands for exactly one person: packForCard(row, outfit) finds the showcase pack with that outfit whose person belongs to that row. Clicking the card in the mock-up switches the showcase to that person.

You do not edit the ribbon. ribbonRow(offset) takes the six showcase people in alternating order (man, woman, man, …) and gives each a different sticker per round, stepping by 7 — coprime with 12, so nobody’s sticker repeats before all have appeared. Two rows are built with offsets 1 and 8.

One sticker is excluded by key, woman_u5:9 (sticker number 10 of that pack): its hand is cut by the frame, which is fine in a pack and odd alone. If you replace that pack, update or remove the exclusion in ribbonRow and the matching assertion in the test.

The form’s success burst reuses the first ten stickers of ribbon row 1, so they come from the browser cache.

Test Fails when
every pack has its twelve stickers on disk, in both sizes any public/stickers/<pack>/<n>-192.webp or -384.webp is missing for a pack in PACKS
every outfit has a preview card public/outfits/<id>.webp is missing for an id in OUTFITS
the kiosk is exactly 3 + 3 rows are not men, women; a row does not have three cards; an outfit appears twice; the kiosk does not use exactly the ids in OUTFITS
six demo people, one outfit each SHOWCASE_PACKS is not six long; a person has two packs; the owner is in the showcase; a kiosk card has no person behind it; a person is on the wrong row
the hero assembles the owner’s pack HERO_PACK.person is not owner; its card is not men / u1; the owner appears in more than one pack
the sticker ribbon a row is not 16 long; the owner is in it; one of the six people is missing; a sticker is twice in one row; woman_u5:9 is present

npm run audit adds live checks: six faces with accessible names, the owner’s pack absent, keyboard and mouse switching, the pack box keeping its height, kiosk rows men:3,women:3.

Step by step: replace a demo person’s pack

Section titled “Step by step: replace a demo person’s pack”

This is the supported change: same person slot, new stickers or a new outfit.

  1. Get the sources. Either generate the pack in the product and run npm run demo-packs, or place 12 PNGs by hand:

    public/stickers/_src/<person>_<outfit>/sticker_1.png
    …
    public/stickers/_src/<person>_<outfit>/sticker_12.png

    Sticker order matters: index 0 to 11 maps to the 12 labels in stickers.labels of each dictionary, which are the images’ alt texts.

  2. Check the face rule. Fictional or the owner. Nothing else.

  3. Find white holes.

    Terminal window
    npm run sticker-gap -- --scan=<person>_<outfit>

    Add entries to FIXES, then run npm run sticker-gap.

  4. If the outfit is new to the page, put its preview at public/outfits/_src/<outfit>.webp, add the id to OUTFITS, and add scene.kiosk.outfits.<outfit> to all three dictionaries.

  5. Build the files.

    Terminal window
    npm run assets

    Expect ✓ stickers/<person>_<outfit>: 12 × 2.

  6. Edit packs.ts. Change the outfit of that person’s line in SHOWCASE_PACKS. If the outfit changed, swap it into the right place in KIOSK_ROWS so the kiosk still shows each outfit once, on the person’s row.

  7. Update the share-image collage if the folder name changed. See below.

  8. Remove the old folder public/stickers/<old pack>/ from git if nothing references it any more.

  9. Verify.

    Terminal window
    npm test
    npm run shots -- --only=360,1280
    npm run audit
  10. Commit the WebP files, packs.ts, the FIXES change, dictionaries and people.png. Do not commit _src.

Adding a seventh person is not a one-line change. These places assume six people with these exact ids:

Place Assumption
packs.test.ts exactly six showcase packs; the kiosk is exactly 3 + 3 and each card is one person
scripts/audit.mjs six faces; the last face opens woman3_u2; the second opens man2_u4; cards men-u1, women-u3, men-m1 open man3_u1, woman2_u3, man_m1
scripts/assets.mjs the collage’s hard-coded list of six pack folders
WhyChat.tsx the chat’s four faces are man, man3, woman2, woman3
Dictionaries showcase.people has one description per demo person id, in three languages
Layout six round faces in one row above the pack (60 px, 48 px on a phone)

To add a new person id you would extend PEOPLE, PERSON_ROW and showcase.people, and the folder name must match the pattern demo-packs accepts. Because every kiosk card must map to exactly one person and there are six outfits, a seventh person has no card unless the kiosk design changes. Discuss the design first.

Renaming or reordering people breaks the audit’s hard-coded expectations listed above; update them in the same commit.

The share image (Open Graph and Twitter card, 1200 × 630, one per language) shows the H1 next to six people, one sticker each. next/og cannot draw WebP, so npm run assets builds a PNG.

Property Value
Output src/assets/og/people.png, committed
Layout 3 × 2 cells of 200 px, each sticker tilted by a few degrees, transparent background
Canvas 616 × 416 px
Source list the COLLAGE array in scripts/assets.mjs: [pack folder, sticker number, tilt] for each of six demo people
Built when all six source files exist in _src; otherwise the step is silently skipped and the committed PNG stays

Consequences:

  • With only your new pack in _src, the collage is not rebuilt. To change it you need the sources of all six packs.
  • If you renamed a pack folder, edit COLLAGE to match.
  • The same PNG is used as the link preview picture inside the “Why” chat mock-up.

Check the result at http://localhost:3000/ru/opengraph-image.

Place Which packs Notes
Hero scene HERO_PACK only 12 stickers, eager with low fetch priority
Showcase SHOWCASE_PACKS switched by six faces; the next pack is prefetched on hover or focus
Kiosk mock-up beside the showcase KIOSK_ROWS with OUTFITS cards cards are buttons outside the tab order; keyboard users use the faces
Sticker ribbon RIBBON_ROWS 96 px, mounted on approach
“Why” chat four demo people’s avatar stickers
Success burst first ten of ribbon row 1
Share image people.png

Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.