@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,375 @@
1
+ :root {
2
+ --paper: #14161c;
3
+ --surface: #faf9f5;
4
+ --surface-alt: #f2efe6;
5
+ --accent: #1B365D;
6
+ --accent-soft: #dbe2ec;
7
+ --rule: #ddd9cc;
8
+ --text-primary: #23201a;
9
+ --text-secondary: #5c574c;
10
+ --text-muted: #8a8371;
11
+ --code-bg: #efece1;
12
+ --warn: #8a5a12;
13
+ /* Page geometry is the `one-page` layout's logical screen, shipped on the
14
+ markup as data-ksb-canvas (src/layouts/one-page.js). Not restated here: a
15
+ second copy of a size is a copy free to drift from the one that decides
16
+ anything. Comments in this file are ASCII on purpose -- every distinct
17
+ character in a stylesheet pulls a font subset into every artifact. */
18
+ /* ksb:maxWidth --max-width */
19
+ /* ksb:measure --measure */
20
+ /* Declared means embedded (CONTRACT A4): only families that really ship with
21
+ the artifact are named here; the rest fall back to generic keywords. */
22
+ --serif: 'Noto Serif TC', serif;
23
+ --mono: monospace;
24
+ }
25
+
26
+ * {
27
+ box-sizing: border-box;
28
+ }
29
+
30
+ html,
31
+ body {
32
+ margin: 0;
33
+ padding: 0;
34
+ }
35
+
36
+ body {
37
+ background: var(--paper);
38
+ color: var(--text-primary);
39
+ font-family: var(--serif);
40
+ font-size: 18px;
41
+ line-height: 1.6;
42
+ -webkit-font-smoothing: antialiased;
43
+ }
44
+
45
+ /* The site: a scrollable sequence of screens. Its width is not capped here --
46
+ a screen carries its own fixed logical width inline, so a narrower window
47
+ scrolls sideways rather than squeezing a canvas that is not allowed to
48
+ reflow. */
49
+ .one-page {
50
+ margin: 0 auto;
51
+ overflow-x: auto;
52
+ }
53
+
54
+ /* One screen *is* the bounded canvas: its size arrives inline and the
55
+ skeleton's scale-to-fit asset scales it (src/render/scale-to-fit.client.js).
56
+ Only the skin lives here. Content is centred vertically, because a screen is
57
+ a fixed frame rather than a column that grows. */
58
+ .screen {
59
+ margin: 0 auto;
60
+ padding: 76px 88px;
61
+ background: var(--surface);
62
+ overflow: hidden;
63
+ display: flex;
64
+ flex-direction: column;
65
+ justify-content: center;
66
+ }
67
+
68
+ /* Alternating tone, so scrolling past a screen boundary is visible even when
69
+ two neighbours happen to start with the same kind of block. */
70
+ .screen:nth-child(even) {
71
+ background: var(--surface-alt);
72
+ }
73
+
74
+ /* Content region: capped by the page width and centred. A screen that let type
75
+ run the full canvas would have no margin at all. */
76
+ .screen-inner {
77
+ width: 100%;
78
+ max-width: var(--max-width);
79
+ margin: 0 auto;
80
+ overflow-y: auto;
81
+ max-height: 100%;
82
+ }
83
+
84
+ /* Running text keeps the measure even though the screen is far wider: the page
85
+ and the column are two numbers (CONTRACT C3). */
86
+ .prose,
87
+ .list,
88
+ .quote,
89
+ .callout {
90
+ max-width: var(--measure);
91
+ }
92
+
93
+ .masthead {
94
+ border-bottom: 2px solid var(--accent);
95
+ padding-bottom: 24px;
96
+ margin-bottom: 32px;
97
+ }
98
+
99
+ .masthead .kicker {
100
+ font-size: 13px;
101
+ letter-spacing: 0.22em;
102
+ text-transform: uppercase;
103
+ color: var(--accent);
104
+ margin: 0 0 12px;
105
+ }
106
+
107
+ .masthead .doc-title {
108
+ font-size: 52px;
109
+ line-height: 1.2;
110
+ font-weight: 700;
111
+ margin: 0;
112
+ color: var(--text-primary);
113
+ }
114
+
115
+ .masthead .byline {
116
+ margin: 16px 0 0;
117
+ font-size: 15px;
118
+ color: var(--text-secondary);
119
+ }
120
+
121
+ /* A screen holds exactly one chapter, so the section gives up its outer margins
122
+ and keeps only its inner rhythm. */
123
+ .screen-inner > .section {
124
+ margin: 0;
125
+ }
126
+
127
+ .section {
128
+ margin: 0 0 24px;
129
+ }
130
+
131
+ .section > .section-title {
132
+ font-weight: 700;
133
+ color: var(--accent);
134
+ line-height: 1.3;
135
+ }
136
+
137
+ .section-l1 > .section-title,
138
+ .section-l2 > .section-title {
139
+ font-size: 40px;
140
+ margin: 0 0 28px;
141
+ }
142
+
143
+ .section .section-l2 > .section-title,
144
+ .section .section-l3 > .section-title {
145
+ font-size: 24px;
146
+ margin: 32px 0 12px;
147
+ }
148
+
149
+ .section-l4 > .section-title,
150
+ .section-l5 > .section-title,
151
+ .section-l6 > .section-title {
152
+ font-size: 19px;
153
+ margin: 24px 0 10px;
154
+ color: var(--text-primary);
155
+ }
156
+
157
+ .prose {
158
+ margin: 0 0 16px;
159
+ }
160
+
161
+ .prose:last-child {
162
+ margin-bottom: 0;
163
+ }
164
+
165
+ .prose a {
166
+ color: var(--accent);
167
+ text-underline-offset: 3px;
168
+ }
169
+
170
+ /* Lists arrive two ways -- a list block, and list markup pushed into prose by a
171
+ raw island -- and both get the same rules, or the same source would look
172
+ different depending on which door it came through. */
173
+ .prose ul,
174
+ .prose ol,
175
+ .list {
176
+ margin: 0 0 16px;
177
+ padding-left: 1.4em;
178
+ }
179
+
180
+ .prose li,
181
+ .list li {
182
+ margin: 0 0 8px;
183
+ }
184
+
185
+ .prose li:last-child,
186
+ .list li:last-child {
187
+ margin-bottom: 0;
188
+ }
189
+
190
+ .prose li::marker,
191
+ .list li::marker {
192
+ color: var(--text-muted);
193
+ }
194
+
195
+ .list .list {
196
+ margin: 8px 0 0;
197
+ }
198
+
199
+ .list li > .prose {
200
+ margin: 0;
201
+ }
202
+
203
+ .list li > .prose + * {
204
+ margin-top: 8px;
205
+ }
206
+
207
+ .prose code,
208
+ .callout code,
209
+ .quote code,
210
+ .table code {
211
+ font-family: var(--mono);
212
+ font-size: 0.9em;
213
+ background: var(--code-bg);
214
+ border-radius: 3px;
215
+ padding: 1px 5px;
216
+ }
217
+
218
+ .quote {
219
+ margin: 0 0 18px;
220
+ padding: 4px 0 4px 18px;
221
+ border-left: 3px solid var(--accent-soft);
222
+ color: var(--text-secondary);
223
+ }
224
+
225
+ .quote > .prose:last-child {
226
+ margin-bottom: 0;
227
+ }
228
+
229
+ .callout {
230
+ margin: 0 0 18px;
231
+ padding: 16px 18px;
232
+ background: var(--code-bg);
233
+ border-left: 4px solid var(--accent);
234
+ border-radius: 2px;
235
+ }
236
+
237
+ .callout-warn {
238
+ border-left-color: var(--warn);
239
+ }
240
+
241
+ .callout .callout-label {
242
+ display: block;
243
+ font-size: 11px;
244
+ letter-spacing: 0.18em;
245
+ text-transform: uppercase;
246
+ color: var(--accent);
247
+ margin-bottom: 6px;
248
+ }
249
+
250
+ .callout-warn .callout-label {
251
+ color: var(--warn);
252
+ }
253
+
254
+ .callout > .prose:last-child {
255
+ margin-bottom: 0;
256
+ }
257
+
258
+ .code {
259
+ margin: 0 0 18px;
260
+ padding: 16px 18px;
261
+ background: var(--code-bg);
262
+ border: 1px solid var(--rule);
263
+ border-radius: 4px;
264
+ overflow-x: auto;
265
+ font-family: var(--mono);
266
+ font-size: 14px;
267
+ line-height: 1.5;
268
+ }
269
+
270
+ .code code {
271
+ font-family: inherit;
272
+ }
273
+
274
+ .table-wrap {
275
+ margin: 0 0 18px;
276
+ overflow-x: auto;
277
+ }
278
+
279
+ .table {
280
+ width: 100%;
281
+ border-collapse: collapse;
282
+ font-size: 15px;
283
+ }
284
+
285
+ .table th,
286
+ .table td {
287
+ border-bottom: 1px solid var(--rule);
288
+ padding: 8px 10px;
289
+ text-align: left;
290
+ vertical-align: top;
291
+ }
292
+
293
+ .table th {
294
+ color: var(--accent);
295
+ font-weight: 700;
296
+ border-bottom: 2px solid var(--accent-soft);
297
+ }
298
+
299
+ .island {
300
+ margin: 0 0 18px;
301
+ }
302
+
303
+ /* diagram (CONTRACT D1) -- the geometry is computed by core/diagram.js and the
304
+ skin only colours it, with variables every Kami skin agrees on, so one spec
305
+ draws one picture rather than several that look alike. */
306
+ .diagram {
307
+ margin: 0 0 18px;
308
+ overflow-x: auto;
309
+ }
310
+
311
+ .diagram-svg {
312
+ display: block;
313
+ max-width: 100%;
314
+ height: auto;
315
+ font-family: var(--serif);
316
+ }
317
+
318
+ .diagram-node-box {
319
+ fill: var(--accent-soft);
320
+ stroke: var(--accent);
321
+ stroke-width: 1.5;
322
+ }
323
+
324
+ .diagram-node-label {
325
+ fill: var(--text-primary);
326
+ font-size: 15px;
327
+ }
328
+
329
+ .diagram-edge-line {
330
+ fill: none;
331
+ stroke: var(--accent);
332
+ stroke-width: 1.5;
333
+ }
334
+
335
+ .diagram-arrow {
336
+ fill: var(--accent);
337
+ }
338
+
339
+ .diagram-edge-label {
340
+ fill: var(--text-secondary);
341
+ font-size: 12px;
342
+ }
343
+
344
+ /* Outside every canvas, and deliberately so: a colophon is the one thing on a
345
+ site that is allowed to be small and unscaled. */
346
+ .colophon {
347
+ padding: 28px;
348
+ font-size: 12px;
349
+ color: var(--text-muted);
350
+ letter-spacing: 0.06em;
351
+ text-align: center;
352
+ }
353
+
354
+ @media print {
355
+ body {
356
+ background: #fff;
357
+ }
358
+
359
+ /* scale-to-fit is a screen affordance. On paper (and in `export`, which asks
360
+ for the print form) the inline transform would shrink every screen into a
361
+ corner of its page. */
362
+ .screen {
363
+ transform: none !important;
364
+ break-inside: avoid;
365
+ background: #fff;
366
+ }
367
+
368
+ .screen:nth-child(even) {
369
+ background: #fff;
370
+ }
371
+
372
+ .colophon {
373
+ color: #555;
374
+ }
375
+ }
@@ -0,0 +1,51 @@
1
+ import { el } from '../../../src/blocks/element.js'
2
+ import { manifest } from './manifest.js'
3
+
4
+ /**
5
+ * Chrome of the Kami résumé template.
6
+ *
7
+ * The shape of the artifact — the sheets, their A4 canvas, which top-level
8
+ * blocks join the first sheet — is the `resume` 文體's, and every template on
9
+ * that layout gets the same one (CONTRACT C1). What is left here is exactly this
10
+ * template's own business: the masthead, the colophon, and every word of them.
11
+ *
12
+ * Nothing here is a component: each slot returns the same neutral element tree a
13
+ * block module returns, so this file is data with a little arithmetic in it and
14
+ * imports no rendering library at all (CONTRACT A1).
15
+ *
16
+ * Both slots land **inside** a sheet — the head on the first, the foot on the
17
+ * last — because this 文體 is printed. Anything outside a canvas is laid onto a
18
+ * page of its own, so a colophon floating below the last sheet would cost every
19
+ * résumé an extra printed page carrying one line of small type.
20
+ */
21
+
22
+ const TEMPLATE_LABEL = `${manifest.namespace}/${manifest.name}`
23
+
24
+ /**
25
+ * Name, one line of positioning, and the contact line.
26
+ *
27
+ * `kicker` reads as the role someone is applying with and `author`/`date` as the
28
+ * contact line, exactly as they do on the other Kami skins — the same
29
+ * frontmatter draws in every 文體, which is what 「換皮不換文體」 promises.
30
+ */
31
+ const masthead = (meta) =>
32
+ el('header', { class: 'masthead' }, [
33
+ el('h1', { class: 'doc-title' }, [meta.title]),
34
+ meta.kicker ? el('p', { class: 'kicker' }, [meta.kicker]) : null,
35
+ meta.author || meta.date
36
+ ? el('p', { class: 'byline' }, [[meta.author, meta.date].filter(Boolean).join(' · ')])
37
+ : null,
38
+ ])
39
+
40
+ export const chrome = Object.freeze({
41
+ /** The top of the first sheet: who this is and how to reach them. */
42
+ 'resume-head': (meta) => [masthead(meta)],
43
+
44
+ /**
45
+ * The foot of the last sheet: the engine and skin that drew it, in this
46
+ * template's words. Inside the canvas on purpose — see the file header.
47
+ */
48
+ 'resume-foot': () => [
49
+ el('footer', { class: 'colophon' }, [`kamishibai · ${TEMPLATE_LABEL}@${manifest.version}`]),
50
+ ],
51
+ })
@@ -0,0 +1,27 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { manifest, templateKey } from './manifest.js'
4
+ import { chrome } from './components.js'
5
+
6
+ const STYLES_PATH = fileURLToPath(new URL('./styles.css', import.meta.url))
7
+
8
+ /**
9
+ * Template package as consumed by the render layer.
10
+ *
11
+ * No `root`: the shape of the artifact belongs to the `resume` layout this
12
+ * manifest names, and this package supplies only skin (`styles`), config
13
+ * (`manifest`) and chrome (CONTRACT C1). The scale-to-fit behaviour is not here
14
+ * either — it is skeleton, keyed on the bounded canvas, so a third-party skin on
15
+ * this 文體 inherits it and cannot decline it (`src/render/skeleton.js`).
16
+ */
17
+ export default Object.freeze({
18
+ manifest,
19
+ key: templateKey,
20
+ chrome,
21
+ language: manifest.language,
22
+ /** Fonts subset into the artifact (SPEC §7.2 出廠字體堆疊). */
23
+ fonts: Object.freeze([
24
+ Object.freeze({ package: '@fontsource/noto-serif-tc', weights: Object.freeze(['400', '700']) }),
25
+ ]),
26
+ styles: () => readFileSync(STYLES_PATH, 'utf8'),
27
+ })
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Template package descriptor (SPEC §5.2 `[template]` section) — the built-in
3
+ * generic skin of the `resume` 文體.
4
+ */
5
+ export const manifest = Object.freeze({
6
+ namespace: 'kami',
7
+ name: 'resume',
8
+ version: '0.1.0',
9
+ description: 'Kami 履歷模板 — 一張紙一塊 A4 畫布的列印優先版面',
10
+ language: 'zh-TW',
11
+ /**
12
+ * The 文體 this skin is a skin *of* (CONTRACT C1). The frame, the sheets, the
13
+ * fixed A4 canvas and the two chrome slots all come from there; this package
14
+ * only decides what they look like.
15
+ */
16
+ layout: 'resume',
17
+ /** No required root form: this template renders whatever sits under `doc`. */
18
+ root: null,
19
+ blocks: Object.freeze([
20
+ 'doc',
21
+ 'section',
22
+ 'prose',
23
+ 'list',
24
+ 'quote',
25
+ 'callout',
26
+ 'code',
27
+ 'table',
28
+ 'raw',
29
+ 'diagram',
30
+ // F6a 四型資料 block(11 號票 P1「資料」)。五張 Kami 皮一起宣告:
31
+ // 詞彙表是准入規則,一張皮少宣告一種,同一份文件在它底下就會被
32
+ // KSB_TEMPLATE_BLOCK_UNSUPPORTED 擋掉——而那不是守門,是缺角。
33
+ 'stat',
34
+ 'timeline',
35
+ 'board',
36
+ 'graph',
37
+ ]),
38
+ /**
39
+ * 轉子/<型別>/config — the callout labels are *wording*, and wording is this
40
+ * template's answer rather than the SDK's (CONTRACT A7). Identical to the
41
+ * other Kami skins on purpose: they are one family.
42
+ */
43
+ blockConfig: Object.freeze({
44
+ callout: Object.freeze({ labels: Object.freeze({ note: 'NOTE', warn: 'WARNING' }) }),
45
+ }),
46
+ /**
47
+ * The applied state of the `resume` layout's geometry, in CSS pixels.
48
+ *
49
+ * The logical canvas (794×1123, one A4 sheet) is **not** here: it belongs to
50
+ * the 文體 and arrives with the layout. What this template supplies is the
51
+ * pair every 文體 has — `maxWidth` 660 is how wide the content region inside a
52
+ * sheet may grow (the sheet minus a printer-safe margin on each side), and
53
+ * `measure` 620 is the width of a line of text on paper. This is the one 文體
54
+ * where that number is not 740: a 740px column plus any margin at all is
55
+ * wider than A4 (CONTRACT C3 is about keeping the two numbers apart, not
56
+ * about the value of either).
57
+ *
58
+ * `styles.css` holds neither number; it declares where they go and the render
59
+ * layer writes them in (`src/render/styles.js`).
60
+ */
61
+ maxWidth: 660,
62
+ measure: 620,
63
+ })
64
+
65
+ export const templateKey = `${manifest.namespace}/${manifest.name}`