@erclx/canon 5.53.0 → 5.55.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 (127) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +10 -9
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +5 -5
  5. package/claude/skills/create-skill/SKILL.md +1 -1
  6. package/claude/skills/design-identity/REQUIREMENT.md +2 -2
  7. package/claude/skills/design-identity/SKILL.md +6 -6
  8. package/claude/skills/design-record/REQUIREMENT.md +2 -2
  9. package/claude/skills/design-record/SKILL.md +4 -4
  10. package/claude/skills/design-record/references/folder.md +1 -1
  11. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  12. package/claude/skills/document-health/SKILL.md +6 -6
  13. package/claude/skills/document-repair/SKILL.md +5 -5
  14. package/claude/skills/draw-figure/REQUIREMENT.md +1 -1
  15. package/claude/skills/draw-figure/SKILL.md +1 -1
  16. package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -4
  17. package/claude/skills/pick-by-eye/SKILL.md +34 -41
  18. package/claude/skills/pick-by-eye/references/board.md +42 -0
  19. package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
  20. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  21. package/claude/skills/target-check/SKILL.md +6 -8
  22. package/claude/skills/target-check/references/domains.md +1 -4
  23. package/claude/skills/target-setup/SKILL.md +8 -8
  24. package/claude/skills/target-update/SKILL.md +5 -5
  25. package/claude/skills/teach-craft/REQUIREMENT.md +1 -1
  26. package/claude/skills/teach-craft/SKILL.md +32 -1
  27. package/claude/skills/teach-craft/references/adopted.md +113 -0
  28. package/claude/skills/teach-craft/references/lesson-craft.md +10 -2
  29. package/claude/skills/teach-craft/references/pedagogy.md +29 -2
  30. package/docs/agents/install-and-sync.md +2 -10
  31. package/docs/agents/output-shape.md +11 -10
  32. package/docs/target/migrations.md +8 -3
  33. package/docs/target/projects.md +1 -1
  34. package/docs/target/sync.md +1 -1
  35. package/docs/workflow/skill-map.md +0 -1
  36. package/docs/workflow/visual-design-workflow.md +1 -4
  37. package/governance/detect.toml +1 -6
  38. package/governance/renames.toml +0 -1
  39. package/package.json +2 -5
  40. package/scripts/core/install-check.sh +1 -1
  41. package/scripts/lib/ui.sh +12 -65
  42. package/src/audits/run.ts +1 -1
  43. package/src/claude/cases/misc.ts +0 -5
  44. package/src/claude/routing.ts +1 -1
  45. package/src/claude/skills-audit.ts +1 -0
  46. package/src/cli/help.ts +0 -3
  47. package/src/cli/run.ts +5 -6
  48. package/src/cli/ui.ts +87 -28
  49. package/src/cli.ts +6 -2
  50. package/src/commands/claude.ts +2 -3
  51. package/src/commands/design.ts +20 -324
  52. package/src/commands/gate.ts +5 -3
  53. package/src/commands/gov.ts +11 -21
  54. package/src/commands/labels.ts +1 -1
  55. package/src/commands/pr.ts +1 -1
  56. package/src/commands/sync.ts +5 -6
  57. package/src/commands/tooling.ts +5 -8
  58. package/src/commands/transcripts.ts +6 -11
  59. package/src/design/regen.ts +5 -32
  60. package/src/design/render.ts +1 -1
  61. package/src/design/tokens.ts +3 -2
  62. package/src/gate/sequencer.ts +1 -1
  63. package/src/gate/stages.ts +4 -20
  64. package/src/gov/audit.ts +1 -6
  65. package/src/hooks/post-merge.ts +2 -5
  66. package/src/markdown/scope.ts +2 -4
  67. package/src/process/harness.ts +3 -4
  68. package/src/sandbox/equivalence.ts +0 -1
  69. package/src/sandbox/provision.ts +8 -13
  70. package/src/sandbox/scenario.ts +6 -12
  71. package/src/shipped/references.ts +1 -2
  72. package/src/sync/check.ts +4 -29
  73. package/src/sync/engine.ts +3 -8
  74. package/src/sync/stamp.ts +6 -6
  75. package/src/sync/target.ts +1 -4
  76. package/src/sync/workflow.ts +3 -5
  77. package/src/tooling/verify-stack.ts +1 -1
  78. package/tooling/base/configs/.husky/post-merge +6 -10
  79. package/tooling/claude/reference.md +1 -1
  80. package/tooling/claude/seeds/canon/DESIGN.md +1 -1
  81. package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
  82. package/claude/skills/canvas-board/SKILL.md +0 -143
  83. package/claude/skills/canvas-board/references/canvas.md +0 -170
  84. package/governance/rules/ui/canvas.md +0 -13
  85. package/scripts/core/check-color-source.sh +0 -51
  86. package/src/canvas/address.ts +0 -227
  87. package/src/canvas/arrange.ts +0 -66
  88. package/src/canvas/capture.ts +0 -391
  89. package/src/canvas/client/app.tsx +0 -188
  90. package/src/canvas/client/inspector/align-grid.tsx +0 -162
  91. package/src/canvas/client/inspector/color-field.tsx +0 -398
  92. package/src/canvas/client/inspector/color-inputs.tsx +0 -139
  93. package/src/canvas/client/inspector/color-picker.tsx +0 -668
  94. package/src/canvas/client/inspector/color-space.ts +0 -240
  95. package/src/canvas/client/inspector/field.tsx +0 -263
  96. package/src/canvas/client/inspector/fields.css +0 -763
  97. package/src/canvas/client/inspector/section.tsx +0 -27
  98. package/src/canvas/client/inspector/size-field.tsx +0 -69
  99. package/src/canvas/client/inspector/token-select.tsx +0 -109
  100. package/src/canvas/client/inspector/values.ts +0 -297
  101. package/src/canvas/client/inspector.tsx +0 -692
  102. package/src/canvas/client/keys.ts +0 -71
  103. package/src/canvas/client/layers.tsx +0 -118
  104. package/src/canvas/client/main.tsx +0 -71
  105. package/src/canvas/client/pages-panel.tsx +0 -212
  106. package/src/canvas/client/panel-handle.tsx +0 -90
  107. package/src/canvas/client/selection.tsx +0 -199
  108. package/src/canvas/client/shell.css +0 -762
  109. package/src/canvas/client/state.ts +0 -792
  110. package/src/canvas/client/surface.tsx +0 -803
  111. package/src/canvas/client/theme-panel.tsx +0 -55
  112. package/src/canvas/client/tools.tsx +0 -153
  113. package/src/canvas/content.ts +0 -781
  114. package/src/canvas/edit.ts +0 -575
  115. package/src/canvas/editing.ts +0 -203
  116. package/src/canvas/fit.ts +0 -238
  117. package/src/canvas/global.d.ts +0 -15
  118. package/src/canvas/history.ts +0 -116
  119. package/src/canvas/index.html +0 -20
  120. package/src/canvas/server.ts +0 -766
  121. package/src/canvas/tokens.ts +0 -221
  122. package/src/commands/canvas.ts +0 -1097
  123. package/src/design/adapter.ts +0 -59
  124. package/src/design/base.css +0 -203
  125. package/src/design/brand-favicon.ts +0 -18
  126. package/src/design/neutral.ts +0 -127
  127. package/src/design/project-fonts.ts +0 -392
@@ -1,692 +0,0 @@
1
- /** @jsxImportSource preact */
2
- import type { JSX } from 'preact'
3
- import { useState } from 'preact/hooks'
4
- import { addressOf, elementAt, excerpt, isRawText } from '@/canvas/address'
5
- import { AlignGrid } from '@/canvas/client/inspector/align-grid'
6
- import { ColorField } from '@/canvas/client/inspector/color-field'
7
- import {
8
- Field,
9
- ReadOnlyField,
10
- type Scrub,
11
- SelectField,
12
- } from '@/canvas/client/inspector/field'
13
- import { Section } from '@/canvas/client/inspector/section'
14
- import { SizeField } from '@/canvas/client/inspector/size-field'
15
- import { TokenSelect } from '@/canvas/client/inspector/token-select'
16
- import {
17
- clampScrub,
18
- declarationValue,
19
- displayValue,
20
- firstFamily,
21
- lineHeightValue,
22
- opacityToCss,
23
- parseColor,
24
- sharedRadius,
25
- textAlignOf,
26
- toCssValue,
27
- tokenOf,
28
- } from '@/canvas/client/inspector/values'
29
- import {
30
- currentPage,
31
- editElement,
32
- editRefusal,
33
- type FrameRef,
34
- frameDocuments,
35
- frameKey,
36
- frameVersions,
37
- newStep,
38
- pendingEdit,
39
- savedEdit,
40
- selectedElement,
41
- selectedFrame,
42
- tokens,
43
- writeError,
44
- } from '@/canvas/client/state'
45
- import type { TokenKind } from '@/canvas/tokens'
46
- import '@/canvas/client/inspector/fields.css'
47
-
48
- /** Elements the parser never gives content, so they hold no text to edit. */
49
- const VOID = new Set([
50
- 'area',
51
- 'base',
52
- 'br',
53
- 'col',
54
- 'embed',
55
- 'hr',
56
- 'img',
57
- 'input',
58
- 'link',
59
- 'meta',
60
- 'source',
61
- 'track',
62
- 'wbr',
63
- ])
64
-
65
- interface StyleField {
66
- /** The full word, which names the field. */
67
- readonly label: string
68
- readonly glyph: string
69
- readonly property: string
70
- readonly isWide?: boolean
71
- readonly placeholder?: string
72
- }
73
-
74
- /**
75
- * The basic set, grouped as the panel lists it. The writer refuses any
76
- * property outside it, so these lists and the writer's have to agree.
77
- */
78
- const SIZE_FIELDS: readonly StyleField[] = [
79
- { label: 'width', glyph: 'W', property: 'width' },
80
- { label: 'height', glyph: 'H', property: 'height' },
81
- ]
82
-
83
- const DIRECTION_FIELD: StyleField = {
84
- label: 'direction',
85
- glyph: 'Dir',
86
- property: 'flex-direction',
87
- }
88
-
89
- const FLEX_FIELDS: readonly StyleField[] = [
90
- { label: 'gap', glyph: 'Gap', property: 'gap', placeholder: '0' },
91
- ]
92
-
93
- /** Padding applies to any box, so it sits in Layout rather than Flex. */
94
- const PADDING_FIELD: StyleField = {
95
- label: 'padding',
96
- glyph: 'Pad',
97
- property: 'padding',
98
- isWide: true,
99
- }
100
-
101
- /** The corners in the order the radius shorthand lists them. */
102
- const CORNER_FIELDS: readonly StyleField[] = [
103
- { label: 'top left radius', glyph: 'TL', property: 'border-top-left-radius' },
104
- {
105
- label: 'top right radius',
106
- glyph: 'TR',
107
- property: 'border-top-right-radius',
108
- },
109
- {
110
- label: 'bottom right radius',
111
- glyph: 'BR',
112
- property: 'border-bottom-right-radius',
113
- },
114
- {
115
- label: 'bottom left radius',
116
- glyph: 'BL',
117
- property: 'border-bottom-left-radius',
118
- },
119
- ]
120
-
121
- const FLEX_DISPLAYS = new Set(['flex', 'inline-flex'])
122
- const GRID_DISPLAYS = new Set(['grid', 'inline-grid'])
123
-
124
- const WEIGHTS = ['100', '200', '300', '400', '500', '600', '700', '800', '900']
125
-
126
- /** Line height and letter spacing move in tenths, since their range is small. */
127
- const SPACING_FIELDS: readonly StyleField[] = [
128
- { label: 'line height', glyph: 'LH', property: 'line-height' },
129
- {
130
- label: 'letter spacing',
131
- glyph: 'LS',
132
- property: 'letter-spacing',
133
- placeholder: '0',
134
- },
135
- ]
136
-
137
- const TEXT_ALIGNS = [
138
- { value: 'left', path: 'M2.5 4h11M2.5 8h7M2.5 12h9' },
139
- { value: 'center', path: 'M2.5 4h11M4.5 8h7M3.5 12h9' },
140
- { value: 'right', path: 'M2.5 4h11M6.5 8h7M4.5 12h9' },
141
- { value: 'justify', path: 'M2.5 4h11M2.5 8h11M2.5 12h11' },
142
- ] as const
143
-
144
- const FILL_FIELDS = [
145
- { label: 'color', property: 'color' },
146
- { label: 'background', property: 'background-color' },
147
- ] as const
148
-
149
- function inlineValue(element: Element, property: string): string {
150
- return 'style' in element
151
- ? (element as HTMLElement).style.getPropertyValue(property)
152
- : ''
153
- }
154
-
155
- /** Values that defer to something else, so they follow the theme as it does. */
156
- const DEFERRING = new Set([
157
- 'currentcolor',
158
- 'inherit',
159
- 'initial',
160
- 'unset',
161
- 'revert',
162
- 'revert-layer',
163
- ])
164
-
165
- /**
166
- * A color that stays put when the theme changes: one naming no token anywhere
167
- * in it and deferring to nothing, so `color-mix()` over a `var()` is not raw.
168
- */
169
- export function isRawValue(value: string): boolean {
170
- const normalized = value.trim().toLowerCase()
171
- return (
172
- normalized !== '' &&
173
- !normalized.includes('var(') &&
174
- !DEFERRING.has(normalized)
175
- )
176
- }
177
-
178
- /**
179
- * Inherited and stylesheet values are not the element's own, so only an
180
- * inline value counts.
181
- */
182
- function isRawInline(element: Element, property: string): boolean {
183
- return isRawValue(inlineValue(element, property))
184
- }
185
-
186
- /**
187
- * What the element states inline, else the value the browser settled on, so
188
- * an edit starts from what the operator sees.
189
- */
190
- function currentValue(element: Element, property: string): string {
191
- return inlineValue(element, property) || computedValue(element, property)
192
- }
193
-
194
- function computedValue(element: Element, property: string): string {
195
- return (
196
- element.ownerDocument.defaultView
197
- ?.getComputedStyle(element)
198
- .getPropertyValue(property) ?? ''
199
- )
200
- }
201
-
202
- /**
203
- * The color a token paints in the frame, read off a probe rather than the
204
- * token's text, since a token's value may itself be a `var()` or a mix.
205
- */
206
- function tokenPaint(doc: Document, name: string): string {
207
- const probe = doc.createElement('span')
208
- probe.style.setProperty('color', `var(${name})`)
209
- ;(doc.body ?? doc.documentElement).append(probe)
210
- const painted = computedValue(probe, 'color')
211
- probe.remove()
212
- return painted
213
- }
214
-
215
- function holdsTextAlone(element: Element): boolean {
216
- const tag = element.tagName.toLowerCase()
217
- return element.children.length === 0 && !VOID.has(tag) && !isRawText(tag)
218
- }
219
-
220
- function elementName(element: Element): string {
221
- const tag = element.tagName.toLowerCase()
222
- return [tag, ...element.classList].join('.')
223
- }
224
-
225
- function ElementFields({
226
- node,
227
- doc,
228
- frameRef,
229
- frameKey: key,
230
- }: {
231
- readonly node: Element
232
- readonly doc: Document
233
- readonly frameRef: FrameRef
234
- readonly frameKey: string
235
- }): JSX.Element {
236
- const address = addressOf(doc, node)
237
- const pending = pendingEdit.value
238
- const isBusy = pending?.key === key && pending.index === address?.index
239
- const commit = (property: string) => (value: string) => {
240
- if (address) void editElement(frameRef, key, address, property, value)
241
- }
242
- /*
243
- * The writer takes one property an edit, so a control setting two posts
244
- * them in turn and stops at the first refusal, under one step so they undo
245
- * as one. Each carries the hash the last answered with in its own address,
246
- * since a frame reloading between the two clears the stored one.
247
- */
248
- const commitAll = async (
249
- changes: readonly (readonly [string, string])[],
250
- ): Promise<void> => {
251
- const step = newStep()
252
- let current = address
253
- for (const [property, value] of changes) {
254
- if (!current) return
255
- const written = await editElement(
256
- frameRef,
257
- key,
258
- current,
259
- property,
260
- value,
261
- step,
262
- )
263
- if (!written) return
264
- if (written.hash) current = { ...current, hash: written.hash }
265
- }
266
- }
267
- const corners = CORNER_FIELDS.map((field) =>
268
- currentValue(node, field.property).trim(),
269
- )
270
- const radius = sharedRadius(corners)
271
- const [isPerCorner, setPerCorner] = useState(radius === undefined)
272
- const display = computedValue(node, 'display').trim()
273
- const isFlex = FLEX_DISPLAYS.has(display)
274
- const isWrapped = computedValue(node, 'flex-wrap').trim() === 'wrap'
275
- const rect = node.getBoundingClientRect()
276
- const text = node.textContent ?? ''
277
- const groupTokens = (kind: TokenKind): readonly string[] =>
278
- tokens.value?.groups
279
- ?.find((group) => group.kind === kind)
280
- ?.tokens.map((token) => token.name) ?? []
281
- const colorTokens = groupTokens('color')
282
- const family = currentValue(node, 'font-family')
283
- const familyToken = tokenOf(family)
284
- const size = currentValue(node, 'font-size')
285
- const sizeToken = tokenOf(size)
286
- const textAlign = textAlignOf(
287
- computedValue(node, 'text-align'),
288
- computedValue(node, 'direction'),
289
- )
290
-
291
- /**
292
- * Previews into the frame's own inline style and writes through the same
293
- * edit as typing. A reload mid-drag replaces the document, so a release
294
- * against one no longer on screen posts nothing.
295
- */
296
- const scrubOf = (property: string, step?: number): Scrub => ({
297
- clamp: (value) => clampScrub(property, value),
298
- step,
299
- begin: () => {
300
- const original = inlineValue(node, property)
301
- const style = (node as HTMLElement).style
302
- return {
303
- preview: (shown) =>
304
- style.setProperty(property, toCssValue(property, shown)),
305
- restore: () =>
306
- original
307
- ? style.setProperty(property, original)
308
- : style.removeProperty(property),
309
- commit: (shown) => {
310
- if (frameDocuments.value.get(key) !== doc || !node.isConnected) return
311
- commit(property)(toCssValue(property, shown))
312
- },
313
- }
314
- },
315
- })
316
-
317
- const styleField = (field: StyleField): JSX.Element => (
318
- <Field
319
- key={field.property}
320
- label={field.label}
321
- glyph={field.glyph}
322
- initial={displayValue(field.property, currentValue(node, field.property))}
323
- isBusy={isBusy}
324
- isWide={field.isWide}
325
- placeholder={field.placeholder}
326
- onCommit={(typed) =>
327
- commit(field.property)(toCssValue(field.property, typed))
328
- }
329
- scrub={'style' in node ? scrubOf(field.property) : undefined}
330
- />
331
- )
332
-
333
- return (
334
- <>
335
- <Section title="Layout">
336
- <ReadOnlyField
337
- label="x"
338
- glyph="X"
339
- value={String(Math.round(rect.left))}
340
- />
341
- <ReadOnlyField
342
- label="y"
343
- glyph="Y"
344
- value={String(Math.round(rect.top))}
345
- />
346
- {SIZE_FIELDS.map((field) => (
347
- <SizeField
348
- key={field.property}
349
- label={field.label}
350
- glyph={field.glyph}
351
- initial={displayValue(
352
- field.property,
353
- currentValue(node, field.property),
354
- )}
355
- isBusy={isBusy}
356
- onCommit={(typed) =>
357
- commit(field.property)(toCssValue(field.property, typed))
358
- }
359
- scrub={'style' in node ? scrubOf(field.property) : undefined}
360
- />
361
- ))}
362
- {styleField(PADDING_FIELD)}
363
- </Section>
364
- <Section
365
- title="Flex"
366
- action={
367
- isFlex || GRID_DISPLAYS.has(display) ? null : (
368
- <button
369
- type="button"
370
- class="section-icon"
371
- aria-label="Add flex layout"
372
- title="Add flex layout"
373
- disabled={isBusy}
374
- onClick={() => commit('display')('flex')}
375
- >
376
- <svg viewBox="0 0 16 16" aria-hidden="true">
377
- <path d="M8 3.5v9M3.5 8h9" />
378
- </svg>
379
- </button>
380
- )
381
- }
382
- >
383
- {isFlex ? (
384
- <>
385
- <div class="flex-layout is-wide">
386
- <AlignGrid
387
- direction={computedValue(node, 'flex-direction').trim()}
388
- justifyContent={computedValue(node, 'justify-content')}
389
- alignItems={computedValue(node, 'align-items')}
390
- isBusy={isBusy}
391
- onPick={(alignment) =>
392
- void commitAll([
393
- ['justify-content', alignment.justifyContent],
394
- ['align-items', alignment.alignItems],
395
- ])
396
- }
397
- />
398
- <div class="flex-layout-side">
399
- {styleField(DIRECTION_FIELD)}
400
- <button
401
- type="button"
402
- class="section-icon"
403
- aria-label="Wrap"
404
- title="Wrap"
405
- aria-pressed={isWrapped}
406
- disabled={isBusy}
407
- onClick={() =>
408
- commit('flex-wrap')(isWrapped ? 'nowrap' : 'wrap')
409
- }
410
- >
411
- <svg viewBox="0 0 16 16" aria-hidden="true">
412
- <path d="M2.5 4.5h9a2.5 2.5 0 0 1 0 5h-6M7.5 7.5l-2 2 2 2M2.5 12.5h1" />
413
- </svg>
414
- </button>
415
- </div>
416
- </div>
417
- {FLEX_FIELDS.map(styleField)}
418
- </>
419
- ) : null}
420
- </Section>
421
- <Section
422
- title="Appearance"
423
- action={
424
- <button
425
- type="button"
426
- class="section-icon"
427
- aria-label="Per-corner radius"
428
- title="Per-corner radius"
429
- aria-pressed={isPerCorner}
430
- onClick={() => setPerCorner(!isPerCorner)}
431
- >
432
- <svg viewBox="0 0 16 16" aria-hidden="true">
433
- <path d="M3 6.5V5a2 2 0 0 1 2-2h1.5M9.5 3H11a2 2 0 0 1 2 2v1.5M13 9.5V11a2 2 0 0 1-2 2H9.5M6.5 13H5a2 2 0 0 1-2-2V9.5" />
434
- </svg>
435
- </button>
436
- }
437
- >
438
- <Field
439
- label="opacity"
440
- glyph="Op"
441
- initial={displayValue('opacity', computedValue(node, 'opacity'))}
442
- isBusy={isBusy}
443
- isWide={isPerCorner}
444
- onCommit={(typed) => {
445
- const value = opacityToCss(typed)
446
- if (value !== undefined) commit('opacity')(value)
447
- }}
448
- />
449
- {isPerCorner ? (
450
- CORNER_FIELDS.map(styleField)
451
- ) : (
452
- <Field
453
- label="radius"
454
- glyph="R"
455
- initial={displayValue('border-radius', radius ?? '')}
456
- isBusy={isBusy}
457
- placeholder="0"
458
- onCommit={(typed) =>
459
- commit('border-radius')(toCssValue('border-radius', typed))
460
- }
461
- scrub={'style' in node ? scrubOf('border-radius') : undefined}
462
- />
463
- )}
464
- </Section>
465
- <Section title="Typography">
466
- <TokenSelect
467
- label="family"
468
- glyph="Aa"
469
- initial={familyToken ?? firstFamily(family)}
470
- tokens={groupTokens('font-family')}
471
- token={familyToken}
472
- isRaw={isRawInline(node, 'font-family')}
473
- isBusy={isBusy}
474
- isWide
475
- onCommit={(typed) =>
476
- commit('font-family')(declarationValue('font-family', typed))
477
- }
478
- onPick={(name) => commit('font-family')(`var(${name})`)}
479
- />
480
- <TokenSelect
481
- label="size"
482
- glyph="Size"
483
- initial={sizeToken ?? displayValue('font-size', size)}
484
- tokens={groupTokens('font-size')}
485
- token={sizeToken}
486
- isRaw={isRawInline(node, 'font-size')}
487
- isBusy={isBusy}
488
- onCommit={(typed) =>
489
- commit('font-size')(toCssValue('font-size', typed))
490
- }
491
- onPick={(name) => commit('font-size')(`var(${name})`)}
492
- scrub={'style' in node ? scrubOf('font-size') : undefined}
493
- />
494
- <SelectField
495
- label="weight"
496
- glyph="Wt"
497
- options={WEIGHTS}
498
- value={currentValue(node, 'font-weight').trim()}
499
- isBusy={isBusy}
500
- onCommit={commit('font-weight')}
501
- />
502
- {SPACING_FIELDS.map((field) => (
503
- <Field
504
- key={field.property}
505
- label={field.label}
506
- glyph={field.glyph}
507
- initial={
508
- field.property === 'line-height'
509
- ? lineHeightValue(
510
- currentValue(node, 'line-height'),
511
- computedValue(node, 'font-size'),
512
- )
513
- : displayValue(
514
- field.property,
515
- currentValue(node, field.property),
516
- )
517
- }
518
- isBusy={isBusy}
519
- placeholder={field.placeholder}
520
- onCommit={(typed) =>
521
- commit(field.property)(toCssValue(field.property, typed))
522
- }
523
- scrub={'style' in node ? scrubOf(field.property, 0.1) : undefined}
524
- />
525
- ))}
526
- <div class="segmented is-wide" role="group" aria-label="text alignment">
527
- {TEXT_ALIGNS.map((align) => (
528
- <button
529
- key={align.value}
530
- type="button"
531
- class="segment"
532
- aria-label={`Align ${align.value}`}
533
- title={`Align ${align.value}`}
534
- aria-pressed={textAlign === align.value}
535
- disabled={isBusy}
536
- onClick={() => {
537
- if (textAlign !== align.value) {
538
- commit('text-align')(align.value)
539
- }
540
- }}
541
- >
542
- <svg viewBox="0 0 16 16" aria-hidden="true">
543
- <path d={align.path} />
544
- </svg>
545
- </button>
546
- ))}
547
- </div>
548
- </Section>
549
- <Section title="Fill">
550
- {FILL_FIELDS.map((field) => {
551
- const painted = computedValue(node, field.property)
552
- const inline = inlineValue(node, field.property)
553
- /*
554
- * An inline value the picker cannot read stays as written. Reading
555
- * the computed color in its place would show a token-following
556
- * value as a literal, and an opacity edit would freeze it.
557
- */
558
- return (
559
- <ColorField
560
- key={field.property}
561
- label={field.label}
562
- color={parseColor(inline || painted)}
563
- text={inline || painted}
564
- inline={inline}
565
- painted={painted}
566
- owner={`${key}:${address?.index ?? -1}:${field.property}`}
567
- tokens={colorTokens}
568
- paint={(name) => tokenPaint(doc, name)}
569
- isRaw={isRawInline(node, field.property)}
570
- isBusy={isBusy}
571
- preview={
572
- 'style' in node ? scrubOf(field.property).begin : undefined
573
- }
574
- onCommit={commit(field.property)}
575
- />
576
- )
577
- })}
578
- </Section>
579
- <Section title="Text">
580
- {holdsTextAlone(node) ? (
581
- <Field
582
- label="text"
583
- glyph="T"
584
- initial={text}
585
- isBusy={isBusy}
586
- isWide
587
- onCommit={commit('text')}
588
- />
589
- ) : (
590
- <p class="text-excerpt is-wide" title={excerpt(text)}>
591
- {excerpt(text)}
592
- </p>
593
- )}
594
- </Section>
595
- </>
596
- )
597
- }
598
-
599
- function ElementDetails(): JSX.Element | null {
600
- const picked = selectedElement.value
601
- const page = currentPage.value
602
- if (!picked || !page) return null
603
- const { frame, element } = picked
604
- const key = frameKey(page.name, frame)
605
- const doc = frameDocuments.value.get(key)
606
- const node = doc && !element.stale ? elementAt(doc, element) : undefined
607
- const saved = savedEdit.value
608
- const isSaved = saved?.key === key && saved.index === element.index
609
-
610
- return (
611
- <section aria-label="Element">
612
- <h2 class="section-label">
613
- Element
614
- {isSaved ? (
615
- <span class="saved" role="status">
616
- Saved
617
- </span>
618
- ) : null}
619
- </h2>
620
- {element.stale ? (
621
- <p class="notice">
622
- The frame changed since this element was picked, so its index may name
623
- another element now. Pick it again.
624
- </p>
625
- ) : doc && node ? (
626
- <>
627
- <p class="detail" title={elementName(node)}>
628
- {elementName(node)}
629
- </p>
630
- {/*
631
- * Keyed by the frame's version and the element, so a reload drops
632
- * any value typed against the document it replaced.
633
- */}
634
- <ElementFields
635
- key={`${frameVersions.value.get(key) ?? 0}:${element.index}`}
636
- node={node}
637
- doc={doc}
638
- frameRef={{ page: page.name, frame: frame.name }}
639
- frameKey={key}
640
- />
641
- </>
642
- ) : (
643
- <p class="empty">Loading the frame to read this element</p>
644
- )}
645
- </section>
646
- )
647
- }
648
-
649
- export function Inspector(): JSX.Element {
650
- const frame = selectedFrame.value
651
- return (
652
- <>
653
- <section aria-label="Frame">
654
- <h2 class="section-label">Frame</h2>
655
- {frame ? (
656
- <>
657
- <p class="detail" title={frame.name}>
658
- {frame.name}
659
- </p>
660
- <div class="field-grid">
661
- <ReadOnlyField label="x" glyph="X" value={String(frame.x)} />
662
- <ReadOnlyField label="y" glyph="Y" value={String(frame.y)} />
663
- <ReadOnlyField
664
- label="width"
665
- glyph="W"
666
- value={String(frame.width)}
667
- />
668
- <ReadOnlyField
669
- label="height"
670
- glyph="H"
671
- value={String(frame.height)}
672
- />
673
- </div>
674
- </>
675
- ) : (
676
- <p class="empty">Select a frame to see its box</p>
677
- )}
678
- {writeError.value ? (
679
- <p class="notice" role="alert">
680
- {writeError.value}
681
- </p>
682
- ) : null}
683
- </section>
684
- <ElementDetails />
685
- {editRefusal.value ? (
686
- <p class="notice" role="alert">
687
- {editRefusal.value}
688
- </p>
689
- ) : null}
690
- </>
691
- )
692
- }