v0.6: template-manager owns the product catalog

The host no longer carries a runtime product catalog. products.js
shrinks to a legacy single-product stub; the canonical catalog lives in
template-manager's productsStore, seeded ONCE on first boot from the
new seedProducts.js (after that the store under templates/ is
authoritative and the seed is ignored — intentional, so restarts never
rewrite an admin-managed catalog). phoneModels.js and
templateCategories.js are the supporting constant sets, and
docs/template-schema.md documents the v4 schema.

templates/ is the admin-managed store itself (_products_index.json +
products/<id>/product.json, JSON only) — committed so the configured
catalog (sizes, pricing, options built up in the admin UI) survives a
fresh checkout rather than regressing to the seed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
khalid@traclabs.com
2026-09-01 23:28:48 -05:00
parent b5b8808251
commit aba2dbedfb
17 changed files with 1549 additions and 76 deletions

View File

@@ -2,6 +2,12 @@
This document describes the template structure for creating custom t-shirt design templates.
> **Note (2026-07-06):** this doc predates the goods-editor extraction and the
> v4 multi-view template schema. The authoritative element schema lives in
> `goods-editor-module/CLAUDE.md` ("Element schema") and the accepted payload
> shape in `template-manager/src/server/lib/validation.js`. The property
> tables below are still accurate for the fields they list.
## Template Structure
```json
@@ -42,6 +48,27 @@ This document describes the template structure for creating custom t-shirt desig
| `fontFamily`| string| No | Google Font name (default: "DM Sans") |
| `fill` | string | No | Hex color (default: "#0f172a") |
| `rotation` | number | No | Rotation in degrees (default: 0) |
| `arc` | number | No | Curve amount (default: 0 = flat). `> 0` arcs up (rainbow), `< 0` arcs down (smile). See "Curved text" below. |
| `align` | string | No | `left` \| `center` \| `right` (default: `center`; multi-line) |
| `lineHeight` | number | No | Line-box ratio of fontSize (default: 1.2) |
| `letterSpacing` | number | No | Absolute tracking in canvas units (default: 0; suspended while curved) |
#### Curved text (`arc`)
- The editor's Curve slider writes values in **−100..100**; hand-authored
templates should stay in that range.
- The stored value up to **±200** is honored by the renderer and the print
exporter (`ARC_MODEL_MAX` in the goods-editor module, 2026-07-06): the
editor itself *derives* values above 100 when preserving an authored
curve on long customer text whose font size auto-shrank. Don't hand-author
above ±100 — the headroom exists for derived values.
- What is visually preserved across customer edits is the curve's
**density** (`arc · fontSize / width`), not the raw number: the editor
rescales `arc` as text, font, or size changes so the perceived sweep the
template author designed stays constant. Expect saved customer designs to
carry arc values that differ from the authored one.
- `letterSpacing` has no effect while `|arc| > 0.5` (suspension rule); the
stored value survives and reapplies when the text goes flat.
### Image Element

View File

@@ -0,0 +1,174 @@
/**
* Phone model catalog for the phone-case product type.
*
* Coverage: iPhone 13 through iPhone 17 (21 models).
*
* Schema per entry
* ─────────────────
* id: kebab-case stable identifier (used in template IDs,
* URLs, REST payloads)
* label: human-readable name shown in pickers
* series: '13' | '14' | '15' | '16' | '17' (for grouping)
* body: { heightMm, widthMm, thicknessMm } — external case
* dimensions, source: Apple specs + tech press as of
* May 2026.
* canvas: { width, height } — pixel dimensions of the editor
* canvas. Aspect ratio matches body height:width
* because the phone case wraps the back of the phone.
* Canvas size 600 (vs 300 for shirts) gives finer
* resolution for the typically-smaller phone case
* designs.
* cameraRegion: placeholder rectangle (or rectangles) in canvas
* coordinates marking the camera bump cutout — the
* non-printable area. Approximate for v1; production
* ranges should refine these against real die-line
* specs from the print shop.
* silhouette: SVG path of the case outline in canvas coords.
* v1: rounded-rect approximation; production should
* use the actual die-line.
*
* Why placeholder camera regions
* ──────────────────────────────
* The plan explicitly says variant catalog ships as placeholders. Camera
* regions on real production cases need exact die-line measurements from
* the print shop — those come during pre-launch QA, not now. The
* placeholder values are visually plausible (right corner, right size)
* so the editor's UX of "you can't put your design here" works end-to-end
* for testing. Replace with real die-line coordinates before launch.
*/
// Canvas height (longest dimension); width is derived from each model's
// body aspect ratio. Square-ish canvases tend to give the editor's
// transformer controls room without the controls cropping off-screen.
const CANVAS_LONG_EDGE = 600;
function buildCanvas(heightMm, widthMm) {
// Phone is taller than wide; canvas height matches the long edge.
const aspect = widthMm / heightMm;
return {
width: Math.round(CANVAS_LONG_EDGE * aspect),
height: CANVAS_LONG_EDGE,
};
}
// Camera-region pattern helpers. Each returns a rectangle (or array of
// them for compound bumps) sized as a fraction of the canvas, top-left
// origin. The actual phone case manufacturer crops circles inside these
// rects; we use rects because they're the conservative non-printable
// area that contains the cutouts.
function squareCorner(canvas) {
// iPhone 13-15 base: square bump in top-left corner.
const w = canvas.width * 0.32;
const h = canvas.width * 0.32; // square in canvas units
return [{ x: canvas.width * 0.05, y: canvas.width * 0.05, width: w, height: h }];
}
function verticalStrip(canvas) {
// iPhone 16 / 16e / 16-plus / 17 / 17-air: vertical pill, top-left.
const w = canvas.width * 0.14;
const h = canvas.width * 0.34;
return [{ x: canvas.width * 0.06, y: canvas.width * 0.05, width: w, height: h }];
}
function triangularBump(canvas) {
// iPhone 16-pro / 16-pro-max: triangular three-lens cluster top-left.
// Use the conservative bounding rect.
const w = canvas.width * 0.34;
const h = canvas.width * 0.34;
return [{ x: canvas.width * 0.04, y: canvas.width * 0.04, width: w, height: h }];
}
function plateauBar(canvas) {
// iPhone 17-pro / 17-pro-max: horizontal "plateau" bar spanning most
// of the back top edge.
const w = canvas.width * 0.88;
const h = canvas.width * 0.18;
return [{ x: canvas.width * 0.06, y: canvas.width * 0.04, width: w, height: h }];
}
// Helper: build a rounded-rect silhouette path in canvas coords.
// (M…L…A…Z form; corner radius ≈ 8% of width which matches Apple's
// hardware roundness reasonably for v1 visualization.)
function silhouettePath(canvas) {
const w = canvas.width;
const h = canvas.height;
const r = Math.round(w * 0.08);
return `M${r},0 L${w - r},0 A${r},${r} 0 0 1 ${w},${r} L${w},${h - r} A${r},${r} 0 0 1 ${w - r},${h} L${r},${h} A${r},${r} 0 0 1 0,${h - r} L0,${r} A${r},${r} 0 0 1 ${r},0 Z`;
}
function buildModel({ id, label, series, heightMm, widthMm, thicknessMm, cameraPattern }) {
const canvas = buildCanvas(heightMm, widthMm);
return {
id,
label,
series,
body: { heightMm, widthMm, thicknessMm },
canvas,
cameraRegion: cameraPattern(canvas),
silhouettePath: silhouettePath(canvas),
};
}
// Dimensions from Apple official specs + tech-press round-ups (Apr–May 2026):
// • iPhone 13 line: Apple spec sheets
// • iPhone 14 line: Apple spec sheets
// • iPhone 15 line: Apple spec sheets
// • iPhone 16 line + 16e: Apple spec sheets, ecoATM/SimplyMac aggregates
// • iPhone 17 line: TechCrunch lineup comparison + Apple specs
export const PHONE_MODELS = Object.fromEntries([
// iPhone 13 (4 models) — square camera top-left
buildModel({ id: 'iphone-13-mini', label: 'iPhone 13 mini', series: '13', heightMm: 131.5, widthMm: 64.2, thicknessMm: 7.65, cameraPattern: squareCorner }),
buildModel({ id: 'iphone-13', label: 'iPhone 13', series: '13', heightMm: 146.7, widthMm: 71.5, thicknessMm: 7.65, cameraPattern: squareCorner }),
buildModel({ id: 'iphone-13-pro', label: 'iPhone 13 Pro', series: '13', heightMm: 146.7, widthMm: 71.5, thicknessMm: 7.65, cameraPattern: squareCorner }),
buildModel({ id: 'iphone-13-pro-max', label: 'iPhone 13 Pro Max', series: '13', heightMm: 160.8, widthMm: 78.1, thicknessMm: 7.65, cameraPattern: squareCorner }),
// iPhone 14 (4 models) — square camera top-left, slightly larger on Pros
buildModel({ id: 'iphone-14', label: 'iPhone 14', series: '14', heightMm: 146.7, widthMm: 71.5, thicknessMm: 7.8, cameraPattern: squareCorner }),
buildModel({ id: 'iphone-14-plus', label: 'iPhone 14 Plus', series: '14', heightMm: 160.8, widthMm: 78.1, thicknessMm: 7.8, cameraPattern: squareCorner }),
buildModel({ id: 'iphone-14-pro', label: 'iPhone 14 Pro', series: '14', heightMm: 147.5, widthMm: 71.5, thicknessMm: 7.85, cameraPattern: squareCorner }),
buildModel({ id: 'iphone-14-pro-max', label: 'iPhone 14 Pro Max', series: '14', heightMm: 160.7, widthMm: 77.6, thicknessMm: 7.85, cameraPattern: squareCorner }),
// iPhone 15 (4 models) — square camera continues
buildModel({ id: 'iphone-15', label: 'iPhone 15', series: '15', heightMm: 147.6, widthMm: 71.6, thicknessMm: 7.8, cameraPattern: squareCorner }),
buildModel({ id: 'iphone-15-plus', label: 'iPhone 15 Plus', series: '15', heightMm: 160.9, widthMm: 77.8, thicknessMm: 7.8, cameraPattern: squareCorner }),
buildModel({ id: 'iphone-15-pro', label: 'iPhone 15 Pro', series: '15', heightMm: 146.6, widthMm: 70.6, thicknessMm: 8.25, cameraPattern: squareCorner }),
buildModel({ id: 'iphone-15-pro-max', label: 'iPhone 15 Pro Max', series: '15', heightMm: 159.9, widthMm: 76.7, thicknessMm: 8.25, cameraPattern: squareCorner }),
// iPhone 16 (5 models) — vertical strip on base/16e/Plus, triangular on Pros
buildModel({ id: 'iphone-16e', label: 'iPhone 16e', series: '16', heightMm: 138.4, widthMm: 67.0, thicknessMm: 7.8, cameraPattern: verticalStrip }),
buildModel({ id: 'iphone-16', label: 'iPhone 16', series: '16', heightMm: 147.6, widthMm: 71.6, thicknessMm: 7.8, cameraPattern: verticalStrip }),
buildModel({ id: 'iphone-16-plus', label: 'iPhone 16 Plus', series: '16', heightMm: 160.9, widthMm: 77.8, thicknessMm: 7.8, cameraPattern: verticalStrip }),
buildModel({ id: 'iphone-16-pro', label: 'iPhone 16 Pro', series: '16', heightMm: 149.6, widthMm: 71.5, thicknessMm: 8.25, cameraPattern: triangularBump }),
buildModel({ id: 'iphone-16-pro-max', label: 'iPhone 16 Pro Max', series: '16', heightMm: 163.0, widthMm: 77.6, thicknessMm: 8.25, cameraPattern: triangularBump }),
// iPhone 17 (4 models) — Pros get the new horizontal "plateau" bar
buildModel({ id: 'iphone-17', label: 'iPhone 17', series: '17', heightMm: 149.6, widthMm: 71.5, thicknessMm: 7.95, cameraPattern: verticalStrip }),
buildModel({ id: 'iphone-17-air', label: 'iPhone Air', series: '17', heightMm: 156.2, widthMm: 74.7, thicknessMm: 5.64, cameraPattern: verticalStrip }),
buildModel({ id: 'iphone-17-pro', label: 'iPhone 17 Pro', series: '17', heightMm: 150.0, widthMm: 71.9, thicknessMm: 8.75, cameraPattern: plateauBar }),
buildModel({ id: 'iphone-17-pro-max', label: 'iPhone 17 Pro Max', series: '17', heightMm: 163.4, widthMm: 78.0, thicknessMm: 8.75, cameraPattern: plateauBar }),
].map((m) => [m.id, m]));
/**
* Default phone model ID used when the user lands on the phone-case
* customize route without specifying a model. Picks the most common
* current-generation flagship as of catalog timestamp (May 2026).
*/
export const DEFAULT_PHONE_MODEL_ID = 'iphone-16';
/**
* Group phone models by series for menus. The picker UI typically
* shows series headers ("iPhone 17", "iPhone 16", ...) with the
* models under each.
*/
export function groupBySeries(models = PHONE_MODELS) {
const groups = {};
for (const model of Object.values(models)) {
if (!groups[model.series]) groups[model.series] = [];
groups[model.series].push(model);
}
// Sort each series by canvas height descending (biggest first within
// a generation feels right in pickers).
for (const series of Object.keys(groups)) {
groups[series].sort((a, b) => b.body.heightMm - a.body.heightMm);
}
// Return series in DESCENDING order (newest first).
return Object.entries(groups).sort(([a], [b]) => b.localeCompare(a));
}

View File

@@ -1,89 +1,69 @@
/**
* Product type configuration for the Pawfectly Yours editor.
* Legacy single-product stub.
*
* The frontend rendering is parameterized by these objects so that the same
* editor code can drive shirts, magnets, earrings, stickers, and so on. Only
* code reads this — there is intentionally no UI for picking the active
* product.
* v0.6 — the host no longer owns a runtime product catalog. The
* canonical catalog lives in template-manager (productsStore) and is
* seeded once from src/constants/seedProducts.js.
*
* Coordinate systems
* ──────────────────
* Every product has TWO coordinate systems:
* What's left in this file
* ────────────────────────
* The legacy single-product editor flow at `/` (apparel-designer's
* App.jsx) calls `getActiveProduct()` and reads a small set of
* shape-only fields off the result: `canvasSize`, `printZone`,
* `productSvg`, `background`. We keep a minimal stub here so that
* flow continues booting without re-architecting it for the v0.6
* data shape.
*
* 1. **Design coordinates** (`canvasSize`) — the integer space in which
* individual elements (images, text, stickers) live. Element x/y/width/
* height are in these units. The server export pipeline scales from
* `canvasSize` to the final 4500×4500 print resolution.
*
* 2. **Product SVG coordinates** (`productSvg.width / .height`) — the
* viewBox of the product mockup that's drawn behind the design canvas.
* The `printZone` field positions the design canvas inside the product
* mockup, in these units.
*
* Keeping `canvasSize` at 300 (the historical value) means existing templates
* and the server-side export math do not need to change. The visual fit
* (canvas overlapping the print zone of the product) is achieved by CSS
* scaling the rendered Konva Stage at runtime.
* Migration intent
* ────────────────
* The legacy `/` flow is slated for retirement; the customer-facing
* surface is `/customize?template=…` which reads the template's
* embedded variant. When the legacy flow is removed, this file goes
* with it.
*/
export const PRODUCT_TYPES = {
shirt: {
id: 'shirt',
label: 'T-Shirt',
canvasSize: 300,
// Where on the product mockup the design canvas should overlay, in
// product SVG coordinates. Sized roughly to a real DTG print zone for
// a youth-medium tee.
//
// Note: the product SVG's viewBox is offset (starts at 40, 50) to crop
// empty margin around the shirt outline. printZone coords are in that
// same viewBox space — i.e. (90, 130) is what the chest landed at
// after the crop, not the path's raw (130, 180).
printZone: { x: 90, y: 130, width: 220, height: 220 },
productSvg: {
// Component name (looked up by DesignCanvas) and the dimensions of its
// viewBox. Must match the actual SVG.
component: 'shirt',
width: 400,
height: 470,
},
background: {
// 'color' = user can pick from SHIRT_COLORS
// 'fixed' = use background.color as a constant fill
mode: 'color',
defaultColorId: 'white',
},
},
// Reference shapes for future products. None of these are wired into the
// UI yet, but the DesignCanvas/ProductSVG plumbing is ready to handle them.
//
// magnet: {
// id: 'magnet',
// label: 'Magnet',
// canvasSize: 300,
// printZone: { x: 30, y: 30, width: 240, height: 240 },
// productSvg: { component: 'magnet', width: 300, height: 300 },
// background: { mode: 'fixed', color: '#f4f1ee' },
// },
// earring: { ... },
// sticker: { ... },
// Bare shirt shape with just enough fields for the legacy
// getActiveProduct() consumers in App.jsx + the editor's
// legacy single-product code paths.
const LEGACY_SHIRT_STUB = {
id: 'shirt',
label: 'T-Shirt',
canvasSize: 300,
printZone: { x: 90, y: 130, width: 220, height: 220 },
productSvg: { component: 'shirt', width: 400, height: 470 },
background: { mode: 'color', defaultColorId: 'white' },
// No variants — v0.6 catalog lives in template-manager.
variants: [],
};
/**
* Which product the editor is currently configured for. Change this constant
* to point at a different `PRODUCT_TYPES` entry. There's intentionally no UI
* exposure — switching products is a code change.
* Return the active product for the legacy single-product editor.
* Always returns the shirt stub in v0.6.
*/
export function getActiveProduct() {
return LEGACY_SHIRT_STUB;
}
// ── Deprecated exports ────────────────────────────────────────
//
// These names are referenced in places that haven't been swept yet.
// They resolve to the legacy stub or to undefined so any straggling
// importer doesn't crash on module load. Will be removed in a
// follow-up cleanup pass.
/** @deprecated Use seedProducts.DEFAULT_PRODUCT_ID. */
export const DEFAULT_PRODUCT_TYPE_ID = 'shirt';
/** @deprecated Use seedProducts.DEFAULT_SIZE_ID. */
export const DEFAULT_VARIANT_ID = 'shirt-default';
/** @deprecated Use seedProducts. */
export const ACTIVE_PRODUCT_ID = 'shirt';
export function getActiveProduct() {
const product = PRODUCT_TYPES[ACTIVE_PRODUCT_ID];
if (!product) {
throw new Error(
`Unknown ACTIVE_PRODUCT_ID="${ACTIVE_PRODUCT_ID}". `
+ `Valid ids: ${Object.keys(PRODUCT_TYPES).join(', ')}`
);
}
return product;
/** @deprecated Use seedProducts; map productId/sizeId via the products API. */
export const PRODUCT_TYPES = { shirt: LEGACY_SHIRT_STUB };
/** @deprecated Use the resolved variant from a template GET response. */
export function getVariant(_productTypeId, _variantId) {
return null;
}

View File

@@ -0,0 +1,445 @@
/**
* Seed product catalog — used by template-manager's mountTemplateStore
* to populate `_products_index.json` on first boot only.
*
* v0.6 — replaces the pre-v0.6 PRODUCT_TYPES constant. The host no
* longer owns the catalog at runtime; this file is read once at boot
* and the data is then persisted in the storage dir.
*
* UUIDs are hardcoded so re-running the seed (after a wipe) produces
* the same identifiers, and so any test fixtures / docs that
* reference these IDs stay stable.
*
* Schema (per product)
* ────────────────────
* { id, slug, label, description, category, kind, options[], sizes[] }
* kind: 'customizable' | 'fixed' | 'material' (v0.7 + v0.8)
*
* Schema (per option value)
* ─────────────────────────
* { id, slug, label, hex?, priceCents }
* priceCents is v0.8 — non-negative integer, default 0; the unit
* price the customer pays is `size.basePriceCents + sum(selected
* values' priceCents)`.
*
* Schema (per size)
* ─────────────────
* { id, slug, label,
* physical: { width, height, unit: 'in' },
* canvas: { width, height } | null, // null for fixed + material
* printZone: { x, y, width, height } | null, // null for fixed + material
* silhouettePath, mockupFallbackColor, basePriceCents,
* defaultParcel: { length_in, width_in, height_in, weight_oz } } // v0.8
*
* Canvas convention
* ─────────────────
* 100 canvas units per physical inch (per locked decision §6). Exports
* at 300 DPI map cleanly via 3× resample on the export pipeline.
*
* Print zone convention
* ─────────────────────
* 10% inset from canvas edges by default. Each size carries an
* explicit rectangle so the admin can tune per-size if needed.
*/
// ── Pre-allocated UUIDs ────────────────────────────────────────
//
// Hardcoded so the seed is idempotent: wiping the storage dir and
// re-seeding produces identical IDs, which means existing template
// references (if any survived the wipe) still resolve.
//
// Naming: P_<slug> for products, S_<slug>_<size-slug> for sizes,
// O_<slug>_<option-slug> for option groups, V_<slug>_<option-slug>_<value-slug>
// for option values.
const P_SHIRT = 'c3ac22c7-ef49-4997-a525-d4f0eb32da6f';
const P_HOODIE = '6202b99a-a4a9-4f11-8b2d-32c17516ecfa';
const P_MAGNET = 'f530dc06-ef10-46b4-9903-3927f85bd9a7';
const P_MUG = '951cc0aa-6b28-460a-ad31-8ac8fdcc5ebd';
const P_POSTER = '95dca14e-28e4-46e8-93b1-b45e3b556530';
const P_TOTE = '1d69616b-96a8-4280-9beb-c8926222c589';
const P_PILLOW = 'c722b19b-d6c6-4639-b0eb-dec34731e50d';
const P_NOTEBOOK = '1ed028ad-1998-4ace-813b-79023bd08c1d';
const P_STICKER = '04507704-50a9-4de9-9b23-475d6b76bf0e';
const P_HAT = 'cb43b749-05c0-4d1c-8d0b-dadd9b28574d';
const P_PHONE_CASE = 'f5cbb673-8696-4dad-a63c-3a802ba53fd3';
// v0.7 — first fixed-kind (ready-made) product. No customization;
// customer picks size + color and adds to cart.
const P_RABBIT_TEE = '8b1f4c2d-3e9a-4b76-9c8e-2a1d5f6e7b89';
const S_SHIRT_S = '3df58f51-70ce-4898-8091-2bb186b04193';
const S_SHIRT_M = '86787c88-c4e0-4798-a34f-d1e173b2704b';
const S_SHIRT_L = '8cf5377f-8c6b-4afe-80c3-9337e6303176';
const S_SHIRT_XL = 'ecc7707a-1703-4c27-9da7-6b39709837ba';
const S_SHIRT_XXL = '3667ec33-6123-44bd-a12e-5fdfef3c2874';
const S_HOODIE_S = 'f002d98e-6601-46cb-9ed0-ad6608a794ac';
const S_HOODIE_M = '13849307-bed4-4654-9476-fb135561f05f';
const S_HOODIE_L = 'b327d6c5-8f0b-4639-b8ec-8d3f4ec74d1a';
const S_HOODIE_XL = 'af9ed947-e705-4205-87b0-6c529ce6b342';
const S_HOODIE_XXL = '83156320-5f30-4cf8-b28c-3f69fcf7887e';
const S_MAGNET_3X3 = '6f88ed75-7ab5-49ae-8dc5-18568da63899';
const S_MAGNET_4X4 = '3a9d60f5-821c-4f3f-8d41-62cfeaba208a';
const S_MAGNET_ROUND = '17e3cc66-f897-4717-8b08-c5b0308668df';
const S_MUG_11OZ = 'ccaf7033-3e43-4171-9cde-fe6be676ea05';
const S_MUG_15OZ = 'ae4ad837-6669-4491-af8e-445f90794779';
const S_POSTER_12X18 = '7d015072-f884-4e04-8a39-ce7c8e149059';
const S_POSTER_18X24 = '9a5529fd-e54e-4868-acbb-5a62c44d1297';
const S_TOTE_14X14 = '61e5519d-1230-4397-a795-7303a30e0881';
const S_PILLOW_18 = '22d04708-d964-4cdc-a682-fa9c02372a88';
const S_NOTEBOOK_A5 = 'add070b4-1fa3-47bb-84a5-df8d99c2b821';
const S_STICKER_3 = '6744bd80-e64b-4351-a0b8-9316dd138db5';
const S_HAT_CAP = '778c4b47-a46b-4753-8fa2-be8af1523f4a';
const S_PHONE_DEFAULT = '4be821c2-8ba5-4dd1-ad0b-1a188dd89796';
// Rabbit Tee (v0.7 fixed product) — same 5 apparel sizes as the
// shirt, with the same shape but distinct UUIDs so they're separable
// in the index + audit log.
const S_RABBIT_S = 'a1b2c3d4-e5f6-4a78-8b9c-1d2e3f4a5b6c';
const S_RABBIT_M = 'b2c3d4e5-f6a7-4b89-9c8d-2e3f4a5b6c7d';
const S_RABBIT_L = 'c3d4e5f6-a7b8-4c9d-8a9e-3f4a5b6c7d8e';
const S_RABBIT_XL = 'd4e5f6a7-b8c9-4d8e-9b8a-4a5b6c7d8e9f';
const S_RABBIT_XXL = 'e5f6a7b8-c9d8-4e8f-8c9b-5b6c7d8e9f8a';
// Option group + value UUIDs (shirt + hoodie colors).
const O_SHIRT_COLOR = '9d58a8b1-5976-4512-9095-883f16a4c1b8';
const V_SHIRT_WHITE = '29092f97-2654-4150-89e5-7a323eb891e4';
const V_SHIRT_BLACK = 'c25f746a-efc2-4213-9328-728ac6881bc6';
const V_SHIRT_PINK = '8e077db8-bc48-45c0-a3e1-df10b6f1367f';
const O_HOODIE_COLOR = '49a9bd93-84c9-4e9c-a4e6-c1e742c6777a';
const V_HOODIE_BLACK = '4e8d2854-8f0e-4c99-b854-8c0e07b59a49';
const V_HOODIE_NAVY = '618c49fc-9fc6-486b-9d83-a2fff373092f';
const V_HOODIE_GRAY = '0d1d8ec2-c7b9-40c9-9da8-9b5f1c6d1e3f';
// Rabbit tee (v0.7) color option.
const O_RABBIT_COLOR = 'f6a7b8c9-d8e9-4f8a-9d8e-6c7d8e9f8a7b';
const V_RABBIT_WHITE = 'a7b8c9d8-e9f8-4a9b-8e9f-7d8e9f8a7b6c';
const V_RABBIT_BLACK = 'b8c9d8e9-f8a7-4b8c-9f8a-8e9f8a7b6c5d';
const V_RABBIT_PINK = 'c9d8e9f8-a7b6-4c8d-8a7b-9f8a7b6c5d4e';
// ── Geometry helpers ──────────────────────────────────────────
//
// Same helpers the pre-v0.6 PRODUCT_TYPES used, lifted here so the
// seed file is self-contained (no import from the legacy products.js,
// which is being stripped down in this same PR).
function roundedRectPath(w, h, r) {
return `M${r},0 L${w - r},0 A${r},${r} 0 0 1 ${w},${r} L${w},${h - r} A${r},${r} 0 0 1 ${w - r},${h} L${r},${h} A${r},${r} 0 0 1 0,${h - r} L0,${r} A${r},${r} 0 0 1 ${r},0 Z`;
}
function circlePath(r) {
return `M${r},0 A${r},${r} 0 1 0 ${r},${2 * r} A${r},${r} 0 1 0 ${r},0 Z`;
}
/**
* Build a printZone rect inset by `pct` (0..1) from canvas edges.
*/
function insetPrintZone(canvasW, canvasH, pct = 0.1) {
const insetW = Math.round(canvasW * pct);
const insetH = Math.round(canvasH * pct);
return {
x: insetW,
y: insetH,
width: canvasW - 2 * insetW,
height: canvasH - 2 * insetH,
};
}
/**
* Build a size record for a fixed (ready-made) product. No canvas,
* no print zone, no silhouette — just physical dimensions + price.
* Used by v0.7 fixed-kind products like the rabbit tee.
*/
function makeFixedSize({ id, slug, label, widthIn, heightIn, fallbackColor, basePriceCents = 0 }) {
return {
id,
slug,
label,
physical: { width: widthIn, height: heightIn, unit: 'in' },
canvas: null,
printZone: null,
silhouettePath: null,
mockupFallbackColor: fallbackColor,
basePriceCents,
};
}
/**
* Build a size record for a rectangular product with physical inches
* x 100 canvas units. The helper centralizes the conversion so a
* future canvas-units change (e.g. moving to 144 units/inch) is a
* one-place edit.
*/
function makeRectSize({ id, slug, label, widthIn, heightIn, fallbackColor, basePriceCents = 0, printZonePct = 0.1, cornerPct = 0.04 }) {
const cW = widthIn * 100;
const cH = heightIn * 100;
const r = Math.round(Math.min(cW, cH) * cornerPct);
return {
id,
slug,
label,
physical: { width: widthIn, height: heightIn, unit: 'in' },
canvas: { width: cW, height: cH },
printZone: insetPrintZone(cW, cH, printZonePct),
silhouettePath: roundedRectPath(cW, cH, r),
mockupFallbackColor: fallbackColor,
basePriceCents,
};
}
function makeCircleSize({ id, slug, label, diameterIn, fallbackColor, basePriceCents = 0, printZonePct = 0.1 }) {
const c = diameterIn * 100;
const r = c / 2;
const inset = Math.round(c * printZonePct);
return {
id,
slug,
label,
physical: { width: diameterIn, height: diameterIn, unit: 'in' },
canvas: { width: c, height: c },
printZone: { x: inset, y: inset, width: c - 2 * inset, height: c - 2 * inset },
silhouettePath: circlePath(r),
mockupFallbackColor: fallbackColor,
basePriceCents,
};
}
// ── Seed catalog ──────────────────────────────────────────────
export const SEED_PRODUCTS = [
// ── T-Shirt ────────────────────────────────────────────────
{
id: P_SHIRT,
slug: 'shirt',
label: 'T-Shirt',
description: 'Soft cotton tee, printed on the front.',
category: 'Apparel',
options: [
{
id: O_SHIRT_COLOR,
slug: 'color',
label: 'Color',
kind: 'color',
values: [
{ id: V_SHIRT_WHITE, slug: 'white', label: 'White', hex: '#ffffff' },
{ id: V_SHIRT_BLACK, slug: 'black', label: 'Black', hex: '#1f2937' },
{ id: V_SHIRT_PINK, slug: 'pink', label: 'Dusty Pink', hex: '#fbcfe8' },
],
},
],
sizes: [
makeRectSize({ id: S_SHIRT_S, slug: 's', label: 'Small', widthIn: 12, heightIn: 16, fallbackColor: '#f4f4f5', basePriceCents: 2200 }),
makeRectSize({ id: S_SHIRT_M, slug: 'm', label: 'Medium', widthIn: 12, heightIn: 16, fallbackColor: '#f4f4f5', basePriceCents: 2200 }),
makeRectSize({ id: S_SHIRT_L, slug: 'l', label: 'Large', widthIn: 12, heightIn: 16, fallbackColor: '#f4f4f5', basePriceCents: 2200 }),
makeRectSize({ id: S_SHIRT_XL, slug: 'xl', label: 'X-Large', widthIn: 12, heightIn: 16, fallbackColor: '#f4f4f5', basePriceCents: 2400 }),
makeRectSize({ id: S_SHIRT_XXL, slug: 'xxl', label: 'XX-Large', widthIn: 12, heightIn: 16, fallbackColor: '#f4f4f5', basePriceCents: 2600 }),
],
},
// ── Hoodie ─────────────────────────────────────────────────
{
id: P_HOODIE,
slug: 'hoodie',
label: 'Hoodie',
description: 'Heavyweight pullover hoodie with front-and-back print options.',
category: 'Apparel',
options: [
{
id: O_HOODIE_COLOR,
slug: 'color',
label: 'Color',
kind: 'color',
values: [
{ id: V_HOODIE_BLACK, slug: 'black', label: 'Black', hex: '#111827' },
{ id: V_HOODIE_NAVY, slug: 'navy', label: 'Navy', hex: '#1e3a8a' },
{ id: V_HOODIE_GRAY, slug: 'gray', label: 'Heather Gray', hex: '#9ca3af' },
],
},
],
sizes: [
makeRectSize({ id: S_HOODIE_S, slug: 's', label: 'Small', widthIn: 12, heightIn: 16, fallbackColor: '#475569', basePriceCents: 4800 }),
makeRectSize({ id: S_HOODIE_M, slug: 'm', label: 'Medium', widthIn: 12, heightIn: 16, fallbackColor: '#475569', basePriceCents: 4800 }),
makeRectSize({ id: S_HOODIE_L, slug: 'l', label: 'Large', widthIn: 12, heightIn: 16, fallbackColor: '#475569', basePriceCents: 4800 }),
makeRectSize({ id: S_HOODIE_XL, slug: 'xl', label: 'X-Large', widthIn: 12, heightIn: 16, fallbackColor: '#475569', basePriceCents: 5200 }),
makeRectSize({ id: S_HOODIE_XXL, slug: 'xxl', label: 'XX-Large', widthIn: 12, heightIn: 16, fallbackColor: '#475569', basePriceCents: 5400 }),
],
},
// ── Magnet ─────────────────────────────────────────────────
{
id: P_MAGNET,
slug: 'magnet',
label: 'Magnet',
description: 'Refrigerator-safe matte magnet, square or die-cut round.',
category: 'Home',
options: [],
sizes: [
makeRectSize({ id: S_MAGNET_3X3, slug: '3x3-square', label: '3" × 3" Square', widthIn: 3, heightIn: 3, fallbackColor: '#fef9c3', basePriceCents: 700 }),
makeRectSize({ id: S_MAGNET_4X4, slug: '4x4-square', label: '4" × 4" Square', widthIn: 4, heightIn: 4, fallbackColor: '#fef9c3', basePriceCents: 900 }),
makeCircleSize({ id: S_MAGNET_ROUND, slug: '3in-round', label: '3" Round', diameterIn: 3, fallbackColor: '#fde68a', basePriceCents: 700 }),
],
},
// ── Mug ────────────────────────────────────────────────────
{
id: P_MUG,
slug: 'mug',
label: 'Ceramic Mug',
description: 'Dishwasher-safe ceramic mug. Print wraps the body.',
category: 'Drinkware',
options: [],
sizes: [
makeRectSize({ id: S_MUG_11OZ, slug: '11oz', label: '11oz', widthIn: 8, heightIn: 3.25, fallbackColor: '#f8fafc', basePriceCents: 1500 }),
makeRectSize({ id: S_MUG_15OZ, slug: '15oz', label: '15oz', widthIn: 8.5, heightIn: 4.25, fallbackColor: '#f1f5f9', basePriceCents: 1800 }),
],
},
// ── Poster ─────────────────────────────────────────────────
{
id: P_POSTER,
slug: 'poster',
label: 'Poster',
description: 'Premium matte poster, suitable for framing.',
category: 'Wall Art',
options: [],
sizes: [
makeRectSize({ id: S_POSTER_12X18, slug: '12x18', label: '12" × 18"', widthIn: 12, heightIn: 18, fallbackColor: '#f3f4f6', basePriceCents: 1800 }),
makeRectSize({ id: S_POSTER_18X24, slug: '18x24', label: '18" × 24"', widthIn: 18, heightIn: 24, fallbackColor: '#e5e7eb', basePriceCents: 2400 }),
],
},
// ── Tote Bag ───────────────────────────────────────────────
{
id: P_TOTE,
slug: 'tote',
label: 'Tote Bag',
description: 'Sturdy canvas tote with reinforced straps.',
category: 'Accessories',
options: [],
sizes: [
makeRectSize({ id: S_TOTE_14X14, slug: '14x14', label: '14" × 14"', widthIn: 14, heightIn: 14, fallbackColor: '#fef3c7', basePriceCents: 1800 }),
],
},
// ── Pillow ─────────────────────────────────────────────────
{
id: P_PILLOW,
slug: 'pillow',
label: 'Throw Pillow',
description: 'Plush throw pillow with hidden zipper and insert.',
category: 'Home',
options: [],
sizes: [
makeRectSize({ id: S_PILLOW_18, slug: '18x18', label: '18" × 18"', widthIn: 18, heightIn: 18, fallbackColor: '#dcfce7', basePriceCents: 2400, cornerPct: 0.09 }),
],
},
// ── Notebook ───────────────────────────────────────────────
{
id: P_NOTEBOOK,
slug: 'notebook',
label: 'Notebook',
description: 'A5 hardcover notebook, 120 lined pages.',
category: 'Stationery',
options: [],
sizes: [
makeRectSize({ id: S_NOTEBOOK_A5, slug: 'a5', label: 'A5', widthIn: 5.5, heightIn: 8.5, fallbackColor: '#fafaf9', basePriceCents: 1500 }),
],
},
// ── Sticker ────────────────────────────────────────────────
{
id: P_STICKER,
slug: 'sticker',
label: 'Sticker',
description: 'Weatherproof vinyl sticker, die-cut to your design.',
category: 'Stationery',
options: [],
sizes: [
makeRectSize({ id: S_STICKER_3, slug: '3in-die-cut', label: '3" Die-Cut', widthIn: 3, heightIn: 3, fallbackColor: '#fce7f3', basePriceCents: 400 }),
],
},
// ── Hat ────────────────────────────────────────────────────
{
id: P_HAT,
slug: 'hat',
label: 'Hat',
description: 'Adjustable baseball cap with embroidered front print.',
category: 'Apparel',
options: [],
sizes: [
makeRectSize({ id: S_HAT_CAP, slug: 'cap', label: 'Baseball Cap', widthIn: 5, heightIn: 2, fallbackColor: '#fce7f3', basePriceCents: 2200, cornerPct: 0.15 }),
],
},
// ── Phone Case ─────────────────────────────────────────────
//
// v0.6 — per-phone-model sizing is deferred. The single seed size
// here uses a generic 2.7×5.4 inch canvas (~iPhone 13/14/15 wrap
// area). When per-model sizing lands, this product will gain a
// size per phone model (each with its own canvas).
{
id: P_PHONE_CASE,
slug: 'phone-case',
label: 'Phone Case',
description: 'Slim flexible case with edge-to-edge print.',
category: 'Accessories',
options: [],
sizes: [
makeRectSize({ id: S_PHONE_DEFAULT, slug: 'generic', label: 'Generic Wrap', widthIn: 2.7, heightIn: 5.4, fallbackColor: '#e0e7ff', basePriceCents: 2200, cornerPct: 0.1 }),
],
},
// ── Rabbit T-Shirt (v0.7 fixed-kind example) ───────────────
//
// First ready-made product in the catalog. Pre-printed with a rabbit
// illustration; customer picks size + color and adds to cart. No
// editor mount, no templates can be pinned to it (server rejects).
// images[] is empty at seed time — admin uploads gallery photos via
// the product editor's new Photos section.
{
id: P_RABBIT_TEE,
slug: 'rabbit-tee',
label: 'Rabbit T-Shirt',
description: 'Ready-made tee featuring our resident bunny mascot. Pre-printed, soft cotton, ships fast.',
category: 'Apparel',
kind: 'fixed',
options: [
{
id: O_RABBIT_COLOR,
slug: 'color',
label: 'Color',
kind: 'color',
values: [
{ id: V_RABBIT_WHITE, slug: 'white', label: 'White', hex: '#ffffff' },
{ id: V_RABBIT_BLACK, slug: 'black', label: 'Black', hex: '#1f2937' },
{ id: V_RABBIT_PINK, slug: 'pink', label: 'Dusty Pink', hex: '#fbcfe8' },
],
},
],
sizes: [
makeFixedSize({ id: S_RABBIT_S, slug: 's', label: 'Small', widthIn: 12, heightIn: 16, fallbackColor: '#fce7f3', basePriceCents: 2400 }),
makeFixedSize({ id: S_RABBIT_M, slug: 'm', label: 'Medium', widthIn: 12, heightIn: 16, fallbackColor: '#fce7f3', basePriceCents: 2400 }),
makeFixedSize({ id: S_RABBIT_L, slug: 'l', label: 'Large', widthIn: 12, heightIn: 16, fallbackColor: '#fce7f3', basePriceCents: 2400 }),
makeFixedSize({ id: S_RABBIT_XL, slug: 'xl', label: 'X-Large', widthIn: 12, heightIn: 16, fallbackColor: '#fce7f3', basePriceCents: 2600 }),
makeFixedSize({ id: S_RABBIT_XXL, slug: 'xxl', label: 'XX-Large', widthIn: 12, heightIn: 16, fallbackColor: '#fce7f3', basePriceCents: 2800 }),
],
images: [],
},
];
// ── Convenience exports ────────────────────────────────────────
//
// Hosts using the legacy default-product flow at `/` still expect a
// pair of `defaultProductId` + `defaultSizeId` they can hand to the
// editor. Pre-v0.6 these were string slugs; v0.6 they're UUIDs. The
// legacy App.jsx in apparel-designer reads these via the host
// constants module (src/constants/products.js) which now re-exports
// them from here.
export const DEFAULT_PRODUCT_ID = P_SHIRT;
export const DEFAULT_SIZE_ID = S_SHIRT_M;

View File

@@ -0,0 +1,28 @@
/**
* Template category catalog. Lifted from the legacy
* goods-editor TEMPLATE_CATEGORIES constant — the 8 categories that
* the original 8 templates used. Hosts can add categories ad-hoc
* (templates store category as a free-text field), but having a
* curated list makes the picker UI cleaner.
*
* The plan defers the more sophisticated "categories per product
* type" structure to a future iteration; v1 is a flat list.
*/
export const TEMPLATE_CATEGORIES = [
'All',
'Sports',
'Music',
'Quotes',
'Animals',
'Abstract',
'Vintage',
'Nature',
'Tech',
];
/**
* Default category for newly-created templates when admin hasn't
* picked one explicitly.
*/
export const DEFAULT_CATEGORY = null;

View File

@@ -0,0 +1,104 @@
{
"products": {
"c3ac22c7-ef49-4997-a525-d4f0eb32da6f": {
"id": "c3ac22c7-ef49-4997-a525-d4f0eb32da6f",
"slug": "shirt",
"label": "T-Shirt",
"category": "Apparel",
"sizeCount": 5,
"createdAt": "2026-05-25T12:31:21.547Z",
"updatedAt": "2026-05-25T12:31:21.547Z"
},
"6202b99a-a4a9-4f11-8b2d-32c17516ecfa": {
"id": "6202b99a-a4a9-4f11-8b2d-32c17516ecfa",
"slug": "hoodie",
"label": "Hoodie",
"category": "Apparel",
"sizeCount": 5,
"createdAt": "2026-05-25T12:31:21.552Z",
"updatedAt": "2026-05-25T12:31:21.552Z"
},
"f530dc06-ef10-46b4-9903-3927f85bd9a7": {
"id": "f530dc06-ef10-46b4-9903-3927f85bd9a7",
"slug": "magnet",
"label": "Magnet",
"category": "Home",
"sizeCount": 3,
"createdAt": "2026-05-25T12:31:21.554Z",
"updatedAt": "2026-05-25T12:31:21.554Z"
},
"951cc0aa-6b28-460a-ad31-8ac8fdcc5ebd": {
"id": "951cc0aa-6b28-460a-ad31-8ac8fdcc5ebd",
"slug": "mug",
"label": "Ceramic Mug",
"category": "Drinkware",
"sizeCount": 2,
"createdAt": "2026-05-25T12:31:21.555Z",
"updatedAt": "2026-05-25T12:31:21.555Z"
},
"95dca14e-28e4-46e8-93b1-b45e3b556530": {
"id": "95dca14e-28e4-46e8-93b1-b45e3b556530",
"slug": "poster",
"label": "Poster",
"category": "Wall Art",
"sizeCount": 2,
"createdAt": "2026-05-25T12:31:21.556Z",
"updatedAt": "2026-05-25T12:31:21.556Z"
},
"1d69616b-96a8-4280-9beb-c8926222c589": {
"id": "1d69616b-96a8-4280-9beb-c8926222c589",
"slug": "tote",
"label": "Tote Bag",
"category": "Accessories",
"sizeCount": 1,
"createdAt": "2026-05-25T12:31:21.557Z",
"updatedAt": "2026-05-25T12:31:21.557Z"
},
"c722b19b-d6c6-4639-b0eb-dec34731e50d": {
"id": "c722b19b-d6c6-4639-b0eb-dec34731e50d",
"slug": "pillow",
"label": "Throw Pillow",
"category": "Home",
"sizeCount": 1,
"createdAt": "2026-05-25T12:31:21.558Z",
"updatedAt": "2026-05-25T12:31:21.558Z"
},
"1ed028ad-1998-4ace-813b-79023bd08c1d": {
"id": "1ed028ad-1998-4ace-813b-79023bd08c1d",
"slug": "notebook",
"label": "Notebook",
"category": "Stationery",
"sizeCount": 1,
"createdAt": "2026-05-25T12:31:21.559Z",
"updatedAt": "2026-05-25T12:31:21.559Z"
},
"04507704-50a9-4de9-9b23-475d6b76bf0e": {
"id": "04507704-50a9-4de9-9b23-475d6b76bf0e",
"slug": "sticker",
"label": "Sticker",
"category": "Stationery",
"sizeCount": 1,
"createdAt": "2026-05-25T12:31:21.560Z",
"updatedAt": "2026-05-25T12:31:21.560Z"
},
"cb43b749-05c0-4d1c-8d0b-dadd9b28574d": {
"id": "cb43b749-05c0-4d1c-8d0b-dadd9b28574d",
"slug": "hat",
"label": "Hat",
"category": "Apparel",
"sizeCount": 1,
"createdAt": "2026-05-25T12:31:21.562Z",
"updatedAt": "2026-05-25T12:31:21.562Z"
},
"f5cbb673-8696-4dad-a63c-3a802ba53fd3": {
"id": "f5cbb673-8696-4dad-a63c-3a802ba53fd3",
"slug": "phone-case",
"label": "Phone Case",
"category": "Accessories",
"sizeCount": 1,
"createdAt": "2026-05-25T12:31:21.563Z",
"updatedAt": "2026-05-25T12:31:21.563Z"
}
},
"version": 1
}

View File

@@ -0,0 +1,35 @@
{
"id": "04507704-50a9-4de9-9b23-475d6b76bf0e",
"slug": "sticker",
"label": "Sticker",
"description": "Weatherproof vinyl sticker, die-cut to your design.",
"category": "Stationery",
"options": [],
"sizes": [
{
"id": "6744bd80-e64b-4351-a0b8-9316dd138db5",
"slug": "3in-die-cut",
"label": "3\" Die-Cut",
"physical": {
"width": 3,
"height": 3,
"unit": "in"
},
"canvas": {
"width": 300,
"height": 300
},
"printZone": {
"x": 30,
"y": 30,
"width": 240,
"height": 240
},
"silhouettePath": "M12,0 L288,0 A12,12 0 0 1 300,12 L300,288 A12,12 0 0 1 288,300 L12,300 A12,12 0 0 1 0,288 L0,12 A12,12 0 0 1 12,0 Z",
"mockupFallbackColor": "#fce7f3",
"basePriceCents": 400
}
],
"createdAt": "2026-05-25T12:31:21.560Z",
"updatedAt": "2026-05-25T12:31:21.560Z"
}

View File

@@ -0,0 +1,35 @@
{
"id": "1d69616b-96a8-4280-9beb-c8926222c589",
"slug": "tote",
"label": "Tote Bag",
"description": "Sturdy canvas tote with reinforced straps.",
"category": "Accessories",
"options": [],
"sizes": [
{
"id": "61e5519d-1230-4397-a795-7303a30e0881",
"slug": "14x14",
"label": "14\" × 14\"",
"physical": {
"width": 14,
"height": 14,
"unit": "in"
},
"canvas": {
"width": 1400,
"height": 1400
},
"printZone": {
"x": 140,
"y": 140,
"width": 1120,
"height": 1120
},
"silhouettePath": "M56,0 L1344,0 A56,56 0 0 1 1400,56 L1400,1344 A56,56 0 0 1 1344,1400 L56,1400 A56,56 0 0 1 0,1344 L0,56 A56,56 0 0 1 56,0 Z",
"mockupFallbackColor": "#fef3c7",
"basePriceCents": 1800
}
],
"createdAt": "2026-05-25T12:31:21.557Z",
"updatedAt": "2026-05-25T12:31:21.557Z"
}

View File

@@ -0,0 +1,35 @@
{
"id": "1ed028ad-1998-4ace-813b-79023bd08c1d",
"slug": "notebook",
"label": "Notebook",
"description": "A5 hardcover notebook, 120 lined pages.",
"category": "Stationery",
"options": [],
"sizes": [
{
"id": "add070b4-1fa3-47bb-84a5-df8d99c2b821",
"slug": "a5",
"label": "A5",
"physical": {
"width": 5.5,
"height": 8.5,
"unit": "in"
},
"canvas": {
"width": 550,
"height": 850
},
"printZone": {
"x": 55,
"y": 85,
"width": 440,
"height": 680
},
"silhouettePath": "M22,0 L528,0 A22,22 0 0 1 550,22 L550,828 A22,22 0 0 1 528,850 L22,850 A22,22 0 0 1 0,828 L0,22 A22,22 0 0 1 22,0 Z",
"mockupFallbackColor": "#fafaf9",
"basePriceCents": 1500
}
],
"createdAt": "2026-05-25T12:31:21.559Z",
"updatedAt": "2026-05-25T12:31:21.559Z"
}

View File

@@ -0,0 +1,154 @@
{
"id": "6202b99a-a4a9-4f11-8b2d-32c17516ecfa",
"slug": "hoodie",
"label": "Hoodie",
"description": "Heavyweight pullover hoodie with front-and-back print options.",
"category": "Apparel",
"options": [
{
"id": "49a9bd93-84c9-4e9c-a4e6-c1e742c6777a",
"slug": "color",
"label": "Color",
"kind": "color",
"values": [
{
"id": "4e8d2854-8f0e-4c99-b854-8c0e07b59a49",
"slug": "black",
"label": "Black",
"hex": "#111827"
},
{
"id": "618c49fc-9fc6-486b-9d83-a2fff373092f",
"slug": "navy",
"label": "Navy",
"hex": "#1e3a8a"
},
{
"id": "0d1d8ec2-c7b9-40c9-9da8-9b5f1c6d1e3f",
"slug": "gray",
"label": "Heather Gray",
"hex": "#9ca3af"
}
]
}
],
"sizes": [
{
"id": "f002d98e-6601-46cb-9ed0-ad6608a794ac",
"slug": "s",
"label": "Small",
"physical": {
"width": 12,
"height": 16,
"unit": "in"
},
"canvas": {
"width": 1200,
"height": 1600
},
"printZone": {
"x": 120,
"y": 160,
"width": 960,
"height": 1280
},
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
"mockupFallbackColor": "#475569",
"basePriceCents": 4800
},
{
"id": "13849307-bed4-4654-9476-fb135561f05f",
"slug": "m",
"label": "Medium",
"physical": {
"width": 12,
"height": 16,
"unit": "in"
},
"canvas": {
"width": 1200,
"height": 1600
},
"printZone": {
"x": 120,
"y": 160,
"width": 960,
"height": 1280
},
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
"mockupFallbackColor": "#475569",
"basePriceCents": 4800
},
{
"id": "b327d6c5-8f0b-4639-b8ec-8d3f4ec74d1a",
"slug": "l",
"label": "Large",
"physical": {
"width": 12,
"height": 16,
"unit": "in"
},
"canvas": {
"width": 1200,
"height": 1600
},
"printZone": {
"x": 120,
"y": 160,
"width": 960,
"height": 1280
},
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
"mockupFallbackColor": "#475569",
"basePriceCents": 4800
},
{
"id": "af9ed947-e705-4205-87b0-6c529ce6b342",
"slug": "xl",
"label": "X-Large",
"physical": {
"width": 12,
"height": 16,
"unit": "in"
},
"canvas": {
"width": 1200,
"height": 1600
},
"printZone": {
"x": 120,
"y": 160,
"width": 960,
"height": 1280
},
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
"mockupFallbackColor": "#475569",
"basePriceCents": 5200
},
{
"id": "83156320-5f30-4cf8-b28c-3f69fcf7887e",
"slug": "xxl",
"label": "XX-Large",
"physical": {
"width": 12,
"height": 16,
"unit": "in"
},
"canvas": {
"width": 1200,
"height": 1600
},
"printZone": {
"x": 120,
"y": 160,
"width": 960,
"height": 1280
},
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
"mockupFallbackColor": "#475569",
"basePriceCents": 5400
}
],
"createdAt": "2026-05-25T12:31:21.552Z",
"updatedAt": "2026-05-25T12:31:21.552Z"
}

View File

@@ -0,0 +1,58 @@
{
"id": "951cc0aa-6b28-460a-ad31-8ac8fdcc5ebd",
"slug": "mug",
"label": "Ceramic Mug",
"description": "Dishwasher-safe ceramic mug. Print wraps the body.",
"category": "Drinkware",
"options": [],
"sizes": [
{
"id": "ccaf7033-3e43-4171-9cde-fe6be676ea05",
"slug": "11oz",
"label": "11oz",
"physical": {
"width": 8,
"height": 3.25,
"unit": "in"
},
"canvas": {
"width": 800,
"height": 325
},
"printZone": {
"x": 80,
"y": 33,
"width": 640,
"height": 259
},
"silhouettePath": "M13,0 L787,0 A13,13 0 0 1 800,13 L800,312 A13,13 0 0 1 787,325 L13,325 A13,13 0 0 1 0,312 L0,13 A13,13 0 0 1 13,0 Z",
"mockupFallbackColor": "#f8fafc",
"basePriceCents": 1500
},
{
"id": "ae4ad837-6669-4491-af8e-445f90794779",
"slug": "15oz",
"label": "15oz",
"physical": {
"width": 8.5,
"height": 4.25,
"unit": "in"
},
"canvas": {
"width": 850,
"height": 425
},
"printZone": {
"x": 85,
"y": 43,
"width": 680,
"height": 339
},
"silhouettePath": "M17,0 L833,0 A17,17 0 0 1 850,17 L850,408 A17,17 0 0 1 833,425 L17,425 A17,17 0 0 1 0,408 L0,17 A17,17 0 0 1 17,0 Z",
"mockupFallbackColor": "#f1f5f9",
"basePriceCents": 1800
}
],
"createdAt": "2026-05-25T12:31:21.555Z",
"updatedAt": "2026-05-25T12:31:21.555Z"
}

View File

@@ -0,0 +1,58 @@
{
"id": "95dca14e-28e4-46e8-93b1-b45e3b556530",
"slug": "poster",
"label": "Poster",
"description": "Premium matte poster, suitable for framing.",
"category": "Wall Art",
"options": [],
"sizes": [
{
"id": "7d015072-f884-4e04-8a39-ce7c8e149059",
"slug": "12x18",
"label": "12\" × 18\"",
"physical": {
"width": 12,
"height": 18,
"unit": "in"
},
"canvas": {
"width": 1200,
"height": 1800
},
"printZone": {
"x": 120,
"y": 180,
"width": 960,
"height": 1440
},
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1752 A48,48 0 0 1 1152,1800 L48,1800 A48,48 0 0 1 0,1752 L0,48 A48,48 0 0 1 48,0 Z",
"mockupFallbackColor": "#f3f4f6",
"basePriceCents": 1800
},
{
"id": "9a5529fd-e54e-4868-acbb-5a62c44d1297",
"slug": "18x24",
"label": "18\" × 24\"",
"physical": {
"width": 18,
"height": 24,
"unit": "in"
},
"canvas": {
"width": 1800,
"height": 2400
},
"printZone": {
"x": 180,
"y": 240,
"width": 1440,
"height": 1920
},
"silhouettePath": "M72,0 L1728,0 A72,72 0 0 1 1800,72 L1800,2328 A72,72 0 0 1 1728,2400 L72,2400 A72,72 0 0 1 0,2328 L0,72 A72,72 0 0 1 72,0 Z",
"mockupFallbackColor": "#e5e7eb",
"basePriceCents": 2400
}
],
"createdAt": "2026-05-25T12:31:21.556Z",
"updatedAt": "2026-05-25T12:31:21.556Z"
}

View File

@@ -0,0 +1,154 @@
{
"id": "c3ac22c7-ef49-4997-a525-d4f0eb32da6f",
"slug": "shirt",
"label": "T-Shirt",
"description": "Soft cotton tee, printed on the front.",
"category": "Apparel",
"options": [
{
"id": "9d58a8b1-5976-4512-9095-883f16a4c1b8",
"slug": "color",
"label": "Color",
"kind": "color",
"values": [
{
"id": "29092f97-2654-4150-89e5-7a323eb891e4",
"slug": "white",
"label": "White",
"hex": "#ffffff"
},
{
"id": "c25f746a-efc2-4213-9328-728ac6881bc6",
"slug": "black",
"label": "Black",
"hex": "#1f2937"
},
{
"id": "8e077db8-bc48-45c0-a3e1-df10b6f1367f",
"slug": "pink",
"label": "Dusty Pink",
"hex": "#fbcfe8"
}
]
}
],
"sizes": [
{
"id": "3df58f51-70ce-4898-8091-2bb186b04193",
"slug": "s",
"label": "Small",
"physical": {
"width": 12,
"height": 16,
"unit": "in"
},
"canvas": {
"width": 1200,
"height": 1600
},
"printZone": {
"x": 120,
"y": 160,
"width": 960,
"height": 1280
},
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
"mockupFallbackColor": "#f4f4f5",
"basePriceCents": 2200
},
{
"id": "86787c88-c4e0-4798-a34f-d1e173b2704b",
"slug": "m",
"label": "Medium",
"physical": {
"width": 12,
"height": 16,
"unit": "in"
},
"canvas": {
"width": 1200,
"height": 1600
},
"printZone": {
"x": 120,
"y": 160,
"width": 960,
"height": 1280
},
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
"mockupFallbackColor": "#f4f4f5",
"basePriceCents": 2200
},
{
"id": "8cf5377f-8c6b-4afe-80c3-9337e6303176",
"slug": "l",
"label": "Large",
"physical": {
"width": 12,
"height": 16,
"unit": "in"
},
"canvas": {
"width": 1200,
"height": 1600
},
"printZone": {
"x": 120,
"y": 160,
"width": 960,
"height": 1280
},
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
"mockupFallbackColor": "#f4f4f5",
"basePriceCents": 2200
},
{
"id": "ecc7707a-1703-4c27-9da7-6b39709837ba",
"slug": "xl",
"label": "X-Large",
"physical": {
"width": 12,
"height": 16,
"unit": "in"
},
"canvas": {
"width": 1200,
"height": 1600
},
"printZone": {
"x": 120,
"y": 160,
"width": 960,
"height": 1280
},
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
"mockupFallbackColor": "#f4f4f5",
"basePriceCents": 2400
},
{
"id": "3667ec33-6123-44bd-a12e-5fdfef3c2874",
"slug": "xxl",
"label": "XX-Large",
"physical": {
"width": 12,
"height": 16,
"unit": "in"
},
"canvas": {
"width": 1200,
"height": 1600
},
"printZone": {
"x": 120,
"y": 160,
"width": 960,
"height": 1280
},
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
"mockupFallbackColor": "#f4f4f5",
"basePriceCents": 2600
}
],
"createdAt": "2026-05-25T12:31:21.547Z",
"updatedAt": "2026-05-25T12:31:21.547Z"
}

View File

@@ -0,0 +1,35 @@
{
"id": "c722b19b-d6c6-4639-b0eb-dec34731e50d",
"slug": "pillow",
"label": "Throw Pillow",
"description": "Plush throw pillow with hidden zipper and insert.",
"category": "Home",
"options": [],
"sizes": [
{
"id": "22d04708-d964-4cdc-a682-fa9c02372a88",
"slug": "18x18",
"label": "18\" × 18\"",
"physical": {
"width": 18,
"height": 18,
"unit": "in"
},
"canvas": {
"width": 1800,
"height": 1800
},
"printZone": {
"x": 180,
"y": 180,
"width": 1440,
"height": 1440
},
"silhouettePath": "M162,0 L1638,0 A162,162 0 0 1 1800,162 L1800,1638 A162,162 0 0 1 1638,1800 L162,1800 A162,162 0 0 1 0,1638 L0,162 A162,162 0 0 1 162,0 Z",
"mockupFallbackColor": "#dcfce7",
"basePriceCents": 2400
}
],
"createdAt": "2026-05-25T12:31:21.558Z",
"updatedAt": "2026-05-25T12:31:21.558Z"
}

View File

@@ -0,0 +1,35 @@
{
"id": "cb43b749-05c0-4d1c-8d0b-dadd9b28574d",
"slug": "hat",
"label": "Hat",
"description": "Adjustable baseball cap with embroidered front print.",
"category": "Apparel",
"options": [],
"sizes": [
{
"id": "778c4b47-a46b-4753-8fa2-be8af1523f4a",
"slug": "cap",
"label": "Baseball Cap",
"physical": {
"width": 5,
"height": 2,
"unit": "in"
},
"canvas": {
"width": 500,
"height": 200
},
"printZone": {
"x": 50,
"y": 20,
"width": 400,
"height": 160
},
"silhouettePath": "M30,0 L470,0 A30,30 0 0 1 500,30 L500,170 A30,30 0 0 1 470,200 L30,200 A30,30 0 0 1 0,170 L0,30 A30,30 0 0 1 30,0 Z",
"mockupFallbackColor": "#fce7f3",
"basePriceCents": 2200
}
],
"createdAt": "2026-05-25T12:31:21.562Z",
"updatedAt": "2026-05-25T12:31:21.562Z"
}

View File

@@ -0,0 +1,81 @@
{
"id": "f530dc06-ef10-46b4-9903-3927f85bd9a7",
"slug": "magnet",
"label": "Magnet",
"description": "Refrigerator-safe matte magnet, square or die-cut round.",
"category": "Home",
"options": [],
"sizes": [
{
"id": "6f88ed75-7ab5-49ae-8dc5-18568da63899",
"slug": "3x3-square",
"label": "3\" × 3\" Square",
"physical": {
"width": 3,
"height": 3,
"unit": "in"
},
"canvas": {
"width": 300,
"height": 300
},
"printZone": {
"x": 30,
"y": 30,
"width": 240,
"height": 240
},
"silhouettePath": "M12,0 L288,0 A12,12 0 0 1 300,12 L300,288 A12,12 0 0 1 288,300 L12,300 A12,12 0 0 1 0,288 L0,12 A12,12 0 0 1 12,0 Z",
"mockupFallbackColor": "#fef9c3",
"basePriceCents": 700
},
{
"id": "3a9d60f5-821c-4f3f-8d41-62cfeaba208a",
"slug": "4x4-square",
"label": "4\" × 4\" Square",
"physical": {
"width": 4,
"height": 4,
"unit": "in"
},
"canvas": {
"width": 400,
"height": 400
},
"printZone": {
"x": 40,
"y": 40,
"width": 320,
"height": 320
},
"silhouettePath": "M16,0 L384,0 A16,16 0 0 1 400,16 L400,384 A16,16 0 0 1 384,400 L16,400 A16,16 0 0 1 0,384 L0,16 A16,16 0 0 1 16,0 Z",
"mockupFallbackColor": "#fef9c3",
"basePriceCents": 900
},
{
"id": "17e3cc66-f897-4717-8b08-c5b0308668df",
"slug": "3in-round",
"label": "3\" Round",
"physical": {
"width": 3,
"height": 3,
"unit": "in"
},
"canvas": {
"width": 300,
"height": 300
},
"printZone": {
"x": 30,
"y": 30,
"width": 240,
"height": 240
},
"silhouettePath": "M150,0 A150,150 0 1 0 150,300 A150,150 0 1 0 150,0 Z",
"mockupFallbackColor": "#fde68a",
"basePriceCents": 700
}
],
"createdAt": "2026-05-25T12:31:21.554Z",
"updatedAt": "2026-05-25T12:31:21.554Z"
}

View File

@@ -0,0 +1,35 @@
{
"id": "f5cbb673-8696-4dad-a63c-3a802ba53fd3",
"slug": "phone-case",
"label": "Phone Case",
"description": "Slim flexible case with edge-to-edge print.",
"category": "Accessories",
"options": [],
"sizes": [
{
"id": "4be821c2-8ba5-4dd1-ad0b-1a188dd89796",
"slug": "generic",
"label": "Generic Wrap",
"physical": {
"width": 2.7,
"height": 5.4,
"unit": "in"
},
"canvas": {
"width": 270,
"height": 540
},
"printZone": {
"x": 27,
"y": 54,
"width": 216,
"height": 432
},
"silhouettePath": "M27,0 L243,0 A27,27 0 0 1 270,27 L270,513 A27,27 0 0 1 243,540 L27,540 A27,27 0 0 1 0,513 L0,27 A27,27 0 0 1 27,0 Z",
"mockupFallbackColor": "#e0e7ff",
"basePriceCents": 2200
}
],
"createdAt": "2026-05-25T12:31:21.563Z",
"updatedAt": "2026-05-25T12:31:21.563Z"
}