@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.
- package/README.md +192 -0
- package/package.json +54 -0
- package/src/blocks/board.js +210 -0
- package/src/blocks/callout.js +63 -0
- package/src/blocks/code.js +28 -0
- package/src/blocks/deck.js +76 -0
- package/src/blocks/diagram.js +265 -0
- package/src/blocks/element.js +51 -0
- package/src/blocks/graph.js +264 -0
- package/src/blocks/grid.js +156 -0
- package/src/blocks/index.js +106 -0
- package/src/blocks/list.js +50 -0
- package/src/blocks/placement.js +119 -0
- package/src/blocks/prose.js +28 -0
- package/src/blocks/quote.js +25 -0
- package/src/blocks/raw.js +47 -0
- package/src/blocks/registry.js +158 -0
- package/src/blocks/schema-parts.js +19 -0
- package/src/blocks/section.js +53 -0
- package/src/blocks/slide.js +104 -0
- package/src/blocks/stat.js +83 -0
- package/src/blocks/table.js +50 -0
- package/src/blocks/timeline.js +80 -0
- package/src/cli/commands/close.js +51 -0
- package/src/cli/commands/comments.js +73 -0
- package/src/cli/commands/debug.js +34 -0
- package/src/cli/commands/example.js +22 -0
- package/src/cli/commands/export.js +10 -0
- package/src/cli/commands/init.js +53 -0
- package/src/cli/commands/lint.js +93 -0
- package/src/cli/commands/list.js +54 -0
- package/src/cli/commands/open.js +31 -0
- package/src/cli/commands/promote.js +38 -0
- package/src/cli/commands/render.js +31 -0
- package/src/cli/commands/replay.js +49 -0
- package/src/cli/commands/schema.js +10 -0
- package/src/cli/commands/serve.js +178 -0
- package/src/cli/commands/setup.js +64 -0
- package/src/cli/commands/snapshot.js +29 -0
- package/src/cli/commands/templates.js +75 -0
- package/src/cli/deliver.js +54 -0
- package/src/cli/emit.js +29 -0
- package/src/cli/format.js +153 -0
- package/src/cli/index.js +365 -0
- package/src/cli/registry.js +18 -0
- package/src/core/blocks.js +147 -0
- package/src/core/diagram.js +282 -0
- package/src/core/errors.js +156 -0
- package/src/core/example.js +109 -0
- package/src/core/ir.js +62 -0
- package/src/core/lint-gates.js +427 -0
- package/src/core/lint.js +281 -0
- package/src/core/scan.js +84 -0
- package/src/core/schema.js +88 -0
- package/src/core/spec-check.js +33 -0
- package/src/core/validate.js +44 -0
- package/src/core/version.js +18 -0
- package/src/core/vocabulary.js +140 -0
- package/src/delivery/atomic.js +71 -0
- package/src/delivery/comments.js +180 -0
- package/src/delivery/home.js +70 -0
- package/src/delivery/open.js +31 -0
- package/src/delivery/project.js +141 -0
- package/src/delivery/read.js +109 -0
- package/src/delivery/run.js +95 -0
- package/src/delivery/scaffold-blueprints.js +728 -0
- package/src/delivery/store.js +219 -0
- package/src/delivery/template-extensions.js +183 -0
- package/src/delivery/template-format.js +112 -0
- package/src/delivery/template-package.js +376 -0
- package/src/delivery/template-promote.js +240 -0
- package/src/delivery/template-scaffold.js +181 -0
- package/src/delivery/templates.js +192 -0
- package/src/delivery/toml.js +195 -0
- package/src/delivery/write.js +35 -0
- package/src/export/browser.js +130 -0
- package/src/export/index.js +96 -0
- package/src/export/pdf.js +25 -0
- package/src/export/png.js +40 -0
- package/src/export/pptx.js +48 -0
- package/src/export/slides.js +33 -0
- package/src/export/snapshot.js +33 -0
- package/src/layouts/article.js +103 -0
- package/src/layouts/canvas.js +144 -0
- package/src/layouts/card.js +128 -0
- package/src/layouts/deck.js +88 -0
- package/src/layouts/index.js +90 -0
- package/src/layouts/one-page.js +161 -0
- package/src/layouts/registry.js +251 -0
- package/src/layouts/resume.js +172 -0
- package/src/layouts/template-index.js +78 -0
- package/src/parser/artifact.js +38 -0
- package/src/parser/container.js +103 -0
- package/src/parser/index.js +223 -0
- package/src/parser/tokens.js +265 -0
- package/src/render/board-filter.client.js +80 -0
- package/src/render/compile.js +29 -0
- package/src/render/context.js +98 -0
- package/src/render/element.js +32 -0
- package/src/render/fonts.js +129 -0
- package/src/render/graph-hover.client.js +148 -0
- package/src/render/html.js +52 -0
- package/src/render/index.js +241 -0
- package/src/render/measure.js +60 -0
- package/src/render/placement.js +136 -0
- package/src/render/playback.client.js +74 -0
- package/src/render/scale-to-fit.client.js +136 -0
- package/src/render/scale.js +41 -0
- package/src/render/skeleton.js +131 -0
- package/src/render/ssr.js +24 -0
- package/src/render/styles.js +56 -0
- package/src/render/templates.js +191 -0
- package/src/serve/daemon.js +117 -0
- package/src/serve/overlay.js +213 -0
- package/src/serve/protocol.js +36 -0
- package/src/serve/server.js +264 -0
- package/templates/kami/cards/components.js +40 -0
- package/templates/kami/cards/index.js +25 -0
- package/templates/kami/cards/manifest.js +67 -0
- package/templates/kami/cards/styles.css +389 -0
- package/templates/kami/long-form/components.js +102 -0
- package/templates/kami/long-form/index.js +25 -0
- package/templates/kami/long-form/manifest.js +87 -0
- package/templates/kami/long-form/styles.css +481 -0
- package/templates/kami/one-page/components.js +48 -0
- package/templates/kami/one-page/index.js +27 -0
- package/templates/kami/one-page/manifest.js +65 -0
- package/templates/kami/one-page/styles.css +375 -0
- package/templates/kami/resume/components.js +51 -0
- package/templates/kami/resume/index.js +27 -0
- package/templates/kami/resume/manifest.js +65 -0
- package/templates/kami/resume/styles.css +424 -0
- package/templates/kami/slides/components.js +41 -0
- package/templates/kami/slides/index.js +26 -0
- package/templates/kami/slides/manifest.js +64 -0
- package/templates/kami/slides/styles.css +406 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { createServer } from 'node:http'
|
|
2
|
+
import { watch } from 'node:fs'
|
|
3
|
+
import { basename, dirname, resolve } from 'node:path'
|
|
4
|
+
import { CODES, EXIT, KsbError, ROOT_PATH } from '../core/errors.js'
|
|
5
|
+
import { collectBlockIds } from '../core/blocks.js'
|
|
6
|
+
import { appendComment, listComments } from '../delivery/comments.js'
|
|
7
|
+
import { COMMENTS_PATH, ERROR_EVENT, EVENTS_PATH, READY_EVENT, RELOAD_EVENT } from './protocol.js'
|
|
8
|
+
import { injectDevRuntime } from './overlay.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The dev preview server (CONTRACT E1/E2, SPEC §10.2 watch 模式).
|
|
12
|
+
*
|
|
13
|
+
* Deliberately node's own `http` module and nothing else. P3 settled the scope:
|
|
14
|
+
* v0.1 `serve` is file-watch plus a reload push, not Vite HMR — the need is
|
|
15
|
+
* "stop pressing refresh", and a bundler integration would drag a second build
|
|
16
|
+
* pipeline into an SDK whose entire promise is one deterministic renderer. The
|
|
17
|
+
* page the browser gets is the *same bytes* `render` writes, plus an injected
|
|
18
|
+
* dev runtime; there is no second rendering path to disagree with the first.
|
|
19
|
+
*
|
|
20
|
+
* Nothing here writes outside the caller-provided artifact path (which the CLI
|
|
21
|
+
* places inside the central store) and that artifact's comment sidecar.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Loopback only. A preview of unpublished work is not a thing to expose. */
|
|
25
|
+
export const SERVE_HOST = '127.0.0.1'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Coalescing window for filesystem events. Editors save by writing, truncating
|
|
29
|
+
* and renaming, so one human save arrives as a burst; re-rendering per event
|
|
30
|
+
* would push several reloads for one keystroke.
|
|
31
|
+
*/
|
|
32
|
+
const WATCH_DEBOUNCE_MS = 60
|
|
33
|
+
|
|
34
|
+
const JSON_HEADERS = Object.freeze({
|
|
35
|
+
'content-type': 'application/json; charset=utf-8',
|
|
36
|
+
'cache-control': 'no-store',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const failure = (error) => ({
|
|
40
|
+
ok: false,
|
|
41
|
+
errors: [
|
|
42
|
+
error instanceof KsbError
|
|
43
|
+
? error.toFinding()
|
|
44
|
+
: { path: ROOT_PATH, code: CODES.PARSE_FAILED, message: error?.message ?? String(error) },
|
|
45
|
+
],
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const sendJson = (res, status, payload) => {
|
|
49
|
+
res.writeHead(status, JSON_HEADERS)
|
|
50
|
+
res.end(JSON.stringify(payload))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Body reader with a hard cap: a preview server must not be a memory sink. */
|
|
54
|
+
const MAX_BODY_BYTES = 64 * 1024
|
|
55
|
+
|
|
56
|
+
const readBody = (req) =>
|
|
57
|
+
new Promise((resolvePromise, rejectPromise) => {
|
|
58
|
+
const chunks = []
|
|
59
|
+
let size = 0
|
|
60
|
+
req.on('data', (chunk) => {
|
|
61
|
+
size += chunk.length
|
|
62
|
+
if (size > MAX_BODY_BYTES) {
|
|
63
|
+
rejectPromise(
|
|
64
|
+
new KsbError({
|
|
65
|
+
code: CODES.INPUT_EMPTY,
|
|
66
|
+
message: `request body exceeds ${MAX_BODY_BYTES} bytes`,
|
|
67
|
+
exitCode: EXIT.VALIDATION,
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
req.destroy()
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
chunks.push(chunk)
|
|
74
|
+
})
|
|
75
|
+
req.on('error', rejectPromise)
|
|
76
|
+
req.on('end', () => resolvePromise(Buffer.concat(chunks).toString('utf8')))
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Start the preview server.
|
|
81
|
+
*
|
|
82
|
+
* @param {{compile: () => Promise<{html: string, ir: object}>, initial?: object,
|
|
83
|
+
* sourcePath: string, artifactPath: string, port?: number, host?: string}} input
|
|
84
|
+
* `compile` is injected rather than imported so the server owns no opinion
|
|
85
|
+
* about parsing — and so tests can drive the reload path without a renderer.
|
|
86
|
+
* `initial` lets the caller hand over a render it already performed, so the
|
|
87
|
+
* bytes it archived and the bytes it serves are the same object rather than
|
|
88
|
+
* two renders that merely ought to agree.
|
|
89
|
+
* `persist` receives every later successful render *before* it goes live, so
|
|
90
|
+
* the caller can keep the canonical copy in step (CONTRACT E1's living
|
|
91
|
+
* document). Injected rather than imported: this layer must not know that a
|
|
92
|
+
* central store exists.
|
|
93
|
+
* @returns {Promise<{url: string, port: number, close: () => Promise<void>}>}
|
|
94
|
+
*/
|
|
95
|
+
export async function startServer({
|
|
96
|
+
compile,
|
|
97
|
+
initial,
|
|
98
|
+
persist,
|
|
99
|
+
sourcePath,
|
|
100
|
+
artifactPath,
|
|
101
|
+
port = 0,
|
|
102
|
+
host = SERVE_HOST,
|
|
103
|
+
}) {
|
|
104
|
+
const source = resolve(sourcePath)
|
|
105
|
+
let current = initial ?? (await compile())
|
|
106
|
+
|
|
107
|
+
/** Open SSE responses. Held so shutdown can end them instead of hanging. */
|
|
108
|
+
const clients = new Set()
|
|
109
|
+
|
|
110
|
+
const broadcast = (event, data) => {
|
|
111
|
+
const frame = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
|
|
112
|
+
for (const client of clients) {
|
|
113
|
+
try {
|
|
114
|
+
client.write(frame)
|
|
115
|
+
} catch {
|
|
116
|
+
clients.delete(client)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const server = createServer((req, res) => {
|
|
122
|
+
handle(req, res).catch((error) => {
|
|
123
|
+
if (!res.headersSent) sendJson(res, 500, failure(error))
|
|
124
|
+
else res.end()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
async function handle(req, res) {
|
|
129
|
+
const url = new URL(req.url ?? '/', `http://${host}`)
|
|
130
|
+
|
|
131
|
+
if (url.pathname === EVENTS_PATH) {
|
|
132
|
+
res.writeHead(200, {
|
|
133
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
134
|
+
'cache-control': 'no-store',
|
|
135
|
+
connection: 'keep-alive',
|
|
136
|
+
})
|
|
137
|
+
res.write(`event: ${READY_EVENT}\ndata: ${JSON.stringify({ ok: true })}\n\n`)
|
|
138
|
+
clients.add(res)
|
|
139
|
+
req.on('close', () => clients.delete(res))
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (url.pathname === COMMENTS_PATH && req.method === 'GET') {
|
|
144
|
+
sendJson(res, 200, listComments(artifactPath))
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (url.pathname === COMMENTS_PATH && req.method === 'POST') {
|
|
149
|
+
const raw = await readBody(req)
|
|
150
|
+
let body
|
|
151
|
+
try {
|
|
152
|
+
body = JSON.parse(raw)
|
|
153
|
+
} catch (cause) {
|
|
154
|
+
sendJson(
|
|
155
|
+
res,
|
|
156
|
+
400,
|
|
157
|
+
failure(
|
|
158
|
+
new KsbError({
|
|
159
|
+
code: CODES.PARSE_FAILED,
|
|
160
|
+
message: `comment body is not valid JSON: ${cause.message}`,
|
|
161
|
+
exitCode: EXIT.VALIDATION,
|
|
162
|
+
}),
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
// Validated against the *live* IR: the human commented on the page in
|
|
169
|
+
// front of them, so that is the tree whose ids are legal.
|
|
170
|
+
const entry = appendComment({
|
|
171
|
+
artifactPath,
|
|
172
|
+
blockId: body?.blockId,
|
|
173
|
+
text: body?.text,
|
|
174
|
+
blockIds: collectBlockIds(current.ir?.doc),
|
|
175
|
+
})
|
|
176
|
+
sendJson(res, 201, entry)
|
|
177
|
+
} catch (error) {
|
|
178
|
+
sendJson(res, 400, failure(error))
|
|
179
|
+
}
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (url.pathname === '/' && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
184
|
+
const html = injectDevRuntime(current.html)
|
|
185
|
+
res.writeHead(200, {
|
|
186
|
+
'content-type': 'text/html; charset=utf-8',
|
|
187
|
+
'cache-control': 'no-store',
|
|
188
|
+
})
|
|
189
|
+
res.end(req.method === 'HEAD' ? undefined : html)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
sendJson(res, 404, failure(new Error(`no route for ${req.method} ${url.pathname}`)))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let timer = null
|
|
197
|
+
let watcher = null
|
|
198
|
+
|
|
199
|
+
const rerender = async () => {
|
|
200
|
+
try {
|
|
201
|
+
const next = await compile()
|
|
202
|
+
// Order is the invariant, not an optimisation: persist, *then* go live.
|
|
203
|
+
// Comments are validated against whatever is live, so a page that led the
|
|
204
|
+
// canonical by even one render could accept an anchor the canonical
|
|
205
|
+
// cannot resolve — the drift seal F1 caught. If persisting fails, the old
|
|
206
|
+
// render stays live and the failure is announced; the two never disagree.
|
|
207
|
+
if (typeof persist === 'function') await persist(next)
|
|
208
|
+
current = next
|
|
209
|
+
broadcast(RELOAD_EVENT, { ok: true, at: new Date().toISOString() })
|
|
210
|
+
} catch (error) {
|
|
211
|
+
// A source the author has half-typed is not an emergency: keep serving the
|
|
212
|
+
// last good render and say so, rather than dropping the preview or dying.
|
|
213
|
+
broadcast(ERROR_EVENT, failure(error))
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const onFsEvent = (_eventType, filename) => {
|
|
218
|
+
if (filename !== null && filename !== undefined && basename(filename) !== basename(source)) return
|
|
219
|
+
if (timer !== null) clearTimeout(timer)
|
|
220
|
+
timer = setTimeout(() => {
|
|
221
|
+
timer = null
|
|
222
|
+
void rerender()
|
|
223
|
+
}, WATCH_DEBOUNCE_MS)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// The *directory* is watched, not the file: a save that replaces the inode
|
|
227
|
+
// (write-temp-then-rename, which is what most editors do) leaves a file
|
|
228
|
+
// watcher bound to a path nobody writes to again — reload would work once and
|
|
229
|
+
// then quietly stop.
|
|
230
|
+
try {
|
|
231
|
+
watcher = watch(dirname(source), { persistent: true }, onFsEvent)
|
|
232
|
+
} catch (cause) {
|
|
233
|
+
throw new KsbError({
|
|
234
|
+
code: CODES.SERVE_START_FAILED,
|
|
235
|
+
message: `could not watch ${dirname(source)}: ${cause.message}`,
|
|
236
|
+
exitCode: EXIT.VALIDATION,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
241
|
+
server.once('error', (cause) =>
|
|
242
|
+
rejectPromise(
|
|
243
|
+
new KsbError({
|
|
244
|
+
code: CODES.SERVE_START_FAILED,
|
|
245
|
+
message: `could not listen on ${host}:${port}: ${cause.message}`,
|
|
246
|
+
exitCode: EXIT.VALIDATION,
|
|
247
|
+
}),
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
server.listen(port, host, resolvePromise)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const actualPort = server.address().port
|
|
254
|
+
|
|
255
|
+
const close = async () => {
|
|
256
|
+
if (timer !== null) clearTimeout(timer)
|
|
257
|
+
if (watcher !== null) watcher.close()
|
|
258
|
+
for (const client of clients) client.end()
|
|
259
|
+
clients.clear()
|
|
260
|
+
await new Promise((done) => server.close(done))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { url: `http://${host}:${actualPort}/`, port: actualPort, close }
|
|
264
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { el } from '../../../src/blocks/element.js'
|
|
2
|
+
import { manifest } from './manifest.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Chrome of the Kami card template.
|
|
6
|
+
*
|
|
7
|
+
* The shape of the page — the board, the lead-in region, the card grid — is the
|
|
8
|
+
* `card` 文體's, and every template on that layout gets the same one (CONTRACT
|
|
9
|
+
* C1). What is left here is exactly this template's own business: the masthead,
|
|
10
|
+
* 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
|
+
* There is deliberately **no chrome computed from the document** here, unlike
|
|
17
|
+
* `kami/long-form`'s table of contents. On a board the cards *are* the index:
|
|
18
|
+
* a list of links above a grid of the same headings would say everything twice.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const TEMPLATE_LABEL = `${manifest.namespace}/${manifest.name}`
|
|
22
|
+
|
|
23
|
+
const masthead = (meta) =>
|
|
24
|
+
el('header', { class: 'masthead' }, [
|
|
25
|
+
meta.kicker ? el('p', { class: 'kicker' }, [meta.kicker]) : null,
|
|
26
|
+
el('h1', { class: 'doc-title' }, [meta.title]),
|
|
27
|
+
meta.author || meta.date
|
|
28
|
+
? el('p', { class: 'byline' }, [[meta.author, meta.date].filter(Boolean).join(' · ')])
|
|
29
|
+
: null,
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
export const chrome = Object.freeze({
|
|
33
|
+
/** Above the board: who wrote this and what it is called. */
|
|
34
|
+
'cards-lead': (meta) => [masthead(meta)],
|
|
35
|
+
|
|
36
|
+
/** Below it: the engine and skin that drew the page, in this template's words. */
|
|
37
|
+
'cards-foot': () => [
|
|
38
|
+
el('footer', { class: 'colophon' }, [`kamishibai · ${TEMPLATE_LABEL}@${manifest.version}`]),
|
|
39
|
+
],
|
|
40
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
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 page belongs to the `card` layout this manifest
|
|
12
|
+
* names, and this package supplies only skin (`styles`), config (`manifest`)
|
|
13
|
+
* and chrome (CONTRACT C1).
|
|
14
|
+
*/
|
|
15
|
+
export default Object.freeze({
|
|
16
|
+
manifest,
|
|
17
|
+
key: templateKey,
|
|
18
|
+
chrome,
|
|
19
|
+
language: manifest.language,
|
|
20
|
+
/** Fonts subset into the artifact (SPEC §7.2 出廠字體堆疊). */
|
|
21
|
+
fonts: Object.freeze([
|
|
22
|
+
Object.freeze({ package: '@fontsource/noto-serif-tc', weights: Object.freeze(['400', '700']) }),
|
|
23
|
+
]),
|
|
24
|
+
styles: () => readFileSync(STYLES_PATH, 'utf8'),
|
|
25
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template package descriptor (SPEC §5.2 `[template]` section) — the built-in
|
|
3
|
+
* generic skin of the `card` 文體.
|
|
4
|
+
*/
|
|
5
|
+
export const manifest = Object.freeze({
|
|
6
|
+
namespace: 'kami',
|
|
7
|
+
name: 'cards',
|
|
8
|
+
version: '0.1.0',
|
|
9
|
+
description: 'Kami 卡片模板 — 卡面鋪排的看板版面',
|
|
10
|
+
language: 'zh-TW',
|
|
11
|
+
/**
|
|
12
|
+
* The 文體 this skin is a skin *of* (CONTRACT C1). The frame, the lead-in
|
|
13
|
+
* region, the card grid and the two chrome slots all come from there; this
|
|
14
|
+
* package only decides what they look like.
|
|
15
|
+
*/
|
|
16
|
+
layout: 'card',
|
|
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
|
|
41
|
+
* `kami/long-form`'s on purpose: the two Kami skins are one family, and a card
|
|
42
|
+
* board that said 「注意」 while the long-form page said `NOTE` would read as
|
|
43
|
+
* two products.
|
|
44
|
+
*/
|
|
45
|
+
blockConfig: Object.freeze({
|
|
46
|
+
callout: Object.freeze({ labels: Object.freeze({ note: 'NOTE', warn: 'WARNING' }) }),
|
|
47
|
+
}),
|
|
48
|
+
/**
|
|
49
|
+
* The applied state of the `card` layout's geometry, in CSS pixels.
|
|
50
|
+
*
|
|
51
|
+
* `maxWidth` 1200 is the width of the board; `measure` 740 is the width of a
|
|
52
|
+
* line of text (CONTRACT C3). They are two numbers, and on this 文體 they are
|
|
53
|
+
* as far apart as they ever get: the lead-in paragraph respects the measure,
|
|
54
|
+
* while the board beside it is a 24-track grid three cards across. Collapsing
|
|
55
|
+
* them — the pre-F2a habit — would either squeeze the board into a column or
|
|
56
|
+
* stretch every line of prose across 1200px.
|
|
57
|
+
*
|
|
58
|
+
* Both values are this *template's*, not the layout's: the layout only says
|
|
59
|
+
* which knobs exist and what they fall back to. `styles.css` holds neither
|
|
60
|
+
* number; it declares where they go and the render layer writes them in
|
|
61
|
+
* (`src/render/styles.js`).
|
|
62
|
+
*/
|
|
63
|
+
maxWidth: 1200,
|
|
64
|
+
measure: 740,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
export const templateKey = `${manifest.namespace}/${manifest.name}`
|