gutterpress 0.10.6 → 0.10.7

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 (42) hide show
  1. package/README.md +71 -3
  2. package/dist/api/index.d.ts +1 -1
  3. package/dist/api/index.js +4 -2
  4. package/dist/{audit-0a4sp2w5.js → audit-19e2431j.js} +4 -4
  5. package/dist/{build-z53bbxtx.js → build-gqqew2vr.js} +4 -4
  6. package/dist/checks/source/css-ownership.d.ts +43 -0
  7. package/dist/checks/source/index.d.ts +1 -0
  8. package/dist/{cli-he23vgjj.js → cli-h8ejbtyr.js} +15 -2
  9. package/dist/{cli-kwt5ftvc.js → cli-kzf64c2g.js} +4991 -3933
  10. package/dist/{cli-m99hj80k.js → cli-pvk2s9d7.js} +1 -1
  11. package/dist/{cli-bwbs3qm4.js → cli-s8q42x3r.js} +1 -1
  12. package/dist/cli.js +70 -37
  13. package/dist/{doctor-d8vpv1pt.js → doctor-3ccz35yz.js} +2 -2
  14. package/dist/{engine-4ctgbwj5.js → engine-88mynx9a.js} +2 -2
  15. package/dist/{engine-bhhcne06.js → engine-pb6h089e.js} +1 -1
  16. package/dist/{index-k5hcp8wj.js → index-837htyjy.js} +1 -1
  17. package/dist/{index-eyzv51dw.js → index-a0xpm188.js} +1 -1
  18. package/dist/{index-1rcfsx67.js → index-r4ny4fyr.js} +2586 -2073
  19. package/dist/index.js +5 -3
  20. package/dist/lib/cli-args.d.ts +9 -0
  21. package/dist/lib/manifest-doc.d.ts +8 -2
  22. package/dist/lib/markdown/assemble.d.ts +24 -7
  23. package/dist/lib/markdown/gfm-alerts.d.ts +7 -0
  24. package/dist/lib/markdown/gutterpress-css.d.ts +1 -1
  25. package/dist/lib/markdown/index.d.ts +16 -0
  26. package/dist/lib/markdown/plugins.d.ts +8 -12
  27. package/dist/lib/markdown/renderer.d.ts +48 -1
  28. package/dist/lib/style-declarations.d.ts +20 -0
  29. package/dist/lib/style-resolver.d.ts +2 -0
  30. package/dist/lib/theme-manager.d.ts +110 -9
  31. package/dist/{lint-nm57gn26.js → lint-mpz7vfc8.js} +4 -4
  32. package/dist/{manifest.schema-hn6ac0ae.json → manifest.schema-kwnrwv8b.json} +11 -8
  33. package/dist/{new-sawc7zh4.js → new-nbrpsw7t.js} +4 -4
  34. package/dist/{plugin-p8yghztz.js → plugin-gg5mk26h.js} +5 -11
  35. package/dist/{preflight-48kwf2pm.js → preflight-jnmd4mg2.js} +4 -4
  36. package/dist/{preview-10r4dk3d.js → preview-hsr9d12d.js} +4 -4
  37. package/dist/{publish-cd05sbdx.js → publish-pcrrn6fd.js} +4 -4
  38. package/dist/render.js +155 -52
  39. package/dist/schema/manifest.types.d.ts +20 -7
  40. package/dist/theme-6fa0yjzq.js +323 -0
  41. package/dist/{validate-70ta3br9.js → validate-7n25fx0g.js} +4 -4
  42. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  connectPublishProvider,
40
40
  defaultConfigDir,
41
41
  deleteSnippet,
42
+ detectLegacyForkedTheme,
42
43
  diagnoseProjectRemote,
43
44
  disconnectPublishCredential,
44
45
  escapeYamlScalar,
@@ -124,15 +125,15 @@ import {
124
125
  syncProject,
125
126
  testRemoteAccess,
126
127
  validateProjectPlugins
127
- } from "./index-1rcfsx67.js";
128
+ } from "./index-r4ny4fyr.js";
128
129
  import {
129
130
  BUILD_DIAGNOSTIC_CODES
130
- } from "./index-eyzv51dw.js";
131
+ } from "./index-a0xpm188.js";
131
132
  import {
132
133
  BuildError,
133
134
  DEFAULT_PRINT_OPTS,
134
135
  readyProbeExpr
135
- } from "./index-k5hcp8wj.js";
136
+ } from "./index-837htyjy.js";
136
137
  import {
137
138
  AUTO_SNAPSHOT_MESSAGE,
138
139
  HISTORY_PAGE_LIMIT,
@@ -245,6 +246,7 @@ export {
245
246
  disconnectPublishCredential,
246
247
  diagnoseProjectRemote,
247
248
  detectProjectSource,
249
+ detectLegacyForkedTheme,
248
250
  deleteSnippet,
249
251
  defaultConfigDir,
250
252
  connectPublishProvider,
@@ -50,3 +50,12 @@ export declare function parsePdfxFlavor(raw: unknown, format: BuildFormat): Pdfx
50
50
  export declare function parseEngine(raw: unknown): "paged" | "native" | undefined;
51
51
  /** Parse `--port`, defaulting to {@link NETWORK.DEFAULT_PORT} (0 = OS-assigned). */
52
52
  export declare function resolvePort(raw: unknown): number;
53
+ /**
54
+ * THE command-boundary handler for a {@link UsageError}: print the message and
55
+ * exit with the error's own code. Every subcommand-dispatching command
56
+ * (`plugin`, `theme`) catches its parse errors here, so the usage-error→exit
57
+ * mapping has one definition rather than one per command.
58
+ */
59
+ export declare function exitForUsage(error: unknown): never;
60
+ /** Resolve a `--dir` argument to an absolute path, defaulting to the cwd. */
61
+ export declare function resolveProjectDir(dir: unknown): string;
@@ -9,8 +9,14 @@ export declare function loadManifestDoc(projectDir: string): Promise<{
9
9
  }>;
10
10
  /** Atomically write the doc back, creating the project dir if needed. */
11
11
  export declare function writeManifestDoc(file: string, doc: Document.Parsed): Promise<void>;
12
- /** The named sequence node, creating (and attaching) an empty one if missing. */
13
- export declare function ensureSeq(doc: Document.Parsed, key: string): YAMLSeq;
12
+ /**
13
+ * The named sequence node, creating (and attaching) an empty one if missing.
14
+ * `key` is a single top-level key (the original, still-exact behavior) OR a
15
+ * path for a nested key (e.g. `["engineStyles", "native"]`, #239) — the
16
+ * `getIn`/`setIn` branch auto-vivifies any missing intermediate map, exactly
17
+ * like a hand-written `engineStyles: { native: [...] }` would parse.
18
+ */
19
+ export declare function ensureSeq(doc: Document.Parsed, key: string | readonly string[]): YAMLSeq;
14
20
  /**
15
21
  * Unwrap a yaml seq item (or `getIn`-style Pair) to its string value: `null`
16
22
  * when the item isn't a string. Handles both a bare Scalar/Pair-shaped node
@@ -23,6 +23,16 @@ export interface LayoutWarning {
23
23
  message: string;
24
24
  marker?: unknown;
25
25
  }
26
+ /**
27
+ * The Chromium milestone that paints a `@page { background: url() }` image
28
+ * with no other reference to it (spec gap #152). Measured 2026-09-03: Chrome
29
+ * for Testing 152.0.7977.54 and the CI runner's Chrome stable both paint it;
30
+ * Chrome 151.0.7922.75 dropped it (docs/known-limitations.md §3). The
31
+ * `preloadImages` shim below expires when the engine's floor
32
+ * (`REQUIRED_MILESTONE`, engine/shared/cdp.ts) reaches this — the canary in
33
+ * engine/compiler/page-background-chromium-bug.canary.test.ts enforces that.
34
+ */
35
+ export declare const PAGE_BACKGROUND_FIXED_MILESTONE = 152;
26
36
  export interface AssembleBookHtmlOptions {
27
37
  /** Ordered list of project-root-relative `.md` files to concatenate. */
28
38
  files: string[];
@@ -42,9 +52,10 @@ export interface AssembleBookHtmlOptions {
42
52
  */
43
53
  projectCss?: string;
44
54
  /**
45
- * SHIM — spec gap #152. Output-relative hrefs of the images the project's
46
- * stylesheets staged (`inlineStyles`'s copy plan, verbatim), each emitted as
47
- * one `<link rel="preload" as="image">`.
55
+ * SHIM — spec gap #152, fixed upstream in Chromium
56
+ * {@link PAGE_BACKGROUND_FIXED_MILESTONE}. Output-relative hrefs of the
57
+ * images the project's stylesheets staged (`inlineStyles`'s copy plan,
58
+ * verbatim), each emitted as one `<link rel="preload" as="image">`.
48
59
  *
49
60
  * Chromium reaches an `@page`-only `url()` lazily, during the print, and the
50
61
  * print path CDP drives never waits for a pending resource — so the sheet
@@ -64,10 +75,16 @@ export interface AssembleBookHtmlOptions {
64
75
  * either document order) — which is why `asset-inline.ts` content-addresses
65
76
  * every CSS image so no element can name one.
66
77
  *
67
- * WHAT PROVES IT IS STILL NEEDED: the expiry canary,
68
- * `engine/compiler/page-background-chromium-bug.canary.test.ts`. The day it
69
- * goes red, Chromium has fixed the bug delete this option, the `.map()`
70
- * that feeds it in `markdown/index.ts`, and the canary.
78
+ * WHEN IT GOES: Chromium fixed the bug in milestone
79
+ * {@link PAGE_BACKGROUND_FIXED_MILESTONE} (measured 2026-09-03: Chrome 152
80
+ * paints the sole-referenced image, 151 dropped it). The preload changes
81
+ * nothing on a fixed Chromium, and it still protects every Chromium the
82
+ * engine accepts below that (`REQUIRED_MILESTONE` in `engine/shared/cdp.ts`,
83
+ * 148 — what Electron 42 ships), so it stays until the floor reaches the
84
+ * fix. The expiry canary,
85
+ * `engine/compiler/page-background-chromium-bug.canary.test.ts`, goes red
86
+ * the day the floor is raised that far — delete this option, the `.map()`
87
+ * that feeds it in `markdown/index.ts`, the constant, and the canary.
71
88
  *
72
89
  * The copy plan is the source, NOT a scan of the assembled CSS: `pluginCss`
73
90
  * never passes through `inlineStyles`, so a `url()` inside it is never
@@ -0,0 +1,7 @@
1
+ import type MarkdownIt from "markdown-it";
2
+ /**
3
+ * The plugin export — a plain `(md) => void` markdown-it plugin (CLAUDE.md
4
+ * §5), registered by name from `BUILTIN_OPTIONAL_PLUGINS` when a project
5
+ * lists `gutterpress-gfm-alerts` in its manifest.
6
+ */
7
+ export default function gfmAlerts(md: MarkdownIt): void;
@@ -136,7 +136,7 @@
136
136
  * carrying gp-left could start shifting; that is standards-tracking per
137
137
  * CLAUDE.md ("Chrome wins once it ships"), not a bug in the author's book.
138
138
  */
139
- export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary. One vocabulary, gp-* only \u2014 the\n pre-vocabulary utility names (.center/.float-left/.float-right/\n .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename\n the classes in their markdown (see the migration note). Source ORDER is\n the contract \u2014 see the doctrine comment above. */\n\n/* flow positions */\n.gp-left {\n float: left;\n margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;\n max-width: 50%;\n}\n.gp-right {\n float: right;\n margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);\n max-width: 50%;\n}\n.gp-center {\n display: block;\n float: none;\n margin-left: auto;\n margin-right: auto;\n max-width: 100%;\n}\n.gp-full {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n@page gp-full-bleed { margin-left: 0; margin-right: 0; }\n.gp-bleed {\n display: block;\n float: none;\n break-before: page;\n page: gp-full-bleed;\n max-width: none;\n width: 100%;\n margin-left: 0;\n margin-right: 0;\n}\n\n/* sizes \u2014 AFTER the flow positions so max-width:100% lifts the floats' 50%\n cap at equal specificity */\n.gp-small { width: 25%; max-width: 100%; }\n.gp-medium { width: 50%; max-width: 100%; }\n.gp-large { width: 75%; max-width: 100%; }\n\n/* float clearance presets \u2014 consumed by var(--gp-gap) in the float rules\n above and by .gp-shape's shape-margin below; --gp-gap itself is\n author-settable CSS */\n.gp-tight { --gp-gap: 0.5em; }\n.gp-loose { --gp-gap: 2em; }\n\n/* column runs \u2014 plain CSS Multi-column, exposed as author vocabulary so\n \"put this in two columns\" does not require borrowing a styled container\n from the book's own component layer. That borrowing is what this exists\n to prevent: a book whose theme paints .section chrome by default gives\n every author who opens a section just to start a column run a panel they\n did not ask for, and the book then needs a reset rule to take it back.\n With a neutral primitive the author opts into columns and nothing else.\n\n Permanent vocabulary, not a shim: Chromium implements multicol natively\n and these rules are the standard properties verbatim, so there is no\n spec gap here to remove later. Deliberately minimal \u2014 column-fill is\n NOT set, because the correct value depends on whether the run fragments\n across pages (auto packs each page's columns; the CSS initial balance is\n right for a run that fits on one page) and only the author knows which.\n --gp-column-gap is author-settable. */\n.gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }\n.gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }\n\n/* the per-shape decisions the paragraph above deliberately leaves to the\n author, named instead of left as raw CSS every book was reinventing\n (2026-09-01 CSS architecture review, findings C1/C7 \u2014 CLAUDE.md \u00A70:\n \"behavior broadly useful to non-technical authors belongs in core\").\n Permanent vocabulary, standard properties verbatim \u2014 same rationale as\n the column/grid runs above. One name each, no aliases:\n .gp-columns-all column-span: all a heading or block that\n spans every column in the\n run it sits inside.\n .gp-columns-flow column-fill: auto a run that FRAGMENTS across\n pages \u2014 every page's\n columns fill instead of\n only the last one\n balancing (the dead-column\n collapse the build's\n engine.multicol.dead-column\n warning names this fix\n for).\n .gp-columns-balanced column-fill: balance a run that fits on ONE\n page (the CSS initial\n value \u2014 ragged columns\n would be wrong here). */\n.gp-columns-all { column-span: all; }\n.gp-columns-flow { column-fill: auto; }\n.gp-columns-balanced { column-fill: balance; }\n\n/* grid runs \u2014 the SLOTTED counterpart to the column runs above. Grid places\n each child into the next cell, across then down (deterministic slots: card\n layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run\n down then across. Same neutral-primitive rationale as .gp-columns-*, and\n permanent vocabulary for the same reason: standard CSS Grid verbatim, no\n spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):\n grid rows fragment across sheets with EXACT print/viewer parity \u2014 2- and\n 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,\n break-inside:avoid, gap geometry \u2014 so a grid taller than the page is safe,\n no fit-one-page constraint. Two things to know, not fix:\n - on a min-height page root (MARKER_CSS), default align-content\n stretches rows apart to fill the page \u2014 identically in both engines.\n Authors wanting packed rows set align-content: start.\n - a @page-break / @column-break marker DIRECTLY inside a grid container\n becomes a grid item and corrupts placement (the one measured parity\n break); markers.js diagnoses it (break_inside_grid).\n --gp-grid-gap is author-settable. */\n.gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n.gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n\n/* fragmentation controls \u2014 generic pagination utilities for any element,\n independent of the column/grid vocabulary above (a book reached for\n these just as often outside a multicol run: keeping a card whole,\n forcing a section to start a fresh page). Standard properties only \u2014\n this project is Chromium-only (CLAUDE.md), so there are no legacy\n page-break-* twins to also emit. One name each. */\n.gp-no-break { break-inside: avoid; }\n.gp-break-before { break-before: page; }\n\n/* shape wrap \u2014 text follows the image's alpha silhouette instead of its\n rectangular box. shape-outside only applies to floats, so this is inert\n without .gp-left/.gp-right (and under .gp-pin, which un-floats). The\n shape URL cannot be written in CSS (url() contexts can't read attr()),\n so the image renderer rule (images.ts) mirrors the src into an inline\n --gp-shape:url(...) custom property whenever it sees this class --\n authors only ever type the class. threshold 0.2 ignores near-transparent\n anti-aliasing halos; shape-margin shares the float-gap vocabulary. */\nimg.gp-shape {\n shape-outside: var(--gp-shape);\n shape-image-threshold: 0.2;\n shape-margin: var(--gp-gap, 1em);\n}\n\n/* pin \u2014 within the nearest positioned ancestor (.page/.spread, rule above).\n inset:0 and the explicit centers are load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n inset: 0;\n align-self: center;\n justify-self: center;\n margin: 0;\n max-width: 100%;\n}\n\n/* pin edge modifiers \u2014 AFTER .gp-pin to beat its center defaults;\n justify-self is inert on in-flow floats, so gp-left/gp-right safely do\n double duty as flow float + pin edge */\n.gp-top { align-self: start; }\n.gp-bottom { align-self: end; }\n.gp-left { justify-self: start; }\n.gp-right { justify-self: end; }\n\n/* wrapper-margin neutralization (same pattern and rationale as the\n .gp-bleed paragraph-margin note in the doctrine comment; for pin, the\n emptied paragraph would otherwise leave a phantom margin gap in flow) */\n:where(p:has(> img.gp-bleed:only-child)) { margin: 0; }\n:where(p:has(> img.gp-pin:only-child)) { margin: 0; }\n\n/* depth \u2014 a named ladder for z-index, so books stop hand-tuning bare\n integers. A real book measured 21 z-index declarations using only four\n distinct values (-1, 0, 1, 2), each written literally at its use site.\n The custom properties are the author-settable surface (a book needing a\n deeper stack raises them once); the classes are the shorthand.\n\n NOT named \"layer\": CSS Paged Media 3 \u00A73.1 already defines \"page layers\"\n (page background, canvas, borders, contents, margin boxes) and those are\n parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a\n different concept would collide with the spec vocabulary this project\n tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the\n ladder avoids those words too.\n\n .gp-behind is the one that earns its place: it puts a pinned image UNDER\n the page's text, which is otherwise impossible to express without a bare\n negative z-index. \"Above\" needs no class \u2014 an out-of-flow pin already\n paints above in-flow content.\n\n Two things silently defeat .gp-behind, neither visible at the use site:\n - a stacking context on the .page/.spread ancestor (z-index, isolation,\n opacity, filter, transform on it traps the negative layer inside).\n Core keeps .page/.spread at 'position: relative; z-index: auto'\n precisely so they are not stacking contexts.\n - a clipping ancestor (overflow other than visible) \u2014 but only where\n the art actually overhangs that ancestor's clip box on a clipped\n axis: the overhang is cut off, the same mechanism that clips a\n .gp-bleed plate back to the wrapper's width. Clipping never reorders\n layers \u2014 within-bounds art under a clipping .page prints whole and\n still behind (measured; see the build audit's comment in\n engine/compiler/build.ts), and a static wrapper's overflow never\n binds an abspos .gp-pin at all.\n The build-time engine.layer.trapped audit reports both against the live\n ancestor chain. printsafe/page-containment is only an early source hint for\n declarations written directly on .page/.spread. */\n:root {\n --gp-z-behind: -1;\n --gp-z-base: 0;\n --gp-z-raised: 1;\n --gp-z-front: 2;\n}\n.gp-behind { z-index: var(--gp-z-behind); }\n.gp-base { z-index: var(--gp-z-base); }\n.gp-raised { z-index: var(--gp-z-raised); }\n.gp-front { z-index: var(--gp-z-front); }\n";
139
+ export declare const GUTTERPRESS_CSS = "\n/* gp-* author image/block vocabulary. One vocabulary, gp-* only \u2014 the\n pre-vocabulary utility names (.center/.float-left/.float-right/\n .full-width/.full-bleed) were REMOVED when gp-* shipped; books rename\n the classes in their markdown (see the migration note). Source ORDER is\n the contract \u2014 see the doctrine comment above. */\n\n/* flow positions */\n.gp-left {\n float: left;\n margin: 0 var(--gp-gap, 1em) var(--gp-gap, 1em) 0;\n max-width: 50%;\n}\n.gp-right {\n float: right;\n margin: 0 0 var(--gp-gap, 1em) var(--gp-gap, 1em);\n max-width: 50%;\n}\n.gp-center {\n display: block;\n float: none;\n margin-left: auto;\n margin-right: auto;\n max-width: 100%;\n}\n.gp-full {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n@page gp-full-bleed { margin-left: 0; margin-right: 0; }\n.gp-bleed {\n display: block;\n float: none;\n break-before: page;\n page: gp-full-bleed;\n max-width: none;\n width: 100%;\n margin-left: 0;\n margin-right: 0;\n}\n\n/* sizes \u2014 AFTER the flow positions so max-width:100% lifts the floats' 50%\n cap at equal specificity */\n.gp-small { width: 25%; max-width: 100%; }\n.gp-medium { width: 50%; max-width: 100%; }\n.gp-large { width: 75%; max-width: 100%; }\n\n/* float clearance presets \u2014 consumed by var(--gp-gap) in the float rules\n above and by .gp-shape's shape-margin below; --gp-gap itself is\n author-settable CSS */\n.gp-tight { --gp-gap: 0.5em; }\n.gp-loose { --gp-gap: 2em; }\n\n/* column runs \u2014 plain CSS Multi-column, exposed as author vocabulary so\n \"put this in two columns\" does not require borrowing a styled container\n from the book's own component layer. That borrowing is what this exists\n to prevent: a book whose theme paints .section chrome by default gives\n every author who opens a section just to start a column run a panel they\n did not ask for, and the book then needs a reset rule to take it back.\n With a neutral primitive the author opts into columns and nothing else.\n\n Permanent vocabulary, not a shim: Chromium implements multicol natively\n and these rules are the standard properties verbatim, so there is no\n spec gap here to remove later. Deliberately minimal \u2014 column-fill is\n NOT set, because the correct value depends on whether the run fragments\n across pages (auto packs each page's columns; the CSS initial balance is\n right for a run that fits on one page) and only the author knows which.\n --gp-column-gap is author-settable. */\n.gp-columns-2 { columns: 2; column-gap: var(--gp-column-gap, 1.5em); }\n.gp-columns-3 { columns: 3; column-gap: var(--gp-column-gap, 1.5em); }\n\n/* the per-shape decisions the paragraph above deliberately leaves to the\n author, named instead of left as raw CSS every book was reinventing\n (2026-09-01 CSS architecture review, findings C1/C7 \u2014 CLAUDE.md \u00A70:\n \"behavior broadly useful to non-technical authors belongs in core\").\n Permanent vocabulary, standard properties verbatim \u2014 same rationale as\n the column/grid runs above. One name each, no aliases:\n .gp-columns-all column-span: all a heading or block that\n spans every column in the\n run it sits inside.\n .gp-columns-flow column-fill: auto a run that FRAGMENTS across\n pages \u2014 every page's\n columns fill instead of\n only the last one\n balancing (the dead-column\n collapse the build's\n engine.multicol.dead-column\n warning names this fix\n for).\n .gp-columns-balanced column-fill: balance a run that fits on ONE\n page (the CSS initial\n value \u2014 ragged columns\n would be wrong here). */\n.gp-columns-all { column-span: all; }\n.gp-columns-flow { column-fill: auto; }\n.gp-columns-balanced { column-fill: balance; }\n\n/* grid runs \u2014 the SLOTTED counterpart to the column runs above. Grid places\n each child into the next cell, across then down (deterministic slots: card\n layouts, stat blocks, image-plus-caption pairs); columns FLOW one text run\n down then across. Same neutral-primitive rationale as .gp-columns-*, and\n permanent vocabulary for the same reason: standard CSS Grid verbatim, no\n spec gap to remove later. MEASURED (Chromium 151, gp-grid evidence pack):\n grid rows fragment across sheets with EXACT print/viewer parity \u2014 2- and\n 3-col, unequal item heights, mid-row cuts, multi-sheet overflow,\n break-inside:avoid, gap geometry \u2014 so a grid taller than the page is safe,\n no fit-one-page constraint. Two things to know, not fix:\n - on a min-height page root (MARKER_CSS), default align-content\n stretches rows apart to fill the page \u2014 identically in both engines.\n Authors wanting packed rows set align-content: start.\n - a @page-break / @column-break marker DIRECTLY inside a grid container\n becomes a grid item and corrupts placement (the one measured parity\n break); markers.js diagnoses it (break_inside_grid).\n --gp-grid-gap is author-settable. */\n.gp-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n.gp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gp-grid-gap, 1.5em); }\n\n/* fragmentation controls \u2014 generic pagination utilities for any element,\n independent of the column/grid vocabulary above (a book reached for\n these just as often outside a multicol run: keeping a card whole,\n forcing a section to start a fresh page). Standard properties only \u2014\n this project is Chromium-only (CLAUDE.md), so there are no legacy\n page-break-* twins to also emit. One name each. */\n.gp-no-break { break-inside: avoid; }\n.gp-break-before { break-before: page; }\n\n/* shape wrap \u2014 text follows the image's alpha silhouette instead of its\n rectangular box. shape-outside only applies to floats, so this is inert\n without .gp-left/.gp-right (and under .gp-pin, which un-floats). The\n shape URL cannot be written in CSS (url() contexts can't read attr()),\n so the image renderer rule (images.ts) mirrors the src into an inline\n --gp-shape:url(...) custom property whenever it sees this class --\n authors only ever type the class. threshold 0.2 ignores near-transparent\n anti-aliasing halos; shape-margin shares the float-gap vocabulary. */\nimg.gp-shape {\n shape-outside: var(--gp-shape);\n shape-image-threshold: 0.2;\n shape-margin: var(--gp-gap, 1em);\n}\n\n/* pin \u2014 within the nearest positioned ancestor (.page/.spread, rule above).\n inset:0 and the explicit centers are load-bearing; see doctrine comment. */\n.gp-pin {\n position: absolute;\n inset: 0;\n align-self: center;\n justify-self: center;\n margin: 0;\n max-width: 100%;\n}\n\n/* pin edge modifiers \u2014 AFTER .gp-pin to beat its center defaults;\n justify-self is inert on in-flow floats, so gp-left/gp-right safely do\n double duty as flow float + pin edge */\n.gp-top { align-self: start; }\n.gp-bottom { align-self: end; }\n.gp-left { justify-self: start; }\n.gp-right { justify-self: end; }\n\n/* wrapper-margin neutralization (same pattern and rationale as the\n .gp-bleed paragraph-margin note in the doctrine comment; for pin, the\n emptied paragraph would otherwise leave a phantom margin gap in flow) */\n:where(p:has(> img.gp-bleed:only-child)) { margin: 0; }\n:where(p:has(> img.gp-pin:only-child)) { margin: 0; }\n\n/* depth \u2014 a named ladder for z-index, so books stop hand-tuning bare\n integers. A real book measured 21 z-index declarations using only four\n distinct values (-1, 0, 1, 2), each written literally at its use site.\n The custom properties are the author-settable surface (a book needing a\n deeper stack raises them once); the classes are the shorthand.\n\n NOT named \"layer\": CSS Paged Media 3 \u00A73.1 already defines \"page layers\"\n (page background, canvas, borders, contents, margin boxes) and those are\n parts of the PAGE BOX, not a z-ladder for content. Reusing the word for a\n different concept would collide with the spec vocabulary this project\n tracks. The pin EDGE modifiers already own .gp-top/.gp-bottom, so the\n ladder avoids those words too.\n\n .gp-behind is the one that earns its place: it puts a pinned image UNDER\n the page's text, which is otherwise impossible to express without a bare\n negative z-index. \"Above\" needs no class \u2014 an out-of-flow pin already\n paints above in-flow content.\n\n Two things silently defeat .gp-behind, neither visible at the use site:\n - a stacking context on the .page/.spread ancestor (z-index, isolation,\n opacity, filter, transform on it traps the negative layer inside).\n Core keeps .page/.spread at 'position: relative; z-index: auto'\n precisely so they are not stacking contexts.\n - a clipping ancestor (overflow other than visible) \u2014 but only where\n the art actually overhangs that ancestor's clip box on a clipped\n axis: the overhang is cut off, the same mechanism that clips a\n .gp-bleed plate back to the wrapper's width. Clipping never reorders\n layers \u2014 within-bounds art under a clipping .page prints whole and\n still behind (measured; see the build audit's comment in\n engine/compiler/build.ts), and a static wrapper's overflow never\n binds an abspos .gp-pin at all.\n The build-time engine.layer.trapped audit reports both against the live\n ancestor chain. printsafe/page-containment is only an early source hint for\n declarations written directly on .page/.spread. */\n:root {\n --gp-z-behind: -1;\n --gp-z-base: 0;\n --gp-z-raised: 1;\n --gp-z-front: 2;\n}\n.gp-behind { z-index: var(--gp-z-behind); }\n.gp-base { z-index: var(--gp-z-base); }\n.gp-raised { z-index: var(--gp-z-raised); }\n.gp-front { z-index: var(--gp-z-front); }\n\n/* GFM-style alert/callout boxes (#237) \u2014 the DOM gfm-alerts.ts emits from\n \"> [!TYPE]\" blockquote syntax when a project opts into the bundled\n gutterpress-gfm-alerts feature (BUILTIN_OPTIONAL_PLUGINS, renderer.ts).\n The plugin is optional and inert by default; these rules are equally\n inert wherever the classes never appear, exactly like every other\n utility above, so shipping them costs nothing to a book that has not\n turned the feature on.\n\n This is authored-COMPONENT vocabulary, not the @marker structural family\n (markers.js/MARKER_CSS) \u2014 CLAUDE.md \u00A76 splits the two core CSS blocks by\n ROLE, and an author-facing box an author invokes via inline syntax is the\n same role as every other gp-* utility here, not page/chapter/section DOM.\n\n Deliberately minimal (CLAUDE.md \"thin over capable\" / the issue's own\n \"a rule and a label\"): a left border plus a bold label, :where()'d to\n zero specificity so a theme's own (unlayered) rule for any of these\n classes wins outright regardless of selector weight \u2014 see the\n cascade-layers note atop this file. Five semantic accent colors (the same\n NOTE/TIP/IMPORTANT/WARNING/CAUTION hues GitHub's own alerts use \u2014 a\n long-established, non-proprietary convention, not DC branding) are\n exposed as :root custom properties, the same author-settable-surface\n pattern as the --gp-z-* ladder above: a book retints every alert of one\n type by overriding a single property, no specificity fight required.\n --gp-alert-color cascades from the type modifier down to .gp-alert-title\n through ordinary CSS inheritance (the title is always a DOM descendant of\n the box), so it is set in exactly five places, not ten. */\n:root {\n --gp-alert-note-color: #0969da;\n --gp-alert-tip-color: #1a7f37;\n --gp-alert-important-color: #8250df;\n --gp-alert-warning-color: #9a6700;\n --gp-alert-caution-color: #cf222e;\n}\n:where(.gp-alert) {\n margin: 1em 0;\n padding: 0.5em 1em;\n border-left: 0.25em solid var(--gp-alert-color, currentColor);\n}\n:where(.gp-alert-title) {\n margin: 0 0 0.5em;\n font-weight: bold;\n color: var(--gp-alert-color, inherit);\n}\n:where(.gp-alert-note) { --gp-alert-color: var(--gp-alert-note-color); }\n:where(.gp-alert-tip) { --gp-alert-color: var(--gp-alert-tip-color); }\n:where(.gp-alert-important) { --gp-alert-color: var(--gp-alert-important-color); }\n:where(.gp-alert-warning) { --gp-alert-color: var(--gp-alert-warning-color); }\n:where(.gp-alert-caution) { --gp-alert-color: var(--gp-alert-caution-color); }\n";
140
140
  /**
141
141
  * GP_CLASSES — every class an author may legitimately write with a `gp-`
142
142
  * prefix: the `.gp-*` selectors in `GUTTERPRESS_CSS` above, the two
@@ -37,6 +37,20 @@ export declare function renderChapters(inputDir: string, opts?: {
37
37
  files?: string[] | null;
38
38
  plugins?: LoadedPlugin[];
39
39
  pluginCss?: string;
40
+ /**
41
+ * #238 — absolute paths of plugin-declared `styles` files, in plugin load
42
+ * order (`loadPluginsWithCss`'s `pluginStylePaths`). Inlined through the
43
+ * SAME pipeline the project's own stylesheets get (`inlineStyles`: fonts/
44
+ * images embedded, local `@import` followed, print-safety lintable) and
45
+ * placed in the SAME cascade position the legacy `pluginCss` string
46
+ * already holds — after core, before the project's own stylesheets.
47
+ * Absolute paths pass through `inlineStyles` unchanged (`path.resolve` is
48
+ * a no-op on an already-absolute input), so a plugin's stylesheet
49
+ * location is independent of `inputDir`. Omitting this (existing
50
+ * string-only-`css` plugins, or none at all) is a zero-cost, byte-
51
+ * identical no-op — see the test asserting exactly that.
52
+ */
53
+ pluginStylePaths?: string[];
40
54
  /** Wrap each source file for incremental preview pagination. */
41
55
  wrapChapters?: boolean;
42
56
  /** Add source-file ids to source-mapped preview blocks without wrappers. */
@@ -70,6 +84,8 @@ export declare function renderChaptersToFile(inputDir: string, outDir: string, o
70
84
  files?: string[] | null;
71
85
  plugins?: LoadedPlugin[];
72
86
  pluginCss?: string;
87
+ /** #238 — see {@link renderChapters}'s option of the same name. */
88
+ pluginStylePaths?: string[];
73
89
  /** ARCH finding #4 — see {@link renderChapters}'s option of the same name. */
74
90
  onChapterWarnings?: (file: string, warnings: LayoutWarning[]) => void;
75
91
  /** See {@link renderChapters}'s options of the same names. */
@@ -1,23 +1,12 @@
1
1
  import type { ResolvedPluginConfig } from "../../schema/manifest.types";
2
2
  import type { LoadedPlugin } from "./renderer";
3
3
  export type { GutterpressPlugin, GutterpressPluginMetadata, GutterpressPluginExport, LoadedPlugin, } from "./renderer";
4
- export { applyPlugins, collectPluginCss } from "./renderer";
4
+ export { applyPlugins, collectPluginCss, collectPluginStylePaths } from "./renderer";
5
5
  /** Test-only mutation seam between snapshot completion and snapshot verification. */
6
6
  export declare function __setVendorSnapshotHookForTests(hook?: (sourceRoot: string, snapshotRoot: string) => void | Promise<void>): void;
7
7
  export declare function clearVendoredPluginResolver(baseDir: string, packageName: string, version: string): void;
8
8
  /** Test-only: reset the path-plugin cache between test cases. */
9
9
  export declare function __resetPathPluginCacheForTests(): void;
10
- /**
11
- * Load a single plugin from a file path or npm package.
12
- *
13
- * Throws if the plugin cannot be resolved, imported, or doesn't export a
14
- * valid plugin function. The error message identifies which manifest entry
15
- * failed so users can find it.
16
- *
17
- * Path plugins always go through the mtime cache (see the call below): it is
18
- * correct in both a one-shot CLI build and the long-lived Electron host that
19
- * runs `runBuild` in-process, so no caller-selected cache mode is needed.
20
- */
21
10
  export declare function loadPlugin(config: ResolvedPluginConfig, baseDir: string): Promise<LoadedPlugin>;
22
11
  /**
23
12
  * Load all plugins from the resolved configuration.
@@ -50,6 +39,13 @@ export interface LoadedPluginsWithCss {
50
39
  * straight through without an `?? []` at every call site. */
51
40
  plugins: LoadedPlugin[] | undefined;
52
41
  pluginCss: string;
42
+ /**
43
+ * #238 — absolute paths of every plugin-declared `styles` file, flattened
44
+ * in plugin load order. `[]` when no loaded plugin declares any (including
45
+ * when `configs` was empty), so callers can pass this straight through to
46
+ * `renderChapters`'s `pluginStylePaths` without an `?? []` guard.
47
+ */
48
+ pluginStylePaths: string[];
53
49
  }
54
50
  /**
55
51
  * Shared "load plugins -> collect their CSS" preamble (ARCH finding #53).
@@ -47,6 +47,7 @@ export interface GutterpressPluginMetadata {
47
47
  * export default plugin;
48
48
  * export const metadata: GutterpressPluginMetadata = { name: 'my-plugin', version: '1.0.0' };
49
49
  * export const css = `.my-class { color: red; }`;
50
+ * export const styles = ["./styles/components.css", "./styles/callouts.css"];
50
51
  * ```
51
52
  */
52
53
  export interface GutterpressPluginExport {
@@ -54,6 +55,17 @@ export interface GutterpressPluginExport {
54
55
  metadata?: GutterpressPluginMetadata;
55
56
  /** CSS injected into <head> after user stylesheets. Use sparingly — has equal cascade specificity. */
56
57
  css?: string;
58
+ /**
59
+ * File-based plugin CSS (#238), as paths RELATIVE TO THIS MODULE — resolved
60
+ * by the node-coupled loader (`plugins.ts`), never by this pure module. Each
61
+ * file enters the SAME pipeline a manifest `styles:` entry does: asset-
62
+ * inlined (fonts/images embedded, local `@import` followed) and print-safety
63
+ * lintable — unlike `css`, which is an opaque string no other subsystem can
64
+ * see. Kept alongside `css` (not a replacement) for one-liners that don't
65
+ * warrant a separate file. Cascade position matches `css`'s: after core,
66
+ * before the project's own stylesheets.
67
+ */
68
+ styles?: string[];
57
69
  }
58
70
  /** Internal representation of a loaded plugin, ready for `md.use()`. */
59
71
  export interface LoadedPlugin {
@@ -61,6 +73,13 @@ export interface LoadedPlugin {
61
73
  plugin: GutterpressPlugin;
62
74
  metadata?: GutterpressPluginMetadata;
63
75
  css?: string;
76
+ /**
77
+ * #238 — UNLIKE `GutterpressPluginExport.styles` (author-declared, relative
78
+ * to the plugin module), this is already resolved to ABSOLUTE filesystem
79
+ * paths by the loader (`plugins.ts`'s `loadPlugin`), in the plugin's own
80
+ * declared order. `undefined`/`[]` for a plugin that declares none.
81
+ */
82
+ styles?: string[];
64
83
  options: Record<string, unknown>;
65
84
  }
66
85
  /**
@@ -72,6 +91,14 @@ export interface LoadedPlugin {
72
91
  * non-technical author gets the feature instantly instead of a "not installed"
73
92
  * error. (attrs/footnote/deflist are NOT here — they are always-on defaults
74
93
  * applied unconditionally below.)
94
+ *
95
+ * `gutterpress-gfm-alerts` (#237) is keyed differently from its four
96
+ * siblings: it is not a real npm package, it is Gutterpress's OWN code
97
+ * (gfm-alerts.ts) registered under a name that reads like one, matching the
98
+ * shape "keyed by npm name" is written for. There is nothing to install
99
+ * either way — the lookup below always wins before any npm resolution is
100
+ * attempted (`plugins.ts`'s `loadPlugin`) — so a real npm package never
101
+ * existing under this exact name costs nothing.
75
102
  */
76
103
  export declare const BUILTIN_OPTIONAL_PLUGINS: Record<string, GutterpressPlugin>;
77
104
  /**
@@ -97,7 +124,16 @@ export declare const BUILTIN_OPTIONAL_PLUGINS: Record<string, GutterpressPlugin>
97
124
  *
98
125
  * GFM-style `> [!NOTE]` alerts were also moved into the DC plugin on the
99
126
  * same date because the emitted classes (dc-alert, dc-vibe-callout, etc.)
100
- * are DC-branded. Core should not leak DC identifiers.
127
+ * were DC-branded core should not leak DC identifiers. #237 (0.10.7)
128
+ * restored a core-owned, unbranded equivalent as an OPT-IN bundled feature —
129
+ * `gfm-alerts.ts`, registered below as `gutterpress-gfm-alerts` — emitting
130
+ * only the standard GitHub five (NOTE/TIP/IMPORTANT/WARNING/CAUTION) as
131
+ * neutral `gp-alert`/`gp-alert-<type>` structure (see that file's header).
132
+ * This does not re-converge with the DC plugin: DC's branded extra types
133
+ * (`[!DM]`/`[!VIBE]`/`[!ORIGIN]`, etc.) and its own class names stay exactly
134
+ * where they were moved to, layered on top of (or independent from) this
135
+ * primitive. A project using neither plugin still renders `> [!NOTE]` as a
136
+ * literal blockquote, unchanged — this feature is opt-in, not a default.
101
137
  *
102
138
  * @param customPlugins - Optional array of custom plugins to load
103
139
  */
@@ -114,3 +150,14 @@ export declare function applyPlugins(md: MarkdownIt, plugins: LoadedPlugin[]): v
114
150
  * Collect CSS from all loaded plugins, concatenated in load order.
115
151
  */
116
152
  export declare function collectPluginCss(plugins: LoadedPlugin[]): string;
153
+ /**
154
+ * Collect every plugin-declared stylesheet PATH (#238), flattened in plugin
155
+ * load order (a plugin's own files keep their declared order). These are
156
+ * already-resolved ABSOLUTE filesystem paths — `plugins.ts`'s loader resolves
157
+ * them before a `LoadedPlugin` exists, so this stays a pure list operation
158
+ * with no `node:*` needed, matching {@link collectPluginCss}. The caller feeds
159
+ * the result through the SAME asset-inline pipeline a manifest `styles:` list
160
+ * gets (see `lib/markdown/index.ts`'s `renderChapters`), which is where the
161
+ * files are actually read.
162
+ */
163
+ export declare function collectPluginStylePaths(plugins: LoadedPlugin[]): string[];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Resolve a declared stylesheet list to absolute, existence-checked paths, in
3
+ * declared order. `undefined`/`[]` returns `undefined` — a plugin/theme that
4
+ * declares no `styles` pays zero cost here, the exact contract #238
5
+ * established and #239 now shares. Any entry that doesn't resolve to a real
6
+ * file THROWS immediately: a final artifact must never silently drop an
7
+ * author-declared stylesheet from the cascade (CLAUDE.md §5's fail-fast
8
+ * doctrine) — this is the load-time check, matching a plugin's own
9
+ * missing-file error, not a render-time warning.
10
+ *
11
+ * `baseDir` is never resolved by this function — whether a plugin/theme even
12
+ * HAS a usable base directory (e.g. a plugin loaded via a bare npm specifier
13
+ * with no on-disk path) is a caller-specific question the caller answers
14
+ * before calling this; by the time `baseDir` reaches here it names a real
15
+ * directory to resolve `rawStyles` against.
16
+ *
17
+ * @param subject Names the declaring thing for the error message, e.g.
18
+ * `Plugin "my-plugin"` or `Theme "dc-design"`.
19
+ */
20
+ export declare function resolveDeclaredStyles(rawStyles: string[] | undefined, baseDir: string, subject: string): string[] | undefined;
@@ -7,6 +7,8 @@ export interface ProjectStyle {
7
7
  /** True when this stylesheet is in the manifest `styles:` list (the active set). */
8
8
  active: boolean;
9
9
  }
10
+ /** Project-relative, forward-slash display path for an absolute css path. */
11
+ export declare function relDisplay(projectDir: string, absPath: string): string;
10
12
  /**
11
13
  * THE canonical "which stylesheet does this project use?" resolver — consumed by
12
14
  * BOTH the renderer (to `<link>` them) and the editor (to edit them), so they
@@ -10,6 +10,34 @@ export interface ThemeMetadata {
10
10
  description?: string;
11
11
  /** Optional preview image path (relative to the theme folder). */
12
12
  preview?: string | null;
13
+ /**
14
+ * #239 — ordered stylesheets, relative to the theme folder. A theme is no
15
+ * longer capped at one file: this is what lets a real component library
16
+ * (tokens/base/components/templates/rules as separate layered sheets) be a
17
+ * theme. Absent/empty defaults to `["theme.css"]` — every theme published
18
+ * before this field existed keeps working, byte-for-byte, untouched. Globs
19
+ * are NOT supported, matching the manifest's own `styles:` list (§0 — one
20
+ * authoring convention, not two).
21
+ */
22
+ styles?: string[];
23
+ /**
24
+ * #239 — engine-conditional sheets, relative to the theme folder. Applying
25
+ * the theme appends these to the manifest's `engineStyles.native` list
26
+ * (loaded last, so furniture wins — see manifest.ts's resolveWithPreset).
27
+ */
28
+ engineStyles?: {
29
+ native?: string[];
30
+ };
31
+ /**
32
+ * #239 — which declared sheet (a path from `styles`, or `theme.css` when
33
+ * `styles` is absent) carries the author-facing `:root` token surface. Lets
34
+ * a rich multi-sheet theme tell the Design panel's guided token editor
35
+ * where to look instead of it guessing at the single active stylesheet.
36
+ * Purely advisory metadata — nothing in this module enforces it. Absent
37
+ * here means "no override"; {@link ThemeInfo.tokensFile} is where the
38
+ * default (the theme's primary/only sheet) gets filled in.
39
+ */
40
+ tokensFile?: string;
13
41
  }
14
42
  /** Author-friendly metadata for one theme (built-in or project). */
15
43
  export interface ThemeInfo {
@@ -25,6 +53,23 @@ export interface ThemeInfo {
25
53
  kind: "builtin" | "project";
26
54
  /** Optional preview image path relative to the theme folder. */
27
55
  preview?: string | null;
56
+ /**
57
+ * #239 — the theme's declared stylesheets, relative to the theme folder, in
58
+ * cascade order. Always populated (defaults to `["theme.css"]` when the
59
+ * theme declares no `styles`), so a caller never needs to re-derive the
60
+ * default itself.
61
+ */
62
+ styles: string[];
63
+ /**
64
+ * #239 — which declared sheet (relative to the theme folder) carries the
65
+ * `:root` token surface — see {@link ThemeMetadata.tokensFile}. ALWAYS
66
+ * populated: an explicit `theme.json` declaration wins, otherwise this
67
+ * defaults to the theme's primary sheet (`styles[0]`) — `"theme.css"` for
68
+ * every legacy single-sheet theme, exactly as `styles` itself defaults.
69
+ * A caller (the Design panel) never needs its own "guess the token sheet"
70
+ * fallback.
71
+ */
72
+ tokensFile: string;
28
73
  }
29
74
  /** A built-in theme resolved to disk (extracted from the embedded assets). */
30
75
  export interface ResolvedTheme {
@@ -42,19 +87,54 @@ export type ApplyThemeTarget = {
42
87
  kind: "project";
43
88
  id: string;
44
89
  };
90
+ /**
91
+ * A theme's declared stylesheets, relative to its folder, in cascade order
92
+ * (#239). An absent OR empty `styles` defaults to `["theme.css"]` — the
93
+ * single-file format every theme published before this field existed already
94
+ * satisfies, so nothing needs migrating. Exported for `theme-import.ts`,
95
+ * which validates every one of these (existence + print-safety) at import
96
+ * time, the same way it always validated the lone `theme.css`.
97
+ */
98
+ export declare function themeStyleList(meta: ThemeMetadata): string[];
99
+ /**
100
+ * Every declared sheet must live INSIDE the theme folder. A theme is
101
+ * self-contained by contract (apply copies the whole folder), and an imported
102
+ * package is untrusted input: a `../` or absolute entry would make apply read
103
+ * a file from anywhere on disk into the book.
104
+ *
105
+ * This is a WRITE-BOUNDARY guard — `applyTheme`, `importThemeFromFolder` and
106
+ * `theme-import.ts`'s package validation call it before copying anything or
107
+ * wiring a manifest. It deliberately does NOT live in {@link themeStyleList},
108
+ * which the read paths (`listProjectThemes`, `getActiveTheme`,
109
+ * `getPreviousTheme`, `readThemeCss`) also call: one hand-edited theme.json
110
+ * must not take down listing every theme, the same reason `readThemeMeta`
111
+ * returns `{}` for an unparseable file instead of throwing.
112
+ */
113
+ export declare function assertThemeSheetsContained(meta: ThemeMetadata): void;
114
+ /**
115
+ * A theme's declared engine-conditional sheets, relative to its folder
116
+ * (#239). `Array.isArray` guards a malformed `engineStyles.native` (authored
117
+ * as something other than a list) — treated as "none declared" rather than a
118
+ * hard crash on a JSON author's typo. Exported for `theme-import.ts`'s import
119
+ * validation, same reuse rationale as {@link themeStyleList}.
120
+ */
121
+ export declare function themeEngineStyleList(meta: ThemeMetadata): string[];
45
122
  /** List the built-in themes (metadata read from the extracted embedded assets). */
46
123
  export declare function listBuiltInThemes(): Promise<ThemeInfo[]>;
47
124
  /** Resolve a built-in theme to its extracted `theme.css` + parsed metadata. */
48
125
  export declare function resolveBuiltInTheme(id: string): Promise<ResolvedTheme>;
49
126
  /**
50
127
  * List the themes that live inside the project under `themes/<id>/` (each a
51
- * folder with a `theme.css`). Returns `[]` when there is no `themes/` folder.
128
+ * folder whose PRIMARY declared sheet exists — `theme.css` by default, or
129
+ * `theme.json`'s `styles[0]` for a multi-sheet theme, #239). Returns `[]`
130
+ * when there is no `themes/` folder.
52
131
  */
53
132
  export declare function listProjectThemes(projectDir: string): Promise<ThemeInfo[]>;
54
133
  /**
55
- * Read the project's currently active theme (the theme whose `theme.css` is in
56
- * the manifest `styles:` list AND whose folder exists under `themes/`). Returns
57
- * `null` when no theme is applied.
134
+ * Read the project's currently active theme (the theme with an entry under
135
+ * `themes/<id>/…` in the manifest `styles:` list AND whose folder's PRIMARY
136
+ * declared sheet still exists on disk, #239). Returns `null` when no theme is
137
+ * applied.
58
138
  */
59
139
  export declare function getActiveTheme(projectDir: string): Promise<ThemeInfo | null>;
60
140
  /**
@@ -92,8 +172,10 @@ export declare function revertTheme(projectDir: string): Promise<ThemeInfo>;
92
172
  export declare function applyTheme(projectDir: string, target: ApplyThemeTarget): Promise<ThemeInfo>;
93
173
  /**
94
174
  * Import a theme from a local folder by copying it into the project's
95
- * `themes/<id>/`. The folder MUST contain a `theme.css`. Metadata comes from
96
- * its `theme.json` (synthesised from the folder name when absent). The imported
175
+ * `themes/<id>/`. The folder MUST contain its PRIMARY declared stylesheet
176
+ * `theme.css` by default, or `theme.json`'s `styles[0]` for a multi-sheet
177
+ * theme (#239) that ships no `theme.css` at all. Metadata comes from its
178
+ * `theme.json` (synthesised from the folder name when absent). The imported
97
179
  * theme becomes available to {@link applyTheme}; importing does NOT auto-apply.
98
180
  */
99
181
  export declare function importThemeFromFolder(projectDir: string, sourceDir: string): Promise<ThemeInfo>;
@@ -114,8 +196,11 @@ export declare function importThemeFromFolder(projectDir: string, sourceDir: str
114
196
  export declare function importThemeFromUrl(projectDir: string, url: string): Promise<ThemeInfo>;
115
197
  /**
116
198
  * Read a theme's CSS for previewing. Built-in themes read from the embedded
117
- * assets; project themes read from `themes/<id>/theme.css`. Used by the host to
118
- * feed the renderer a sample-render thumbnail (the renderer never touches fs).
199
+ * assets; project themes read and concatenate EVERY sheet `theme.json`
200
+ * declares (#239 `theme.css` alone when it declares none), in cascade
201
+ * order. Used by the host to feed the renderer a sample-render thumbnail (the
202
+ * renderer never touches fs) — a read-only preview, so concatenation is fine
203
+ * even though it wouldn't be for an editable surface.
119
204
  */
120
205
  export declare function readThemeCss(projectDir: string | null, source: {
121
206
  kind: "builtin" | "project";
@@ -123,6 +208,22 @@ export declare function readThemeCss(projectDir: string | null, source: {
123
208
  }): Promise<string>;
124
209
  /**
125
210
  * Remove an imported/applied project theme folder. If it was the active theme,
126
- * its `styles:` entry is dropped too. Never touches built-in (embedded) themes.
211
+ * its ENTIRE block is dropped too every `styles:` entry AND every
212
+ * `engineStyles.native` entry under `themes/<id>/…` (#239), not just a single
213
+ * `theme.css` href. Never touches built-in (embedded) themes.
127
214
  */
128
215
  export declare function removeProjectTheme(projectDir: string, id: string): Promise<void>;
216
+ /**
217
+ * #236 follow-through: detect a project whose `styles/book.css` is
218
+ * byte-identical to a built-in theme's `theme.css` while NO theme is tracked
219
+ * as active. That shape is exactly what a pre-0.10.7 `gutterpress new` (or
220
+ * "set up as a book") produced — a real, working stylesheet that is simply
221
+ * invisible to this command and to the desktop's Theme panel, and which will
222
+ * keep loading AFTER (so silently override) whatever theme is applied next.
223
+ * Read-only: this never modifies the project — it only surfaces a note so the
224
+ * author can decide whether to run `theme apply` themselves.
225
+ */
226
+ export declare function detectLegacyForkedTheme(projectDir: string, active: ThemeInfo | null): Promise<{
227
+ id: string;
228
+ name: string;
229
+ } | null>;
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  MANIFEST_FILENAMES,
3
3
  runLint
4
- } from "./cli-kwt5ftvc.js";
4
+ } from "./cli-kzf64c2g.js";
5
5
  import {
6
6
  log
7
- } from "./cli-m99hj80k.js";
7
+ } from "./cli-pvk2s9d7.js";
8
8
  import {
9
9
  UsageError,
10
10
  rejectExtraPositionals,
11
11
  rejectUnknownFlags
12
- } from "./cli-he23vgjj.js";
13
- import"./cli-bwbs3qm4.js";
12
+ } from "./cli-h8ejbtyr.js";
13
+ import"./cli-s8q42x3r.js";
14
14
  import"./cli-c41yr7he.js";
15
15
  import {
16
16
  EXIT_CODES
@@ -38,13 +38,8 @@
38
38
  },
39
39
  "engineStyles": {
40
40
  "type": "object",
41
- "description": "Engine-conditional stylesheets, appended AFTER `styles`. `.native` is the only list that still applies; `.paged` is accepted-but-ignored (a warning fires if it has entries).",
41
+ "description": "Engine-conditional stylesheets, appended AFTER `styles`. `.native` is the only list the native engine is the only engine, so there is nothing else to condition on. A manifest from the dual-engine era may still carry `.paged`; it keeps loading (with a one-line warning) but is no longer part of this schema.",
42
42
  "properties": {
43
- "paged": {
44
- "type": "array",
45
- "description": "Accepted for backward-compatible manifest parsing only; entries here are ignored with a warning.",
46
- "items": { "type": "string" }
47
- },
48
43
  "native": { "type": "array", "items": { "type": "string" } }
49
44
  },
50
45
  "additionalProperties": false,
@@ -58,7 +53,7 @@
58
53
  },
59
54
  "styles": {
60
55
  "type": "array",
61
- "description": "Project-relative CSS files to <link> into the rendered book, applied in order. If omitted, Gutterpress discovers one: styles/book.css, else css/print.css, css/index.css, css/style.css, css/main.css, else the first discovered .css, else none.",
56
+ "description": "Project-relative CSS files to <link> into the rendered book, applied in order. If omitted, Gutterpress discovers one: styles/book.css (what `gutterpress new` scaffolds), else four legacy names kept only for pre-existing projects — css/print.css, css/index.css, css/style.css, css/main.css else the first discovered .css, else none. New projects should set this explicitly or use styles/book.css.",
62
57
  "items": {
63
58
  "type": "string",
64
59
  "minLength": 1,
@@ -115,7 +110,7 @@
115
110
  },
116
111
  "priority": {
117
112
  "type": "integer",
118
- "description": "Load order. Higher priority loads first.",
113
+ "description": "Advanced, rarely needed. Load order: higher loads first. If one of your plugins needs to see tokens another one of your plugins produces, give it the LOWER number of the two, so it loads (and runs) after.",
119
114
  "default": 100,
120
115
  "examples": [100, 200]
121
116
  },
@@ -322,6 +317,14 @@
322
317
  { "type": "boolean", "const": false }
323
318
  ]
324
319
  },
320
+ "cssOwnership": {
321
+ "description": "Path to a CSS ownership contract (default discovery: .gutterpress/css-contract.yaml), or false to disable the source.css-ownership check. Optional — absent means no contract, so the check reports nothing.",
322
+ "default": null,
323
+ "oneOf": [
324
+ { "type": "string", "minLength": 1 },
325
+ { "type": "boolean", "const": false }
326
+ ]
327
+ },
325
328
  "allowedCallouts": {
326
329
  "type": "array",
327
330
  "deprecated": true,
@@ -5,16 +5,16 @@ import {
5
5
  TARGETS,
6
6
  TARGET_IDS,
7
7
  scaffoldProject
8
- } from "./cli-kwt5ftvc.js";
8
+ } from "./cli-kzf64c2g.js";
9
9
  import {
10
10
  resolveGhostscript
11
- } from "./cli-m99hj80k.js";
11
+ } from "./cli-pvk2s9d7.js";
12
12
  import {
13
13
  UsageError,
14
14
  rejectExtraPositionals,
15
15
  rejectUnknownFlags
16
- } from "./cli-he23vgjj.js";
17
- import"./cli-bwbs3qm4.js";
16
+ } from "./cli-h8ejbtyr.js";
17
+ import"./cli-s8q42x3r.js";
18
18
  import {
19
19
  isToolAvailable
20
20
  } from "./cli-c41yr7he.js";