@erclx/canon 5.24.0 → 5.26.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 +2 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
- package/claude/skills/backlog-triage/SKILL.md +19 -6
- package/claude/skills/canvas/REQUIREMENT.md +49 -0
- package/claude/skills/canvas/SKILL.md +106 -0
- package/claude/skills/context-fold/REQUIREMENT.md +6 -2
- package/claude/skills/context-fold/SKILL.md +8 -6
- package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
- package/claude/skills/design-extract/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/design-taste/SKILL.md +4 -2
- package/claude/skills/design-taste/references/adopted.md +40 -0
- package/claude/skills/design-taste/references/kinds.md +1 -1
- package/claude/skills/design-taste/references/systems.md +1 -7
- package/claude/skills/design-taste/references/tells.md +2 -24
- package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
- package/claude/skills/document-health/REQUIREMENT.md +2 -0
- package/claude/skills/document-health/SKILL.md +21 -0
- package/claude/skills/document-health/references/axes.md +12 -0
- package/claude/skills/draft-and-pick/SKILL.md +1 -1
- package/claude/skills/draft-doc/REQUIREMENT.md +1 -1
- package/claude/skills/draft-figure/REQUIREMENT.md +14 -3
- package/claude/skills/draft-figure/SKILL.md +24 -51
- package/claude/skills/draft-figure/references/architecture-set.md +35 -0
- package/claude/skills/draft-figure/references/hand-drawn.md +31 -0
- package/claude/skills/draft-figure/references/mermaid.md +55 -0
- package/claude/skills/draft-identity/SKILL.md +1 -1
- package/claude/skills/draft-screencast/SKILL.md +9 -7
- package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
- package/claude/skills/draft-slides/SKILL.md +31 -20
- package/claude/skills/git-followup/SKILL.md +17 -6
- package/claude/skills/git-issue/references/issue.md +10 -0
- package/claude/skills/git-pr/references/evidence.md +12 -2
- package/claude/skills/git-pr/references/labels.md +12 -0
- package/claude/skills/git-split/SKILL.md +19 -8
- package/claude/skills/index-lookup/SKILL.md +2 -2
- package/claude/skills/markdown-propose/references/format.md +9 -0
- package/claude/skills/memory-review/references/later-phases.md +7 -0
- package/claude/skills/review-address/SKILL.md +6 -13
- package/claude/skills/review-address/references/rest-reads.md +43 -0
- package/claude/skills/review-craft/references/ui.md +7 -1
- package/claude/skills/review-pr/REQUIREMENT.md +1 -0
- package/claude/skills/review-pr/SKILL.md +9 -3
- package/claude/skills/review-pr/references/close-out.md +2 -2
- package/claude/skills/review-pr/references/post.md +17 -5
- package/claude/skills/review-pr/references/scope.md +10 -8
- package/claude/skills/review-ui/SKILL.md +12 -4
- package/claude/skills/review-ui/references/post.md +4 -2
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +38 -1
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +4 -0
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +4 -4
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +6 -0
- package/claude/skills/role-orchestrator/scripts/poll.ts +90 -27
- package/claude/skills/role-orchestrator/scripts/repo.ts +3 -5
- package/claude/skills/role-orchestrator/scripts/watch.ts +4 -7
- package/claude/skills/search-craft/REQUIREMENT.md +1 -1
- package/claude/skills/search-craft/SKILL.md +3 -1
- package/claude/skills/search-craft/references/adopted.md +30 -0
- package/claude/skills/target-setup/references/detect.md +8 -0
- package/claude/skills/target-setup/references/indexes.md +13 -0
- package/claude/skills/target-setup/references/verify.md +8 -0
- package/claude/skills/test-first/SKILL.md +2 -0
- package/claude/skills/test-first/references/adopted.md +36 -0
- package/claude/skills/write-human/SKILL.md +1 -1
- package/claude/skills/write-human/references/machine-tells.md +13 -0
- package/docs/agents/canvas.md +4 -2
- package/docs/agents/capture.md +1 -1
- package/docs/agents/commands.md +47 -47
- package/docs/agents/context-audit-checks.md +2 -2
- package/docs/agents/index.md +2 -3
- package/docs/agents/key-changes.md +3 -3
- package/docs/agents/pr-evidence.md +11 -2
- package/docs/agents/pr-reads.md +5 -4
- package/docs/agents/records-stale.md +22 -9
- package/docs/agents/records.md +1 -1
- package/docs/agents/sandbox.md +1 -1
- package/docs/agents/skills-audit.md +10 -3
- package/docs/agents/slides.md +15 -3
- package/docs/target/projects.md +4 -4
- package/docs/workflow/ai-workflow.md +1 -2
- package/docs/workflow/index.md +1 -0
- package/docs/workflow/parallel-features.md +1 -1
- package/docs/workflow/skill-map-on-request.md +42 -0
- package/docs/workflow/skill-map.md +3 -36
- package/docs/workflow/visual-design-workflow.md +1 -0
- package/governance/renames.toml +0 -4
- package/governance/rules/standards/markdown.md +1 -0
- package/package.json +1 -2
- package/scripts/core/install-check.sh +1 -1
- package/scripts/lib/sandbox-dispatch.sh +1 -1
- package/scripts/lib/sandbox-git.sh +1 -9
- package/scripts/lib/sandbox-path.sh +1 -1
- package/scripts/sandbox-hook.sh +99 -0
- package/src/audits/catalog.ts +2 -2
- package/src/canvas/capture.ts +1 -9
- package/src/canvas/client/inspector/color-field.tsx +368 -0
- package/src/canvas/client/inspector/field.tsx +200 -0
- package/src/canvas/client/inspector/fields.css +292 -0
- package/src/canvas/client/inspector/section.tsx +27 -0
- package/src/canvas/client/inspector/values.ts +183 -0
- package/src/canvas/client/inspector.tsx +176 -160
- package/src/canvas/client/shell.css +0 -101
- package/src/capture/render.ts +45 -7
- package/src/capture/sources.ts +43 -0
- package/src/census/count.ts +1 -1
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/cases/workflow.ts +6 -1
- package/src/claude/seeds.ts +1 -2
- package/src/claude/skills-audit.ts +68 -14
- package/src/client-commands.ts +1 -1
- package/src/commands/claude.ts +27 -2
- package/src/commands/design.ts +2 -64
- package/src/commands/feedback.ts +3 -3
- package/src/commands/pr.ts +219 -202
- package/src/commands/records.ts +88 -6
- package/src/commands/sandbox.ts +50 -10
- package/src/commands/slides.ts +31 -77
- package/src/context/audit.ts +8 -7
- package/src/design/tokens.ts +2 -18
- package/src/docs/read.ts +1 -1
- package/src/{github.ts → feedback/github.ts} +1 -1
- package/src/gate/measures.ts +6 -4
- package/src/gov/citations.ts +1 -1
- package/src/gov/payload.ts +1 -1
- package/src/gov/superseded.ts +2 -2
- package/src/help.ts +2 -2
- package/src/markdown/ceiling.ts +1 -1
- package/src/migrate/skill-names.ts +7 -1
- package/src/pr/evidence.ts +25 -5
- package/src/pr/rest.ts +137 -0
- package/src/record-root.ts +1 -0
- package/src/records/backup.ts +1 -0
- package/src/records/push-guard.ts +1 -1
- package/src/records/stale-canonical.ts +146 -0
- package/src/records/stale.ts +1 -1
- package/src/sandbox/hooks.ts +128 -0
- package/src/sandbox/provision.ts +596 -0
- package/src/sandbox/tree.ts +103 -4
- package/src/secrets/marker.ts +2 -2
- package/src/secrets/scan.ts +1 -1
- package/src/shipped/references.ts +2 -2
- package/src/slides/convert/export.ts +2 -16
- package/src/slides/decks.ts +110 -0
- package/src/standards/read.ts +1 -1
- package/src/sync/seeds-report.ts +1 -1
- package/src/tasks/answers.ts +1 -1
- package/src/tasks/archive.ts +1 -1
- package/src/teach/nav.ts +1 -1
- package/src/tooling/inject.ts +1 -1
- package/src/web/readme-citations.ts +1 -1
- package/standards/architecture.md +1 -0
- package/standards/canvas.md +112 -0
- package/standards/context.md +2 -1
- package/standards/figures.md +0 -1
- package/standards/index.md +1 -1
- package/standards/markdown.md +1 -0
- package/standards/mermaid.md +1 -7
- package/standards/requirements.md +2 -0
- package/standards/skill-practice.md +2 -1
- package/standards/skill.md +1 -0
- package/tooling/claude/reference.md +0 -5
- package/claude/skills/draft-diagram/REQUIREMENT.md +0 -45
- package/claude/skills/draft-diagram/SKILL.md +0 -177
- package/docs/agents/design-board.md +0 -33
- package/governance/rules/standards/diagrams.md +0 -14
- package/governance/rules/standards/mermaid.md +0 -15
- package/scripts/core/check-gallery-exclusion.sh +0 -50
- package/scripts/manage-sandbox.sh +0 -472
- package/src/design/board.ts +0 -576
- package/src/slides/layouts.ts +0 -478
- package/src/slides/parse.ts +0 -84
- package/src/slides/render.ts +0 -110
- package/src/slides/styles.ts +0 -96
- package/standards/diagrams.md +0 -132
- package/tooling/claude/seeds/.claude/diagrams/index.md +0 -8
- /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
- /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
- /package/src/{github-format.ts → feedback/github-format.ts} +0 -0
- /package/src/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
- /package/src/{binary.ts → sweep/binary.ts} +0 -0
- /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
- /package/src/{paths.ts → tasks/paths.ts} +0 -0
- /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
/** @jsxImportSource preact */
|
|
2
2
|
import type { JSX } from 'preact'
|
|
3
3
|
import { addressOf, elementAt, excerpt, isRawText } from '@/canvas/address'
|
|
4
|
+
import { ColorField } from '@/canvas/client/inspector/color-field'
|
|
5
|
+
import {
|
|
6
|
+
Field,
|
|
7
|
+
ReadOnlyField,
|
|
8
|
+
type Scrub,
|
|
9
|
+
} from '@/canvas/client/inspector/field'
|
|
10
|
+
import { Section } from '@/canvas/client/inspector/section'
|
|
11
|
+
import {
|
|
12
|
+
clampScrub,
|
|
13
|
+
displayValue,
|
|
14
|
+
parseColor,
|
|
15
|
+
toCssValue,
|
|
16
|
+
} from '@/canvas/client/inspector/values'
|
|
4
17
|
import {
|
|
5
18
|
currentPage,
|
|
6
19
|
editElement,
|
|
@@ -16,8 +29,7 @@ import {
|
|
|
16
29
|
tokens,
|
|
17
30
|
writeError,
|
|
18
31
|
} from '@/canvas/client/state'
|
|
19
|
-
|
|
20
|
-
const FIELDS = ['x', 'y', 'width', 'height'] as const
|
|
32
|
+
import '@/canvas/client/inspector/fields.css'
|
|
21
33
|
|
|
22
34
|
/** Elements the parser never gives content, so they hold no text to edit. */
|
|
23
35
|
const VOID = new Set([
|
|
@@ -37,44 +49,38 @@ const VOID = new Set([
|
|
|
37
49
|
])
|
|
38
50
|
|
|
39
51
|
interface StyleField {
|
|
52
|
+
/** The full word, which names the field. */
|
|
40
53
|
readonly label: string
|
|
54
|
+
readonly glyph: string
|
|
41
55
|
readonly property: string
|
|
42
|
-
readonly
|
|
56
|
+
readonly isWide?: boolean
|
|
57
|
+
readonly placeholder?: string
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
/**
|
|
46
|
-
* The basic set,
|
|
47
|
-
* property outside it, so
|
|
61
|
+
* The basic set, grouped as the panel lists it. The writer refuses any
|
|
62
|
+
* property outside it, so these lists and the writer's have to agree.
|
|
48
63
|
*/
|
|
49
|
-
const
|
|
50
|
-
{ label: '
|
|
51
|
-
{ label: '
|
|
52
|
-
{ label: 'size', property: 'font-size' },
|
|
53
|
-
{ label: 'weight', property: 'font-weight' },
|
|
54
|
-
{ label: 'width', property: 'width' },
|
|
55
|
-
{ label: 'height', property: 'height' },
|
|
56
|
-
{ label: 'padding', property: 'padding' },
|
|
57
|
-
{ label: 'gap', property: 'gap' },
|
|
58
|
-
{ label: 'direction', property: 'flex-direction' },
|
|
64
|
+
const SIZE_FIELDS: readonly StyleField[] = [
|
|
65
|
+
{ label: 'width', glyph: 'W', property: 'width' },
|
|
66
|
+
{ label: 'height', glyph: 'H', property: 'height' },
|
|
59
67
|
]
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
69
|
+
const FLEX_FIELDS: readonly StyleField[] = [
|
|
70
|
+
{ label: 'direction', glyph: 'Dir', property: 'flex-direction' },
|
|
71
|
+
{ label: 'gap', glyph: 'Gap', property: 'gap', placeholder: '0' },
|
|
72
|
+
{ label: 'padding', glyph: 'Pad', property: 'padding', isWide: true },
|
|
73
|
+
]
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
...(font ? [{ name: 'font', value: font }] : []),
|
|
76
|
-
]
|
|
77
|
-
}
|
|
75
|
+
const TYPE_FIELDS: readonly StyleField[] = [
|
|
76
|
+
{ label: 'size', glyph: 'Size', property: 'font-size' },
|
|
77
|
+
{ label: 'weight', glyph: 'Wt', property: 'font-weight' },
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
const FILL_FIELDS = [
|
|
81
|
+
{ label: 'color', property: 'color' },
|
|
82
|
+
{ label: 'background', property: 'background-color' },
|
|
83
|
+
] as const
|
|
78
84
|
|
|
79
85
|
function inlineValue(element: Element, property: string): string {
|
|
80
86
|
return 'style' in element
|
|
@@ -118,8 +124,10 @@ function isRawInline(element: Element, property: string): boolean {
|
|
|
118
124
|
* an edit starts from what the operator sees.
|
|
119
125
|
*/
|
|
120
126
|
function currentValue(element: Element, property: string): string {
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
return inlineValue(element, property) || computedValue(element, property)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function computedValue(element: Element, property: string): string {
|
|
123
131
|
return (
|
|
124
132
|
element.ownerDocument.defaultView
|
|
125
133
|
?.getComputedStyle(element)
|
|
@@ -127,6 +135,19 @@ function currentValue(element: Element, property: string): string {
|
|
|
127
135
|
)
|
|
128
136
|
}
|
|
129
137
|
|
|
138
|
+
/**
|
|
139
|
+
* The color a token paints in the frame, read off a probe rather than the
|
|
140
|
+
* token's text, since a token's value may itself be a `var()` or a mix.
|
|
141
|
+
*/
|
|
142
|
+
function tokenPaint(doc: Document, name: string): string {
|
|
143
|
+
const probe = doc.createElement('span')
|
|
144
|
+
probe.style.setProperty('color', `var(${name})`)
|
|
145
|
+
;(doc.body ?? doc.documentElement).append(probe)
|
|
146
|
+
const painted = computedValue(probe, 'color')
|
|
147
|
+
probe.remove()
|
|
148
|
+
return painted
|
|
149
|
+
}
|
|
150
|
+
|
|
130
151
|
function holdsTextAlone(element: Element): boolean {
|
|
131
152
|
const tag = element.tagName.toLowerCase()
|
|
132
153
|
return element.children.length === 0 && !VOID.has(tag) && !isRawText(tag)
|
|
@@ -137,71 +158,6 @@ function elementName(element: Element): string {
|
|
|
137
158
|
return [tag, ...element.classList].join('.')
|
|
138
159
|
}
|
|
139
160
|
|
|
140
|
-
interface FieldProps {
|
|
141
|
-
readonly label: string
|
|
142
|
-
readonly initial: string
|
|
143
|
-
readonly isBusy: boolean
|
|
144
|
-
readonly onCommit: (value: string) => void
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Commits on change, which a text input fires on Enter or on leaving it, and
|
|
149
|
-
* sends nothing when the value is what it started at. Escape puts it back.
|
|
150
|
-
*/
|
|
151
|
-
function Field({ label, initial, isBusy, onCommit }: FieldProps): JSX.Element {
|
|
152
|
-
return (
|
|
153
|
-
<input
|
|
154
|
-
class="field"
|
|
155
|
-
type="text"
|
|
156
|
-
aria-label={label}
|
|
157
|
-
title={initial}
|
|
158
|
-
defaultValue={initial}
|
|
159
|
-
disabled={isBusy}
|
|
160
|
-
spellcheck={false}
|
|
161
|
-
onChange={(event) => {
|
|
162
|
-
const value = event.currentTarget.value.trim()
|
|
163
|
-
if (value !== initial.trim()) onCommit(value)
|
|
164
|
-
}}
|
|
165
|
-
onKeyDown={(event) => {
|
|
166
|
-
if (event.key === 'Escape') event.currentTarget.value = initial
|
|
167
|
-
}}
|
|
168
|
-
/>
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function TokenPicker({
|
|
173
|
-
label,
|
|
174
|
-
initial,
|
|
175
|
-
isBusy,
|
|
176
|
-
onCommit,
|
|
177
|
-
}: FieldProps): JSX.Element | null {
|
|
178
|
-
const colors =
|
|
179
|
-
tokens.value?.groups?.find((group) => group.kind === 'color')?.tokens ?? []
|
|
180
|
-
if (colors.length === 0) return null
|
|
181
|
-
return (
|
|
182
|
-
<select
|
|
183
|
-
class="token-picker"
|
|
184
|
-
aria-label={`${label} token`}
|
|
185
|
-
title="Pick a project token"
|
|
186
|
-
value={
|
|
187
|
-
colors.some((token) => initial === `var(${token.name})`) ? initial : ''
|
|
188
|
-
}
|
|
189
|
-
disabled={isBusy}
|
|
190
|
-
onChange={(event) => {
|
|
191
|
-
const { value } = event.currentTarget
|
|
192
|
-
if (value !== '' && value !== initial) onCommit(value)
|
|
193
|
-
}}
|
|
194
|
-
>
|
|
195
|
-
<option value="">Token</option>
|
|
196
|
-
{colors.map((token) => (
|
|
197
|
-
<option key={token.name} value={`var(${token.name})`}>
|
|
198
|
-
{token.name}
|
|
199
|
-
</option>
|
|
200
|
-
))}
|
|
201
|
-
</select>
|
|
202
|
-
)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
161
|
function ElementFields({
|
|
206
162
|
node,
|
|
207
163
|
doc,
|
|
@@ -219,65 +175,119 @@ function ElementFields({
|
|
|
219
175
|
const commit = (property: string) => (value: string) => {
|
|
220
176
|
if (address) void editElement(frameRef, key, address, property, value)
|
|
221
177
|
}
|
|
222
|
-
const
|
|
178
|
+
const rect = node.getBoundingClientRect()
|
|
179
|
+
const font = currentValue(node, 'font-family')
|
|
223
180
|
const text = node.textContent ?? ''
|
|
181
|
+
const colorTokens =
|
|
182
|
+
tokens.value?.groups
|
|
183
|
+
?.find((group) => group.kind === 'color')
|
|
184
|
+
?.tokens.map((token) => token.name) ?? []
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Previews into the frame's own inline style and writes through the same
|
|
188
|
+
* edit as typing. A reload mid-drag replaces the document, so a release
|
|
189
|
+
* against one no longer on screen posts nothing.
|
|
190
|
+
*/
|
|
191
|
+
const scrubOf = (property: string): Scrub => ({
|
|
192
|
+
clamp: (value) => clampScrub(property, value),
|
|
193
|
+
begin: () => {
|
|
194
|
+
const original = inlineValue(node, property)
|
|
195
|
+
const style = (node as HTMLElement).style
|
|
196
|
+
return {
|
|
197
|
+
preview: (shown) =>
|
|
198
|
+
style.setProperty(property, toCssValue(property, shown)),
|
|
199
|
+
restore: () =>
|
|
200
|
+
original
|
|
201
|
+
? style.setProperty(property, original)
|
|
202
|
+
: style.removeProperty(property),
|
|
203
|
+
commit: (shown) => {
|
|
204
|
+
if (frameDocuments.value.get(key) !== doc || !node.isConnected) return
|
|
205
|
+
commit(property)(toCssValue(property, shown))
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const styleField = (field: StyleField): JSX.Element => (
|
|
212
|
+
<Field
|
|
213
|
+
key={field.property}
|
|
214
|
+
label={field.label}
|
|
215
|
+
glyph={field.glyph}
|
|
216
|
+
initial={displayValue(field.property, currentValue(node, field.property))}
|
|
217
|
+
isBusy={isBusy}
|
|
218
|
+
isWide={field.isWide}
|
|
219
|
+
placeholder={field.placeholder}
|
|
220
|
+
onCommit={(typed) =>
|
|
221
|
+
commit(field.property)(toCssValue(field.property, typed))
|
|
222
|
+
}
|
|
223
|
+
scrub={'style' in node ? scrubOf(field.property) : undefined}
|
|
224
|
+
/>
|
|
225
|
+
)
|
|
224
226
|
|
|
225
227
|
return (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
{
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
228
|
+
<>
|
|
229
|
+
<Section title="Layout">
|
|
230
|
+
<ReadOnlyField
|
|
231
|
+
label="x"
|
|
232
|
+
glyph="X"
|
|
233
|
+
value={String(Math.round(rect.left))}
|
|
234
|
+
/>
|
|
235
|
+
<ReadOnlyField
|
|
236
|
+
label="y"
|
|
237
|
+
glyph="Y"
|
|
238
|
+
value={String(Math.round(rect.top))}
|
|
239
|
+
/>
|
|
240
|
+
{SIZE_FIELDS.map(styleField)}
|
|
241
|
+
</Section>
|
|
242
|
+
<Section title="Flex">{FLEX_FIELDS.map(styleField)}</Section>
|
|
243
|
+
<Section title="Typography">
|
|
244
|
+
{TYPE_FIELDS.map(styleField)}
|
|
245
|
+
{font ? (
|
|
246
|
+
<ReadOnlyField label="font" glyph="Font" value={font} isWide />
|
|
247
|
+
) : null}
|
|
248
|
+
</Section>
|
|
249
|
+
<Section title="Fill">
|
|
250
|
+
{FILL_FIELDS.map((field) => {
|
|
251
|
+
const painted = computedValue(node, field.property)
|
|
252
|
+
const inline = inlineValue(node, field.property)
|
|
253
|
+
/*
|
|
254
|
+
* An inline value the picker cannot read stays as written. Reading
|
|
255
|
+
* the computed color in its place would show a token-following
|
|
256
|
+
* value as a literal, and an opacity edit would freeze it.
|
|
257
|
+
*/
|
|
258
|
+
return (
|
|
259
|
+
<ColorField
|
|
260
|
+
key={field.property}
|
|
261
|
+
label={field.label}
|
|
262
|
+
color={parseColor(inline || painted)}
|
|
263
|
+
text={inline || painted}
|
|
264
|
+
painted={painted}
|
|
265
|
+
tokens={colorTokens}
|
|
266
|
+
paint={(name) => tokenPaint(doc, name)}
|
|
267
|
+
isRaw={isRawInline(node, field.property)}
|
|
240
268
|
isBusy={isBusy}
|
|
241
|
-
onCommit={commit(
|
|
269
|
+
onCommit={commit(field.property)}
|
|
242
270
|
/>
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
title="Set as a raw value, so it will not follow the theme. Pick a token to fix it"
|
|
264
|
-
>
|
|
265
|
-
raw
|
|
266
|
-
</span>
|
|
267
|
-
) : null}
|
|
268
|
-
{field.isColor ? (
|
|
269
|
-
<TokenPicker
|
|
270
|
-
label={field.label}
|
|
271
|
-
initial={initial}
|
|
272
|
-
isBusy={isBusy}
|
|
273
|
-
onCommit={commit(field.property)}
|
|
274
|
-
/>
|
|
275
|
-
) : null}
|
|
276
|
-
</dd>
|
|
277
|
-
</div>
|
|
278
|
-
)
|
|
279
|
-
})}
|
|
280
|
-
</dl>
|
|
271
|
+
)
|
|
272
|
+
})}
|
|
273
|
+
</Section>
|
|
274
|
+
<Section title="Text">
|
|
275
|
+
{holdsTextAlone(node) ? (
|
|
276
|
+
<Field
|
|
277
|
+
label="text"
|
|
278
|
+
glyph="T"
|
|
279
|
+
initial={text}
|
|
280
|
+
isBusy={isBusy}
|
|
281
|
+
isWide
|
|
282
|
+
onCommit={commit('text')}
|
|
283
|
+
/>
|
|
284
|
+
) : (
|
|
285
|
+
<p class="text-excerpt is-wide" title={excerpt(text)}>
|
|
286
|
+
{excerpt(text)}
|
|
287
|
+
</p>
|
|
288
|
+
)}
|
|
289
|
+
</Section>
|
|
290
|
+
</>
|
|
281
291
|
)
|
|
282
292
|
}
|
|
283
293
|
|
|
@@ -342,14 +352,20 @@ export function Inspector(): JSX.Element {
|
|
|
342
352
|
<p class="detail" title={frame.name}>
|
|
343
353
|
{frame.name}
|
|
344
354
|
</p>
|
|
345
|
-
<
|
|
346
|
-
{
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
355
|
+
<div class="field-grid">
|
|
356
|
+
<ReadOnlyField label="x" glyph="X" value={String(frame.x)} />
|
|
357
|
+
<ReadOnlyField label="y" glyph="Y" value={String(frame.y)} />
|
|
358
|
+
<ReadOnlyField
|
|
359
|
+
label="width"
|
|
360
|
+
glyph="W"
|
|
361
|
+
value={String(frame.width)}
|
|
362
|
+
/>
|
|
363
|
+
<ReadOnlyField
|
|
364
|
+
label="height"
|
|
365
|
+
glyph="H"
|
|
366
|
+
value={String(frame.height)}
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
353
369
|
</>
|
|
354
370
|
) : (
|
|
355
371
|
<p class="empty">Select a frame to see its box</p>
|
|
@@ -320,29 +320,6 @@ code {
|
|
|
320
320
|
outline-offset: 6px;
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
.box {
|
|
324
|
-
display: grid;
|
|
325
|
-
grid-template-columns: 1fr 1fr;
|
|
326
|
-
gap: var(--space-xs) var(--space-md);
|
|
327
|
-
margin: var(--space-xs) 0 0;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
.box-row {
|
|
331
|
-
display: flex;
|
|
332
|
-
justify-content: space-between;
|
|
333
|
-
gap: var(--space-sm);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
.box dt {
|
|
337
|
-
color: var(--color-muted);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
.box dd {
|
|
341
|
-
margin: 0;
|
|
342
|
-
color: var(--color-text);
|
|
343
|
-
font-variant-numeric: tabular-nums;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
323
|
.toolbar {
|
|
347
324
|
position: absolute;
|
|
348
325
|
right: var(--space-md);
|
|
@@ -436,84 +413,6 @@ code {
|
|
|
436
413
|
white-space: nowrap;
|
|
437
414
|
}
|
|
438
415
|
|
|
439
|
-
/* One property a row, since a computed color or font stack outgrows a half. */
|
|
440
|
-
.styles {
|
|
441
|
-
grid-template-columns: 1fr;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
.styles dd {
|
|
445
|
-
min-width: 0;
|
|
446
|
-
overflow: hidden;
|
|
447
|
-
text-overflow: ellipsis;
|
|
448
|
-
white-space: nowrap;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
.styles .box-row {
|
|
452
|
-
align-items: center;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
.styles dt {
|
|
456
|
-
flex: none;
|
|
457
|
-
width: 4.5rem;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
.styles dd {
|
|
461
|
-
flex: 1;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
.field-cell {
|
|
465
|
-
display: flex;
|
|
466
|
-
gap: var(--space-xs);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
.field,
|
|
470
|
-
.token-picker {
|
|
471
|
-
min-width: 0;
|
|
472
|
-
min-height: 24px;
|
|
473
|
-
padding: 2px var(--space-xs);
|
|
474
|
-
border: 1px solid var(--color-border);
|
|
475
|
-
border-radius: var(--radius-action);
|
|
476
|
-
background: var(--color-surface);
|
|
477
|
-
color: var(--color-text);
|
|
478
|
-
font: inherit;
|
|
479
|
-
font-variant-numeric: tabular-nums;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
.field {
|
|
483
|
-
flex: 1;
|
|
484
|
-
width: 100%;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
.token-picker {
|
|
488
|
-
flex: none;
|
|
489
|
-
width: 4.5rem;
|
|
490
|
-
cursor: pointer;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
.raw {
|
|
494
|
-
flex: none;
|
|
495
|
-
align-self: center;
|
|
496
|
-
color: var(--color-text-secondary);
|
|
497
|
-
cursor: help;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
.field:hover,
|
|
501
|
-
.token-picker:hover {
|
|
502
|
-
border-color: var(--color-muted);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
.field:disabled,
|
|
506
|
-
.token-picker:disabled {
|
|
507
|
-
opacity: 0.5;
|
|
508
|
-
cursor: not-allowed;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
.saved {
|
|
512
|
-
margin-left: var(--space-sm);
|
|
513
|
-
color: var(--color-success);
|
|
514
|
-
font-weight: 400;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
416
|
.tabs {
|
|
518
417
|
display: flex;
|
|
519
418
|
gap: 2px;
|
package/src/capture/render.ts
CHANGED
|
@@ -2,9 +2,13 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
|
2
2
|
import { basename, dirname } from 'node:path'
|
|
3
3
|
import { pathToFileURL } from 'node:url'
|
|
4
4
|
import { chromium } from 'playwright-core'
|
|
5
|
-
import type { Browser, Page } from 'playwright-core'
|
|
5
|
+
import type { Browser, Locator, Page } from 'playwright-core'
|
|
6
6
|
import type { CaptureSource } from '@/capture/sources'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
isGenericFamily,
|
|
9
|
+
resolveCaptureSources,
|
|
10
|
+
textFamilies,
|
|
11
|
+
} from '@/capture/sources'
|
|
8
12
|
import { formatStamp, hashSource, stampPath } from '@/capture/stamp'
|
|
9
13
|
|
|
10
14
|
/**
|
|
@@ -97,15 +101,22 @@ async function captureOne(
|
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
await page.evaluate(() => document.fonts.ready.then(() => undefined))
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
if (family && !(await resolvesFont(page, family))) {
|
|
104
|
+
const declarations = await renderedTextFamilies(element)
|
|
105
|
+
const generic = declarations.find(isGenericFamily)
|
|
106
|
+
if (generic !== undefined) {
|
|
104
107
|
return failed(
|
|
105
108
|
source,
|
|
106
|
-
`${
|
|
109
|
+
`${generic.split(',')[0]?.trim()} is a generic family that resolves differently per machine, so name a real font`,
|
|
107
110
|
)
|
|
108
111
|
}
|
|
112
|
+
for (const family of textFamilies(declarations)) {
|
|
113
|
+
if (!(await resolvesFont(page, family))) {
|
|
114
|
+
return failed(
|
|
115
|
+
source,
|
|
116
|
+
`${family} is not installed, so the capture would rewrap against a fallback`,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
109
120
|
|
|
110
121
|
mkdirSync(dirname(source.pngPath), { recursive: true })
|
|
111
122
|
const png = await element.screenshot({ omitBackground: true })
|
|
@@ -170,6 +181,33 @@ function failed(source: CaptureSource, reason: string): CaptureResult {
|
|
|
170
181
|
return { status: 'failed', source: sourceIdentifier(source), reason }
|
|
171
182
|
}
|
|
172
183
|
|
|
184
|
+
/**
|
|
185
|
+
* The computed families of the captured element and of each element under it
|
|
186
|
+
* holding a non-blank text node of its own that draws a box, which is the text
|
|
187
|
+
* the screenshot shows. Testing the text node's box rather than the element's
|
|
188
|
+
* keeps out `<style>`, `<script>`, `<title>`, and anything hidden, whose
|
|
189
|
+
* computed family is the browser default and would refuse every source that
|
|
190
|
+
* styles a descendant rather than the root, and keeps in text directly inside a
|
|
191
|
+
* `display: contents` element, which draws no box of its own.
|
|
192
|
+
*
|
|
193
|
+
* Returns nothing when no element holds text, since a font cannot rewrap text
|
|
194
|
+
* that is not there, so a container of iframes skips the probe.
|
|
195
|
+
*/
|
|
196
|
+
async function renderedTextFamilies(element: Locator): Promise<string[]> {
|
|
197
|
+
return element.evaluate((root) => {
|
|
198
|
+
const range = document.createRange()
|
|
199
|
+
const holdsText = (node: Element): boolean =>
|
|
200
|
+
[...node.childNodes].some((child) => {
|
|
201
|
+
if (child.nodeType !== Node.TEXT_NODE) return false
|
|
202
|
+
if ((child.textContent ?? '').trim() === '') return false
|
|
203
|
+
range.selectNodeContents(child)
|
|
204
|
+
return range.getClientRects().length > 0
|
|
205
|
+
})
|
|
206
|
+
const texts = [root, ...root.querySelectorAll('*')].filter(holdsText)
|
|
207
|
+
return texts.map((node) => getComputedStyle(node).fontFamily)
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
173
211
|
/**
|
|
174
212
|
* Compares the family's text metrics against a family that cannot exist. Equal
|
|
175
213
|
* widths mean the browser fell through to its default for both, which is the
|
package/src/capture/sources.ts
CHANGED
|
@@ -66,3 +66,46 @@ export function primaryFontFamily(declaration: string): string {
|
|
|
66
66
|
const first = declaration.split(',')[0]?.trim() ?? ''
|
|
67
67
|
return first.replace(/^['"]|['"]$/g, '')
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The CSS generic family keywords. A generic names no font, so each machine
|
|
72
|
+
* maps it to whatever it has, and a capture measured against one cannot
|
|
73
|
+
* reproduce on another.
|
|
74
|
+
*/
|
|
75
|
+
export const GENERIC_FAMILIES: ReadonlySet<string> = new Set([
|
|
76
|
+
'serif',
|
|
77
|
+
'sans-serif',
|
|
78
|
+
'monospace',
|
|
79
|
+
'cursive',
|
|
80
|
+
'fantasy',
|
|
81
|
+
'system-ui',
|
|
82
|
+
'ui-serif',
|
|
83
|
+
'ui-sans-serif',
|
|
84
|
+
'ui-monospace',
|
|
85
|
+
'ui-rounded',
|
|
86
|
+
'math',
|
|
87
|
+
'emoji',
|
|
88
|
+
'fangsong',
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether the first family of a computed `font-family` is a generic keyword.
|
|
93
|
+
* Takes the raw declaration rather than `primaryFontFamily`'s result, since
|
|
94
|
+
* quoting is what separates the keyword from a family that happens to share its
|
|
95
|
+
* name, and the stripped form has lost it. Keywords match case-insensitively.
|
|
96
|
+
*/
|
|
97
|
+
export function isGenericFamily(declaration: string): boolean {
|
|
98
|
+
const first = declaration.split(',')[0]?.trim() ?? ''
|
|
99
|
+
return GENERIC_FAMILIES.has(first.toLowerCase())
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reduces the computed `font-family` of each rendered text element to the
|
|
104
|
+
* distinct families a capture has to prove resolved, in document order, so a
|
|
105
|
+
* frame that sets its font on any ancestor of its text is checked against the
|
|
106
|
+
* face the text actually draws in.
|
|
107
|
+
*/
|
|
108
|
+
export function textFamilies(declarations: readonly string[]): string[] {
|
|
109
|
+
const families = declarations.map(primaryFontFamily).filter(Boolean)
|
|
110
|
+
return [...new Set(families)]
|
|
111
|
+
}
|
package/src/census/count.ts
CHANGED
package/src/claude/cases/misc.ts
CHANGED
|
@@ -27,6 +27,11 @@ export const MISC_CASES: readonly SkillCase[] = [
|
|
|
27
27
|
"I found two sites whose look I want us to draw from. Put them side by side and let's settle which one the design should follow before anyone writes a token.",
|
|
28
28
|
expect: 'sketch-design',
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
prompt:
|
|
32
|
+
'Draw the signup screen as a frame on the canvas, then restyle the element I just selected there.',
|
|
33
|
+
expect: 'canvas',
|
|
34
|
+
},
|
|
30
35
|
{
|
|
31
36
|
prompt: 'Fire up the dev server the way this project documents it.',
|
|
32
37
|
expect: 'project-commands',
|
|
@@ -26,7 +26,7 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
prompt: 'Draw me a diagram of how the pieces of this system connect.',
|
|
29
|
-
expect: 'draft-
|
|
29
|
+
expect: 'draft-figure',
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
prompt:
|
|
@@ -146,6 +146,11 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
|
|
|
146
146
|
'Which of our docs have gone stale, grown too long, or ended up in the wrong file?',
|
|
147
147
|
expect: 'document-health',
|
|
148
148
|
},
|
|
149
|
+
{
|
|
150
|
+
prompt:
|
|
151
|
+
'Are our requirements and architecture docs still true, and when were they last reviewed?',
|
|
152
|
+
expect: 'document-health',
|
|
153
|
+
},
|
|
149
154
|
{
|
|
150
155
|
prompt: 'Open a new entry on the task board for this piece of work.',
|
|
151
156
|
expect: 'task-board',
|