@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
|
@@ -0,0 +1,292 @@
|
|
|
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
|
+
.raw {
|
|
112
|
+
flex: none;
|
|
113
|
+
color: var(--color-text-secondary);
|
|
114
|
+
cursor: help;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.text-excerpt {
|
|
118
|
+
margin: var(--space-xs) 0 0;
|
|
119
|
+
color: var(--color-text-body);
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
text-overflow: ellipsis;
|
|
122
|
+
white-space: nowrap;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.saved {
|
|
126
|
+
margin-left: var(--space-sm);
|
|
127
|
+
color: var(--color-success);
|
|
128
|
+
font-weight: 400;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
* A color is one field: a swatch leading in the glyph's lane, the hex, and the
|
|
133
|
+
* opacity right-aligned. The picker opens in the flow under it, so it can never
|
|
134
|
+
* reach past the panel's edge or scroll it sideways.
|
|
135
|
+
*/
|
|
136
|
+
.color-field {
|
|
137
|
+
min-width: 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.swatch {
|
|
141
|
+
flex: none;
|
|
142
|
+
display: grid;
|
|
143
|
+
place-items: center;
|
|
144
|
+
width: 1.75rem;
|
|
145
|
+
min-height: 22px;
|
|
146
|
+
padding: 0;
|
|
147
|
+
border: 0;
|
|
148
|
+
background: transparent;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.swatch:disabled {
|
|
153
|
+
opacity: 0.5;
|
|
154
|
+
cursor: not-allowed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/*
|
|
158
|
+
* The paint is the top layer over a checker, so an opacity below full reads
|
|
159
|
+
* as one. `--paint` carries the color, since a background color sits under
|
|
160
|
+
* every image layer and the checker would cover it.
|
|
161
|
+
*/
|
|
162
|
+
.swatch-paint {
|
|
163
|
+
--paint: transparent;
|
|
164
|
+
flex: none;
|
|
165
|
+
width: 12px;
|
|
166
|
+
height: 12px;
|
|
167
|
+
border: 1px solid var(--color-border);
|
|
168
|
+
border-radius: 2px;
|
|
169
|
+
background-image:
|
|
170
|
+
linear-gradient(var(--paint), var(--paint)),
|
|
171
|
+
linear-gradient(45deg, var(--color-border) 25%, transparent 25%),
|
|
172
|
+
linear-gradient(-45deg, var(--color-border) 25%, transparent 25%),
|
|
173
|
+
linear-gradient(45deg, transparent 75%, var(--color-border) 75%),
|
|
174
|
+
linear-gradient(-45deg, transparent 75%, var(--color-border) 75%);
|
|
175
|
+
background-size:
|
|
176
|
+
auto,
|
|
177
|
+
6px 6px,
|
|
178
|
+
6px 6px,
|
|
179
|
+
6px 6px,
|
|
180
|
+
6px 6px;
|
|
181
|
+
background-position:
|
|
182
|
+
0 0,
|
|
183
|
+
0 0,
|
|
184
|
+
0 3px,
|
|
185
|
+
3px -3px,
|
|
186
|
+
-3px 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.swatch:hover .swatch-paint {
|
|
190
|
+
border-color: var(--color-muted);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* No fill draws a slash rather than a color. */
|
|
194
|
+
.swatch.is-none .swatch-paint {
|
|
195
|
+
background-image: linear-gradient(
|
|
196
|
+
to top right,
|
|
197
|
+
transparent calc(50% - 1px),
|
|
198
|
+
var(--color-muted) calc(50% - 1px),
|
|
199
|
+
var(--color-muted) calc(50% + 1px),
|
|
200
|
+
transparent calc(50% + 1px)
|
|
201
|
+
);
|
|
202
|
+
background-size: auto;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.color-field .glyph-field-input {
|
|
206
|
+
text-transform: uppercase;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Only a hex reads in capitals. A value the picker cannot read stays as written. */
|
|
210
|
+
.color-field .glyph-field-input::placeholder,
|
|
211
|
+
.color-field .glyph-field-input.is-text {
|
|
212
|
+
text-transform: none;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* A token name is a name, so it keeps its own case. */
|
|
216
|
+
.color-field .glyph-field-input.is-token {
|
|
217
|
+
flex: 0 1 auto;
|
|
218
|
+
margin-right: auto;
|
|
219
|
+
padding: 0 var(--space-xs);
|
|
220
|
+
border-radius: var(--radius-action);
|
|
221
|
+
background: var(--color-background);
|
|
222
|
+
text-transform: none;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.color-opacity {
|
|
226
|
+
flex: none;
|
|
227
|
+
width: 2.25rem;
|
|
228
|
+
padding: 2px 0;
|
|
229
|
+
border: 0;
|
|
230
|
+
background: transparent;
|
|
231
|
+
color: var(--color-text);
|
|
232
|
+
font: inherit;
|
|
233
|
+
font-variant-numeric: tabular-nums;
|
|
234
|
+
text-align: right;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.color-opacity:disabled {
|
|
238
|
+
color: var(--color-muted);
|
|
239
|
+
cursor: not-allowed;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.color-percent {
|
|
243
|
+
flex: none;
|
|
244
|
+
padding-right: var(--space-xs);
|
|
245
|
+
color: var(--color-muted);
|
|
246
|
+
user-select: none;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.color-picker {
|
|
250
|
+
display: flex;
|
|
251
|
+
flex-direction: column;
|
|
252
|
+
gap: var(--space-xs);
|
|
253
|
+
margin-top: var(--space-xs);
|
|
254
|
+
padding: var(--space-xs);
|
|
255
|
+
border: 1px solid var(--color-border);
|
|
256
|
+
border-radius: var(--radius-action);
|
|
257
|
+
background: var(--color-chrome);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.token-list {
|
|
261
|
+
max-height: 12rem;
|
|
262
|
+
margin: 0;
|
|
263
|
+
padding: 0;
|
|
264
|
+
overflow-y: auto;
|
|
265
|
+
list-style: none;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.token-option {
|
|
269
|
+
display: flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
gap: var(--space-xs);
|
|
272
|
+
min-height: 24px;
|
|
273
|
+
padding: 0 var(--space-xs);
|
|
274
|
+
border-radius: var(--radius-action);
|
|
275
|
+
cursor: pointer;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.token-option:hover,
|
|
279
|
+
.token-option.is-active {
|
|
280
|
+
background: var(--color-surface);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.token-option[aria-selected='true'] .token-name {
|
|
284
|
+
color: var(--color-accent);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.token-name {
|
|
288
|
+
min-width: 0;
|
|
289
|
+
overflow: hidden;
|
|
290
|
+
text-overflow: ellipsis;
|
|
291
|
+
white-space: nowrap;
|
|
292
|
+
}
|
|
@@ -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,183 @@
|
|
|
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
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A color the picker can write: a token or a hex, each with an opacity. */
|
|
63
|
+
export type WrittenColor =
|
|
64
|
+
| {
|
|
65
|
+
readonly kind: 'token'
|
|
66
|
+
/** The custom property name as the token sheet gives it. */
|
|
67
|
+
readonly name: string
|
|
68
|
+
readonly opacity: number
|
|
69
|
+
}
|
|
70
|
+
| { readonly kind: 'hex'; readonly hex: string; readonly opacity: number }
|
|
71
|
+
|
|
72
|
+
export type ParsedColor = WrittenColor | { readonly kind: 'none' }
|
|
73
|
+
|
|
74
|
+
const HEX = /^#?([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i
|
|
75
|
+
const RGB =
|
|
76
|
+
/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/i
|
|
77
|
+
/** What a browser computes `color-mix()` in srgb to, channels 0 to 1. */
|
|
78
|
+
const SRGB =
|
|
79
|
+
/^color\(\s*srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+%?))?\s*\)$/i
|
|
80
|
+
const VAR = /^var\(\s*(--[\w-]+)\s*\)$/i
|
|
81
|
+
const MIX =
|
|
82
|
+
/^color-mix\(\s*in\s+srgb\s*,\s*var\(\s*(--[\w-]+)\s*\)\s+([\d.]+)%\s*,\s*transparent\s*\)$/i
|
|
83
|
+
|
|
84
|
+
function channel(value: number): string {
|
|
85
|
+
return Math.round(Math.min(255, Math.max(0, value)))
|
|
86
|
+
.toString(16)
|
|
87
|
+
.padStart(2, '0')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* An alpha as a percent. A value past 1 clamps to full rather than reading as
|
|
92
|
+
* a 0 to 255 scale, since CSS clamps it the same way when it paints.
|
|
93
|
+
*/
|
|
94
|
+
function alphaPercent(alpha: string | undefined): number {
|
|
95
|
+
if (alpha === undefined) return 100
|
|
96
|
+
const fraction = alpha.endsWith('%')
|
|
97
|
+
? Number(alpha.slice(0, -1)) / 100
|
|
98
|
+
: Number(alpha)
|
|
99
|
+
return Math.round(Math.min(1, Math.max(0, fraction)) * 100)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A typed hex in any of its lengths, normalized to six digits. */
|
|
103
|
+
export function readHex(
|
|
104
|
+
typed: string,
|
|
105
|
+
): { readonly hex: string; readonly opacity?: number } | undefined {
|
|
106
|
+
const match = typed.trim().match(HEX)
|
|
107
|
+
if (!match) return undefined
|
|
108
|
+
const digits = match[1].toLowerCase()
|
|
109
|
+
const full =
|
|
110
|
+
digits.length <= 4
|
|
111
|
+
? [...digits].map((digit) => digit + digit).join('')
|
|
112
|
+
: digits
|
|
113
|
+
const hex = full.slice(0, 6)
|
|
114
|
+
if (full.length === 6) return { hex }
|
|
115
|
+
return {
|
|
116
|
+
hex,
|
|
117
|
+
opacity: Math.round((parseInt(full.slice(6), 16) / 255) * 100),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A typed opacity percent, refused outside 0 to 100. */
|
|
122
|
+
export function readOpacity(typed: string): number | undefined {
|
|
123
|
+
const value = typed.trim().replace(/%$/, '')
|
|
124
|
+
if (!/^-?\d*\.?\d+$/.test(value)) return undefined
|
|
125
|
+
const percent = Number(value)
|
|
126
|
+
return percent >= 0 && percent <= 100 ? Math.round(percent) : undefined
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Reads a computed or inline color into what the picker shows. A form it does
|
|
131
|
+
* not know, such as a named color, comes back undefined.
|
|
132
|
+
*/
|
|
133
|
+
export function parseColor(value: string): ParsedColor | undefined {
|
|
134
|
+
const text = value.trim()
|
|
135
|
+
if (isNoFill(text)) return { kind: 'none' }
|
|
136
|
+
const token = text.match(VAR)
|
|
137
|
+
if (token) return { kind: 'token', name: token[1], opacity: 100 }
|
|
138
|
+
const mix = text.match(MIX)
|
|
139
|
+
if (mix) {
|
|
140
|
+
return {
|
|
141
|
+
kind: 'token',
|
|
142
|
+
name: mix[1],
|
|
143
|
+
opacity: Math.round(Math.min(100, Number(mix[2]))),
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const hex = text.startsWith('#') ? readHex(text) : undefined
|
|
147
|
+
if (hex) return painted(hex.hex, hex.opacity ?? 100)
|
|
148
|
+
const rgb = text.match(RGB)
|
|
149
|
+
if (rgb) {
|
|
150
|
+
return painted(
|
|
151
|
+
[rgb[1], rgb[2], rgb[3]].map((part) => channel(Number(part))).join(''),
|
|
152
|
+
alphaPercent(rgb[4]),
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
const srgb = text.match(SRGB)
|
|
156
|
+
if (!srgb) return undefined
|
|
157
|
+
return painted(
|
|
158
|
+
[srgb[1], srgb[2], srgb[3]]
|
|
159
|
+
.map((part) => channel(Number(part) * 255))
|
|
160
|
+
.join(''),
|
|
161
|
+
alphaPercent(srgb[4]),
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A color at zero opacity paints nothing, whichever form wrote it. */
|
|
166
|
+
function painted(hex: string, opacity: number): ParsedColor {
|
|
167
|
+
return opacity === 0 ? { kind: 'none' } : { kind: 'hex', hex, opacity }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The value a pick writes. A token below full opacity mixes with transparent
|
|
172
|
+
* rather than resolving to a hex, so it keeps following the theme.
|
|
173
|
+
*/
|
|
174
|
+
export function composeColor(color: WrittenColor): string {
|
|
175
|
+
if (color.kind === 'token') {
|
|
176
|
+
return color.opacity >= 100
|
|
177
|
+
? `var(${color.name})`
|
|
178
|
+
: `color-mix(in srgb, var(${color.name}) ${color.opacity}%, transparent)`
|
|
179
|
+
}
|
|
180
|
+
return color.opacity >= 100
|
|
181
|
+
? `#${color.hex}`
|
|
182
|
+
: `#${color.hex}${channel((color.opacity / 100) * 255)}`
|
|
183
|
+
}
|