@appshoteditor/shot-dsl 0.5.1 → 0.5.3
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 +17 -8
- package/package.json +1 -1
- package/src/compose.ts +11 -1
- package/src/layout-system.ts +84 -17
- package/src/variants.ts +21 -13
package/README.md
CHANGED
|
@@ -60,11 +60,18 @@ validateTemplate(template); // { valid: true, errors: [] }
|
|
|
60
60
|
2. **No tangents.** A device (or frameless screenshot) either clears the far edge by ≥ 4% of H or
|
|
61
61
|
bleeds off it by ≥ 12% of its own height. The band in between is the "just touching" look, and
|
|
62
62
|
`solveVertical` never emits it. `style.bleed` picks how a tangent is resolved:
|
|
63
|
-
- `auto` (default): a device that already clears by the margin
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
- `auto` (default): a device that already clears by the margin, or already bleeds decisively
|
|
64
|
+
(≥ 12%, without cropping its focus band), stays as it is. Otherwise it grows toward the
|
|
65
|
+
90%-of-W cap; if that's still short of a 12% bleed, "minimum disturbance" decides between
|
|
66
|
+
clearing and shifting down: it clears at the near edge (no shift) when that clear scale is
|
|
67
|
+
≥ 88% of the grown/capped scale (`NO_TANGENT.clearOverShift`); otherwise it shifts down only
|
|
68
|
+
as far as exactly a 12% bleed.
|
|
66
69
|
- `none`: always fully visible.
|
|
67
|
-
- `deep`: bleed
|
|
70
|
+
- `deep`: grows the subject toward a 25% bleed (still capped at 90% W). When growth alone can't
|
|
71
|
+
reach 25% and a down-shift would be the only way to reach even 12%, the same minimum-disturbance
|
|
72
|
+
rule applies: it clears at the near edge when that costs ≤ 12% of the grown/capped scale;
|
|
73
|
+
otherwise it shifts down only as far as exactly a 12% bleed needs — never deeper, even though
|
|
74
|
+
the target is 25%.
|
|
68
75
|
3. **No side tangents.** No subject is wider than 90% of W (`NO_TANGENT.maxWidth`). The part that is
|
|
69
76
|
actually on the canvas keeps ≥ 5% of W from both side edges. The only side bleed is a deliberate
|
|
70
77
|
panorama straddle across a seam.
|
|
@@ -96,7 +103,7 @@ Plan additions (all optional; a 0.3.0 plan composes and validates unchanged):
|
|
|
96
103
|
| Field | Where | Meaning |
|
|
97
104
|
| --- | --- | --- |
|
|
98
105
|
| `style.presentation` | plan | `device` (default) · `frameless` (rounded screenshot + shadow) · `zoom` (magnified crop of `crop`, else the centre of the `focus` band) |
|
|
99
|
-
| `style.tilt`, `style.tiltScreens` | plan | Degrees (clockwise) applied only to the listed screen indices. A tilted subject prefers a bleed under `auto`/`deep
|
|
106
|
+
| `style.tilt`, `style.tiltScreens` | plan | Degrees (clockwise) applied only to the listed screen indices. A tilted subject prefers a deeper bleed under `auto`/`deep`, but clears instead when its scale cap (from keeping the side margins) stops it reaching a decisive bleed without a big down-shift (minimum disturbance, see above); the tilt itself is reduced (`tilt-reduced`) if it can't keep the side margins at all. |
|
|
100
107
|
| `style.bleed` | plan | `auto` · `none` · `deep` (see above) |
|
|
101
108
|
| `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
109
|
| `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). |
|
|
@@ -232,9 +239,9 @@ Warnings never block composing.
|
|
|
232
239
|
test candidates. Each is named `<name> — A Brand Classic` / `— B Clean Frameless` / `— C Story Panorama`:
|
|
233
240
|
|
|
234
241
|
- **A Brand Classic:** device frames on the `vivid` tonal palette, hero with the mascot (first `art`)
|
|
235
|
-
+ badge,
|
|
242
|
+
+ badge, callouts (the input's explicit `style.callouts`, else `auto`), rhythm every 4 (`text-bottom`).
|
|
236
243
|
- **B Clean Frameless:** `light` tonal palette, full-width frameless screenshots with a `deep` bleed
|
|
237
|
-
(a zoom card only for an explicit `crop` — B1), callouts, hero at 1.3× with the mascot.
|
|
244
|
+
(a zoom card only for an explicit `crop` — B1), callouts (same default as A), hero at 1.3× with the mascot.
|
|
238
245
|
- **C Story Panorama:** `deep` tonal palette; spans = the input's `panorama.spans`, else triples and
|
|
239
246
|
pairs (`panoramaRuns`); `decoration` = the input's (not `orbs`), else `wave`; a mascot crossing the
|
|
240
247
|
first seam of every span but the hero's; the hero tilted (the input's `style.tilt`, or 8°); bleed
|
|
@@ -250,7 +257,9 @@ test candidates. Each is named `<name> — A Brand Classic` / `— B Clean Frame
|
|
|
250
257
|
**Overridden:**
|
|
251
258
|
- every `layout` becomes `text-top` (the hero and rhythm accents differ on purpose);
|
|
252
259
|
- per-screen `presentation` and `tilt` are dropped;
|
|
253
|
-
- palette mode → `tonal` with the concept's tone; hero, rhythm,
|
|
260
|
+
- palette mode → `tonal` with the concept's tone; hero, rhythm, panorama per concept;
|
|
261
|
+
- `callouts` per concept too, but only C's is fixed (`none`) — A and B keep the input's explicit
|
|
262
|
+
`style.callouts`, defaulting to `auto` only when the input leaves it unset.
|
|
254
263
|
|
|
255
264
|
## Layer order
|
|
256
265
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appshoteditor/shot-dsl",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
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.).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/compose.ts
CHANGED
|
@@ -157,7 +157,10 @@ export interface ComposeMascot {
|
|
|
157
157
|
flip?: boolean;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* The hero (screen 1 by default): larger type, optional mascot + badge, and/or tilt. Prefers a deeper
|
|
162
|
+
* bleed, but clears rather than leave a gap under the text.
|
|
163
|
+
*/
|
|
161
164
|
export interface ComposeHero {
|
|
162
165
|
/** Hero screen index. Default 0. */
|
|
163
166
|
screen?: number;
|
|
@@ -295,6 +298,12 @@ export interface ComposeScreenMetrics {
|
|
|
295
298
|
headlineSize: number;
|
|
296
299
|
/** Headline box top ÷ H (identical across a set by construction). */
|
|
297
300
|
headlineTop: number;
|
|
301
|
+
/**
|
|
302
|
+
* Bottom of the reserved text block (headline + subheadline + badge row) ÷ H. For `text-bottom`
|
|
303
|
+
* layouts the text block sits near the canvas bottom (not the top), so `top − textBottom` (the
|
|
304
|
+
* subject's top minus this) isn't a meaningful "gap under the text" for that layout.
|
|
305
|
+
*/
|
|
306
|
+
textBottom: number;
|
|
298
307
|
tilt: number;
|
|
299
308
|
/** Horizontal centre ÷ W (≠ 0.5 for a straddling panorama device). */
|
|
300
309
|
centerX: number;
|
|
@@ -2023,6 +2032,7 @@ export function composeSet(plan: ComposePlan): { template: Template; report: Com
|
|
|
2023
2032
|
tangent: inTangentZone(overshoot, p.boxHeight, H),
|
|
2024
2033
|
headlineSize: typo.headlineSize / W,
|
|
2025
2034
|
headlineTop: g.headlineTop / H,
|
|
2035
|
+
textBottom: (g.areaTop + typo.textArea) / H,
|
|
2026
2036
|
tilt: p.angle,
|
|
2027
2037
|
centerX: cx / W,
|
|
2028
2038
|
role: r.role,
|
package/src/layout-system.ts
CHANGED
|
@@ -17,7 +17,11 @@ export const NO_TANGENT = {
|
|
|
17
17
|
clearGap: 0.04,
|
|
18
18
|
/** A subject that bleeds must lose at least this × its own (rotated) height. */
|
|
19
19
|
minBleed: 0.12,
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* `bleed: "deep"` target, × the subject's height. Reached by growing the subject (up to
|
|
22
|
+
* `maxWidth`·W) — never by shifting it down past a decisive (`minBleed`) overshoot, since a
|
|
23
|
+
* further down-shift would just be empty space between the text and the subject.
|
|
24
|
+
*/
|
|
21
25
|
deepBleed: 0.25,
|
|
22
26
|
/** The focus band must end at least this × H inside the canvas edge. */
|
|
23
27
|
focusSafe: 0.02,
|
|
@@ -32,6 +36,14 @@ export const NO_TANGENT = {
|
|
|
32
36
|
focusSideSafe: 0.02,
|
|
33
37
|
/** A clear-with-margin option that shrinks the subject below this × its natural scale is rejected. */
|
|
34
38
|
minClearScale: 0.6,
|
|
39
|
+
/**
|
|
40
|
+
* "Minimum disturbance": when growing toward a decisive bleed is capped short and the only way to
|
|
41
|
+
* reach `minBleed` is to shift the subject down (away from the text), shrinking it to clear the
|
|
42
|
+
* edge instead is preferred whenever that clear scale is still at least this × the (grown/capped)
|
|
43
|
+
* bleed scale — shrinking by ≤ 12% to clear beats pushing the subject down and leaving a gap under
|
|
44
|
+
* the text.
|
|
45
|
+
*/
|
|
46
|
+
clearOverShift: 0.88,
|
|
35
47
|
/** Below this × the layout's target scale the copy leaves no usable room (composeSet throws). */
|
|
36
48
|
minScale: 0.5
|
|
37
49
|
} as const;
|
|
@@ -182,7 +194,7 @@ export interface VerticalInput {
|
|
|
182
194
|
/** See `focusReach`; null = no focus band marked (unconstrained). */
|
|
183
195
|
focusReach: number | null;
|
|
184
196
|
bleed: BleedPreference;
|
|
185
|
-
/** Tilted subjects:
|
|
197
|
+
/** Tilted subjects: prefers a deeper bleed, but clears rather than leave a gap under the text. */
|
|
186
198
|
preferBleed?: boolean;
|
|
187
199
|
/** Hard upper bound on the scale (e.g. from the horizontal rules); applied to every option. */
|
|
188
200
|
scaleCap?: number;
|
|
@@ -207,14 +219,28 @@ export interface VerticalResult {
|
|
|
207
219
|
* ≥ `clearGap`·H or bleeds by ≥ `minBleed` of the subject's height — never in between — and never
|
|
208
220
|
* pushes the focus band off-canvas. Options:
|
|
209
221
|
* - CLEAR: keep the near edge, shrink until the far edge clears with the margin.
|
|
210
|
-
* - BLEED: grow (up to
|
|
211
|
-
* the
|
|
212
|
-
*
|
|
213
|
-
*
|
|
222
|
+
* - BLEED: grow (up to its size cap) toward the target overshoot; only when growth alone can't get
|
|
223
|
+
* there (the size cap bites first) does the subject need to shift toward the far edge to reach a
|
|
224
|
+
* decisive (`minBleed`) overshoot. When that shift would itself stay focus-safe, "minimum
|
|
225
|
+
* disturbance" is tried first: if shrinking the (grown/capped) subject back down clears the edge
|
|
226
|
+
* with the margin and loses at most `1 − clearOverShift` of that scale, that clear placement
|
|
227
|
+
* (sitting at near0, undisturbed) is used INSTEAD of shifting it down — see
|
|
228
|
+
* `NO_TANGENT.clearOverShift`. Only when clearing would shrink it more than that (or the shift
|
|
229
|
+
* wouldn't be focus-safe in the first place, so there's no real shift to prefer clearing over) does
|
|
230
|
+
* it fall back to shifting down, and even then never further than `minBleed` needs — never all the
|
|
231
|
+
* way to a deeper target, which would otherwise leave empty space between the text and the subject.
|
|
232
|
+
* If the (shifted) result hides the focus band, the placement instead targets the smallest decisive
|
|
233
|
+
* bleed (`minBleed`) that stays focus-safe — or near0 itself, when near0 already sits further down
|
|
234
|
+
* than that — else the bleed option is unavailable.
|
|
235
|
+
* `none` → always clear. `deep` → grow toward `deepBleed` (≥ `minBleed` at the width cap, ≥ `deepBleed`
|
|
236
|
+
* whenever growth alone reaches it, else the minimum-disturbance clear above), or the most the focus
|
|
237
|
+
* allows, else clear.
|
|
214
238
|
* `auto` → keep the natural placement when it already clears by the margin or is a focus-safe
|
|
215
|
-
* decisive bleed; otherwise (the tangent zone) PREFER the bleed option
|
|
216
|
-
* when
|
|
217
|
-
*
|
|
239
|
+
* decisive bleed; otherwise (the tangent zone) PREFER the bleed option — or its minimum-disturbance
|
|
240
|
+
* clear, when a shallow shrink beats the shift a decisive bleed would need — and fall back to clear
|
|
241
|
+
* when no ≥ `minBleed` bleed keeps the focus band visible.
|
|
242
|
+
* `preferBleed` (tilt) also turns a natural clear into a bleed — or its minimum-disturbance clear, when
|
|
243
|
+
* the tilt's scale cap stops it from reaching a decisive bleed without a shift.
|
|
218
244
|
*/
|
|
219
245
|
export function solveVertical(inp: VerticalInput): VerticalResult {
|
|
220
246
|
const W = inp.canvasWidth;
|
|
@@ -240,24 +266,63 @@ export function solveVertical(inp: VerticalInput): VerticalResult {
|
|
|
240
266
|
return make(s, near0, s < s0 - EPS ? 'shrunk to clear the edge with a margin' : 'clears the edge');
|
|
241
267
|
};
|
|
242
268
|
|
|
243
|
-
/**
|
|
269
|
+
/**
|
|
270
|
+
* Smallest move reaching overshoot ≥ t·height, then made focus-safe (null if impossible). Growing
|
|
271
|
+
* the subject (up to its size cap — the width cap, or a tighter `scaleCap` when one binds) is
|
|
272
|
+
* always tried first. Only when that growth is capped short of t does the subject need to shift
|
|
273
|
+
* toward the far edge to reach a decisive (`minBleed`) overshoot — and even then, only when that
|
|
274
|
+
* shift would actually stay focus-safe, "minimum disturbance" is tried first: shrinking the grown
|
|
275
|
+
* subject back down to clear the edge, if that only costs ≤ `1 − clearOverShift` of its scale,
|
|
276
|
+
* beats shifting it away from the text. (When the shift itself wouldn't be focus-safe, there's no
|
|
277
|
+
* real shift to prefer clearing over, so this is skipped — the focus-constrained fallback below
|
|
278
|
+
* decides instead, exactly as it did before minimum disturbance existed.) Otherwise — clearing
|
|
279
|
+
* would shrink it more than that, or no shift is possible to compare against — it shifts down, and
|
|
280
|
+
* even then never further than `minBleed` needs, never all the way to t: a deeper target like
|
|
281
|
+
* `deepBleed` is something to grow toward, not something worth shifting the subject away from the
|
|
282
|
+
* text to force.
|
|
283
|
+
*/
|
|
244
284
|
const bleedOption = (t: number): VerticalResult | null => {
|
|
245
285
|
let s = s0;
|
|
246
286
|
let a = near0;
|
|
247
287
|
if (overshoot(s, a) < t * s * eh - EPS) {
|
|
248
288
|
const need = (H - a) / ((1 - t) * eh);
|
|
249
289
|
s = Math.max(s0, Math.min(need, Math.max(sCap, s0)));
|
|
250
|
-
if (overshoot(s, a) <
|
|
290
|
+
if (overshoot(s, a) < NO_TANGENT.minBleed * s * eh - EPS) {
|
|
291
|
+
const shifted = H - s * eh * (1 - NO_TANGENT.minBleed);
|
|
292
|
+
// Minimum disturbance only competes with a shift that would actually work: if shifting
|
|
293
|
+
// down to `shifted` wouldn't even keep the focus band visible, there's no real shift
|
|
294
|
+
// alternative to prefer clearing over — let the focus-constrained path below (and its
|
|
295
|
+
// own reasons) decide instead, same as before minimum disturbance existed.
|
|
296
|
+
if (focusOk(s, shifted)) {
|
|
297
|
+
// Prefer shrinking to clear (undisturbed, at near0) over shifting the subject down,
|
|
298
|
+
// as long as that shrink is shallow (≤ 1 − clearOverShift of the bleed's own
|
|
299
|
+
// grown/capped scale) — a small shrink beats a gap-forcing down-shift.
|
|
300
|
+
const clear = clearOption();
|
|
301
|
+
if (clear && clear.scale >= NO_TANGENT.clearOverShift * s - EPS) {
|
|
302
|
+
const reason =
|
|
303
|
+
clear.scale < s0 - EPS
|
|
304
|
+
? `shrunk ${Math.round((1 - clear.scale / s0) * 100)}% to clear instead of shifting down for a bleed`
|
|
305
|
+
: 'clears the edge (preferred over shifting down for a bleed)';
|
|
306
|
+
return { ...clear, reason };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
a = shifted;
|
|
310
|
+
}
|
|
251
311
|
}
|
|
252
|
-
|
|
312
|
+
const reason =
|
|
313
|
+
overshoot(s, a) >= t * s * eh - EPS
|
|
314
|
+
? `bleeds ${Math.round(t * 100)}%+`
|
|
315
|
+
: `grows to its size cap (no gap forced to reach ${Math.round(t * 100)}%+)`;
|
|
316
|
+
if (focusOk(s, a)) return make(s, a, reason);
|
|
253
317
|
// Focus-constrained: keep the scale (≤ what the focus allows at near0) and bleed only as deep
|
|
254
|
-
// as the focus band allows — accepted if that is still a decisive (≥ minBleed) bleed.
|
|
318
|
+
// as the focus band allows — accepted if that is still a decisive (≥ minBleed) bleed. The shift
|
|
319
|
+
// itself only ever targets minBleed (never t), for the same no-gap reason as above.
|
|
255
320
|
if (inp.focusReach == null) return null;
|
|
256
321
|
const fr = inp.focusReach;
|
|
257
322
|
const sF = Math.min(s, Math.max(sCap, s0), (focusLimit - near0) / fr);
|
|
258
323
|
const denom = eh * (1 - NO_TANGENT.minBleed) - fr;
|
|
259
324
|
if (!(sF > 0) || denom <= 0 || sF < (H - focusLimit) / denom - EPS) return null;
|
|
260
|
-
const aF = Math.max(near0, Math.min(focusLimit - sF * fr, H - sF * eh * (1 -
|
|
325
|
+
const aF = Math.max(near0, Math.min(focusLimit - sF * fr, H - sF * eh * (1 - NO_TANGENT.minBleed)));
|
|
261
326
|
if (overshoot(sF, aF) < NO_TANGENT.minBleed * sF * eh - EPS || !focusOk(sF, aF)) return null;
|
|
262
327
|
return make(sF, aF, 'bleed reduced to keep the focus band visible');
|
|
263
328
|
};
|
|
@@ -279,10 +344,12 @@ export function solveVertical(inp: VerticalInput): VerticalResult {
|
|
|
279
344
|
// auto
|
|
280
345
|
if (naturalClear && !inp.preferBleed) return make(s0, near0, 'natural placement clears the edge');
|
|
281
346
|
if (naturalBleed && focusOk(s0, near0)) return make(s0, near0, 'natural placement bleeds decisively');
|
|
282
|
-
// In the tangent zone (or a tilted subject): resolve by bleeding decisively
|
|
283
|
-
//
|
|
347
|
+
// In the tangent zone (or a tilted subject): resolve by bleeding decisively — or its
|
|
348
|
+
// minimum-disturbance clear, when the size cap stops a decisive bleed without a shift.
|
|
349
|
+
// Clear-with-margin (the naturalClear fallback below) only when no ≥ minBleed bleed keeps the
|
|
350
|
+
// focus band visible.
|
|
284
351
|
const bleed = bleedOption(NO_TANGENT.minBleed);
|
|
285
|
-
if (bleed) return inp.preferBleed ? { ...bleed, reason: `${bleed.reason} (tilt pairs with a bleed)` } : bleed;
|
|
352
|
+
if (bleed) return inp.preferBleed && bleed.mode === 'bleed' ? { ...bleed, reason: `${bleed.reason} (tilt pairs with a bleed)` } : bleed;
|
|
286
353
|
if (naturalClear) return make(s0, near0, 'natural placement clears the edge');
|
|
287
354
|
return clearOption() ?? forcedClear();
|
|
288
355
|
}
|
package/src/variants.ts
CHANGED
|
@@ -14,19 +14,22 @@ import type { Motif } from './decor';
|
|
|
14
14
|
* Three DISTINCT art directions from one plan — Product Page Optimization test candidates, not tweaks
|
|
15
15
|
* (shot-dsl 0.5.0):
|
|
16
16
|
* - **A Brand Classic**: device frames on the vivid tonal brand palette; hero with the mascot + badge;
|
|
17
|
-
* callouts on the selling screens (explicit, else from a tight `focus
|
|
17
|
+
* callouts on the selling screens by default (explicit, else from a tight `focus`; the input can
|
|
18
|
+
* turn this off with `style.callouts: 'none'`); an accent rhythm (every 4th screen text-bottom); shadows.
|
|
18
19
|
* - **B Clean Frameless**: pale tonal backgrounds with dark text; big full-width frameless
|
|
19
20
|
* screenshots bleeding deep off the bottom (a zoom card only where the plan marks an explicit
|
|
20
|
-
* `crop`); magnified callouts (explicit, else derived from a tight `focus
|
|
21
|
-
* + the mascot.
|
|
21
|
+
* `crop`); magnified callouts by default (explicit, else derived from a tight `focus`; same
|
|
22
|
+
* `style.callouts: 'none'` opt-out as A); hero with large type + the mascot.
|
|
22
23
|
* - **C Story Panorama**: the deep tonal palette as ONE continuous scene across triples/pairs of
|
|
23
24
|
* screens with a flowing motif (`style.panorama.decoration`, default `wave`), the mascot
|
|
24
|
-
* travelling across the seams, a tilted hero
|
|
25
|
+
* travelling across the seams, a tilted hero that prefers a deeper bleed (but clears rather than
|
|
26
|
+
* leave a gap under the text).
|
|
25
27
|
*
|
|
26
28
|
* KEPT from the input (every concept): name (+ suffix), canvas size, copy (headline / subheadline /
|
|
27
29
|
* badge), colours (headlineColor / subheadlineColor), `background`, `deviceId`, `screenshot`,
|
|
28
|
-
* `focus`, `crop`, `callout`, per-screen `mascot`, `art`,
|
|
29
|
-
*
|
|
30
|
+
* `focus`, `crop`, `callout`, per-screen `mascot`, `art`, the style's `font`, its `bleed` preference
|
|
31
|
+
* (default `auto`; B and C use `deep` unless the input says `none`), and its `callouts` preference
|
|
32
|
+
* (default `auto` for A and B if the input doesn't set one explicitly; C is always `none`).
|
|
30
33
|
*
|
|
31
34
|
* BRAND COLOURS: `style.palette.colors` (the first is the base, a tonal palette's second is the
|
|
32
35
|
* accent); without a palette, the first screen's background colour. Concepts then use
|
|
@@ -35,8 +38,10 @@ import type { Motif } from './decor';
|
|
|
35
38
|
* OVERRIDDEN (the concept decides these):
|
|
36
39
|
* - every screen's `layout` → `text-top` (one shared system; the hero and rhythm accents differ);
|
|
37
40
|
* - per-screen `presentation` / `tilt` are dropped;
|
|
38
|
-
* - `hero`, `rhythm`, `
|
|
39
|
-
*
|
|
41
|
+
* - `hero`, `rhythm`, `tilt`/`tiltScreens` and `panorama` are set per concept (C keeps the input's
|
|
42
|
+
* `panorama.spans` / `decoration` when given);
|
|
43
|
+
* - `callouts` is set per concept too, but only C's is fixed (`none`) regardless of the input — A and
|
|
44
|
+
* B fall back to `auto` only when the input leaves `callouts` unset (see `baseStyle`).
|
|
40
45
|
*/
|
|
41
46
|
export type VariantKey = 'A' | 'B' | 'C';
|
|
42
47
|
|
|
@@ -80,9 +85,14 @@ function tonal(plan: ComposePlan, tone: 'light' | 'vivid' | 'deep'): ComposePale
|
|
|
80
85
|
return { mode: 'tonal', colors: brand.accent ? [brand.base, brand.accent] : [brand.base], tone };
|
|
81
86
|
}
|
|
82
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Style knobs every concept inherits from the input, defaulted the same way for all three: `bleed`
|
|
90
|
+
* (default `auto`), `font` (kept only if set), and `callouts` (default `auto`) — concepts that want a
|
|
91
|
+
* different `callouts` (currently only C, always `none`) set it explicitly afterwards, which wins.
|
|
92
|
+
*/
|
|
83
93
|
function baseStyle(plan: ComposePlan): ComposeStyle {
|
|
84
94
|
const style = plan.style;
|
|
85
|
-
const out: ComposeStyle = { bleed: style?.bleed ?? 'auto' };
|
|
95
|
+
const out: ComposeStyle = { bleed: style?.bleed ?? 'auto', callouts: style?.callouts ?? 'auto' };
|
|
86
96
|
if (style?.font) out.font = style.font;
|
|
87
97
|
return out;
|
|
88
98
|
}
|
|
@@ -160,8 +170,7 @@ export function makeVariants(plan: ComposePlan): ComposeVariant[] {
|
|
|
160
170
|
presentation: 'device',
|
|
161
171
|
palette: tonal(plan, 'vivid'),
|
|
162
172
|
hero: hero(plan, { badge }),
|
|
163
|
-
rhythm: { every: VARIANT_RHYTHM_EVERY, treatment: 'text-bottom' }
|
|
164
|
-
callouts: 'auto'
|
|
173
|
+
rhythm: { every: VARIANT_RHYTHM_EVERY, treatment: 'text-bottom' }
|
|
165
174
|
},
|
|
166
175
|
screens: plan.screens.map(baseScreen)
|
|
167
176
|
});
|
|
@@ -175,8 +184,7 @@ export function makeVariants(plan: ComposePlan): ComposeVariant[] {
|
|
|
175
184
|
bleed: plan.style?.bleed === 'none' ? 'none' : 'deep',
|
|
176
185
|
presentation: 'frameless',
|
|
177
186
|
palette: tonal(plan, 'light'),
|
|
178
|
-
hero: hero(plan, { badge, scale: 1.3 })
|
|
179
|
-
callouts: 'auto'
|
|
187
|
+
hero: hero(plan, { badge, scale: 1.3 })
|
|
180
188
|
},
|
|
181
189
|
screens: plan.screens.map((screen) => {
|
|
182
190
|
const s = baseScreen(screen);
|