@erclx/canon 4.90.0 → 4.93.0
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 +5 -5
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +78 -0
- package/claude/skills/design-taste/SKILL.md +148 -0
- package/claude/skills/design-taste/references/craft.md +58 -0
- package/claude/skills/design-taste/references/kinds.md +52 -0
- package/claude/skills/design-taste/references/preflight.md +77 -0
- package/claude/skills/design-taste/references/systems.md +49 -0
- package/claude/skills/design-taste/references/tells.md +101 -0
- package/claude/skills/design-taste/references/vocabulary.md +62 -0
- package/claude/skills/draft-and-pick/REQUIREMENT.md +8 -2
- package/claude/skills/draft-and-pick/SKILL.md +13 -5
- package/claude/skills/draft-ready/REQUIREMENT.md +53 -0
- package/claude/skills/draft-ready/SKILL.md +118 -0
- package/claude/skills/draft-ready/references/assembly.md +67 -0
- package/claude/skills/session-compact/REQUIREMENT.md +44 -0
- package/claude/skills/session-compact/SKILL.md +60 -0
- package/claude/skills/session-compact/references/handoff-note.md +68 -0
- package/claude/skills/session-map/REQUIREMENT.md +3 -2
- package/claude/skills/session-map/SKILL.md +2 -2
- package/claude/skills/session-resume/SKILL.md +3 -3
- package/claude/skills/teach-workspace/references/lesson-craft.md +1 -1
- package/docs/agents/capture.md +12 -5
- package/docs/agents/commands.md +3 -3
- package/docs/agents/rule-citations.md +1 -1
- package/docs/agents/teach.md +2 -2
- package/docs/workflow/ai-workflow.md +3 -1
- package/governance/rules/claude/563-ready.md +4 -0
- package/governance/rules/lib/305-e2e-reliability.md +3 -2
- package/governance/rules/lib/306-test-scope.md +1 -2
- package/governance/rules/ui/410-a11y.md +9 -0
- package/governance/rules/ui/420-forms.md +9 -0
- package/governance/rules/ui/460-design-taste.md +29 -0
- package/governance/rules/ui/470-motion.md +23 -0
- package/governance/stacks/astro.toml +1 -1
- package/governance/stacks/react.toml +1 -1
- package/package.json +3 -2
- package/scripts/core/regen-hero.sh +4 -3
- package/src/capture/render.ts +15 -1
- package/src/claude/cases/authoring.ts +5 -0
- package/src/claude/cases/misc.ts +10 -0
- package/src/cli.ts +1 -1
- package/src/commands/capture.ts +26 -1
- package/src/commands/design.ts +5 -0
- package/src/commands/feedback.ts +1 -0
- package/src/commands/serve.ts +11 -1
- package/src/commands/slides.ts +3 -0
- package/src/commands/transcripts.ts +1 -0
- package/src/design/base.css +25 -19
- package/src/design/css.ts +4 -0
- package/src/design/fonts.ts +17 -0
- package/src/design/tokens.ts +45 -30
- package/src/gate/measures.ts +5 -4
- package/src/gate/stages.ts +1 -1
- package/src/serve/static.ts +90 -6
- package/src/teach/render.ts +2 -2
- package/src/teach/workspace.ts +1 -0
- package/standards/ready.md +2 -0
- package/standards/session.md +1 -0
- package/tooling/astro/configs/playwright.config.ts +2 -0
- package/tooling/nextjs/configs/playwright.config.ts +2 -0
- package/tooling/vite-react/configs/playwright.config.ts +2 -0
- package/src/teach/render-fixture.tsx +0 -95
package/src/design/tokens.ts
CHANGED
|
@@ -39,6 +39,12 @@ export interface TypeToken {
|
|
|
39
39
|
readonly verify?: readonly ('family' | 'weight' | 'size' | 'lineHeight')[]
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** One size the system offers, named apart from any role that takes it. */
|
|
43
|
+
export interface TypeStep {
|
|
44
|
+
readonly step: string
|
|
45
|
+
readonly size: string
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
export interface SpaceToken {
|
|
43
49
|
readonly step: string
|
|
44
50
|
readonly multiplier: string
|
|
@@ -57,6 +63,7 @@ export interface DesignTokens {
|
|
|
57
63
|
readonly personality: string
|
|
58
64
|
readonly color: readonly ColorToken[]
|
|
59
65
|
readonly colorNote: string
|
|
66
|
+
readonly typeScale: readonly TypeStep[]
|
|
60
67
|
readonly typography: readonly TypeToken[]
|
|
61
68
|
readonly typographyNote: string
|
|
62
69
|
readonly spacing: readonly SpaceToken[]
|
|
@@ -68,20 +75,19 @@ export interface DesignTokens {
|
|
|
68
75
|
readonly preamble: string
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
/** The monospace stack
|
|
78
|
+
/** The monospace stack, taken by code and by the terminal frames a page embeds. */
|
|
72
79
|
const MONO = 'Noto Sans Mono, DejaVu Sans Mono, monospace'
|
|
73
80
|
|
|
74
81
|
/**
|
|
75
|
-
* The proportional stack
|
|
76
|
-
*
|
|
77
|
-
* picked for taste, so the pairing is one superfamily rather than two systems.
|
|
82
|
+
* The proportional stack every role but `code` declares. Geist is the face the
|
|
83
|
+
* visual-direction track set throughout, embedded through `FONT_FACES`.
|
|
78
84
|
*
|
|
79
85
|
* The stack stops at two names and a generic because `src/design/base.css` is
|
|
80
86
|
* written by `canon design regen` and formatted by prettier, and a declaration
|
|
81
87
|
* past 80 columns is wrapped by the second and flattened by the first. Every
|
|
82
88
|
* emitted line stays inside the width so the two writers never disagree.
|
|
83
89
|
*/
|
|
84
|
-
const SANS = '
|
|
90
|
+
const SANS = 'Geist Variable, DejaVu Sans, sans-serif'
|
|
85
91
|
|
|
86
92
|
const DARK_GROUNDS = ['background', 'surface'] as const
|
|
87
93
|
const LIGHT_GROUNDS = ['light-background', 'light-surface'] as const
|
|
@@ -98,9 +104,9 @@ export const TOKENS: DesignTokens = {
|
|
|
98
104
|
].join('\n'),
|
|
99
105
|
|
|
100
106
|
personality: [
|
|
101
|
-
'Warm neutrals carry the frame under a single rust accent,
|
|
107
|
+
'Warm neutrals carry the frame under a single rust accent, set in Geist. The subject is a toolkit for people who read documents and diffs as much as they run commands, so the voice is a proportional one and the page reads as prose. One accent carries every count, link, and primary action. Promoting a second and third into structural roles is what reads as a generated interface, so the palette stays at one.',
|
|
102
108
|
'',
|
|
103
|
-
'
|
|
109
|
+
'Monospace is a role the page uses rather than its voice. The `code` role takes it, and so does any terminal frame a surface embeds, since those show what the shell printed and matching the shell is what makes them legible. Every other role is Geist.',
|
|
104
110
|
].join('\n'),
|
|
105
111
|
|
|
106
112
|
colorNote: [
|
|
@@ -236,52 +242,62 @@ export const TOKENS: DesignTokens = {
|
|
|
236
242
|
],
|
|
237
243
|
|
|
238
244
|
typographyNote: [
|
|
239
|
-
'
|
|
245
|
+
'Seven sizes are on offer, named `t0` through `t6` and emitted as custom properties beside the roles. They are 3.175, 2.375, 1.375, 1.125, 0.9375, 0.8125 and 0.6875 rem, which paint at 50.8, 38, 22, 18, 15, 13 and 11 pixels, and each role takes one. A step answers what sizes exist and a role answers what a stylesheet asks for, so the two stay separate: a role whose step did not move renders as it did, and a role that moved is the only thing that changes what a surface paints. Two steps, `t3` at 18 pixels and `t6` at 11, are offered with no role pointing at them yet.',
|
|
240
246
|
'',
|
|
241
|
-
'
|
|
247
|
+
'Every role but `code` is Geist and `code` is monospace. The hero headline takes the largest step at 50.8 pixels, which no other role reaches, since a headline set at the display cap reads as an opening rather than as a section heading.',
|
|
242
248
|
'',
|
|
243
249
|
'A tagged cell is one no rendering surface exercises yet, which is a declaration the system has not tested rather than one it has.',
|
|
244
250
|
'',
|
|
245
251
|
'Two rules set tracking and no others touch it. The label role carries `0.05em`, and the display role tightens to `-0.01em`.',
|
|
246
252
|
].join('\n'),
|
|
247
253
|
|
|
254
|
+
typeScale: [
|
|
255
|
+
{ step: 't0', size: '3.175rem' },
|
|
256
|
+
{ step: 't1', size: '2.375rem' },
|
|
257
|
+
{ step: 't2', size: '1.375rem' },
|
|
258
|
+
{ step: 't3', size: '1.125rem' },
|
|
259
|
+
{ step: 't4', size: '0.9375rem' },
|
|
260
|
+
{ step: 't5', size: '0.8125rem' },
|
|
261
|
+
{ step: 't6', size: '0.6875rem' },
|
|
262
|
+
],
|
|
263
|
+
|
|
248
264
|
typography: [
|
|
249
265
|
{
|
|
250
266
|
role: 'display',
|
|
251
|
-
family:
|
|
267
|
+
family: SANS,
|
|
252
268
|
weight: '700',
|
|
253
|
-
size: '
|
|
269
|
+
size: '2.375rem',
|
|
254
270
|
lineHeight: '1.3',
|
|
255
271
|
},
|
|
256
272
|
{
|
|
257
273
|
role: 'page-display',
|
|
258
274
|
family: SANS,
|
|
259
275
|
weight: '700',
|
|
260
|
-
size: '
|
|
276
|
+
size: '3.175rem',
|
|
261
277
|
lineHeight: '1.1',
|
|
262
|
-
verify: ['
|
|
278
|
+
verify: ['size', 'lineHeight'],
|
|
263
279
|
},
|
|
264
280
|
{
|
|
265
281
|
role: 'heading',
|
|
266
|
-
family:
|
|
282
|
+
family: SANS,
|
|
267
283
|
weight: '700',
|
|
268
|
-
size: '
|
|
284
|
+
size: '1.375rem',
|
|
269
285
|
lineHeight: '1.3',
|
|
270
286
|
verify: ['lineHeight'],
|
|
271
287
|
},
|
|
272
288
|
{
|
|
273
289
|
role: 'body',
|
|
274
|
-
family:
|
|
290
|
+
family: SANS,
|
|
275
291
|
weight: '400',
|
|
276
|
-
size: '
|
|
292
|
+
size: '0.9375rem',
|
|
277
293
|
lineHeight: '1.65',
|
|
278
294
|
verify: ['weight'],
|
|
279
295
|
},
|
|
280
296
|
{
|
|
281
297
|
role: 'label',
|
|
282
|
-
family:
|
|
298
|
+
family: SANS,
|
|
283
299
|
weight: '400',
|
|
284
|
-
size: '
|
|
300
|
+
size: '0.8125rem',
|
|
285
301
|
lineHeight: '1.45',
|
|
286
302
|
verify: ['weight', 'lineHeight'],
|
|
287
303
|
},
|
|
@@ -289,27 +305,26 @@ export const TOKENS: DesignTokens = {
|
|
|
289
305
|
role: 'code',
|
|
290
306
|
family: MONO,
|
|
291
307
|
weight: '700',
|
|
292
|
-
size: '
|
|
308
|
+
size: '0.8125rem',
|
|
293
309
|
lineHeight: '1.3',
|
|
294
310
|
verify: ['lineHeight'],
|
|
295
311
|
},
|
|
296
312
|
],
|
|
297
313
|
|
|
298
314
|
spacingNote: [
|
|
299
|
-
'
|
|
315
|
+
'Seven steps run from a quarter rem to six, at 4, 8, 14, 24, 40, 64 and 96 pixels on a 16 pixel root. The scale is not a grid of one base, since the 14 between the 8 and the 24 is the step a control needs and no multiple of four supplies it, so the multiplier column counts quarter rems rather than claiming a unit.',
|
|
300
316
|
'',
|
|
301
|
-
'The outer window padding
|
|
317
|
+
'The outer window padding of the capture frames, which the previous record carried as a three-value frame register, is not part of the scale. Those frames set their own padding in the template and read no step, so the register is retired here rather than mapped onto steps no frame uses.',
|
|
302
318
|
].join('\n'),
|
|
303
319
|
|
|
304
320
|
spacing: [
|
|
305
|
-
{ step: 'xs', multiplier: '1', value: '
|
|
306
|
-
{ step: 'sm', multiplier: '2', value: '
|
|
307
|
-
{ step: 'md', multiplier: '3', value: '
|
|
308
|
-
{ step: 'lg', multiplier: '
|
|
309
|
-
{ step: 'xl', multiplier: '
|
|
310
|
-
{ step: '
|
|
311
|
-
{ step: '
|
|
312
|
-
{ step: 'frame-bottom', multiplier: 'none', value: '38px' },
|
|
321
|
+
{ step: 'xs', multiplier: '1', value: '0.25rem' },
|
|
322
|
+
{ step: 'sm', multiplier: '2', value: '0.5rem' },
|
|
323
|
+
{ step: 'md', multiplier: '3.5', value: '0.875rem' },
|
|
324
|
+
{ step: 'lg', multiplier: '6', value: '1.5rem' },
|
|
325
|
+
{ step: 'xl', multiplier: '10', value: '2.5rem' },
|
|
326
|
+
{ step: '2xl', multiplier: '16', value: '4rem' },
|
|
327
|
+
{ step: '3xl', multiplier: '24', value: '6rem' },
|
|
313
328
|
],
|
|
314
329
|
|
|
315
330
|
bordersNote: [
|
package/src/gate/measures.ts
CHANGED
|
@@ -142,7 +142,7 @@ export function auditsBaselineRel(root: string): string {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
export const CAPTURE_STAMP_FAILURE =
|
|
145
|
-
'A capture set disagrees with the stamp written when its image was captured. Run canon capture assets/captures --selector .window --out assets and commit each frame with its image and its stamp.'
|
|
145
|
+
'A capture set disagrees with the stamp written when its image was captured. Run canon capture assets/captures --selector .window --out assets/evidence and commit each frame with its image and its stamp.'
|
|
146
146
|
|
|
147
147
|
function parseJson(payload: string): unknown {
|
|
148
148
|
try {
|
|
@@ -1118,11 +1118,12 @@ async function collectPluginManifests(ctx: MeasureContext): Promise<string[]> {
|
|
|
1118
1118
|
/**
|
|
1119
1119
|
* A capture set spans two folders. The markup and the template beside it are
|
|
1120
1120
|
* the authored half and sit under `assets/captures/`, while the image a
|
|
1121
|
-
* document points at and the stamp answering for it
|
|
1122
|
-
* where `--out assets` sends them
|
|
1121
|
+
* document points at and the stamp answering for it sit under
|
|
1122
|
+
* `assets/evidence/`, which is where `--out assets/evidence` sends them and where
|
|
1123
|
+
* `canon pr evidence` looks for a changed image.
|
|
1123
1124
|
*/
|
|
1124
1125
|
const CAPTURE_MARKUP_DIR = 'assets/captures'
|
|
1125
|
-
const CAPTURE_OUTPUT_DIR = 'assets'
|
|
1126
|
+
const CAPTURE_OUTPUT_DIR = 'assets/evidence'
|
|
1126
1127
|
|
|
1127
1128
|
/**
|
|
1128
1129
|
* Every capture under `assets/captures/`, named by the base its three files
|
package/src/gate/stages.ts
CHANGED
|
@@ -201,7 +201,7 @@ export const STAGES: readonly Stage[] = [
|
|
|
201
201
|
kind: 'drift',
|
|
202
202
|
pathspec: 'assets/captures/*.html',
|
|
203
203
|
failure:
|
|
204
|
-
'A generated frame drifted from the catalogs or the design source. Run bun run check, then canon capture assets/captures --selector .window --out assets, and commit each assets/captures/*.html with its .png and .stamp.',
|
|
204
|
+
'A generated frame drifted from the catalogs or the design source. Run bun run check, then canon capture assets/captures --selector .window --out assets/evidence, and commit each assets/captures/*.html with its .png and .stamp.',
|
|
205
205
|
},
|
|
206
206
|
{ kind: 'measure', measure: captureStamps },
|
|
207
207
|
],
|
package/src/serve/static.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, realpathSync, statSync } from 'node:fs'
|
|
1
|
+
import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs'
|
|
2
2
|
import { join, resolve, sep } from 'node:path'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -81,6 +81,66 @@ export type ServeOutcome = ServeStarted | ServeRefused
|
|
|
81
81
|
export interface ServeOptions {
|
|
82
82
|
readonly port?: number
|
|
83
83
|
readonly entry?: string
|
|
84
|
+
/** Answers a directory request with no index page by listing it. Off by default. */
|
|
85
|
+
readonly index?: boolean
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function escapeHtml(text: string): string {
|
|
89
|
+
return text
|
|
90
|
+
.replaceAll('&', '&')
|
|
91
|
+
.replaceAll('<', '<')
|
|
92
|
+
.replaceAll('>', '>')
|
|
93
|
+
.replaceAll('"', '"')
|
|
94
|
+
.replaceAll("'", ''')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface ListingEntry {
|
|
98
|
+
readonly name: string
|
|
99
|
+
readonly isDirectory: boolean
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reads the entries a listing may name. Each one takes the same containment
|
|
104
|
+
* test a request for it would, since a listed name is itself a fact about a
|
|
105
|
+
* place outside the root. Dotfiles are hidden because a listing is for
|
|
106
|
+
* navigating pages, and a hidden file is still reachable by its URL.
|
|
107
|
+
*/
|
|
108
|
+
function listEntries(root: string, directory: string): ListingEntry[] {
|
|
109
|
+
return readdirSync(directory)
|
|
110
|
+
.filter((name) => !name.startsWith('.'))
|
|
111
|
+
.filter((name) => !escapesThroughLink(root, join(directory, name)))
|
|
112
|
+
.map((name) => ({
|
|
113
|
+
name,
|
|
114
|
+
isDirectory:
|
|
115
|
+
statSync(join(directory, name), {
|
|
116
|
+
throwIfNoEntry: false,
|
|
117
|
+
})?.isDirectory() ?? false,
|
|
118
|
+
}))
|
|
119
|
+
.sort(
|
|
120
|
+
(a, b) =>
|
|
121
|
+
Number(b.isDirectory) - Number(a.isDirectory) ||
|
|
122
|
+
a.name.localeCompare(b.name),
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function renderListing(
|
|
127
|
+
pathname: string,
|
|
128
|
+
entries: readonly ListingEntry[],
|
|
129
|
+
hasParent: boolean,
|
|
130
|
+
): string {
|
|
131
|
+
const rows = entries.map(({ name, isDirectory }) => {
|
|
132
|
+
const label = isDirectory ? `${name}/` : name
|
|
133
|
+
return `<li><a href="${escapeHtml(encodeURIComponent(name))}${isDirectory ? '/' : ''}">${escapeHtml(label)}</a></li>`
|
|
134
|
+
})
|
|
135
|
+
if (hasParent) rows.unshift('<li><a href="../">../</a></li>')
|
|
136
|
+
return `<!doctype html>
|
|
137
|
+
<meta charset="utf-8">
|
|
138
|
+
<title>Index of ${escapeHtml(pathname)}</title>
|
|
139
|
+
<h1>Index of ${escapeHtml(pathname)}</h1>
|
|
140
|
+
<ul>
|
|
141
|
+
${rows.join('\n')}
|
|
142
|
+
</ul>
|
|
143
|
+
`
|
|
84
144
|
}
|
|
85
145
|
|
|
86
146
|
function refuse(reason: ServeRefusal, detail: string): ServeRefused {
|
|
@@ -165,13 +225,14 @@ export function shouldWalkPast(error: unknown): boolean {
|
|
|
165
225
|
function listen(
|
|
166
226
|
root: string,
|
|
167
227
|
first: number,
|
|
228
|
+
index: boolean,
|
|
168
229
|
): { server: ReturnType<typeof Bun.serve>; port: number } | undefined {
|
|
169
230
|
for (let port = first; port < first + PORT_ATTEMPTS; port++) {
|
|
170
231
|
try {
|
|
171
232
|
const server = Bun.serve({
|
|
172
233
|
hostname: SERVE_HOST,
|
|
173
234
|
port,
|
|
174
|
-
fetch: (request) => respond(root, request),
|
|
235
|
+
fetch: (request) => respond(root, request, index),
|
|
175
236
|
})
|
|
176
237
|
/**
|
|
177
238
|
* The bound port rather than the requested one. Port 0 asks the OS to
|
|
@@ -196,6 +257,7 @@ function listen(
|
|
|
196
257
|
export async function respond(
|
|
197
258
|
root: string,
|
|
198
259
|
request: Request,
|
|
260
|
+
index = false,
|
|
199
261
|
): Promise<Response> {
|
|
200
262
|
const { pathname, search } = new URL(request.url)
|
|
201
263
|
const forbidden = () =>
|
|
@@ -229,7 +291,19 @@ export async function respond(
|
|
|
229
291
|
headers: { location: `${pathname}/${search}` },
|
|
230
292
|
})
|
|
231
293
|
}
|
|
232
|
-
|
|
294
|
+
const indexPage = join(path, DEFAULT_ENTRY)
|
|
295
|
+
if (index && !existsSync(indexPage)) {
|
|
296
|
+
return new Response(
|
|
297
|
+
renderListing(pathname, listEntries(root, path), path !== root),
|
|
298
|
+
{
|
|
299
|
+
headers: {
|
|
300
|
+
'content-type': 'text/html; charset=utf-8',
|
|
301
|
+
'cache-control': 'no-store',
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
path = indexPage
|
|
233
307
|
}
|
|
234
308
|
|
|
235
309
|
/**
|
|
@@ -291,7 +365,7 @@ export function startServer(
|
|
|
291
365
|
*/
|
|
292
366
|
let bound: ReturnType<typeof listen>
|
|
293
367
|
try {
|
|
294
|
-
bound = listen(root, first)
|
|
368
|
+
bound = listen(root, first, options.index ?? false)
|
|
295
369
|
} catch (error) {
|
|
296
370
|
const code = (error as NodeJS.ErrnoException).code ?? 'unknown'
|
|
297
371
|
return refuse(
|
|
@@ -307,14 +381,24 @@ export function startServer(
|
|
|
307
381
|
)
|
|
308
382
|
}
|
|
309
383
|
|
|
384
|
+
/**
|
|
385
|
+
* A listing answers `/` where no index page exists, so under the flag the
|
|
386
|
+
* default entry is the root itself. An explicit `--entry` keeps its own link
|
|
387
|
+
* and its own warning.
|
|
388
|
+
*/
|
|
389
|
+
const listsRoot =
|
|
390
|
+
(options.index ?? false) &&
|
|
391
|
+
options.entry === undefined &&
|
|
392
|
+
!existsSync(entryPath)
|
|
393
|
+
|
|
310
394
|
return {
|
|
311
395
|
ok: true,
|
|
312
396
|
root,
|
|
313
397
|
host: SERVE_HOST,
|
|
314
398
|
port: bound.port,
|
|
315
399
|
entry,
|
|
316
|
-
url: `http://${SERVE_HOST}:${bound.port}/${entry}`,
|
|
317
|
-
entryExists: existsSync(entryPath),
|
|
400
|
+
url: `http://${SERVE_HOST}:${bound.port}/${listsRoot ? '' : entry}`,
|
|
401
|
+
entryExists: listsRoot || existsSync(entryPath),
|
|
318
402
|
stop: async () => {
|
|
319
403
|
await bound.server.stop(true)
|
|
320
404
|
},
|
package/src/teach/render.ts
CHANGED
|
@@ -109,8 +109,8 @@ function renderBlock(
|
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
111
|
* Renders a lesson body's structural blocks through the same components
|
|
112
|
-
* `
|
|
113
|
-
*
|
|
112
|
+
* `lesson.test.tsx` composes as JSX, so a real lesson and the component test
|
|
113
|
+
* share one rendering mechanism. Every block past the first refusal
|
|
114
114
|
* goes unread, matching how a malformed JSON parse refuses the whole call.
|
|
115
115
|
*/
|
|
116
116
|
export function renderLessonBody(blocks: readonly unknown[]): RenderOutcome {
|
package/src/teach/workspace.ts
CHANGED
package/standards/ready.md
CHANGED
|
@@ -11,6 +11,8 @@ The folder is gitignored, and backed wherever a records remote is configured: `c
|
|
|
11
11
|
|
|
12
12
|
## Scope
|
|
13
13
|
|
|
14
|
+
The `draft-ready` skill owns the procedure that writes a ready folder, and this standard owns the shape it writes to.
|
|
15
|
+
|
|
14
16
|
Governs a ready folder under `.canon/ready/<nn>-<slug>/`: folder layout, ordinal naming, the overview's frontmatter, what the mirrored tree holds, the thin-plan contract that ships it, and the lifecycle from the live folder to the archive.
|
|
15
17
|
|
|
16
18
|
Does not govern:
|
package/standards/session.md
CHANGED
|
@@ -18,6 +18,7 @@ Does not govern:
|
|
|
18
18
|
- The task board the file sits beside, its filenames, and its archiving: `tasks.md`
|
|
19
19
|
- The transform from a branch name to the slug in the filename: `slug.md`
|
|
20
20
|
- The role-specific sections a caller adds over the core, which belong to that caller
|
|
21
|
+
- The note a plain session writes at `.canon/compact/<slug>.md`, whose shape lives in the `session-compact` skill's own reference
|
|
21
22
|
- Which memories a session captures before writing, which is the capture surface's own subject
|
|
22
23
|
|
|
23
24
|
## What a working session map looks like
|
|
@@ -13,6 +13,8 @@ export default defineConfig({
|
|
|
13
13
|
use: {
|
|
14
14
|
trace: 'on-first-retry',
|
|
15
15
|
baseURL,
|
|
16
|
+
// Nothing to wait out unless a test asserts motion. Opt back in per test with page.emulateMedia({ reducedMotion: 'no-preference' }).
|
|
17
|
+
reducedMotion: 'reduce',
|
|
16
18
|
},
|
|
17
19
|
projects: [
|
|
18
20
|
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
@@ -11,6 +11,8 @@ export default defineConfig({
|
|
|
11
11
|
use: {
|
|
12
12
|
trace: 'on-first-retry',
|
|
13
13
|
baseURL,
|
|
14
|
+
// Nothing to wait out unless a test asserts motion. Opt back in per test with page.emulateMedia({ reducedMotion: 'no-preference' }).
|
|
15
|
+
reducedMotion: 'reduce',
|
|
14
16
|
},
|
|
15
17
|
projects: [
|
|
16
18
|
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
@@ -13,6 +13,8 @@ export default defineConfig({
|
|
|
13
13
|
use: {
|
|
14
14
|
trace: 'on-first-retry',
|
|
15
15
|
baseURL,
|
|
16
|
+
// Nothing to wait out unless a test asserts motion. Opt back in per test with page.emulateMedia({ reducedMotion: 'no-preference' }).
|
|
17
|
+
reducedMotion: 'reduce',
|
|
16
18
|
},
|
|
17
19
|
projects: [
|
|
18
20
|
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource ./html */
|
|
2
|
-
import { mkdir, writeFile } from 'node:fs/promises'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
import { TEACH_STYLESHEET_COMPONENTS } from '@/design/components'
|
|
5
|
-
import { buildDesignCss } from '@/design/css'
|
|
6
|
-
import { TEACH_FONT_FACES } from '@/teach/fonts'
|
|
7
|
-
import { Heading } from '@/teach/components/heading'
|
|
8
|
-
import { List } from '@/teach/components/list'
|
|
9
|
-
import { Paragraph } from '@/teach/components/paragraph'
|
|
10
|
-
import { render } from '@/teach/html/jsx-runtime'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Regenerates the committed fixture lesson under
|
|
14
|
-
* `examples/teach/00-fixture/`, the same shape `examples/slides/showcase.md`
|
|
15
|
-
* takes against its own hand-rendered snapshot. Run with
|
|
16
|
-
* `bun src/teach/render-fixture.tsx` after a component changes shape.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const FIXTURE_ROOT = join(
|
|
20
|
-
import.meta.dir,
|
|
21
|
-
'..',
|
|
22
|
-
'..',
|
|
23
|
-
'examples',
|
|
24
|
-
'teach',
|
|
25
|
-
'00-fixture',
|
|
26
|
-
)
|
|
27
|
-
const LESSON_TITLE = 'Compass bearings'
|
|
28
|
-
const LESSON_FILE = '0001-compass-bearings.html'
|
|
29
|
-
|
|
30
|
-
const STEPS = [
|
|
31
|
-
'Point the direction-of-travel arrow at the landmark.',
|
|
32
|
-
'Rotate the bezel until the orienting arrow lines up with the needle.',
|
|
33
|
-
'Read the bearing where the direction-of-travel arrow meets the bezel.',
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
const body = (
|
|
37
|
-
<>
|
|
38
|
-
<Heading level={1}>{LESSON_TITLE}</Heading>
|
|
39
|
-
<Paragraph lede>
|
|
40
|
-
A bearing is the compass direction from where you stand to whatever you
|
|
41
|
-
are aiming at, measured clockwise from north.
|
|
42
|
-
</Paragraph>
|
|
43
|
-
<Paragraph>
|
|
44
|
-
Hold the compass level and let the needle settle before reading anything
|
|
45
|
-
off it. A bearing taken while walking or tilted reads confidently and
|
|
46
|
-
wrong.
|
|
47
|
-
</Paragraph>
|
|
48
|
-
<Paragraph>
|
|
49
|
-
Three steps turn a sighted landmark into a number you can act on:
|
|
50
|
-
</Paragraph>
|
|
51
|
-
<List ordered items={STEPS} />
|
|
52
|
-
<Paragraph>
|
|
53
|
-
The same three steps run in reverse turn a bearing on a map into a
|
|
54
|
-
direction to walk, which is the only reason to learn them in this order.
|
|
55
|
-
</Paragraph>
|
|
56
|
-
</>
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
function page(title: string, main: string): string {
|
|
60
|
-
return `<!doctype html>
|
|
61
|
-
<html lang="en">
|
|
62
|
-
<head>
|
|
63
|
-
<meta charset="utf-8">
|
|
64
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
65
|
-
<title>${title}</title>
|
|
66
|
-
<link rel="stylesheet" href="../assets/course.css">
|
|
67
|
-
</head>
|
|
68
|
-
<body>
|
|
69
|
-
<main class="wide-body">
|
|
70
|
-
${main}
|
|
71
|
-
</main>
|
|
72
|
-
</body>
|
|
73
|
-
</html>
|
|
74
|
-
`
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function main(): Promise<void> {
|
|
78
|
-
await mkdir(join(FIXTURE_ROOT, 'lessons'), { recursive: true })
|
|
79
|
-
await mkdir(join(FIXTURE_ROOT, 'assets'), { recursive: true })
|
|
80
|
-
|
|
81
|
-
await writeFile(
|
|
82
|
-
join(FIXTURE_ROOT, 'assets', 'course.css'),
|
|
83
|
-
buildDesignCss(undefined, {
|
|
84
|
-
embedFonts: TEACH_FONT_FACES,
|
|
85
|
-
components: TEACH_STYLESHEET_COMPONENTS,
|
|
86
|
-
}),
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
await writeFile(
|
|
90
|
-
join(FIXTURE_ROOT, 'lessons', LESSON_FILE),
|
|
91
|
-
page(LESSON_TITLE, render(body)),
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
await main()
|