@erclx/canon 4.74.0 → 4.75.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/create-skill/SKILL.md +0 -1
- package/claude/skills/docs-fold/SKILL.md +1 -1
- package/claude/skills/git-followup/SKILL.md +9 -5
- package/claude/skills/git-pr/SKILL.md +21 -0
- package/claude/skills/markdown-propose/REQUIREMENT.md +1 -1
- package/claude/skills/markdown-propose/SKILL.md +9 -7
- package/claude/skills/markdown-propose/references/format.md +5 -3
- package/claude/skills/plan-feature/SKILL.md +1 -1
- package/claude/skills/review-pr/SKILL.md +2 -2
- package/claude/skills/role-orchestrator/scripts/poll.sh +16 -10
- package/claude/skills/teach-workspace/references/lesson-craft.md +1 -6
- package/docs/agents/commands.md +1 -0
- package/docs/agents/index.md +1 -0
- package/docs/agents/pr-evidence.md +108 -0
- package/governance/rules/ui/440-surface-capture.md +1 -0
- package/package.json +1 -1
- package/src/commands/design.ts +10 -2
- package/src/commands/pr.ts +220 -0
- package/src/design/base.css +59 -0
- package/src/design/components.ts +80 -50
- package/src/design/fonts.ts +21 -0
- package/src/pr/evidence.ts +171 -0
- package/src/teach/fonts.ts +9 -11
- package/standards/architecture.md +1 -0
- package/standards/figures.md +51 -0
- package/standards/groundwork.md +1 -0
- package/standards/index.md +1 -0
- package/standards/intake.md +1 -0
- package/standards/skill.md +1 -1
package/src/commands/pr.ts
CHANGED
|
@@ -20,6 +20,11 @@ import {
|
|
|
20
20
|
treeRoots,
|
|
21
21
|
} from '@/pr/bijection'
|
|
22
22
|
import { type CheckRunListing, collapseChecks } from '@/pr/checks'
|
|
23
|
+
import {
|
|
24
|
+
findEvidenceCommentId,
|
|
25
|
+
groupEvidence,
|
|
26
|
+
renderEvidenceBody,
|
|
27
|
+
} from '@/pr/evidence'
|
|
23
28
|
import { type HeadRefusal, resolveHead, resolveTip } from '@/pr/head'
|
|
24
29
|
import { KEY_CHANGES } from '@/pr/paths'
|
|
25
30
|
import { type ReviewListing, resolveReviewScope } from '@/pr/review-scope'
|
|
@@ -81,6 +86,27 @@ const PULL_REFUSALS: Record<PullRefusal | HeadRefusal, string> = {
|
|
|
81
86
|
'The reviews on this pull request could not be read. An empty answer here would report a reviewed pull request as never reviewed, which routes the next pass to the whole change, so nothing is reported.',
|
|
82
87
|
}
|
|
83
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Why `canon pr evidence` produced no comment body, past the ones
|
|
91
|
+
* `readIdentity` already owns (`gh-missing`, `gh-failed`, `no-branch`) and the
|
|
92
|
+
* one `identity.head` owns (`no-object-head`).
|
|
93
|
+
*/
|
|
94
|
+
type EvidenceRefusal =
|
|
95
|
+
| 'gh-failed'
|
|
96
|
+
| 'no-base'
|
|
97
|
+
| 'unreadable-tree'
|
|
98
|
+
| 'unreadable-changes'
|
|
99
|
+
|
|
100
|
+
const EVIDENCE_REFUSALS: Record<EvidenceRefusal, string> = {
|
|
101
|
+
'gh-failed':
|
|
102
|
+
'gh could not answer for this repository. Name the pull request number.',
|
|
103
|
+
'no-base': 'No base resolves against the trunk. Fetch origin and re-run.',
|
|
104
|
+
'unreadable-tree':
|
|
105
|
+
'git could not read the tree at the base commit, so no path could be judged added or changed.',
|
|
106
|
+
'unreadable-changes':
|
|
107
|
+
'git could not list what this branch changed, so the set is unknown.',
|
|
108
|
+
}
|
|
109
|
+
|
|
84
110
|
/** Why the read produced no comparison, ahead of the ones the compare owns. */
|
|
85
111
|
type SourceRefusal =
|
|
86
112
|
| 'gh-missing'
|
|
@@ -289,6 +315,46 @@ export function register(program: Command): void {
|
|
|
289
315
|
.action(async (number: string | undefined, opts: ReadOptions) => {
|
|
290
316
|
process.exitCode = await runReviewState(number, opts)
|
|
291
317
|
})
|
|
318
|
+
|
|
319
|
+
pr.command('evidence')
|
|
320
|
+
.description(
|
|
321
|
+
"Render a before/after comparison for the pull request's changed evidence images",
|
|
322
|
+
)
|
|
323
|
+
.argument('[number]', 'Pull request to read, defaulting to this branch')
|
|
324
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
325
|
+
.option('--root <path>', 'Repository to read, defaulting to the cwd')
|
|
326
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
327
|
+
.addHelpText(
|
|
328
|
+
'after',
|
|
329
|
+
[
|
|
330
|
+
'',
|
|
331
|
+
'Compares the merge base with the trunk against the current head, never',
|
|
332
|
+
'the previous push against the new one, so the comment never claims more',
|
|
333
|
+
'than the branch currently shows. A path counts as evidence when one of',
|
|
334
|
+
'its segments is literally `evidence` and the filename carries an image',
|
|
335
|
+
'extension (png, jpg, jpeg, gif, webp, avif, svg). A README, a capture',
|
|
336
|
+
'script, or a raw data file kept beside the images is left out rather',
|
|
337
|
+
'than rendered as a broken embed.',
|
|
338
|
+
'',
|
|
339
|
+
'Read `reason` on the JSON record before posting anything:',
|
|
340
|
+
' ok a body was rendered, with `commentId` set when a marked',
|
|
341
|
+
' comment already exists and should be edited in place',
|
|
342
|
+
' no-evidence nothing in the diff carries an evidence/ segment, which',
|
|
343
|
+
' is an ordinary, silent no-op rather than a refusal',
|
|
344
|
+
'',
|
|
345
|
+
'Exit codes:',
|
|
346
|
+
' 0 read, whether it produced a body or reported no-evidence',
|
|
347
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
348
|
+
'',
|
|
349
|
+
'Examples:',
|
|
350
|
+
' canon pr evidence --json',
|
|
351
|
+
' canon pr evidence 1341 --json',
|
|
352
|
+
'',
|
|
353
|
+
].join('\n'),
|
|
354
|
+
)
|
|
355
|
+
.action(async (number: string | undefined, opts: ReadOptions) => {
|
|
356
|
+
process.exitCode = await runEvidence(number, opts)
|
|
357
|
+
})
|
|
292
358
|
}
|
|
293
359
|
|
|
294
360
|
interface PullRequestRead {
|
|
@@ -1019,6 +1085,160 @@ async function runReviewState(
|
|
|
1019
1085
|
return 0
|
|
1020
1086
|
}
|
|
1021
1087
|
|
|
1088
|
+
/**
|
|
1089
|
+
* Every path `git ls-tree` reports for `ref`, or undefined when the read
|
|
1090
|
+
* failed. One call for the whole tree rather than one `cat-file -e` per
|
|
1091
|
+
* evidence path, since existence at base is checked once per changed path and
|
|
1092
|
+
* a batch read is one round trip instead of many.
|
|
1093
|
+
*/
|
|
1094
|
+
async function listTreePaths(
|
|
1095
|
+
root: string,
|
|
1096
|
+
ref: string,
|
|
1097
|
+
): Promise<Set<string> | undefined> {
|
|
1098
|
+
const result = await $`git -C ${root} ls-tree -r --name-only ${ref}`
|
|
1099
|
+
.env(gitEnv())
|
|
1100
|
+
.quiet()
|
|
1101
|
+
.nothrow()
|
|
1102
|
+
if (result.exitCode !== 0) return undefined
|
|
1103
|
+
return new Set(result.text().split('\n').filter(Boolean))
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
async function runEvidence(
|
|
1107
|
+
number: string | undefined,
|
|
1108
|
+
opts: ReadOptions,
|
|
1109
|
+
): Promise<number> {
|
|
1110
|
+
const root = resolve(opts.root ?? process.cwd())
|
|
1111
|
+
const emitJson = opts.json ?? false
|
|
1112
|
+
|
|
1113
|
+
intro('canon pr evidence')
|
|
1114
|
+
|
|
1115
|
+
const read = await readIdentity(root, number)
|
|
1116
|
+
if (read.kind === 'refused') {
|
|
1117
|
+
return refuseWith(read.reason, PULL_REFUSALS[read.reason], emitJson, root)
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const { identity } = read
|
|
1121
|
+
if (identity.head === undefined) {
|
|
1122
|
+
return refuseWith(
|
|
1123
|
+
'no-object-head',
|
|
1124
|
+
PULL_REFUSALS['no-object-head'],
|
|
1125
|
+
emitJson,
|
|
1126
|
+
root,
|
|
1127
|
+
)
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const base = await resolveBaseRef(root)
|
|
1131
|
+
if (base === undefined) {
|
|
1132
|
+
return refuseWith('no-base', EVIDENCE_REFUSALS['no-base'], emitJson, root)
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const [changed, baseTree] = await Promise.all([
|
|
1136
|
+
listChangedFiles(root, base),
|
|
1137
|
+
listTreePaths(root, base),
|
|
1138
|
+
])
|
|
1139
|
+
if (changed === undefined) {
|
|
1140
|
+
return refuseWith(
|
|
1141
|
+
'unreadable-changes',
|
|
1142
|
+
EVIDENCE_REFUSALS['unreadable-changes'],
|
|
1143
|
+
emitJson,
|
|
1144
|
+
root,
|
|
1145
|
+
)
|
|
1146
|
+
}
|
|
1147
|
+
if (baseTree === undefined) {
|
|
1148
|
+
return refuseWith(
|
|
1149
|
+
'unreadable-tree',
|
|
1150
|
+
EVIDENCE_REFUSALS['unreadable-tree'],
|
|
1151
|
+
emitJson,
|
|
1152
|
+
root,
|
|
1153
|
+
)
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const grouped = await groupEvidence(changed, async (path) =>
|
|
1157
|
+
baseTree.has(path),
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
if (grouped.kind === 'refused') {
|
|
1161
|
+
logStep('Skipped')
|
|
1162
|
+
logInfo(
|
|
1163
|
+
'No changed path carries an evidence/ segment, so there is nothing to post.',
|
|
1164
|
+
)
|
|
1165
|
+
outro()
|
|
1166
|
+
if (emitJson) {
|
|
1167
|
+
process.stdout.write(
|
|
1168
|
+
`${JSON.stringify({
|
|
1169
|
+
root,
|
|
1170
|
+
...(identity.number !== undefined && { number: identity.number }),
|
|
1171
|
+
reason: 'no-evidence',
|
|
1172
|
+
})}\n`,
|
|
1173
|
+
)
|
|
1174
|
+
}
|
|
1175
|
+
return 0
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const repoRow = await gh(root, ['repo', 'view', '--json', 'nameWithOwner'])
|
|
1179
|
+
const repo =
|
|
1180
|
+
repoRow === null
|
|
1181
|
+
? undefined
|
|
1182
|
+
: (JSON.parse(repoRow) as { nameWithOwner?: string }).nameWithOwner
|
|
1183
|
+
|
|
1184
|
+
if (repo === undefined) {
|
|
1185
|
+
return refuseWith(
|
|
1186
|
+
'gh-failed',
|
|
1187
|
+
EVIDENCE_REFUSALS['gh-failed'],
|
|
1188
|
+
emitJson,
|
|
1189
|
+
root,
|
|
1190
|
+
)
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const body = renderEvidenceBody(grouped.states, repo, base, identity.head)
|
|
1194
|
+
|
|
1195
|
+
let commentId: number | undefined
|
|
1196
|
+
if (identity.number !== undefined) {
|
|
1197
|
+
const commentsRow = await gh(root, [
|
|
1198
|
+
'pr',
|
|
1199
|
+
'view',
|
|
1200
|
+
String(identity.number),
|
|
1201
|
+
'--json',
|
|
1202
|
+
'comments',
|
|
1203
|
+
])
|
|
1204
|
+
if (commentsRow !== null) {
|
|
1205
|
+
try {
|
|
1206
|
+
const parsed = JSON.parse(commentsRow) as {
|
|
1207
|
+
comments?: readonly { url?: string; body: string }[]
|
|
1208
|
+
}
|
|
1209
|
+
commentId = findEvidenceCommentId(parsed.comments ?? [])
|
|
1210
|
+
} catch {
|
|
1211
|
+
commentId = undefined
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const caseCount = grouped.states.reduce((n, s) => n + s.items.length, 0)
|
|
1217
|
+
|
|
1218
|
+
logStep('Scope')
|
|
1219
|
+
logInfo(
|
|
1220
|
+
`${plural(caseCount, 'case')} across ${plural(grouped.states.length, 'state')}`,
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
outro()
|
|
1224
|
+
|
|
1225
|
+
if (emitJson) {
|
|
1226
|
+
process.stdout.write(
|
|
1227
|
+
`${JSON.stringify({
|
|
1228
|
+
root,
|
|
1229
|
+
...(identity.number !== undefined && { number: identity.number }),
|
|
1230
|
+
reason: 'ok',
|
|
1231
|
+
base,
|
|
1232
|
+
head: identity.head,
|
|
1233
|
+
body,
|
|
1234
|
+
...(commentId !== undefined && { commentId }),
|
|
1235
|
+
})}\n`,
|
|
1236
|
+
)
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
return 0
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1022
1242
|
/**
|
|
1023
1243
|
* Frames a refusal on stderr in both modes and puts the record on stdout alone,
|
|
1024
1244
|
* so an operator reading the terminal sees the reason rather than a command
|
package/src/design/base.css
CHANGED
|
@@ -136,3 +136,62 @@
|
|
|
136
136
|
*::-webkit-scrollbar-corner {
|
|
137
137
|
background: transparent;
|
|
138
138
|
}
|
|
139
|
+
|
|
140
|
+
/* hand-drawn-figure
|
|
141
|
+
The hand-drawn SVG diagram figure and its caption, sized wider than the
|
|
142
|
+
reading measure and cleared past an outline rail on a wide viewport. */
|
|
143
|
+
|
|
144
|
+
/* ---- Figures: hand-drawn, and wider than the measure ---- */
|
|
145
|
+
|
|
146
|
+
:root {
|
|
147
|
+
--figure-hand: 'Virgil', 'Excalifont', cursive;
|
|
148
|
+
--figure-wide: 64rem;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
figure {
|
|
152
|
+
margin: 2.75rem 0;
|
|
153
|
+
width: var(--figure-wide);
|
|
154
|
+
max-width: 92vw;
|
|
155
|
+
margin-left: 50%;
|
|
156
|
+
transform: translateX(-50%);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
figure svg {
|
|
160
|
+
width: 100%;
|
|
161
|
+
height: auto;
|
|
162
|
+
display: block;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* CSS beats an SVG presentation attribute, so the diagrams pick up the
|
|
166
|
+
hand face without editing a single lesson. */
|
|
167
|
+
figure svg text {
|
|
168
|
+
font-family: var(--figure-hand);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
figcaption {
|
|
172
|
+
font-family: var(--figure-hand);
|
|
173
|
+
font-size: 1rem;
|
|
174
|
+
color: var(--color-muted);
|
|
175
|
+
margin: 1rem auto 0;
|
|
176
|
+
line-height: 1.5;
|
|
177
|
+
max-width: 52rem;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* A figure breaks the measure and must still clear an outline rail. Deriving
|
|
181
|
+
the ceiling from the viewport does not hold, since a rail positioned from
|
|
182
|
+
the centre keeps overlapping as the window widens, so the ceiling is fixed
|
|
183
|
+
instead. */
|
|
184
|
+
@media (min-width: 1421px) {
|
|
185
|
+
figure {
|
|
186
|
+
max-width: 54rem;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@media (max-width: 640px) {
|
|
191
|
+
figure {
|
|
192
|
+
width: 100%;
|
|
193
|
+
max-width: 100%;
|
|
194
|
+
margin-left: 0;
|
|
195
|
+
transform: none;
|
|
196
|
+
}
|
|
197
|
+
}
|
package/src/design/components.ts
CHANGED
|
@@ -119,7 +119,86 @@ const SCROLLBAR: Component = {
|
|
|
119
119
|
}`,
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
/**
|
|
123
|
+
* The hand-drawn SVG diagram figure and its caption, sized wider than the
|
|
124
|
+
* reading measure and cleared past an outline rail on a wide viewport.
|
|
125
|
+
* Promoted out of `TEACH_COMPONENTS` per
|
|
126
|
+
* `.canon/groundwork/86-portable-hand-drawn-figures/06-decision.md` item 4:
|
|
127
|
+
* any project can draw one hand-drawn-styled figure without teach's own quiz
|
|
128
|
+
* and schedule machinery. It could not move as `TEACH_FIGURES` as-is, since
|
|
129
|
+
* that component's `reads` list named `--teach-hand` and `--teach-wide`
|
|
130
|
+
* while only `TEACH_CHROME` declared them, so this component declares its
|
|
131
|
+
* own `--figure-hand` and `--figure-wide` defaults rather than borrowing
|
|
132
|
+
* undeclared custom properties from a teach-only sibling.
|
|
133
|
+
*/
|
|
134
|
+
const HAND_DRAWN_FIGURE: Component = {
|
|
135
|
+
name: 'hand-drawn-figure',
|
|
136
|
+
note: [
|
|
137
|
+
'The hand-drawn SVG diagram figure and its caption, sized wider than the',
|
|
138
|
+
'reading measure and cleared past an outline rail on a wide viewport.',
|
|
139
|
+
].join('\n '),
|
|
140
|
+
reads: ['--color-muted', '--figure-hand', '--figure-wide'],
|
|
141
|
+
rules: `/* ---- Figures: hand-drawn, and wider than the measure ---- */
|
|
142
|
+
|
|
143
|
+
:root {
|
|
144
|
+
--figure-hand: 'Virgil', 'Excalifont', cursive;
|
|
145
|
+
--figure-wide: 64rem;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
figure {
|
|
149
|
+
margin: 2.75rem 0;
|
|
150
|
+
width: var(--figure-wide);
|
|
151
|
+
max-width: 92vw;
|
|
152
|
+
margin-left: 50%;
|
|
153
|
+
transform: translateX(-50%);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
figure svg {
|
|
157
|
+
width: 100%;
|
|
158
|
+
height: auto;
|
|
159
|
+
display: block;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* CSS beats an SVG presentation attribute, so the diagrams pick up the
|
|
163
|
+
hand face without editing a single lesson. */
|
|
164
|
+
figure svg text {
|
|
165
|
+
font-family: var(--figure-hand);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
figcaption {
|
|
169
|
+
font-family: var(--figure-hand);
|
|
170
|
+
font-size: 1rem;
|
|
171
|
+
color: var(--color-muted);
|
|
172
|
+
margin: 1rem auto 0;
|
|
173
|
+
line-height: 1.5;
|
|
174
|
+
max-width: 52rem;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* A figure breaks the measure and must still clear an outline rail. Deriving
|
|
178
|
+
the ceiling from the viewport does not hold, since a rail positioned from
|
|
179
|
+
the centre keeps overlapping as the window widens, so the ceiling is fixed
|
|
180
|
+
instead. */
|
|
181
|
+
@media (min-width: 1421px) {
|
|
182
|
+
figure {
|
|
183
|
+
max-width: 54rem;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@media (max-width: 640px) {
|
|
188
|
+
figure {
|
|
189
|
+
width: 100%;
|
|
190
|
+
max-width: 100%;
|
|
191
|
+
margin-left: 0;
|
|
192
|
+
transform: none;
|
|
193
|
+
}
|
|
194
|
+
}`,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const COMPONENTS: readonly Component[] = [
|
|
198
|
+
STATUS,
|
|
199
|
+
SCROLLBAR,
|
|
200
|
+
HAND_DRAWN_FIGURE,
|
|
201
|
+
]
|
|
123
202
|
|
|
124
203
|
const TEACH_CHROME: Component = {
|
|
125
204
|
name: 'teach-chrome',
|
|
@@ -153,7 +232,6 @@ const TEACH_CHROME: Component = {
|
|
|
153
232
|
--teach-mono: 'Cascadia Code', ui-monospace, monospace;
|
|
154
233
|
--teach-measure: 52rem;
|
|
155
234
|
--teach-chrome: 52rem;
|
|
156
|
-
--teach-wide: 64rem;
|
|
157
235
|
--teach-mast-h: 4.4rem;
|
|
158
236
|
--teach-shadow: 0 1px 2px rgba(20, 20, 20, 0.04);
|
|
159
237
|
--color-teach-accent-bg: color-mix(in srgb, var(--color-accent) 14%, var(--color-background));
|
|
@@ -1126,53 +1204,6 @@ const TEACH_OUTLINE: Component = {
|
|
|
1126
1204
|
@media (max-width: 1420px) { .outline { display: none; } }`,
|
|
1127
1205
|
}
|
|
1128
1206
|
|
|
1129
|
-
const TEACH_FIGURES: Component = {
|
|
1130
|
-
name: 'teach-figures',
|
|
1131
|
-
note: [
|
|
1132
|
-
'The hand-drawn SVG diagram figure and its caption, sized wider than the',
|
|
1133
|
-
'reading measure and cleared past the outline rail on a wide viewport.',
|
|
1134
|
-
].join('\n '),
|
|
1135
|
-
reads: ['--color-muted', '--teach-hand', '--teach-measure', '--teach-wide'],
|
|
1136
|
-
rules: `/* ---- Figures: hand-drawn, and wider than the measure ---- */
|
|
1137
|
-
|
|
1138
|
-
figure {
|
|
1139
|
-
margin: 2.75rem 0;
|
|
1140
|
-
width: var(--teach-wide);
|
|
1141
|
-
max-width: 92vw;
|
|
1142
|
-
margin-left: 50%;
|
|
1143
|
-
transform: translateX(-50%);
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
figure svg { width: 100%; height: auto; display: block; }
|
|
1147
|
-
|
|
1148
|
-
/* CSS beats an SVG presentation attribute, so the diagrams pick up the
|
|
1149
|
-
hand face without editing a single lesson. */
|
|
1150
|
-
figure svg text { font-family: var(--teach-hand); }
|
|
1151
|
-
|
|
1152
|
-
figcaption {
|
|
1153
|
-
font-family: var(--teach-hand);
|
|
1154
|
-
font-size: 1rem;
|
|
1155
|
-
color: var(--color-muted);
|
|
1156
|
-
margin: 1rem auto 0;
|
|
1157
|
-
line-height: 1.5;
|
|
1158
|
-
max-width: var(--teach-measure);
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
/* A figure breaks the measure and must still clear the outline rail. The rail
|
|
1162
|
-
starts at \`50% + 26rem + 2.5rem\`, so a figure centred on the same axis may
|
|
1163
|
-
reach 27rem from centre and no further, which is 54rem wide with a 1.5rem
|
|
1164
|
-
gap left over. Deriving it from the viewport was the earlier attempt and it
|
|
1165
|
-
does not hold: the rail is positioned from the centre, not from the edge, so
|
|
1166
|
-
a wider window moved both and kept the overlap. */
|
|
1167
|
-
@media (min-width: 1421px) {
|
|
1168
|
-
figure { max-width: 54rem; }
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
@media (max-width: 640px) {
|
|
1172
|
-
figure { width: 100%; max-width: 100%; margin-left: 0; transform: none; }
|
|
1173
|
-
}`,
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
1207
|
const TEACH_REFERENCES: Component = {
|
|
1177
1208
|
name: 'teach-references',
|
|
1178
1209
|
note: [
|
|
@@ -1269,7 +1300,6 @@ export const TEACH_COMPONENTS: readonly Component[] = [
|
|
|
1269
1300
|
TEACH_QUIZ,
|
|
1270
1301
|
TEACH_GLOSSARY,
|
|
1271
1302
|
TEACH_OUTLINE,
|
|
1272
|
-
TEACH_FIGURES,
|
|
1273
1303
|
TEACH_REFERENCES,
|
|
1274
1304
|
]
|
|
1275
1305
|
|