@erclx/canon 5.24.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/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 +1 -1
- 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 +1 -1
- 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 +1 -0
- 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 +154 -115
- package/src/canvas/client/shell.css +0 -101
- 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/design.ts +2 -64
- package/src/commands/pr.ts +219 -202
- package/src/context/audit.ts +7 -6
- 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/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/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,200 @@
|
|
|
1
|
+
/** @jsxImportSource preact */
|
|
2
|
+
import type { JSX } from 'preact'
|
|
3
|
+
import { useRef } from 'preact/hooks'
|
|
4
|
+
import { scrubStep } from '@/canvas/client/inspector/values'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One drag of a number field's glyph. Each call takes the value as the field
|
|
8
|
+
* shows it. Only `commit` writes, so a drag posts one edit at most.
|
|
9
|
+
*/
|
|
10
|
+
export interface ScrubSession {
|
|
11
|
+
readonly preview: (value: string) => void
|
|
12
|
+
readonly restore: () => void
|
|
13
|
+
readonly commit: (value: string) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Opens a session at press, so what `restore` returns to is read before any
|
|
18
|
+
* preview, however often the panel renders during the drag.
|
|
19
|
+
*/
|
|
20
|
+
export interface Scrub {
|
|
21
|
+
readonly begin: () => ScrubSession
|
|
22
|
+
/** Holds a scrubbed number inside what the property accepts. */
|
|
23
|
+
readonly clamp: (value: number) => number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface FieldProps {
|
|
27
|
+
/** The full word, which names the input for assistive technology. */
|
|
28
|
+
readonly label: string
|
|
29
|
+
/** The short mark drawn inside the field, hidden from assistive technology. */
|
|
30
|
+
readonly glyph: string
|
|
31
|
+
readonly initial: string
|
|
32
|
+
readonly isBusy: boolean
|
|
33
|
+
readonly isWide?: boolean
|
|
34
|
+
readonly placeholder?: string
|
|
35
|
+
readonly onCommit: (value: string) => void
|
|
36
|
+
/** Makes the glyph a drag handle. Typing stays the keyboard path. */
|
|
37
|
+
readonly scrub?: Scrub
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const NUMBER = /^-?\d*\.?\d+$/
|
|
41
|
+
|
|
42
|
+
interface Drag {
|
|
43
|
+
readonly pointerId: number
|
|
44
|
+
/** What the input held at press, which a cancel puts back. */
|
|
45
|
+
readonly typed: string
|
|
46
|
+
/** The number the drag started from, an empty field's placeholder included. */
|
|
47
|
+
readonly start: string
|
|
48
|
+
readonly session: ScrubSession
|
|
49
|
+
lastX: number
|
|
50
|
+
value: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fieldClass(isWide: boolean | undefined, modifier?: string): string {
|
|
54
|
+
return ['glyph-field', isWide ? 'is-wide' : '', modifier ?? '']
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join(' ')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Commits on change, which a text input fires on Enter or on leaving it, and
|
|
61
|
+
* sends nothing when the value is what it started at. Escape puts it back.
|
|
62
|
+
*/
|
|
63
|
+
export function Field({
|
|
64
|
+
label,
|
|
65
|
+
glyph,
|
|
66
|
+
initial,
|
|
67
|
+
isBusy,
|
|
68
|
+
isWide,
|
|
69
|
+
placeholder,
|
|
70
|
+
onCommit,
|
|
71
|
+
scrub,
|
|
72
|
+
}: FieldProps): JSX.Element {
|
|
73
|
+
const input = useRef<HTMLInputElement>(null)
|
|
74
|
+
const handle = useRef<HTMLSpanElement>(null)
|
|
75
|
+
const drag = useRef<Drag | null>(null)
|
|
76
|
+
|
|
77
|
+
const end = (pointerId: number): Drag | undefined => {
|
|
78
|
+
const held = drag.current
|
|
79
|
+
if (!held || held.pointerId !== pointerId) return undefined
|
|
80
|
+
drag.current = null
|
|
81
|
+
return held
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* An empty field scrubs from the number its placeholder shows. */
|
|
85
|
+
const scrubFrom = (value: string): string =>
|
|
86
|
+
value.trim() || placeholder?.trim() || ''
|
|
87
|
+
const isScrubbable = scrub !== undefined && NUMBER.test(scrubFrom(initial))
|
|
88
|
+
|
|
89
|
+
const handlers = isScrubbable
|
|
90
|
+
? {
|
|
91
|
+
onPointerDown: (event: PointerEvent) => {
|
|
92
|
+
const typed = input.current?.value ?? ''
|
|
93
|
+
const start = scrubFrom(typed)
|
|
94
|
+
if (isBusy || !NUMBER.test(start)) return
|
|
95
|
+
event.preventDefault()
|
|
96
|
+
/*
|
|
97
|
+
* Capture keeps the release coming here when the pointer leaves the
|
|
98
|
+
* glyph, the panel, or crosses into a frame's own document.
|
|
99
|
+
*/
|
|
100
|
+
try {
|
|
101
|
+
handle.current?.setPointerCapture(event.pointerId)
|
|
102
|
+
} catch {
|
|
103
|
+
/* A pointer the browser no longer tracks ends on cancel instead. */
|
|
104
|
+
}
|
|
105
|
+
drag.current = {
|
|
106
|
+
pointerId: event.pointerId,
|
|
107
|
+
typed,
|
|
108
|
+
start,
|
|
109
|
+
session: scrub.begin(),
|
|
110
|
+
lastX: event.clientX,
|
|
111
|
+
value: Number(start),
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
onPointerMove: (event: PointerEvent) => {
|
|
115
|
+
const held = drag.current
|
|
116
|
+
if (!held || held.pointerId !== event.pointerId) return
|
|
117
|
+
const moved = event.clientX - held.lastX
|
|
118
|
+
held.lastX = event.clientX
|
|
119
|
+
held.value = scrub.clamp(
|
|
120
|
+
Math.round((held.value + moved * scrubStep(event.shiftKey)) * 10) /
|
|
121
|
+
10,
|
|
122
|
+
)
|
|
123
|
+
const shown = String(held.value)
|
|
124
|
+
if (input.current) input.current.value = shown
|
|
125
|
+
held.session.preview(shown)
|
|
126
|
+
},
|
|
127
|
+
onPointerUp: (event: PointerEvent) => {
|
|
128
|
+
const held = end(event.pointerId)
|
|
129
|
+
if (!held) return
|
|
130
|
+
const shown = String(held.value)
|
|
131
|
+
if (shown === held.start) held.session.restore()
|
|
132
|
+
else held.session.commit(shown)
|
|
133
|
+
},
|
|
134
|
+
onPointerCancel: (event: PointerEvent) => {
|
|
135
|
+
const held = end(event.pointerId)
|
|
136
|
+
if (!held) return
|
|
137
|
+
if (input.current) input.current.value = held.typed
|
|
138
|
+
held.session.restore()
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
: {}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div class={fieldClass(isWide)}>
|
|
145
|
+
<span
|
|
146
|
+
ref={handle}
|
|
147
|
+
class={isScrubbable ? 'glyph is-scrub' : 'glyph'}
|
|
148
|
+
aria-hidden="true"
|
|
149
|
+
title={label}
|
|
150
|
+
{...handlers}
|
|
151
|
+
>
|
|
152
|
+
{glyph}
|
|
153
|
+
</span>
|
|
154
|
+
<input
|
|
155
|
+
ref={input}
|
|
156
|
+
class="glyph-field-input"
|
|
157
|
+
type="text"
|
|
158
|
+
aria-label={label}
|
|
159
|
+
title={initial}
|
|
160
|
+
defaultValue={initial}
|
|
161
|
+
placeholder={placeholder}
|
|
162
|
+
disabled={isBusy}
|
|
163
|
+
spellcheck={false}
|
|
164
|
+
onChange={(event) => {
|
|
165
|
+
const value = event.currentTarget.value.trim()
|
|
166
|
+
if (value !== initial.trim()) onCommit(value)
|
|
167
|
+
}}
|
|
168
|
+
onKeyDown={(event) => {
|
|
169
|
+
if (event.key === 'Escape') event.currentTarget.value = initial
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface ReadOnlyFieldProps {
|
|
177
|
+
readonly label: string
|
|
178
|
+
readonly glyph: string
|
|
179
|
+
readonly value: string
|
|
180
|
+
readonly isWide?: boolean
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A value the panel shows and nothing here writes, drawn like a field. */
|
|
184
|
+
export function ReadOnlyField({
|
|
185
|
+
label,
|
|
186
|
+
glyph,
|
|
187
|
+
value,
|
|
188
|
+
isWide,
|
|
189
|
+
}: ReadOnlyFieldProps): JSX.Element {
|
|
190
|
+
return (
|
|
191
|
+
<div class={fieldClass(isWide, 'is-readonly')}>
|
|
192
|
+
<span class="glyph" aria-hidden="true" title={label}>
|
|
193
|
+
{glyph}
|
|
194
|
+
</span>
|
|
195
|
+
<output class="glyph-field-input" aria-label={label} title={value}>
|
|
196
|
+
{value}
|
|
197
|
+
</output>
|
|
198
|
+
</div>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
@@ -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
|
+
}
|