Showcase packs
Who is this for
Section titled “Who is this for”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 insrc/content/packs.ts. Components are not touched. npm run demo-packscopies finished demo packs from a sibling checkout of the product repository.npm run sticker-gapclears enclosed white holes.npm run assetswrites 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.
Asset rules
Section titled “Asset rules”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.
How the pieces fit
Section titled “How the pieces fit”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 |
Pack folder names
Section titled “Pack folder names”| 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.
The scripts
Section titled “The scripts”npm run demo-packs
Section titled “npm run demo-packs”scripts/demo-packs.mjs brings finished demo packs in.
- 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. - Takes every sub-folder whose name matches
<man|woman><optional digits>_<outfit>, for exampleman2_u4. Folders such as_archiveand_portraitsdo not match and are skipped. - Skips a folder that does not hold exactly 12 files named
sticker_<number>.png(a pack still being made). - Copies each file to
public/stickers/_src/<name>/, renamingsticker_01.pngtosticker_1.png.
npm run demo-packs -- --list # only report what is therenpm run demo-packs # copy every complete packIt is read-only on the product side: files are copied, nothing there is run or written.
npm run sticker-gap
Section titled “npm run sticker-gap”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:
npm run sticker-gap -- --scan=<pack>- Open
shots/sticker-gap/scan-<pack>.png. Decide by eye which numbered regions are holes and which are eyes or highlights. - Add one entry per file to the
FIXESarray in the script:{ file: "<pack>/sticker_<n>.png", seeds: [[x, y], …] }. npm run sticker-gap, then check the_before_after.pngpictures.
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.
npm run assets
Section titled “npm run assets”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).
The entry in src/content/packs.ts
Section titled “The entry in src/content/packs.ts”src/content/packs.ts is everything the page knows about people, outfits and packs.
Constants, field by field
Section titled “Constants, field by field”| 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.
How RIBBON_ROWS is derived
Section titled “How RIBBON_ROWS is derived”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.
What packs.test.ts enforces
Section titled “What packs.test.ts enforces”| 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.
-
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.pngSticker order matters: index 0 to 11 maps to the 12 labels in
stickers.labelsof each dictionary, which are the images’ alt texts. -
Check the face rule. Fictional or the owner. Nothing else.
-
Find white holes.
Terminal window npm run sticker-gap -- --scan=<person>_<outfit>Add entries to
FIXES, then runnpm run sticker-gap. -
If the outfit is new to the page, put its preview at
public/outfits/_src/<outfit>.webp, add the id toOUTFITS, and addscene.kiosk.outfits.<outfit>to all three dictionaries. -
Build the files.
Terminal window npm run assetsExpect
✓ stickers/<person>_<outfit>: 12 × 2. -
Edit
packs.ts. Change theoutfitof that person’s line inSHOWCASE_PACKS. If the outfit changed, swap it into the right place inKIOSK_ROWSso the kiosk still shows each outfit once, on the person’s row. -
Update the share-image collage if the folder name changed. See below.
-
Remove the old folder
public/stickers/<old pack>/from git if nothing references it any more. -
Verify.
Terminal window npm testnpm run shots -- --only=360,1280npm run audit -
Commit the WebP files,
packs.ts, theFIXESchange, dictionaries andpeople.png. Do not commit_src.
Adding a person or renaming one
Section titled “Adding a person or renaming one”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 OG collage
Section titled “The OG collage”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
COLLAGEto 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.
Where the packs are used
Section titled “Where the packs are used”| 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 |
Where to go next
Section titled “Where to go next”- Landing quality tooling —
shotsandaudit, which you run after any asset change. - i18n and copy — adding the outfit and person labels to the dictionaries.
- Outfits, templates and styles — where outfit ids and the packs themselves come from in the product.
- Privacy — why real guests’ faces never leave the product.
- Known gaps — demo sources that live outside any repository.
Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.