Designing with an AI agent
How a coding agent (Claude Code, Codex, Cursor and others) makes real designs in PopCraft from a terminal: write the design as a script, build it, check it, render it, look, fix, and hand it over.
Designing with an AI agent
An AI coding agent working in a terminal can make PopCraft designs as good as the ones in the template gallery, because it works the same way the gallery's templates were made: it writes the whole design as code, builds it, checks it, renders it, looks at the pictures, and fixes what it sees, round after round. This page is the guide to give such an agent.
It needs the popcraft command line and nothing else: no access to PopCraft's source. The
packages page says what each npm package is for. The designs
end up as ordinary .popcraft files or cloud files, fully editable by a person afterwards.
npm i -g @popcraft/cli puppeteer # puppeteer renders the pictures
npm i @popcraft/kit # the design kit's types, for writing scripts in your editor
popcraft new launch.popcraft --name "Launch"getpopcraft/examples has working projects to start from: a launch in three sizes from one script, a design check for CI, translated copies of a design.
The loop
The whole method is one loop. An agent that skips the looking step makes flat, half-empty designs; one that keeps it makes good ones.
-
Write the design as a script (
reel.js): plain JavaScript using the template kit, described below. -
Build it:
popcraft run launch.popcraft write_design --size ig-story --name "Launch reel" --script @reel.js \\ --look '{"at":[0,800,6000,13900],"kits":true}'The design lands in the file as layers, with every size the script asked for. The result lists the frames it made and checks: text that fails contrast against what is behind it, text too small for the size, text off the frame, text where Instagram, TikTok and Shorts draw their own buttons, and text overlapping text. A script error comes back with its line number; nothing is written until the script builds.
-
Render it and look (the full review is under "Reviewing your own work" below):
popcraft run launch.popcraft vision.lookAt --ids <frame>,<frame> --out look/ popcraft run launch.popcraft vision.lookAt --ids <frame> --at 0,2000,8000,13900 --out look/The first renders every size at rest;
--atrenders a moving design at those times (ms), so the opening, the big moment and the last frame can all be seen. Open the PNGs and judge them as an art director would (see "Judging a render" below). -
Fix the script and build again in place:
popcraft run launch.popcraft write_design --frame <frame> --script @reel.js--framerebuilds that design where it stands. Repeat 3 and 4 at least three times per piece. -
Hand it over:
popcraft push launch.popcraftmakes it a cloud file (it prints the key), orpopcraft open launch.popcraftopens it in the desktop app. Exports:export.mp4,export.gif,export.nodes.
With the desktop app running, popcraft run --ui … does all of this in the open file instead, so a person can watch
the design appear.
Editing what is there: run_script
For changes to an existing file (rename every layer, recolour a brand, lay out forty cards, audit text sizes), the agent writes JavaScript that reads the document and calls tools:
popcraft run launch.popcraft run_script --code @fix.js// fix.js — `doc` is the document as data; `tools.<name>(params)` calls any tool; print() reports; return a value.
const page = doc.nodes[doc.pageId]
const small = Object.values(doc.nodes).filter(n => n.type === 'TEXT' && n.fontSize < 16)
tools.update_nodes({ updates: small.map(t => ({ id: t.id, props: { fontSize: 16 } })) })
print('raised', small.length, 'texts')
return small.map(t => t.name)Every change the script makes is one undo step, and a script that fails part-way changes nothing.
popcraft commands lists every tool and popcraft commands <tool> its parameters.
Writing a design script
The full reference — every part of the kit by topic, editing existing designs, how the decisions are made, and six worked examples that build with no check failing — is the design reference. The summary below is enough to start.
A design script is the code PopCraft's official templates are written in. It runs in a sandbox with these names:
| Name | What it is |
|---|---|
k | The kit, building into k.sheet (the frame the design starts as) |
W, H, NAME | The frame's size and the design's name |
ease | Curves: ease.in('cubic'), ease.out('cubic'), ease.inOut('quint'), ease.back(1.7), ease.spring(170, 26), ease.linear, ease.hold |
hex, preset | A colour from hex; a size by its preset id |
beat | Music: beat.bed(track, beats) gives { beat(n), duration, audio } for a shipped track |
draw, marks | Vector paths (box, line, poly, circle, smooth; grids, dots, confetti, bursts, arrows, charts) |
captions, overlays, texture, pointer, windows, devices, type, motion | The helper modules the templates share |
social, video, site, effects | Post and feed variants; reel sheets, cuts and camera moves; page type scales, columns, nav and footers; shader backdrops |
Plain JavaScript runs too: loops, arrays, Math, functions of your own. What the kit hands back (ids, paints) is a
reference: pass it on to the kit, but do the arithmetic on your own numbers (W, H, your layout constants).
The kit in short:
- Colours and type, first:
k.brand({ paper: '#F4F1EA', ink: '#141414', hot: '#E8361E' }, { name: 'Acme', site: 'acme.example' })makes brand colour roles (a person's brand kit re-skins them later); every colour after that is a role name.k.textStyle('Hook', { size: 132, fontWeight: 900, lineHeight: 120, letterSpacing: -4 })makes a named text style. A font file the person gave you goes in the file first:k.font('Brand', 'media://<hash>', { weight: 700 }), thenfontFamily: 'Brand'in a style. Outside a design script, theadd_fonttool does the same (list_fontsshows the file's fonts and any missing ones). - Layout:
k.flowSheet(k.sheet, gap, { top, right, bottom, left }, 'paper')makes the frame itself a vertical stack with those margins.k.stack(parent, { direction, gap, padding, width: 'FILL', fill, radius, stroke, name })nests stacks.k.frame(parent, { x, y, width, height })is a free frame for placed art. - Layers:
k.text(parent, words, { style, color, width: 'FILL', name }),k.rect,k.ellipse,k.star,k.vector(parent, w, h, paths, fill),k.photo(a slot a person fills),k.logo,k.patch(id, { … })for any property,k.component(name, { props }, build)andk.instance(parent, name, props)for anything repeated. - Motion:
k.timeline(k.sheet, { duration, fps: 30, audio }), thenk.cues(k.sheet, [{ layer, preset, at }])with presets such as rise-in, pop-in, wipe-in, words-in, letters-in, typewriter, pulse, shake, push-in, drift, orbit, camera-shake;k.countsfor numbers that count,k.swapsfor one text that says several things in turn (captions),k.animate(id, path, keys)for keys (opacity, translateX, translateY, scaleX, rotation,trim.endto draw a line on),k.behavefor idle motion. - Sizes:
k.variants(k.sheet, [{ preset: 'ig-post', adjust: (kit, id) => { … } }])re-lays the design out at other sizes, motion included;adjustrecomposes each one (kit.named(id, 'Hook')[0]finds a layer in that copy).
A complete script, a 14-second sale reel cut to music in three sizes, each recomposed for its shape. It builds with no check failing:
// A 14 s sale reel on the Countdown track, cut on its beats. Idea: a till receipt that will not stop printing.
const music = beat.bed('tension', 28)
const b = music.beat
k.brand({ paper: '#F4F1EA', ink: '#141414', hot: '#E8361E', muted: '#6B6760' }, { name: 'Tillwise', site: 'tillwise.example' })
k.textStyle('Hook', { size: 132, fontWeight: 900, lineHeight: 120, letterSpacing: -4 })
k.textStyle('Price', { size: 220, fontWeight: 900, lineHeight: 236, letterSpacing: -8, fontFeatures: { tnum: 1 } })
k.textStyle('Line', { size: 34, fontWeight: 500, lineHeight: 44, fontFamily: 'JetBrains Mono' })
k.textStyle('Caption', { size: 44, fontWeight: 700, lineHeight: 52 })
k.textStyle('Hook small', { size: 96, fontWeight: 900, lineHeight: 90, letterSpacing: -3 })
k.textStyle('Hook square', { size: 72, fontWeight: 900, lineHeight: 70, letterSpacing: -2 })
k.textStyle('Price small', { size: 120, fontWeight: 900, lineHeight: 128, letterSpacing: -5, fontFeatures: { tnum: 1 } })
const sheet = k.sheet
// The frame itself lays out its content: a vertical stack with margins clear of the apps' buttons (the safe area).
k.flowSheet(sheet, 36, { top: 270, right: 150, bottom: 500, left: 80 }, 'paper')
k.timeline(sheet, { duration: music.duration, fps: 30, audio: music.audio })
const col = sheet
k.text(col, 'This receipt won\u2019t stop printing.', { style: 'Hook', color: 'ink', width: 'FILL', name: 'Hook' })
const slip = k.stack(col, { gap: 18, padding: 48, width: 'FILL', fill: 'paper', radius: 6, stroke: { color: 'ink', width: 3, dash: [10, 8] }, name: 'Receipt' })
for (const [item, was] of [['Counter plan', '$228.00'], ['Shop plan', '$588.00'], ['Chain plan', '$1,428.00']]) {
const row = k.stack(slip, { direction: 'HORIZONTAL', justify: 'SPACE_BETWEEN', width: 'FILL', name: 'Item' })
k.text(row, item, { style: 'Line', color: 'ink', autoWidth: true })
k.text(row, was, { style: 'Line', color: 'muted', autoWidth: true, name: 'Was' })
}
k.text(slip, '$235', { style: 'Price', color: 'hot', width: 'FILL', name: 'Total' })
k.text(col, 'Cyber Monday: 60% off.', { style: 'Caption', color: 'ink', width: 'FILL', name: 'Caption' })
// The till the receipt prints from fills the foot of the frame (no words below the apps' safe line, so it is all drawing).
k.rect(sheet, W - 160, 460, 'ink', { radius: 48, name: 'Printer', absolute: { x: 80, y: H - 360 } })
k.rect(sheet, W - 300, 18, 'muted', { radius: 9, name: 'Paper slot', absolute: { x: 150, y: H - 300 } })
k.rect(sheet, W - 360, 120, 'paper', { name: 'Paper tongue', stroke: { color: 'ink', width: 3, dash: [10, 8] }, absolute: { x: 180, y: H - 420 } })
k.ellipse(sheet, 36, 'hot', { name: 'Power light', absolute: { x: W - 190, y: H - 220 } })
k.cues(sheet, [
{ layer: 'Printer', preset: 'rise-in', at: 0 },
{ layer: 'Paper tongue', preset: 'slide-in', at: b(1), params: { direction: 'down' } },
{ layer: 'Hook', preset: 'words-in', at: 0 },
{ layer: 'Receipt', preset: 'wipe-in', at: b(2), params: { direction: 'down' } },
{ layer: 'Item', preset: 'rise-in', at: b(3), stagger: { delay: b(1) } },
{ layer: 'Total', preset: 'pop-in', at: b(8) },
{ layer: 'Total', preset: 'pulse', at: b(20) },
])
k.counts(sheet, [{ layer: 'Total', at: b(8), duration: b(4), counter: { from: 588, to: 235, prefix: '$' } }])
k.swaps(sheet, [{ layer: 'Caption', words: ['Cyber Monday: 60% off.', 'Every plan reprices.', 'Ends at midnight.'], at: b(10), hold: b(4), stagger: 30, in: 300, out: 240, unit: 'word', rise: 24 }])
// Each size is recomposed, not just resized: tighter margins and smaller display type where the frame is shorter.
const recompose = (top, bottom, hook, price, gap, h) => (kit, id) => {
kit.patch(id, { padding: { top, right: 72, bottom, left: 72 }, itemSpacing: gap })
// The till keeps to the foot of each size: placed from that size's height, not stretched with the frame.
kit.patch(kit.named(id, 'Printer')[0], { y: h - 210 })
kit.patch(kit.named(id, 'Paper slot')[0], { y: h - 150 })
kit.patch(kit.named(id, 'Paper tongue')[0], { y: h - 250, height: 100 })
kit.patch(kit.named(id, 'Power light')[0], { y: h - 90 })
kit.patch(kit.named(id, 'Receipt')[0], { padding: { top: 28, right: 32, bottom: 28, left: 32 }, itemSpacing: 8 })
kit.restyle(kit.named(id, 'Hook')[0], hook)
kit.restyle(kit.named(id, 'Total')[0], price)
}
k.variants(sheet, [{ preset: 'ig-post', adjust: recompose(56, 230, 'Hook square', 'Price small', 20, 1080) }, { preset: 'ig-portrait', adjust: recompose(96, 260, 'Hook small', 'Price', 36, 1350) }])Sizes by preset id: ig-story 1080×1920, ig-post 1080×1080, ig-portrait 1080×1350, video-1080p 1920×1080,
web-desktop 1440 wide, og-image 1200×630, email-header 600×300, ad-landscape 1200×628, ad-square 1200×1200;
or --size '{"width":1600,"height":900}'. Tracks for beat.bed: launch (120 BPM), tension (120, its drop at 2.0 s),
trailer (100), festive (108), spooky (104), acoustic (96), calm (72), noirJazz (90), bigBand (140), chiptune (150),
organ (66), psychRock (110), edm, synthwave, lofi, hiphop, house, indie.
Judging a render
The checks catch what can be measured. These cannot be, and are what separates a good design from a correct one:
- One idea, at once. Before any word is read, does the picture say what it is? Find it by listing five or six candidate worlds and rejecting the clichés; keep the real material nobody uses for this job.
- The information lives on an object. Put the list where that world would write it down (tour dates as tape on a road case, postmarks on a letter, bars of a score), and make every state an action on it: sold out is stamped or sprayed, a new date is slapped on or pencilled in. Then the motion follows from the actions.
- A motion character and a voice. Slams and shakes, or placed with soft thuds, or legato fades on the beat; copy in box-office capitals, or the artist's first person, or a programme's formality. Facts stay plain in any voice.
- Space arithmetic first. Add up the heights of title, list, caption and call to action against the room (a 9:16 keeps words in x 140–940, y 250–1440); if they do not fit, change the composition, never shrink the type.
- A full, composed frame. The usual failure is flat shapes on a flat ground with a third of the frame empty. In a tall size, put something that matters in the bottom third.
- Depth. A textured or shaded ground, a middle layer of scenery, the subject, details on top; things overlap and cross the frame's edge.
- Big, well-set type. A headline that is large, tight and broken in the right places; nothing small that should be large. Scale small things (buttons, stamps, tags) up with the headline in tall sizes.
- Each size composed for its shape. A square, a story and a 16:9 are different pictures of the same idea; never a scaled copy. Look at every size.
- The whole duration. Render the opening, the big moment and the last second; a loop must end where it began.
- Not machine-made. No purple or lavender accent, no centred hero under a pill badge, no three identical cards with an icon on top, no emoji bullets, no gradient on everything, no grey text on a dark ground, no vague headline.
- Real copy, honest urgency. One invented product or act with a believable name on a
.exampledomain, real prices, dates with time zones, codes built the way real ones are, the same on every piece. Say what is left, never "selling fast". Words are live text, never drawn as shapes.
Reviewing your own work
Every piece goes round this loop at least three times. Most real problems only show in a render. Work in code so each round is ONE call: a script builds or changes everything at once, and the call returns its own review.
- Build and review in one call:
write_designwithlook: { at: [0, <hook>, <big moment>, <end − 100>], kits: true }It returns every size at rest, the first size at those moments and under a light and a dark brand kit, and the checks. From a terminal,popcraft run --ui write_design … --look '{"at":[0,800,6000,13900],"kits":true}'does the same in the desktop app, and a localpopcraft run <file> write_design … --look '{…}' --out look/builds the file and renders the same review through the app intolook/(sizes-.webp, moments-.webp, kit-light/dark-*.webp). - Read the checks and fix every one: contrast against what is really behind the text, contrast under a light and a dark brand kit, text below the size's minimum, text off the frame, text where the apps draw their buttons, text on text, a loop that jumps where it restarts, a band of the frame with nothing but ground, a frame past 8,192 px.
- Look at every picture it returned, and judge it as an art director would.
- Change existing layers in one call too:
run_scriptedits any number of layers and can calllook_atat the end, so the same call returns the pictures of the result. - For another moment or size,
look_atwithatandkit(vision.lookAt --at … --kit dark); up to 8 pictures a call. A tall page is unreadable as one picture:look_atwithparts: truerenders it top to bottom in bands, one per section, each on the page's own ground with the art behind it (one frame per call). - Name each problem specifically before fixing it ("the stamp covers the city", "the bottom 500 px are empty
brick", "the scenery arrives at 11 s, so the lower half is empty until then"), fix it in the script, and rebuild in
place (
frame) with the samelook. - Say what you did not look at when you report. A size or a stretch of time you did not render is not checked.
The bar for each format
- Reel, short or story (9:16, 1080×1920). Words stay inside x 64–940 and y 250–1440 (the apps' buttons and captions cover the rest). The hook is on screen within the first second; entrances land on the music's beats; the piece's one loud moment sits on the music's loudest. Captions are one text that swaps, on a plate. The bottom third holds drawing that belongs to the idea, and it arrives within the first beats. The last seconds are not a dead hold: the call to action arrives or something keeps moving. Make the 1:1, 4:5 and 16:9 as new layouts, not scaled copies.
- Square (1:1) and portrait (4:5) cuts. Use the full width; keep every element (do not drop the small ones); no empty band between the middle and the foot; type no smaller than the master's in proportion.
- Landscape (16:9). Two columns or a wide scene, composed for the shape: no sparse lower-left, no empty quarter, nothing letterboxed.
- Looping post or story. Nothing enters in a loop: every move is emphasis (pulse, shake, blink, sweep) that returns to rest, and the last frame looks like the first. A countdown loops only as a seconds column that wraps.
- Landing page (desktop 1440, laptop 1280, tablet 768, phone 375). At least six sections: navigation, a hero that shows the product working, the offer, proof, questions, a closing call to action, a legal footer. Sections below the hero are illustrated too. The hero loops in six seconds. On the phone recompose the hero (do not crop it), keep the name in the navigation, space a logo marquee so several names show at once, and keep body text 14 px or more. Leave room in every text box: a line that only just fits wraps differently in a browser.
- Pricing page. A monthly/yearly switch that really changes every price, a comparison table, questions; the plans line up (same heights, same rules); one primary button colour.
- Email header (600×300). The whole message is readable on the first frame (many mail apps show only that); no text under 12 px; a seamless loop.
- Display ads (1200×628, 1200×1200, 1080×1350, LinkedIn). Static, so depth and detail must come from drawing, not motion. In the tall cuts scale the tag, button and stamp up with the headline.
- Link preview (1200×630, GitHub 1280×640, X 1600×900). Seen at thumbnail size: the name and the offer large, art that fills, and nothing important hidden behind a card.
- Mobile ticket or app screen (phone sizes). The real layout of that screen (a wallet pass: art header, holder, tier, gate, a QR block; a confirmation screen). Product UI is re-laid out for the phone, never shrunk until its text is unreadable. Fill the lower third with something useful.
- Every format. One invented product or act, with a believable name on a
.exampledomain, real prices, dates with time zones, codes built the way real ones are, and the same facts on every piece. Honest urgency: say what is left, never "selling fast".
Traps that cost rounds
-
A script's functions run before the kit. Everything the kit hands back, and every argument a callback gets, is a reference: you cannot loop over it, print its value or test it (
size === 'mobile'inside a callback is always false). Index results (k.named(id, 'Card')[0]), loop over your own data, and give per-size changes as per-size options (webResponsive'stweaks: { mobile: … }). -
A rule line is a layer. A stack's
strokedraws a whole box, whatever itsalign. Between rows put a 1 pxk.rectpatched tolayoutSizingHorizontal: 'FILL'. -
Wrapping needs fixed-width children.
wrap: true(orlayoutWrap: 'WRAP') wraps only children with their own width; FILL children take the row and never wrap. -
Art inside a stack sits in a box of its own. A drawing (a
k.framewith absolute children) dropped into an auto-layout row is not in the flow: give it a fixed-sizek.stackand put the art inside withabsolute. -
flowSheetdoes not centre across by default. Its arguments are gap, padding, ground, then justify (along) and align (across); align defaults to the start, so pass'CENTER'as the last one to centre. -
Listing the frames:
run_scriptwithreturn doc.nodes[doc.pageId].childIds.map(id => [id, doc.nodes[id].name])gives every top-level frame;doc.outlineis a summary and may shorten a long page. -
A wipe keeps its first size. A wipe-in or wipe-out wraps the layer in a mask that does not follow the other sizes: labels clip to slivers and text runs into the apps' buttons. On anything that moves or resizes per size, use a slide, a fade or a draw-on instead.
-
Big scenery is the background of every word over its box. A full-frame drawing, a rotating beam, a big starburst: the contrast check reads their whole box. Keep scenery layers' boxes tight, and give words a plate.
-
Text hidden at rest reads as no contrast. A status that changes is one text whose words change, not two texts cross-fading; a caption that swaps is one text.
-
The rest frame is the finished picture. Entrances end on the composed design; anything that only appears mid-animation is missing from the still, the thumbnail and the checks.
-
Do not turn or scale an animated layer differently per size. Move it instead; a stamp that is scaled per size should arrive with a drop or slide, not a pop.
-
A counting figure changes width as it counts. Give it a fixed-width box. A count cannot count back down by itself in a loop: fade it at the seam or let a ticking count wrap.
-
Italic and capitals measure differently. Italic text measures as upright when laid out, so keep italic lines short and out of wrapping paragraphs; type capitals as capitals instead of a case transform on wrapping text.
-
Small italic text fails contrast first. Keep it at 24 px or more where the ground is busy or a kit could lighten it.
-
Words in the brand colour on the ground break under a kit (orange on an orange ground). Words use the text roles; the brand colour is for fills, stamps and shadows with their own on-colour.
-
Place labels after measuring. A tag, tab or stamp placed at a guessed position lands on what it labels. Read the box of the thing first, and put stamps on top of everything, never over the one word they are about.
-
A style's alignment wins. A centred text style overrides a left alignment on the text: make a left copy of the style for left-set layouts.
-
Vector paths do not follow a resize. A drawing made for one size keeps its shape in another; redraw or re-place it per size, and keep its box tight.
-
Product UI scaled down is unreadable. At a smaller size re-lay the product out (fewer rows, larger type), never shrink the drawing until its 7 px text is mush.
-
Art hides behind cards. A mascot or a scene drawn before a card sits under it: check the order, and that what is meant to show still shows in every size.
-
Quote marks and symbols need the right characters. Use real quotation marks (“ ”) and check the font has the glyph; drawn scribbles read as brackets.
-
Text inside a 3D scene is measured flat. The checks read its unscaled size and box: keep the flat box inside the safe area and make the type large enough at the scale the camera shows it, then look at the render.
-
Grain over a whole frame makes it heavy. A full-frame noise or grain layer multiplies the size of every picture of the design (thumbnails, previews, exports); keep grain to the ground behind the content, or leave it out.
-
A page taller than 8,192 px does not draw. A very long phone or tablet page renders blank past the graphics card's limit: keep every size of a page shorter than that (fewer sections on the phone, tighter spacing).
-
Text takes one solid colour. A gradient or chrome fill on words draws as its first colour only; fake metal with shadows and highlights inside the letters, and keep that first colour a colour role.
-
A scrolling strip is checked where it rests. The checks read a marquee's cells at their resting place, not where the scroll carries them: rest every cell inside the strip and in the safe area, and move them with a wrapping offset.
-
Fonts have limits. Only a few faces are measured exactly (Inter, Source Serif 4, JetBrains Mono); box-drawing, block and arrow characters may be missing from them, so draw those as shapes.
Making it a template
A template for the gallery is made exactly as PopCraft's own are: a defineTemplate source written with the Kit,
started from whichever of the 1,275 official sources is the best match, and held to every check the official ones pass.
Making templates has the whole pipeline. The short form:
npx popcraft template list <what you are making> # find the closest official template
npx popcraft template new templates/mine.ts --from <id> # its source, as yours
npx popcraft template check # build, then every official check
npx popcraft template look templates/mine.ts # every size, four moments, two brand kits: look, fix, repeat
npx popcraft template pictures && npx popcraft template publishRead the official source you start from before changing it: it is the best worked example of its format there is.
A shop for a real store
A storefront is drawn in PopCraft and run by a Next.js Commerce store (Storefronts); the Ecommerce plugin adds the Shopify store's catalogue, its setup and a Shopify theme. Bind it, do not fake it:
- Products, the cart's lines, the cart's totals and the menu are collections with a
schema(commerce.product,commerce.cartLine,commerce.cart,commerce.menu);edit_collectionwith a schema and no fields makes its fields. - A card is a component with properties (
edit_component_property: TEXT Title and Price, IMAGE Image, whichbindLayerfeeds to the picture'simageUrl). An instance in a list over the products binds each property to a field withbind_field(property: "Title", field: "title"), so restyling the component restyles every card. A loose text can still say{{item.title}}. The cart's totals are a list of one overcommerce.cart. - One product in a hero or a feature is picked with
set_record(collection: "Products", record: "<handle>"), not a list of one with an offset: the person sees which product it is and can pick another, and a published store shows its own product with that handle. - Add to cart, remove, checkout and search are forms with a storefront
formAction. Add to cart sends a hidden inputmerchandiseIdwith the value{{item.variant_id}}. popcraft storefront shop.popcraft --target next-commerce --out <store>writes components that take the store's own data (products,product,cart,cartLines,menu) and its server actions (onAddToCart,onCheckout, …);--target next-commerce-storewrites the whole store, to deploy with the Shopify store's address and public Storefront token as its environment (set_site_environment:SHOPIFY_STORE_DOMAIN,SHOPIFY_STOREFRONT_ACCESS_TOKEN).
Start from web-shop-fieldwork or web-shop-kiln: they are whole stores, and their source
(templates/src/web/shop-shared.ts in @popcraft/kit) is the worked example.
Prompts to give an agent
These worked when we made the gallery's templates. Paste one, after pointing the agent at this page.
A campaign pack
Read https://<popcraft host>/docs/api/agents and follow its loop exactly. Make a Black Friday campaign for a
status-page product in one world: a supermarket coupon circular (clip-out coupons, dashed cut lines, scissors, a
REDEEMED stamp, loud two-ink newsprint, heavy condensed capitals). Make, each as its own design in one file: a 9:16
deal reel with music (1:1, 4:5 and a real 16:9 layout), an animated countdown post, a last-24-hours story, a 600×300
email header that reads on its first frame, and a static ad set (1200×628, 1200×1200, 1080×1350). Invent the product,
the offer, prices and code. For every piece: build, fix every check, render every size and the reel at four moments,
improve at least three rounds. Finish each piece before the next. Then tell me what is still not right and what you
did not look at.One video cut to music
Read https://<popcraft host>/docs/api/agents. Make a 15-second feature demo for an email client that clears an
inbox: a product window on a backdrop, a pointer selects a pile of messages, a command palette opens, the unread count
runs from 248 to 0, the list empties row by row, a done state lands on the last beat. Use the launch track and start
every entrance on a beat. 9:16 with 1:1 and a real 16:9. Render the opening, 5 s, 10 s and the last second at every
size and fix what you see, three rounds at least.A template for the gallery
Read https://<popcraft host>/docs/api/templates and https://<popcraft host>/docs/api/agents. In a new folder, install
@popcraft/kit and @popcraft/cli. Make a launch announcement for a note-taking app: a 9:16 reel with music and 1:1 and
4:5 cuts, with its own product screenshot (media('assets/…')) and real copy. Find the closest official template with
popcraft template list, start from its source with --from, and read it first. Run popcraft template check until
nothing fails, then popcraft template look and improve what you see, three rounds at least. Then run pictures, look
at the thumbnail and the loop, and publish.Fixing a file a person made
Read https://<popcraft host>/docs/api/agents. Open launch.popcraft. With run_script, find every text under 16px, every
text that fails contrast, and every layer still called "Rectangle" or "Frame"; fix them (raise sizes, darken text or add
a plate, name layers by what they are). Render each frame before and after and show me both.Limits
- Design scripts are built on the machine running the CLI (or in the desktop app), never on PopCraft's servers. To
put one in a cloud file, build it locally and
popcraft push, or usepopcraft run --ui. - Rendering needs puppeteer and a PopCraft app to render with (
--app-url, by default the server). - The checks read layers' boxes: they do not judge composition, and they skip text over pictures. Look at the render.
- A script's text is measured with font metrics; the editor re-measures on open, as it does for templates.