diff --git a/.gitignore b/.gitignore index e1ac1a95..17ba30c6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ data.db-wal data.db-shm uploads/ .env +.test-build/ diff --git a/README.md b/README.md index ef3a147a..ff8bd126 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Unlike Canva or Adobe Express, this runs entirely on your own infrastructure. No ## Features +- **Template fields** — name any text or image object, then fill it from data or an agent over the API; one name can drive several objects and pages at once - **Fabric.js canvas** — full object manipulation with retina/HiDPI rendering (2x device pixel ratio) - **Pre-built templates** — LinkedIn-optimized: Quote Card, Stats Highlight, Announcement, Tips List, Profile Card, Minimal Text - **10 Google Fonts** — Inter, Playfair Display, Montserrat, Poppins, Roboto, Open Sans, Lora, Raleway, Source Sans Pro, Merriweather @@ -98,7 +99,8 @@ src/ server/ schema.sql — SQLite schema (designs, templates) + template seeds db.ts — SQLite wrapper (query, get, run, transaction) - index.ts — Hono REST API (designs CRUD, templates, uploads) + index.ts — Hono REST API (designs CRUD, templates, fields, uploads) + fields.ts — Template fields: read the fill schema, substitute values uploads.ts — Local file upload management dev.ts — Dev server with static file serving client/ @@ -133,11 +135,77 @@ templates (id, name, category, canvas_json, width, height, thumbnail_url, sort_o | GET | `/api/designs/:id` | Get a design | | PUT | `/api/designs/:id` | Update a design | | DELETE | `/api/designs/:id` | Delete a design | +| GET | `/api/designs/:id/fields` | List the design's fillable fields | +| POST | `/api/designs/:id/fill` | Fill those fields with values | | GET | `/api/templates` | List all templates | | GET | `/api/templates/:id` | Get a template | | POST | `/api/uploads` | Upload an image file | | GET | `/api/uploads/:filename` | Serve an uploaded image | +## Template Fields + +Design something once, then produce as many variants of it as you have rows of +data — without anything on the outside having to understand canvas JSON. + +Select a text or image object in the editor and give it a **Field name** in the +properties panel. That object is now a fill slot: + +```bash +curl localhost:8787/api/designs/$ID/fields +``` + +```json +[ + { "name": "headline", "type": "text", "value": "Your inspiring quote goes here", "page_ids": ["p1"] }, + { "name": "logo", "type": "image", "value": "https://.../old.png", "page_ids": ["p1", "p2"] } +] +``` + +Fill it. Text fields take a string (numbers are accepted and stringified), +image fields take a URL: + +```bash +curl -X POST localhost:8787/api/designs/$ID/fill \ + -H 'content-type: application/json' \ + -d '{"values": {"headline": "Q3 revenue up 40%", "logo": "https://.../new.png"}}' +``` + +The response carries the filled pages plus `filled` and `unmatched`, so a name +that matches nothing is reported rather than failing the request. The stored +design is left alone — add `"save": true` (and optionally `"name"`) to persist +the result as a new design instead. + +### How far this scales today + +`save: true` writes a **new editable design**, not a rendered image, because +there is no server-side rasterizer. That makes it the right tool for a handful +of variants — an agent fills a card, opens it, exports it — and the wrong one +for a spreadsheet: + +- Each saved variant is a row in `designs`, and `GET /api/designs` returns every + row with its full `canvas_json` and no pagination. A three-object card is + ~2.5 KB serialised; a branded design with an image is 10-40 KB. A few hundred + variants turn the gallery response into megabytes. +- So a few hundred saved variants give you a few hundred gallery entries, not a + few hundred finished graphics. + +For real bulk output, drive `fill` without `save` and rasterize in the browser — +the canvas is already there, and that loop is the missing piece rather than a +server-side renderer. Until it exists, treat `save: true` as a small-N +convenience. + +Worth knowing: + +- A field name may repeat. One `logo` across five pages fills all five, which is + what you want for a carousel. +- What gets written depends on the object, not the declared type: text objects + take `text`, image objects take `src`. +- A replacement image keeps the original object's box, so a different aspect + ratio will be stretched. Size the slot for the images you intend to feed it. +- Only text and image objects can be fields. Naming a shape does nothing. +- Rendering to PNG still happens in the browser — there is no server-side + rasterizer, because Workers have no canvas. + ## Community & Contributions This project is part of the [OpenClaw](https://github.com/openclaw/openclaw) ecosystem. Contributions are welcome — open an issue or submit a PR. diff --git a/agent.md b/agent.md index a635d6f5..45251fc0 100644 --- a/agent.md +++ b/agent.md @@ -9,6 +9,27 @@ A design editor for creating professional social media graphics, especially Link - Image uploads and placement - Multiple canvas sizes (1080x1080 square, 1200x627 landscape) - Save and manage multiple designs +- Template fields — named slots you can fill from data over the API + +## Filling a design from data +Objects in a design can be given a field name in the editor, which turns the +design into a template you can drive without touching canvas JSON: + +- `GET /api/designs/{id}/fields` — what this design can be filled with. Returns + `name`, `type` (`text` or `image`), the current `value`, and the pages the + field appears on. +- `POST /api/designs/{id}/fill` — `{"values": {"headline": "..."}}`. Returns the + filled pages plus `filled` and `unmatched`; the stored design is unchanged. + Add `"save": true` to write the result as a new design you can then open and + export. + +Prefer these over editing `canvas_json` by hand. Text fields take a string, +image fields take a URL, and one name may cover several objects or pages. + +`save: true` produces an editable design, not a rendered image — there is no +server-side renderer. Use it for a handful of variants. Do not loop it over a +large list: every variant becomes a row that `GET /api/designs` returns in full, +and the user ends up with gallery entries rather than finished graphics. ## When to use this template Use this template when the user wants to: @@ -16,3 +37,4 @@ Use this template when the user wants to: - Design LinkedIn posts, quote cards, or announcement banners - Build a simple graphic design tool - Create branded visual content +- Generate on-brand variants of one design by filling it with data diff --git a/clawnify.json b/clawnify.json index 308dd912..c2d4cb6c 100644 --- a/clawnify.json +++ b/clawnify.json @@ -2,7 +2,7 @@ "$schema": "https://app.clawnify.com/schema/v1/clawnify.json", "version": 1, "name": "OpenDesign", - "description": "Design editor for creating professional social media graphics, LinkedIn posts, and visual content with a canvas-based UI, templates, multi-page support, image uploads, and text/shape tools. Like Canva. Best for graphic design tools, social media post creators, banner makers, or any visual content editor.", + "description": "Design editor for creating professional social media graphics, LinkedIn posts, and visual content with a canvas-based UI, templates, multi-page support, image uploads, and text/shape tools. Named template fields let designs be filled from data or an agent over the API, so one design can produce on-brand variants. Like Canva. Best for graphic design tools, social media post creators, banner makers, or any visual content editor.", "icon": "icon.svg", "screenshot": "https://github.com/user-attachments/assets/55fcc0aa-4606-4f80-b56a-7734d7673914", "tags": ["design", "graphics", "social-media", "canvas"], diff --git a/package.json b/package.json index c35deb04..83be9d0e 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "wrangler d1 execute open-design-db --local --file=src/server/schema.sql && concurrently -n ui,api -c cyan,green \"vite\" \"wrangler dev --port 8787\"", + "test": "esbuild src/server/fields.test.ts --bundle --platform=node --format=esm --outfile=.test-build/fields.test.mjs --log-level=warning && node --test .test-build/fields.test.mjs", "build": "vite build" }, "dependencies": { @@ -22,8 +23,10 @@ }, "devDependencies": { "@preact/preset-vite": "^2.9.0", + "@types/node": "^26.4.1", "@types/webfontloader": "^1.6.38", "concurrently": "^9.0.0", + "esbuild": "^0.28.2", "typescript": "^5.7.0", "vite": "^6.0.0", "wrangler": "^4.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfe201b4..2d9491b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 0.18.4(hono@4.12.10)(zod@3.25.76) '@tailwindcss/vite': specifier: ^4.2.2 - version: 4.2.2(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.2.2(vite@6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) fabric: specifier: ^6.0.0 version: 6.9.1 @@ -44,19 +44,25 @@ importers: devDependencies: '@preact/preset-vite': specifier: ^2.9.0 - version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) + '@types/node': + specifier: ^26.4.1 + version: 26.4.1 '@types/webfontloader': specifier: ^1.6.38 version: 1.6.38 concurrently: specifier: ^9.0.0 version: 9.2.1 + esbuild: + specifier: ^0.28.2 + version: 0.28.2 typescript: specifier: ^5.7.0 version: 5.9.3 vite: specifier: ^6.0.0 - version: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + version: 6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) wrangler: specifier: ^4.0.0 version: 4.93.1 @@ -244,6 +250,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -262,6 +274,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -280,6 +298,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -298,6 +322,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -316,6 +346,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -334,6 +370,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -352,6 +394,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -370,6 +418,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -388,6 +442,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -406,6 +466,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -424,6 +490,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -442,6 +514,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -460,6 +538,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -478,6 +562,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -496,6 +586,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -514,6 +610,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -532,6 +634,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} @@ -550,6 +658,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -568,6 +682,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} @@ -586,6 +706,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -604,6 +730,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} @@ -622,6 +754,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -640,6 +778,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -658,6 +802,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -676,6 +826,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -694,6 +850,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@hono/zod-openapi@0.18.4': resolution: {integrity: sha512-6NHMHU96Hh32B1yDhb94Z4Z5/POsmEu2AXpWLWcBq9arskRnOMt2752yEoXoADV8WUAc7H1IkNaQHGj1ytXbYw==} engines: {node: '>=16.0.0'} @@ -1174,8 +1336,8 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/node@25.5.2': - resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} + '@types/node@26.4.1': + resolution: {integrity: sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==} '@types/webfontloader@1.6.38': resolution: {integrity: sha512-kUaF72Fv202suFx6yBrwXqeVRMx7hGtJTesyESZgn9sEPCUeDXm2p0SiyS1MTqW74nQP4p7JyrOCwZ7pNFns4w==} @@ -1519,6 +1681,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2078,8 +2245,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} undici@7.24.8: resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} @@ -2523,6 +2690,9 @@ snapshots: '@esbuild/aix-ppc64@0.27.7': optional: true + '@esbuild/aix-ppc64@0.28.2': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true @@ -2532,6 +2702,9 @@ snapshots: '@esbuild/android-arm64@0.27.7': optional: true + '@esbuild/android-arm64@0.28.2': + optional: true + '@esbuild/android-arm@0.25.12': optional: true @@ -2541,6 +2714,9 @@ snapshots: '@esbuild/android-arm@0.27.7': optional: true + '@esbuild/android-arm@0.28.2': + optional: true + '@esbuild/android-x64@0.25.12': optional: true @@ -2550,6 +2726,9 @@ snapshots: '@esbuild/android-x64@0.27.7': optional: true + '@esbuild/android-x64@0.28.2': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true @@ -2559,6 +2738,9 @@ snapshots: '@esbuild/darwin-arm64@0.27.7': optional: true + '@esbuild/darwin-arm64@0.28.2': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true @@ -2568,6 +2750,9 @@ snapshots: '@esbuild/darwin-x64@0.27.7': optional: true + '@esbuild/darwin-x64@0.28.2': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true @@ -2577,6 +2762,9 @@ snapshots: '@esbuild/freebsd-arm64@0.27.7': optional: true + '@esbuild/freebsd-arm64@0.28.2': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true @@ -2586,6 +2774,9 @@ snapshots: '@esbuild/freebsd-x64@0.27.7': optional: true + '@esbuild/freebsd-x64@0.28.2': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true @@ -2595,6 +2786,9 @@ snapshots: '@esbuild/linux-arm64@0.27.7': optional: true + '@esbuild/linux-arm64@0.28.2': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true @@ -2604,6 +2798,9 @@ snapshots: '@esbuild/linux-arm@0.27.7': optional: true + '@esbuild/linux-arm@0.28.2': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true @@ -2613,6 +2810,9 @@ snapshots: '@esbuild/linux-ia32@0.27.7': optional: true + '@esbuild/linux-ia32@0.28.2': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true @@ -2622,6 +2822,9 @@ snapshots: '@esbuild/linux-loong64@0.27.7': optional: true + '@esbuild/linux-loong64@0.28.2': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true @@ -2631,6 +2834,9 @@ snapshots: '@esbuild/linux-mips64el@0.27.7': optional: true + '@esbuild/linux-mips64el@0.28.2': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true @@ -2640,6 +2846,9 @@ snapshots: '@esbuild/linux-ppc64@0.27.7': optional: true + '@esbuild/linux-ppc64@0.28.2': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true @@ -2649,6 +2858,9 @@ snapshots: '@esbuild/linux-riscv64@0.27.7': optional: true + '@esbuild/linux-riscv64@0.28.2': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true @@ -2658,6 +2870,9 @@ snapshots: '@esbuild/linux-s390x@0.27.7': optional: true + '@esbuild/linux-s390x@0.28.2': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true @@ -2667,6 +2882,9 @@ snapshots: '@esbuild/linux-x64@0.27.7': optional: true + '@esbuild/linux-x64@0.28.2': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true @@ -2676,6 +2894,9 @@ snapshots: '@esbuild/netbsd-arm64@0.27.7': optional: true + '@esbuild/netbsd-arm64@0.28.2': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true @@ -2685,6 +2906,9 @@ snapshots: '@esbuild/netbsd-x64@0.27.7': optional: true + '@esbuild/netbsd-x64@0.28.2': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true @@ -2694,6 +2918,9 @@ snapshots: '@esbuild/openbsd-arm64@0.27.7': optional: true + '@esbuild/openbsd-arm64@0.28.2': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true @@ -2703,6 +2930,9 @@ snapshots: '@esbuild/openbsd-x64@0.27.7': optional: true + '@esbuild/openbsd-x64@0.28.2': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true @@ -2712,6 +2942,9 @@ snapshots: '@esbuild/openharmony-arm64@0.27.7': optional: true + '@esbuild/openharmony-arm64@0.28.2': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true @@ -2721,6 +2954,9 @@ snapshots: '@esbuild/sunos-x64@0.27.7': optional: true + '@esbuild/sunos-x64@0.28.2': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true @@ -2730,6 +2966,9 @@ snapshots: '@esbuild/win32-arm64@0.27.7': optional: true + '@esbuild/win32-arm64@0.28.2': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true @@ -2739,6 +2978,9 @@ snapshots: '@esbuild/win32-ia32@0.27.7': optional: true + '@esbuild/win32-ia32@0.28.2': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true @@ -2748,6 +2990,9 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true + '@esbuild/win32-x64@0.28.2': + optional: true + '@hono/zod-openapi@0.18.4(hono@4.12.10)(zod@3.25.76)': dependencies: '@asteasolutions/zod-to-openapi': 7.3.4(zod@3.25.76) @@ -2908,19 +3153,19 @@ snapshots: '@poppinss/exception@1.2.3': {} - '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': + '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.60.1)(vite@6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) - '@prefresh/vite': 2.4.12(preact@10.29.1)(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) + '@prefresh/vite': 2.4.12(preact@10.29.1)(vite@6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) '@rollup/pluginutils': 5.3.0(rollup@4.60.1) babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0) debug: 4.4.3 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) - vite-prerender-plugin: 0.5.13(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) + vite: 6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + vite-prerender-plugin: 0.5.13(vite@6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) zimmerframe: 1.1.4 transitivePeerDependencies: - preact @@ -2935,7 +3180,7 @@ snapshots: '@prefresh/utils@1.2.1': {} - '@prefresh/vite@2.4.12(preact@10.29.1)(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': + '@prefresh/vite@2.4.12(preact@10.29.1)(vite@6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@prefresh/babel-plugin': 0.5.3 @@ -2943,7 +3188,7 @@ snapshots: '@prefresh/utils': 1.2.1 '@rollup/pluginutils': 4.2.1 preact: 10.29.1 - vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -3100,22 +3345,21 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - '@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': + '@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) '@tootallnate/once@2.0.0': optional: true '@types/estree@1.0.8': {} - '@types/node@25.5.2': + '@types/node@26.4.1': dependencies: - undici-types: 7.18.2 - optional: true + undici-types: 8.3.0 '@types/webfontloader@1.6.38': {} @@ -3459,6 +3703,35 @@ snapshots: '@esbuild/win32-x64': 0.27.7 optional: true + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + escalade@3.2.0: {} escodegen@2.1.0: @@ -4097,8 +4370,7 @@ snapshots: typescript@5.9.3: {} - undici-types@7.18.2: - optional: true + undici-types@8.3.0: {} undici@7.24.8: {} @@ -4124,7 +4396,7 @@ snapshots: util-deprecate@1.0.2: optional: true - vite-prerender-plugin@0.5.13(vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)): + vite-prerender-plugin@0.5.13(vite@6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)): dependencies: kolorist: 1.8.0 magic-string: 0.30.21 @@ -4132,9 +4404,9 @@ snapshots: simple-code-frame: 1.3.0 source-map: 0.7.6 stack-trace: 1.0.0-pre2 - vite: 6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) - vite@6.4.1(@types/node@25.5.2)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3): + vite@6.4.1(@types/node@26.4.1)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -4143,7 +4415,7 @@ snapshots: rollup: 4.60.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.5.2 + '@types/node': 26.4.1 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.32.0 diff --git a/src/client/components/right-sidebar.tsx b/src/client/components/right-sidebar.tsx index 3a52b0ac..586d0488 100644 --- a/src/client/components/right-sidebar.tsx +++ b/src/client/components/right-sidebar.tsx @@ -27,6 +27,53 @@ const FONT_FAMILIES = [ "Merriweather", ]; +/** + * Names an object so `/api/designs/:id/fields` publishes it and + * `/api/designs/:id/fill` can write to it. Committed on blur rather than per + * keystroke, so naming a field costs one undo step instead of one per letter. + */ +function FieldNameInput({ + object, + onCommit, +}: { + object: fabric.FabricObject; + onCommit: (name: string) => void; +}) { + const saved = ((object as any).fieldName as string) || ""; + const [draft, setDraft] = useState(saved); + + // Re-seed when the selection moves to a different object. + useEffect(() => setDraft(saved), [object]); + + const commit = () => { + const next = draft.trim(); + if (next !== saved) onCommit(next); + setDraft(next); + }; + + return ( +
+ {saved + ? "Fillable through the design's /fill endpoint." + : "Name it to fill it from data or an agent."} +
+ {
+ /** The input pages with values substituted — every other column preserved. */
+ pages: P[];
+ /** Field names that matched at least one object. */
+ filled: string[];
+ /** Supplied names that matched nothing — a typo, not an error. */
+ unmatched: string[];
+}
+
+/** Fabric serializes text as one of three type names depending on the class. */
+const TEXT_TYPES = new Set(["text", "i-text", "itext", "textbox"]);
+
+type FabricNode = Record (pages: P[], values: Record {
+ const filled = new Set