@erclx/canon 4.75.1 → 4.77.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/draft-and-pick/SKILL.md +3 -3
- package/claude/skills/draft-figure/REQUIREMENT.md +42 -0
- package/claude/skills/draft-figure/SKILL.md +93 -0
- package/docs/agents/commands.md +1 -0
- package/docs/agents/design-board.md +31 -0
- package/docs/agents/index.md +1 -0
- package/docs/workflow/ai-workflow.md +18 -17
- package/package.json +2 -1
- package/scripts/core/check-gallery-exclusion.sh +50 -0
- package/src/claude/cases/workflow.ts +5 -0
- package/src/cli.ts +2 -1
- package/src/commands/design.ts +46 -1
- package/src/design/board.ts +475 -0
- package/src/gate/measures.ts +4 -2
- package/src/shipped/references.ts +41 -7
- package/src/teach/components/heading.tsx +11 -0
- package/src/teach/components/list.tsx +17 -0
- package/src/teach/components/paragraph.tsx +12 -0
- package/src/teach/html/jsx-dev-runtime.ts +13 -0
- package/src/teach/html/jsx-runtime.ts +113 -0
- package/src/teach/html/raw.ts +28 -0
- package/src/teach/render-fixture.tsx +95 -0
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { escape, isRaw, raw, type RawMarkup } from './raw'
|
|
2
|
+
|
|
3
|
+
export type Child =
|
|
4
|
+
| string
|
|
5
|
+
| number
|
|
6
|
+
| boolean
|
|
7
|
+
| null
|
|
8
|
+
| undefined
|
|
9
|
+
| RawMarkup
|
|
10
|
+
| readonly Child[]
|
|
11
|
+
|
|
12
|
+
export interface Props {
|
|
13
|
+
readonly children?: Child
|
|
14
|
+
readonly [key: string]: unknown
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* What a component returns and what `jsx` produces: markup already rendered,
|
|
19
|
+
* carrying the same brand `raw` gives an author's own escape hatch. One brand
|
|
20
|
+
* for both is what lets a parent's `{children}` compose a child's output
|
|
21
|
+
* without escaping it a second time, since a plain string cannot say whether
|
|
22
|
+
* it is author text or already-rendered markup.
|
|
23
|
+
*/
|
|
24
|
+
export type Element = RawMarkup
|
|
25
|
+
|
|
26
|
+
export type Component<P extends Props = Props> = (props: P) => Element
|
|
27
|
+
|
|
28
|
+
export const Fragment = Symbol('Fragment')
|
|
29
|
+
|
|
30
|
+
const ARIA_PREFIX = 'aria-'
|
|
31
|
+
|
|
32
|
+
const VOID_ELEMENTS = new Set([
|
|
33
|
+
'area',
|
|
34
|
+
'base',
|
|
35
|
+
'br',
|
|
36
|
+
'col',
|
|
37
|
+
'embed',
|
|
38
|
+
'hr',
|
|
39
|
+
'img',
|
|
40
|
+
'input',
|
|
41
|
+
'link',
|
|
42
|
+
'meta',
|
|
43
|
+
'source',
|
|
44
|
+
'track',
|
|
45
|
+
'wbr',
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
function renderChild(child: Child): string {
|
|
49
|
+
if (child === null || child === undefined || typeof child === 'boolean') {
|
|
50
|
+
return ''
|
|
51
|
+
}
|
|
52
|
+
if (typeof child === 'number') return String(child)
|
|
53
|
+
if (typeof child === 'string') return escape(child)
|
|
54
|
+
if (isRaw(child)) return child.html
|
|
55
|
+
return child.map(renderChild).join('')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* ARIA takes the literal word for a boolean, where HTML boolean attributes
|
|
60
|
+
* take presence or absence. Spike 1's first draft rendered a bare
|
|
61
|
+
* `aria-disabled` for `true` and dropped `aria-disabled={false}` entirely,
|
|
62
|
+
* silently removing state a screen reader reads.
|
|
63
|
+
*/
|
|
64
|
+
function renderAttribute(key: string, value: unknown): string {
|
|
65
|
+
if (key.startsWith(ARIA_PREFIX) && typeof value === 'boolean') {
|
|
66
|
+
return ` ${key}="${value}"`
|
|
67
|
+
}
|
|
68
|
+
if (value === undefined || value === null || value === false) return ''
|
|
69
|
+
if (value === true) return ` ${key}`
|
|
70
|
+
if (isRaw(value)) return ` ${key}="${escape(value.html)}"`
|
|
71
|
+
return ` ${key}="${escape(String(value))}"`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderAttributes(props: Props): string {
|
|
75
|
+
return Object.entries(props)
|
|
76
|
+
.filter(([key]) => key !== 'children')
|
|
77
|
+
.map(([key, value]) => renderAttribute(key, value))
|
|
78
|
+
.join('')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function jsx(
|
|
82
|
+
type: string | Component | typeof Fragment,
|
|
83
|
+
props: Props,
|
|
84
|
+
): Element {
|
|
85
|
+
if (type === Fragment) return raw(renderChild(props.children ?? null))
|
|
86
|
+
if (typeof type === 'function') return type(props)
|
|
87
|
+
|
|
88
|
+
const attrs = renderAttributes(props)
|
|
89
|
+
if (VOID_ELEMENTS.has(type)) return raw(`<${type}${attrs}>`)
|
|
90
|
+
|
|
91
|
+
return raw(
|
|
92
|
+
`<${type}${attrs}>${renderChild(props.children ?? null)}</${type}>`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const jsxs = jsx
|
|
97
|
+
|
|
98
|
+
/** Unwraps a rendered element to the plain string a file writes to disk. */
|
|
99
|
+
export function render(element: Element): string {
|
|
100
|
+
return element.html
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export namespace JSX {
|
|
104
|
+
export type Element = RawMarkup
|
|
105
|
+
|
|
106
|
+
export interface ElementChildrenAttribute {
|
|
107
|
+
children: Record<string, never>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface IntrinsicElements {
|
|
111
|
+
[name: string]: Props
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const RAW = Symbol('raw')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Markup already rendered, exempt from the escaping every other string
|
|
5
|
+
* receives. `raw` is the only way to produce one, so a lesson has to opt in
|
|
6
|
+
* explicitly rather than an author text string accidentally passing through
|
|
7
|
+
* unescaped.
|
|
8
|
+
*/
|
|
9
|
+
export interface RawMarkup {
|
|
10
|
+
readonly [RAW]: true
|
|
11
|
+
readonly html: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function raw(html: string): RawMarkup {
|
|
15
|
+
return { [RAW]: true, html }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isRaw(value: unknown): value is RawMarkup {
|
|
19
|
+
return typeof value === 'object' && value !== null && RAW in value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function escape(value: string): string {
|
|
23
|
+
return value
|
|
24
|
+
.replace(/&/g, '&')
|
|
25
|
+
.replace(/</g, '<')
|
|
26
|
+
.replace(/>/g, '>')
|
|
27
|
+
.replace(/"/g, '"')
|
|
28
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
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()
|