From aba2dbedfb0aa9a164826f7537eefa51cd54f6a9 Mon Sep 17 00:00:00 2001 From: "khalid@traclabs.com" Date: Tue, 1 Sep 2026 23:28:48 -0500 Subject: [PATCH] v0.6: template-manager owns the product catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//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 --- docs/template-schema.md | 27 ++ src/constants/phoneModels.js | 174 +++++++ src/constants/products.js | 132 +++--- src/constants/seedProducts.js | 445 ++++++++++++++++++ src/constants/templateCategories.js | 28 ++ templates/_products_index.json | 104 ++++ .../product.json | 35 ++ .../product.json | 35 ++ .../product.json | 35 ++ .../product.json | 154 ++++++ .../product.json | 58 +++ .../product.json | 58 +++ .../product.json | 154 ++++++ .../product.json | 35 ++ .../product.json | 35 ++ .../product.json | 81 ++++ .../product.json | 35 ++ 17 files changed, 1549 insertions(+), 76 deletions(-) create mode 100644 src/constants/phoneModels.js create mode 100644 src/constants/seedProducts.js create mode 100644 src/constants/templateCategories.js create mode 100644 templates/_products_index.json create mode 100644 templates/products/04507704-50a9-4de9-9b23-475d6b76bf0e/product.json create mode 100644 templates/products/1d69616b-96a8-4280-9beb-c8926222c589/product.json create mode 100644 templates/products/1ed028ad-1998-4ace-813b-79023bd08c1d/product.json create mode 100644 templates/products/6202b99a-a4a9-4f11-8b2d-32c17516ecfa/product.json create mode 100644 templates/products/951cc0aa-6b28-460a-ad31-8ac8fdcc5ebd/product.json create mode 100644 templates/products/95dca14e-28e4-46e8-93b1-b45e3b556530/product.json create mode 100644 templates/products/c3ac22c7-ef49-4997-a525-d4f0eb32da6f/product.json create mode 100644 templates/products/c722b19b-d6c6-4639-b0eb-dec34731e50d/product.json create mode 100644 templates/products/cb43b749-05c0-4d1c-8d0b-dadd9b28574d/product.json create mode 100644 templates/products/f530dc06-ef10-46b4-9903-3927f85bd9a7/product.json create mode 100644 templates/products/f5cbb673-8696-4dad-a63c-3a802ba53fd3/product.json diff --git a/docs/template-schema.md b/docs/template-schema.md index 9d1b1b5..393e02e 100644 --- a/docs/template-schema.md +++ b/docs/template-schema.md @@ -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 diff --git a/src/constants/phoneModels.js b/src/constants/phoneModels.js new file mode 100644 index 0000000..de33cd5 --- /dev/null +++ b/src/constants/phoneModels.js @@ -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)); +} diff --git a/src/constants/products.js b/src/constants/products.js index a1ce40d..ab8ccd3 100644 --- a/src/constants/products.js +++ b/src/constants/products.js @@ -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; } diff --git a/src/constants/seedProducts.js b/src/constants/seedProducts.js new file mode 100644 index 0000000..dc0ff23 --- /dev/null +++ b/src/constants/seedProducts.js @@ -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_ for products, S__ for sizes, +// O__ for option groups, V___ +// 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; diff --git a/src/constants/templateCategories.js b/src/constants/templateCategories.js new file mode 100644 index 0000000..69493a4 --- /dev/null +++ b/src/constants/templateCategories.js @@ -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; diff --git a/templates/_products_index.json b/templates/_products_index.json new file mode 100644 index 0000000..3dd2cdc --- /dev/null +++ b/templates/_products_index.json @@ -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 +} diff --git a/templates/products/04507704-50a9-4de9-9b23-475d6b76bf0e/product.json b/templates/products/04507704-50a9-4de9-9b23-475d6b76bf0e/product.json new file mode 100644 index 0000000..9079f39 --- /dev/null +++ b/templates/products/04507704-50a9-4de9-9b23-475d6b76bf0e/product.json @@ -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" +} diff --git a/templates/products/1d69616b-96a8-4280-9beb-c8926222c589/product.json b/templates/products/1d69616b-96a8-4280-9beb-c8926222c589/product.json new file mode 100644 index 0000000..fe82ad9 --- /dev/null +++ b/templates/products/1d69616b-96a8-4280-9beb-c8926222c589/product.json @@ -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" +} diff --git a/templates/products/1ed028ad-1998-4ace-813b-79023bd08c1d/product.json b/templates/products/1ed028ad-1998-4ace-813b-79023bd08c1d/product.json new file mode 100644 index 0000000..1143d3d --- /dev/null +++ b/templates/products/1ed028ad-1998-4ace-813b-79023bd08c1d/product.json @@ -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" +} diff --git a/templates/products/6202b99a-a4a9-4f11-8b2d-32c17516ecfa/product.json b/templates/products/6202b99a-a4a9-4f11-8b2d-32c17516ecfa/product.json new file mode 100644 index 0000000..1dda0b4 --- /dev/null +++ b/templates/products/6202b99a-a4a9-4f11-8b2d-32c17516ecfa/product.json @@ -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" +} diff --git a/templates/products/951cc0aa-6b28-460a-ad31-8ac8fdcc5ebd/product.json b/templates/products/951cc0aa-6b28-460a-ad31-8ac8fdcc5ebd/product.json new file mode 100644 index 0000000..6e5e40f --- /dev/null +++ b/templates/products/951cc0aa-6b28-460a-ad31-8ac8fdcc5ebd/product.json @@ -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" +} diff --git a/templates/products/95dca14e-28e4-46e8-93b1-b45e3b556530/product.json b/templates/products/95dca14e-28e4-46e8-93b1-b45e3b556530/product.json new file mode 100644 index 0000000..7c165c0 --- /dev/null +++ b/templates/products/95dca14e-28e4-46e8-93b1-b45e3b556530/product.json @@ -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" +} diff --git a/templates/products/c3ac22c7-ef49-4997-a525-d4f0eb32da6f/product.json b/templates/products/c3ac22c7-ef49-4997-a525-d4f0eb32da6f/product.json new file mode 100644 index 0000000..616937f --- /dev/null +++ b/templates/products/c3ac22c7-ef49-4997-a525-d4f0eb32da6f/product.json @@ -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" +} diff --git a/templates/products/c722b19b-d6c6-4639-b0eb-dec34731e50d/product.json b/templates/products/c722b19b-d6c6-4639-b0eb-dec34731e50d/product.json new file mode 100644 index 0000000..90630ad --- /dev/null +++ b/templates/products/c722b19b-d6c6-4639-b0eb-dec34731e50d/product.json @@ -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" +} diff --git a/templates/products/cb43b749-05c0-4d1c-8d0b-dadd9b28574d/product.json b/templates/products/cb43b749-05c0-4d1c-8d0b-dadd9b28574d/product.json new file mode 100644 index 0000000..3044130 --- /dev/null +++ b/templates/products/cb43b749-05c0-4d1c-8d0b-dadd9b28574d/product.json @@ -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" +} diff --git a/templates/products/f530dc06-ef10-46b4-9903-3927f85bd9a7/product.json b/templates/products/f530dc06-ef10-46b4-9903-3927f85bd9a7/product.json new file mode 100644 index 0000000..38eb053 --- /dev/null +++ b/templates/products/f530dc06-ef10-46b4-9903-3927f85bd9a7/product.json @@ -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" +} diff --git a/templates/products/f5cbb673-8696-4dad-a63c-3a802ba53fd3/product.json b/templates/products/f5cbb673-8696-4dad-a63c-3a802ba53fd3/product.json new file mode 100644 index 0000000..af05646 --- /dev/null +++ b/templates/products/f5cbb673-8696-4dad-a63c-3a802ba53fd3/product.json @@ -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" +}