@erclx/canon 5.23.0 → 5.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
  3. package/claude/skills/backlog-triage/SKILL.md +19 -6
  4. package/claude/skills/canon-operator/REQUIREMENT.md +1 -0
  5. package/claude/skills/canon-operator/SKILL.md +5 -1
  6. package/claude/skills/canvas/REQUIREMENT.md +49 -0
  7. package/claude/skills/canvas/SKILL.md +106 -0
  8. package/claude/skills/design-extract/REQUIREMENT.md +1 -1
  9. package/claude/skills/draft-doc/REQUIREMENT.md +2 -1
  10. package/claude/skills/draft-doc/references/readme/agent-facing.md +59 -0
  11. package/claude/skills/draft-doc/references/readme/application.md +66 -0
  12. package/claude/skills/draft-doc/references/readme/cli.md +64 -0
  13. package/claude/skills/draft-doc/references/readme/library.md +57 -0
  14. package/claude/skills/draft-doc/references/readme/plugin.md +66 -0
  15. package/claude/skills/draft-doc/references/readme.md +33 -4
  16. package/claude/skills/draft-figure/REQUIREMENT.md +14 -3
  17. package/claude/skills/draft-figure/SKILL.md +24 -51
  18. package/claude/skills/draft-figure/references/architecture-set.md +35 -0
  19. package/claude/skills/draft-figure/references/hand-drawn.md +31 -0
  20. package/claude/skills/draft-figure/references/mermaid.md +55 -0
  21. package/claude/skills/git-followup/SKILL.md +17 -6
  22. package/claude/skills/git-pr/references/evidence.md +4 -2
  23. package/claude/skills/git-split/SKILL.md +1 -1
  24. package/claude/skills/index-lookup/SKILL.md +2 -2
  25. package/claude/skills/review-address/SKILL.md +6 -13
  26. package/claude/skills/review-address/references/rest-reads.md +43 -0
  27. package/claude/skills/review-craft/references/ui.md +7 -1
  28. package/claude/skills/review-pr/REQUIREMENT.md +1 -0
  29. package/claude/skills/review-pr/SKILL.md +9 -3
  30. package/claude/skills/review-pr/references/close-out.md +2 -2
  31. package/claude/skills/review-pr/references/post.md +17 -5
  32. package/claude/skills/review-pr/references/scope.md +10 -8
  33. package/claude/skills/review-ui/SKILL.md +12 -4
  34. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +30 -1
  35. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +4 -0
  36. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +4 -4
  37. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +6 -0
  38. package/claude/skills/role-orchestrator/scripts/poll.ts +90 -27
  39. package/claude/skills/role-orchestrator/scripts/repo.ts +3 -5
  40. package/claude/skills/role-orchestrator/scripts/watch.ts +4 -7
  41. package/docs/agents/canvas.md +6 -2
  42. package/docs/agents/commands.md +45 -45
  43. package/docs/agents/context-audit-checks.md +2 -2
  44. package/docs/agents/index.md +0 -1
  45. package/docs/agents/key-changes.md +3 -3
  46. package/docs/agents/pr-evidence.md +11 -2
  47. package/docs/agents/pr-reads.md +5 -4
  48. package/docs/target/projects.md +4 -4
  49. package/docs/workflow/ai-workflow.md +0 -1
  50. package/docs/workflow/skill-map.md +3 -3
  51. package/docs/workflow/visual-design-workflow.md +2 -1
  52. package/governance/renames.toml +0 -4
  53. package/governance/rules/standards/markdown.md +1 -0
  54. package/package.json +1 -2
  55. package/scripts/core/install-check.sh +1 -1
  56. package/src/audits/catalog.ts +2 -2
  57. package/src/canvas/client/inspector/field.tsx +200 -0
  58. package/src/canvas/client/inspector/fields.css +151 -0
  59. package/src/canvas/client/inspector/section.tsx +27 -0
  60. package/src/canvas/client/inspector/values.ts +60 -0
  61. package/src/canvas/client/inspector.tsx +192 -111
  62. package/src/canvas/client/shell.css +0 -94
  63. package/src/canvas/tokens.ts +9 -1
  64. package/src/claude/cases/misc.ts +5 -0
  65. package/src/claude/cases/workflow.ts +1 -1
  66. package/src/claude/seeds.ts +0 -1
  67. package/src/commands/canvas.ts +29 -0
  68. package/src/commands/design.ts +2 -64
  69. package/src/commands/pr.ts +219 -202
  70. package/src/context/audit.ts +7 -6
  71. package/src/design/budget.ts +149 -0
  72. package/src/design/document.ts +22 -10
  73. package/src/design/render.ts +11 -1
  74. package/src/design/tokens.ts +40 -61
  75. package/src/gate/measures.ts +49 -0
  76. package/src/gate/stages.ts +5 -1
  77. package/src/help.ts +2 -2
  78. package/src/migrate/skill-names.ts +7 -1
  79. package/src/pr/evidence.ts +25 -5
  80. package/src/pr/rest.ts +137 -0
  81. package/standards/canvas.md +112 -0
  82. package/standards/context.md +2 -1
  83. package/standards/design.md +13 -3
  84. package/standards/figures.md +0 -1
  85. package/standards/index.md +1 -1
  86. package/standards/markdown.md +1 -0
  87. package/standards/mermaid.md +1 -7
  88. package/tooling/claude/reference.md +0 -5
  89. package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +1 -1
  90. package/claude/skills/draft-diagram/REQUIREMENT.md +0 -45
  91. package/claude/skills/draft-diagram/SKILL.md +0 -177
  92. package/docs/agents/design-board.md +0 -33
  93. package/governance/rules/standards/diagrams.md +0 -14
  94. package/governance/rules/standards/mermaid.md +0 -15
  95. package/scripts/core/check-gallery-exclusion.sh +0 -50
  96. package/src/design/board.ts +0 -576
  97. package/standards/diagrams.md +0 -132
  98. package/tooling/claude/seeds/.claude/diagrams/index.md +0 -8
@@ -0,0 +1,151 @@
1
+ /*
2
+ * The inspector's own rules. Every value reads a chrome token, the same set
3
+ * shell.css reads. Each field's glyph sits inside its box, so two fields share
4
+ * a row without a label column eating the width.
5
+ */
6
+
7
+ .inspector-section {
8
+ margin-top: var(--space-sm);
9
+ }
10
+
11
+ .inspector-section-head {
12
+ display: flex;
13
+ align-items: center;
14
+ min-height: 24px;
15
+ }
16
+
17
+ .inspector-section-title {
18
+ flex: 1;
19
+ min-width: 0;
20
+ margin: 0;
21
+ font-size: var(--type-label-size);
22
+ font-weight: 500;
23
+ color: var(--color-text);
24
+ }
25
+
26
+ .inspector-section-action {
27
+ flex: none;
28
+ display: flex;
29
+ justify-content: flex-end;
30
+ min-width: 24px;
31
+ }
32
+
33
+ .field-grid {
34
+ display: grid;
35
+ grid-template-columns: repeat(2, minmax(0, 1fr));
36
+ gap: var(--space-xs);
37
+ margin-top: var(--space-xs);
38
+ }
39
+
40
+ .glyph-field {
41
+ display: flex;
42
+ align-items: center;
43
+ min-width: 0;
44
+ min-height: 24px;
45
+ border: 1px solid var(--color-border);
46
+ border-radius: var(--radius-action);
47
+ background: var(--color-surface);
48
+ }
49
+
50
+ .glyph-field:hover {
51
+ border-color: var(--color-muted);
52
+ }
53
+
54
+ .glyph-field.is-readonly {
55
+ background: transparent;
56
+ }
57
+
58
+ .glyph-field.is-readonly:hover {
59
+ border-color: var(--color-border);
60
+ }
61
+
62
+ .is-wide {
63
+ grid-column: 1 / -1;
64
+ }
65
+
66
+ .glyph {
67
+ flex: none;
68
+ min-width: 1.75rem;
69
+ padding: 0 var(--space-xs);
70
+ color: var(--color-muted);
71
+ text-align: center;
72
+ user-select: none;
73
+ }
74
+
75
+ /* A number field's glyph scrubs its value, so a drag must not pan or scroll. */
76
+ .glyph.is-scrub {
77
+ cursor: ew-resize;
78
+ touch-action: none;
79
+ }
80
+
81
+ .glyph-field-input {
82
+ flex: 1;
83
+ min-width: 0;
84
+ padding: 2px var(--space-xs) 2px 0;
85
+ border: 0;
86
+ background: transparent;
87
+ color: var(--color-text);
88
+ font: inherit;
89
+ font-variant-numeric: tabular-nums;
90
+ overflow: hidden;
91
+ text-overflow: ellipsis;
92
+ white-space: nowrap;
93
+ }
94
+
95
+ .glyph-field-input:disabled {
96
+ opacity: 0.5;
97
+ cursor: not-allowed;
98
+ }
99
+
100
+ .fill-row {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: var(--space-xs);
104
+ min-width: 0;
105
+ }
106
+
107
+ .fill-row .glyph-field {
108
+ flex: 1;
109
+ }
110
+
111
+ .token-picker {
112
+ flex: none;
113
+ width: 4.5rem;
114
+ min-height: 24px;
115
+ padding: 2px var(--space-xs);
116
+ border: 1px solid var(--color-border);
117
+ border-radius: var(--radius-action);
118
+ background: var(--color-surface);
119
+ color: var(--color-text);
120
+ font: inherit;
121
+ cursor: pointer;
122
+ }
123
+
124
+ .token-picker:hover {
125
+ border-color: var(--color-muted);
126
+ }
127
+
128
+ .token-picker:disabled {
129
+ opacity: 0.5;
130
+ cursor: not-allowed;
131
+ }
132
+
133
+ .raw {
134
+ flex: none;
135
+ color: var(--color-text-secondary);
136
+ cursor: help;
137
+ }
138
+
139
+ .text-excerpt {
140
+ margin: var(--space-xs) 0 0;
141
+ color: var(--color-text-body);
142
+ overflow: hidden;
143
+ text-overflow: ellipsis;
144
+ white-space: nowrap;
145
+ }
146
+
147
+ .saved {
148
+ margin-left: var(--space-sm);
149
+ color: var(--color-success);
150
+ font-weight: 400;
151
+ }
@@ -0,0 +1,27 @@
1
+ /** @jsxImportSource preact */
2
+ import { type ComponentChildren, type JSX, toChildArray } from 'preact'
3
+
4
+ interface SectionProps {
5
+ readonly title: string
6
+ /** Sits in a fixed lane right of the title, so adding one never moves it. */
7
+ readonly action?: ComponentChildren
8
+ readonly children?: ComponentChildren
9
+ }
10
+
11
+ /** A titled group of fields. With no fields it renders its header alone. */
12
+ export function Section({
13
+ title,
14
+ action,
15
+ children,
16
+ }: SectionProps): JSX.Element {
17
+ const fields = toChildArray(children)
18
+ return (
19
+ <section class="inspector-section">
20
+ <header class="inspector-section-head">
21
+ <h3 class="inspector-section-title">{title}</h3>
22
+ <span class="inspector-section-action">{action}</span>
23
+ </header>
24
+ {fields.length > 0 ? <div class="field-grid">{fields}</div> : null}
25
+ </section>
26
+ )
27
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * How the inspector reads a computed value and writes a typed one back. A
3
+ * field shows the formatted form, and only a value the operator changed goes
4
+ * back through `toCssValue`, so rounding never writes a value nobody typed.
5
+ */
6
+
7
+ /** Properties whose bare numbers are pixel lengths. */
8
+ const LENGTHS = new Set(['width', 'height', 'padding', 'gap', 'font-size'])
9
+
10
+ const PX = /^(-?\d*\.?\d+)px$/
11
+
12
+ function formatPx(token: string): string {
13
+ const match = token.match(PX)
14
+ if (!match) return token
15
+ const value = Number(match[1])
16
+ return String(
17
+ Math.abs(value) < 10 ? Math.round(value * 10) / 10 : Math.round(value),
18
+ )
19
+ }
20
+
21
+ /** A color that paints nothing, which the panel reads as no fill. */
22
+ export function isNoFill(value: string): boolean {
23
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, '')
24
+ return normalized === 'transparent' || normalized === 'rgba(0,0,0,0)'
25
+ }
26
+
27
+ export function displayValue(property: string, raw: string): string {
28
+ const value = raw.trim()
29
+ if (property === 'gap' && (value === '' || value === 'normal')) return ''
30
+ if (property === 'background-color' && isNoFill(value)) return ''
31
+ if (value === 'auto') return 'Auto'
32
+ if (value === 'normal') return 'Normal'
33
+ return value.split(/\s+/).map(formatPx).join(' ')
34
+ }
35
+
36
+ export function toCssValue(property: string, typed: string): string {
37
+ const value = typed.trim()
38
+ if (value === 'Auto' || value === 'Normal') return value.toLowerCase()
39
+ if (!LENGTHS.has(property)) return value
40
+ return value
41
+ .split(/\s+/)
42
+ .map((token) => (/^-?\d*\.?\d+$/.test(token) ? `${token}px` : token))
43
+ .join(' ')
44
+ }
45
+
46
+ /**
47
+ * Holds a scrubbed number inside what the property accepts. The writer takes
48
+ * any declaration that does not break the rule, so a negative length or a
49
+ * weight past 1000 would reach the file and the browser would drop it there.
50
+ */
51
+ export function clampScrub(property: string, value: number): number {
52
+ if (property === 'font-weight') return Math.min(1000, Math.max(1, value))
53
+ if (LENGTHS.has(property)) return Math.max(0, value)
54
+ return value
55
+ }
56
+
57
+ /** Units a scrub moves for each pixel the pointer travels. */
58
+ export function scrubStep(isCoarse: boolean): number {
59
+ return isCoarse ? 10 : 1
60
+ }
@@ -1,6 +1,17 @@
1
1
  /** @jsxImportSource preact */
2
2
  import type { JSX } from 'preact'
3
3
  import { addressOf, elementAt, excerpt, isRawText } from '@/canvas/address'
4
+ import {
5
+ Field,
6
+ ReadOnlyField,
7
+ type Scrub,
8
+ } from '@/canvas/client/inspector/field'
9
+ import { Section } from '@/canvas/client/inspector/section'
10
+ import {
11
+ clampScrub,
12
+ displayValue,
13
+ toCssValue,
14
+ } from '@/canvas/client/inspector/values'
4
15
  import {
5
16
  currentPage,
6
17
  editElement,
@@ -16,8 +27,7 @@ import {
16
27
  tokens,
17
28
  writeError,
18
29
  } from '@/canvas/client/state'
19
-
20
- const FIELDS = ['x', 'y', 'width', 'height'] as const
30
+ import '@/canvas/client/inspector/fields.css'
21
31
 
22
32
  /** Elements the parser never gives content, so they hold no text to edit. */
23
33
  const VOID = new Set([
@@ -37,43 +47,79 @@ const VOID = new Set([
37
47
  ])
38
48
 
39
49
  interface StyleField {
50
+ /** The full word, which names the field. */
40
51
  readonly label: string
52
+ readonly glyph: string
41
53
  readonly property: string
42
- readonly isColor?: boolean
54
+ readonly isWide?: boolean
55
+ readonly placeholder?: string
43
56
  }
44
57
 
45
58
  /**
46
- * The basic set, in the order the panel lists it. The writer refuses any
47
- * property outside it, so this list and the writer's have to agree.
59
+ * The basic set, grouped as the panel lists it. The writer refuses any
60
+ * property outside it, so these lists and the writer's have to agree.
48
61
  */
49
- const STYLE_FIELDS: readonly StyleField[] = [
50
- { label: 'color', property: 'color', isColor: true },
51
- { label: 'background', property: 'background-color', isColor: true },
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' },
62
+ const SIZE_FIELDS: readonly StyleField[] = [
63
+ { label: 'width', glyph: 'W', property: 'width' },
64
+ { label: 'height', glyph: 'H', property: 'height' },
65
+ ]
66
+
67
+ const FLEX_FIELDS: readonly StyleField[] = [
68
+ { label: 'direction', glyph: 'Dir', property: 'flex-direction' },
69
+ { label: 'gap', glyph: 'Gap', property: 'gap', placeholder: '0' },
70
+ { label: 'padding', glyph: 'Pad', property: 'padding', isWide: true },
71
+ ]
72
+
73
+ const TYPE_FIELDS: readonly StyleField[] = [
74
+ { label: 'size', glyph: 'Size', property: 'font-size' },
75
+ { label: 'weight', glyph: 'Wt', property: 'font-weight' },
76
+ ]
77
+
78
+ const FILL_FIELDS: readonly StyleField[] = [
79
+ { label: 'color', glyph: 'Fg', property: 'color' },
80
+ {
81
+ label: 'background',
82
+ glyph: 'Bg',
83
+ property: 'background-color',
84
+ placeholder: 'None',
85
+ },
59
86
  ]
60
87
 
61
- interface Row {
62
- readonly name: string
63
- readonly value: string
88
+ function inlineValue(element: Element, property: string): string {
89
+ return 'style' in element
90
+ ? (element as HTMLElement).style.getPropertyValue(property)
91
+ : ''
92
+ }
93
+
94
+ /** Values that defer to something else, so they follow the theme as it does. */
95
+ const DEFERRING = new Set([
96
+ 'currentcolor',
97
+ 'inherit',
98
+ 'initial',
99
+ 'unset',
100
+ 'revert',
101
+ 'revert-layer',
102
+ ])
103
+
104
+ /**
105
+ * A color that stays put when the theme changes: one naming no token anywhere
106
+ * in it and deferring to nothing, so `color-mix()` over a `var()` is not raw.
107
+ */
108
+ export function isRawValue(value: string): boolean {
109
+ const normalized = value.trim().toLowerCase()
110
+ return (
111
+ normalized !== '' &&
112
+ !normalized.includes('var(') &&
113
+ !DEFERRING.has(normalized)
114
+ )
64
115
  }
65
116
 
66
- /** Where the element sits in its frame, which the panel shows and never edits. */
67
- function positionRows(element: Element): Row[] {
68
- const rect = element.getBoundingClientRect()
69
- const font = element.ownerDocument.defaultView
70
- ?.getComputedStyle(element)
71
- .getPropertyValue('font-family')
72
- return [
73
- { name: 'x', value: String(Math.round(rect.left)) },
74
- { name: 'y', value: String(Math.round(rect.top)) },
75
- ...(font ? [{ name: 'font', value: font }] : []),
76
- ]
117
+ /**
118
+ * Inherited and stylesheet values are not the element's own, so only an
119
+ * inline value counts.
120
+ */
121
+ function isRawInline(element: Element, property: string): boolean {
122
+ return isRawValue(inlineValue(element, property))
77
123
  }
78
124
 
79
125
  /**
@@ -81,10 +127,7 @@ function positionRows(element: Element): Row[] {
81
127
  * an edit starts from what the operator sees.
82
128
  */
83
129
  function currentValue(element: Element, property: string): string {
84
- const inline =
85
- 'style' in element
86
- ? (element as HTMLElement).style.getPropertyValue(property)
87
- : ''
130
+ const inline = inlineValue(element, property)
88
131
  if (inline) return inline
89
132
  return (
90
133
  element.ownerDocument.defaultView
@@ -103,44 +146,19 @@ function elementName(element: Element): string {
103
146
  return [tag, ...element.classList].join('.')
104
147
  }
105
148
 
106
- interface FieldProps {
149
+ interface TokenPickerProps {
107
150
  readonly label: string
108
151
  readonly initial: string
109
152
  readonly isBusy: boolean
110
153
  readonly onCommit: (value: string) => void
111
154
  }
112
155
 
113
- /**
114
- * Commits on change, which a text input fires on Enter or on leaving it, and
115
- * sends nothing when the value is what it started at. Escape puts it back.
116
- */
117
- function Field({ label, initial, isBusy, onCommit }: FieldProps): JSX.Element {
118
- return (
119
- <input
120
- class="field"
121
- type="text"
122
- aria-label={label}
123
- title={initial}
124
- defaultValue={initial}
125
- disabled={isBusy}
126
- spellcheck={false}
127
- onChange={(event) => {
128
- const value = event.currentTarget.value.trim()
129
- if (value !== initial.trim()) onCommit(value)
130
- }}
131
- onKeyDown={(event) => {
132
- if (event.key === 'Escape') event.currentTarget.value = initial
133
- }}
134
- />
135
- )
136
- }
137
-
138
156
  function TokenPicker({
139
157
  label,
140
158
  initial,
141
159
  isBusy,
142
160
  onCommit,
143
- }: FieldProps): JSX.Element | null {
161
+ }: TokenPickerProps): JSX.Element | null {
144
162
  const colors =
145
163
  tokens.value?.groups?.find((group) => group.kind === 'color')?.tokens ?? []
146
164
  if (colors.length === 0) return null
@@ -185,57 +203,114 @@ function ElementFields({
185
203
  const commit = (property: string) => (value: string) => {
186
204
  if (address) void editElement(frameRef, key, address, property, value)
187
205
  }
188
- const isTextOnly = holdsTextAlone(node)
206
+ const rect = node.getBoundingClientRect()
207
+ const font = currentValue(node, 'font-family')
189
208
  const text = node.textContent ?? ''
190
209
 
210
+ /**
211
+ * Previews into the frame's own inline style and writes through the same
212
+ * edit as typing. A reload mid-drag replaces the document, so a release
213
+ * against one no longer on screen posts nothing.
214
+ */
215
+ const scrubOf = (property: string): Scrub => ({
216
+ clamp: (value) => clampScrub(property, value),
217
+ begin: () => {
218
+ const original = inlineValue(node, property)
219
+ const style = (node as HTMLElement).style
220
+ return {
221
+ preview: (shown) =>
222
+ style.setProperty(property, toCssValue(property, shown)),
223
+ restore: () =>
224
+ original
225
+ ? style.setProperty(property, original)
226
+ : style.removeProperty(property),
227
+ commit: (shown) => {
228
+ if (frameDocuments.value.get(key) !== doc || !node.isConnected) return
229
+ commit(property)(toCssValue(property, shown))
230
+ },
231
+ }
232
+ },
233
+ })
234
+
235
+ const styleField = (field: StyleField): JSX.Element => (
236
+ <Field
237
+ key={field.property}
238
+ label={field.label}
239
+ glyph={field.glyph}
240
+ initial={displayValue(field.property, currentValue(node, field.property))}
241
+ isBusy={isBusy}
242
+ isWide={field.isWide}
243
+ placeholder={field.placeholder}
244
+ onCommit={(typed) =>
245
+ commit(field.property)(toCssValue(field.property, typed))
246
+ }
247
+ scrub={'style' in node ? scrubOf(field.property) : undefined}
248
+ />
249
+ )
250
+
191
251
  return (
192
- <dl class="box styles">
193
- {positionRows(node).map((row) => (
194
- <div key={row.name} class="box-row">
195
- <dt>{row.name}</dt>
196
- <dd title={row.value}>{row.value}</dd>
197
- </div>
198
- ))}
199
- <div class="box-row">
200
- <dt>text</dt>
201
- <dd title={excerpt(text)}>
202
- {isTextOnly ? (
203
- <Field
204
- label="text"
205
- initial={text}
206
- isBusy={isBusy}
207
- onCommit={commit('text')}
208
- />
209
- ) : (
210
- excerpt(text)
211
- )}
212
- </dd>
213
- </div>
214
- {STYLE_FIELDS.map((field) => {
215
- const initial = currentValue(node, field.property)
216
- return (
217
- <div key={field.property} class="box-row">
218
- <dt>{field.label}</dt>
219
- <dd class="field-cell">
220
- <Field
252
+ <>
253
+ <Section title="Layout">
254
+ <ReadOnlyField
255
+ label="x"
256
+ glyph="X"
257
+ value={String(Math.round(rect.left))}
258
+ />
259
+ <ReadOnlyField
260
+ label="y"
261
+ glyph="Y"
262
+ value={String(Math.round(rect.top))}
263
+ />
264
+ {SIZE_FIELDS.map(styleField)}
265
+ </Section>
266
+ <Section title="Flex">{FLEX_FIELDS.map(styleField)}</Section>
267
+ <Section title="Typography">
268
+ {TYPE_FIELDS.map(styleField)}
269
+ {font ? (
270
+ <ReadOnlyField label="font" glyph="Font" value={font} isWide />
271
+ ) : null}
272
+ </Section>
273
+ <Section title="Fill">
274
+ {FILL_FIELDS.map((field) => {
275
+ const initial = currentValue(node, field.property)
276
+ return (
277
+ <div key={field.property} class="fill-row is-wide">
278
+ {styleField(field)}
279
+ {isRawInline(node, field.property) ? (
280
+ <span
281
+ class="raw"
282
+ title="Set as a raw value, so it will not follow the theme. Pick a token to fix it"
283
+ >
284
+ raw
285
+ </span>
286
+ ) : null}
287
+ <TokenPicker
221
288
  label={field.label}
222
289
  initial={initial}
223
290
  isBusy={isBusy}
224
291
  onCommit={commit(field.property)}
225
292
  />
226
- {field.isColor ? (
227
- <TokenPicker
228
- label={field.label}
229
- initial={initial}
230
- isBusy={isBusy}
231
- onCommit={commit(field.property)}
232
- />
233
- ) : null}
234
- </dd>
235
- </div>
236
- )
237
- })}
238
- </dl>
293
+ </div>
294
+ )
295
+ })}
296
+ </Section>
297
+ <Section title="Text">
298
+ {holdsTextAlone(node) ? (
299
+ <Field
300
+ label="text"
301
+ glyph="T"
302
+ initial={text}
303
+ isBusy={isBusy}
304
+ isWide
305
+ onCommit={commit('text')}
306
+ />
307
+ ) : (
308
+ <p class="text-excerpt is-wide" title={excerpt(text)}>
309
+ {excerpt(text)}
310
+ </p>
311
+ )}
312
+ </Section>
313
+ </>
239
314
  )
240
315
  }
241
316
 
@@ -300,14 +375,20 @@ export function Inspector(): JSX.Element {
300
375
  <p class="detail" title={frame.name}>
301
376
  {frame.name}
302
377
  </p>
303
- <dl class="box">
304
- {FIELDS.map((field) => (
305
- <div key={field} class="box-row">
306
- <dt>{field}</dt>
307
- <dd>{frame[field]}</dd>
308
- </div>
309
- ))}
310
- </dl>
378
+ <div class="field-grid">
379
+ <ReadOnlyField label="x" glyph="X" value={String(frame.x)} />
380
+ <ReadOnlyField label="y" glyph="Y" value={String(frame.y)} />
381
+ <ReadOnlyField
382
+ label="width"
383
+ glyph="W"
384
+ value={String(frame.width)}
385
+ />
386
+ <ReadOnlyField
387
+ label="height"
388
+ glyph="H"
389
+ value={String(frame.height)}
390
+ />
391
+ </div>
311
392
  </>
312
393
  ) : (
313
394
  <p class="empty">Select a frame to see its box</p>