@erclx/canon 5.24.0 → 5.25.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.
Files changed (80) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
  3. package/claude/skills/backlog-triage/SKILL.md +19 -6
  4. package/claude/skills/canvas/REQUIREMENT.md +49 -0
  5. package/claude/skills/canvas/SKILL.md +106 -0
  6. package/claude/skills/design-extract/REQUIREMENT.md +1 -1
  7. package/claude/skills/draft-doc/REQUIREMENT.md +1 -1
  8. package/claude/skills/draft-figure/REQUIREMENT.md +14 -3
  9. package/claude/skills/draft-figure/SKILL.md +24 -51
  10. package/claude/skills/draft-figure/references/architecture-set.md +35 -0
  11. package/claude/skills/draft-figure/references/hand-drawn.md +31 -0
  12. package/claude/skills/draft-figure/references/mermaid.md +55 -0
  13. package/claude/skills/git-followup/SKILL.md +17 -6
  14. package/claude/skills/git-pr/references/evidence.md +4 -2
  15. package/claude/skills/git-split/SKILL.md +1 -1
  16. package/claude/skills/index-lookup/SKILL.md +2 -2
  17. package/claude/skills/review-address/SKILL.md +6 -13
  18. package/claude/skills/review-address/references/rest-reads.md +43 -0
  19. package/claude/skills/review-craft/references/ui.md +7 -1
  20. package/claude/skills/review-pr/REQUIREMENT.md +1 -0
  21. package/claude/skills/review-pr/SKILL.md +9 -3
  22. package/claude/skills/review-pr/references/close-out.md +2 -2
  23. package/claude/skills/review-pr/references/post.md +17 -5
  24. package/claude/skills/review-pr/references/scope.md +10 -8
  25. package/claude/skills/review-ui/SKILL.md +12 -4
  26. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +30 -1
  27. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +4 -0
  28. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +4 -4
  29. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +6 -0
  30. package/claude/skills/role-orchestrator/scripts/poll.ts +90 -27
  31. package/claude/skills/role-orchestrator/scripts/repo.ts +3 -5
  32. package/claude/skills/role-orchestrator/scripts/watch.ts +4 -7
  33. package/docs/agents/canvas.md +1 -1
  34. package/docs/agents/commands.md +45 -45
  35. package/docs/agents/context-audit-checks.md +2 -2
  36. package/docs/agents/index.md +0 -1
  37. package/docs/agents/key-changes.md +3 -3
  38. package/docs/agents/pr-evidence.md +11 -2
  39. package/docs/agents/pr-reads.md +5 -4
  40. package/docs/target/projects.md +4 -4
  41. package/docs/workflow/ai-workflow.md +0 -1
  42. package/docs/workflow/skill-map.md +3 -3
  43. package/docs/workflow/visual-design-workflow.md +1 -0
  44. package/governance/renames.toml +0 -4
  45. package/governance/rules/standards/markdown.md +1 -0
  46. package/package.json +1 -2
  47. package/scripts/core/install-check.sh +1 -1
  48. package/src/audits/catalog.ts +2 -2
  49. package/src/canvas/client/inspector/field.tsx +200 -0
  50. package/src/canvas/client/inspector/fields.css +151 -0
  51. package/src/canvas/client/inspector/section.tsx +27 -0
  52. package/src/canvas/client/inspector/values.ts +60 -0
  53. package/src/canvas/client/inspector.tsx +154 -115
  54. package/src/canvas/client/shell.css +0 -101
  55. package/src/claude/cases/misc.ts +5 -0
  56. package/src/claude/cases/workflow.ts +1 -1
  57. package/src/claude/seeds.ts +0 -1
  58. package/src/commands/design.ts +2 -64
  59. package/src/commands/pr.ts +219 -202
  60. package/src/context/audit.ts +7 -6
  61. package/src/help.ts +2 -2
  62. package/src/migrate/skill-names.ts +7 -1
  63. package/src/pr/evidence.ts +25 -5
  64. package/src/pr/rest.ts +137 -0
  65. package/standards/canvas.md +112 -0
  66. package/standards/context.md +2 -1
  67. package/standards/figures.md +0 -1
  68. package/standards/index.md +1 -1
  69. package/standards/markdown.md +1 -0
  70. package/standards/mermaid.md +1 -7
  71. package/tooling/claude/reference.md +0 -5
  72. package/claude/skills/draft-diagram/REQUIREMENT.md +0 -45
  73. package/claude/skills/draft-diagram/SKILL.md +0 -177
  74. package/docs/agents/design-board.md +0 -33
  75. package/governance/rules/standards/diagrams.md +0 -14
  76. package/governance/rules/standards/mermaid.md +0 -15
  77. package/scripts/core/check-gallery-exclusion.sh +0 -50
  78. package/src/design/board.ts +0 -576
  79. package/standards/diagrams.md +0 -132
  80. package/tooling/claude/seeds/.claude/diagrams/index.md +0 -8
@@ -0,0 +1,200 @@
1
+ /** @jsxImportSource preact */
2
+ import type { JSX } from 'preact'
3
+ import { useRef } from 'preact/hooks'
4
+ import { scrubStep } from '@/canvas/client/inspector/values'
5
+
6
+ /**
7
+ * One drag of a number field's glyph. Each call takes the value as the field
8
+ * shows it. Only `commit` writes, so a drag posts one edit at most.
9
+ */
10
+ export interface ScrubSession {
11
+ readonly preview: (value: string) => void
12
+ readonly restore: () => void
13
+ readonly commit: (value: string) => void
14
+ }
15
+
16
+ /**
17
+ * Opens a session at press, so what `restore` returns to is read before any
18
+ * preview, however often the panel renders during the drag.
19
+ */
20
+ export interface Scrub {
21
+ readonly begin: () => ScrubSession
22
+ /** Holds a scrubbed number inside what the property accepts. */
23
+ readonly clamp: (value: number) => number
24
+ }
25
+
26
+ interface FieldProps {
27
+ /** The full word, which names the input for assistive technology. */
28
+ readonly label: string
29
+ /** The short mark drawn inside the field, hidden from assistive technology. */
30
+ readonly glyph: string
31
+ readonly initial: string
32
+ readonly isBusy: boolean
33
+ readonly isWide?: boolean
34
+ readonly placeholder?: string
35
+ readonly onCommit: (value: string) => void
36
+ /** Makes the glyph a drag handle. Typing stays the keyboard path. */
37
+ readonly scrub?: Scrub
38
+ }
39
+
40
+ const NUMBER = /^-?\d*\.?\d+$/
41
+
42
+ interface Drag {
43
+ readonly pointerId: number
44
+ /** What the input held at press, which a cancel puts back. */
45
+ readonly typed: string
46
+ /** The number the drag started from, an empty field's placeholder included. */
47
+ readonly start: string
48
+ readonly session: ScrubSession
49
+ lastX: number
50
+ value: number
51
+ }
52
+
53
+ function fieldClass(isWide: boolean | undefined, modifier?: string): string {
54
+ return ['glyph-field', isWide ? 'is-wide' : '', modifier ?? '']
55
+ .filter(Boolean)
56
+ .join(' ')
57
+ }
58
+
59
+ /**
60
+ * Commits on change, which a text input fires on Enter or on leaving it, and
61
+ * sends nothing when the value is what it started at. Escape puts it back.
62
+ */
63
+ export function Field({
64
+ label,
65
+ glyph,
66
+ initial,
67
+ isBusy,
68
+ isWide,
69
+ placeholder,
70
+ onCommit,
71
+ scrub,
72
+ }: FieldProps): JSX.Element {
73
+ const input = useRef<HTMLInputElement>(null)
74
+ const handle = useRef<HTMLSpanElement>(null)
75
+ const drag = useRef<Drag | null>(null)
76
+
77
+ const end = (pointerId: number): Drag | undefined => {
78
+ const held = drag.current
79
+ if (!held || held.pointerId !== pointerId) return undefined
80
+ drag.current = null
81
+ return held
82
+ }
83
+
84
+ /* An empty field scrubs from the number its placeholder shows. */
85
+ const scrubFrom = (value: string): string =>
86
+ value.trim() || placeholder?.trim() || ''
87
+ const isScrubbable = scrub !== undefined && NUMBER.test(scrubFrom(initial))
88
+
89
+ const handlers = isScrubbable
90
+ ? {
91
+ onPointerDown: (event: PointerEvent) => {
92
+ const typed = input.current?.value ?? ''
93
+ const start = scrubFrom(typed)
94
+ if (isBusy || !NUMBER.test(start)) return
95
+ event.preventDefault()
96
+ /*
97
+ * Capture keeps the release coming here when the pointer leaves the
98
+ * glyph, the panel, or crosses into a frame's own document.
99
+ */
100
+ try {
101
+ handle.current?.setPointerCapture(event.pointerId)
102
+ } catch {
103
+ /* A pointer the browser no longer tracks ends on cancel instead. */
104
+ }
105
+ drag.current = {
106
+ pointerId: event.pointerId,
107
+ typed,
108
+ start,
109
+ session: scrub.begin(),
110
+ lastX: event.clientX,
111
+ value: Number(start),
112
+ }
113
+ },
114
+ onPointerMove: (event: PointerEvent) => {
115
+ const held = drag.current
116
+ if (!held || held.pointerId !== event.pointerId) return
117
+ const moved = event.clientX - held.lastX
118
+ held.lastX = event.clientX
119
+ held.value = scrub.clamp(
120
+ Math.round((held.value + moved * scrubStep(event.shiftKey)) * 10) /
121
+ 10,
122
+ )
123
+ const shown = String(held.value)
124
+ if (input.current) input.current.value = shown
125
+ held.session.preview(shown)
126
+ },
127
+ onPointerUp: (event: PointerEvent) => {
128
+ const held = end(event.pointerId)
129
+ if (!held) return
130
+ const shown = String(held.value)
131
+ if (shown === held.start) held.session.restore()
132
+ else held.session.commit(shown)
133
+ },
134
+ onPointerCancel: (event: PointerEvent) => {
135
+ const held = end(event.pointerId)
136
+ if (!held) return
137
+ if (input.current) input.current.value = held.typed
138
+ held.session.restore()
139
+ },
140
+ }
141
+ : {}
142
+
143
+ return (
144
+ <div class={fieldClass(isWide)}>
145
+ <span
146
+ ref={handle}
147
+ class={isScrubbable ? 'glyph is-scrub' : 'glyph'}
148
+ aria-hidden="true"
149
+ title={label}
150
+ {...handlers}
151
+ >
152
+ {glyph}
153
+ </span>
154
+ <input
155
+ ref={input}
156
+ class="glyph-field-input"
157
+ type="text"
158
+ aria-label={label}
159
+ title={initial}
160
+ defaultValue={initial}
161
+ placeholder={placeholder}
162
+ disabled={isBusy}
163
+ spellcheck={false}
164
+ onChange={(event) => {
165
+ const value = event.currentTarget.value.trim()
166
+ if (value !== initial.trim()) onCommit(value)
167
+ }}
168
+ onKeyDown={(event) => {
169
+ if (event.key === 'Escape') event.currentTarget.value = initial
170
+ }}
171
+ />
172
+ </div>
173
+ )
174
+ }
175
+
176
+ interface ReadOnlyFieldProps {
177
+ readonly label: string
178
+ readonly glyph: string
179
+ readonly value: string
180
+ readonly isWide?: boolean
181
+ }
182
+
183
+ /** A value the panel shows and nothing here writes, drawn like a field. */
184
+ export function ReadOnlyField({
185
+ label,
186
+ glyph,
187
+ value,
188
+ isWide,
189
+ }: ReadOnlyFieldProps): JSX.Element {
190
+ return (
191
+ <div class={fieldClass(isWide, 'is-readonly')}>
192
+ <span class="glyph" aria-hidden="true" title={label}>
193
+ {glyph}
194
+ </span>
195
+ <output class="glyph-field-input" aria-label={label} title={value}>
196
+ {value}
197
+ </output>
198
+ </div>
199
+ )
200
+ }
@@ -0,0 +1,151 @@
1
+ /*
2
+ * The inspector's own rules. Every value reads a chrome token, the same set
3
+ * shell.css reads. Each field's glyph sits inside its box, so two fields share
4
+ * a row without a label column eating the width.
5
+ */
6
+
7
+ .inspector-section {
8
+ margin-top: var(--space-sm);
9
+ }
10
+
11
+ .inspector-section-head {
12
+ display: flex;
13
+ align-items: center;
14
+ min-height: 24px;
15
+ }
16
+
17
+ .inspector-section-title {
18
+ flex: 1;
19
+ min-width: 0;
20
+ margin: 0;
21
+ font-size: var(--type-label-size);
22
+ font-weight: 500;
23
+ color: var(--color-text);
24
+ }
25
+
26
+ .inspector-section-action {
27
+ flex: none;
28
+ display: flex;
29
+ justify-content: flex-end;
30
+ min-width: 24px;
31
+ }
32
+
33
+ .field-grid {
34
+ display: grid;
35
+ grid-template-columns: repeat(2, minmax(0, 1fr));
36
+ gap: var(--space-xs);
37
+ margin-top: var(--space-xs);
38
+ }
39
+
40
+ .glyph-field {
41
+ display: flex;
42
+ align-items: center;
43
+ min-width: 0;
44
+ min-height: 24px;
45
+ border: 1px solid var(--color-border);
46
+ border-radius: var(--radius-action);
47
+ background: var(--color-surface);
48
+ }
49
+
50
+ .glyph-field:hover {
51
+ border-color: var(--color-muted);
52
+ }
53
+
54
+ .glyph-field.is-readonly {
55
+ background: transparent;
56
+ }
57
+
58
+ .glyph-field.is-readonly:hover {
59
+ border-color: var(--color-border);
60
+ }
61
+
62
+ .is-wide {
63
+ grid-column: 1 / -1;
64
+ }
65
+
66
+ .glyph {
67
+ flex: none;
68
+ min-width: 1.75rem;
69
+ padding: 0 var(--space-xs);
70
+ color: var(--color-muted);
71
+ text-align: center;
72
+ user-select: none;
73
+ }
74
+
75
+ /* A number field's glyph scrubs its value, so a drag must not pan or scroll. */
76
+ .glyph.is-scrub {
77
+ cursor: ew-resize;
78
+ touch-action: none;
79
+ }
80
+
81
+ .glyph-field-input {
82
+ flex: 1;
83
+ min-width: 0;
84
+ padding: 2px var(--space-xs) 2px 0;
85
+ border: 0;
86
+ background: transparent;
87
+ color: var(--color-text);
88
+ font: inherit;
89
+ font-variant-numeric: tabular-nums;
90
+ overflow: hidden;
91
+ text-overflow: ellipsis;
92
+ white-space: nowrap;
93
+ }
94
+
95
+ .glyph-field-input:disabled {
96
+ opacity: 0.5;
97
+ cursor: not-allowed;
98
+ }
99
+
100
+ .fill-row {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: var(--space-xs);
104
+ min-width: 0;
105
+ }
106
+
107
+ .fill-row .glyph-field {
108
+ flex: 1;
109
+ }
110
+
111
+ .token-picker {
112
+ flex: none;
113
+ width: 4.5rem;
114
+ min-height: 24px;
115
+ padding: 2px var(--space-xs);
116
+ border: 1px solid var(--color-border);
117
+ border-radius: var(--radius-action);
118
+ background: var(--color-surface);
119
+ color: var(--color-text);
120
+ font: inherit;
121
+ cursor: pointer;
122
+ }
123
+
124
+ .token-picker:hover {
125
+ border-color: var(--color-muted);
126
+ }
127
+
128
+ .token-picker:disabled {
129
+ opacity: 0.5;
130
+ cursor: not-allowed;
131
+ }
132
+
133
+ .raw {
134
+ flex: none;
135
+ color: var(--color-text-secondary);
136
+ cursor: help;
137
+ }
138
+
139
+ .text-excerpt {
140
+ margin: var(--space-xs) 0 0;
141
+ color: var(--color-text-body);
142
+ overflow: hidden;
143
+ text-overflow: ellipsis;
144
+ white-space: nowrap;
145
+ }
146
+
147
+ .saved {
148
+ margin-left: var(--space-sm);
149
+ color: var(--color-success);
150
+ font-weight: 400;
151
+ }
@@ -0,0 +1,27 @@
1
+ /** @jsxImportSource preact */
2
+ import { type ComponentChildren, type JSX, toChildArray } from 'preact'
3
+
4
+ interface SectionProps {
5
+ readonly title: string
6
+ /** Sits in a fixed lane right of the title, so adding one never moves it. */
7
+ readonly action?: ComponentChildren
8
+ readonly children?: ComponentChildren
9
+ }
10
+
11
+ /** A titled group of fields. With no fields it renders its header alone. */
12
+ export function Section({
13
+ title,
14
+ action,
15
+ children,
16
+ }: SectionProps): JSX.Element {
17
+ const fields = toChildArray(children)
18
+ return (
19
+ <section class="inspector-section">
20
+ <header class="inspector-section-head">
21
+ <h3 class="inspector-section-title">{title}</h3>
22
+ <span class="inspector-section-action">{action}</span>
23
+ </header>
24
+ {fields.length > 0 ? <div class="field-grid">{fields}</div> : null}
25
+ </section>
26
+ )
27
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * How the inspector reads a computed value and writes a typed one back. A
3
+ * field shows the formatted form, and only a value the operator changed goes
4
+ * back through `toCssValue`, so rounding never writes a value nobody typed.
5
+ */
6
+
7
+ /** Properties whose bare numbers are pixel lengths. */
8
+ const LENGTHS = new Set(['width', 'height', 'padding', 'gap', 'font-size'])
9
+
10
+ const PX = /^(-?\d*\.?\d+)px$/
11
+
12
+ function formatPx(token: string): string {
13
+ const match = token.match(PX)
14
+ if (!match) return token
15
+ const value = Number(match[1])
16
+ return String(
17
+ Math.abs(value) < 10 ? Math.round(value * 10) / 10 : Math.round(value),
18
+ )
19
+ }
20
+
21
+ /** A color that paints nothing, which the panel reads as no fill. */
22
+ export function isNoFill(value: string): boolean {
23
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, '')
24
+ return normalized === 'transparent' || normalized === 'rgba(0,0,0,0)'
25
+ }
26
+
27
+ export function displayValue(property: string, raw: string): string {
28
+ const value = raw.trim()
29
+ if (property === 'gap' && (value === '' || value === 'normal')) return ''
30
+ if (property === 'background-color' && isNoFill(value)) return ''
31
+ if (value === 'auto') return 'Auto'
32
+ if (value === 'normal') return 'Normal'
33
+ return value.split(/\s+/).map(formatPx).join(' ')
34
+ }
35
+
36
+ export function toCssValue(property: string, typed: string): string {
37
+ const value = typed.trim()
38
+ if (value === 'Auto' || value === 'Normal') return value.toLowerCase()
39
+ if (!LENGTHS.has(property)) return value
40
+ return value
41
+ .split(/\s+/)
42
+ .map((token) => (/^-?\d*\.?\d+$/.test(token) ? `${token}px` : token))
43
+ .join(' ')
44
+ }
45
+
46
+ /**
47
+ * Holds a scrubbed number inside what the property accepts. The writer takes
48
+ * any declaration that does not break the rule, so a negative length or a
49
+ * weight past 1000 would reach the file and the browser would drop it there.
50
+ */
51
+ export function clampScrub(property: string, value: number): number {
52
+ if (property === 'font-weight') return Math.min(1000, Math.max(1, value))
53
+ if (LENGTHS.has(property)) return Math.max(0, value)
54
+ return value
55
+ }
56
+
57
+ /** Units a scrub moves for each pixel the pointer travels. */
58
+ export function scrubStep(isCoarse: boolean): number {
59
+ return isCoarse ? 10 : 1
60
+ }