@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,728 @@
1
+ import { NO_ROOT } from './template-format.js'
2
+
3
+ /**
4
+ * What the scaffold has to *say* to produce a working package, per 文體.
5
+ *
6
+ * `template-scaffold.js` owns the mechanism — validate, refuse an occupied
7
+ * directory, write three files atomically — and none of that differs between
8
+ * 文體. What differs is entirely content: which blocks the package admits, what
9
+ * root form it restates, how its skin paints the hooks that 文體 emits, and what
10
+ * its 預設 md looks like. Keeping the content here is what lets a new 文體 be
11
+ * supported by writing a blueprint rather than by editing the writer, and it is
12
+ * why `SCAFFOLD_SUPPORTED_LAYOUTS` is *derived* from this table: a 文體 the flag
13
+ * lets through but has no blueprint for would scaffold a package that cannot
14
+ * draw, which is the failure F3's 複驗 F-1 narrowed the flag to prevent.
15
+ *
16
+ * Every stylesheet here is deliberately small and deliberately **complete**:
17
+ * every hook the admitted blocks emit gets at least a rule, because a block
18
+ * nobody painted renders as invisible data loss with exit 0 (issue 13「這類系統
19
+ * 最惡毒的失敗模式」). The `ksb:` comment is the geometry marker
20
+ * `src/render/styles.js` fills from the manifest — the scaffold teaches the
21
+ * mechanism by using it.
22
+ */
23
+
24
+ /** The applied state the article scaffold turns its 文體's knobs to, in CSS pixels. */
25
+ const ARTICLE_MAX_WIDTH = 740
26
+ const ARTICLE_MEASURE = 740
27
+
28
+ /**
29
+ * The deck scaffold's applied state. The two numbers are *not* the logical
30
+ * canvas: the canvas (1280×720) belongs to the `deck` 文體 and arrives with it,
31
+ * while `maxWidth` is how wide the content region inside a slide may grow and
32
+ * `measure` is still the width of a line of text (CONTRACT C3).
33
+ */
34
+ const DECK_MAX_WIDTH = 1120
35
+ const DECK_MEASURE = 740
36
+
37
+ /**
38
+ * The card scaffold's applied state — the board and the column, at their
39
+ * furthest apart. `maxWidth` 1200 is how wide the board of cards may get;
40
+ * `measure` 740 is still the width of a line of text, and on this 文體 it is the
41
+ * lead-in paragraph rather than the cards that respects it.
42
+ */
43
+ const CARD_MAX_WIDTH = 1200
44
+ const CARD_MEASURE = 740
45
+
46
+ /**
47
+ * The one-page scaffold's applied state. Like the deck's, the two numbers are
48
+ * *not* the logical canvas: the canvas (1440×900, one screen) belongs to the
49
+ * `one-page` 文體 and arrives with it, while `maxWidth` is how wide the content
50
+ * region inside a screen may grow and `measure` is still the width of a line of
51
+ * text (CONTRACT C3).
52
+ */
53
+ const ONE_PAGE_MAX_WIDTH = 1200
54
+ const ONE_PAGE_MEASURE = 740
55
+
56
+ /**
57
+ * The resume scaffold's applied state. Again the two numbers are *not* the
58
+ * logical canvas — the canvas (794×1123, one sheet of A4) belongs to the
59
+ * `resume` 文體 and arrives with it. What is different here is that neither
60
+ * number is the usual one: on paper `measure` cannot be 740, because a 740px
61
+ * column plus any margin at all is wider than an A4 sheet.
62
+ */
63
+ const RESUME_MAX_WIDTH = 660
64
+ const RESUME_MEASURE = 620
65
+
66
+ /** The block types the article scaffold admits — the core long-form vocabulary. */
67
+ const ARTICLE_BLOCKS = Object.freeze([
68
+ 'doc',
69
+ 'section',
70
+ 'prose',
71
+ 'list',
72
+ 'quote',
73
+ 'callout',
74
+ 'code',
75
+ 'table',
76
+ 'raw',
77
+ 'diagram',
78
+ ])
79
+
80
+ /**
81
+ * The deck scaffold admits the same vocabulary **plus the two container types a
82
+ * deck is made of**. `deck`/`slide` are not optional extras here: the 文體
83
+ * requires a `deck` root form, so a package that did not admit them would be
84
+ * refused by the vocabulary guard on its own 預設 md.
85
+ */
86
+ const DECK_BLOCKS = Object.freeze(['doc', 'deck', 'slide', ...ARTICLE_BLOCKS.slice(1)])
87
+
88
+ /** Rules every 文體's skin needs, because every 文體 admits these blocks. */
89
+ const commonBlockRules = () => [
90
+ '.section-title { line-height: 1.3; }',
91
+ '.prose { margin: 0; }',
92
+ '.list { padding-left: 1.4rem; }',
93
+ '.quote { margin: 0; padding-left: 1rem; border-left: 3px solid var(--rule); }',
94
+ '.callout { padding: 1rem; border: 1px solid var(--rule); border-radius: 4px; }',
95
+ '.callout-label { font-size: 0.75rem; letter-spacing: 0.08em; }',
96
+ '.code { padding: 1rem; overflow-x: auto; background: #f2efe9; }',
97
+ '.table-wrap { overflow-x: auto; }',
98
+ '.table { border-collapse: collapse; width: 100%; }',
99
+ '.table th, .table td { border: 1px solid var(--rule); padding: 0.4rem 0.6rem; }',
100
+ '.island { margin: 0; }',
101
+ '.diagram { max-width: 100%; }',
102
+ ]
103
+
104
+ const articleStylesheet = () =>
105
+ [
106
+ ':root {',
107
+ ' /* ksb:measure --measure */',
108
+ ' --ink: #1a1a1a;',
109
+ ' --paper: #fdfdfb;',
110
+ ' --rule: #d8d4cc;',
111
+ '}',
112
+ '',
113
+ 'body { margin: 0; background: var(--paper); color: var(--ink);',
114
+ ' font-family: Georgia, "Noto Serif TC", serif; line-height: 1.75; }',
115
+ '.paper { max-width: var(--measure); margin: 0 auto; padding: 4rem 1.5rem; }',
116
+ '.doc-body > * + * { margin-top: 1.2rem; }',
117
+ ...commonBlockRules(),
118
+ '',
119
+ ].join('\n')
120
+
121
+ /**
122
+ * The deck skin.
123
+ *
124
+ * Three rules here are not decoration and would each break 開箱即用 if dropped:
125
+ *
126
+ * - `.deck.is-live` / `.is-current` — the playback asset only adds classes.
127
+ * A skin that never acts on them ships a deck whose arrow keys do nothing,
128
+ * which looks like a broken artifact rather than an unpainted one.
129
+ * - `.slide-cell { min-width: 0 }` — a grid cell defaults to `min-width: auto`,
130
+ * so one long unbroken word widens its track and silently narrows its
131
+ * neighbours.
132
+ * - `transform: none !important` under `@media print` — scale-to-fit is a
133
+ * screen affordance; on paper (and in `export`, which asks for the print
134
+ * form) the inline transform would shrink every slide into a corner.
135
+ */
136
+ const deckStylesheet = () =>
137
+ [
138
+ ':root {',
139
+ ' /* ksb:maxWidth --slide-measure */',
140
+ ' --ink: #1a1a1a;',
141
+ ' --paper: #14161c;',
142
+ ' --surface: #fdfdfb;',
143
+ ' --rule: #d8d4cc;',
144
+ '}',
145
+ '',
146
+ '* { box-sizing: border-box; }',
147
+ 'body { margin: 0; background: var(--paper); color: var(--ink);',
148
+ ' font-family: Georgia, "Noto Serif TC", serif; line-height: 1.6; }',
149
+ '',
150
+ '/* 每張投影片就是文體宣告的有界邏輯畫布;寬高隨標記出貨,縮放由骨架資產做。 */',
151
+ '.deck { margin: 0 auto; padding: 2rem 1.5rem; overflow-x: auto; }',
152
+ '.slide { margin: 0 auto 1.5rem; padding: 3rem 3.5rem; background: var(--surface);',
153
+ ' border-radius: 4px; overflow: hidden; display: flex; flex-direction: column;',
154
+ ' justify-content: center; }',
155
+ // 列距走**下**緣,不走上緣、也不走 row-gap。上緣會讓同一列的側欄比主欄低
156
+ // 一截;row-gap 更糟——格軌有 24 條顯式列,gap 連空列都算,於是每一張都被
157
+ // 撐掉半頁高。兩種錯都只是「看起來有點怪」,沒有人會去查。
158
+ '.slide-inner { width: 100%; max-width: var(--slide-measure); margin: 0 auto;',
159
+ ' overflow-y: auto; max-height: 100%; }',
160
+ '.slide-cell { min-width: 0; margin-bottom: 1.2rem; }',
161
+ '.slide-cell:last-child { margin-bottom: 0; }',
162
+ '',
163
+ '/* 播放模式:沒有 JS 時每張照常可讀,整份產物退化成一份可捲動的文件。 */',
164
+ '.deck.is-live { position: fixed; inset: 0; padding: 0; overflow: hidden; }',
165
+ '.deck.is-live .slide { display: none; margin: 0; }',
166
+ '.deck.is-live .slide.is-current { display: flex; }',
167
+ '',
168
+ ...commonBlockRules(),
169
+ '',
170
+ '@media print {',
171
+ ' body { background: #fff; }',
172
+ ' .deck.is-live { position: static; overflow: visible; padding: 2rem 1.5rem; }',
173
+ ' .deck.is-live .slide { display: flex; margin: 0 auto 1.5rem; }',
174
+ ' .slide { break-inside: avoid; transform: none !important; }',
175
+ '}',
176
+ '',
177
+ ].join('\n')
178
+
179
+ /**
180
+ * The card skin.
181
+ *
182
+ * Three rules here are load-bearing rather than decorative:
183
+ *
184
+ * - the 24 tracks plus `grid-column: span N` — this is the 文體's whole shape,
185
+ * and a scaffold that laid no tracks would produce a package whose cards
186
+ * stack in one column, i.e. an `article` wearing another name.
187
+ * - `.card { min-width: 0 }` — a grid item defaults to `min-width: auto`, so
188
+ * one long unbroken word widens its track and silently narrows the rest of
189
+ * the row.
190
+ * - the two breakpoints — 「寬視窗多欄、窄視窗單欄」 is part of what the 文體
191
+ * promises (CONTRACT F2c P1); without them the starter package is unreadable
192
+ * on the first phone that opens it.
193
+ */
194
+ const cardStylesheet = () =>
195
+ [
196
+ ':root {',
197
+ ' /* ksb:measure --measure */',
198
+ ' /* ksb:maxWidth --max-width */',
199
+ ' --card-columns: 24;',
200
+ ' --card-span: 8;',
201
+ ' --card-gap: 24px;',
202
+ ' --ink: #1a1a1a;',
203
+ ' --paper: #fdfdfb;',
204
+ ' --surface: #ffffff;',
205
+ ' --rule: #d8d4cc;',
206
+ '}',
207
+ '',
208
+ '* { box-sizing: border-box; }',
209
+ 'body { margin: 0; background: var(--paper); color: var(--ink);',
210
+ ' font-family: Georgia, "Noto Serif TC", serif; line-height: 1.6; }',
211
+ '',
212
+ '.cards { max-width: var(--max-width); margin: 0 auto; padding: 3rem 1.5rem; }',
213
+ '.cards-intro { max-width: var(--measure); margin: 0 0 2rem; }',
214
+ '.cards-intro > * + * { margin-top: 1rem; }',
215
+ '',
216
+ '/* The board: 24 real tracks, and a card spans some of them. */',
217
+ '.card-grid { display: grid;',
218
+ ' grid-template-columns: repeat(var(--card-columns), minmax(0, 1fr));',
219
+ ' column-gap: var(--card-gap); row-gap: var(--card-gap); align-items: start; }',
220
+ '.card { grid-column: span var(--card-span); min-width: 0;',
221
+ ' padding: 1.25rem 1.4rem; background: var(--surface);',
222
+ ' border: 1px solid var(--rule); border-radius: 6px; }',
223
+ '.card > .section { margin: 0; }',
224
+ '.card > .section > .section-title { margin-top: 0; }',
225
+ ...commonBlockRules(),
226
+ '',
227
+ '@media (max-width: 1024px) { .card { grid-column: span 12; } }',
228
+ '@media (max-width: 680px) { .card { grid-column: 1 / -1; } }',
229
+ '',
230
+ '@media print {',
231
+ ' body { background: #fff; }',
232
+ ' .card-grid { display: block; }',
233
+ ' .card { break-inside: avoid; margin: 0 0 var(--card-gap); }',
234
+ '}',
235
+ '',
236
+ ].join('\n')
237
+
238
+ /**
239
+ * The one-page skin.
240
+ *
241
+ * Three rules here are load-bearing rather than decorative:
242
+ *
243
+ * - `.screen { display: flex; justify-content: center }` plus `overflow:
244
+ * hidden` — a screen is a *fixed frame*, not a column that grows. Without
245
+ * them the content sits at the top of a 900px box and anything longer
246
+ * silently spills past the canvas the skeleton is scaling.
247
+ * - `.one-page { overflow-x: auto }` — the screen carries its logical width
248
+ * inline and is not allowed to reflow, so a narrower window has to scroll.
249
+ * Without this the starter package overflows the document instead.
250
+ * - `transform: none !important` under `@media print` — scale-to-fit is a
251
+ * screen affordance; on paper (and in `export`, which asks for the print
252
+ * form) the inline transform would shrink every screen into a corner.
253
+ */
254
+ const onePageStylesheet = () =>
255
+ [
256
+ ':root {',
257
+ ' /* ksb:measure --measure */',
258
+ ' /* ksb:maxWidth --max-width */',
259
+ ' --ink: #1a1a1a;',
260
+ ' --paper: #14161c;',
261
+ ' --surface: #fdfdfb;',
262
+ ' --rule: #d8d4cc;',
263
+ '}',
264
+ '',
265
+ '* { box-sizing: border-box; }',
266
+ 'body { margin: 0; background: var(--paper); color: var(--ink);',
267
+ ' font-family: Georgia, "Noto Serif TC", serif; line-height: 1.6; }',
268
+ '',
269
+ '/* The site is a scrollable sequence of screens; a screen is the bounded',
270
+ ' logical canvas, and its size arrives inline on the markup. */',
271
+ '.one-page { margin: 0 auto; overflow-x: auto; }',
272
+ '.screen { margin: 0 auto; padding: 4rem 5rem; background: var(--surface);',
273
+ ' overflow: hidden; display: flex; flex-direction: column;',
274
+ ' justify-content: center; }',
275
+ '.screen-inner { width: 100%; max-width: var(--max-width); margin: 0 auto;',
276
+ ' overflow-y: auto; max-height: 100%; }',
277
+ '.screen-inner > .section { margin: 0; }',
278
+ '.prose, .list, .quote, .callout { max-width: var(--measure); }',
279
+ '.colophon { padding: 1.5rem; text-align: center; font-size: 0.75rem;',
280
+ ' color: #8a8371; }',
281
+ ...commonBlockRules(),
282
+ '',
283
+ '@media print {',
284
+ ' body { background: #fff; }',
285
+ ' /* scale-to-fit is for screens; on paper the inline transform stands down. */',
286
+ ' .screen { transform: none !important; break-inside: avoid; background: #fff; }',
287
+ '}',
288
+ '',
289
+ ].join('\n')
290
+
291
+ /**
292
+ * The resume skin.
293
+ *
294
+ * Three rules here are load-bearing rather than decorative:
295
+ *
296
+ * - `@page { margin: 0 }` — this SDK's PDF export prints A4 with 14mm margins
297
+ * (`src/export/pdf.js`), so the printable box is smaller than one sheet. A
298
+ * package that never claimed the page box back would lose a strip off the
299
+ * right edge and another off the bottom of *every* printed page, silently.
300
+ * - `.sheet + .sheet { break-before: page }` — one sheet *opens* one printed
301
+ * page, alone. `break-inside: avoid` is deliberately absent: a sheet with
302
+ * more than a page of content has to be allowed to split, and a rule that
303
+ * says otherwise is a rule the browser ignores while the reader believes it.
304
+ * - the print form hands back the three screen devices (`display: block;
305
+ * overflow: visible; height: auto !important`) — on screen a sheet is a
306
+ * fixed frame whose inner region scrolls; on paper there is no scrollbar,
307
+ * so a frame that kept its fixed height would paint everything past the
308
+ * first page nowhere and report it to nobody. `overflow: hidden` is a
309
+ * screen-only affordance.
310
+ */
311
+ const resumeStylesheet = () =>
312
+ [
313
+ ':root {',
314
+ ' /* ksb:measure --measure */',
315
+ ' /* ksb:maxWidth --max-width */',
316
+ ' --ink: #1a1a1a;',
317
+ ' --backdrop: #2b2f3a;',
318
+ ' --surface: #ffffff;',
319
+ ' --rule: #d8d4cc;',
320
+ '}',
321
+ '',
322
+ '* { box-sizing: border-box; }',
323
+ 'body { margin: 0; background: var(--backdrop); color: var(--ink);',
324
+ ' font-family: Georgia, "Noto Serif TC", serif; font-size: 15px; line-height: 1.55; }',
325
+ '',
326
+ '/* The page box belongs to this genre, not to the printer defaults: the',
327
+ ' exporter prints A4 with margins, and a sheet drawn at its natural size',
328
+ ' would be clipped by them. */',
329
+ '@page {',
330
+ ' size: A4;',
331
+ ' margin: 0;',
332
+ '}',
333
+ '',
334
+ '/* A resume is a stack of sheets; a sheet is the bounded logical canvas,',
335
+ ' and its size arrives inline on the markup. */',
336
+ '.resume { margin: 0 auto; overflow-x: auto; }',
337
+ '/* No horizontal margin on the sheet: the skeleton already translates every',
338
+ ' canvas by its own offsetX, so centring it here as well centres it twice',
339
+ ' and the sheet drifts off to one side. */',
340
+ '.sheet { padding: 72px 67px; background: var(--surface);',
341
+ ' overflow: hidden; display: flex; flex-direction: column;',
342
+ ' box-shadow: 0 2px 18px rgba(0, 0, 0, 0.35); }',
343
+ '.sheet-inner { width: 100%; max-width: var(--max-width); margin: 0 auto;',
344
+ ' overflow-y: auto; max-height: 100%; }',
345
+ '.sheet-inner > .section { margin: 0; }',
346
+ '.prose, .list, .quote, .callout { max-width: var(--measure); }',
347
+ '.colophon { margin-top: auto; padding-top: 1rem; text-align: right;',
348
+ ' font-size: 0.7rem; color: #8a8371; }',
349
+ ...commonBlockRules(),
350
+ '',
351
+ '@media print {',
352
+ ' body { background: #fff; }',
353
+ ' /* scale-to-fit is for screens; on paper the inline transform stands down.',
354
+ ' The fixed height, the clip and the inner scroll region stand down with',
355
+ ' it: on screen they make a sheet a fixed frame, on paper -- where there',
356
+ ' is no scrollbar -- they mean anything past the first page is printed',
357
+ ' nowhere and reported by nothing. `height: auto` is the load-bearing one',
358
+ ' and needs !important, because the height arrives inline. */',
359
+ ' .sheet { transform: none !important; box-shadow: none; display: block;',
360
+ ' overflow: visible; height: auto !important; }',
361
+ ' .sheet-inner { overflow: visible; max-height: none; }',
362
+ ' /* One sheet starts one printed page, and runs on when it holds more. */',
363
+ ' .sheet + .sheet { break-before: page; }',
364
+ '}',
365
+ '',
366
+ ].join('\n')
367
+
368
+ /**
369
+ * The 預設 md — 「起手內容文件,render 即得該模板預設樣貌」 (issue 14 補充裁定).
370
+ *
371
+ * The frontmatter names the package itself, so the file renders with the
372
+ * template it belongs to even without `-t`. That is the property D4 turns into
373
+ * a test: the scaffold is only 開箱即 render if this exact file, unedited, comes
374
+ * out the other side as a clean artifact.
375
+ */
376
+ const articleDocument = ({ namespace, name }) =>
377
+ [
378
+ '---',
379
+ `template: ${namespace}/${name}`,
380
+ `title: ${namespace}/${name}`,
381
+ 'kicker: kamishibai 模板包骨架',
382
+ '---',
383
+ '',
384
+ '# 這是預設 md',
385
+ '',
386
+ `這份文件跟著模板包 \`${namespace}/${name}\` 一起被建立。它的用途是「秀出這個模板`,
387
+ '預設長什麼樣」——改它的內容,或改隔壁的 `stylesheet.css`,兩者都不需要動 SDK。',
388
+ '',
389
+ ':::note',
390
+ 'manifest.toml 管宣告與套用態(所騎的 layout、准入的 block、measure 與 maxWidth);',
391
+ 'stylesheet.css 管皮;這份 md 管預設樣貌。三者分工,缺一不可。',
392
+ ':::',
393
+ '',
394
+ '## 想再往前一步',
395
+ '',
396
+ '- 純資料就夠用:多數模板只要改 `stylesheet.css` 與這份 md。',
397
+ '- 要換掉某個型別的轉換:放 `rotors/<型別>/override.js`。',
398
+ '- 要加新的 block 型別:放 `plugins/x-<名字>.js`,並把 `x-*` 寫進 manifest 的 blocks。',
399
+ '',
400
+ ].join('\n')
401
+
402
+ /**
403
+ * The deck 預設 md. Three slides, because the thing a deck scaffold has to
404
+ * demonstrate is the one thing a one-slide file cannot: that `---` is a page
405
+ * break under this package's own key, not decoration. `:::place` appears on the
406
+ * third slide for the same reason — the 文體 declares the whole page addressable
407
+ * by 24-grid coordinates, and a starter file that never uses them leaves the
408
+ * reader to discover the feature from a specification.
409
+ *
410
+ * Note that the `:::place` fences stand on their own, with no `::::grid` around
411
+ * them. That is the visible consequence of the 文體's `gridAddressed` claim: on
412
+ * a deck the *page* lays the tracks, so a coordinate resolves without anyone
413
+ * wrapping it — and on a flowing 文體 the same three lines are refused loudly
414
+ * rather than silently dropped (`src/render/placement.js`).
415
+ */
416
+ const deckDocument = ({ namespace, name }) =>
417
+ [
418
+ '---',
419
+ `template: ${namespace}/${name}`,
420
+ `title: ${namespace}/${name}`,
421
+ 'kicker: kamishibai 簡報骨架',
422
+ '---',
423
+ '',
424
+ '# 這是預設簡報',
425
+ '',
426
+ `這份文件跟著模板包 \`${namespace}/${name}\` 一起被建立。\`---\` 在這個包底下是切頁符——`,
427
+ '前後各留一個空行,否則 Markdown 會把它讀成 setext 標題。',
428
+ '',
429
+ '---',
430
+ '',
431
+ '# 三份分工',
432
+ '',
433
+ '- `manifest.toml` 管宣告與套用態(所騎的 layout、准入的 block、maxWidth 與 measure)',
434
+ '- `stylesheet.css` 管皮',
435
+ '- 這份 md 管預設樣貌',
436
+ '',
437
+ ':::note',
438
+ '產物內建播放:方向鍵/空白鍵翻頁,f 全螢幕,Esc 離開。',
439
+ ':::',
440
+ '',
441
+ '---',
442
+ '',
443
+ ':::place col=1 colSpan=16',
444
+ '**24 格座標**:這一段占 24 格中的 16 格。這個文體宣告了整頁以 24 格軌定址,',
445
+ '所以座標不必再包一層 `grid`。',
446
+ ':::',
447
+ '',
448
+ ':::place col=17 colSpan=8',
449
+ '**側欄**:占右邊 8 格。',
450
+ ':::',
451
+ '',
452
+ ':::place col=1 colSpan=24',
453
+ '這一張沒有標題,是刻意的:`#` 標題會開一個 section,把後面的內容都收進去,',
454
+ '於是擺位是相對那個 section 而不是相對整張投影片的格軌。',
455
+ ':::',
456
+ '',
457
+ ].join('\n')
458
+
459
+ /**
460
+ * The card 預設 md. **Three cards**, because the one thing a card board has to
461
+ * demonstrate is the thing a single-card file cannot: that a top-level heading
462
+ * is a card, that the cards sit side by side, and that they reflow. A starter
463
+ * file with one heading is indistinguishable from an `article`.
464
+ *
465
+ * It opens with a paragraph *before* the first heading, on purpose: that is the
466
+ * lead-in region, the other half of the 文體's cut, and a starter file that never
467
+ * produced one would leave the reader to discover it from a specification.
468
+ *
469
+ * No `---` anywhere. On this 文體 it is not a page break (that is a `deck` fact,
470
+ * `src/layouts/template-index.js`), so a stray one would come out as a thematic
471
+ * rule and teach exactly the wrong thing.
472
+ */
473
+ const cardDocument = ({ namespace, name }) =>
474
+ [
475
+ '---',
476
+ `template: ${namespace}/${name}`,
477
+ `title: ${namespace}/${name}`,
478
+ 'kicker: kamishibai 卡片骨架',
479
+ '---',
480
+ '',
481
+ `這份文件跟著模板包 \`${namespace}/${name}\` 一起被建立。第一個標題之前的段落`,
482
+ '不會變成卡片,而是卡群的前導語區——文體把頂層內容切成這兩塊。',
483
+ '',
484
+ '# 一張卡就是一個頂層標題',
485
+ '',
486
+ '標題底下的所有東西都留在同一張卡裡,子標題也是。卡片之間彼此獨立,',
487
+ '所以讀的人可以只挑一張看。',
488
+ '',
489
+ '## 子標題留在卡內',
490
+ '',
491
+ '- 卡內照常有清單',
492
+ '- 也照常有行內 `程式碼`',
493
+ '',
494
+ '# 三張卡才看得出鋪排',
495
+ '',
496
+ '一張卡看起來與長文沒有兩樣,兩張看不出換行,三張才畫得出「寬視窗多欄、',
497
+ '窄視窗單欄」這件事。',
498
+ '',
499
+ ':::note',
500
+ '卡格是 24 條格軌,一張卡佔其中幾條由 `stylesheet.css` 的 `--card-span` 決定。',
501
+ '想要兩欄看板就把它改成 12。',
502
+ ':::',
503
+ '',
504
+ '# 三份分工',
505
+ '',
506
+ '- `manifest.toml` 管宣告與套用態(所騎的 layout、准入的 block、maxWidth 與 measure)',
507
+ '- `stylesheet.css` 管皮,包含卡格與斷點',
508
+ '- 這份 md 管預設樣貌',
509
+ '',
510
+ ].join('\n')
511
+
512
+ /**
513
+ * The one-page 預設 md. **Two screens plus an opening**, because the one thing a
514
+ * one-page site has to demonstrate is the thing a single-screen file cannot:
515
+ * that a top-level heading starts a new screen, and that scrolling is what moves
516
+ * between them. A starter file with one heading is indistinguishable from an
517
+ * `article` that happens to be scaled.
518
+ *
519
+ * It opens with a paragraph *before* the first heading, on purpose: together
520
+ * with the package's masthead that is the opening screen — the other half of the
521
+ * 文體's cut, and a starter file that never produced one would leave the reader
522
+ * to discover it from a specification.
523
+ *
524
+ * No `---` anywhere. On this 文體 it is not a page break (that is a `deck` fact,
525
+ * `src/layouts/template-index.js`), so a stray one would come out as a thematic
526
+ * rule *inside* a screen and teach exactly the wrong thing — the screen break is
527
+ * the heading.
528
+ */
529
+ const onePageDocument = ({ namespace, name }) =>
530
+ [
531
+ '---',
532
+ `template: ${namespace}/${name}`,
533
+ `title: ${namespace}/${name}`,
534
+ 'kicker: kamishibai 一頁式骨架',
535
+ '---',
536
+ '',
537
+ `這份文件跟著模板包 \`${namespace}/${name}\` 一起被建立。第一個標題之前的段落`,
538
+ '不會自成一屏,而是與抬頭一起組成開場屏——文體把頂層內容切成這兩塊。',
539
+ '',
540
+ '# 一屏就是一個頂層標題',
541
+ '',
542
+ '標題底下的所有東西都留在同一屏裡,子標題也是。一屏是一塊固定邏輯畫布,',
543
+ '視窗多大它就等比縮放到多大,所以在投影幕上與在筆電上看到的是同一個版面。',
544
+ '',
545
+ '## 子標題留在屏內',
546
+ '',
547
+ '- 屏內照常有清單',
548
+ '- 也照常有行內 `程式碼`',
549
+ '',
550
+ '# 兩屏才看得出捲動',
551
+ '',
552
+ '一屏看起來與一頁縮放過的長文沒有兩樣,兩屏才畫得出「往下捲,換一屏」這件事。',
553
+ '',
554
+ ':::note',
555
+ '切屏的是頂層標題,不是切頁符:deck 用的那組三橫線在這個文體上只會畫出一條分隔線。',
556
+ ':::',
557
+ '',
558
+ '# 三份分工',
559
+ '',
560
+ '- `manifest.toml` 管宣告與套用態(所騎的 layout、准入的 block、maxWidth 與 measure)',
561
+ '- `stylesheet.css` 管皮,包含屏的內距與列印形',
562
+ '- 這份 md 管預設樣貌',
563
+ '',
564
+ ].join('\n')
565
+
566
+ /**
567
+ * The resume 預設 md. **Two sheets**, because the one thing a résumé scaffold has
568
+ * to demonstrate is the thing a single-sheet file cannot: that a top-level
569
+ * heading starts a **new sheet of paper**, and that a résumé section is `##`
570
+ * rather than `#`. A starter file with one heading would teach the opposite
571
+ * convention by omission, and the reader would find out at the printer.
572
+ *
573
+ * It opens with a line *before* the first heading, on purpose: together with the
574
+ * package's masthead that is the top of the first sheet — and, unlike every
575
+ * other 文體 here, it does **not** get a page of its own. An A4 sheet holding
576
+ * only a name is the failure this cut exists to avoid.
577
+ *
578
+ * No `---` anywhere. On this 文體 it is not a page break (that is a `deck` fact,
579
+ * `src/layouts/template-index.js`), so a stray one would come out as a thematic
580
+ * rule *inside* a sheet and teach exactly the wrong thing — the page break is
581
+ * the top-level heading.
582
+ */
583
+ const resumeDocument = ({ namespace, name }) =>
584
+ [
585
+ '---',
586
+ `template: ${namespace}/${name}`,
587
+ `title: ${namespace}/${name}`,
588
+ 'kicker: kamishibai 履歷骨架',
589
+ 'author: you@example.org',
590
+ '---',
591
+ '',
592
+ `這份文件跟著模板包 \`${namespace}/${name}\` 一起被建立。第一個標題之前的段落`,
593
+ '不會自成一張紙,而是與抬頭一起排在**第一張紙的頂端**——紙上只印一個名字',
594
+ '是這個文體刻意避開的浪費。',
595
+ '',
596
+ '# 這一張紙',
597
+ '',
598
+ '一個頂層標題就是一張 A4。履歷的各段落請用 `##`:它們留在同一張紙裡,',
599
+ '而列印時一張紙就是一頁,不多也不少。',
600
+ '',
601
+ '## 經歷',
602
+ '',
603
+ '- **某某公司**(2023 - 至今)|職稱',
604
+ ' - 一行寫清楚做了什麼、對誰有用',
605
+ '- **另一間公司**(2021 - 2023)|職稱',
606
+ '',
607
+ '## 技能',
608
+ '',
609
+ '- 一組工具、一組語言、一組領域知識',
610
+ '',
611
+ ':::note',
612
+ '切紙的是頂層標題,不是切頁符:deck 用的那組三橫線在這個文體上只會畫出一條分隔線。',
613
+ ':::',
614
+ '',
615
+ '# 第二張紙',
616
+ '',
617
+ '兩張紙才看得出換頁。第二張紙用來放作品、著作或推薦人——內容放不下的時候,',
618
+ '開一個新的頂層標題就是開一張新的紙。',
619
+ '',
620
+ '## 三份分工',
621
+ '',
622
+ '- `manifest.toml` 管宣告與套用態(所騎的 layout、准入的 block、maxWidth 與 measure)',
623
+ '- `stylesheet.css` 管皮,包含紙的內距、@page 邊界與列印形',
624
+ '- 這份 md 管預設樣貌',
625
+ '',
626
+ ].join('\n')
627
+
628
+ /**
629
+ * The 文體 this scaffold can actually produce a **working** package for, and
630
+ * everything it needs in order to.
631
+ *
632
+ * 複驗 F-1 (F3): `-l` used to accept any string. `init x/y -l deck` therefore
633
+ * wrote a manifest declaring the `deck` 文體 next to an article-shaped 預設 md,
634
+ * and then printed 「開箱即用:kamishibai render …」 — a command that cannot
635
+ * work, because the deck layout requires a `deck` root form the scaffolded
636
+ * document does not have. Two harms, and the second is the worse one: a dead
637
+ * package is a file you can delete, while a wrong copy-paste command from the
638
+ * SDK's own mouth is something the user will trust and then blame themselves for.
639
+ *
640
+ * So the flag is narrowed to what this table has content for. `deck` joined it
641
+ * in F2f, together with the other half it could not work without — a store deck
642
+ * package's `---` actually cutting pages (`src/layouts/template-index.js`).
643
+ */
644
+ export const SCAFFOLD_BLUEPRINTS = Object.freeze({
645
+ article: Object.freeze({
646
+ root: NO_ROOT,
647
+ blocks: ARTICLE_BLOCKS,
648
+ maxWidth: ARTICLE_MAX_WIDTH,
649
+ measure: ARTICLE_MEASURE,
650
+ stylesheet: articleStylesheet,
651
+ document: articleDocument,
652
+ }),
653
+ /**
654
+ * `card` joined the table in F2c, and the entry is the measurement that slice
655
+ * exists to take: supporting a third 文體 cost this object and nothing else —
656
+ * no edit to `template-scaffold.js`, no new branch anywhere in the writer.
657
+ *
658
+ * `root: NO_ROOT` restated for the same reason `article` restates it: the
659
+ * `card` 文體 requires no root form, and a manifest that said nothing would be
660
+ * a disagreement rather than a silence (`KSB_LAYOUT_ROOT_CONFLICT`).
661
+ */
662
+ card: Object.freeze({
663
+ root: NO_ROOT,
664
+ blocks: ARTICLE_BLOCKS,
665
+ maxWidth: CARD_MAX_WIDTH,
666
+ measure: CARD_MEASURE,
667
+ stylesheet: cardStylesheet,
668
+ document: cardDocument,
669
+ }),
670
+ deck: Object.freeze({
671
+ /**
672
+ * Restated, never invented: the `deck` 文體 requires this root form, and a
673
+ * manifest that disagreed with its 文體 is refused outright
674
+ * (`KSB_LAYOUT_ROOT_CONFLICT`). Saying nothing would be a disagreement too.
675
+ */
676
+ root: 'deck',
677
+ blocks: DECK_BLOCKS,
678
+ maxWidth: DECK_MAX_WIDTH,
679
+ measure: DECK_MEASURE,
680
+ stylesheet: deckStylesheet,
681
+ document: deckDocument,
682
+ }),
683
+ /**
684
+ * `one-page` joined the table in F2d — the first **bounded** 文體 the scaffold
685
+ * can produce a working package for. That matters here rather than being a
686
+ * label: the logical canvas is the 文體's and arrives on the markup, so this
687
+ * entry supplies no size at all, and the skin's whole job is to be a frame
688
+ * that does not grow (see `onePageStylesheet`). A blueprint that laid the
689
+ * screen out like an article would produce a package whose content overflows
690
+ * the very canvas the skeleton is scaling — a page that looks broken in a way
691
+ * no error ever mentions.
692
+ *
693
+ * `root: NO_ROOT` restated for the same reason `article` and `card` restate
694
+ * it: the 文體 requires no root form, and a manifest that said nothing would
695
+ * be a disagreement rather than a silence (`KSB_LAYOUT_ROOT_CONFLICT`).
696
+ */
697
+ 'one-page': Object.freeze({
698
+ root: NO_ROOT,
699
+ blocks: ARTICLE_BLOCKS,
700
+ maxWidth: ONE_PAGE_MAX_WIDTH,
701
+ measure: ONE_PAGE_MEASURE,
702
+ stylesheet: onePageStylesheet,
703
+ document: onePageDocument,
704
+ }),
705
+ /**
706
+ * `resume` joined the table in F2e, completing the v1 五版面. It is the first
707
+ * entry whose skin has to be right about **paper** rather than about a
708
+ * window: the `@page` box, the forced break between sheets and the print
709
+ * stand-down are not polish here, they are the difference between a PDF that
710
+ * is a résumé and a PDF that is a clipped ribbon of one.
711
+ *
712
+ * `root: NO_ROOT` restated for the same reason the other flowing-rooted
713
+ * entries restate it: the 文體 requires no root form, and a manifest that said
714
+ * nothing would be a disagreement rather than a silence
715
+ * (`KSB_LAYOUT_ROOT_CONFLICT`).
716
+ */
717
+ resume: Object.freeze({
718
+ root: NO_ROOT,
719
+ blocks: ARTICLE_BLOCKS,
720
+ maxWidth: RESUME_MAX_WIDTH,
721
+ measure: RESUME_MEASURE,
722
+ stylesheet: resumeStylesheet,
723
+ document: resumeDocument,
724
+ }),
725
+ })
726
+
727
+ /** Verbatim — the 文體 `init -l` lets through, derived from the table above. */
728
+ export const SCAFFOLD_SUPPORTED_LAYOUTS = Object.freeze(Object.keys(SCAFFOLD_BLUEPRINTS))