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>
267 lines
7.8 KiB
Markdown
267 lines
7.8 KiB
Markdown
# Template JSON Schema
|
||
|
||
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
|
||
{
|
||
"id": "string (unique identifier)",
|
||
"name": "string (display name)",
|
||
"category": "string (Sports|Music|Quotes|Animals|Abstract|Vintage|Nature|Tech)",
|
||
"description": "string (short description)",
|
||
"elements": [
|
||
{
|
||
"type": "text" | "image",
|
||
"text": "string (for text elements only)",
|
||
"x": "number (x position in canvas units)",
|
||
"y": "number (y position in canvas units)",
|
||
"fontSize": "number (for text elements)",
|
||
"fontFamily": "string (Google Font name)",
|
||
"fill": "string (hex color)",
|
||
"rotation": "number (degrees, -180 to 180)",
|
||
"width": "number (for image elements)",
|
||
"height": "number (for image elements)",
|
||
"src": "string (image URL or path)"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
## Element Properties
|
||
|
||
### Text Element
|
||
|
||
| Property | Type | Required | Description |
|
||
|------------|--------|----------|---------------------------------------|
|
||
| `type` | string | Yes | Must be `"text"` |
|
||
| `text` | string | Yes | The text content to display |
|
||
| `x` | number | Yes | X position on canvas (0-300) |
|
||
| `y` | number | Yes | Y position on canvas (0-300) |
|
||
| `fontSize` | number | Yes | Font size in pixels (12-120) |
|
||
| `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
|
||
|
||
| Property | Type | Required | Description |
|
||
|------------|--------|----------|---------------------------------------|
|
||
| `type` | string | Yes | Must be `"image"` |
|
||
| `src` | string | Yes | Image URL or server path |
|
||
| `x` | number | Yes | X position on canvas (0-300) |
|
||
| `y` | number | Yes | Y position on canvas (0-300) |
|
||
| `width` | number | Yes | Width in canvas units (min: 20) |
|
||
| `height` | number | Yes | Height in canvas units (min: 20) |
|
||
| `rotation` | number | No | Rotation in degrees (default: 0) |
|
||
|
||
## Advanced Template Features
|
||
|
||
### Template with Background and Overlay
|
||
|
||
For templates that include background images and overlay elements:
|
||
|
||
```json
|
||
{
|
||
"id": "team-sport",
|
||
"name": "Team Sport",
|
||
"category": "Sports",
|
||
"description": "Classic team jersey with number and text",
|
||
"background": {
|
||
"type": "color" | "image",
|
||
"color": "string (hex, if type is color)",
|
||
"src": "string (URL/path, if type is image)"
|
||
},
|
||
"overlay": [
|
||
{
|
||
"type": "text" | "image",
|
||
"nonPrintable": false,
|
||
"...": "same properties as regular elements"
|
||
}
|
||
],
|
||
"elements": [...]
|
||
}
|
||
```
|
||
|
||
### Slot-Based Templates
|
||
|
||
For templates with image slots (auto-crop regions):
|
||
|
||
```json
|
||
{
|
||
"id": "classic-tee-front",
|
||
"name": "Classic Tee - Front",
|
||
"slots": [
|
||
{
|
||
"id": "chest-logo",
|
||
"bounds": { "x": 100, "y": 80, "width": 100, "height": 100 },
|
||
"aspectRatio": 1.0,
|
||
"label": "Chest Logo"
|
||
},
|
||
{
|
||
"id": "sleeve-left",
|
||
"bounds": { "x": 20, "y": 100, "width": 60, "height": 60 },
|
||
"aspectRatio": 1.0,
|
||
"label": "Left Sleeve"
|
||
}
|
||
],
|
||
"elements": []
|
||
}
|
||
```
|
||
|
||
### Slot Crop Region
|
||
|
||
When an image is assigned to a slot, the crop property is applied:
|
||
|
||
```json
|
||
{
|
||
"type": "image",
|
||
"src": "/uploads/image.png",
|
||
"x": 100,
|
||
"y": 80,
|
||
"width": 100,
|
||
"height": 100,
|
||
"crop": {
|
||
"sx": 0,
|
||
"sy": 0,
|
||
"sWidth": 500,
|
||
"sHeight": 500
|
||
}
|
||
}
|
||
```
|
||
|
||
| Property | Type | Description |
|
||
|-----------|--------|--------------------------------------|
|
||
| `sx` | number | Source x coordinate for crop |
|
||
| `sy` | number | Source y coordinate for crop |
|
||
| `sWidth` | number | Source width for crop |
|
||
| `sHeight` | number | Source height for crop |
|
||
|
||
## Non-Printable Elements
|
||
|
||
Elements can be marked as non-printable to exclude them from export:
|
||
|
||
```json
|
||
{
|
||
"type": "text",
|
||
"text": "Guide Text",
|
||
"nonPrintable": true,
|
||
"x": 150,
|
||
"y": 150,
|
||
"fontSize": 12,
|
||
"fill": "#cccccc"
|
||
}
|
||
```
|
||
|
||
## Example Templates
|
||
|
||
### Minimal Quote Template
|
||
|
||
```json
|
||
{
|
||
"id": "minimal-quote",
|
||
"name": "Minimal Quote",
|
||
"category": "Quotes",
|
||
"description": "Simple centered quote design",
|
||
"elements": [
|
||
{
|
||
"type": "text",
|
||
"text": "\"Be the change\"",
|
||
"x": 150,
|
||
"y": 130,
|
||
"fontSize": 24,
|
||
"fontFamily": "Georgia",
|
||
"fill": "#1e293b",
|
||
"rotation": 0
|
||
},
|
||
{
|
||
"type": "text",
|
||
"text": "you wish to see",
|
||
"x": 150,
|
||
"y": 160,
|
||
"fontSize": 18,
|
||
"fontFamily": "Arial",
|
||
"fill": "#64748b",
|
||
"rotation": 0
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### Team Sport Template
|
||
|
||
```json
|
||
{
|
||
"id": "team-sport",
|
||
"name": "Team Sport",
|
||
"category": "Sports",
|
||
"description": "Classic team jersey with number and text",
|
||
"elements": [
|
||
{
|
||
"type": "text",
|
||
"text": "TEAM NAME",
|
||
"x": 75,
|
||
"y": 80,
|
||
"fontSize": 28,
|
||
"fontFamily": "Impact",
|
||
"fill": "#ffffff",
|
||
"rotation": 0
|
||
},
|
||
{
|
||
"type": "text",
|
||
"text": "23",
|
||
"x": 150,
|
||
"y": 150,
|
||
"fontSize": 72,
|
||
"fontFamily": "Impact",
|
||
"fill": "#ffffff",
|
||
"rotation": 0
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
## Canvas Coordinate System
|
||
|
||
- Canvas size: 300x300 units (preview)
|
||
- Export size: 4500x4500 pixels (300 DPI, 15"x15")
|
||
- Scale factor: 15x (export / preview)
|
||
- Origin (0,0): Top-left corner
|
||
- X increases: Left to right
|
||
- Y increases: Top to bottom
|
||
|
||
## Best Practices
|
||
|
||
1. **Keep text readable**: Use font sizes between 18-72px for main text
|
||
2. **Respect print area**: Keep elements within 0-300 canvas bounds
|
||
3. **Use contrasting colors**: Ensure text is visible on shirt colors
|
||
4. **Test at export size**: Verify designs look good at 4500x4500px
|
||
5. **Limit elements**: 5-10 elements maximum for performance
|