@erclx/canon 5.26.0 → 5.27.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/README.md +2 -2
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +2 -0
- package/claude/skills/auto-ship/SKILL.md +3 -11
- package/claude/skills/auto-ship/references/review-findings.md +20 -0
- package/claude/skills/backlog-triage/SKILL.md +2 -0
- package/claude/skills/bash-cli-script/SKILL.md +2 -0
- package/claude/skills/build-in-slices/SKILL.md +2 -0
- package/claude/skills/canon-cli/SKILL.md +2 -0
- package/claude/skills/canon-feedback/SKILL.md +2 -0
- package/claude/skills/canon-feedback-triage/SKILL.md +2 -0
- package/claude/skills/canon-operator/SKILL.md +2 -0
- package/claude/skills/canon-rollout/SKILL.md +3 -11
- package/claude/skills/canon-rollout/references/failure-recovery.md +20 -0
- package/claude/skills/canvas/SKILL.md +13 -0
- package/claude/skills/ci-workflow/SKILL.md +2 -0
- package/claude/skills/code-craft/SKILL.md +2 -0
- package/claude/skills/codebase-layout/SKILL.md +2 -0
- package/claude/skills/context-fold/SKILL.md +3 -17
- package/claude/skills/context-fold/references/completion-report.md +26 -0
- package/claude/skills/create-rule/SKILL.md +2 -0
- package/claude/skills/create-skill/SKILL.md +2 -0
- package/claude/skills/create-standard/SKILL.md +2 -0
- package/claude/skills/decision-escalate/SKILL.md +2 -0
- package/claude/skills/deploy-app/SKILL.md +2 -0
- package/claude/skills/deprecation-migration/SKILL.md +2 -0
- package/claude/skills/design-extract/SKILL.md +2 -0
- package/claude/skills/design-taste/SKILL.md +2 -0
- package/claude/skills/docs-sync/SKILL.md +2 -0
- package/claude/skills/document-health/SKILL.md +2 -0
- package/claude/skills/draft-and-pick/SKILL.md +2 -0
- package/claude/skills/draft-doc/SKILL.md +2 -0
- package/claude/skills/draft-doc/references/wiki.md +1 -1
- package/claude/skills/draft-figure/SKILL.md +2 -0
- package/claude/skills/draft-identity/SKILL.md +2 -0
- package/claude/skills/draft-ready/SKILL.md +2 -0
- package/claude/skills/draft-screencast/SKILL.md +2 -0
- package/claude/skills/draft-slides/SKILL.md +2 -0
- package/claude/skills/git-branch/SKILL.md +2 -0
- package/claude/skills/git-commit/SKILL.md +2 -0
- package/claude/skills/git-followup/SKILL.md +2 -0
- package/claude/skills/git-issue/SKILL.md +2 -0
- package/claude/skills/git-pr/SKILL.md +2 -0
- package/claude/skills/git-pr/references/evidence.md +6 -6
- package/claude/skills/git-ship/SKILL.md +2 -0
- package/claude/skills/git-split/SKILL.md +2 -0
- package/claude/skills/git-stage/SKILL.md +2 -0
- package/claude/skills/git-worktree/SKILL.md +2 -0
- package/claude/skills/index-lookup/SKILL.md +2 -0
- package/claude/skills/markdown-propose/SKILL.md +2 -0
- package/claude/skills/memory-capture/SKILL.md +2 -0
- package/claude/skills/memory-review/SKILL.md +2 -0
- package/claude/skills/plan-feature/SKILL.md +2 -0
- package/claude/skills/plan-groundwork/SKILL.md +2 -0
- package/claude/skills/plan-intake/SKILL.md +2 -0
- package/claude/skills/plan-intake-answer/SKILL.md +2 -0
- package/claude/skills/project-commands/SKILL.md +2 -0
- package/claude/skills/read-frames/SKILL.md +2 -0
- package/claude/skills/record-screencast/SKILL.md +2 -0
- package/claude/skills/repo-metadata/SKILL.md +2 -0
- package/claude/skills/restate-plainly/SKILL.md +2 -0
- package/claude/skills/review-address/SKILL.md +3 -7
- package/claude/skills/review-address/references/post-review-findings.md +16 -0
- package/claude/skills/review-branch/SKILL.md +2 -0
- package/claude/skills/review-craft/SKILL.md +2 -0
- package/claude/skills/review-craft/references/ui.md +2 -2
- package/claude/skills/review-pr/SKILL.md +4 -8
- package/claude/skills/review-pr/references/body-variants.md +52 -0
- package/claude/skills/review-pr/references/key-changes.md +16 -0
- package/claude/skills/review-pr/references/post.md +15 -43
- package/claude/skills/review-ui/SKILL.md +3 -1
- package/claude/skills/role-orchestrator/SKILL.md +5 -14
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +23 -0
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +5 -33
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -21
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +38 -0
- package/claude/skills/role-orchestrator/references/orchestrator-watch.md +26 -0
- package/claude/skills/role-planner/SKILL.md +2 -0
- package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
- package/claude/skills/role-reviewer/SKILL.md +2 -0
- package/claude/skills/role-worker/SKILL.md +2 -0
- package/claude/skills/search-craft/SKILL.md +2 -0
- package/claude/skills/seed-sync/SKILL.md +2 -0
- package/claude/skills/session-compact/SKILL.md +2 -0
- package/claude/skills/session-map/SKILL.md +2 -0
- package/claude/skills/session-relay/SKILL.md +2 -0
- package/claude/skills/session-resume/SKILL.md +2 -0
- package/claude/skills/session-worktree/SKILL.md +3 -11
- package/claude/skills/session-worktree/references/install-command.md +20 -0
- package/claude/skills/sketch-design/SKILL.md +2 -0
- package/claude/skills/systematic-debugging/SKILL.md +2 -0
- package/claude/skills/target-check/SKILL.md +2 -0
- package/claude/skills/target-setup/SKILL.md +2 -0
- package/claude/skills/task-board/SKILL.md +2 -0
- package/claude/skills/teach-workspace/SKILL.md +5 -15
- package/claude/skills/teach-workspace/references/lesson-build.md +75 -0
- package/claude/skills/teach-workspace/references/lesson-craft.md +13 -69
- package/claude/skills/teach-workspace/references/revisit.md +22 -0
- package/claude/skills/test-craft/SKILL.md +2 -0
- package/claude/skills/test-first/SKILL.md +2 -0
- package/claude/skills/ui-checklist/SKILL.md +2 -0
- package/claude/skills/ux-audit/SKILL.md +2 -0
- package/claude/skills/ux-measure/SKILL.md +2 -0
- package/claude/skills/ux-walkthrough/SKILL.md +2 -0
- package/claude/skills/video-craft/SKILL.md +2 -0
- package/claude/skills/write-human/SKILL.md +2 -0
- package/claude/skills/youtube-transcripts/SKILL.md +2 -0
- package/docs/agents/canvas.md +23 -7
- package/docs/agents/capture.md +5 -5
- package/docs/agents/commands.md +2 -2
- package/docs/agents/index.md +3 -3
- package/docs/agents/install-and-sync.md +7 -0
- package/docs/agents/key-changes.md +1 -1
- package/docs/agents/pr-evidence.md +38 -15
- package/docs/agents/pr-local.md +19 -13
- package/docs/agents/pr-preview.md +44 -21
- package/docs/agents/sandbox.md +28 -4
- package/docs/agents/scripting.md +8 -1
- package/docs/agents/skills-audit.md +2 -1
- package/docs/workflow/operating-model.md +5 -6
- package/docs/workflow/parallel-features.md +22 -1
- package/docs/workflow/skill-map.md +3 -1
- package/docs/workflow/zshrc-aliases.md +1 -1
- package/package.json +1 -1
- package/scripts/core/regen-hero.ts +1 -1
- package/scripts/lib/sandbox-path.sh +6 -162
- package/src/audits/run.ts +1 -1
- package/src/canvas/address.ts +12 -3
- package/src/canvas/client/inspector/align-grid.tsx +162 -0
- package/src/canvas/client/inspector/color-field.tsx +201 -171
- package/src/canvas/client/inspector/color-inputs.tsx +139 -0
- package/src/canvas/client/inspector/color-picker.tsx +668 -0
- package/src/canvas/client/inspector/color-space.ts +240 -0
- package/src/canvas/client/inspector/field.tsx +5 -1
- package/src/canvas/client/inspector/fields.css +395 -6
- package/src/canvas/client/inspector/size-field.tsx +69 -0
- package/src/canvas/client/inspector/values.ts +43 -1
- package/src/canvas/client/inspector.tsx +190 -4
- package/src/canvas/client/selection.tsx +197 -0
- package/src/canvas/client/shell.css +104 -12
- package/src/canvas/client/state.ts +69 -9
- package/src/canvas/client/surface.tsx +147 -22
- package/src/canvas/content.ts +50 -1
- package/src/canvas/edit.ts +37 -4
- package/src/canvas/editing.ts +203 -0
- package/src/canvas/global.d.ts +15 -0
- package/src/canvas/server.ts +149 -12
- package/src/canvas/tokens.ts +59 -17
- package/src/census/count.ts +1 -1
- package/src/claude/seeds.ts +1 -1
- package/src/claude/skills-audit.ts +28 -0
- package/src/claude/skills-drift.ts +1 -1
- package/src/claude/skills-families.ts +36 -0
- package/src/claude/skills-list.ts +76 -15
- package/src/commands/audits.ts +2 -2
- package/src/commands/canvas.ts +179 -2
- package/src/commands/claude.ts +67 -9
- package/src/commands/design.ts +142 -2
- package/src/commands/gov.ts +1 -1
- package/src/commands/hooks.ts +1 -1
- package/src/commands/indexes.ts +1 -1
- package/src/commands/init.ts +1 -1
- package/src/commands/intake.ts +1 -1
- package/src/commands/migrate.ts +1 -1
- package/src/commands/pr.ts +105 -58
- package/src/commands/records.ts +1 -1
- package/src/commands/repo.ts +1 -1
- package/src/commands/sandbox.ts +54 -2
- package/src/commands/sessions.ts +1 -1
- package/src/commands/sync.ts +1 -1
- package/src/commands/tasks.ts +2 -2
- package/src/commands/teach.ts +1 -1
- package/src/commands/transcripts.ts +1 -1
- package/src/commands/worktrees.ts +1 -1
- package/src/comments/scan.ts +1 -1
- package/src/comments/trend.ts +1 -1
- package/src/context/citations.ts +1 -1
- package/src/context/classify/extract.ts +2 -2
- package/src/design/project-fonts.ts +392 -0
- package/src/feedback/github.ts +1 -1
- package/src/gate/measures.ts +47 -9
- package/src/gate/sequencer.ts +1 -1
- package/src/gate/stages.ts +9 -0
- package/src/{git-files.ts → git/files.ts} +2 -2
- package/src/{git-ignore.ts → git/ignore.ts} +1 -1
- package/src/{worktree.ts → git/worktree.ts} +1 -1
- package/src/gov/citations.ts +1 -1
- package/src/gov/install.ts +1 -1
- package/src/gov/superseded.ts +1 -1
- package/src/gov/test-order.ts +2 -2
- package/src/help.ts +1 -1
- package/src/hooks/post-merge.ts +1 -1
- package/src/indexes/walk.ts +1 -1
- package/src/labels/audit.ts +1 -1
- package/src/markdown/files.ts +1 -1
- package/src/migrate/apply.ts +1 -1
- package/src/pr/bijection.ts +1 -1
- package/src/pr/evidence.ts +96 -43
- package/src/pr/local.ts +16 -10
- package/src/pr/preview.ts +78 -2
- package/src/records/backup.ts +1 -1
- package/src/records/stale-canonical.ts +1 -1
- package/src/sandbox/headless/dispatch.ts +264 -0
- package/src/sandbox/headless/run.ts +653 -0
- package/src/sandbox/headless/watch.ts +145 -0
- package/src/sandbox/provision.ts +12 -1
- package/src/sandbox/tree.ts +4 -8
- package/src/secrets/scan.ts +1 -1
- package/src/sessions/claim.ts +1 -1
- package/src/sessions/resolve.ts +1 -1
- package/src/sessions/transfer/bundle.ts +1 -1
- package/src/slides/package/fonts.ts +6 -2
- package/src/standards/audit.ts +2 -2
- package/src/sync/check.ts +2 -2
- package/src/sync/engine.ts +2 -2
- package/src/sync/git.ts +1 -1
- package/src/sync/history.ts +1 -1
- package/src/sync/reverse.ts +1 -1
- package/src/sync/target.ts +1 -1
- package/src/sync/workflow.ts +1 -1
- package/src/targets/pulls.ts +2 -2
- package/src/targets/sweep.ts +2 -2
- package/src/tasks/reach.ts +1 -1
- package/src/tasks/trunk.ts +1 -1
- package/src/tooling/inject.ts +1 -1
- package/src/tooling/subfolder.ts +1 -1
- package/src/web/readme-citations.ts +1 -1
- package/src/worktrees/bare-flag.ts +1 -1
- package/src/worktrees/reclaim.ts +2 -2
- package/src/worktrees/remove.ts +1 -1
- package/standards/canvas.md +2 -1
- package/standards/skill.md +1 -0
- package/standards/wiki.md +12 -3
- package/tooling/base/configs/.github/workflows/pr-local-preview.yml +2 -2
- package/scripts/lib/sandbox-dispatch.sh +0 -190
- /package/src/{client-commands.ts → gate/client-commands.ts} +0 -0
- /package/src/{git-base.ts → git/base.ts} +0 -0
- /package/src/{git-env.ts → git/env.ts} +0 -0
- /package/src/{copy.ts → sync/copy.ts} +0 -0
- /package/src/{target.ts → targets/validate.ts} +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversions between the hex the writer takes and the spaces the picker
|
|
3
|
+
* edits in. Channels are 0 to 255, hues are degrees, and every other component
|
|
4
|
+
* is 0 to 100. LCH is CIE LCH as CSS `lch()` defines it, and the matrices are
|
|
5
|
+
* the ones CSS Color 4 publishes in its sample code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface Rgb {
|
|
9
|
+
readonly r: number
|
|
10
|
+
readonly g: number
|
|
11
|
+
readonly b: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Hsv {
|
|
15
|
+
readonly h: number
|
|
16
|
+
readonly s: number
|
|
17
|
+
readonly v: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Hsl {
|
|
21
|
+
readonly h: number
|
|
22
|
+
readonly s: number
|
|
23
|
+
readonly l: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Lch {
|
|
27
|
+
readonly l: number
|
|
28
|
+
readonly c: number
|
|
29
|
+
readonly h: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type Vector = readonly [number, number, number]
|
|
33
|
+
type Matrix = readonly [Vector, Vector, Vector]
|
|
34
|
+
|
|
35
|
+
const LINEAR_SRGB_TO_XYZ: Matrix = [
|
|
36
|
+
[506752 / 1228815, 87881 / 245763, 12673 / 70218],
|
|
37
|
+
[87098 / 409605, 175762 / 245763, 12673 / 175545],
|
|
38
|
+
[7918 / 409605, 87881 / 737289, 1001167 / 1053270],
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const XYZ_TO_LINEAR_SRGB: Matrix = [
|
|
42
|
+
[12831 / 3959, -329 / 214, -1974 / 3959],
|
|
43
|
+
[-851781 / 878810, 1648619 / 878810, 36519 / 878810],
|
|
44
|
+
[705 / 12673, -2585 / 12673, 705 / 667],
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/** Bradford chromatic adaptation, since sRGB is D65 and `lch()` is D50. */
|
|
48
|
+
const D65_TO_D50: Matrix = [
|
|
49
|
+
[1.0479297925449969, 0.022946870601609652, -0.05019226628920524],
|
|
50
|
+
[0.02962780877005599, 0.9904344267538799, -0.017073799063418826],
|
|
51
|
+
[-0.009243040646204504, 0.015055191490298152, 0.7518742814281371],
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const D50_TO_D65: Matrix = [
|
|
55
|
+
[0.955473421488075, -0.02309845494876471, 0.06325924320057072],
|
|
56
|
+
[-0.0283697093338637, 1.0099953980813041, 0.021041441191917323],
|
|
57
|
+
[0.012314014864481998, -0.020507649298898964, 1.330365926242124],
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
const D50_WHITE: Vector = [0.3457 / 0.3585, 1, (1 - 0.3457 - 0.3585) / 0.3585]
|
|
61
|
+
|
|
62
|
+
const EPSILON = 216 / 24389
|
|
63
|
+
const KAPPA = 24389 / 27
|
|
64
|
+
|
|
65
|
+
/** Chroma below this reads as no hue, which rounding noise never reaches. */
|
|
66
|
+
const ACHROMATIC = 0.02
|
|
67
|
+
|
|
68
|
+
function multiply(matrix: Matrix, vector: Vector): Vector {
|
|
69
|
+
const [row0, row1, row2] = matrix.map(
|
|
70
|
+
(row) => row[0] * vector[0] + row[1] * vector[1] + row[2] * vector[2],
|
|
71
|
+
)
|
|
72
|
+
return [row0 ?? 0, row1 ?? 0, row2 ?? 0]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clamp(value: number, low: number, high: number): number {
|
|
76
|
+
return Math.min(high, Math.max(low, value))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function wrapHue(hue: number): number {
|
|
80
|
+
return ((hue % 360) + 360) % 360
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function hexToRgb(hex: string): Rgb {
|
|
84
|
+
return {
|
|
85
|
+
r: parseInt(hex.slice(0, 2), 16),
|
|
86
|
+
g: parseInt(hex.slice(2, 4), 16),
|
|
87
|
+
b: parseInt(hex.slice(4, 6), 16),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function rgbToHex({ r, g, b }: Rgb): string {
|
|
92
|
+
return [r, g, b]
|
|
93
|
+
.map((part) =>
|
|
94
|
+
Math.round(clamp(part, 0, 255))
|
|
95
|
+
.toString(16)
|
|
96
|
+
.padStart(2, '0'),
|
|
97
|
+
)
|
|
98
|
+
.join('')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* HSV off RGB. A color with no saturation has no hue of its own, so it takes
|
|
103
|
+
* the one handed in, which keeps a grey's hue slider where the operator left it.
|
|
104
|
+
*/
|
|
105
|
+
export function rgbToHsv({ r, g, b }: Rgb, hue = 0): Hsv {
|
|
106
|
+
const max = Math.max(r, g, b)
|
|
107
|
+
const delta = max - Math.min(r, g, b)
|
|
108
|
+
return {
|
|
109
|
+
h: delta === 0 ? hue : hueOf(r, g, b, max, delta),
|
|
110
|
+
s: max === 0 ? 0 : (delta / max) * 100,
|
|
111
|
+
v: (max / 255) * 100,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function hsvToRgb({ h, s, v }: Hsv): Rgb {
|
|
116
|
+
const value = (v / 100) * 255
|
|
117
|
+
const chroma = value * (s / 100)
|
|
118
|
+
return fromChroma(h, chroma, value - chroma)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** HSL off RGB, taking the hue handed in when there is no saturation. */
|
|
122
|
+
export function rgbToHsl({ r, g, b }: Rgb, hue = 0): Hsl {
|
|
123
|
+
const max = Math.max(r, g, b)
|
|
124
|
+
const min = Math.min(r, g, b)
|
|
125
|
+
const delta = max - min
|
|
126
|
+
const lightness = (max + min) / 2 / 255
|
|
127
|
+
const saturation =
|
|
128
|
+
delta === 0 ? 0 : delta / 255 / (1 - Math.abs(2 * lightness - 1))
|
|
129
|
+
return {
|
|
130
|
+
h: delta === 0 ? hue : hueOf(r, g, b, max, delta),
|
|
131
|
+
s: saturation * 100,
|
|
132
|
+
l: lightness * 100,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function hslToRgb({ h, s, l }: Hsl): Rgb {
|
|
137
|
+
const lightness = l / 100
|
|
138
|
+
const chroma = (1 - Math.abs(2 * lightness - 1)) * (s / 100) * 255
|
|
139
|
+
return fromChroma(h, chroma, lightness * 255 - chroma / 2)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function hueOf(
|
|
143
|
+
r: number,
|
|
144
|
+
g: number,
|
|
145
|
+
b: number,
|
|
146
|
+
max: number,
|
|
147
|
+
delta: number,
|
|
148
|
+
): number {
|
|
149
|
+
const sector =
|
|
150
|
+
max === r
|
|
151
|
+
? (g - b) / delta + (g < b ? 6 : 0)
|
|
152
|
+
: max === g
|
|
153
|
+
? (b - r) / delta + 2
|
|
154
|
+
: (r - g) / delta + 4
|
|
155
|
+
return sector * 60
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function fromChroma(hue: number, chroma: number, base: number): Rgb {
|
|
159
|
+
const sector = wrapHue(hue) / 60
|
|
160
|
+
const second = chroma * (1 - Math.abs((sector % 2) - 1))
|
|
161
|
+
const [r, g, b]: Vector =
|
|
162
|
+
sector < 1
|
|
163
|
+
? [chroma, second, 0]
|
|
164
|
+
: sector < 2
|
|
165
|
+
? [second, chroma, 0]
|
|
166
|
+
: sector < 3
|
|
167
|
+
? [0, chroma, second]
|
|
168
|
+
: sector < 4
|
|
169
|
+
? [0, second, chroma]
|
|
170
|
+
: sector < 5
|
|
171
|
+
? [second, 0, chroma]
|
|
172
|
+
: [chroma, 0, second]
|
|
173
|
+
return { r: r + base, g: g + base, b: b + base }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function toLinear(channel: number): number {
|
|
177
|
+
const value = channel / 255
|
|
178
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function fromLinear(value: number): number {
|
|
182
|
+
const encoded =
|
|
183
|
+
value <= 0.0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - 0.055
|
|
184
|
+
return encoded * 255
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** CIE LCH off RGB, taking the hue handed in when there is no chroma. */
|
|
188
|
+
export function rgbToLch({ r, g, b }: Rgb, hue = 0): Lch {
|
|
189
|
+
const xyz = multiply(
|
|
190
|
+
D65_TO_D50,
|
|
191
|
+
multiply(LINEAR_SRGB_TO_XYZ, [toLinear(r), toLinear(g), toLinear(b)]),
|
|
192
|
+
)
|
|
193
|
+
const f = (ratio: number): number =>
|
|
194
|
+
ratio > EPSILON ? Math.cbrt(ratio) : (KAPPA * ratio + 16) / 116
|
|
195
|
+
const fx = f(xyz[0] / D50_WHITE[0])
|
|
196
|
+
const fy = f(xyz[1] / D50_WHITE[1])
|
|
197
|
+
const fz = f(xyz[2] / D50_WHITE[2])
|
|
198
|
+
const a = 500 * (fx - fy)
|
|
199
|
+
const bAxis = 200 * (fy - fz)
|
|
200
|
+
const chroma = Math.sqrt(a * a + bAxis * bAxis)
|
|
201
|
+
return {
|
|
202
|
+
l: 116 * fy - 16,
|
|
203
|
+
c: chroma < ACHROMATIC ? 0 : chroma,
|
|
204
|
+
h:
|
|
205
|
+
chroma < ACHROMATIC
|
|
206
|
+
? hue
|
|
207
|
+
: wrapHue((Math.atan2(bAxis, a) * 180) / Math.PI),
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* RGB off CIE LCH. A color outside sRGB has no faithful hex, so each channel
|
|
213
|
+
* clamps and `isClamped` says so, letting the row show the color written.
|
|
214
|
+
*/
|
|
215
|
+
export function lchToRgb({ l, c, h }: Lch): {
|
|
216
|
+
readonly rgb: Rgb
|
|
217
|
+
readonly isClamped: boolean
|
|
218
|
+
} {
|
|
219
|
+
const radians = (h * Math.PI) / 180
|
|
220
|
+
const fy = (l + 16) / 116
|
|
221
|
+
const fx = fy + (c * Math.cos(radians)) / 500
|
|
222
|
+
const fz = fy - (c * Math.sin(radians)) / 200
|
|
223
|
+
const ratios: Vector = [
|
|
224
|
+
fx ** 3 > EPSILON ? fx ** 3 : (116 * fx - 16) / KAPPA,
|
|
225
|
+
l > KAPPA * EPSILON ? fy ** 3 : l / KAPPA,
|
|
226
|
+
fz ** 3 > EPSILON ? fz ** 3 : (116 * fz - 16) / KAPPA,
|
|
227
|
+
]
|
|
228
|
+
const xyz: Vector = [
|
|
229
|
+
ratios[0] * D50_WHITE[0],
|
|
230
|
+
ratios[1] * D50_WHITE[1],
|
|
231
|
+
ratios[2] * D50_WHITE[2],
|
|
232
|
+
]
|
|
233
|
+
const channels = multiply(XYZ_TO_LINEAR_SRGB, multiply(D50_TO_D65, xyz)).map(
|
|
234
|
+
fromLinear,
|
|
235
|
+
)
|
|
236
|
+
/* Half a step either side rounds into range, so it is not a clamp. */
|
|
237
|
+
const isClamped = channels.some((part) => part < -0.5 || part > 255.5)
|
|
238
|
+
const [r, g, b] = channels.map((part) => clamp(part, 0, 255))
|
|
239
|
+
return { rgb: { r: r ?? 0, g: g ?? 0, b: b ?? 0 }, isClamped }
|
|
240
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource preact */
|
|
2
|
-
import type { JSX } from 'preact'
|
|
2
|
+
import type { ComponentChildren, JSX } from 'preact'
|
|
3
3
|
import { useRef } from 'preact/hooks'
|
|
4
4
|
import { scrubStep } from '@/canvas/client/inspector/values'
|
|
5
5
|
|
|
@@ -35,6 +35,8 @@ interface FieldProps {
|
|
|
35
35
|
readonly onCommit: (value: string) => void
|
|
36
36
|
/** Makes the glyph a drag handle. Typing stays the keyboard path. */
|
|
37
37
|
readonly scrub?: Scrub
|
|
38
|
+
/** A control inside the field's box after the value, such as a mode menu. */
|
|
39
|
+
readonly trailing?: ComponentChildren
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const NUMBER = /^-?\d*\.?\d+$/
|
|
@@ -69,6 +71,7 @@ export function Field({
|
|
|
69
71
|
placeholder,
|
|
70
72
|
onCommit,
|
|
71
73
|
scrub,
|
|
74
|
+
trailing,
|
|
72
75
|
}: FieldProps): JSX.Element {
|
|
73
76
|
const input = useRef<HTMLInputElement>(null)
|
|
74
77
|
const handle = useRef<HTMLSpanElement>(null)
|
|
@@ -169,6 +172,7 @@ export function Field({
|
|
|
169
172
|
if (event.key === 'Escape') event.currentTarget.value = initial
|
|
170
173
|
}}
|
|
171
174
|
/>
|
|
175
|
+
{trailing}
|
|
172
176
|
</div>
|
|
173
177
|
)
|
|
174
178
|
}
|
|
@@ -128,10 +128,164 @@
|
|
|
128
128
|
font-weight: 400;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/* An icon in a section's header lane or beside a field, square at row height. */
|
|
132
|
+
.section-icon {
|
|
133
|
+
flex: none;
|
|
134
|
+
display: grid;
|
|
135
|
+
place-items: center;
|
|
136
|
+
width: 24px;
|
|
137
|
+
height: 24px;
|
|
138
|
+
padding: 0;
|
|
139
|
+
border: 0;
|
|
140
|
+
border-radius: var(--radius-action);
|
|
141
|
+
background: transparent;
|
|
142
|
+
color: var(--color-muted);
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.section-icon:hover,
|
|
147
|
+
.section-icon[aria-pressed='true'] {
|
|
148
|
+
background: var(--color-surface);
|
|
149
|
+
color: var(--color-text);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.section-icon:disabled {
|
|
153
|
+
opacity: 0.5;
|
|
154
|
+
cursor: not-allowed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.section-icon svg {
|
|
158
|
+
width: 14px;
|
|
159
|
+
height: 14px;
|
|
160
|
+
fill: none;
|
|
161
|
+
stroke: currentColor;
|
|
162
|
+
stroke-width: 1.5;
|
|
163
|
+
stroke-linecap: round;
|
|
164
|
+
stroke-linejoin: round;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* The alignment grid on the left, the direction and wrap stacked beside it. */
|
|
168
|
+
.flex-layout {
|
|
169
|
+
display: grid;
|
|
170
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
171
|
+
gap: var(--space-xs);
|
|
172
|
+
align-items: start;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.flex-layout-side {
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
gap: var(--space-xs);
|
|
179
|
+
min-width: 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.flex-layout-side .glyph-field {
|
|
183
|
+
flex: 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.align-grid {
|
|
187
|
+
display: grid;
|
|
188
|
+
grid-template-columns: repeat(3, 1fr);
|
|
189
|
+
grid-template-rows: repeat(3, 24px);
|
|
190
|
+
padding: 2px;
|
|
191
|
+
border: 1px solid var(--color-border);
|
|
192
|
+
border-radius: var(--radius-action);
|
|
193
|
+
background: var(--color-surface);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.align-cell {
|
|
197
|
+
display: grid;
|
|
198
|
+
place-items: center;
|
|
199
|
+
min-width: 0;
|
|
200
|
+
padding: 0;
|
|
201
|
+
border: 0;
|
|
202
|
+
border-radius: var(--radius-action);
|
|
203
|
+
background: transparent;
|
|
204
|
+
color: var(--color-muted);
|
|
205
|
+
cursor: pointer;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.align-cell:hover {
|
|
209
|
+
color: var(--color-text);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.align-cell:disabled {
|
|
213
|
+
opacity: 0.5;
|
|
214
|
+
cursor: not-allowed;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.align-dot {
|
|
218
|
+
width: 3px;
|
|
219
|
+
height: 3px;
|
|
220
|
+
border-radius: 50%;
|
|
221
|
+
background: currentColor;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.align-cell[aria-checked='true'] {
|
|
225
|
+
color: var(--color-accent);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.align-cell[aria-checked='true'] .align-dot {
|
|
229
|
+
width: 10px;
|
|
230
|
+
height: 10px;
|
|
231
|
+
border-radius: 2px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/*
|
|
235
|
+
* A size's mode menu trails its number. The select is transparent over the
|
|
236
|
+
* chevron, so the platform's own list opens and the field keeps one height.
|
|
237
|
+
*/
|
|
238
|
+
.size-mode {
|
|
239
|
+
position: relative;
|
|
240
|
+
flex: none;
|
|
241
|
+
display: grid;
|
|
242
|
+
place-items: center;
|
|
243
|
+
width: 24px;
|
|
244
|
+
height: 22px;
|
|
245
|
+
border-radius: var(--radius-action);
|
|
246
|
+
color: var(--color-muted);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.size-mode:hover,
|
|
250
|
+
.size-mode:focus-within {
|
|
251
|
+
color: var(--color-text);
|
|
252
|
+
background: var(--color-chrome);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.size-mode-select {
|
|
256
|
+
position: absolute;
|
|
257
|
+
inset: 0;
|
|
258
|
+
width: 100%;
|
|
259
|
+
height: 100%;
|
|
260
|
+
opacity: 0;
|
|
261
|
+
cursor: pointer;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.size-mode-select:disabled {
|
|
265
|
+
cursor: not-allowed;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.size-mode:has(.size-mode-select:disabled) {
|
|
269
|
+
opacity: 0.5;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* The select is invisible, so the project ring draws on the lane holding it. */
|
|
273
|
+
.size-mode:has(.size-mode-select:focus-visible) {
|
|
274
|
+
outline: 2px solid var(--color-accent);
|
|
275
|
+
outline-offset: 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.size-mode-chevron {
|
|
279
|
+
width: 12px;
|
|
280
|
+
height: 12px;
|
|
281
|
+
fill: none;
|
|
282
|
+
stroke: currentColor;
|
|
283
|
+
stroke-width: 1.5;
|
|
284
|
+
}
|
|
285
|
+
|
|
131
286
|
/*
|
|
132
287
|
* A color is one field: a swatch leading in the glyph's lane, the hex, and the
|
|
133
|
-
* opacity right-aligned
|
|
134
|
-
* reach past the panel's edge or scroll it sideways.
|
|
288
|
+
* opacity right-aligned, with the tokens and the eyedropper trailing the row.
|
|
135
289
|
*/
|
|
136
290
|
.color-field {
|
|
137
291
|
min-width: 0;
|
|
@@ -246,15 +400,218 @@
|
|
|
246
400
|
user-select: none;
|
|
247
401
|
}
|
|
248
402
|
|
|
249
|
-
|
|
403
|
+
/*
|
|
404
|
+
* The picker and the token list float beside the panel, in the top layer
|
|
405
|
+
* where the browser has one. The panel scrolls and clips, so neither sits in
|
|
406
|
+
* its flow. The script places each at fixed coordinates inside the window, and
|
|
407
|
+
* the caps below hold a window too small for the whole picker.
|
|
408
|
+
*/
|
|
409
|
+
.color-popover {
|
|
410
|
+
position: fixed;
|
|
411
|
+
inset: auto;
|
|
412
|
+
z-index: 10;
|
|
413
|
+
max-width: calc(100vw - 16px);
|
|
414
|
+
max-height: calc(100vh - 16px);
|
|
415
|
+
margin: 0;
|
|
416
|
+
padding: var(--space-sm);
|
|
417
|
+
overflow: auto;
|
|
418
|
+
border: 1px solid var(--color-border);
|
|
419
|
+
border-radius: var(--radius-panel);
|
|
420
|
+
background: var(--color-chrome);
|
|
421
|
+
color: var(--color-text);
|
|
422
|
+
box-shadow: 0 8px 24px rgb(0 0 0 / 0.35);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.color-popover.is-tokens {
|
|
426
|
+
width: 14rem;
|
|
427
|
+
padding: var(--space-xs);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.picker {
|
|
431
|
+
--picker-hue: red;
|
|
432
|
+
--picker-solid: transparent;
|
|
433
|
+
display: grid;
|
|
434
|
+
grid-template-columns: 14rem 14px 14px 13rem;
|
|
435
|
+
gap: var(--space-sm);
|
|
436
|
+
align-items: stretch;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/*
|
|
440
|
+
* The area draws white to the hue left to right, then black over it bottom
|
|
441
|
+
* to top, so a point reads as saturation across and brightness up.
|
|
442
|
+
*/
|
|
443
|
+
.picker-area {
|
|
444
|
+
position: relative;
|
|
445
|
+
aspect-ratio: 1;
|
|
446
|
+
border-radius: var(--radius-action);
|
|
447
|
+
background:
|
|
448
|
+
linear-gradient(to top, #000, transparent),
|
|
449
|
+
linear-gradient(to right, #fff, var(--picker-hue));
|
|
450
|
+
cursor: crosshair;
|
|
451
|
+
touch-action: none;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.picker-thumb {
|
|
455
|
+
position: absolute;
|
|
456
|
+
width: 12px;
|
|
457
|
+
height: 12px;
|
|
458
|
+
border: 2px solid #fff;
|
|
459
|
+
border-radius: 50%;
|
|
460
|
+
box-shadow: 0 0 0 1px rgb(0 0 0 / 0.5);
|
|
461
|
+
transform: translate(-50%, -50%);
|
|
462
|
+
pointer-events: none;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.picker-slider {
|
|
466
|
+
appearance: none;
|
|
467
|
+
width: 14px;
|
|
468
|
+
height: 100%;
|
|
469
|
+
margin: 0;
|
|
470
|
+
border-radius: var(--radius-action);
|
|
471
|
+
writing-mode: vertical-lr;
|
|
472
|
+
cursor: pointer;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* Hue runs red at the top through the wheel and back to red at the foot. */
|
|
476
|
+
.picker-hue {
|
|
477
|
+
background: linear-gradient(
|
|
478
|
+
to bottom,
|
|
479
|
+
#f00,
|
|
480
|
+
#ff0 17%,
|
|
481
|
+
#0f0 33%,
|
|
482
|
+
#0ff 50%,
|
|
483
|
+
#00f 67%,
|
|
484
|
+
#f0f 83%,
|
|
485
|
+
#f00
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Alpha runs opaque at the top to clear at the foot, over a checker. */
|
|
490
|
+
.picker-alpha {
|
|
491
|
+
direction: rtl;
|
|
492
|
+
background-image:
|
|
493
|
+
linear-gradient(to bottom, var(--picker-solid), transparent),
|
|
494
|
+
linear-gradient(45deg, var(--color-border) 25%, transparent 25%),
|
|
495
|
+
linear-gradient(-45deg, var(--color-border) 25%, transparent 25%),
|
|
496
|
+
linear-gradient(45deg, transparent 75%, var(--color-border) 75%),
|
|
497
|
+
linear-gradient(-45deg, transparent 75%, var(--color-border) 75%);
|
|
498
|
+
background-size:
|
|
499
|
+
auto,
|
|
500
|
+
8px 8px,
|
|
501
|
+
8px 8px,
|
|
502
|
+
8px 8px,
|
|
503
|
+
8px 8px;
|
|
504
|
+
background-position:
|
|
505
|
+
0 0,
|
|
506
|
+
0 0,
|
|
507
|
+
0 4px,
|
|
508
|
+
4px -4px,
|
|
509
|
+
-4px 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.picker-slider::-webkit-slider-thumb {
|
|
513
|
+
appearance: none;
|
|
514
|
+
width: 14px;
|
|
515
|
+
height: 6px;
|
|
516
|
+
border: 2px solid #fff;
|
|
517
|
+
border-radius: 2px;
|
|
518
|
+
box-shadow: 0 0 0 1px rgb(0 0 0 / 0.5);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.picker-slider::-moz-range-thumb {
|
|
522
|
+
width: 10px;
|
|
523
|
+
height: 2px;
|
|
524
|
+
border: 2px solid #fff;
|
|
525
|
+
border-radius: 2px;
|
|
526
|
+
background: transparent;
|
|
527
|
+
box-shadow: 0 0 0 1px rgb(0 0 0 / 0.5);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.picker-side {
|
|
250
531
|
display: flex;
|
|
251
532
|
flex-direction: column;
|
|
252
533
|
gap: var(--space-xs);
|
|
253
|
-
|
|
254
|
-
|
|
534
|
+
min-width: 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.picker-compare {
|
|
538
|
+
display: grid;
|
|
539
|
+
grid-template-columns: 1fr 1fr;
|
|
540
|
+
height: 3.5rem;
|
|
541
|
+
overflow: hidden;
|
|
255
542
|
border: 1px solid var(--color-border);
|
|
256
543
|
border-radius: var(--radius-action);
|
|
257
|
-
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/* Each half paints over the checker, so an alpha below full reads as one. */
|
|
547
|
+
.picker-previous,
|
|
548
|
+
.picker-new {
|
|
549
|
+
--paint: transparent;
|
|
550
|
+
padding: 0;
|
|
551
|
+
border: 0;
|
|
552
|
+
background-image:
|
|
553
|
+
linear-gradient(var(--paint), var(--paint)),
|
|
554
|
+
linear-gradient(45deg, var(--color-border) 25%, transparent 25%),
|
|
555
|
+
linear-gradient(-45deg, var(--color-border) 25%, transparent 25%),
|
|
556
|
+
linear-gradient(45deg, transparent 75%, var(--color-border) 75%),
|
|
557
|
+
linear-gradient(-45deg, transparent 75%, var(--color-border) 75%);
|
|
558
|
+
background-size:
|
|
559
|
+
auto,
|
|
560
|
+
8px 8px,
|
|
561
|
+
8px 8px,
|
|
562
|
+
8px 8px,
|
|
563
|
+
8px 8px;
|
|
564
|
+
background-position:
|
|
565
|
+
0 0,
|
|
566
|
+
0 0,
|
|
567
|
+
0 4px,
|
|
568
|
+
4px -4px,
|
|
569
|
+
-4px 0;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.picker-previous {
|
|
573
|
+
cursor: pointer;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.picker-compare-labels {
|
|
577
|
+
display: grid;
|
|
578
|
+
grid-template-columns: 1fr 1fr;
|
|
579
|
+
color: var(--color-text-secondary);
|
|
580
|
+
text-align: center;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.picker-row {
|
|
584
|
+
display: grid;
|
|
585
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
586
|
+
gap: 1px;
|
|
587
|
+
overflow: hidden;
|
|
588
|
+
border-radius: var(--radius-action);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.picker-channel {
|
|
592
|
+
display: flex;
|
|
593
|
+
flex-direction: column;
|
|
594
|
+
align-items: stretch;
|
|
595
|
+
min-width: 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.picker-number {
|
|
599
|
+
min-width: 0;
|
|
600
|
+
min-height: 24px;
|
|
601
|
+
padding: 2px var(--space-xs);
|
|
602
|
+
border: 0;
|
|
603
|
+
background: var(--color-surface);
|
|
604
|
+
color: var(--color-text);
|
|
605
|
+
font: inherit;
|
|
606
|
+
font-variant-numeric: tabular-nums;
|
|
607
|
+
text-align: center;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.picker-letter {
|
|
611
|
+
color: var(--color-muted);
|
|
612
|
+
font-size: 0.75em;
|
|
613
|
+
text-align: center;
|
|
614
|
+
user-select: none;
|
|
258
615
|
}
|
|
259
616
|
|
|
260
617
|
.token-list {
|
|
@@ -290,3 +647,35 @@
|
|
|
290
647
|
text-overflow: ellipsis;
|
|
291
648
|
white-space: nowrap;
|
|
292
649
|
}
|
|
650
|
+
|
|
651
|
+
/* A trailing icon on the color row, square at the row's height. */
|
|
652
|
+
.color-icon {
|
|
653
|
+
flex: none;
|
|
654
|
+
display: grid;
|
|
655
|
+
place-items: center;
|
|
656
|
+
width: 24px;
|
|
657
|
+
height: 24px;
|
|
658
|
+
padding: 0;
|
|
659
|
+
border: 0;
|
|
660
|
+
border-radius: var(--radius-action);
|
|
661
|
+
background: transparent;
|
|
662
|
+
color: var(--color-muted);
|
|
663
|
+
cursor: pointer;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.color-icon:hover,
|
|
667
|
+
.color-icon[aria-expanded='true'] {
|
|
668
|
+
background: var(--color-surface);
|
|
669
|
+
color: var(--color-text);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.color-icon:disabled {
|
|
673
|
+
opacity: 0.5;
|
|
674
|
+
cursor: not-allowed;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.color-icon svg {
|
|
678
|
+
width: 14px;
|
|
679
|
+
height: 14px;
|
|
680
|
+
fill: currentColor;
|
|
681
|
+
}
|