@appshoteditor/shot-dsl 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,6 +25,9 @@ composer — so a layout composed by the skill renders identically in the editor
25
25
  - **Set layout system (0.4.0)** — `solveVertical` / `NO_TANGENT` (the no-tangent rule), `focusReach`,
26
26
  `rotatedBox` (`layout-system.ts`); `makeVariants(plan)` (three concept plans, `variants.ts`); small
27
27
  colour helpers (`color.ts`).
28
+ - **Art direction (0.5.0)** — `breakLines` (balanced line breaks, `typography.ts`), tonal palettes +
29
+ `harmonize` (one WCAG-AA text colour per set, `palette.ts`), panorama motifs (`decor.ts`), and in
30
+ `composeSet`: the hero, accent rhythm, callouts, mascots and shadows (see "Art direction" below).
28
31
 
29
32
  ```ts
30
33
  import { composeTemplate, validateTemplate } from '@appshoteditor/shot-dsl';
@@ -95,19 +98,30 @@ Plan additions (all optional; a 0.3.0 plan composes and validates unchanged):
95
98
  | `style.presentation` | plan | `device` (default) · `frameless` (rounded screenshot + shadow) · `zoom` (magnified crop of `crop`, else the centre of the `focus` band) |
96
99
  | `style.tilt`, `style.tiltScreens` | plan | Degrees (clockwise) applied only to the listed screen indices. A tilted subject prefers a bleed under `auto`/`deep`; the tilt is reduced (`tilt-reduced`) if it can't keep the side margins. |
97
100
  | `style.bleed` | plan | `auto` · `none` · `deep` (see above) |
98
- | `style.palette` | plan | `{ mode: "family" \| "sequence", colors: [hex…] }`. Background for screens that omit one. |
99
- | `style.panorama` | plan | `{ spans: [[0,1], …], straddle?: true \| [screen…], decoration?: "orbs" \| "none" }`. Adjacent screens share one continuous background; `straddle` picks the span-start screens whose device crosses the seam. |
101
+ | `style.palette` | plan | `{ mode: "tonal" \| "family" \| "sequence", colors: [hex…], tone? }`. Background for screens that omit one; ONE text colour per set (see Art direction). |
102
+ | `style.panorama` | plan | `{ spans?: [[0,1], …], straddle?: true \| [screen…], decoration?: "orbs" \| "honeycomb" \| "wave" \| "none" }`. Adjacent screens share one continuous background; `straddle` picks the span-start screens whose device crosses the seam (only by ≥ 18% of it). |
103
+ | `style.hero`, `style.rhythm`, `style.callouts`, `style.shadows` | plan | 0.5.0 — see Art direction. |
104
+ | `art` | plan | 0.5.0 `[{ id, url, width, height, faces? }]` brand art (uploaded-asset URLs only). |
105
+ | `callout`, `mascot` | screen | 0.5.0 — a magnified crop of the selling element · brand art placement. |
100
106
  | `style.font` | plan | One of `COMPOSE_FONTS` (the editor's font list). |
101
107
  | `focus`, `crop`, `presentation`, `tilt`, `badge` | screen | Focus band · zoom crop `{x,y,w,h}` · per-screen overrides · a social-proof pill above the headline |
102
108
 
103
109
  Output shapes:
104
110
 
105
- - **Headline colour.** An explicit `headlineColor` / `subheadlineColor` always wins. Otherwise, on a
106
- palette background or in a panorama span, it is white or `#111827`, whichever has the better
107
- worst-case WCAG contrast. The contrast is checked against the background actually behind the text
108
- block, sampled over it. For a span, that is the span's N·W gradient, which uses the FIRST
109
- screen's background, at this screen's offset. A screen with its own explicit background outside a
110
- span keeps the white default.
111
+ - **Text colour (0.5.0: ONE per set).** An explicit `headlineColor` / `subheadlineColor` always
112
+ wins. Otherwise EVERY screen takes part in choosing one set colour: `harmonize` tries a dark and a
113
+ light candidate (a deep / pale tint of the brand hue for tonal palettes, else `#111827` / white),
114
+ sampling every screen on a 5×7 grid. Palette / span backgrounds may be re-toned (moved away from
115
+ the text in small steps until the worst sample passes WCAG 4.5); explicit backgrounds are sampled
116
+ as they are and never recoloured. The candidate needing the least re-toning wins (the other only
117
+ if it also passes on the explicit backgrounds for ≤ 0.15 more). If the set colour still can't reach
118
+ 4.5 on an explicit background, ONLY that screen gets its own best colour (a panorama span whose
119
+ fill is explicit counts as ONE unit: one colour for the whole span, so the scene stays consistent) —
120
+ `report.screens[i].textColorSource === "screen"` (else `"set"`, or `"plan"` for an explicit
121
+ `headlineColor`). An explicit-only set keeps white whenever white passes (0.4.0). The headline and
122
+ subheadline are also checked densely right behind their boxes; a screen that no colour can bring to
123
+ 4.5 warns `contrast-low` — never silent. The subheadline's brand tint is used only when it clears
124
+ 4.6 (a margin, so it never lands at 4.49).
111
125
  - **Frameless / zoom** is a plain `type: 'image'` layer:
112
126
  - Its `src` is the uploaded screenshot, with `crossOrigin: 'anonymous'`.
113
127
  - It has a rounded `clipPath` Rect in the image's local space, `imageCornerRadius` (canvas units,
@@ -137,6 +151,58 @@ Output shapes:
137
151
  - **Badge** is a rounded `Rect` pill plus a `Textbox` (`templateKey: 'badge'`). They sit in a badge row
138
152
  above the headline, reserved set-wide, so they never collide with the text or the device.
139
153
 
154
+ ### Art direction (0.5.0)
155
+
156
+ - **Line breaks.** Headlines and subheadlines are set in `breakLines` lines. An explicit `\n` is ALWAYS
157
+ a break — lines the author broke are never merged, on any device or role. Only a line (explicit or
158
+ not) wider than the box is broken further: into the greedy line count, re-balanced (no single word
159
+ on the last line, breaks after punctuation preferred, never after "the"/"to"/…; past 40 tokens it
160
+ breaks greedily, so absurd copy fails fast). CJK / kana / Hangul count ~1 em per glyph and may
161
+ break between glyphs. The breaks are emitted as `\n` in the Textbox. A single word wider than the
162
+ box throws (Fabric can't wrap inside a word — it would widen the box past the margins).
163
+ Subheadline: 0.5× the headline, weight 600, full opacity, tinted 18% toward the brand hue when
164
+ that still passes AA. Text box 0.88 W.
165
+ - **Tonal palette.** `{ mode: "tonal", colors: [base, accent?], tone: "light" | "vivid" | "deep" }`:
166
+ screen i gets tonal step i % 4 of the base hue as a vertical gradient (the hero step 0). The accent
167
+ fills badge pills. Tonal panorama spans run one ramp through their members' tones.
168
+ - **Hero** (`style.hero`, default ON for screen 1; `false` = off): headline 1.25× (stepped down in
169
+ 0.05s — to 1× if needed — so it never has more lines than at the set size; `scale` 1–1.4), its own group with bleed `deep`, optional `layout`, `tilt`,
170
+ `badge`, `mascot`. `report.screens[i].role` is `hero`.
171
+ - **Rhythm** (`style.rhythm: { every, treatment }`): screens where `i % every === every − 1` are
172
+ `accent`s — `text-bottom` layout, or `callout` (a forced callout + deep bleed). All `set`-role
173
+ screens still share one group, type size, text area, scale and baseline.
174
+ - **Callouts** (`screen.callout` crop, or `style.callouts: "auto"` from a focus band ≤ 50% tall): an
175
+ image layer with the SAME uploaded `src` + `cropX`/`cropY`, rounded, shadowed, magnified 1.6–2.2×
176
+ (down to 1.25× to fit), inside 3.5% side margins, between the text block and a 4% far-edge margin.
177
+ It may cover its own source slice (the pop-out), but it hides at most `CALLOUT_MAX_FOCUS_COVER`
178
+ (35%) of the REST of the focus band: it is centred on its source when that allows, else moved
179
+ up/down or out to a side edge (breaking out of the device), else made smaller; if nothing works it
180
+ is skipped (`callout-skipped`). `report.screens[i].callout.focusCover` is the share hidden. Never on
181
+ zoom cards or a straddling device.
182
+ - **Mascots** (`plan.art` + `screen.mascot` / `style.hero.mascot`): an image layer (uploaded src,
183
+ `flipX` to face into the canvas when `faces` is given, shadow). Anchors `headline` / `device-top` /
184
+ `device-side` / `seam` are tried in order with fallbacks at 100/85/70% size; a placement must clear
185
+ every text line, the badge, the subheadline, the callout, every other mascot on the screen
186
+ (including a neighbour's seam half) and the focus band's core (its outer 10% W may be overlapped).
187
+ `seam` puts one mascot across a panorama seam, emitted on both screens (±W). Otherwise
188
+ `mascot-skipped`.
189
+ - **Badges** keep their label at WCAG ≥ 4.5 on the pill: a solid (tonal accent) pill is toned
190
+ toward black or white until it does; a translucent one gets fainter.
191
+ - **Shadows** (`style.shadows`, default true), tinted with the brand hue: device layers get
192
+ `fabricData.deviceShadow = { color, blur, offsetX, offsetY }` in CANVAS units — the editor casts it
193
+ from the frame's screenshot (a Fabric shadow on the frame PNG would fall into its transparent screen
194
+ hole); frameless / zoom / callouts / mascots get a Fabric `shadow` in object units.
195
+ `validateTemplate` checks `deviceShadow` (device layers only; colour ≤ 64 chars; blur 0…canvasWidth;
196
+ offsets ±canvasWidth).
197
+ - **Panorama motif** (`decoration: "honeycomb" | "wave"`): the motif is laid out across the whole span,
198
+ but each screen emits only ITS part (the hexagons / wave segments touching it plus a 2-stroke
199
+ overdraw) as one locked `Path` in its own coordinates, rounded to 0.1 unit; both neighbours emit
200
+ the same points around a seam, so it runs on continuously. (0.5.0-pre emitted the whole N·W path on
201
+ every screen — O(N²) bytes.)
202
+ - **Handoff size.** `MAX_HANDOFF_BYTES` (256 KiB, the server's cap) and `handoffBytes(template)` (UTF-8
203
+ size of the `{ template }` request body). The skill's `lint` / `compose` / `publish` / `variants`
204
+ error over the cap and warn over 80%. A 30-screen phone + iPad + Mac honeycomb panorama is ≈ 166 KB.
205
+
140
206
  `report.warnings` (`ComposeWarning`) flags:
141
207
 
142
208
  - a headline over 5 words, or estimated at more than 2 lines at the set size;
@@ -147,31 +213,41 @@ Output shapes:
147
213
  - skipped straddles, and more than one straddle per set;
148
214
  - tilts reduced to keep the side margins;
149
215
  - zoom crops that can't show their whole focus band;
150
- - long badges.
216
+ - long badges;
217
+ - 0.5.0: a stranded last headline word (`headline-orphan`), a screen no colour can bring to AA
218
+ (`contrast-low`), a callout that can't be magnified or placed without hiding the focus band
219
+ (`callout-skipped`), no room for a mascot (`mascot-skipped`).
220
+
221
+ Backward compatibility (0.5.0): a 0.4.0 plan still composes, but screen 1 is now the hero (1.25×
222
+ headline, `deep` bleed, its own device pose) and devices get shadows — `style.hero: false` /
223
+ `style.shadows: false` restore the 0.4.0 look. Balanced line breaks can change where a headline
224
+ wraps, and the new warnings can make `--strict` fail for a plan that passed before.
151
225
 
152
226
  Warnings never block composing.
153
227
 
154
- `makeVariants(plan)` returns three distinct concepts, intended as Product Page Optimization test
155
- candidates. Each is named `<name> — A Framed` / `— B Frameless` / `— C Panorama`:
228
+ `makeVariants(plan)` returns three distinct ART DIRECTIONS, intended as Product Page Optimization
229
+ test candidates. Each is named `<name> — A Brand Classic` / `— B Clean Frameless` / `— C Story Panorama`:
156
230
 
157
- - **A:** device frames, straight, all `text-top`.
158
- - **B:** frameless. Screens with a `crop`, or a `focus` band ≤ 45% tall, become zoom cards.
159
- - **C:** panorama spans: the input's `panorama.spans`, or adjacent pairs, with a continuous
160
- background and seam orbs. Only screen 1 (the hero) is tilted (the input's `style.tilt`, or 8°)
161
- and straddles its seam, unless the input sets `straddle`. Every other device stays centred and
162
- straight.
231
+ - **A Brand Classic:** device frames on the `vivid` tonal palette, hero with the mascot (first `art`)
232
+ + badge, `callouts: "auto"` (plus explicit ones), rhythm every 4 (`text-bottom`).
233
+ - **B Clean Frameless:** `light` tonal palette, full-width frameless screenshots with a `deep` bleed
234
+ (a zoom card only for an explicit `crop` — B1), callouts, hero at 1.3× with the mascot.
235
+ - **C Story Panorama:** `deep` tonal palette; spans = the input's `panorama.spans`, else triples and
236
+ pairs (`panoramaRuns`); `decoration` = the input's (not `orbs`), else `wave`; a mascot crossing the
237
+ first seam of every span but the hero's; the hero tilted (the input's `style.tilt`, or 8°); bleed
238
+ `deep`; a straddle only if the input asks.
163
239
 
164
240
  **Kept from the input** in every concept:
165
241
  - copy (headline / subheadline / badge) and `headlineColor` / `subheadlineColor`;
166
- - `background`, `deviceId`, `screenshot`, `focus`, `crop`;
242
+ - `background`, `deviceId`, `screenshot`, `focus`, `crop`, `callout`, `mascot`, `art`;
167
243
  - canvas size;
168
- - `style.palette`, `style.font` and `style.bleed` (default `auto`).
244
+ - the brand colours (`style.palette.colors`; else the first screen's background colour), `style.font`,
245
+ and `style.bleed: none`.
169
246
 
170
247
  **Overridden:**
171
- - every `layout` becomes `text-top`: screens only share one device scale and baseline when they
172
- share a layout;
248
+ - every `layout` becomes `text-top` (the hero and rhythm accents differ on purpose);
173
249
  - per-screen `presentation` and `tilt` are dropped;
174
- - A and B are straight, with no panorama.
250
+ - palette mode → `tonal` with the concept's tone; hero, rhythm, callouts, panorama per concept.
175
251
 
176
252
  ## Layer order
177
253
 
@@ -236,7 +312,10 @@ and the editor converts it on import.
236
312
  > Consumers: a template in the 0.3.0 shape needs an editor that understands `fabricData.screenshot`
237
313
  > (appshoteditor.com with the matching importer). 0.4.0 adds no new layer shapes that the 0.3.0 validator
238
314
  > or importer would reject: frameless/zoom are ordinary image layers, and panorama and badges are
239
- > ordinary shape and text layers.
315
+ > ordinary shape and text layers. 0.5.0 likewise: callouts and mascots are image layers with
316
+ > uploaded-asset srcs, motifs are `Path` shape layers — all accepted by the 0.4.0 validator (and by
317
+ > 0.5.0's, which additionally checks the shape of `deviceShadow`). Only the device shadow needs a
318
+ > 0.5.0-aware editor to render; older editors ignore it.
240
319
 
241
320
  Zero runtime dependencies. The `schemaVersion` is the compatibility contract between producers and the editor.
242
321
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@appshoteditor/shot-dsl",
3
- "version": "0.4.0",
4
- "description": "App Shot Editor layout DSL + device-frame geometry — framework-free building blocks for composing editable App Store screenshot layouts. Intended for use via a bundler (Vite, esbuild, etc.).",
3
+ "version": "0.5.0",
4
+ "description": "App Shot Editor layout DSL + device-frame geometry \u2014 framework-free building blocks for composing editable App Store screenshot layouts. Intended for use via a bundler (Vite, esbuild, etc.).",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./src/index.ts",
@@ -23,7 +23,10 @@
23
23
  "src/device-frames.ts",
24
24
  "src/layout-system.ts",
25
25
  "src/color.ts",
26
- "src/variants.ts"
26
+ "src/variants.ts",
27
+ "src/typography.ts",
28
+ "src/palette.ts",
29
+ "src/decor.ts"
27
30
  ],
28
31
  "sideEffects": false,
29
32
  "keywords": [