@erclx/canon 5.52.0 → 5.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +11 -13
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +5 -8
  5. package/claude/skills/context-fold/REQUIREMENT.md +0 -1
  6. package/claude/skills/design-identity/REQUIREMENT.md +2 -2
  7. package/claude/skills/design-identity/SKILL.md +6 -6
  8. package/claude/skills/design-record/REQUIREMENT.md +2 -2
  9. package/claude/skills/design-record/SKILL.md +4 -4
  10. package/claude/skills/design-record/references/folder.md +1 -1
  11. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  12. package/claude/skills/design-taste/references/kinds.md +0 -1
  13. package/claude/skills/draw-figure/REQUIREMENT.md +3 -4
  14. package/claude/skills/draw-figure/SKILL.md +5 -6
  15. package/claude/skills/draw-figure/references/figures.md +0 -1
  16. package/claude/skills/draw-figure/references/hand-drawn.md +1 -1
  17. package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -5
  18. package/claude/skills/pick-by-eye/SKILL.md +34 -41
  19. package/claude/skills/pick-by-eye/references/board.md +42 -0
  20. package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
  21. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  22. package/claude/skills/teach-craft/REQUIREMENT.md +30 -0
  23. package/claude/skills/teach-craft/SKILL.md +70 -0
  24. package/claude/skills/teach-craft/references/adopted.md +113 -0
  25. package/claude/skills/teach-craft/references/lesson-craft.md +60 -0
  26. package/claude/skills/{teach-workspace → teach-craft}/references/pedagogy.md +41 -6
  27. package/docs/agents/audits.md +0 -1
  28. package/docs/agents/install-and-sync.md +0 -8
  29. package/docs/target/migrations.md +16 -2
  30. package/docs/target/projects.md +1 -1
  31. package/docs/workflow/parallel-features.md +1 -1
  32. package/docs/workflow/skill-map-on-request.md +10 -22
  33. package/docs/workflow/skill-map.md +11 -14
  34. package/docs/workflow/visual-design-workflow.md +1 -4
  35. package/governance/detect.toml +1 -6
  36. package/governance/renames.toml +0 -1
  37. package/package.json +2 -8
  38. package/scripts/core/regen-web-previews.ts +2 -67
  39. package/src/audits/catalog.ts +0 -1
  40. package/src/browser/engine.ts +3 -6
  41. package/src/capture/render.ts +1 -1
  42. package/src/claude/cases/authoring.ts +0 -12
  43. package/src/claude/cases/misc.ts +0 -22
  44. package/src/claude/cases/workflow.ts +2 -38
  45. package/src/claude/skills-audit.ts +1 -1
  46. package/src/cli/help.ts +0 -10
  47. package/src/cli.ts +0 -8
  48. package/src/commands/design.ts +16 -315
  49. package/src/commands/records.ts +0 -1
  50. package/src/commands/serve.ts +2 -2
  51. package/src/commands/sync.ts +4 -5
  52. package/src/design/components.ts +8 -1514
  53. package/src/design/css.ts +5 -6
  54. package/src/design/fonts.ts +3 -5
  55. package/src/design/regen.ts +5 -32
  56. package/src/design/render.ts +1 -1
  57. package/src/design/tokens.ts +3 -2
  58. package/src/driver/drive.ts +3 -4
  59. package/src/driver/steps.ts +2 -5
  60. package/src/gate/stages.ts +4 -10
  61. package/src/gov/audit.ts +1 -6
  62. package/src/inventory/walk.ts +1 -1
  63. package/src/markdown/scope.ts +6 -35
  64. package/src/records/validate.ts +4 -176
  65. package/src/sandbox/equivalence.ts +0 -5
  66. package/src/shipped/references.ts +1 -2
  67. package/src/sync/check.ts +4 -29
  68. package/src/sync/stamp.ts +6 -6
  69. package/src/sync/target.ts +1 -4
  70. package/tooling/claude/reference.md +1 -1
  71. package/tooling/claude/seeds/canon/DESIGN.md +1 -1
  72. package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
  73. package/claude/skills/canvas-board/SKILL.md +0 -143
  74. package/claude/skills/canvas-board/references/canvas.md +0 -170
  75. package/claude/skills/draft-and-pick/REQUIREMENT.md +0 -26
  76. package/claude/skills/draft-and-pick/SKILL.md +0 -11
  77. package/claude/skills/draft-doc/REQUIREMENT.md +0 -26
  78. package/claude/skills/draft-doc/SKILL.md +0 -11
  79. package/claude/skills/draft-figure/REQUIREMENT.md +0 -26
  80. package/claude/skills/draft-figure/SKILL.md +0 -11
  81. package/claude/skills/draft-identity/REQUIREMENT.md +0 -26
  82. package/claude/skills/draft-identity/SKILL.md +0 -11
  83. package/claude/skills/draft-ready/REQUIREMENT.md +0 -26
  84. package/claude/skills/draft-ready/SKILL.md +0 -11
  85. package/claude/skills/draft-screencast/REQUIREMENT.md +0 -26
  86. package/claude/skills/draft-screencast/SKILL.md +0 -11
  87. package/claude/skills/draft-slides/REQUIREMENT.md +0 -26
  88. package/claude/skills/draft-slides/SKILL.md +0 -11
  89. package/claude/skills/make-deck/REQUIREMENT.md +0 -36
  90. package/claude/skills/make-deck/SKILL.md +0 -80
  91. package/claude/skills/make-deck/references/deck.md +0 -183
  92. package/claude/skills/read-frames/REQUIREMENT.md +0 -30
  93. package/claude/skills/read-frames/SKILL.md +0 -55
  94. package/claude/skills/record-screencast/REQUIREMENT.md +0 -35
  95. package/claude/skills/record-screencast/SKILL.md +0 -99
  96. package/claude/skills/script-screencast/REQUIREMENT.md +0 -34
  97. package/claude/skills/script-screencast/SKILL.md +0 -70
  98. package/claude/skills/script-screencast/references/beats-template.md +0 -119
  99. package/claude/skills/teach-workspace/REQUIREMENT.md +0 -34
  100. package/claude/skills/teach-workspace/SKILL.md +0 -196
  101. package/claude/skills/teach-workspace/references/glossary.md +0 -90
  102. package/claude/skills/teach-workspace/references/lesson-build.md +0 -106
  103. package/claude/skills/teach-workspace/references/lesson-craft.md +0 -135
  104. package/claude/skills/teach-workspace/references/promotion-fold.md +0 -35
  105. package/claude/skills/teach-workspace/references/promotion.md +0 -116
  106. package/claude/skills/teach-workspace/references/revisit.md +0 -28
  107. package/claude/skills/teach-workspace/references/teach.md +0 -211
  108. package/claude/skills/video-craft/REQUIREMENT.md +0 -35
  109. package/claude/skills/video-craft/SKILL.md +0 -96
  110. package/claude/skills/video-craft/references/adopted.md +0 -110
  111. package/governance/rules/ui/canvas.md +0 -13
  112. package/src/canvas/address.ts +0 -227
  113. package/src/canvas/arrange.ts +0 -66
  114. package/src/canvas/capture.ts +0 -391
  115. package/src/canvas/client/app.tsx +0 -188
  116. package/src/canvas/client/inspector/align-grid.tsx +0 -162
  117. package/src/canvas/client/inspector/color-field.tsx +0 -398
  118. package/src/canvas/client/inspector/color-inputs.tsx +0 -139
  119. package/src/canvas/client/inspector/color-picker.tsx +0 -668
  120. package/src/canvas/client/inspector/color-space.ts +0 -240
  121. package/src/canvas/client/inspector/field.tsx +0 -263
  122. package/src/canvas/client/inspector/fields.css +0 -763
  123. package/src/canvas/client/inspector/section.tsx +0 -27
  124. package/src/canvas/client/inspector/size-field.tsx +0 -69
  125. package/src/canvas/client/inspector/token-select.tsx +0 -109
  126. package/src/canvas/client/inspector/values.ts +0 -297
  127. package/src/canvas/client/inspector.tsx +0 -692
  128. package/src/canvas/client/keys.ts +0 -71
  129. package/src/canvas/client/layers.tsx +0 -118
  130. package/src/canvas/client/main.tsx +0 -71
  131. package/src/canvas/client/pages-panel.tsx +0 -212
  132. package/src/canvas/client/panel-handle.tsx +0 -90
  133. package/src/canvas/client/selection.tsx +0 -199
  134. package/src/canvas/client/shell.css +0 -762
  135. package/src/canvas/client/state.ts +0 -792
  136. package/src/canvas/client/surface.tsx +0 -803
  137. package/src/canvas/client/theme-panel.tsx +0 -55
  138. package/src/canvas/client/tools.tsx +0 -153
  139. package/src/canvas/content.ts +0 -781
  140. package/src/canvas/edit.ts +0 -575
  141. package/src/canvas/editing.ts +0 -203
  142. package/src/canvas/fit.ts +0 -238
  143. package/src/canvas/global.d.ts +0 -15
  144. package/src/canvas/history.ts +0 -116
  145. package/src/canvas/index.html +0 -20
  146. package/src/canvas/server.ts +0 -766
  147. package/src/canvas/tokens.ts +0 -221
  148. package/src/commands/canvas.ts +0 -1097
  149. package/src/commands/demo.ts +0 -542
  150. package/src/commands/slides.ts +0 -184
  151. package/src/commands/teach.ts +0 -1401
  152. package/src/demo/beats.ts +0 -135
  153. package/src/demo/compile.ts +0 -350
  154. package/src/demo/container.ts +0 -219
  155. package/src/demo/cursors.ts +0 -55
  156. package/src/demo/drive.ts +0 -442
  157. package/src/demo/pointer.ts +0 -178
  158. package/src/demo/theme.ts +0 -112
  159. package/src/demo/timeline.ts +0 -64
  160. package/src/design/adapter.ts +0 -59
  161. package/src/design/base.css +0 -203
  162. package/src/design/brand-favicon.ts +0 -18
  163. package/src/design/neutral.ts +0 -127
  164. package/src/design/project-fonts.ts +0 -392
  165. package/src/slides/convert/chart.ts +0 -165
  166. package/src/slides/convert/deck.ts +0 -244
  167. package/src/slides/convert/export.ts +0 -567
  168. package/src/slides/convert/master.ts +0 -198
  169. package/src/slides/convert/shapes.ts +0 -804
  170. package/src/slides/convert/svg.ts +0 -38
  171. package/src/slides/convert/walk.ts +0 -671
  172. package/src/slides/decks.ts +0 -110
  173. package/src/slides/open.ts +0 -18
  174. package/src/slides/package/fonts.ts +0 -301
  175. package/src/slides/package/motion.ts +0 -316
  176. package/src/slides/package/test-face.ts +0 -113
  177. package/src/teach/browser/compile.ts +0 -44
  178. package/src/teach/browser/dismiss.ts +0 -76
  179. package/src/teach/browser/focus-line.ts +0 -19
  180. package/src/teach/browser/glossary-filter.ts +0 -44
  181. package/src/teach/browser/head.ts +0 -30
  182. package/src/teach/browser/quiz.ts +0 -19
  183. package/src/teach/browser/reload.ts +0 -9
  184. package/src/teach/browser/sidebar.ts +0 -233
  185. package/src/teach/browser/theme.ts +0 -18
  186. package/src/teach/components/code.tsx +0 -24
  187. package/src/teach/components/heading.tsx +0 -11
  188. package/src/teach/components/list.tsx +0 -17
  189. package/src/teach/components/paragraph.tsx +0 -29
  190. package/src/teach/components/refs.tsx +0 -28
  191. package/src/teach/cross-references.ts +0 -78
  192. package/src/teach/fonts.ts +0 -16
  193. package/src/teach/highlight.ts +0 -71
  194. package/src/teach/html/jsx-dev-runtime.ts +0 -13
  195. package/src/teach/html/jsx-runtime.ts +0 -113
  196. package/src/teach/html/raw.ts +0 -28
  197. package/src/teach/lesson.ts +0 -178
  198. package/src/teach/nav.ts +0 -1337
  199. package/src/teach/render.ts +0 -240
  200. package/src/teach/workspace.ts +0 -1164
  201. package/src/teach/write-if-changed.ts +0 -17
@@ -1,240 +0,0 @@
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,263 +0,0 @@
1
- /** @jsxImportSource preact */
2
- import type { ComponentChildren, 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
- /** Scales each pixel's move, for a value whose useful range is small. */
25
- readonly step?: number
26
- }
27
-
28
- interface FieldProps {
29
- /** The full word, which names the input for assistive technology. */
30
- readonly label: string
31
- /** The short mark drawn inside the field, hidden from assistive technology. */
32
- readonly glyph: string
33
- readonly initial: string
34
- readonly isBusy: boolean
35
- readonly isWide?: boolean
36
- readonly placeholder?: string
37
- readonly onCommit: (value: string) => void
38
- /** Makes the glyph a drag handle. Typing stays the keyboard path. */
39
- readonly scrub?: Scrub
40
- /** A control inside the field's box after the value, such as a mode menu. */
41
- readonly trailing?: ComponentChildren
42
- }
43
-
44
- const NUMBER = /^-?\d*\.?\d+$/
45
-
46
- interface Drag {
47
- readonly pointerId: number
48
- /** What the input held at press, which a cancel puts back. */
49
- readonly typed: string
50
- /** The number the drag started from, an empty field's placeholder included. */
51
- readonly start: string
52
- readonly session: ScrubSession
53
- lastX: number
54
- value: number
55
- }
56
-
57
- function fieldClass(isWide: boolean | undefined, modifier?: string): string {
58
- return ['glyph-field', isWide ? 'is-wide' : '', modifier ?? '']
59
- .filter(Boolean)
60
- .join(' ')
61
- }
62
-
63
- /**
64
- * Commits on change, which a text input fires on Enter or on leaving it, and
65
- * sends nothing when the value is what it started at. Escape puts it back.
66
- */
67
- export function Field({
68
- label,
69
- glyph,
70
- initial,
71
- isBusy,
72
- isWide,
73
- placeholder,
74
- onCommit,
75
- scrub,
76
- trailing,
77
- }: FieldProps): JSX.Element {
78
- const input = useRef<HTMLInputElement>(null)
79
- const handle = useRef<HTMLSpanElement>(null)
80
- const drag = useRef<Drag | null>(null)
81
-
82
- const end = (pointerId: number): Drag | undefined => {
83
- const held = drag.current
84
- if (!held || held.pointerId !== pointerId) return undefined
85
- drag.current = null
86
- return held
87
- }
88
-
89
- /* An empty field scrubs from the number its placeholder shows. */
90
- const scrubFrom = (value: string): string =>
91
- value.trim() || placeholder?.trim() || ''
92
- const isScrubbable = scrub !== undefined && NUMBER.test(scrubFrom(initial))
93
-
94
- const handlers = isScrubbable
95
- ? {
96
- onPointerDown: (event: PointerEvent) => {
97
- const typed = input.current?.value ?? ''
98
- const start = scrubFrom(typed)
99
- if (isBusy || !NUMBER.test(start)) return
100
- event.preventDefault()
101
- /*
102
- * Capture keeps the release coming here when the pointer leaves the
103
- * glyph, the panel, or crosses into a frame's own document.
104
- */
105
- try {
106
- handle.current?.setPointerCapture(event.pointerId)
107
- } catch {
108
- /* A pointer the browser no longer tracks ends on cancel instead. */
109
- }
110
- drag.current = {
111
- pointerId: event.pointerId,
112
- typed,
113
- start,
114
- session: scrub.begin(),
115
- lastX: event.clientX,
116
- value: Number(start),
117
- }
118
- },
119
- onPointerMove: (event: PointerEvent) => {
120
- const held = drag.current
121
- if (!held || held.pointerId !== event.pointerId) return
122
- const moved = event.clientX - held.lastX
123
- held.lastX = event.clientX
124
- held.value = scrub.clamp(
125
- Math.round(
126
- (held.value +
127
- moved * scrubStep(event.shiftKey) * (scrub.step ?? 1)) *
128
- 10,
129
- ) / 10,
130
- )
131
- const shown = String(held.value)
132
- if (input.current) input.current.value = shown
133
- held.session.preview(shown)
134
- },
135
- onPointerUp: (event: PointerEvent) => {
136
- const held = end(event.pointerId)
137
- if (!held) return
138
- const shown = String(held.value)
139
- if (shown === held.start) held.session.restore()
140
- else held.session.commit(shown)
141
- },
142
- onPointerCancel: (event: PointerEvent) => {
143
- const held = end(event.pointerId)
144
- if (!held) return
145
- if (input.current) input.current.value = held.typed
146
- held.session.restore()
147
- },
148
- }
149
- : {}
150
-
151
- return (
152
- <div class={fieldClass(isWide)}>
153
- <span
154
- ref={handle}
155
- class={isScrubbable ? 'glyph is-scrub' : 'glyph'}
156
- aria-hidden="true"
157
- title={label}
158
- {...handlers}
159
- >
160
- {glyph}
161
- </span>
162
- <input
163
- ref={input}
164
- class="glyph-field-input"
165
- type="text"
166
- aria-label={label}
167
- title={initial}
168
- defaultValue={initial}
169
- placeholder={placeholder}
170
- disabled={isBusy}
171
- spellcheck={false}
172
- onChange={(event) => {
173
- const value = event.currentTarget.value.trim()
174
- if (value !== initial.trim()) onCommit(value)
175
- }}
176
- onKeyDown={(event) => {
177
- if (event.key === 'Escape') event.currentTarget.value = initial
178
- }}
179
- />
180
- {trailing}
181
- </div>
182
- )
183
- }
184
-
185
- interface SelectFieldProps {
186
- readonly label: string
187
- readonly glyph: string
188
- /** Each option's value, which is also what it shows. */
189
- readonly options: readonly string[]
190
- readonly value: string
191
- readonly isBusy: boolean
192
- readonly isWide?: boolean
193
- readonly onCommit: (value: string) => void
194
- }
195
-
196
- /**
197
- * A value picked from a fixed list, drawn like a field with a chevron. The
198
- * select is native, so its list opens above the panel's clipping. A current
199
- * value outside the list stays as an option, so it shows rather than blanks.
200
- */
201
- export function SelectField({
202
- label,
203
- glyph,
204
- options,
205
- value,
206
- isBusy,
207
- isWide,
208
- onCommit,
209
- }: SelectFieldProps): JSX.Element {
210
- const shown = options.includes(value) ? options : [...options, value]
211
- return (
212
- <div class={fieldClass(isWide, 'is-select')}>
213
- <span class="glyph" aria-hidden="true" title={label}>
214
- {glyph}
215
- </span>
216
- <select
217
- class="glyph-field-input glyph-select"
218
- aria-label={label}
219
- value={value}
220
- disabled={isBusy}
221
- onChange={(event) => {
222
- const picked = event.currentTarget.value
223
- if (picked !== value) onCommit(picked)
224
- }}
225
- >
226
- {shown.map((option) => (
227
- <option key={option} value={option}>
228
- {option}
229
- </option>
230
- ))}
231
- </select>
232
- <svg class="select-chevron" viewBox="0 0 16 16" aria-hidden="true">
233
- <path d="M4.5 6.5 8 10l3.5-3.5" />
234
- </svg>
235
- </div>
236
- )
237
- }
238
-
239
- interface ReadOnlyFieldProps {
240
- readonly label: string
241
- readonly glyph: string
242
- readonly value: string
243
- readonly isWide?: boolean
244
- }
245
-
246
- /** A value the panel shows and nothing here writes, drawn like a field. */
247
- export function ReadOnlyField({
248
- label,
249
- glyph,
250
- value,
251
- isWide,
252
- }: ReadOnlyFieldProps): JSX.Element {
253
- return (
254
- <div class={fieldClass(isWide, 'is-readonly')}>
255
- <span class="glyph" aria-hidden="true" title={label}>
256
- {glyph}
257
- </span>
258
- <output class="glyph-field-input" aria-label={label} title={value}>
259
- {value}
260
- </output>
261
- </div>
262
- )
263
- }