@kamishibai/sdk 0.1.1

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 (136) hide show
  1. package/README.md +192 -0
  2. package/package.json +54 -0
  3. package/src/blocks/board.js +210 -0
  4. package/src/blocks/callout.js +63 -0
  5. package/src/blocks/code.js +28 -0
  6. package/src/blocks/deck.js +76 -0
  7. package/src/blocks/diagram.js +265 -0
  8. package/src/blocks/element.js +51 -0
  9. package/src/blocks/graph.js +264 -0
  10. package/src/blocks/grid.js +156 -0
  11. package/src/blocks/index.js +106 -0
  12. package/src/blocks/list.js +50 -0
  13. package/src/blocks/placement.js +119 -0
  14. package/src/blocks/prose.js +28 -0
  15. package/src/blocks/quote.js +25 -0
  16. package/src/blocks/raw.js +47 -0
  17. package/src/blocks/registry.js +158 -0
  18. package/src/blocks/schema-parts.js +19 -0
  19. package/src/blocks/section.js +53 -0
  20. package/src/blocks/slide.js +104 -0
  21. package/src/blocks/stat.js +83 -0
  22. package/src/blocks/table.js +50 -0
  23. package/src/blocks/timeline.js +80 -0
  24. package/src/cli/commands/close.js +51 -0
  25. package/src/cli/commands/comments.js +73 -0
  26. package/src/cli/commands/debug.js +34 -0
  27. package/src/cli/commands/example.js +22 -0
  28. package/src/cli/commands/export.js +10 -0
  29. package/src/cli/commands/init.js +53 -0
  30. package/src/cli/commands/lint.js +93 -0
  31. package/src/cli/commands/list.js +54 -0
  32. package/src/cli/commands/open.js +31 -0
  33. package/src/cli/commands/promote.js +38 -0
  34. package/src/cli/commands/render.js +31 -0
  35. package/src/cli/commands/replay.js +49 -0
  36. package/src/cli/commands/schema.js +10 -0
  37. package/src/cli/commands/serve.js +178 -0
  38. package/src/cli/commands/setup.js +64 -0
  39. package/src/cli/commands/snapshot.js +29 -0
  40. package/src/cli/commands/templates.js +75 -0
  41. package/src/cli/deliver.js +54 -0
  42. package/src/cli/emit.js +29 -0
  43. package/src/cli/format.js +153 -0
  44. package/src/cli/index.js +365 -0
  45. package/src/cli/registry.js +18 -0
  46. package/src/core/blocks.js +147 -0
  47. package/src/core/diagram.js +282 -0
  48. package/src/core/errors.js +156 -0
  49. package/src/core/example.js +109 -0
  50. package/src/core/ir.js +62 -0
  51. package/src/core/lint-gates.js +427 -0
  52. package/src/core/lint.js +281 -0
  53. package/src/core/scan.js +84 -0
  54. package/src/core/schema.js +88 -0
  55. package/src/core/spec-check.js +33 -0
  56. package/src/core/validate.js +44 -0
  57. package/src/core/version.js +18 -0
  58. package/src/core/vocabulary.js +140 -0
  59. package/src/delivery/atomic.js +71 -0
  60. package/src/delivery/comments.js +180 -0
  61. package/src/delivery/home.js +70 -0
  62. package/src/delivery/open.js +31 -0
  63. package/src/delivery/project.js +141 -0
  64. package/src/delivery/read.js +109 -0
  65. package/src/delivery/run.js +95 -0
  66. package/src/delivery/scaffold-blueprints.js +728 -0
  67. package/src/delivery/store.js +219 -0
  68. package/src/delivery/template-extensions.js +183 -0
  69. package/src/delivery/template-format.js +112 -0
  70. package/src/delivery/template-package.js +376 -0
  71. package/src/delivery/template-promote.js +240 -0
  72. package/src/delivery/template-scaffold.js +181 -0
  73. package/src/delivery/templates.js +192 -0
  74. package/src/delivery/toml.js +195 -0
  75. package/src/delivery/write.js +35 -0
  76. package/src/export/browser.js +130 -0
  77. package/src/export/index.js +96 -0
  78. package/src/export/pdf.js +25 -0
  79. package/src/export/png.js +40 -0
  80. package/src/export/pptx.js +48 -0
  81. package/src/export/slides.js +33 -0
  82. package/src/export/snapshot.js +33 -0
  83. package/src/layouts/article.js +103 -0
  84. package/src/layouts/canvas.js +144 -0
  85. package/src/layouts/card.js +128 -0
  86. package/src/layouts/deck.js +88 -0
  87. package/src/layouts/index.js +90 -0
  88. package/src/layouts/one-page.js +161 -0
  89. package/src/layouts/registry.js +251 -0
  90. package/src/layouts/resume.js +172 -0
  91. package/src/layouts/template-index.js +78 -0
  92. package/src/parser/artifact.js +38 -0
  93. package/src/parser/container.js +103 -0
  94. package/src/parser/index.js +223 -0
  95. package/src/parser/tokens.js +265 -0
  96. package/src/render/board-filter.client.js +80 -0
  97. package/src/render/compile.js +29 -0
  98. package/src/render/context.js +98 -0
  99. package/src/render/element.js +32 -0
  100. package/src/render/fonts.js +129 -0
  101. package/src/render/graph-hover.client.js +148 -0
  102. package/src/render/html.js +52 -0
  103. package/src/render/index.js +241 -0
  104. package/src/render/measure.js +60 -0
  105. package/src/render/placement.js +136 -0
  106. package/src/render/playback.client.js +74 -0
  107. package/src/render/scale-to-fit.client.js +136 -0
  108. package/src/render/scale.js +41 -0
  109. package/src/render/skeleton.js +131 -0
  110. package/src/render/ssr.js +24 -0
  111. package/src/render/styles.js +56 -0
  112. package/src/render/templates.js +191 -0
  113. package/src/serve/daemon.js +117 -0
  114. package/src/serve/overlay.js +213 -0
  115. package/src/serve/protocol.js +36 -0
  116. package/src/serve/server.js +264 -0
  117. package/templates/kami/cards/components.js +40 -0
  118. package/templates/kami/cards/index.js +25 -0
  119. package/templates/kami/cards/manifest.js +67 -0
  120. package/templates/kami/cards/styles.css +389 -0
  121. package/templates/kami/long-form/components.js +102 -0
  122. package/templates/kami/long-form/index.js +25 -0
  123. package/templates/kami/long-form/manifest.js +87 -0
  124. package/templates/kami/long-form/styles.css +481 -0
  125. package/templates/kami/one-page/components.js +48 -0
  126. package/templates/kami/one-page/index.js +27 -0
  127. package/templates/kami/one-page/manifest.js +65 -0
  128. package/templates/kami/one-page/styles.css +375 -0
  129. package/templates/kami/resume/components.js +51 -0
  130. package/templates/kami/resume/index.js +27 -0
  131. package/templates/kami/resume/manifest.js +65 -0
  132. package/templates/kami/resume/styles.css +424 -0
  133. package/templates/kami/slides/components.js +41 -0
  134. package/templates/kami/slides/index.js +26 -0
  135. package/templates/kami/slides/manifest.js +64 -0
  136. package/templates/kami/slides/styles.css +406 -0
@@ -0,0 +1,128 @@
1
+ import { el } from '../blocks/element.js'
2
+ import { CANVAS_FLOWING } from './registry.js'
3
+
4
+ /**
5
+ * `card` — the flowing 文體 whose unit of reading is a **card**, not a column.
6
+ *
7
+ * A long-form page answers 「從頭讀到尾」; this one answers 「掃過去,挑一張看」.
8
+ * That is the whole difference, and it is a difference of *cut* rather than of
9
+ * content: the same document tree that `article` draws as one scrolling column
10
+ * is drawn here as a board of cards laid on a responsive grid — the home of a
11
+ * dashboard, a set of knowledge cards, a status board (CONTRACT F2c P1).
12
+ *
13
+ * The canvas stays `flowing`. A board of cards has no fixed logical size: it
14
+ * reflows to whatever window it lands in, and asking it to scale-to-fit would
15
+ * be asking a dashboard to be a slide. `maxWidth` is therefore how wide the
16
+ * board may get and `measure` is still the width of a line of text (CONTRACT
17
+ * C3) — two numbers, and on this 文體 they are pulled far apart, because a
18
+ * three-across board is nothing like a column of prose.
19
+ *
20
+ * ## The cut: top-level sections become cards, everything else leads in
21
+ *
22
+ * The document tree the parser produces nests every heading under its parent,
23
+ * so a document's **top-level `section` blocks are its chapters**. Each one
24
+ * becomes a card; whatever else sits at the top level (an opening paragraph, a
25
+ * callout, a table of contents someone wrote by hand) is the board's lead-in.
26
+ *
27
+ * CONTRACT F2c P1 words this as 「level-1 section 各成一卡」, and in an ordinary
28
+ * document — one that starts at `#` — the two sentences pick out exactly the
29
+ * same blocks. They part company only for a document that starts at `##`, and
30
+ * there the level test is the worse of the two: it would silently yield a board
31
+ * with no cards at all, because a heading-depth detail the author never thought
32
+ * about decided the shape of the page. Nothing is lost either way (every block
33
+ * is drawn), so the rule that does not depend on how deep the author started is
34
+ * the one this module takes (CONTRACT R10, executor's reason).
35
+ *
36
+ * ## Why there is no knob here
37
+ *
38
+ * `article` declares knobs because a rail is geometry the *markup* has to know
39
+ * about: the region only exists when the knob is on. A card board has no such
40
+ * switch — how many of the 24 columns a card spans, how wide the gutters are,
41
+ * where the grid breaks to one column: every one of those is read by the
42
+ * stylesheet and by nothing else, which makes them decoration, and decoration
43
+ * lives in the skin (`templates/kami/cards/styles.css`), never in a 文體.
44
+ * Declaring them here would put numbers in the layout that no other layer could
45
+ * ever ask about — the exact shape 「layout 只擁有切法」 rules out.
46
+ *
47
+ * It equally does **not** declare `gridAddressed`. The 24 tracks belong to the
48
+ * card grid, whose children are cards and only cards; a bare `col`/`colSpan` on
49
+ * a top-level block would land in the lead-in region, where there are no tracks
50
+ * to resolve against. Claiming the page is grid-addressed would licence exactly
51
+ * that coordinate and let CSS auto-placement drop it in silence.
52
+ */
53
+
54
+ /** Verbatim — the block type one card is made of. */
55
+ const CARD_BLOCK = 'section'
56
+
57
+ /** Verbatim — the three regions of a board, as class names. */
58
+ const BOARD_CLASS = 'cards'
59
+ const LEAD_IN_CLASS = 'cards-intro'
60
+ const GRID_CLASS = 'card-grid'
61
+ const CARD_CLASS = 'card'
62
+
63
+ const isCard = (block) => block?.type === CARD_BLOCK
64
+
65
+ const region = (className, nodes) =>
66
+ nodes.length > 0 ? [el('div', { class: className }, nodes)] : []
67
+
68
+ export default Object.freeze({
69
+ name: 'card',
70
+
71
+ canvas: CANVAS_FLOWING,
72
+
73
+ /** No required root form: a board draws whatever sits under `doc` (as `article`). */
74
+ rootForm: null,
75
+
76
+ /** The built-in generic template of this 文體 — `render` never wants for one. */
77
+ defaultTemplate: 'kami/cards',
78
+
79
+ slots: Object.freeze(['cards-lead', 'cards-foot']),
80
+
81
+ styleHooks: Object.freeze([BOARD_CLASS, LEAD_IN_CLASS, GRID_CLASS, CARD_CLASS]),
82
+
83
+ /**
84
+ * Factory geometry, in CSS pixels. Geometry only — a content-shaped default
85
+ * would make this layout mean something, and a layout that means something can
86
+ * only carry the documents it already imagined.
87
+ *
88
+ * 1200 is a board wide enough for three cards side by side without pretending
89
+ * to be a full-bleed application shell; 740 is the width of a line of text,
90
+ * unchanged from every other 文體 (CONTRACT C3: the page and the column are
91
+ * two numbers).
92
+ */
93
+ defaults: Object.freeze({ maxWidth: 1200, measure: 740 }),
94
+
95
+ /**
96
+ * A region is drawn only when something goes in it.
97
+ *
98
+ * The opposite reading of `article`'s rail, and deliberately so: the rail is
99
+ * *applied state* (a knob the template turned), so it stays in the markup even
100
+ * when empty, or the same package would be a different page for each document.
101
+ * These two regions have no knob and never could — whether a document has a
102
+ * lead-in is a fact about the document. Drawing an empty one would put a box
103
+ * in every artifact to say nothing, and a `null` child would put a real empty
104
+ * comment node there instead, which is worse: it is invisible and permanent.
105
+ */
106
+ root: (doc, ctx) => {
107
+ const children = doc?.children ?? []
108
+ const cards = children.filter(isCard)
109
+ const leadIn = children.filter((block) => !isCard(block))
110
+ return el(
111
+ 'article',
112
+ { class: BOARD_CLASS, 'data-template': ctx.templateKey },
113
+ [
114
+ ...ctx.chrome('cards-lead', ctx.meta),
115
+ ...region(LEAD_IN_CLASS, ctx.renderChildren(leadIn)),
116
+ ...region(
117
+ GRID_CLASS,
118
+ cards.map((block, index) =>
119
+ el('div', { class: CARD_CLASS }, [
120
+ ctx.renderBlock(block, { index, total: cards.length }),
121
+ ]),
122
+ ),
123
+ ),
124
+ ...ctx.chrome('cards-foot', ctx.meta),
125
+ ],
126
+ )
127
+ },
128
+ })
@@ -0,0 +1,88 @@
1
+ import { CANVAS_BOUNDED } from './registry.js'
2
+
3
+ /** Verbatim — the root form that makes an artifact a deck. */
4
+ const DECK_ROOT = 'deck'
5
+
6
+ /**
7
+ * Verbatim — the deck's fixed logical canvas, in CSS pixels (CONTRACT F2f P3).
8
+ *
9
+ * 1280×720 rather than any other 16:9 pair because it is the size every slide
10
+ * tool, every projector mode and every screen-capture default already speaks,
11
+ * and because a *logical* canvas only has to be the one everybody means: the
12
+ * artifact is scaled to whatever the window is (`src/render/scale.js`), so the
13
+ * number's job is to be shared, not to be large.
14
+ */
15
+ const DECK_CANVAS = Object.freeze({ width: 1280, height: 720 })
16
+
17
+ /** The deck sits directly under `doc` (parser/index.js buildDeck). */
18
+ const deckOf = (doc) => (doc?.children ?? []).find((block) => block?.type === DECK_ROOT)
19
+
20
+ /**
21
+ * `deck` — the slide 文體.
22
+ *
23
+ * Its root form is a single `deck` block, and that requirement is the layout's,
24
+ * not any one template's: handing a flat document tree to a deck template used
25
+ * to type-check completely and then draw nothing at all. Stating it here is
26
+ * what turns 「replay 換皮不換文體」 from a rule into a structure — every deck
27
+ * template inherits the refusal, and none of them can decline it.
28
+ *
29
+ * ## The canvas is `bounded`, and the canvas is one slide
30
+ *
31
+ * F2a built the mechanism and deliberately did not connect it: declaring
32
+ * `bounded` moves every slide artifact's bytes, and that slice was only allowed
33
+ * to flip the structure (CONTRACT F2a P4「只建不接」). F2f is the slice that may
34
+ * move them, so the declaration is now true — and with it v1's 「fullscreen
35
+ * 只多黑框」 finally ends: going fullscreen used to enlarge the page and leave
36
+ * the artifact exactly as small as before, because nothing was ever scaled.
37
+ *
38
+ * The fixed logical canvas is **one slide**, not the whole deck. A deck has N
39
+ * pages; a page is the thing with a fixed size, and 1280×720 is what a page of
40
+ * a deck is. Putting the canvas on the deck instead would have made the
41
+ * artifact a single 720px-tall box containing every slide — which reads as
42
+ * one slide and hides the rest, and would take the no-JS fallback (a plain
43
+ * scrolling document, `playback.client.js` の前提) with it.
44
+ *
45
+ * ## Why it also declares `gridAddressed`
46
+ *
47
+ * Because it now lays the tracks. `gridAddressed` is a claim the placement gate
48
+ * takes at face value (`src/render/placement.js`), so a layout that declared it
49
+ * without building the 24 columns would license coordinates that CSS
50
+ * auto-placement then drops in silence — the exact hole F2a's gate closes. The
51
+ * tracks are laid on the slide's content region by `src/blocks/slide.js`, and
52
+ * they are laid **because this layout says so**: the block asks the resolved
53
+ * canvas rather than assuming, so the same module still draws a flowing
54
+ * deck-rooted layout the plain way.
55
+ */
56
+ export default Object.freeze({
57
+ name: 'deck',
58
+
59
+ canvas: CANVAS_BOUNDED,
60
+
61
+ rootForm: DECK_ROOT,
62
+
63
+ defaultTemplate: 'kami/slides',
64
+
65
+ slots: Object.freeze(['slide-lead', 'deck-chrome']),
66
+
67
+ styleHooks: Object.freeze(['deck', 'slide', 'slide-inner', 'slide-cell']),
68
+
69
+ /**
70
+ * The whole page is addressable by 24-grid coordinates — see above, and note
71
+ * that `bounded` deliberately does **not** imply this (F2a 複驗補丁).
72
+ */
73
+ gridAddressed: true,
74
+
75
+ /**
76
+ * Factory geometry, in CSS pixels. Three numbers, three different jobs, and
77
+ * the reason they are three:
78
+ *
79
+ * - `canvasSize` 1280×720 — the logical page that gets scaled to fit.
80
+ * - `maxWidth` 1120 — how wide the content region inside that page may grow.
81
+ * A slide with type running the full 1280 has no margin at all.
82
+ * - `measure` 740 — the width a *line of text* wants, unchanged from every
83
+ * other 文體 (CONTRACT C3: the page and the column are two numbers).
84
+ */
85
+ defaults: Object.freeze({ maxWidth: 1120, measure: 740, canvasSize: DECK_CANVAS }),
86
+
87
+ root: (doc, ctx) => ctx.renderBlock(deckOf(doc)),
88
+ })
@@ -0,0 +1,90 @@
1
+ import { registerLayout, sealCoreLayouts, layoutModule, layoutNames } from './registry.js'
2
+
3
+ import article from './article.js'
4
+ import card from './card.js'
5
+ import deck from './deck.js'
6
+ import onePage from './one-page.js'
7
+ import resume from './resume.js'
8
+
9
+ /**
10
+ * The factory 文體 set, in canonical order.
11
+ *
12
+ * **All five** of the v1 layouts (issue 13 裁決 1) exist as of F2e. `article` and
13
+ * `deck` were here from F2a because they are what the two shipped templates
14
+ * already were; `card` is the first one this SDK *added* rather than lifted, and
15
+ * adding it was the measurement F2c existed to take — a new 文體 should cost one
16
+ * layout module, one built-in template, one scaffold blueprint and this line,
17
+ * with no edit to the layout interface itself (CONTRACT F2c P2).
18
+ *
19
+ * `one-page` (F2d) takes that measurement again on the harder half: it is the
20
+ * first **bounded** 文體 added through the public shape rather than lifted from
21
+ * an existing template, so it is the first to exercise `defaults.canvasSize`,
22
+ * the registry's bounded-geometry refusal and the scale-to-fit skeleton from
23
+ * the outside. It cost the same one line here.
24
+ *
25
+ * `resume` (F2e) closes the set, and it is the one that asks the interface a
26
+ * question none of the other four could: its artifact is *printed*, so its
27
+ * canvas is a sheet of A4 rather than a shape a window has. It too cost one
28
+ * line here — the paper lives in the layout module, not in the SDK.
29
+ *
30
+ * A sixth does not belong here at all: the v1 list is five, and a third-party
31
+ * layout calls `registerLayout` from outside and needs no edit to any file
32
+ * inside `src/`.
33
+ */
34
+ const CORE_LAYOUTS = Object.freeze([article, card, deck, onePage, resume])
35
+
36
+ for (const mod of CORE_LAYOUTS) registerLayout(mod)
37
+ sealCoreLayouts()
38
+
39
+ /**
40
+ * The 文體 an artifact is drawn in, resolved from the template's manifest.
41
+ *
42
+ * A manifest that names no layout falls back to the first factory layout, which
43
+ * keeps every pre-F2a manifest (and every third-party one written against it)
44
+ * loadable: the flowing article is what a template *was* before layouts had
45
+ * names.
46
+ */
47
+ export const DEFAULT_LAYOUT = article.name
48
+
49
+ export function resolveLayout(manifest) {
50
+ return layoutModule(manifest?.layout ?? DEFAULT_LAYOUT)
51
+ }
52
+
53
+ /** The built-in generic template of a layout — the tail of the resolution chain. */
54
+ export const defaultTemplateOf = (name = DEFAULT_LAYOUT) =>
55
+ layoutModule(name)?.defaultTemplate
56
+
57
+ export {
58
+ CANVAS_BOUNDED,
59
+ CANVAS_FLOWING,
60
+ CANVAS_MODES,
61
+ GRID_ADDRESSED_FIELD,
62
+ LAYOUT_DEFAULT_KEYS,
63
+ LAYOUT_KNOBS_FIELD,
64
+ LAYOUT_MODULE_FIELDS,
65
+ PLUGIN_LAYOUT_PREFIX,
66
+ layoutModule,
67
+ layoutModules,
68
+ layoutNames,
69
+ layoutRegistryVersion,
70
+ pluginLayoutNames,
71
+ registerLayout,
72
+ resetLayoutRegistry,
73
+ } from './registry.js'
74
+
75
+ export {
76
+ CANVAS_ATTR,
77
+ CANVAS_INVALID_CODE,
78
+ canvasAttrs,
79
+ canvasProblems,
80
+ resolveCanvas,
81
+ } from './canvas.js'
82
+
83
+ export {
84
+ registerTemplateLayoutSource,
85
+ resetTemplateLayoutSources,
86
+ templateLayoutEntries,
87
+ } from './template-index.js'
88
+
89
+ /** Exported for the layer that has to name every 文體 in a message. */
90
+ export const knownLayouts = () => layoutNames()
@@ -0,0 +1,161 @@
1
+ import { el } from '../blocks/element.js'
2
+ import { CANVAS_BOUNDED } from './registry.js'
3
+
4
+ /**
5
+ * `one-page` — the 文體 of a general one-page static website.
6
+ *
7
+ * A long-form page answers 「從頭讀到尾」, a board of cards answers 「掃過去,挑
8
+ * 一張看」; this one answers 「一直往下捲,一屏換一屏」. It is the shape a landing
9
+ * page, a product page or a service page already has: a few full-bleed screens,
10
+ * each holding one idea, read by scrolling past them in order (13 號票 v1 五版面
11
+ * 之四).
12
+ *
13
+ * ## The cut: one top-level section is one screen
14
+ *
15
+ * The parser nests every heading under its parent, so a document's **top-level
16
+ * `section` blocks are its chapters** — and on this 文體 a chapter is a screen.
17
+ * Whatever else sits at the top level (an opening paragraph, a callout, a lead
18
+ * sentence someone wrote before the first heading) joins the template's
19
+ * `page-lead` chrome to become the **opening screen**, which is what a hero is.
20
+ *
21
+ * The test is 「是不是頂層 section」 and deliberately **not** 「是不是 level 1」,
22
+ * the same R10 reasoning `card` records: an ordinary document (one that starts
23
+ * at `#`) makes the two sentences pick out identical blocks, and they part
24
+ * company only for a document that starts at `##` — where the level test would
25
+ * silently yield a site with no screens at all, because a heading-depth detail
26
+ * the author never thought about decided the shape of the page.
27
+ *
28
+ * ## The canvas is bounded, and **one screen is one canvas** (CONTRACT F2d R10)
29
+ *
30
+ * A bounded 文體 has a fixed logical canvas that is scaled to fit (issue 14
31
+ * 裁決 4), and the contract left *which* canvas to the executor. There were two
32
+ * candidates and the measurement settles it. Making the whole scrolling page a
33
+ * single tall canvas puts a page-height divided by a window-height into
34
+ * `Math.min(vw / w, vh / h)` — so the whole site is shrunk to a postage stamp.
35
+ * Measured in a real browser on `fixtures/one-page-sample.md` (6 screens,
36
+ * natural height 2398px) with the real `scale-to-fit.client.js` running:
37
+ *
38
+ * viewport 1440x900 whole page 0.375x -> 18px body text lands at 6.8px
39
+ * per screen 1.000x -> 18px body text lands at 18.0px
40
+ * viewport 1280x720 whole page 0.300x -> 5.4px per screen 0.800x -> 14.4px
41
+ * viewport 1920x1080 whole page 0.450x -> 8.1px per screen 1.200x -> 21.6px
42
+ *
43
+ * and the whole-page half gets *worse the longer the page is* — which is the
44
+ * defining property of a one-page site. So the canvas is a screen, exactly as a
45
+ * `deck`'s canvas is a slide and not the deck: the artifact is a scrollable
46
+ * sequence of bounded screens, which is also what keeps the no-JS form honest
47
+ * (nothing is scaled, everything is still there, in order).
48
+ *
49
+ * ## Why there is no knob here
50
+ *
51
+ * How a screen is padded, how the opening screen arranges itself, how large the
52
+ * type is: every one of those is read by the stylesheet and by nothing else,
53
+ * which makes them decoration, and decoration lives in the skin
54
+ * (`templates/kami/one-page/styles.css`). The logical canvas is not a knob — it
55
+ * is `defaults.canvasSize`, the one extra field every bounded 文體 has, and the
56
+ * registry requires it at registration time.
57
+ *
58
+ * ## Why it does **not** declare `gridAddressed`
59
+ *
60
+ * `deck` may declare it because a slide's children are a flat run of blocks:
61
+ * the 24 tracks are laid on that content region and a bare `col: 17` resolves
62
+ * to a real cell. A screen is not that shape. A screen's child is exactly **one
63
+ * `section`**, and a `#` heading swallows everything beneath it — which is why
64
+ * even the deck scaffold's own 預設 md warns that a titled slide places its
65
+ * content relative to the section rather than to the page. Tracks laid here
66
+ * would have a single child, and any coordinate an author wrote *inside* the
67
+ * section would not be a direct child of the region at all: CSS auto-placement
68
+ * would drop it in silence, and the placement gate — which takes the
69
+ * declaration at face value (`src/render/placement.js`) — would have stopped
70
+ * refusing it. The claim and the tracks must land together, so neither lands.
71
+ */
72
+
73
+ /** Verbatim — the block type one screen is made of. */
74
+ const SCREEN_BLOCK = 'section'
75
+
76
+ /** Verbatim — the two regions of a one-page site, as class names. */
77
+ const PAGE_CLASS = 'one-page'
78
+ const SCREEN_CLASS = 'screen'
79
+ const SCREEN_INNER_CLASS = 'screen-inner'
80
+
81
+ /**
82
+ * Verbatim — the fixed logical canvas of **one screen**, in CSS pixels.
83
+ *
84
+ * 1440×900 rather than the deck's 1280×720 because a screen of a website is not
85
+ * a slide: 1440 is the width every design tool's desktop artboard already is,
86
+ * and 16:10 rather than 16:9 leaves a screen the extra height a page of a site
87
+ * uses for a heading plus a paragraph plus a call to action. Like every logical
88
+ * canvas the number's job is to be shared, not to be large — the artifact is
89
+ * scaled to whatever window it lands in (`src/render/scale.js`).
90
+ */
91
+ const SCREEN_CANVAS = Object.freeze({ width: 1440, height: 900 })
92
+
93
+ const isScreen = (block) => block?.type === SCREEN_BLOCK
94
+
95
+ export default Object.freeze({
96
+ name: 'one-page',
97
+
98
+ canvas: CANVAS_BOUNDED,
99
+
100
+ /** No required root form: a site draws whatever sits under `doc` (as `article`). */
101
+ rootForm: null,
102
+
103
+ /** The built-in generic template of this 文體 — `render` never wants for one. */
104
+ defaultTemplate: 'kami/one-page',
105
+
106
+ slots: Object.freeze(['page-lead', 'page-foot']),
107
+
108
+ styleHooks: Object.freeze([PAGE_CLASS, SCREEN_CLASS, SCREEN_INNER_CLASS]),
109
+
110
+ /**
111
+ * Factory geometry, in CSS pixels. Three numbers, three different jobs:
112
+ *
113
+ * - `canvasSize` 1440×900 — the logical screen that gets scaled to fit.
114
+ * - `maxWidth` 1200 — how wide the content region inside a screen may grow.
115
+ * A screen with type running the full 1440 has no margin at all.
116
+ * - `measure` 740 — the width a *line of text* wants, unchanged from every
117
+ * other 文體 (CONTRACT C3: the page and the column are two numbers).
118
+ */
119
+ defaults: Object.freeze({ maxWidth: 1200, measure: 740, canvasSize: SCREEN_CANVAS }),
120
+
121
+ /**
122
+ * A screen is drawn only when something goes in it.
123
+ *
124
+ * The opening screen exists when the document has top-level content that is
125
+ * not a chapter, or when the template filled `page-lead` — and not otherwise.
126
+ * The same reading as `card`'s two regions, and for the same reason: whether
127
+ * a site has an opening is a fact about the document, not applied state a
128
+ * template turned, so drawing an empty one would put a blank screen in front
129
+ * of every artifact that has no lead-in. `...(x ? [n] : [])` rather than
130
+ * `x ? n : null`, because a `null` child is a real, invisible, permanent empty
131
+ * comment node in the artifact.
132
+ *
133
+ * The canvas attributes come from `ctx.canvas`, never from the constant above:
134
+ * a template may not move the logical canvas, but the resolved canvas is the
135
+ * one place that answer is computed, and a second computation here is a scale
136
+ * factor free to disagree with the one the skeleton uses.
137
+ */
138
+ root: (doc, ctx) => {
139
+ const children = doc?.children ?? []
140
+ const screens = children.filter(isScreen)
141
+ const opening = [
142
+ ...ctx.chrome('page-lead', ctx.meta),
143
+ ...ctx.renderChildren(children.filter((block) => !isScreen(block))),
144
+ ]
145
+ const screen = (nodes) =>
146
+ el('section', { class: SCREEN_CLASS, ...(ctx.canvas?.canvasAttrs ?? {}) }, [
147
+ el('div', { class: SCREEN_INNER_CLASS }, nodes),
148
+ ])
149
+ return el(
150
+ 'article',
151
+ { class: PAGE_CLASS, 'data-template': ctx.templateKey },
152
+ [
153
+ ...(opening.length > 0 ? [screen(opening)] : []),
154
+ ...screens.map((block, index) =>
155
+ screen([ctx.renderBlock(block, { index, total: screens.length })]),
156
+ ),
157
+ ...ctx.chrome('page-foot', ctx.meta),
158
+ ],
159
+ )
160
+ },
161
+ })