gutterpress 0.11.20-alpha.1 → 0.11.20-alpha.2

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.
Files changed (31) hide show
  1. package/dist/api/index.js +2 -2
  2. package/dist/{build-cmf4nbpm.js → build-qmjg9r2d.js} +4 -4
  3. package/dist/checks/pdf/index.d.ts +1 -0
  4. package/dist/checks/pdf/safe-margins.d.ts +3 -0
  5. package/dist/{cli-7shp2kmj.js → cli-7z29z4wz.js} +1 -1
  6. package/dist/{cli-mmm22cza.js → cli-gh0ym80r.js} +1 -1
  7. package/dist/{cli-rtpm04dv.js → cli-j802s31j.js} +5 -5
  8. package/dist/{cli-x1nfhaf1.js → cli-zacevwcy.js} +284 -180
  9. package/dist/cli.js +12 -12
  10. package/dist/{doctor-0d6esj02.js → doctor-fe7tcs4v.js} +2 -2
  11. package/dist/{engine-1v8kmv3a.js → engine-9xb1ky8d.js} +1 -1
  12. package/dist/{engine-adxptzf0.js → engine-yax79k9m.js} +2 -2
  13. package/dist/{ext-65nfh7st.js → ext-ahwxt6gv.js} +4 -4
  14. package/dist/{gutterpress-viewer-0r4rt4de.js → gutterpress-viewer-retj6ym2.js} +343 -98
  15. package/dist/{index-pnvzgftg.js → index-3nr5rzzp.js} +5 -5
  16. package/dist/{index-cngw658w.js → index-6xwtb7cw.js} +1 -1
  17. package/dist/{index-1y885h1j.js → index-razpty47.js} +282 -178
  18. package/dist/index.js +3 -3
  19. package/dist/lib/presets.d.ts +3 -1
  20. package/dist/{manifest.schema-nrykd2c6.json → manifest.schema-74s1ykyv.json} +8 -2
  21. package/dist/{new-y4a2xz3g.js → new-ftd4fec4.js} +4 -4
  22. package/dist/{preflight-kt7wfgbd.js → preflight-8sqarcsr.js} +4 -4
  23. package/dist/preview/file-watcher.d.ts +7 -1
  24. package/dist/{preview-bridge-evecsh4w.js → preview-bridge-8wjcjec3.js} +3 -2
  25. package/dist/{preview-interface-k2mj5kvq.js → preview-interface-nnmrxyqm.js} +37 -41
  26. package/dist/{preview-5ze728kp.js → preview-q76xn95h.js} +4 -4
  27. package/dist/{preview-shell-7jfe6vsc.js → preview-shell-wk9k3gcd.js} +18 -6
  28. package/dist/{publish-5tpcyysh.js → publish-7gdy6gbf.js} +4 -4
  29. package/dist/schema/manifest.types.d.ts +27 -6
  30. package/dist/{validate-p0hkqkfe.js → validate-e3ryegce.js} +4 -4
  31. package/package.json +1 -1
@@ -2101,41 +2101,79 @@
2101
2101
  const h = area.height * scale;
2102
2102
  const b = Math.max(0, area.bleed) * scale;
2103
2103
  const s = Math.max(0, area.safe) * scale;
2104
+ const a = Math.max(0, area.artSafe) * scale;
2105
+ const [left, right] = side === "verso" ? [a, s] : [s, a];
2104
2106
  const trim = { x: side === "verso" ? b : 0, y: b, w: Math.max(0, w - b), h: Math.max(0, h - 2 * b) };
2105
2107
  return {
2106
2108
  sheet: { x: 0, y: 0, w, h },
2107
2109
  trim,
2108
2110
  safe: { x: trim.x + s, y: trim.y + s, w: Math.max(0, trim.w - 2 * s), h: Math.max(0, trim.h - 2 * s) },
2111
+ safeArt: {
2112
+ x: trim.x + left,
2113
+ y: trim.y + a,
2114
+ w: Math.max(0, trim.w - left - right),
2115
+ h: Math.max(0, trim.h - 2 * a)
2116
+ },
2109
2117
  bleed: b
2110
2118
  };
2111
2119
  }
2112
2120
  var SEVERITY = { inside: 0, unsafe: 1, bleed: 2, "off-page": 3 };
2113
- var within = (r, outer, tol) => r.x >= outer.x - tol && r.y >= outer.y - tol && r.x + r.w <= outer.x + outer.w + tol && r.y + r.h <= outer.y + outer.h + tol;
2114
- function classifyRect(r, boxes, tol = 0) {
2115
- if (within(r, boxes.safe, tol))
2116
- return "inside";
2117
- if (within(r, boxes.trim, tol))
2118
- return "unsafe";
2119
- if (boxes.bleed > 0 && within(r, boxes.sheet, tol))
2120
- return "bleed";
2121
- return "off-page";
2122
- }
2123
- function worstKind(kinds) {
2124
- return kinds.reduce((worst, k) => SEVERITY[k] > SEVERITY[worst] ? k : worst, "inside");
2121
+ function classifyRect(r, boxes, rule, tol = 0) {
2122
+ const { sheet, trim } = boxes;
2123
+ const safe = rule === "art" ? boxes.safeArt : boxes.safe;
2124
+ const sides = [
2125
+ [-r.x, -safe.x, -trim.x, -sheet.x],
2126
+ [r.x + r.w, safe.x + safe.w, trim.x + trim.w, sheet.x + sheet.w],
2127
+ [-r.y, -safe.y, -trim.y, -sheet.y],
2128
+ [r.y + r.h, safe.y + safe.h, trim.y + trim.h, sheet.y + sheet.h]
2129
+ ];
2130
+ let worst = { kind: "inside", gap: 0, need: 0 };
2131
+ let beyond = 0;
2132
+ for (const [ink, limit, edge, sheetEdge] of sides) {
2133
+ const past = ink - limit;
2134
+ if (past <= tol || rule === "art" && ink >= sheetEdge - tol)
2135
+ continue;
2136
+ const kind = ink > sheetEdge + tol ? "off-page" : ink > edge + tol ? "bleed" : "unsafe";
2137
+ if (SEVERITY[kind] > SEVERITY[worst.kind] || kind === worst.kind && past > beyond) {
2138
+ worst = { kind, gap: edge - ink, need: edge - limit };
2139
+ beyond = past;
2140
+ }
2141
+ }
2142
+ return worst;
2125
2143
  }
2126
2144
  var INTENT_SELECTOR = ".gp-bleed, .gp-pin, .gp-flush, .gp-behind, .gp-page-full-bleed";
2145
+ var OVERLAY_LAYERS = ["bleed", "safe", "art", "margins"];
2146
+ function layersFrom(state) {
2147
+ const named = state && typeof state === "object" ? state : null;
2148
+ const on = (layer) => named ? named[layer] === true : !!state;
2149
+ return { bleed: on("bleed"), safe: on("safe"), art: on("art"), margins: on("margins") };
2150
+ }
2127
2151
  var OVERLAY_STRENGTHS = ["subtle", "normal", "strong"];
2128
2152
  var HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
2129
2153
  function sanitizeOverlayStyle(style) {
2130
2154
  const s = style && typeof style === "object" ? style : {};
2155
+ const given = s.colors && typeof s.colors === "object" ? s.colors : {};
2156
+ const hex = (v) => typeof v === "string" && HEX_COLOR.test(v.trim()) ? v.trim() : undefined;
2131
2157
  const out = {};
2132
- if (typeof s.color === "string" && HEX_COLOR.test(s.color.trim()))
2133
- out.color = s.color.trim();
2158
+ for (const layer of OVERLAY_LAYERS) {
2159
+ const color = hex(given[layer]) ?? (layer === "safe" ? hex(s.color) : undefined);
2160
+ if (color)
2161
+ (out.colors ??= {})[layer] = color;
2162
+ }
2134
2163
  if (OVERLAY_STRENGTHS.includes(s.strength))
2135
2164
  out.strength = s.strength;
2136
2165
  return out;
2137
2166
  }
2138
- var OVERLAY_OFF = { on: false, issues: 0, intended: 0, bleedPt: 0, safePt: 0 };
2167
+ var OVERLAY_OFF = {
2168
+ on: false,
2169
+ layers: { bleed: false, safe: false, art: false, margins: false },
2170
+ issues: 0,
2171
+ intended: 0,
2172
+ bleedPt: 0,
2173
+ safePt: 0,
2174
+ artSafePt: 0,
2175
+ counts: { text: 0, art: 0, bleed: 0, offPage: 0, intended: 0 }
2176
+ };
2139
2177
  function printAreaFromMeta() {
2140
2178
  const raw = document.querySelector('meta[name="gp-print-area"]')?.getAttribute("content");
2141
2179
  if (!raw)
@@ -2145,24 +2183,37 @@
2145
2183
  const n = (x) => typeof x === "number" && Number.isFinite(x) && x > 0 ? x : 0;
2146
2184
  if (!n(v.width) || !n(v.height))
2147
2185
  return null;
2148
- return { width: n(v.width), height: n(v.height), bleed: n(v.bleed), safe: n(v.safe) };
2186
+ const safe = n(v.safe);
2187
+ const spec = v.spec && typeof v.spec.label === "string" && typeof v.spec.url === "string" && /^https?:\/\//i.test(v.spec.url) ? v.spec : undefined;
2188
+ return {
2189
+ width: n(v.width),
2190
+ height: n(v.height),
2191
+ bleed: n(v.bleed),
2192
+ safe,
2193
+ artSafe: typeof v.artSafe === "number" ? n(v.artSafe) : safe,
2194
+ preset: typeof v.preset === "string" ? v.preset : undefined,
2195
+ spec
2196
+ };
2149
2197
  } catch {
2150
2198
  return null;
2151
2199
  }
2152
2200
  }
2153
2201
  function sheetArea(sheet) {
2154
2202
  const px = (name) => parseFloat(sheet.style.getPropertyValue(name)) || 0;
2155
- return { width: px("--gp-page-w") / PX_PER_PT, height: px("--gp-page-h") / PX_PER_PT, bleed: 0, safe: 0 };
2203
+ return { width: px("--gp-page-w") / PX_PER_PT, height: px("--gp-page-h") / PX_PER_PT, bleed: 0, safe: 0, artSafe: 0 };
2156
2204
  }
2157
2205
  var OVERLAY_CSS = `
2158
2206
  .root {
2159
- --gp-ov-color: #2f80ed; /* safe-area line + the tint outside it */
2207
+ --gp-ov-color: #2f80ed; /* safe layer: the text line + the tint outside it */
2160
2208
  --gp-ov-width: 2px; /* line width as seen on screen, at any zoom */
2161
- --gp-ov-style: dashed; /* safe-area line style */
2162
- --gp-ov-tint: 0.1; /* opacity of the tint between safe area and trim */
2163
- --gp-ov-bleed-color: #e5484d; /* bleed band */
2209
+ --gp-ov-style: dashed; /* text safe line style */
2210
+ --gp-ov-art-style: dotted; /* art safe line style */
2211
+ --gp-ov-tint: 0.1; /* opacity of the tint between the text line and the trim */
2212
+ --gp-ov-bleed-color: #e5484d; /* bleed layer: the band, and (darker) the trim line */
2164
2213
  --gp-ov-bleed-alpha: 0.45; /* opacity of the bleed band */
2165
- --gp-ov-trim-color: #8a0b18; /* the trim line: darker than the band so it reads as an edge */
2214
+ --gp-ov-trim-color: color-mix(in srgb, var(--gp-ov-bleed-color) 60%, #000); /* darker than the band so it reads as an edge */
2215
+ --gp-ov-art-color: #00b4d8; /* art layer: the art line (dotted, a second cue) */
2216
+ --gp-ov-margins-color: #1a9c6b; /* margins layer: the page's own content box */
2166
2217
  --gp-ov-flag-width: 2px; /* overflow flag outline */
2167
2218
  --gp-ov-warn: #f59e0b; /* leaves the safe area */
2168
2219
  --gp-ov-error: #e5484d; /* crosses the trim / leaves the sheet */
@@ -2171,10 +2222,18 @@
2171
2222
  zoom; dividing by it keeps lines the same width on screen at every zoom. */
2172
2223
  --gp-ov-w: calc(var(--gp-ov-width) / var(--gp-ov-z, 1));
2173
2224
  --gp-ov-flag-w: calc(var(--gp-ov-flag-width) / var(--gp-ov-z, 1));
2225
+ --gp-ov-margins-w: calc(1px / var(--gp-ov-z, 1)); /* thin, whatever the strength */
2174
2226
  position: absolute;
2175
2227
  inset: 0;
2176
2228
  pointer-events: none;
2177
2229
  }
2230
+ /* the stage's zoomed copy, holding the pages' boxes; the legend sits beside it, not in it */
2231
+ .gp-ov-canvas {
2232
+ position: absolute;
2233
+ left: 0;
2234
+ top: 0;
2235
+ transform-origin: 0 0;
2236
+ }
2178
2237
  .root[data-strength="subtle"] {
2179
2238
  --gp-ov-width: 1px;
2180
2239
  --gp-ov-tint: 0.05;
@@ -2186,11 +2245,11 @@
2186
2245
  --gp-ov-bleed-alpha: 0.65;
2187
2246
  --gp-ov-flag-width: 3px;
2188
2247
  }
2189
- .gp-ov-page, .gp-ov-tint, .gp-ov-safe, .gp-ov-bleed, .gp-ov-trim, .gp-ov-flag {
2248
+ .gp-ov-page, .gp-ov-tint, .gp-ov-safe, .gp-ov-art, .gp-ov-bleed, .gp-ov-trim, .gp-ov-margins, .gp-ov-flag {
2190
2249
  position: absolute;
2191
2250
  box-sizing: border-box;
2192
2251
  }
2193
- /* the band between the safe area and the trim, as a REGION (clip-path, set in
2252
+ /* the band between the text line and the trim, as a REGION (clip-path, set in
2194
2253
  overlay.ts), so it reads as an area, not a hairline */
2195
2254
  .gp-ov-tint {
2196
2255
  background: color-mix(in srgb, var(--gp-ov-color) calc(var(--gp-ov-tint) * 100%), transparent);
@@ -2198,6 +2257,9 @@
2198
2257
  .gp-ov-safe {
2199
2258
  border: var(--gp-ov-w) var(--gp-ov-style) var(--gp-ov-color);
2200
2259
  }
2260
+ .gp-ov-art {
2261
+ border: var(--gp-ov-w) var(--gp-ov-art-style) var(--gp-ov-art-color);
2262
+ }
2201
2263
  /* bleed: a translucent band between the trim and the sheet edge (its width is the border) */
2202
2264
  .gp-ov-bleed {
2203
2265
  border-style: solid;
@@ -2208,6 +2270,10 @@
2208
2270
  outline: var(--gp-ov-w) solid var(--gp-ov-trim-color);
2209
2271
  outline-offset: calc(-1 * var(--gp-ov-w));
2210
2272
  }
2273
+ /* margins: the page's own content box, a thin solid line */
2274
+ .gp-ov-margins {
2275
+ border: var(--gp-ov-margins-w) solid var(--gp-ov-margins-color);
2276
+ }
2211
2277
  /* overflow flags: outline + faint tint, never a box change */
2212
2278
  .gp-ov-flag[data-kind="unsafe"] {
2213
2279
  outline: var(--gp-ov-flag-w) solid var(--gp-ov-warn);
@@ -2221,6 +2287,38 @@
2221
2287
  .gp-ov-flag[data-kind="intended"] {
2222
2288
  outline: var(--gp-ov-flag-w) dotted var(--gp-ov-intent);
2223
2289
  }
2290
+ /* the legend: small, fixed in a corner of the viewport, and the one part that takes the pointer (its link) */
2291
+ .gp-ov-legend {
2292
+ position: fixed;
2293
+ right: 12px;
2294
+ bottom: 12px;
2295
+ box-sizing: border-box;
2296
+ max-width: min(420px, calc(100vw - 24px));
2297
+ padding: 8px 10px;
2298
+ border-radius: 8px;
2299
+ background: rgba(20, 20, 24, 0.92);
2300
+ color: #fff;
2301
+ font: 11px/1.45 ui-sans-serif, system-ui, sans-serif;
2302
+ box-shadow: 0 1px 6px rgba(0, 0, 0, 0.4);
2303
+ pointer-events: auto;
2304
+ }
2305
+ .gp-ov-legend > div + div { margin-top: 3px; }
2306
+ .gp-ov-legend a { color: #9cc2ff; }
2307
+ .gp-ov-row { display: flex; align-items: flex-start; gap: 6px; }
2308
+ .gp-ov-swatch {
2309
+ flex: none;
2310
+ box-sizing: border-box;
2311
+ width: 10px;
2312
+ height: 10px;
2313
+ margin-top: 3px;
2314
+ border: 1px solid rgba(255, 255, 255, 0.7);
2315
+ border-radius: 2px;
2316
+ }
2317
+ .gp-ov-swatch[data-layer="bleed"] { background: var(--gp-ov-bleed-color); }
2318
+ .gp-ov-swatch[data-layer="safe"] { background: var(--gp-ov-color); }
2319
+ .gp-ov-swatch[data-layer="art"] { background: var(--gp-ov-art-color); }
2320
+ .gp-ov-swatch[data-layer="margins"] { background: var(--gp-ov-margins-color); }
2321
+ .gp-ov-note { opacity: 0.75; }
2224
2322
  @media print {
2225
2323
  .root { display: none; }
2226
2324
  }
@@ -2244,24 +2342,33 @@
2244
2342
  "mask: none",
2245
2343
  "z-index: 2147483647",
2246
2344
  "pointer-events: none",
2247
- "transform-origin: 0 0",
2248
2345
  "contain: style"
2249
2346
  ].map((d) => `${d} !important`).join("; ");
2250
2347
  var OVERLAY_TAG = "gp-overlay";
2251
2348
  var host = null;
2252
2349
  var root = null;
2350
+ var canvas = null;
2253
2351
  var observer = null;
2254
2352
  var currentStyle = {};
2353
+ var COLOR_TOKEN = {
2354
+ bleed: "--gp-ov-bleed-color",
2355
+ safe: "--gp-ov-color",
2356
+ art: "--gp-ov-art-color",
2357
+ margins: "--gp-ov-margins-color"
2358
+ };
2255
2359
  function applyStyle() {
2256
2360
  if (!root)
2257
2361
  return;
2258
2362
  if (currentStyle.strength)
2259
2363
  root.dataset.strength = currentStyle.strength;
2260
- if (currentStyle.color)
2261
- root.style.setProperty("--gp-ov-color", currentStyle.color);
2364
+ for (const layer of OVERLAY_LAYERS) {
2365
+ const color = currentStyle.colors?.[layer];
2366
+ if (color)
2367
+ root.style.setProperty(COLOR_TOKEN[layer], color);
2368
+ }
2262
2369
  }
2263
2370
  function setOverlayStyle(style) {
2264
- currentStyle = { ...currentStyle, ...style };
2371
+ currentStyle = { ...currentStyle, ...style, colors: { ...currentStyle.colors, ...style.colors } };
2265
2372
  applyStyle();
2266
2373
  }
2267
2374
  function stageFrame() {
@@ -2271,32 +2378,33 @@
2271
2378
  return { left: r.left, top: r.top, zoom: zoom || 1 };
2272
2379
  }
2273
2380
  function syncToStage() {
2274
- if (!host || !root)
2381
+ if (!root || !canvas)
2275
2382
  return;
2276
2383
  const { left, top, zoom } = stageFrame();
2277
- host.style.setProperty("transform", `translate(${left + window.scrollX}px, ${top + window.scrollY}px) scale(${zoom})`, "important");
2384
+ canvas.style.transform = `translate(${left + window.scrollX}px, ${top + window.scrollY}px) scale(${zoom})`;
2278
2385
  root.style.setProperty("--gp-ov-z", String(zoom));
2279
2386
  }
2280
2387
  function ensureRoot() {
2281
- if (host && root && host.isConnected) {
2282
- root.textContent = "";
2283
- return root;
2284
- }
2285
- host = document.createElement(OVERLAY_TAG);
2286
- host.setAttribute("style", HOST_STYLE);
2287
- host.setAttribute("aria-hidden", "true");
2288
- const shadow = host.attachShadow({ mode: "open" });
2289
- const sheet = document.createElement("style");
2290
- sheet.textContent = OVERLAY_CSS;
2291
- root = document.createElement("div");
2292
- root.className = "root";
2293
- shadow.append(sheet, root);
2294
- document.documentElement.appendChild(host);
2295
- applyStyle();
2296
- observer = new MutationObserver(syncToStage);
2297
- observer.observe(document.documentElement, { attributes: true, attributeFilter: ["style"] });
2298
- observer.observe(document.body, { attributes: true, attributeFilter: ["style"] });
2299
- return root;
2388
+ if (!(host && root && host.isConnected)) {
2389
+ host = document.createElement(OVERLAY_TAG);
2390
+ host.setAttribute("style", HOST_STYLE);
2391
+ const shadow = host.attachShadow({ mode: "open" });
2392
+ const sheet = document.createElement("style");
2393
+ sheet.textContent = OVERLAY_CSS;
2394
+ root = document.createElement("div");
2395
+ root.className = "root";
2396
+ shadow.append(sheet, root);
2397
+ document.documentElement.appendChild(host);
2398
+ applyStyle();
2399
+ observer = new MutationObserver(syncToStage);
2400
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["style"] });
2401
+ observer.observe(document.body, { attributes: true, attributeFilter: ["style"] });
2402
+ }
2403
+ root.textContent = "";
2404
+ canvas = div("gp-ov-canvas");
2405
+ canvas.setAttribute("aria-hidden", "true");
2406
+ root.appendChild(canvas);
2407
+ return canvas;
2300
2408
  }
2301
2409
  function clearOverlay() {
2302
2410
  observer?.disconnect();
@@ -2304,6 +2412,7 @@
2304
2412
  host?.remove();
2305
2413
  host = null;
2306
2414
  root = null;
2415
+ canvas = null;
2307
2416
  }
2308
2417
  function div(cls, box) {
2309
2418
  const el = document.createElement("div");
@@ -2325,9 +2434,86 @@
2325
2434
  el.style.clipPath = `polygon(evenodd, ${t.x}px ${t.y}px, ${t.x + t.w}px ${t.y}px, ${t.x + t.w}px ${t.y + t.h}px, ${t.x}px ${t.y + t.h}px, ${t.x}px ${t.y}px, ` + `${s.x}px ${s.y}px, ${s.x}px ${s.y + s.h}px, ${s.x + s.w}px ${s.y + s.h}px, ${s.x + s.w}px ${s.y}px, ${s.x}px ${s.y}px)`;
2326
2435
  return el;
2327
2436
  }
2437
+ function marginsBox(p, side) {
2438
+ const { width, height, margin } = p.strip.geometry;
2439
+ const shift = side === "verso" ? p.strip.versoShift : 0;
2440
+ const left = margin.left - shift;
2441
+ const right = margin.right + shift;
2442
+ return {
2443
+ x: left * PX_PER_PT,
2444
+ y: margin.top * PX_PER_PT,
2445
+ w: Math.max(0, width - left - right) * PX_PER_PT,
2446
+ h: Math.max(0, height - margin.top - margin.bottom) * PX_PER_PT
2447
+ };
2448
+ }
2328
2449
  var TOL_PX = 1;
2329
2450
  var SKIP = new Set(["SCRIPT", "STYLE", "LINK", "TEMPLATE", "BR", "WBR", "SOURCE", "TRACK", "META"]);
2330
- function paintOverlay(pages, strips, area) {
2451
+ var ART_TAGS = new Set(["img", "svg", "video", "canvas"]);
2452
+ var inches = (px) => +(Math.abs(px) / 96).toFixed(2);
2453
+ var ptIn = (pt2) => `${+(pt2 / 72).toFixed(3)}in`;
2454
+ function legend(layers, area, summary) {
2455
+ const el = div("gp-ov-legend");
2456
+ const add = (cls, ...content) => {
2457
+ const line = div(cls);
2458
+ line.append(...content);
2459
+ el.appendChild(line);
2460
+ };
2461
+ const row = (layer, name, text) => {
2462
+ const swatch = div("gp-ov-swatch");
2463
+ swatch.dataset.layer = layer;
2464
+ const bold = document.createElement("b");
2465
+ bold.textContent = name;
2466
+ const label = document.createElement("span");
2467
+ label.append(bold, text);
2468
+ add("gp-ov-row", swatch, label);
2469
+ };
2470
+ const { bleedPt, safePt, artSafePt, counts } = summary;
2471
+ if (layers.bleed) {
2472
+ row("bleed", "Bleed", bleedPt > 0 ? ` ${ptIn(bleedPt)} — top, bottom, outside; none at the binding` : " — none set (page.bleed in manifest.yaml)");
2473
+ }
2474
+ if (layers.safe) {
2475
+ row("safe", "Safe area: text", safePt > 0 ? ` — ${ptIn(safePt)}, every edge` : " — none set (page.safe in manifest.yaml)");
2476
+ }
2477
+ if (layers.art) {
2478
+ row("art", "Safe area: art", safePt <= 0 && artSafePt <= 0 ? " — none set (page.artSafe in manifest.yaml)" : artSafePt === safePt ? ` — ${ptIn(artSafePt)}, every edge` : ` — ${ptIn(artSafePt)} top, bottom, outside; ${ptIn(safePt)} at the binding`);
2479
+ }
2480
+ if (layers.margins)
2481
+ row("margins", "Page margins", " — your stylesheet's @page margins");
2482
+ if (area && (layers.bleed || layers.safe || layers.art)) {
2483
+ if (area.spec) {
2484
+ const link = document.createElement("a");
2485
+ link.href = area.spec.url;
2486
+ link.target = "_blank";
2487
+ link.rel = "noopener";
2488
+ link.textContent = area.spec.label;
2489
+ add("gp-ov-note", "from ", link);
2490
+ } else {
2491
+ add("gp-ov-note", `from manifest.yaml${area.preset ? ` (preset ${area.preset})` : ""}`);
2492
+ }
2493
+ }
2494
+ if (layers.safe || layers.art) {
2495
+ const found = [];
2496
+ if (counts.text)
2497
+ found.push(`${counts.text} text outside the safe area`);
2498
+ if (counts.art)
2499
+ found.push(`${counts.art} art outside${counts.text ? "" : " the safe area"}`);
2500
+ if (counts.bleed)
2501
+ found.push(`${counts.bleed} in the bleed band`);
2502
+ if (counts.offPage)
2503
+ found.push(`${counts.offPage} off the page`);
2504
+ if (!found.length) {
2505
+ found.push(`${layers.safe && layers.art ? "everything" : layers.safe ? "all text" : "all art"} inside the safe area`);
2506
+ }
2507
+ if (counts.intended)
2508
+ found.push(`${counts.intended} placed on purpose`);
2509
+ add("gp-ov-found", found.join(" · "));
2510
+ }
2511
+ const key = document.documentElement.dataset.overlayKey;
2512
+ if (key)
2513
+ add("gp-ov-note", `${key} hides`);
2514
+ return el;
2515
+ }
2516
+ function paintOverlay(pages, strips, area, layers) {
2331
2517
  const layer = ensureRoot();
2332
2518
  syncToStage();
2333
2519
  const stage = stageFrame();
@@ -2343,7 +2529,7 @@
2343
2529
  h: boxes.sheet.h
2344
2530
  });
2345
2531
  host2.dataset.page = String(p.index + 1);
2346
- if (boxes.bleed > 0) {
2532
+ if (layers.bleed && boxes.bleed > 0) {
2347
2533
  const sh = boxes.sheet;
2348
2534
  const t = boxes.trim;
2349
2535
  const band = div("gp-ov-bleed", sh);
@@ -2351,68 +2537,115 @@
2351
2537
  host2.appendChild(band);
2352
2538
  host2.appendChild(div("gp-ov-trim", t));
2353
2539
  }
2354
- if (area && area.safe > 0) {
2540
+ if (layers.safe && area && area.safe > 0) {
2355
2541
  host2.appendChild(unsafeTint(boxes));
2356
2542
  host2.appendChild(div("gp-ov-safe", boxes.safe));
2357
2543
  }
2544
+ if (layers.art && area && area.artSafe > 0)
2545
+ host2.appendChild(div("gp-ov-art", boxes.safeArt));
2546
+ if (layers.margins && !p.padded)
2547
+ host2.appendChild(div("gp-ov-margins", marginsBox(p, side)));
2358
2548
  layer.appendChild(host2);
2359
2549
  painted.set(p.index, { host: host2, boxes, sheet: p.sheet, rect });
2360
2550
  }
2361
2551
  const flags = [];
2362
- let issues = 0;
2363
- let intended = 0;
2364
- for (const strip of strips) {
2365
- const zoom = cssZoomOf(strip.el);
2366
- const colW = (parseFloat(strip.el.style.getPropertyValue("--gp-content-w")) || 0) * zoom;
2367
- const colH = (parseFloat(strip.el.style.getPropertyValue("--gp-content-h")) || 0) * zoom;
2368
- const visit = (el) => {
2369
- if (SKIP.has(el.tagName) || el.classList.contains("gp-wrap-spacer"))
2370
- return;
2371
- const kinds = [];
2372
- const found = [];
2373
- for (const r of Array.from(el.getClientRects())) {
2552
+ const counts = { text: 0, art: 0, bleed: 0, offPage: 0, intended: 0 };
2553
+ const seen = new Set;
2554
+ const range = document.createRange();
2555
+ const source = area?.spec?.label ?? "the safe area";
2556
+ const inspect = (root2, pageOf2, zoom) => {
2557
+ if (!layers.safe && !layers.art)
2558
+ return;
2559
+ const judge = (el, rule, rects) => {
2560
+ for (const r of rects) {
2374
2561
  if (r.width <= 0 && r.height <= 0)
2375
2562
  continue;
2376
- const ax = r.left + Math.min(r.width, colW || r.width) / 2;
2377
- const ay = r.top + Math.min(r.height, colH || r.height) / 2;
2378
- const target = painted.get(strip.offset + indexInStrip(ax, ay, strip));
2563
+ const target = pageOf2(r);
2379
2564
  if (!target)
2380
2565
  continue;
2381
2566
  target.rect ??= target.sheet.getBoundingClientRect();
2382
- const local = {
2567
+ const box = {
2383
2568
  x: (r.left - target.rect.left) / zoom,
2384
2569
  y: (r.top - target.rect.top) / zoom,
2385
2570
  w: r.width / zoom,
2386
2571
  h: r.height / zoom
2387
2572
  };
2388
- const kind = classifyRect(local, target.boxes, TOL_PX);
2389
- kinds.push(kind);
2390
- if (kind !== "inside")
2391
- found.push({ host: target.host, kind, box: local });
2573
+ const { kind, gap, need } = classifyRect(box, target.boxes, rule, TOL_PX);
2574
+ if (kind === "inside")
2575
+ continue;
2576
+ const key = `${target.host.dataset.page} ${box.x} ${box.y} ${box.w} ${box.h}`;
2577
+ if (seen.has(key))
2578
+ continue;
2579
+ seen.add(key);
2580
+ const placed = el.closest(INTENT_SELECTOR) !== null;
2581
+ const bucket = placed ? "intended" : kind === "unsafe" ? rule : kind === "bleed" ? "bleed" : "offPage";
2582
+ counts[bucket]++;
2583
+ const title = `${rule} ${inches(gap)}in ${gap < 0 ? "past" : "from"} the trim; ${source} needs ${inches(need)}in`;
2584
+ flags.push({
2585
+ host: target.host,
2586
+ kind: placed ? "intended" : kind,
2587
+ rule,
2588
+ box,
2589
+ title: placed ? `${title} (placed on purpose)` : title
2590
+ });
2392
2591
  }
2393
- const worst = worstKind(kinds);
2394
- if (worst !== "inside") {
2395
- const isIntended = el.closest(INTENT_SELECTOR) !== null;
2396
- if (isIntended)
2397
- intended++;
2398
- else
2399
- issues++;
2400
- for (const f of found)
2401
- flags.push(isIntended ? { ...f, kind: "intended" } : f);
2592
+ };
2593
+ const visit = (node) => {
2594
+ if (node.nodeType === Node.TEXT_NODE) {
2595
+ if (!layers.safe || !node.nodeValue?.trim())
2596
+ return;
2597
+ range.selectNodeContents(node);
2598
+ judge(node.parentElement, "text", range.getClientRects());
2402
2599
  return;
2403
2600
  }
2404
- for (const child of Array.from(el.children))
2601
+ if (!(node instanceof Element) || SKIP.has(node.tagName) || node.classList.contains("gp-wrap-spacer"))
2602
+ return;
2603
+ if (layers.art) {
2604
+ const cs = getComputedStyle(node);
2605
+ if (ART_TAGS.has(node.localName) || cs.backgroundImage !== "none" || !/^rgba\(.*, 0\)$/.test(cs.backgroundColor) || parseFloat(cs.borderTopWidth) > 0 || parseFloat(cs.borderRightWidth) > 0 || parseFloat(cs.borderBottomWidth) > 0 || parseFloat(cs.borderLeftWidth) > 0) {
2606
+ judge(node, "art", node.getClientRects());
2607
+ }
2608
+ }
2609
+ for (const child of node.childNodes)
2405
2610
  visit(child);
2406
2611
  };
2407
- for (const child of Array.from(strip.el.children))
2408
- visit(child);
2612
+ visit(root2);
2613
+ };
2614
+ for (const strip of strips) {
2615
+ const zoom = cssZoomOf(strip.el);
2616
+ const colW = (parseFloat(strip.el.style.getPropertyValue("--gp-content-w")) || 0) * zoom;
2617
+ const colH = (parseFloat(strip.el.style.getPropertyValue("--gp-content-h")) || 0) * zoom;
2618
+ const pageOf2 = (r) => {
2619
+ const ax = r.left + Math.min(r.width, colW || r.width) / 2;
2620
+ const ay = r.top + Math.min(r.height, colH || r.height) / 2;
2621
+ return painted.get(strip.offset + indexInStrip(ax, ay, strip));
2622
+ };
2623
+ for (const child of strip.el.childNodes)
2624
+ inspect(child, pageOf2, zoom);
2625
+ }
2626
+ for (const target of painted.values()) {
2627
+ for (const furniture of target.sheet.querySelectorAll(".gp-marginbox"))
2628
+ inspect(furniture, () => target, stage.zoom);
2409
2629
  }
2410
2630
  for (const f of flags) {
2411
2631
  const el = div("gp-ov-flag", f.box);
2412
2632
  el.dataset.kind = f.kind;
2633
+ el.dataset.rule = f.rule;
2634
+ el.title = f.title;
2413
2635
  f.host.appendChild(el);
2414
2636
  }
2415
- return { on: true, issues, intended, bleedPt: area?.bleed ?? 0, safePt: area?.safe ?? 0 };
2637
+ const summary = {
2638
+ on: true,
2639
+ layers,
2640
+ issues: counts.text + counts.art + counts.bleed + counts.offPage,
2641
+ intended: counts.intended,
2642
+ bleedPt: area?.bleed ?? 0,
2643
+ safePt: area?.safe ?? 0,
2644
+ artSafePt: area?.artSafe ?? 0,
2645
+ counts
2646
+ };
2647
+ root?.appendChild(legend(layers, area, summary));
2648
+ return summary;
2416
2649
  }
2417
2650
 
2418
2651
  // src/engine/viewer/decorate.ts
@@ -2444,10 +2677,11 @@
2444
2677
  let paddedPages = new Set;
2445
2678
  const warnings = [];
2446
2679
  let overlayOn = false;
2680
+ let overlayLayers = OVERLAY_OFF.layers;
2447
2681
  let overlayPages = [];
2448
2682
  let overlaySummary = OVERLAY_OFF;
2449
2683
  const paintOverlayNow = () => {
2450
- overlaySummary = paintOverlay(overlayPages, layout.strips, printAreaFromMeta());
2684
+ overlaySummary = paintOverlay(overlayPages, layout.strips, printAreaFromMeta(), overlayLayers);
2451
2685
  window.dispatchEvent(new CustomEvent("gp:overlay", { detail: overlaySummary }));
2452
2686
  };
2453
2687
  const api = {
@@ -2460,20 +2694,31 @@
2460
2694
  setDesigner(on) {
2461
2695
  document.body.dataset.designer = on ? "on" : "off";
2462
2696
  },
2463
- setOverlay(on, style) {
2697
+ setOverlay(state, style) {
2464
2698
  if (style) {
2465
2699
  const clean = sanitizeOverlayStyle(style);
2466
2700
  setOverlayStyle(clean);
2467
- if (clean.strength)
2468
- document.body.dataset.overlayStrength = clean.strength;
2469
- if (clean.color)
2470
- document.body.dataset.overlayColor = clean.color;
2701
+ const { strength, colors } = clean;
2702
+ if (strength)
2703
+ document.body.dataset.overlayStrength = strength;
2704
+ if (colors?.safe)
2705
+ document.body.dataset.overlayColor = colors.safe;
2706
+ if (colors?.art)
2707
+ document.body.dataset.overlayArtColor = colors.art;
2708
+ if (colors?.bleed)
2709
+ document.body.dataset.overlayBleedColor = colors.bleed;
2710
+ if (colors?.margins)
2711
+ document.body.dataset.overlayMarginsColor = colors.margins;
2471
2712
  }
2472
2713
  const wasOn = overlayOn;
2473
- overlayOn = on;
2474
- document.body.dataset.overlay = on ? "on" : "off";
2475
- if (on) {
2476
- if (!(wasOn && style))
2714
+ const next = layersFrom(state);
2715
+ const sameLayers = OVERLAY_LAYERS.every((layer) => next[layer] === overlayLayers[layer]);
2716
+ overlayLayers = next;
2717
+ overlayOn = OVERLAY_LAYERS.some((layer) => next[layer]);
2718
+ document.body.dataset.overlay = overlayOn ? "on" : "off";
2719
+ if (overlayOn) {
2720
+ document.body.dataset.overlayLayers = OVERLAY_LAYERS.filter((layer) => next[layer]).join(",");
2721
+ if (!(wasOn && sameLayers && style))
2477
2722
  paintOverlayNow();
2478
2723
  } else {
2479
2724
  clearOverlay();
@@ -2617,8 +2862,8 @@
2617
2862
  }
2618
2863
  if (!marked.length)
2619
2864
  return;
2620
- const canvas = document.createElement("canvas");
2621
- const cx = canvas.getContext("2d");
2865
+ const canvas2 = document.createElement("canvas");
2866
+ const cx = canvas2.getContext("2d");
2622
2867
  for (const m of marked)
2623
2868
  m.el.setAttribute(m.attr, m.raw.replace(LEADER_RE, ""));
2624
2869
  document.body.offsetHeight;
@@ -2682,7 +2927,7 @@
2682
2927
  drawMarginBoxes(sheet, ctx, layout.totalPages - paddedPages.size);
2683
2928
  drawGuides(sheet, ctx);
2684
2929
  drawCropMarks(sheet, ctx);
2685
- overlayPages.push({ index: bookIndex, sheet, strip });
2930
+ overlayPages.push({ index: bookIndex, sheet, strip, padded: ctx.padded });
2686
2931
  }
2687
2932
  const rows = Math.max(1, Math.ceil((strip.pages + shift) / perRow));
2688
2933
  run.style.height = `${rowStride * (rows - 1) + PX_PER_PT * g.height}px`;