From 91d423530bae0f35957d1e2035fe5d0a11b781b0 Mon Sep 17 00:00:00 2001 From: ajianaz Date: Thu, 17 Sep 2026 16:18:07 +0700 Subject: [PATCH 1/2] feat(templates): whatsapp-chat + notes-app cards, inline markup for twitter/tiktok/quote-stack New templates (both 1080x1350, inline-markup enabled): - whatsapp-chat: contact header + 6 message slots with side toggle, dynamic bubble heights, text_color per page - notes-app: iOS notes paper card (note_bg/title_color/body_color), timestamp bar, markup-enabled body Markup rollout: options=["markup"] + segments rendering added to twitter-quote, tiktok-quote, and quote-stack q1/q2/q3. Default renders are md5-identical to the pre-change baseline (backward compatible). Docs: template pages, gallery cards, sidebar entries. --- docs/.vitepress/config.ts | 2 +- docs/templates/index.md | 14 ++ docs/templates/notes-app.md | 50 +++++++ docs/templates/whatsapp-chat.md | 59 +++++++++ templates/notes-app/defaults.json | 18 +++ templates/notes-app/schema.json | 108 ++++++++++++++++ templates/notes-app/template.svg | 51 ++++++++ templates/quote-stack/schema.json | 15 ++- templates/quote-stack/template.svg | 16 ++- templates/tiktok-quote/schema.json | 5 +- templates/tiktok-quote/template.svg | 7 +- templates/twitter-quote/schema.json | 5 +- templates/twitter-quote/template.svg | 7 +- templates/whatsapp-chat/defaults.json | 29 +++++ templates/whatsapp-chat/schema.json | 180 ++++++++++++++++++++++++++ templates/whatsapp-chat/template.svg | 75 +++++++++++ 16 files changed, 629 insertions(+), 12 deletions(-) create mode 100644 docs/templates/notes-app.md create mode 100644 docs/templates/whatsapp-chat.md create mode 100644 templates/notes-app/defaults.json create mode 100644 templates/notes-app/schema.json create mode 100644 templates/notes-app/template.svg create mode 100644 templates/whatsapp-chat/defaults.json create mode 100644 templates/whatsapp-chat/schema.json create mode 100644 templates/whatsapp-chat/template.svg diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 58cdc7d..109a8e2 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -65,7 +65,7 @@ export default defineConfig({ text: 'Social Media', collapsed: true, items: [ - { text: 'Social Quote', link: '/templates/social-quote' }, { text: 'Dev Quote', link: '/templates/dev-quote' }, { text: 'Twitter/X Quote', link: '/templates/twitter-quote' }, { text: 'TikTok Quote', link: '/templates/tiktok-quote' }, { text: 'Instagram Story', link: '/templates/instagram-story' }, { text: 'LinkedIn Card', link: '/templates/linkedin-card' }, { text: 'Pinterest Pin', link: '/templates/pinterest-pin' }, { text: 'Social Poll', link: '/templates/social-poll' }, { text: 'Tweet Embed', link: '/templates/tweet-embed' } + { text: 'Social Quote', link: '/templates/social-quote' }, { text: 'Dev Quote', link: '/templates/dev-quote' }, { text: 'Twitter/X Quote', link: '/templates/twitter-quote' }, { text: 'TikTok Quote', link: '/templates/tiktok-quote' }, { text: 'WhatsApp Chat', link: '/templates/whatsapp-chat' }, { text: 'Notes App', link: '/templates/notes-app' }, { text: 'Instagram Story', link: '/templates/instagram-story' }, { text: 'LinkedIn Card', link: '/templates/linkedin-card' }, { text: 'Pinterest Pin', link: '/templates/pinterest-pin' }, { text: 'Social Poll', link: '/templates/social-poll' }, { text: 'Tweet Embed', link: '/templates/tweet-embed' } ], }, { diff --git a/docs/templates/index.md b/docs/templates/index.md index 84b499a..1a6be72 100644 --- a/docs/templates/index.md +++ b/docs/templates/index.md @@ -759,6 +759,20 @@ Cosy ships with **148 templates** out of the box. Browse the gallery below, or u 1080×1350 + + WhatsApp Chat +
+

WhatsApp Chat

+ 1080×1350 +
+
+ + Notes App +
+

Notes App

+ 1080×1350 +
+
Rate Limit
diff --git a/docs/templates/notes-app.md b/docs/templates/notes-app.md new file mode 100644 index 0000000..385daa5 --- /dev/null +++ b/docs/templates/notes-app.md @@ -0,0 +1,50 @@ +# Notes App + +## Preview + +![Notes App](https://s3.ajianaz.dev/hermes/codecoradev/cosy/preview/notes-app.png) + +## Dimensions + +- **Width:** 1080px +- **Height:** 1350px +- **Aspect Ratio:** 1080:1350 + +## Fonts + +- Inter + +## Slide Fields + +| Field | Type | Required | Max Length | +|-------|------|----------|------------| +| `title` | text | — | 40 | +| `body` | text | ✅ | 400 | +| `note_bg` | color | — | — | +| `title_color` | color | — | — | +| `body_color` | color | — | — | +| `note_timestamp` | text | — | 30 | + +`body` supports inline markup: `*bold*`, `_italic_`, `*color:#hex*...*color*`. +`note_bg` defaults to the iOS notes yellow (`#fef9c3`). + +## Brand Fields + +| Field | Type | Required | Default | +|-------|------|----------|---------| +| `brand_name` | text | ✅ | — | +| `brand_handle` | text | ✅ | — | +| `bg_color` | bg | — | #1e1e2e | +| `bg_color_end` | bg | — | #11111b | +| `accent_color` | text | — | #cba6f7 | +| `accent_color_end` | text | — | #89b4fa | +| `bg_image` | image | — | — | +| `bg_image_opacity` | number | — | 0.15 | +| `bg_overlay_opacity` | number | — | 0.7 | +| `logo` | image | — | — | + +## Usage + +```bash +cosy render --template notes-app --data input.json --output output.png +``` diff --git a/docs/templates/whatsapp-chat.md b/docs/templates/whatsapp-chat.md new file mode 100644 index 0000000..91c0400 --- /dev/null +++ b/docs/templates/whatsapp-chat.md @@ -0,0 +1,59 @@ +# WhatsApp Chat + +## Preview + +![WhatsApp Chat](https://s3.ajianaz.dev/hermes/codecoradev/cosy/preview/whatsapp-chat.png) + +## Dimensions + +- **Width:** 1080px +- **Height:** 1350px +- **Aspect Ratio:** 1080:1350 + +## Fonts + +- Inter + +## Slide Fields + +| Field | Type | Required | Max Length | +|-------|------|----------|------------| +| `contact_name` | text | — | 24 | +| `contact_status` | text | — | 24 | +| `text_color` | color | — | — | +| `m1_text` | text | — | 120 | +| `m1_side` | text | — | 5 | +| `m2_text` | text | — | 120 | +| `m2_side` | text | — | 5 | +| `m3_text` | text | — | 120 | +| `m3_side` | text | — | 5 | +| `m4_text` | text | — | 120 | +| `m4_side` | text | — | 5 | +| `m5_text` | text | — | 120 | +| `m5_side` | text | — | 5 | +| `m6_text` | text | — | 120 | +| `m6_side` | text | — | 5 | + +Message fields support inline markup: `*bold*`, `_italic_`, `*color:#hex*...*color*`. +Each `mN_side` is `left` (default) or `right`. + +## Brand Fields + +| Field | Type | Required | Default | +|-------|------|----------|---------| +| `brand_name` | text | ✅ | — | +| `brand_handle` | text | ✅ | — | +| `bg_color` | bg | — | #1e1e2e | +| `bg_color_end` | bg | — | #11111b | +| `accent_color` | text | — | #cba6f7 | +| `accent_color_end` | text | — | #89b4fa | +| `bg_image` | image | — | — | +| `bg_image_opacity` | number | — | 0.15 | +| `bg_overlay_opacity` | number | — | 0.7 | +| `logo` | image | — | — | + +## Usage + +```bash +cosy render --template whatsapp-chat --data input.json --output output.png +``` diff --git a/templates/notes-app/defaults.json b/templates/notes-app/defaults.json new file mode 100644 index 0000000..548df1a --- /dev/null +++ b/templates/notes-app/defaults.json @@ -0,0 +1,18 @@ +{ + "brand": { + "brand_name": "ajianaz", + "brand_handle": "@ajianaz", + "bg_color": "#1e1e2e", + "bg_color_end": "#11111b", + "accent_color": "#cba6f7", + "accent_color_end": "#89b4fa", + "show_brand": false + }, + "slides": [ + { + "title": "Catatan penting", + "body": " inget ya: *gaji bukan ukuran value kamu*. value itu pas kamu bisa bilang *nggak* ke hal yang _nggak ngerespon_. sisanya cuma _noise_.", + "note_timestamp": "Today 21:14" + } + ] +} diff --git a/templates/notes-app/schema.json b/templates/notes-app/schema.json new file mode 100644 index 0000000..d74a06f --- /dev/null +++ b/templates/notes-app/schema.json @@ -0,0 +1,108 @@ +{ + "id": "notes-app", + "name": "Notes App", + "dimensions": { + "width": 1080, + "height": 1350 + }, + "fonts": [ + "Inter" + ], + "brand_fields": { + "show_brand": { + "type": "boolean", + "required": false, + "default": false, + "description": "Show brand name/handle watermark at bottom. Default: false." + }, + "brand_name": { + "type": "text", + "required": true, + "max": 30 + }, + "brand_handle": { + "type": "text", + "required": true, + "max": 30 + }, + "bg_color": { + "type": "bg", + "required": false, + "default": "#1e1e2e" + }, + "bg_color_end": { + "type": "bg", + "required": false, + "default": "#11111b" + }, + "accent_color": { + "type": "text", + "required": false, + "default": "#cba6f7" + }, + "accent_color_end": { + "type": "text", + "required": false, + "default": "#89b4fa" + }, + "bg_image": { + "type": "image", + "required": false, + "description": "Background image — local path or http(s) URL (PNG/JPG/WebP)." + }, + "bg_image_opacity": { + "type": "number", + "required": false, + "default": 0.15, + "description": "Opacity of the background image (0.0-1.0)." + }, + "bg_overlay_opacity": { + "type": "number", + "required": false, + "default": 0.7, + "description": "Opacity of the gradient overlay when bg_image is set. Default: 0.7." + }, + "logo": { + "type": "image", + "required": false, + "description": "Brand logo path (PNG/SVG). Rendered at bottom-right corner." + } + }, + "slide_fields": { + "title": { + "type": "text", + "required": false, + "max": 40, + "description": "Note title (first line, bold)." + }, + "body": { + "type": "text", + "required": true, + "max": 400, + "wrap_width": 34, + "options": ["markup"], + "description": "Note body text. Supports *bold*, _italic_, *color:#hex*...*color*." + }, + "note_bg": { + "type": "color", + "required": false, + "description": "Note paper color (default iOS yellow #fef9c3)." + }, + "title_color": { + "type": "color", + "required": false, + "description": "Title text color (default #1e1e2e)." + }, + "body_color": { + "type": "color", + "required": false, + "description": "Body text color (default #3a3a3c)." + }, + "note_timestamp": { + "type": "text", + "required": false, + "max": 30, + "description": "Timestamp text in the window bar (default 'Today')." + } + } +} diff --git a/templates/notes-app/template.svg b/templates/notes-app/template.svg new file mode 100644 index 0000000..d61b0b7 --- /dev/null +++ b/templates/notes-app/template.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + {% macro segout(seg) %}{% if seg.b %}{% endif %}{% if seg.i %}{% endif %}{% if seg.c %}{{ seg.t }}{% else %}{{ seg.t }}{% endif %}{% if seg.i %}{% endif %}{% if seg.b %}{% endif %}{% endmacro %} + {% macro lineout(line, first, x) %}{% if first %}{% for seg in line %}{{ segout(seg) }}{% endfor %}{% else %}{% for seg in line %}{{ segout(seg) }}{% endfor %}{% endif %}{% endmacro %} + {% macro plainout(line, first, x) %}{% if first %}{{ line }}{% else %}{{ line }}{% endif %}{% endmacro %} + + + {% if bg_image_data_uri %} + + {% endif %} + + + + + + + + + + + {{ slide.note_timestamp | default('Today') }} + + + {{ slide.title | default('Notes') }} + + + + {% if body_segments %}{% for line in body_segments %}{{ lineout(line, loop.first, 140) }}{% endfor %}{% elif body_lines %}{% for line in body_lines %}{{ plainout(line, loop.first, 140) }}{% endfor %}{% else %}{{ slide.body }}{% endif %} + + + {% if brand.show_brand %} + {{ brand.brand_name | default('') }} + {% endif %} + diff --git a/templates/quote-stack/schema.json b/templates/quote-stack/schema.json index 6c1cbf2..7c9c43a 100644 --- a/templates/quote-stack/schema.json +++ b/templates/quote-stack/schema.json @@ -80,7 +80,10 @@ "type": "text", "required": true, "max": 120, - "wrap_width": 35 + "wrap_width": 35, + "options": [ + "markup" + ] }, "q1_author": { "type": "text", @@ -91,7 +94,10 @@ "type": "text", "required": true, "max": 120, - "wrap_width": 35 + "wrap_width": 35, + "options": [ + "markup" + ] }, "q2_author": { "type": "text", @@ -102,7 +108,10 @@ "type": "text", "required": true, "max": 120, - "wrap_width": 35 + "wrap_width": 35, + "options": [ + "markup" + ] }, "q3_author": { "type": "text", diff --git a/templates/quote-stack/template.svg b/templates/quote-stack/template.svg index 95fd16c..8e8a300 100644 --- a/templates/quote-stack/template.svg +++ b/templates/quote-stack/template.svg @@ -20,6 +20,9 @@ + {# One styled segment #} + {% macro segout(seg) %}{% if seg.b %}{% endif %}{% if seg.i %}{% endif %}{% if seg.c %}{{ seg.t }}{% else %}{{ seg.t }}{% endif %}{% if seg.i %}{% endif %}{% if seg.b %}{% endif %}{% endmacro %} + {% if bg_image_data_uri %} {% endif %} @@ -30,9 +33,9 @@ {{ slide.title }} {% endif %} {% set items = [ - (slide.q1_text, slide.q1_author, q1_text_lines), - (slide.q2_text, slide.q2_author, q2_text_lines), - (slide.q3_text, slide.q3_author, q3_text_lines) + (slide.q1_text, slide.q1_author, q1_text_lines, q1_text_segments), + (slide.q2_text, slide.q2_author, q2_text_lines, q2_text_segments), + (slide.q3_text, slide.q3_author, q3_text_lines, q3_text_segments) ] %} {% for q in items %} {% if q[0] %} @@ -40,7 +43,12 @@ & {% set ql = q[2] %} - {% if ql %} + {% set qsegs = q[3] %} + {% if qsegs %} + {% for line in qsegs %} + {% for seg in line %}{{ segout(seg) }}{% endfor %} + {% endfor %} + {% elif ql %} {% for line in ql %} {{ line }} {% endfor %} diff --git a/templates/tiktok-quote/schema.json b/templates/tiktok-quote/schema.json index 3e4829b..34da4e5 100644 --- a/templates/tiktok-quote/schema.json +++ b/templates/tiktok-quote/schema.json @@ -75,7 +75,10 @@ "type": "text", "required": true, "max": 200, - "wrap_width": 25 + "wrap_width": 25, + "options": [ + "markup" + ] }, "author": { "type": "text", diff --git a/templates/tiktok-quote/template.svg b/templates/tiktok-quote/template.svg index 583c7a9..10bdc23 100644 --- a/templates/tiktok-quote/template.svg +++ b/templates/tiktok-quote/template.svg @@ -16,6 +16,11 @@ + {# One styled segment #} + {% macro segout(seg) %}{% if seg.b %}{% endif %}{% if seg.i %}{% endif %}{% if seg.c %}{{ seg.t }}{% else %}{{ seg.t }}{% endif %}{% if seg.i %}{% endif %}{% if seg.b %}{% endif %}{% endmacro %} + {# One quote line: segments variant #} + {% macro segline(line, first, x, dy) %}{% if first %}{% for seg in line %}{{ segout(seg) }}{% endfor %}{% else %}{% for seg in line %}{{ segout(seg) }}{% endfor %}{% endif %}{% endmacro %} + {% if bg_image_data_uri %} {% endif %} @@ -23,7 +28,7 @@ - {% for line in quote_lines %}{{ line }}{% endfor %} + {% if quote_segments %}{% for line in quote_segments %}{{ segline(line, loop.first, "540", "72") }}{% endfor %}{% elif quote_lines %}{% for line in quote_lines %}{{ line }}{% endfor %}{% endif %} {% if not quote_lines %}{{ slide.quote }}{% endif %} diff --git a/templates/twitter-quote/schema.json b/templates/twitter-quote/schema.json index 112bf40..6132afc 100644 --- a/templates/twitter-quote/schema.json +++ b/templates/twitter-quote/schema.json @@ -75,7 +75,10 @@ "type": "text", "required": true, "max": 280, - "wrap_width": 35 + "wrap_width": 35, + "options": [ + "markup" + ] }, "author": { "type": "text", diff --git a/templates/twitter-quote/template.svg b/templates/twitter-quote/template.svg index 97b2bab..4e6eabd 100644 --- a/templates/twitter-quote/template.svg +++ b/templates/twitter-quote/template.svg @@ -16,6 +16,11 @@ + {# One styled segment #} + {% macro segout(seg) %}{% if seg.b %}{% endif %}{% if seg.i %}{% endif %}{% if seg.c %}{{ seg.t }}{% else %}{{ seg.t }}{% endif %}{% if seg.i %}{% endif %}{% if seg.b %}{% endif %}{% endmacro %} + {# One quote line: segments variant #} + {% macro segline(line, first, x, dy) %}{% if first %}{% for seg in line %}{{ segout(seg) }}{% endfor %}{% else %}{% for seg in line %}{{ segout(seg) }}{% endfor %}{% endif %}{% endmacro %} + {% if bg_image_data_uri %} {% endif %} @@ -30,7 +35,7 @@ {{ brand.brand_name | default('') }}{% if brand.brand_handle %} · {{ brand.brand_handle }}{% endif %} {% endif %} - {% for line in quote_lines %}{{ line }}{% endfor %} + {% if quote_segments %}{% for line in quote_segments %}{{ segline(line, loop.first, "80", "55") }}{% endfor %}{% elif quote_lines %}{% for line in quote_lines %}{{ line }}{% endfor %}{% endif %} {% if not quote_lines %}{{ slide.quote }}{% endif %} {{ slide.author }} diff --git a/templates/whatsapp-chat/defaults.json b/templates/whatsapp-chat/defaults.json new file mode 100644 index 0000000..9a848d1 --- /dev/null +++ b/templates/whatsapp-chat/defaults.json @@ -0,0 +1,29 @@ +{ + "brand": { + "brand_name": "ajianaz", + "brand_handle": "@ajianaz", + "bg_color": "#1e1e2e", + "bg_color_end": "#11111b", + "accent_color": "#cba6f7", + "accent_color_end": "#89b4fa", + "show_brand": false + }, + "slides": [ + { + "contact_name": "Tim Lead", + "contact_status": "online", + "m1_text": "Bro, besok bisa cover standup aku?", + "m1_side": "left", + "m2_text": "Bisa. *Standup jam 9 kan?*", + "m2_side": "right", + "m3_text": "Ye. Tapi jangan bilang siapa siapa", + "m3_side": "left", + "m4_text": "Alasannya apa _anak baru_ ngerasa dikejar", + "m4_side": "left", + "m5_text": "*Tenang.* Aku bilang kita yang minta", + "m5_side": "right", + "m6_text": "Makasih bro, kamu nyelamatin minggu aku", + "m6_side": "left" + } + ] +} diff --git a/templates/whatsapp-chat/schema.json b/templates/whatsapp-chat/schema.json new file mode 100644 index 0000000..df93462 --- /dev/null +++ b/templates/whatsapp-chat/schema.json @@ -0,0 +1,180 @@ +{ + "id": "whatsapp-chat", + "name": "WhatsApp Chat", + "dimensions": { + "width": 1080, + "height": 1350 + }, + "fonts": [ + "Inter" + ], + "brand_fields": { + "show_brand": { + "type": "boolean", + "required": false, + "default": false, + "description": "Show brand name/handle watermark at bottom. Default: false." + }, + "brand_name": { + "type": "text", + "required": true, + "max": 30 + }, + "brand_handle": { + "type": "text", + "required": true, + "max": 30 + }, + "bg_color": { + "type": "bg", + "required": false, + "default": "#1e1e2e" + }, + "bg_color_end": { + "type": "bg", + "required": false, + "default": "#11111b" + }, + "accent_color": { + "type": "text", + "required": false, + "default": "#cba6f7" + }, + "accent_color_end": { + "type": "text", + "required": false, + "default": "#89b4fa" + }, + "bg_image": { + "type": "image", + "required": false, + "description": "Background image — local path or http(s) URL (PNG/JPG/WebP)." + }, + "bg_image_opacity": { + "type": "number", + "required": false, + "default": 0.15, + "description": "Opacity of the background image (0.0-1.0)." + }, + "bg_overlay_opacity": { + "type": "number", + "required": false, + "default": 0.7, + "description": "Opacity of the gradient overlay when bg_image is set. Lower = photo shows through more. Default: 0.7." + }, + "logo": { + "type": "image", + "required": false, + "description": "Brand logo path (PNG/SVG). Rendered at bottom-right corner." + } + }, + "slide_fields": { + "contact_name": { + "type": "text", + "required": false, + "max": 24, + "description": "Chat header contact name." + }, + "contact_status": { + "type": "text", + "required": false, + "max": 24, + "description": "Chat header status line (e.g. 'online', 'last seen today')." + }, + "text_color": { + "type": "color", + "required": false, + "description": "Message text color. Use a dark hex when the background is light." + }, + "m1_text": { + "type": "text", + "required": false, + "max": 120, + "wrap_width": 34, + "options": ["markup"], + "description": "Message 1 text. Supports *bold*, _italic_, *color:#hex*...*color*." + }, + "m1_side": { + "type": "text", + "required": false, + "max": 5, + "options": ["left", "right"], + "description": "Message 1 bubble side: left or right (default left)." + }, + "m2_text": { + "type": "text", + "required": false, + "max": 120, + "wrap_width": 34, + "options": ["markup"], + "description": "Message 2 text. Supports inline markup." + }, + "m2_side": { + "type": "text", + "required": false, + "max": 5, + "options": ["left", "right"], + "description": "Message 2 bubble side (default right)." + }, + "m3_text": { + "type": "text", + "required": false, + "max": 120, + "wrap_width": 34, + "options": ["markup"], + "description": "Message 3 text. Supports inline markup." + }, + "m3_side": { + "type": "text", + "required": false, + "max": 5, + "options": ["left", "right"], + "description": "Message 3 bubble side (default left)." + }, + "m4_text": { + "type": "text", + "required": false, + "max": 120, + "wrap_width": 34, + "options": ["markup"], + "description": "Message 4 text. Supports inline markup." + }, + "m4_side": { + "type": "text", + "required": false, + "max": 5, + "options": ["left", "right"], + "description": "Message 4 bubble side (default right)." + }, + "m5_text": { + "type": "text", + "required": false, + "max": 120, + "wrap_width": 34, + "options": ["markup"], + "description": "Message 5 text. Supports inline markup." + }, + "m5_side": { + "type": "text", + "required": false, + "max": 5, + "options": ["left", "right"], + "description": "Message 5 bubble side (default left)." + }, + "m6_text": { + "type": "text", + "required": false, + "max": 120, + "wrap_width": 34, + "options": ["markup"], + "description": "Message 6 text. Supports inline markup." + }, + "m6_side": { + "type": "text", + "required": false, + "max": 5, + "options": ["left", "right"], + "description": "Message 6 bubble side (default right)." + } + } +} diff --git a/templates/whatsapp-chat/template.svg b/templates/whatsapp-chat/template.svg new file mode 100644 index 0000000..0f9e610 --- /dev/null +++ b/templates/whatsapp-chat/template.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + {# One styled segment: bold/italic via nested tspan, color via fill #} + {% macro segout(seg) %}{% if seg.b %}{% endif %}{% if seg.i %}{% endif %}{% if seg.c %}{{ seg.t }}{% else %}{{ seg.t }}{% endif %}{% if seg.i %}{% endif %}{% if seg.b %}{% endif %}{% endmacro %} + {# One bubble line: first line flows in the text element, later lines get x/dy #} + {% macro lineout(line, first, x) %}{% if first %}{% for seg in line %}{{ segout(seg) }}{% endfor %}{% else %}{% for seg in line %}{{ segout(seg) }}{% endfor %}{% endif %}{% endmacro %} + {# A plain (legacy) line #} + {% macro plainout(line, first, x) %}{% if first %}{{ line }}{% else %}{{ line }}{% endif %}{% endmacro %} + + + {% if bg_image_data_uri %} + + {% endif %} + + + + + + + + + + + + {{ slide.contact_name | default('Chat') }} + {{ slide.contact_status | default('online') }} + + + {% set items = [ + (slide.m1_text, slide.m1_side | default('left'), m1_text_lines, m1_text_segments), + (slide.m2_text, slide.m2_side | default('right'), m2_text_lines, m2_text_segments), + (slide.m3_text, slide.m3_side | default('left'), m3_text_lines, m3_text_segments), + (slide.m4_text, slide.m4_side | default('right'), m4_text_lines, m4_text_segments), + (slide.m5_text, slide.m5_side | default('left'), m5_text_lines, m5_text_segments), + (slide.m6_text, slide.m6_side | default('right'), m6_text_lines, m6_text_segments) + ] %} + {% for msg in items %} + {% if msg[0] %} + {% set segs = msg[3] %} + {% set lines = msg[2] %} + {% set n_lines = segs | default(lines) | length %} + {% set bh = 52 + n_lines * 38 %} + {% set by = 245 + loop.index0 * 170 %} + {% if msg[1] == 'right' %} + + {% if segs %}{% for line in segs %}{{ lineout(line, loop.first, 510) }}{% endfor %}{% else %}{% for line in lines %}{{ plainout(line, loop.first, 510) }}{% endfor %}{% endif %} + {% else %} + + {% if segs %}{% for line in segs %}{{ lineout(line, loop.first, 150) }}{% endfor %}{% else %}{% for line in lines %}{{ plainout(line, loop.first, 150) }}{% endfor %}{% endif %} + {% endif %} + {% endif %} + {% endfor %} + + + {% if brand.show_brand %} + {{ brand.brand_name | default('') }} + {% endif %} + From eaffd1100db5c162bf2f918f2c1fc0e9b5b69d03 Mon Sep 17 00:00:00 2001 From: ajianaz Date: Thu, 17 Sep 2026 16:30:28 +0700 Subject: [PATCH 2/2] fix(whatsapp-chat): dynamic bubble stacking + clamp message length CodeCora finding on the PR was valid: fixed 170px slots with variable bubble heights (schema allowed 4 wrapped lines = 204px) could overlap the next bubble and push past the phone frame. - Bubbles now stack via a minijinja namespace accumulator (y cursor), so heights can never collide - mN_text max clamped 120 -> 60 chars (2 wrapped lines max), keeping the mathematically guaranteed fit: 6 x (52 + 2*38 + 24) = 1188px, inside the 1190px frame even when every message is at max length Verified with a max-length stress render: 6 two-line bubbles, no overlap, last bubble inside the frame, consistent gaps (vision QA). --- templates/whatsapp-chat/schema.json | 66 ++++++++++++++++++++-------- templates/whatsapp-chat/template.svg | 6 ++- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/templates/whatsapp-chat/schema.json b/templates/whatsapp-chat/schema.json index df93462..f38f971 100644 --- a/templates/whatsapp-chat/schema.json +++ b/templates/whatsapp-chat/schema.json @@ -89,91 +89,121 @@ "m1_text": { "type": "text", "required": false, - "max": 120, + "max": 60, "wrap_width": 34, - "options": ["markup"], + "options": [ + "markup" + ], "description": "Message 1 text. Supports *bold*, _italic_, *color:#hex*...*color*." }, "m1_side": { "type": "text", "required": false, "max": 5, - "options": ["left", "right"], + "options": [ + "left", + "right" + ], "description": "Message 1 bubble side: left or right (default left)." }, "m2_text": { "type": "text", "required": false, - "max": 120, + "max": 60, "wrap_width": 34, - "options": ["markup"], + "options": [ + "markup" + ], "description": "Message 2 text. Supports inline markup." }, "m2_side": { "type": "text", "required": false, "max": 5, - "options": ["left", "right"], + "options": [ + "left", + "right" + ], "description": "Message 2 bubble side (default right)." }, "m3_text": { "type": "text", "required": false, - "max": 120, + "max": 60, "wrap_width": 34, - "options": ["markup"], + "options": [ + "markup" + ], "description": "Message 3 text. Supports inline markup." }, "m3_side": { "type": "text", "required": false, "max": 5, - "options": ["left", "right"], + "options": [ + "left", + "right" + ], "description": "Message 3 bubble side (default left)." }, "m4_text": { "type": "text", "required": false, - "max": 120, + "max": 60, "wrap_width": 34, - "options": ["markup"], + "options": [ + "markup" + ], "description": "Message 4 text. Supports inline markup." }, "m4_side": { "type": "text", "required": false, "max": 5, - "options": ["left", "right"], + "options": [ + "left", + "right" + ], "description": "Message 4 bubble side (default right)." }, "m5_text": { "type": "text", "required": false, - "max": 120, + "max": 60, "wrap_width": 34, - "options": ["markup"], + "options": [ + "markup" + ], "description": "Message 5 text. Supports inline markup." }, "m5_side": { "type": "text", "required": false, "max": 5, - "options": ["left", "right"], + "options": [ + "left", + "right" + ], "description": "Message 5 bubble side (default left)." }, "m6_text": { "type": "text", "required": false, - "max": 120, + "max": 60, "wrap_width": 34, - "options": ["markup"], + "options": [ + "markup" + ], "description": "Message 6 text. Supports inline markup." }, "m6_side": { "type": "text", "required": false, "max": 5, - "options": ["left", "right"], + "options": [ + "left", + "right" + ], "description": "Message 6 bubble side (default right)." } } diff --git a/templates/whatsapp-chat/template.svg b/templates/whatsapp-chat/template.svg index 0f9e610..62cc3db 100644 --- a/templates/whatsapp-chat/template.svg +++ b/templates/whatsapp-chat/template.svg @@ -42,7 +42,8 @@ {{ slide.contact_name | default('Chat') }} {{ slide.contact_status | default('online') }} - + + {% set ns = namespace(y = 245) %} {% set items = [ (slide.m1_text, slide.m1_side | default('left'), m1_text_lines, m1_text_segments), (slide.m2_text, slide.m2_side | default('right'), m2_text_lines, m2_text_segments), @@ -57,7 +58,8 @@ {% set lines = msg[2] %} {% set n_lines = segs | default(lines) | length %} {% set bh = 52 + n_lines * 38 %} - {% set by = 245 + loop.index0 * 170 %} + {% set by = ns.y %} + {% set ns.y = by + bh + 24 %} {% if msg[1] == 'right' %} {% if segs %}{% for line in segs %}{{ lineout(line, loop.first, 510) }}{% endfor %}{% else %}{% for line in lines %}{{ plainout(line, loop.first, 510) }}{% endfor %}{% endif %}