@erclx/canon 5.53.0 → 5.54.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 (94) hide show
  1. package/CHANGELOG.md +10 -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/design-identity/REQUIREMENT.md +2 -2
  6. package/claude/skills/design-identity/SKILL.md +6 -6
  7. package/claude/skills/design-record/REQUIREMENT.md +2 -2
  8. package/claude/skills/design-record/SKILL.md +4 -4
  9. package/claude/skills/design-record/references/folder.md +1 -1
  10. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  11. package/claude/skills/draw-figure/REQUIREMENT.md +1 -1
  12. package/claude/skills/draw-figure/SKILL.md +1 -1
  13. package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -4
  14. package/claude/skills/pick-by-eye/SKILL.md +34 -41
  15. package/claude/skills/pick-by-eye/references/board.md +42 -0
  16. package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
  17. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  18. package/claude/skills/teach-craft/REQUIREMENT.md +1 -1
  19. package/claude/skills/teach-craft/SKILL.md +32 -1
  20. package/claude/skills/teach-craft/references/adopted.md +113 -0
  21. package/claude/skills/teach-craft/references/lesson-craft.md +10 -2
  22. package/claude/skills/teach-craft/references/pedagogy.md +29 -2
  23. package/docs/agents/install-and-sync.md +0 -8
  24. package/docs/target/migrations.md +8 -3
  25. package/docs/target/projects.md +1 -1
  26. package/docs/workflow/skill-map.md +0 -1
  27. package/docs/workflow/visual-design-workflow.md +1 -4
  28. package/governance/detect.toml +1 -6
  29. package/governance/renames.toml +0 -1
  30. package/package.json +2 -5
  31. package/src/claude/cases/misc.ts +0 -5
  32. package/src/claude/skills-audit.ts +1 -0
  33. package/src/cli/help.ts +0 -3
  34. package/src/cli.ts +0 -2
  35. package/src/commands/design.ts +16 -315
  36. package/src/commands/sync.ts +4 -5
  37. package/src/design/regen.ts +5 -32
  38. package/src/design/render.ts +1 -1
  39. package/src/design/tokens.ts +3 -2
  40. package/src/gate/stages.ts +4 -10
  41. package/src/gov/audit.ts +1 -6
  42. package/src/markdown/scope.ts +2 -4
  43. package/src/shipped/references.ts +1 -2
  44. package/src/sync/check.ts +4 -29
  45. package/src/sync/stamp.ts +6 -6
  46. package/src/sync/target.ts +1 -4
  47. package/tooling/claude/reference.md +1 -1
  48. package/tooling/claude/seeds/canon/DESIGN.md +1 -1
  49. package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
  50. package/claude/skills/canvas-board/SKILL.md +0 -143
  51. package/claude/skills/canvas-board/references/canvas.md +0 -170
  52. package/governance/rules/ui/canvas.md +0 -13
  53. package/src/canvas/address.ts +0 -227
  54. package/src/canvas/arrange.ts +0 -66
  55. package/src/canvas/capture.ts +0 -391
  56. package/src/canvas/client/app.tsx +0 -188
  57. package/src/canvas/client/inspector/align-grid.tsx +0 -162
  58. package/src/canvas/client/inspector/color-field.tsx +0 -398
  59. package/src/canvas/client/inspector/color-inputs.tsx +0 -139
  60. package/src/canvas/client/inspector/color-picker.tsx +0 -668
  61. package/src/canvas/client/inspector/color-space.ts +0 -240
  62. package/src/canvas/client/inspector/field.tsx +0 -263
  63. package/src/canvas/client/inspector/fields.css +0 -763
  64. package/src/canvas/client/inspector/section.tsx +0 -27
  65. package/src/canvas/client/inspector/size-field.tsx +0 -69
  66. package/src/canvas/client/inspector/token-select.tsx +0 -109
  67. package/src/canvas/client/inspector/values.ts +0 -297
  68. package/src/canvas/client/inspector.tsx +0 -692
  69. package/src/canvas/client/keys.ts +0 -71
  70. package/src/canvas/client/layers.tsx +0 -118
  71. package/src/canvas/client/main.tsx +0 -71
  72. package/src/canvas/client/pages-panel.tsx +0 -212
  73. package/src/canvas/client/panel-handle.tsx +0 -90
  74. package/src/canvas/client/selection.tsx +0 -199
  75. package/src/canvas/client/shell.css +0 -762
  76. package/src/canvas/client/state.ts +0 -792
  77. package/src/canvas/client/surface.tsx +0 -803
  78. package/src/canvas/client/theme-panel.tsx +0 -55
  79. package/src/canvas/client/tools.tsx +0 -153
  80. package/src/canvas/content.ts +0 -781
  81. package/src/canvas/edit.ts +0 -575
  82. package/src/canvas/editing.ts +0 -203
  83. package/src/canvas/fit.ts +0 -238
  84. package/src/canvas/global.d.ts +0 -15
  85. package/src/canvas/history.ts +0 -116
  86. package/src/canvas/index.html +0 -20
  87. package/src/canvas/server.ts +0 -766
  88. package/src/canvas/tokens.ts +0 -221
  89. package/src/commands/canvas.ts +0 -1097
  90. package/src/design/adapter.ts +0 -59
  91. package/src/design/base.css +0 -203
  92. package/src/design/brand-favicon.ts +0 -18
  93. package/src/design/neutral.ts +0 -127
  94. 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
- }