@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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
- package/claude/skills/backlog-triage/SKILL.md +19 -6
- package/claude/skills/canon-operator/REQUIREMENT.md +1 -0
- package/claude/skills/canon-operator/SKILL.md +5 -1
- package/claude/skills/canvas/REQUIREMENT.md +49 -0
- package/claude/skills/canvas/SKILL.md +106 -0
- package/claude/skills/design-extract/REQUIREMENT.md +1 -1
- package/claude/skills/draft-doc/REQUIREMENT.md +2 -1
- package/claude/skills/draft-doc/references/readme/agent-facing.md +59 -0
- package/claude/skills/draft-doc/references/readme/application.md +66 -0
- package/claude/skills/draft-doc/references/readme/cli.md +64 -0
- package/claude/skills/draft-doc/references/readme/library.md +57 -0
- package/claude/skills/draft-doc/references/readme/plugin.md +66 -0
- package/claude/skills/draft-doc/references/readme.md +33 -4
- package/claude/skills/draft-figure/REQUIREMENT.md +14 -3
- package/claude/skills/draft-figure/SKILL.md +24 -51
- package/claude/skills/draft-figure/references/architecture-set.md +35 -0
- package/claude/skills/draft-figure/references/hand-drawn.md +31 -0
- package/claude/skills/draft-figure/references/mermaid.md +55 -0
- package/claude/skills/git-followup/SKILL.md +17 -6
- package/claude/skills/git-pr/references/evidence.md +4 -2
- package/claude/skills/git-split/SKILL.md +1 -1
- package/claude/skills/index-lookup/SKILL.md +2 -2
- package/claude/skills/review-address/SKILL.md +6 -13
- package/claude/skills/review-address/references/rest-reads.md +43 -0
- package/claude/skills/review-craft/references/ui.md +7 -1
- package/claude/skills/review-pr/REQUIREMENT.md +1 -0
- package/claude/skills/review-pr/SKILL.md +9 -3
- package/claude/skills/review-pr/references/close-out.md +2 -2
- package/claude/skills/review-pr/references/post.md +17 -5
- package/claude/skills/review-pr/references/scope.md +10 -8
- package/claude/skills/review-ui/SKILL.md +12 -4
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +30 -1
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +4 -0
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +4 -4
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +6 -0
- package/claude/skills/role-orchestrator/scripts/poll.ts +90 -27
- package/claude/skills/role-orchestrator/scripts/repo.ts +3 -5
- package/claude/skills/role-orchestrator/scripts/watch.ts +4 -7
- package/docs/agents/canvas.md +6 -2
- package/docs/agents/commands.md +45 -45
- package/docs/agents/context-audit-checks.md +2 -2
- package/docs/agents/index.md +0 -1
- package/docs/agents/key-changes.md +3 -3
- package/docs/agents/pr-evidence.md +11 -2
- package/docs/agents/pr-reads.md +5 -4
- package/docs/target/projects.md +4 -4
- package/docs/workflow/ai-workflow.md +0 -1
- package/docs/workflow/skill-map.md +3 -3
- package/docs/workflow/visual-design-workflow.md +2 -1
- package/governance/renames.toml +0 -4
- package/governance/rules/standards/markdown.md +1 -0
- package/package.json +1 -2
- package/scripts/core/install-check.sh +1 -1
- package/src/audits/catalog.ts +2 -2
- package/src/canvas/client/inspector/field.tsx +200 -0
- package/src/canvas/client/inspector/fields.css +151 -0
- package/src/canvas/client/inspector/section.tsx +27 -0
- package/src/canvas/client/inspector/values.ts +60 -0
- package/src/canvas/client/inspector.tsx +192 -111
- package/src/canvas/client/shell.css +0 -94
- package/src/canvas/tokens.ts +9 -1
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/cases/workflow.ts +1 -1
- package/src/claude/seeds.ts +0 -1
- package/src/commands/canvas.ts +29 -0
- package/src/commands/design.ts +2 -64
- package/src/commands/pr.ts +219 -202
- package/src/context/audit.ts +7 -6
- package/src/design/budget.ts +149 -0
- package/src/design/document.ts +22 -10
- package/src/design/render.ts +11 -1
- package/src/design/tokens.ts +40 -61
- package/src/gate/measures.ts +49 -0
- package/src/gate/stages.ts +5 -1
- package/src/help.ts +2 -2
- package/src/migrate/skill-names.ts +7 -1
- package/src/pr/evidence.ts +25 -5
- package/src/pr/rest.ts +137 -0
- package/standards/canvas.md +112 -0
- package/standards/context.md +2 -1
- package/standards/design.md +13 -3
- package/standards/figures.md +0 -1
- package/standards/index.md +1 -1
- package/standards/markdown.md +1 -0
- package/standards/mermaid.md +1 -7
- package/tooling/claude/reference.md +0 -5
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +1 -1
- package/claude/skills/draft-diagram/REQUIREMENT.md +0 -45
- package/claude/skills/draft-diagram/SKILL.md +0 -177
- package/docs/agents/design-board.md +0 -33
- package/governance/rules/standards/diagrams.md +0 -14
- package/governance/rules/standards/mermaid.md +0 -15
- package/scripts/core/check-gallery-exclusion.sh +0 -50
- package/src/design/board.ts +0 -576
- package/standards/diagrams.md +0 -132
- 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
|
|
54
|
+
readonly isWide?: boolean
|
|
55
|
+
readonly placeholder?: string
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
/**
|
|
46
|
-
* The basic set,
|
|
47
|
-
* property outside it, so
|
|
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
|
|
50
|
-
{ label: '
|
|
51
|
-
{ label: '
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{ label: '
|
|
56
|
-
{ label: '
|
|
57
|
-
{ label: '
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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
|
-
}:
|
|
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
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
{
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
<
|
|
304
|
-
{
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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>
|