@erclx/canon 5.52.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 (201) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +11 -13
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +5 -8
  5. package/claude/skills/context-fold/REQUIREMENT.md +0 -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/design-taste/references/kinds.md +0 -1
  13. package/claude/skills/draw-figure/REQUIREMENT.md +3 -4
  14. package/claude/skills/draw-figure/SKILL.md +5 -6
  15. package/claude/skills/draw-figure/references/figures.md +0 -1
  16. package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
  17. package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -5
  18. package/claude/skills/pick-by-eye/SKILL.md +34 -41
  19. package/claude/skills/pick-by-eye/references/board.md +42 -0
  20. package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
  21. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  22. package/claude/skills/teach-craft/REQUIREMENT.md +30 -0
  23. package/claude/skills/teach-craft/SKILL.md +70 -0
  24. package/claude/skills/teach-craft/references/adopted.md +113 -0
  25. package/claude/skills/teach-craft/references/lesson-craft.md +60 -0
  26. package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +41 -6
  27. package/docs/agents/audits.md +0 -1
  28. package/docs/agents/install-and-sync.md +0 -8
  29. package/docs/target/migrations.md +16 -2
  30. package/docs/target/projects.md +1 -1
  31. package/docs/workflow/parallel-features.md +1 -1
  32. package/docs/workflow/skill-map-on-request.md +10 -22
  33. package/docs/workflow/skill-map.md +11 -14
  34. package/docs/workflow/visual-design-workflow.md +1 -4
  35. package/governance/detect.toml +1 -6
  36. package/governance/renames.toml +0 -1
  37. package/package.json +2 -8
  38. package/scripts/core/regen-web-previews.ts +2 -67
  39. package/src/audits/catalog.ts +0 -1
  40. package/src/browser/engine.ts +3 -6
  41. package/src/capture/render.ts +1 -1
  42. package/src/claude/cases/authoring.ts +0 -12
  43. package/src/claude/cases/misc.ts +0 -22
  44. package/src/claude/cases/workflow.ts +2 -38
  45. package/src/claude/skills-audit.ts +1 -1
  46. package/src/cli/help.ts +0 -10
  47. package/src/cli.ts +0 -8
  48. package/src/commands/design.ts +16 -315
  49. package/src/commands/records.ts +0 -1
  50. package/src/commands/serve.ts +2 -2
  51. package/src/commands/sync.ts +4 -5
  52. package/src/design/components.ts +8 -1514
  53. package/src/design/css.ts +5 -6
  54. package/src/design/fonts.ts +3 -5
  55. package/src/design/regen.ts +5 -32
  56. package/src/design/render.ts +1 -1
  57. package/src/design/tokens.ts +3 -2
  58. package/src/driver/drive.ts +3 -4
  59. package/src/driver/steps.ts +2 -5
  60. package/src/gate/stages.ts +4 -10
  61. package/src/gov/audit.ts +1 -6
  62. package/src/inventory/walk.ts +1 -1
  63. package/src/markdown/scope.ts +6 -35
  64. package/src/records/validate.ts +4 -176
  65. package/src/sandbox/equivalence.ts +0 -5
  66. package/src/shipped/references.ts +1 -2
  67. package/src/sync/check.ts +4 -29
  68. package/src/sync/stamp.ts +6 -6
  69. package/src/sync/target.ts +1 -4
  70. package/tooling/claude/reference.md +1 -1
  71. package/tooling/claude/seeds/canon/DESIGN.md +1 -1
  72. package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
  73. package/claude/skills/canvas-board/SKILL.md +0 -143
  74. package/claude/skills/canvas-board/references/canvas.md +0 -170
  75. package/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
  76. package/claude/skills/draft-and-pick/SKILL.md +0 -11
  77. package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
  78. package/claude/skills/draft-doc/SKILL.md +0 -11
  79. package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
  80. package/claude/skills/draft-figure/SKILL.md +0 -11
  81. package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
  82. package/claude/skills/draft-identity/SKILL.md +0 -11
  83. package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
  84. package/claude/skills/draft-ready/SKILL.md +0 -11
  85. package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
  86. package/claude/skills/draft-screencast/SKILL.md +0 -11
  87. package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
  88. package/claude/skills/draft-slides/SKILL.md +0 -11
  89. package/claude/skills/make-deck/REQUIREMENT.md +0 -36
  90. package/claude/skills/make-deck/SKILL.md +0 -80
  91. package/claude/skills/make-deck/references/deck.md +0 -183
  92. package/claude/skills/read-frames/REQUIREMENT.md +0 -30
  93. package/claude/skills/read-frames/SKILL.md +0 -55
  94. package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
  95. package/claude/skills/record-screencast/SKILL.md +0 -99
  96. package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
  97. package/claude/skills/script-screencast/SKILL.md +0 -70
  98. package/claude/skills/script-screencast/references/beats-template.md +0 -119
  99. package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
  100. package/claude/skills/teach-workspace/SKILL.md +0 -196
  101. package/claude/skills/teach-workspace/references/glossary.md +0 -90
  102. package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
  103. package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
  104. package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
  105. package/claude/skills/teach-workspace/references/promotion.md +0 -116
  106. package/claude/skills/teach-workspace/references/revisit.md +0 -28
  107. package/claude/skills/teach-workspace/references/teach.md +0 -211
  108. package/claude/skills/video-craft/REQUIREMENT.md +0 -35
  109. package/claude/skills/video-craft/SKILL.md +0 -96
  110. package/claude/skills/video-craft/references/adopted.md +0 -110
  111. package/governance/rules/ui/canvas.md +0 -13
  112. package/src/canvas/address.ts +0 -227
  113. package/src/canvas/arrange.ts +0 -66
  114. package/src/canvas/capture.ts +0 -391
  115. package/src/canvas/client/app.tsx +0 -188
  116. package/src/canvas/client/inspector/align-grid.tsx +0 -162
  117. package/src/canvas/client/inspector/color-field.tsx +0 -398
  118. package/src/canvas/client/inspector/color-inputs.tsx +0 -139
  119. package/src/canvas/client/inspector/color-picker.tsx +0 -668
  120. package/src/canvas/client/inspector/color-space.ts +0 -240
  121. package/src/canvas/client/inspector/field.tsx +0 -263
  122. package/src/canvas/client/inspector/fields.css +0 -763
  123. package/src/canvas/client/inspector/section.tsx +0 -27
  124. package/src/canvas/client/inspector/size-field.tsx +0 -69
  125. package/src/canvas/client/inspector/token-select.tsx +0 -109
  126. package/src/canvas/client/inspector/values.ts +0 -297
  127. package/src/canvas/client/inspector.tsx +0 -692
  128. package/src/canvas/client/keys.ts +0 -71
  129. package/src/canvas/client/layers.tsx +0 -118
  130. package/src/canvas/client/main.tsx +0 -71
  131. package/src/canvas/client/pages-panel.tsx +0 -212
  132. package/src/canvas/client/panel-handle.tsx +0 -90
  133. package/src/canvas/client/selection.tsx +0 -199
  134. package/src/canvas/client/shell.css +0 -762
  135. package/src/canvas/client/state.ts +0 -792
  136. package/src/canvas/client/surface.tsx +0 -803
  137. package/src/canvas/client/theme-panel.tsx +0 -55
  138. package/src/canvas/client/tools.tsx +0 -153
  139. package/src/canvas/content.ts +0 -781
  140. package/src/canvas/edit.ts +0 -575
  141. package/src/canvas/editing.ts +0 -203
  142. package/src/canvas/fit.ts +0 -238
  143. package/src/canvas/global.d.ts +0 -15
  144. package/src/canvas/history.ts +0 -116
  145. package/src/canvas/index.html +0 -20
  146. package/src/canvas/server.ts +0 -766
  147. package/src/canvas/tokens.ts +0 -221
  148. package/src/commands/canvas.ts +0 -1097
  149. package/src/commands/demo.ts +0 -542
  150. package/src/commands/slides.ts +0 -184
  151. package/src/commands/teach.ts +0 -1401
  152. package/src/demo/beats.ts +0 -135
  153. package/src/demo/compile.ts +0 -350
  154. package/src/demo/container.ts +0 -219
  155. package/src/demo/cursors.ts +0 -55
  156. package/src/demo/drive.ts +0 -442
  157. package/src/demo/pointer.ts +0 -178
  158. package/src/demo/theme.ts +0 -112
  159. package/src/demo/timeline.ts +0 -64
  160. package/src/design/adapter.ts +0 -59
  161. package/src/design/base.css +0 -203
  162. package/src/design/brand-favicon.ts +0 -18
  163. package/src/design/neutral.ts +0 -127
  164. package/src/design/project-fonts.ts +0 -392
  165. package/src/slides/convert/chart.ts +0 -165
  166. package/src/slides/convert/deck.ts +0 -244
  167. package/src/slides/convert/export.ts +0 -567
  168. package/src/slides/convert/master.ts +0 -198
  169. package/src/slides/convert/shapes.ts +0 -804
  170. package/src/slides/convert/svg.ts +0 -38
  171. package/src/slides/convert/walk.ts +0 -671
  172. package/src/slides/decks.ts +0 -110
  173. package/src/slides/open.ts +0 -18
  174. package/src/slides/package/fonts.ts +0 -301
  175. package/src/slides/package/motion.ts +0 -316
  176. package/src/slides/package/test-face.ts +0 -113
  177. package/src/teach/browser/compile.ts +0 -44
  178. package/src/teach/browser/dismiss.ts +0 -76
  179. package/src/teach/browser/focus-line.ts +0 -19
  180. package/src/teach/browser/glossary-filter.ts +0 -44
  181. package/src/teach/browser/head.ts +0 -30
  182. package/src/teach/browser/quiz.ts +0 -19
  183. package/src/teach/browser/reload.ts +0 -9
  184. package/src/teach/browser/sidebar.ts +0 -233
  185. package/src/teach/browser/theme.ts +0 -18
  186. package/src/teach/components/code.tsx +0 -24
  187. package/src/teach/components/heading.tsx +0 -11
  188. package/src/teach/components/list.tsx +0 -17
  189. package/src/teach/components/paragraph.tsx +0 -29
  190. package/src/teach/components/refs.tsx +0 -28
  191. package/src/teach/cross-references.ts +0 -78
  192. package/src/teach/fonts.ts +0 -16
  193. package/src/teach/highlight.ts +0 -71
  194. package/src/teach/html/jsx-dev-runtime.ts +0 -13
  195. package/src/teach/html/jsx-runtime.ts +0 -113
  196. package/src/teach/html/raw.ts +0 -28
  197. package/src/teach/lesson.ts +0 -178
  198. package/src/teach/nav.ts +0 -1337
  199. package/src/teach/render.ts +0 -240
  200. package/src/teach/workspace.ts +0 -1164
  201. package/src/teach/write-if-changed.ts +0 -17
@@ -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
- }