@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.
- package/CHANGELOG.md +10 -0
- package/README.md +10 -9
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/canon-cli/SKILL.md +5 -5
- package/claude/skills/design-identity/REQUIREMENT.md +2 -2
- package/claude/skills/design-identity/SKILL.md +6 -6
- package/claude/skills/design-record/REQUIREMENT.md +2 -2
- package/claude/skills/design-record/SKILL.md +4 -4
- package/claude/skills/design-record/references/folder.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +1 -1
- package/claude/skills/draw-figure/REQUIREMENT.md +1 -1
- package/claude/skills/draw-figure/SKILL.md +1 -1
- package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -4
- package/claude/skills/pick-by-eye/SKILL.md +34 -41
- package/claude/skills/pick-by-eye/references/board.md +42 -0
- package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
- package/claude/skills/plan-groundwork/SKILL.md +1 -1
- package/claude/skills/teach-craft/REQUIREMENT.md +1 -1
- package/claude/skills/teach-craft/SKILL.md +32 -1
- package/claude/skills/teach-craft/references/adopted.md +113 -0
- package/claude/skills/teach-craft/references/lesson-craft.md +10 -2
- package/claude/skills/teach-craft/references/pedagogy.md +29 -2
- package/docs/agents/install-and-sync.md +0 -8
- package/docs/target/migrations.md +8 -3
- package/docs/target/projects.md +1 -1
- package/docs/workflow/skill-map.md +0 -1
- package/docs/workflow/visual-design-workflow.md +1 -4
- package/governance/detect.toml +1 -6
- package/governance/renames.toml +0 -1
- package/package.json +2 -5
- package/src/claude/cases/misc.ts +0 -5
- package/src/claude/skills-audit.ts +1 -0
- package/src/cli/help.ts +0 -3
- package/src/cli.ts +0 -2
- package/src/commands/design.ts +16 -315
- package/src/commands/sync.ts +4 -5
- package/src/design/regen.ts +5 -32
- package/src/design/render.ts +1 -1
- package/src/design/tokens.ts +3 -2
- package/src/gate/stages.ts +4 -10
- package/src/gov/audit.ts +1 -6
- package/src/markdown/scope.ts +2 -4
- package/src/shipped/references.ts +1 -2
- package/src/sync/check.ts +4 -29
- package/src/sync/stamp.ts +6 -6
- package/src/sync/target.ts +1 -4
- package/tooling/claude/reference.md +1 -1
- package/tooling/claude/seeds/canon/DESIGN.md +1 -1
- package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
- package/claude/skills/canvas-board/SKILL.md +0 -143
- package/claude/skills/canvas-board/references/canvas.md +0 -170
- package/governance/rules/ui/canvas.md +0 -13
- package/src/canvas/address.ts +0 -227
- package/src/canvas/arrange.ts +0 -66
- package/src/canvas/capture.ts +0 -391
- package/src/canvas/client/app.tsx +0 -188
- package/src/canvas/client/inspector/align-grid.tsx +0 -162
- package/src/canvas/client/inspector/color-field.tsx +0 -398
- package/src/canvas/client/inspector/color-inputs.tsx +0 -139
- package/src/canvas/client/inspector/color-picker.tsx +0 -668
- package/src/canvas/client/inspector/color-space.ts +0 -240
- package/src/canvas/client/inspector/field.tsx +0 -263
- package/src/canvas/client/inspector/fields.css +0 -763
- package/src/canvas/client/inspector/section.tsx +0 -27
- package/src/canvas/client/inspector/size-field.tsx +0 -69
- package/src/canvas/client/inspector/token-select.tsx +0 -109
- package/src/canvas/client/inspector/values.ts +0 -297
- package/src/canvas/client/inspector.tsx +0 -692
- package/src/canvas/client/keys.ts +0 -71
- package/src/canvas/client/layers.tsx +0 -118
- package/src/canvas/client/main.tsx +0 -71
- package/src/canvas/client/pages-panel.tsx +0 -212
- package/src/canvas/client/panel-handle.tsx +0 -90
- package/src/canvas/client/selection.tsx +0 -199
- package/src/canvas/client/shell.css +0 -762
- package/src/canvas/client/state.ts +0 -792
- package/src/canvas/client/surface.tsx +0 -803
- package/src/canvas/client/theme-panel.tsx +0 -55
- package/src/canvas/client/tools.tsx +0 -153
- package/src/canvas/content.ts +0 -781
- package/src/canvas/edit.ts +0 -575
- package/src/canvas/editing.ts +0 -203
- package/src/canvas/fit.ts +0 -238
- package/src/canvas/global.d.ts +0 -15
- package/src/canvas/history.ts +0 -116
- package/src/canvas/index.html +0 -20
- package/src/canvas/server.ts +0 -766
- package/src/canvas/tokens.ts +0 -221
- package/src/commands/canvas.ts +0 -1097
- package/src/design/adapter.ts +0 -59
- package/src/design/base.css +0 -203
- package/src/design/brand-favicon.ts +0 -18
- package/src/design/neutral.ts +0 -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
|
-
}
|