@erclx/canon 4.110.0 → 4.111.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/canon-operator/REQUIREMENT.md +2 -2
- package/claude/skills/canon-operator/SKILL.md +3 -4
- package/claude/skills/target-check/references/domains.md +2 -2
- package/docs/agents/install-and-sync.md +4 -13
- package/docs/target-projects.md +0 -2
- package/docs/workflow/ai-workflow.md +7 -11
- package/package.json +1 -1
- package/scripts/core/capture-home.sh +59 -0
- package/src/claude/cases/setup.ts +2 -23
- package/src/commands/sync.ts +0 -16
- package/src/design/components.ts +85 -65
- package/src/shipped/references.ts +1 -2
- package/src/sync/reverse.ts +3 -74
- package/src/teach/nav.ts +4 -2
- package/claude/skills/migration-claude-md/REQUIREMENT.md +0 -40
- package/claude/skills/migration-claude-md/SKILL.md +0 -79
- package/claude/skills/migration-context/REQUIREMENT.md +0 -36
- package/claude/skills/migration-context/SKILL.md +0 -97
- package/claude/skills/migration-standards-drop/REQUIREMENT.md +0 -55
- package/claude/skills/migration-standards-drop/SKILL.md +0 -113
- package/claude/skills/migration-superseded/REQUIREMENT.md +0 -44
- package/claude/skills/migration-superseded/SKILL.md +0 -115
|
@@ -26,8 +26,8 @@ The last failure is a section no route reaches. `## Route` maps an intent or a d
|
|
|
26
26
|
- Resolve every stack, rule, snippet, and standard name from a catalog at runtime
|
|
27
27
|
- Run the CLI non-interactively and report the command run, what changed, and the full path of anything written
|
|
28
28
|
- Read the overwrite contract from the body before any install or sync, and name each surface it lists as overwritten before running one
|
|
29
|
-
- Read the reverse walk beside the forward sections, so a folder the toolkit stopped shipping
|
|
30
|
-
-
|
|
29
|
+
- Read the reverse walk beside the forward sections, so a folder the toolkit stopped shipping reaches the user
|
|
30
|
+
- Name an unmigrated domain's root and install path for the user to move themselves, since no skill covers the move
|
|
31
31
|
- Route a measurement intent to the audit offers from the route table itself, rather than leaving the section reachable only by reading past the table
|
|
32
32
|
- Rank a lifecycle row against the audit offers where the route table itself states it, so a session acting on the lifecycle row reads the ranking without opening the section being ranked
|
|
33
33
|
- Offer every audit whose surface the target carries, and offer none whose surface it lacks
|
|
@@ -25,10 +25,10 @@ Run `canon sync --check . --json` before routing. It reports what a target is be
|
|
|
25
25
|
Read seven fields off the report and carry each to `## Route`:
|
|
26
26
|
|
|
27
27
|
- `unmigrated`: a domain sitting at the root layout with nothing under `.claude/`. The most urgent finding, because that domain reports no drift of its own while being entirely behind
|
|
28
|
-
- `superseded`: a file a newer seed folder replaced. No command moves it, since the content is the project's own, so this routes to
|
|
28
|
+
- `superseded`: a file a newer seed folder replaced. No command moves it, since the content is the project's own, so this routes to `canon:target-check` for the full per-domain reading rather than ending at the report
|
|
29
29
|
- `seeds`: entries are `matching`, `stale`, `drifted`, or `missing`. Anything but `matching` needs the seed handoff
|
|
30
30
|
- `domains[].entries`: per-file `stale`, `customized`, `stranded`, and `orphaned` as before
|
|
31
|
-
- `reverse`: what the target holds that no live catalog claims. `unclaimed` lists folders at roots the toolkit stopped shipping, each with an `attribution` of `dropped`, `unattributed`, or `project`. Act on the first two and leave `project` alone, which history proved the project owns
|
|
31
|
+
- `reverse`: what the target holds that no live catalog claims. `unclaimed` lists folders at roots the toolkit stopped shipping, each with an `attribution` of `dropped`, `unattributed`, or `project`. Act on the first two and leave `project` alone, which history proved the project owns
|
|
32
32
|
- `historyUnavailable` on a domain, on `seeds`, or on `reverse`: nothing could be dated, so treat every difference as unverified and say so rather than reporting a file as untouched. It is set on `reverse` when the toolkit itself ships without history, which is the registry-install case, so the walk found nothing rather than finding a clean target
|
|
33
33
|
- `tooling`: read `measured` first. Every count under it is zero when it is false, which is an absence of measurement rather than a measured zero. Past that, `chain` names the stacks the install resolved, nearest first, and `counts.gitignore` counts the managed ignore entries the target is missing.
|
|
34
34
|
|
|
@@ -43,8 +43,7 @@ Map the stated intent, or what `## Diagnose` found, to one lifecycle phase, then
|
|
|
43
43
|
The two can name different rows, and a reply answers both rather than picking one. Run or hand off what the finding names, then answer the intent in the same reply. A measurement intent therefore reaches the audit offers below even on a target whose diagnostic found scaffold work, which is the case a session leaving from the scaffold row would otherwise never read.
|
|
44
44
|
|
|
45
45
|
- A domain in `unmigrated`: name it, the root path, and the install path. No command moves it, so the user runs the relocation themselves
|
|
46
|
-
-
|
|
47
|
-
- Anything in `superseded`: name which files and what replaced them, then hand off to `migration-superseded`. Both skills propose, so neither moves nor deletes the file
|
|
46
|
+
- Anything in `superseded`: name which files and what replaced them, then hand off to `canon:target-check`, which reports and never moves or deletes the file
|
|
48
47
|
- A folder in `unclaimed`: name it and the attribution it carries. No command moves it and the content may be the project's own, so the decision is the user's
|
|
49
48
|
|
|
50
49
|
- First-time scaffold of a fresh project: hand off to `target-setup`
|
|
@@ -25,7 +25,7 @@ Read three fields off the same report, then two verbs:
|
|
|
25
25
|
|
|
26
26
|
- `unmigrated`: a domain still at the root layout with nothing under `.claude/`. The most urgent finding, because such a domain reports no drift of its own while being entirely behind.
|
|
27
27
|
- `superseded`: a file a newer seed folder replaced. No command moves it, since the content is the project's own.
|
|
28
|
-
- `reverse`: `unclaimed` names folders at roots the toolkit stopped shipping, each with an `attribution` of `dropped`, `unattributed`, or `project`.
|
|
28
|
+
- `reverse`: `unclaimed` names folders at roots the toolkit stopped shipping, each with an `attribution` of `dropped`, `unattributed`, or `project`.
|
|
29
29
|
- `canon context audit --json`: required sections, entry length, citations, and index drift across the context folder.
|
|
30
30
|
- `canon records validate <kind> --json`, once per record folder the target carries.
|
|
31
31
|
|
|
@@ -33,7 +33,7 @@ Read three fields off the same report, then two verbs:
|
|
|
33
33
|
- Behind: any of those carrying an entry. Name the entry and its attribution.
|
|
34
34
|
- Unread: `historyUnavailable` set on `reverse`, a `reverse` key the report does not carry at all, or either verb refusing. A missing `reverse` key is a binary predating the field rather than a target with nothing unclaimed.
|
|
35
35
|
|
|
36
|
-
Routes by finding. `unmigrated` routes to a relocation the user runs, since no command moves it. `superseded` routes to
|
|
36
|
+
Routes by finding. `unmigrated` routes to a relocation the user runs, since no command moves it. `superseded` routes to a split the user makes, since no command moves the content and no skill proposes it. An `unclaimed` folder carrying `project` attribution routes nowhere, since history proved the project owns it.
|
|
37
37
|
|
|
38
38
|
## 3. Governance rules
|
|
39
39
|
|
|
@@ -256,11 +256,9 @@ seed tree rather than from a fixed set of filenames, so a folder added later is
|
|
|
256
256
|
covered without a code change. Only an exact stem matches, which leaves a
|
|
257
257
|
suffixed variant such as `TASKS-ARCHIVE.md` unreported.
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
`
|
|
261
|
-
destination shape
|
|
262
|
-
writing. Where a folder has no governing standard, the entry earns a named
|
|
263
|
-
refusal rather than a shape nobody stated.
|
|
259
|
+
No skill splits the file. The standard governing `replacedBy` is the one the
|
|
260
|
+
`appliesTo` the standards catalog declares, and `canon standards <name>` reads
|
|
261
|
+
its destination shape, so the split is the project's own to make.
|
|
264
262
|
|
|
265
263
|
`unmigrated` names a domain sitting at the root layout an older toolkit installed
|
|
266
264
|
to, with nothing at the path the current one reads. It carries `rootPath`,
|
|
@@ -350,7 +348,7 @@ check.
|
|
|
350
348
|
`reverse` is the one section built by walking the target rather than the
|
|
351
349
|
catalog. Every other surface enumerates toolkit-owned keys and asks whether the
|
|
352
350
|
target matches, so a folder the toolkit deleted appears in none of them. It
|
|
353
|
-
carries `unclaimed
|
|
351
|
+
carries `unclaimed` and `historyUnavailable`.
|
|
354
352
|
|
|
355
353
|
`unclaimed` names a folder the target holds at a top-level path the toolkit once
|
|
356
354
|
shipped and has since deleted. The candidate roots come from the toolkit's own
|
|
@@ -381,13 +379,6 @@ colliding on a retired name costs the walk no reads. The cost is that a file the
|
|
|
381
379
|
toolkit shipped and the target renamed goes unmatched, the same limit the
|
|
382
380
|
`unmigrated` count carries.
|
|
383
381
|
|
|
384
|
-
`migrations` names a proposal-only skill with a live case in this target. It
|
|
385
|
-
fires on a `CLAUDE.md` past 250 lines for `migration-claude-md`, and on a `docs/` folder
|
|
386
|
-
holding markdown with no populated `canon/context/` for `migration-context`.
|
|
387
|
-
Each entry carries the skill name and the measurement behind it, so a consumer
|
|
388
|
-
can check the proposal before running it. Without the field both skills are
|
|
389
|
-
documented and unreachable from any report.
|
|
390
|
-
|
|
391
382
|
#### What counts toward the gate
|
|
392
383
|
|
|
393
384
|
`unmigrated` counts toward `--exit-code`, since running the relocation closes it.
|
package/docs/target-projects.md
CHANGED
|
@@ -261,8 +261,6 @@ Each entry carries a verdict, since a dropped folder and one the project wrote a
|
|
|
261
261
|
|
|
262
262
|
Nothing acts on any of them, and the verdict is what makes the list safe to read.
|
|
263
263
|
|
|
264
|
-
The same field names a proposal-only skill with a live case here under `migrations`, which is how `canon:migration-claude-md` and `canon:migration-context` become reachable. Each entry carries the measurement behind the proposal rather than the proposal alone.
|
|
265
|
-
|
|
266
264
|
Add `--json` for the machine-readable report, and `--exit-code` to fail a CI job when a target falls behind. Files the project authored itself never count toward that exit code, and neither do superseded artifacts, seed drift, tooling, or anything the reverse walk reports, since each names content the project is expected to edit or place itself. An unmigrated domain does count, because running the relocation closes it.
|
|
267
265
|
|
|
268
266
|
Tooling reports under a section of its own, and `measured` there says whether the target ever recorded a chain. One that never ran a tooling sync reports unmeasured rather than clean, which is what separates tooling nobody has looked at from tooling that is current. A workspace root records nothing either way, since each package resolves its own chain.
|
|
@@ -285,17 +285,13 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
285
285
|
|
|
286
286
|
### Keep the project current with the toolkit
|
|
287
287
|
|
|
288
|
-
| Skill
|
|
289
|
-
|
|
|
290
|
-
| `canon:target-check`
|
|
291
|
-
| `canon:seed-sync`
|
|
292
|
-
| `canon:
|
|
293
|
-
| `canon:
|
|
294
|
-
| `canon:
|
|
295
|
-
| `canon:migration-standards-drop` | When the project still holds an installed `.claude/standards/` tree |
|
|
296
|
-
| `canon:canon-feedback` | When something in the toolkit is broken, missing, or off, to open an issue on it |
|
|
297
|
-
| `canon:canon-feedback-triage` | In the toolkit repo, to work through the open feedback issues |
|
|
298
|
-
| `canon:canon-rollout` | In the toolkit repo, to take one change out to every consuming project at once |
|
|
288
|
+
| Skill | When to use |
|
|
289
|
+
| ----------------------------- | -------------------------------------------------------------------------------------- |
|
|
290
|
+
| `canon:target-check` | In a target project, to report per domain what it holds against what the toolkit ships |
|
|
291
|
+
| `canon:seed-sync` | After a toolkit update, to reconcile installed seeds without losing customizations |
|
|
292
|
+
| `canon:canon-feedback` | When something in the toolkit is broken, missing, or off, to open an issue on it |
|
|
293
|
+
| `canon:canon-feedback-triage` | In the toolkit repo, to work through the open feedback issues |
|
|
294
|
+
| `canon:canon-rollout` | In the toolkit repo, to take one change out to every consuming project at once |
|
|
299
295
|
|
|
300
296
|
### Generate an artifact on demand
|
|
301
297
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Builds the landing page, captures it, and stages the frames over
|
|
3
|
+
# assets/evidence/home, the baseline `canon pr evidence` compares against.
|
|
4
|
+
#
|
|
5
|
+
# Two workflows call it. pr-visual-checks.yml stages the frames into the runner
|
|
6
|
+
# tree to diff them against the baseline, and refresh-capture-frames.yml stages
|
|
7
|
+
# them to commit as the new baseline. Both need the same frames, so one script
|
|
8
|
+
# keeps the two from drifting apart.
|
|
9
|
+
#
|
|
10
|
+
# `tooling/web/configs/e2e/screenshot.ts` is reused as is. This checkout has no
|
|
11
|
+
# web/package.json, so the preview server and the cases run against the
|
|
12
|
+
# root-level scripts rather than through screenshot.sh, which assumes a
|
|
13
|
+
# per-project `bun run build`/`bun run preview` pair.
|
|
14
|
+
set -e
|
|
15
|
+
set -o pipefail
|
|
16
|
+
|
|
17
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
+
PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
19
|
+
|
|
20
|
+
PORT=4321
|
|
21
|
+
BASE_URL="http://localhost:$PORT"
|
|
22
|
+
BASELINE="$PROJECT_ROOT/assets/evidence/home"
|
|
23
|
+
|
|
24
|
+
cd "$PROJECT_ROOT"
|
|
25
|
+
|
|
26
|
+
bun run web:build
|
|
27
|
+
|
|
28
|
+
bun run web:preview &
|
|
29
|
+
PREVIEW_PID=$!
|
|
30
|
+
trap 'kill "$PREVIEW_PID" 2>/dev/null || true' EXIT
|
|
31
|
+
|
|
32
|
+
for _ in $(seq 1 40); do
|
|
33
|
+
curl -sSf "$BASE_URL/" >/dev/null 2>&1 && break
|
|
34
|
+
sleep 0.25
|
|
35
|
+
done
|
|
36
|
+
curl -sSf "$BASE_URL/" >/dev/null 2>&1 || {
|
|
37
|
+
echo "Preview server not responding on port $PORT after 10s." >&2
|
|
38
|
+
exit 1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
(cd web && SCREENSHOT_BASE_URL="$BASE_URL" bun ../tooling/web/configs/e2e/screenshot.ts)
|
|
42
|
+
|
|
43
|
+
# The committed tree is cleared first rather than copied over. A copy only adds
|
|
44
|
+
# and overwrites, so a section dropped from the case list would leave its
|
|
45
|
+
# frames in place and a diff would see no change. Clearing first shows that
|
|
46
|
+
# section as a deletion.
|
|
47
|
+
rm -rf "$BASELINE"
|
|
48
|
+
mkdir -p "$BASELINE"
|
|
49
|
+
cp -R web/screenshots/localhost/home/. "$BASELINE/"
|
|
50
|
+
|
|
51
|
+
# The sweep holds all four widths and this repository commits two, 320 and
|
|
52
|
+
# 1280. The case list is the place to change if that set moves.
|
|
53
|
+
find "$BASELINE" \( -name '768--*.png' -o -name '1536--*.png' \) -delete
|
|
54
|
+
|
|
55
|
+
# The shared golden config names the case that emulates nothing `default`, and
|
|
56
|
+
# this repository's baseline names that frame `light`.
|
|
57
|
+
find "$BASELINE" -name '*--default.png' -execdir sh -c 'mv "$1" "${1%--default.png}--light.png"' _ {} \;
|
|
58
|
+
|
|
59
|
+
echo "capture-home: staged frames in assets/evidence/home"
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { SkillCase } from '@/claude/skills-rank'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* `target-setup`, `
|
|
5
|
-
*
|
|
6
|
-
* surfaces.
|
|
4
|
+
* `target-setup`, `canon-*`, and `create-rule`: scaffolding and the
|
|
5
|
+
* toolkit's own reference and feedback surfaces.
|
|
7
6
|
*
|
|
8
7
|
* The five `target-setup` cases are one skill read through five entry phrasings
|
|
9
8
|
* rather than five rows that collapsed to a duplicate. Each names a different
|
|
@@ -36,26 +35,6 @@ export const SETUP_CASES: readonly SkillCase[] = [
|
|
|
36
35
|
'Check that the dev server actually starts and the end-to-end suite passes against the scaffold.',
|
|
37
36
|
expect: 'target-setup',
|
|
38
37
|
},
|
|
39
|
-
{
|
|
40
|
-
prompt:
|
|
41
|
-
'This CLAUDE.md file has grown huge, break it apart into the tiered context model.',
|
|
42
|
-
expect: 'migration-claude-md',
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
prompt:
|
|
46
|
-
'Move the agent-flavored docs out of the docs folder and into context.',
|
|
47
|
-
expect: 'migration-context',
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
prompt:
|
|
51
|
-
'This file was replaced by a folder, help me split its content into it.',
|
|
52
|
-
expect: 'migration-superseded',
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
prompt:
|
|
56
|
-
'This project still carries a copy of the standards folder, get rid of it and point everything at the CLI.',
|
|
57
|
-
expect: 'migration-standards-drop',
|
|
58
|
-
},
|
|
59
38
|
{
|
|
60
39
|
prompt:
|
|
61
40
|
"Before I run this sync, tell me exactly what it's going to overwrite.",
|
package/src/commands/sync.ts
CHANGED
|
@@ -189,7 +189,6 @@ function renderCheck(report: CheckReport): void {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
renderUnclaimed(report)
|
|
192
|
-
renderMigrations(report)
|
|
193
192
|
|
|
194
193
|
outro()
|
|
195
194
|
// Scanned domains only. Tooling renders a section on every managed target, so
|
|
@@ -334,21 +333,6 @@ function describeUnclaimed(entry: UnclaimedFolder): string {
|
|
|
334
333
|
return `${counted}, content published at ${entry.since.slice(0, 7)}`
|
|
335
334
|
}
|
|
336
335
|
|
|
337
|
-
/**
|
|
338
|
-
* The proposal-only skills no other field reaches.
|
|
339
|
-
*/
|
|
340
|
-
function renderMigrations(report: CheckReport): void {
|
|
341
|
-
const { migrations } = report.reverse
|
|
342
|
-
if (migrations.length === 0) return
|
|
343
|
-
|
|
344
|
-
logStep('Migrations with a case here')
|
|
345
|
-
|
|
346
|
-
for (const candidate of migrations) {
|
|
347
|
-
logWarn(candidate.reason)
|
|
348
|
-
logInfo(`Run /canon:${candidate.skill} for a proposal.`)
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
336
|
async function runSync(target: string): Promise<number> {
|
|
353
337
|
intro('canon sync')
|
|
354
338
|
|
package/src/design/components.ts
CHANGED
|
@@ -223,6 +223,7 @@ const TEACH_CHROME: Component = {
|
|
|
223
223
|
reads: [
|
|
224
224
|
'--color-background',
|
|
225
225
|
'--color-surface',
|
|
226
|
+
'--color-chrome',
|
|
226
227
|
'--color-border',
|
|
227
228
|
'--color-text',
|
|
228
229
|
'--color-text-body',
|
|
@@ -232,7 +233,6 @@ const TEACH_CHROME: Component = {
|
|
|
232
233
|
'--teach-sans',
|
|
233
234
|
'--teach-mono',
|
|
234
235
|
'--teach-measure',
|
|
235
|
-
'--teach-chrome',
|
|
236
236
|
'--teach-shadow',
|
|
237
237
|
'--color-teach-accent-bg',
|
|
238
238
|
'--type-body-family',
|
|
@@ -247,8 +247,7 @@ const TEACH_CHROME: Component = {
|
|
|
247
247
|
--teach-hand: 'Virgil', 'Excalifont', cursive;
|
|
248
248
|
--teach-mono: var(--type-code-family);
|
|
249
249
|
--teach-measure: 52rem;
|
|
250
|
-
--teach-
|
|
251
|
-
--teach-mast-h: 4.4rem;
|
|
250
|
+
--teach-mast-h: 3.5rem;
|
|
252
251
|
--teach-shadow: 0 1px 2px rgba(20, 20, 20, 0.04);
|
|
253
252
|
--color-teach-accent-bg: color-mix(in srgb, var(--color-accent) 14%, var(--color-background));
|
|
254
253
|
|
|
@@ -328,6 +327,7 @@ html.sb-shut .pane { border-left: 0; }
|
|
|
328
327
|
background: var(--color-background);
|
|
329
328
|
border-bottom: 1px solid var(--color-border);
|
|
330
329
|
padding: 0 1.5rem;
|
|
330
|
+
cursor: default;
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
/* The reading bar rides the masthead's own bottom edge rather than arriving as
|
|
@@ -345,37 +345,26 @@ html.sb-shut .pane { border-left: 0; }
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
/* The height is the token rather than the sum of two paddings, so the sidebar's
|
|
348
|
-
own top row lands on the same seam the bar's bottom border draws.
|
|
348
|
+
own top row lands on the same seam the bar's bottom border draws. The row
|
|
349
|
+
spans the window rather than the reading measure: capped, the toggle and the
|
|
350
|
+
theme control floated in from the edges by an amount that changed per page. */
|
|
349
351
|
.mast {
|
|
350
|
-
max-width:
|
|
351
|
-
margin: 0
|
|
352
|
+
max-width: none;
|
|
353
|
+
margin: 0;
|
|
352
354
|
min-height: var(--teach-mast-h);
|
|
353
|
-
padding: 0;
|
|
355
|
+
padding: 0 0.9rem;
|
|
354
356
|
display: flex;
|
|
355
357
|
justify-content: space-between;
|
|
356
358
|
align-items: center;
|
|
357
|
-
gap:
|
|
359
|
+
gap: 0.55rem;
|
|
358
360
|
font-size: var(--t5);
|
|
359
361
|
color: var(--color-muted);
|
|
362
|
+
cursor: default;
|
|
360
363
|
}
|
|
361
364
|
|
|
362
365
|
.mast a { color: var(--color-accent); text-decoration: none; font-weight: 600; }
|
|
363
366
|
.mast a:hover { text-decoration: underline; }
|
|
364
367
|
|
|
365
|
-
.theme {
|
|
366
|
-
font-family: var(--teach-sans);
|
|
367
|
-
font-size: 0.75rem;
|
|
368
|
-
font-weight: 600;
|
|
369
|
-
color: var(--color-muted);
|
|
370
|
-
background: transparent;
|
|
371
|
-
border: 1px solid var(--color-border);
|
|
372
|
-
border-radius: 99px;
|
|
373
|
-
padding: 0.25rem 0.7rem;
|
|
374
|
-
cursor: pointer;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
.theme:hover { border-color: var(--color-accent); color: var(--color-accent); }
|
|
378
|
-
|
|
379
368
|
.nav {
|
|
380
369
|
width: 100%;
|
|
381
370
|
margin: 0;
|
|
@@ -422,7 +411,7 @@ html.sb-shut .pane { border-left: 0; }
|
|
|
422
411
|
restates either. */
|
|
423
412
|
/* A breadcrumb rather than a row of links. The gap tightens because the
|
|
424
413
|
separators now carry the spacing the gap used to. */
|
|
425
|
-
.mast-left { display: flex; align-items: center; gap: 0.
|
|
414
|
+
.mast-left { display: flex; align-items: center; gap: 0.3rem; min-width: 0; flex-wrap: wrap; cursor: default; }
|
|
426
415
|
|
|
427
416
|
/* \`--rule\` measured 1.27:1 here. It is a border token, and a border is a
|
|
428
417
|
shape a reader infers rather than a glyph they resolve, so it is the wrong
|
|
@@ -430,6 +419,7 @@ html.sb-shut .pane { border-left: 0; }
|
|
|
430
419
|
.crumb-sep {
|
|
431
420
|
color: var(--color-muted);
|
|
432
421
|
font-size: var(--t4);
|
|
422
|
+
margin: 0 0.1rem;
|
|
433
423
|
user-select: none;
|
|
434
424
|
}
|
|
435
425
|
|
|
@@ -442,27 +432,55 @@ html.sb-shut .pane { border-left: 0; }
|
|
|
442
432
|
|
|
443
433
|
.crumb { display: inline-flex; align-items: center; }
|
|
444
434
|
|
|
445
|
-
/* The label and its caret are one
|
|
446
|
-
the label navigates, the caret opens the menu
|
|
447
|
-
|
|
435
|
+
/* The label and its caret are one component with two zones, like a split
|
|
436
|
+
button: the label navigates, the caret opens the menu, and one surface fills
|
|
437
|
+
for both so the pair reads as a single chip rather than two loose pieces.
|
|
438
|
+
The pointer covers the gap between them, which is interactive on both sides. */
|
|
439
|
+
.crumb-item {
|
|
440
|
+
display: inline-flex;
|
|
441
|
+
align-items: center;
|
|
442
|
+
gap: 0;
|
|
443
|
+
border-radius: 7px;
|
|
444
|
+
padding: 0.1rem 0.15rem 0.1rem 0.35rem;
|
|
445
|
+
cursor: pointer;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.crumb-item:has(details.jump):hover,
|
|
449
|
+
.crumb-item:has(details.jump[open]) { background: var(--color-chrome); }
|
|
450
|
+
|
|
451
|
+
/* Accent reports where you are. A crumb is navigation you can take, so it is
|
|
452
|
+
neutral, and \`.mast a\` keeps the accent for any other bar link. */
|
|
453
|
+
.mast a.crumb { color: var(--color-text-secondary); padding: 0.1rem 0.15rem 0.1rem 0; }
|
|
454
|
+
.mast a.crumb:hover { text-decoration: none; }
|
|
455
|
+
.crumb-item:hover a.crumb { color: var(--color-text); }
|
|
456
|
+
|
|
457
|
+
.crumb-here { color: var(--color-text); font-weight: 650; }
|
|
448
458
|
|
|
449
|
-
|
|
459
|
+
/* \`align-items: center\` centres boxes, and a label's line box carries
|
|
460
|
+
half-leading above the cap and descender room below, so its capitals sit
|
|
461
|
+
above its own centre by an amount that scales with the font size. Trimming
|
|
462
|
+
the label to its cap band fixes the box rather than nudging the mark. It
|
|
463
|
+
lands on a span holding the text alone, since the link's own \`inline-flex\`
|
|
464
|
+
ignores it. Firefox ships no \`text-box\` and keeps the caret a pixel low. */
|
|
465
|
+
.crumb-t, .crumb-here, .sb-ws .ws-name {
|
|
466
|
+
display: block;
|
|
467
|
+
text-box: trim-both cap alphabetic;
|
|
468
|
+
}
|
|
450
469
|
|
|
451
|
-
/* The caret is
|
|
452
|
-
|
|
470
|
+
/* The caret is the whole control, so it carries the 24 pixel minimum on its
|
|
471
|
+
own rather than inheriting a label's width. It fills nothing itself, since
|
|
472
|
+
the chip around it does. */
|
|
453
473
|
.crumb-item > .jump > summary {
|
|
454
474
|
min-width: 1.5rem;
|
|
455
475
|
min-height: 1.5rem;
|
|
456
476
|
display: inline-flex;
|
|
457
477
|
align-items: center;
|
|
458
478
|
justify-content: center;
|
|
459
|
-
padding: 0;
|
|
479
|
+
padding: 0.15rem 0.3rem 0.15rem 0;
|
|
460
480
|
border: 0;
|
|
461
481
|
background: transparent;
|
|
462
482
|
}
|
|
463
483
|
|
|
464
|
-
.crumb-item > .jump > summary:hover { background: var(--color-surface); border-radius: 5px; }
|
|
465
|
-
|
|
466
484
|
/* A bar link is its own tap target rather than a line of text, so it carries
|
|
467
485
|
the 24 pixel minimum the a11y rule sets. */
|
|
468
486
|
.mast a,
|
|
@@ -478,23 +496,25 @@ html.sb-shut .pane { border-left: 0; }
|
|
|
478
496
|
color: var(--color-muted);
|
|
479
497
|
}
|
|
480
498
|
|
|
481
|
-
.mast-right { display: flex; align-items: center; gap: 0.
|
|
499
|
+
.mast-right { display: flex; align-items: center; gap: 0.2rem; margin-left: auto; }
|
|
482
500
|
|
|
501
|
+
/* No border, at the fold control's size and radius, so every control on the
|
|
502
|
+
bar is drawn one way. */
|
|
483
503
|
.theme {
|
|
484
|
-
width: 1.
|
|
485
|
-
height: 1.
|
|
504
|
+
width: 1.75rem;
|
|
505
|
+
height: 1.75rem;
|
|
486
506
|
display: inline-flex;
|
|
487
507
|
align-items: center;
|
|
488
508
|
justify-content: center;
|
|
489
509
|
padding: 0;
|
|
490
|
-
border:
|
|
491
|
-
border-radius:
|
|
510
|
+
border: 0;
|
|
511
|
+
border-radius: 6px;
|
|
492
512
|
background: transparent;
|
|
493
513
|
color: var(--color-muted);
|
|
494
514
|
cursor: pointer;
|
|
495
515
|
}
|
|
496
516
|
|
|
497
|
-
.theme:hover {
|
|
517
|
+
.theme:hover { color: var(--color-text); background: var(--color-chrome); }
|
|
498
518
|
.theme svg { width: 0.95rem; height: 0.95rem; display: block; }
|
|
499
519
|
.theme .moon { display: none; }
|
|
500
520
|
:root[data-theme="dark"] .theme .sun { display: none; }
|
|
@@ -528,7 +548,7 @@ html.sb-shut .pane { border-left: 0; }
|
|
|
528
548
|
cursor: pointer;
|
|
529
549
|
}
|
|
530
550
|
|
|
531
|
-
.sb-fold:hover { color: var(--color-text); background: var(--color-
|
|
551
|
+
.sb-fold:hover { color: var(--color-text); background: var(--color-chrome); }
|
|
532
552
|
|
|
533
553
|
.sb {
|
|
534
554
|
flex: 0 0 var(--sb-w, 16rem);
|
|
@@ -564,7 +584,7 @@ html.sb-shut .sb { flex-basis: 0; width: 0; overflow: hidden; }
|
|
|
564
584
|
display: inline-flex;
|
|
565
585
|
align-items: center;
|
|
566
586
|
gap: 0.35rem;
|
|
567
|
-
padding: 0.
|
|
587
|
+
padding: 0.25rem 0.3rem;
|
|
568
588
|
border-radius: 6px;
|
|
569
589
|
font-size: var(--t4);
|
|
570
590
|
font-weight: 650;
|
|
@@ -572,7 +592,7 @@ html.sb-shut .sb { flex-basis: 0; width: 0; overflow: hidden; }
|
|
|
572
592
|
}
|
|
573
593
|
|
|
574
594
|
.sb-ws > summary::-webkit-details-marker { display: none; }
|
|
575
|
-
.sb-ws > summary:hover, .sb-ws[open] > summary { background: var(--color-
|
|
595
|
+
.sb-ws > summary:hover, .sb-ws[open] > summary { background: var(--color-chrome); color: var(--color-text); }
|
|
576
596
|
.sb-ws .car { color: var(--color-muted); display: inline-flex; align-items: center; }
|
|
577
597
|
|
|
578
598
|
.sb-wl {
|
|
@@ -585,7 +605,7 @@ html.sb-shut .sb { flex-basis: 0; width: 0; overflow: hidden; }
|
|
|
585
605
|
border: 1px solid var(--color-border);
|
|
586
606
|
border-radius: 9px;
|
|
587
607
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
|
|
588
|
-
padding: 0.
|
|
608
|
+
padding: 0.3rem;
|
|
589
609
|
}
|
|
590
610
|
|
|
591
611
|
:root[data-theme="dark"] .sb-wl { box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); }
|
|
@@ -644,7 +664,7 @@ html.sb-shut .sb { flex-basis: 0; width: 0; overflow: hidden; }
|
|
|
644
664
|
line-height: 1.45;
|
|
645
665
|
}
|
|
646
666
|
|
|
647
|
-
.sb-l:hover { background: var(--color-
|
|
667
|
+
.sb-l:hover { background: var(--color-chrome); color: var(--color-text); }
|
|
648
668
|
|
|
649
669
|
.sb-n {
|
|
650
670
|
flex: none;
|
|
@@ -764,46 +784,47 @@ html.sb-shut .sb-grip { display: none; }
|
|
|
764
784
|
}
|
|
765
785
|
|
|
766
786
|
.jump summary::-webkit-details-marker { display: none; }
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
.jump summary .caret {
|
|
772
|
-
.jump summary:hover .caret { opacity: 1; }
|
|
773
|
-
.jump[open] summary { border-color: var(--color-accent); color: var(--color-accent); }
|
|
774
|
-
.jump[open] summary .caret { opacity: 1; }
|
|
787
|
+
/* The caret says "this is a menu" and nothing else, so it is drawn at full
|
|
788
|
+
strength in every state and stays muted. A reveal on hover hid the only
|
|
789
|
+
route into the menu from a reader who cannot hover, and an affordance is not
|
|
790
|
+
a state, so it takes no accent on hover or while open. */
|
|
791
|
+
.jump summary .caret { color: var(--color-muted); }
|
|
775
792
|
|
|
776
793
|
.jump-list {
|
|
777
794
|
position: absolute;
|
|
778
795
|
top: calc(100% + 0.4rem);
|
|
779
796
|
left: 0;
|
|
780
797
|
z-index: 20;
|
|
781
|
-
min-width: 21rem;
|
|
782
798
|
background: var(--color-background);
|
|
783
799
|
border: 1px solid var(--color-border);
|
|
784
800
|
border-radius: 9px;
|
|
785
801
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
|
|
786
|
-
padding: 0.
|
|
802
|
+
padding: 0.25rem;
|
|
787
803
|
list-style: none;
|
|
788
804
|
margin: 0;
|
|
789
805
|
}
|
|
790
806
|
|
|
791
807
|
:root[data-theme="dark"] .jump-list { box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); }
|
|
792
808
|
|
|
809
|
+
/* One row recipe wherever the menu is mounted, on the steps the lesson list
|
|
810
|
+
beside it already reads: the label at \`--t5\` and the numeral at \`--t6\` mono.
|
|
811
|
+
The weight is restated because \`.mast a\` sets 600 for the bar's own links. */
|
|
793
812
|
.jump-list a {
|
|
794
813
|
display: grid;
|
|
795
814
|
grid-template-columns: 1.65rem 1fr auto;
|
|
796
|
-
gap: 0.
|
|
815
|
+
gap: 0.55rem;
|
|
797
816
|
align-items: center;
|
|
798
|
-
padding: 0.
|
|
817
|
+
padding: 0.38rem 0.55rem;
|
|
799
818
|
border-radius: 6px;
|
|
800
819
|
text-decoration: none;
|
|
801
820
|
color: var(--color-text-body);
|
|
802
|
-
font-size:
|
|
821
|
+
font-size: var(--t5);
|
|
822
|
+
font-weight: 400;
|
|
823
|
+
line-height: 1.45;
|
|
803
824
|
}
|
|
804
825
|
|
|
805
|
-
.jump-list a:hover { background: var(--color-
|
|
806
|
-
.jump-list .n { font-family: var(--teach-mono); font-size:
|
|
826
|
+
.jump-list a:hover { background: var(--color-chrome); }
|
|
827
|
+
.jump-list .n { font-family: var(--teach-mono); font-size: var(--t6); color: var(--color-muted); }
|
|
807
828
|
/* The third column used to hold a status dot that read the same on every row,
|
|
808
829
|
so it reported nothing. It is a per-entry trailing slot now: the workspace
|
|
809
830
|
menu puts a lesson count there and the lesson menu leaves it empty. */
|
|
@@ -825,11 +846,11 @@ html.sb-shut .sb-grip { display: none; }
|
|
|
825
846
|
.jump-list li.all a { color: var(--color-accent); }
|
|
826
847
|
.jump-list li.all .n { color: var(--color-accent); }
|
|
827
848
|
|
|
828
|
-
/* The menu
|
|
829
|
-
|
|
830
|
-
|
|
849
|
+
/* The menu fits its content above an 11rem floor rather than padding three
|
|
850
|
+
short titles out to a wide panel. It is anchored to its summary, so the
|
|
851
|
+
clamp keeps a long row wrapping on screen without a breakpoint. */
|
|
831
852
|
.jump-list {
|
|
832
|
-
min-width: min(
|
|
853
|
+
min-width: min(11rem, calc(100vw - 2rem));
|
|
833
854
|
max-width: calc(100vw - 2rem);
|
|
834
855
|
}
|
|
835
856
|
|
|
@@ -850,8 +871,7 @@ html.sb-shut .sb-grip { display: none; }
|
|
|
850
871
|
@media (max-width: 640px) {
|
|
851
872
|
.bar { padding: 0 1rem; }
|
|
852
873
|
main { padding-left: 1rem; padding-right: 1rem; }
|
|
853
|
-
.mast { font-size: 0.75rem;
|
|
854
|
-
.mast-left { gap: 0.5rem; row-gap: 0.35rem; }
|
|
874
|
+
.mast { font-size: 0.75rem; }
|
|
855
875
|
}
|
|
856
876
|
|
|
857
877
|
@media (max-width: 640px) {
|
|
@@ -933,7 +953,7 @@ html.sb-shut .sb-grip { display: none; }
|
|
|
933
953
|
cursor: pointer;
|
|
934
954
|
}
|
|
935
955
|
|
|
936
|
-
.sb-close:hover { color: var(--color-text); background: var(--color-
|
|
956
|
+
.sb-close:hover { color: var(--color-text); background: var(--color-chrome); }
|
|
937
957
|
|
|
938
958
|
.sb-scrim {
|
|
939
959
|
display: block;
|
|
@@ -172,8 +172,7 @@ const DOCS_PATH = /(?<![\w./-])docs\/[^\s`)\]]*\.md\b/g
|
|
|
172
172
|
* deliberate placeholder rather than a real citation. A body illustrating
|
|
173
173
|
* the shape a project's own `standards/<name>.md` takes writes a bracketed
|
|
174
174
|
* token this pattern also matches, such as `standards/<slug>.md` in
|
|
175
|
-
* `create-standard/SKILL.md
|
|
176
|
-
* `migration-standards-drop/SKILL.md`, and neither names a real file. A bare,
|
|
175
|
+
* `create-standard/SKILL.md`, which names no real file. A bare,
|
|
177
176
|
* non-bracketed match now reports whether or not the file exists, unlike
|
|
178
177
|
* `DOCS_PATH`, which still gates on existence: a same-repository citation
|
|
179
178
|
* under this corpus names no target project's own tree the way a `docs/`
|