@weasel-js/labkit 1.7.0 → 1.7.2

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 (242) hide show
  1. package/README.md +3 -3
  2. package/dist/_dts/{CanvasStackContext-D2jnIwfk.d.ts → CanvasStackContext-Cjy6I-Iv.d.ts} +4 -1
  3. package/dist/_dts/{Stage-CCWfQQWM.d.ts → Stage-D6t7o0pF.d.ts} +3 -2
  4. package/dist/_dts/{index-CsYqUHD3.d.ts → index-BTmaRy6a.d.ts} +10 -6
  5. package/dist/_dts/{types-CSm9jgol.d.ts → index-DCMA4Fy1.d.ts} +59 -113
  6. package/dist/_dts/{types-CRPSGFAp.d.ts → types-BUBSIZuV.d.ts} +8 -7
  7. package/dist/_dts/{useTrialState-CTZPgeZ7.d.ts → useTrialState-Bh8bzFgE.d.ts} +1 -1
  8. package/dist/canvas/index.d.ts +8 -6
  9. package/dist/canvas/index.js +6 -6
  10. package/dist/chrome/index.d.ts +7 -5
  11. package/dist/chrome/index.js +10 -9
  12. package/dist/chunk-2ES4W47N.js +8 -0
  13. package/dist/chunk-2ES4W47N.js.map +1 -0
  14. package/dist/{chunk-WPLAANH4.js → chunk-2N7UBSS2.js} +12 -12
  15. package/dist/chunk-2N7UBSS2.js.map +1 -0
  16. package/dist/{chunk-275JRRVD.js → chunk-E23Q6A3Y.js} +232 -37
  17. package/dist/chunk-E23Q6A3Y.js.map +1 -0
  18. package/dist/{chunk-GOZLNM4L.js → chunk-E6NARGU7.js} +5 -5
  19. package/dist/{chunk-GOZLNM4L.js.map → chunk-E6NARGU7.js.map} +1 -1
  20. package/dist/{chunk-IJJUK4FU.js → chunk-EESJUSYR.js} +5 -6
  21. package/dist/chunk-EESJUSYR.js.map +1 -0
  22. package/dist/{chunk-HXXTULDN.js → chunk-EFXRM6XQ.js} +8 -11
  23. package/dist/chunk-EFXRM6XQ.js.map +1 -0
  24. package/dist/chunk-F4ACIK7Z.js +210 -0
  25. package/dist/chunk-F4ACIK7Z.js.map +1 -0
  26. package/dist/{chunk-AWQG7FCC.js → chunk-FG3EXZDV.js} +3 -3
  27. package/dist/{chunk-AWQG7FCC.js.map → chunk-FG3EXZDV.js.map} +1 -1
  28. package/dist/{chunk-YT75VYRD.js → chunk-IBVRMHW5.js} +5 -5
  29. package/dist/{chunk-YT75VYRD.js.map → chunk-IBVRMHW5.js.map} +1 -1
  30. package/dist/{chunk-RWNDDKOH.js → chunk-PMLZP7RT.js} +18 -15
  31. package/dist/chunk-PMLZP7RT.js.map +1 -0
  32. package/dist/{chunk-Y23G2VZL.js → chunk-READTDQX.js} +2 -2
  33. package/dist/chunk-READTDQX.js.map +1 -0
  34. package/dist/{chunk-I5DUQ5HM.js → chunk-RODIY7HI.js} +75 -72
  35. package/dist/chunk-RODIY7HI.js.map +1 -0
  36. package/dist/{chunk-JVBYV6TL.js → chunk-ROGJWDQA.js} +3 -3
  37. package/dist/{chunk-JVBYV6TL.js.map → chunk-ROGJWDQA.js.map} +1 -1
  38. package/dist/{chunk-ZIFPHIWZ.js → chunk-T6DLGEJB.js} +12 -14
  39. package/dist/chunk-T6DLGEJB.js.map +1 -0
  40. package/dist/chunk-U6UQVGFP.js +15583 -0
  41. package/dist/chunk-U6UQVGFP.js.map +1 -0
  42. package/dist/{chunk-24W5VEJW.js → chunk-ULYYNWGP.js} +2 -2
  43. package/dist/{chunk-24W5VEJW.js.map → chunk-ULYYNWGP.js.map} +1 -1
  44. package/dist/{chunk-UNAZSLWE.js → chunk-VYVGVBGY.js} +17 -20
  45. package/dist/chunk-VYVGVBGY.js.map +1 -0
  46. package/dist/chunk-WFALF2X2.js +302 -0
  47. package/dist/chunk-WFALF2X2.js.map +1 -0
  48. package/dist/{chunk-KMJ7RWD3.js → chunk-WLMDTI2Q.js} +3 -3
  49. package/dist/{chunk-KMJ7RWD3.js.map → chunk-WLMDTI2Q.js.map} +1 -1
  50. package/dist/{chunk-T5IBSPAM.js → chunk-WWJDPNZZ.js} +43 -33
  51. package/dist/chunk-WWJDPNZZ.js.map +1 -0
  52. package/dist/{chunk-J7OWWTCW.js → chunk-XE5HJOPL.js} +5 -6
  53. package/dist/chunk-XE5HJOPL.js.map +1 -0
  54. package/dist/{chunk-7BO7HNGF.js → chunk-XRX5S5NT.js} +8 -61
  55. package/dist/chunk-XRX5S5NT.js.map +1 -0
  56. package/dist/chunk-XVUKDSYV.js +38 -0
  57. package/dist/chunk-XVUKDSYV.js.map +1 -0
  58. package/dist/{chunk-FMK7UQ6V.js → chunk-XZDCEMEL.js} +64 -47
  59. package/dist/chunk-XZDCEMEL.js.map +1 -0
  60. package/dist/{chunk-FZ7YFQN7.js → chunk-YCFKW5RX.js} +3 -3
  61. package/dist/chunk-YCFKW5RX.js.map +1 -0
  62. package/dist/{chunk-CQLQPQ4P.js → chunk-ZQIHJXP4.js} +2 -2
  63. package/dist/chunk-ZQIHJXP4.js.map +1 -0
  64. package/dist/config/index.d.ts +18 -10
  65. package/dist/config/index.js +4 -4
  66. package/dist/controls/index.d.ts +58 -9
  67. package/dist/controls/index.js +7 -5
  68. package/dist/dragdrop/index.d.ts +23 -3
  69. package/dist/dragdrop/index.js +4 -3
  70. package/dist/fonts/oswald-tabular-digits.woff2 +0 -0
  71. package/dist/index.d.ts +61 -715
  72. package/dist/index.js +456 -432
  73. package/dist/index.js.map +1 -1
  74. package/dist/job/index.d.ts +8 -0
  75. package/dist/job/index.js +1 -1
  76. package/dist/layers/index.d.ts +6 -5
  77. package/dist/layers/index.js +3 -2
  78. package/dist/loupe/index.d.ts +100 -30
  79. package/dist/loupe/index.js +585 -6
  80. package/dist/loupe/index.js.map +1 -1
  81. package/dist/overview/index.d.ts +4 -2
  82. package/dist/overview/index.js +15 -14
  83. package/dist/overview/index.js.map +1 -1
  84. package/dist/passthrough/weasel-canvas.d.ts +2 -0
  85. package/dist/passthrough/weasel-ui.d.ts +2 -2871
  86. package/dist/passthrough/weasel-ui.js +3 -2
  87. package/dist/primitives/index.d.ts +213 -3
  88. package/dist/primitives/index.js +8 -7
  89. package/dist/state/index.d.ts +7 -5
  90. package/dist/state/index.js +6 -6
  91. package/dist/styles.css +284 -51
  92. package/dist/surface/index.d.ts +10 -2
  93. package/dist/surface/index.js +1 -1
  94. package/dist/undo/index.d.ts +5 -3
  95. package/package.json +11 -11
  96. package/src/annotations/AnnotationOverlay.abandon.test.tsx +143 -0
  97. package/src/annotations/AnnotationOverlay.tsx +50 -31
  98. package/src/annotations/Annotations.overlay.test.tsx +31 -0
  99. package/src/annotations/capture.browser.test.ts +47 -0
  100. package/src/annotations/capture.ts +14 -7
  101. package/src/annotations/drawOne.ts +2 -2
  102. package/src/annotations/frac.test.ts +44 -1
  103. package/src/annotations/frac.ts +46 -18
  104. package/src/annotations/index.ts +1 -0
  105. package/src/annotations/paint.test.ts +74 -33
  106. package/src/annotations/paint.ts +70 -32
  107. package/src/annotations/store.test.ts +212 -2
  108. package/src/annotations/store.ts +130 -27
  109. package/src/annotations/svgNodes.test.ts +22 -10
  110. package/src/annotations/svgNodes.ts +3 -3
  111. package/src/annotations/types.ts +29 -10
  112. package/src/canvas/CameraInput.test.tsx +30 -0
  113. package/src/canvas/CameraInput.tsx +16 -15
  114. package/src/canvas/CanvasStack.tsx +5 -5
  115. package/src/canvas/CanvasStackContext.ts +4 -1
  116. package/src/canvas/Stage.test.tsx +34 -25
  117. package/src/canvas/Stage.tsx +3 -3
  118. package/src/canvas/cameraRegistry.ts +1 -0
  119. package/src/canvas/worldSpec.ts +3 -1
  120. package/src/chrome/builtins.test.ts +7 -6
  121. package/src/chrome/builtins.tsx +5 -1
  122. package/src/chrome/index.ts +1 -1
  123. package/src/chrome/labRegions.test.tsx +37 -26
  124. package/src/chrome/merge.test.ts +5 -0
  125. package/src/chrome/merge.ts +4 -2
  126. package/src/chrome/regions/PaletteRegion.test.tsx +4 -5
  127. package/src/chrome/regions/PaletteRegion.tsx +3 -4
  128. package/src/chrome/regions/ViewportRegion.browser.test.less +6 -0
  129. package/src/chrome/regions/ViewportRegion.browser.test.tsx +38 -0
  130. package/src/chrome/regions/ViewportRegion.less +6 -0
  131. package/src/chrome/regions/ViewportRegion.tsx +10 -4
  132. package/src/chrome/types.ts +4 -2
  133. package/src/config/auto.ts +1 -0
  134. package/src/config/builder.ts +6 -0
  135. package/src/config/types.ts +2 -1
  136. package/src/controls/ControlMatrix.less +245 -0
  137. package/src/controls/ControlMatrix.stories.less +13 -0
  138. package/src/controls/ControlMatrix.stories.tsx +135 -0
  139. package/src/controls/ControlMatrix.test.tsx +206 -0
  140. package/src/controls/ControlMatrix.tsx +446 -0
  141. package/src/controls/ControlPanel.test.tsx +37 -3
  142. package/src/controls/ControlPanel.tsx +16 -5
  143. package/src/controls/index.ts +6 -0
  144. package/src/dragdrop/DragDropRuntime.tsx +22 -13
  145. package/src/dragdrop/DragGhost.tsx +3 -0
  146. package/src/dragdrop/Palette.tsx +2 -0
  147. package/src/dragdrop/dragDrop.test.tsx +34 -33
  148. package/src/index.test.ts +5 -0
  149. package/src/index.ts +7 -1
  150. package/src/instrument/types.ts +0 -6
  151. package/src/job/useJob.ts +14 -9
  152. package/src/lab/Lab.chrome.test.tsx +15 -10
  153. package/src/lab/Lab.nebula.test.tsx +5 -5
  154. package/src/lab/Lab.surface.test.tsx +30 -21
  155. package/src/lab/Lab.test.tsx +75 -74
  156. package/src/lab/Lab.tsx +5 -1
  157. package/src/lab/Lab.undock.test.tsx +15 -10
  158. package/src/lab/LabHeader.test.tsx +15 -14
  159. package/src/lab/LabPalette.test.tsx +12 -7
  160. package/src/lab/LabShell.less +1 -1
  161. package/src/lab/LabSwitcher.browser.test.tsx +27 -0
  162. package/src/lab/LabSwitcher.less +2 -0
  163. package/src/lab/LabZoom.test.tsx +55 -44
  164. package/src/lab/LabZoom.tsx +4 -4
  165. package/src/lab/Workspace.surface.test.tsx +16 -13
  166. package/src/lab/Workspace.test.tsx +32 -27
  167. package/src/lab/Workspace.tsx +48 -52
  168. package/src/loupe/AGENTS.md +28 -15
  169. package/src/loupe/CanvasLoupe.tsx +2 -3
  170. package/src/loupe/DomLoupe.tsx +1 -7
  171. package/src/loupe/Loupe.less +16 -0
  172. package/src/loupe/TrialLoupe.tsx +120 -57
  173. package/src/loupe/index.ts +1 -6
  174. package/src/loupe/types.test.ts +3 -3
  175. package/src/loupe/types.ts +24 -27
  176. package/src/loupe/useLoupe.test.tsx +29 -4
  177. package/src/loupe/useLoupe.ts +20 -17
  178. package/src/overview/OverviewMarks.test.tsx +9 -8
  179. package/src/overview/OverviewMarks.tsx +12 -4
  180. package/src/overview/TrialOverview.test.tsx +18 -17
  181. package/src/overview/TrialOverview.tsx +3 -3
  182. package/src/passthrough/weasel-ui.test.ts +1 -1
  183. package/src/passthrough/weasel-ui.ts +1 -1
  184. package/src/primitives/FloatingPanel.tsx +7 -3
  185. package/src/primitives/FpsMeter.less +1 -2
  186. package/src/primitives/JobProgress.less +1 -1
  187. package/src/primitives/Readout.test.tsx +2 -2
  188. package/src/primitives/ScaleIndicator.less +1 -1
  189. package/src/primitives/Split.tsx +69 -41
  190. package/src/primitives/StatusBar.less +1 -1
  191. package/src/primitives/Toolbar.tsx +3 -3
  192. package/src/primitives/ZoomControl.less +1 -2
  193. package/src/specimen/Specimen.less +1 -0
  194. package/src/specimen/Specimen.tsx +0 -1
  195. package/src/state/undock.ts +3 -0
  196. package/src/state/useOpenOnce.ts +20 -16
  197. package/src/state/view.ts +1 -0
  198. package/src/styles.less +3 -0
  199. package/src/surface/index.ts +1 -1
  200. package/src/surface/useTiledSurface.test.tsx +14 -0
  201. package/src/surface/useTiledSurface.ts +11 -3
  202. package/src/trial/Trial.annotations.persist.test.tsx +20 -9
  203. package/src/trial/Trial.annotations.test.tsx +105 -27
  204. package/src/trial/Trial.canvas.test.tsx +21 -17
  205. package/src/trial/Trial.config.test.tsx +34 -27
  206. package/src/trial/Trial.less +0 -16
  207. package/src/trial/Trial.loupe.test.tsx +51 -25
  208. package/src/trial/Trial.targets.test.tsx +9 -8
  209. package/src/trial/Trial.test.tsx +51 -46
  210. package/src/trial/Trial.trialId.test.tsx +7 -6
  211. package/src/trial/Trial.tsx +74 -62
  212. package/src/trial/TrialChrome.tsx +6 -3
  213. package/src/trial/index.ts +2 -0
  214. package/src/trial/loupeSwitch.ts +15 -0
  215. package/dist/_dts/LayerList.d-DiuEArKq.d.ts +0 -83
  216. package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +0 -79
  217. package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +0 -141
  218. package/dist/_dts/Select.d-Bensr_MT.d.ts +0 -579
  219. package/dist/_dts/index-B2Yj5aA0.d.ts +0 -274
  220. package/dist/chunk-275JRRVD.js.map +0 -1
  221. package/dist/chunk-4LJSC7MB.js +0 -143
  222. package/dist/chunk-4LJSC7MB.js.map +0 -1
  223. package/dist/chunk-7BO7HNGF.js.map +0 -1
  224. package/dist/chunk-CQLQPQ4P.js.map +0 -1
  225. package/dist/chunk-FMK7UQ6V.js.map +0 -1
  226. package/dist/chunk-FZ7YFQN7.js.map +0 -1
  227. package/dist/chunk-HXXTULDN.js.map +0 -1
  228. package/dist/chunk-I5DUQ5HM.js.map +0 -1
  229. package/dist/chunk-IJJUK4FU.js.map +0 -1
  230. package/dist/chunk-J7OWWTCW.js.map +0 -1
  231. package/dist/chunk-M7HP3FTK.js +0 -549
  232. package/dist/chunk-M7HP3FTK.js.map +0 -1
  233. package/dist/chunk-MIM5R4ZT.js +0 -14972
  234. package/dist/chunk-MIM5R4ZT.js.map +0 -1
  235. package/dist/chunk-RWNDDKOH.js.map +0 -1
  236. package/dist/chunk-T5IBSPAM.js.map +0 -1
  237. package/dist/chunk-UNAZSLWE.js.map +0 -1
  238. package/dist/chunk-WPLAANH4.js.map +0 -1
  239. package/dist/chunk-Y23G2VZL.js.map +0 -1
  240. package/dist/chunk-ZIFPHIWZ.js.map +0 -1
  241. package/src/primitives/useRovingTabIndex.test.ts +0 -48
  242. package/src/primitives/useRovingTabIndex.ts +0 -88
@@ -10,6 +10,7 @@ export const auto: unique symbol = Symbol('weasel.auto');
10
10
  /** The type of the `auto` sentinel, for widening a value parameter. */
11
11
  export type Auto = typeof auto;
12
12
 
13
+ /** Whether `value` is the `auto` sentinel. */
13
14
  export function isAuto(value: unknown): value is Auto {
14
15
  return value === auto;
15
16
  }
@@ -124,6 +124,7 @@ export abstract class BaseNode<T> implements ConfigNode<T> {
124
124
  }
125
125
  }
126
126
 
127
+ /** A numeric leaf, built by `f.number`. */
127
128
  export class NumberNode extends BaseNode<number> {
128
129
  readonly kind = 'number';
129
130
 
@@ -169,6 +170,7 @@ export class NumberNode extends BaseNode<number> {
169
170
  }
170
171
  }
171
172
 
173
+ /** A true/false leaf, built by `f.boolean`; a checkbox unless `.toggle()`. */
172
174
  export class BooleanNode extends BaseNode<boolean> {
173
175
  readonly kind = 'boolean';
174
176
 
@@ -178,6 +180,7 @@ export class BooleanNode extends BaseNode<boolean> {
178
180
  }
179
181
  }
180
182
 
183
+ /** A text leaf, built by `f.string`. */
181
184
  export class StringNode extends BaseNode<string> {
182
185
  readonly kind = 'string';
183
186
 
@@ -195,6 +198,7 @@ export class StringNode extends BaseNode<string> {
195
198
  }
196
199
  }
197
200
 
201
+ /** A string-list leaf, built by `f.list`. */
198
202
  export class ListNode extends BaseNode<string[]> {
199
203
  readonly kind = 'list';
200
204
 
@@ -204,6 +208,7 @@ export class ListNode extends BaseNode<string[]> {
204
208
  }
205
209
  }
206
210
 
211
+ /** A color leaf holding a hex string, built by `f.color`. */
207
212
  export class ColorNode extends BaseNode<string> {
208
213
  readonly kind = 'color';
209
214
 
@@ -214,6 +219,7 @@ export class ColorNode extends BaseNode<string> {
214
219
  }
215
220
  }
216
221
 
222
+ /** A fixed-choice leaf, built by `f.enum`; a select unless `.radio()`. */
217
223
  export class EnumNode<T extends string> extends BaseNode<T> {
218
224
  readonly kind = 'enum';
219
225
 
@@ -14,7 +14,8 @@ export interface ConfigOption {
14
14
  value: string;
15
15
  label: string;
16
16
  /** A glyph in weasel-ui's icon set (`IconName`). A segmented row draws it in
17
- * place of `label`, which stays the accessible name and becomes the tooltip. */
17
+ * place of `label`, which stays the accessible name and becomes the tooltip;
18
+ * a select draws it beside `label`. */
18
19
  icon?: string;
19
20
  }
20
21
 
@@ -0,0 +1,245 @@
1
+ // A settings-by-scopes grid. Built to sit beside a ControlPanel in a ~360px
2
+ // sidebar: the label column takes what seven value columns leave, and every cell
3
+ // is a fixed-height button so the rows rule up evenly.
4
+ .lk-control-matrix {
5
+ min-width: 0;
6
+ }
7
+
8
+ .lk-control-matrix__table {
9
+ width: 100%;
10
+ table-layout: fixed;
11
+ border-collapse: collapse;
12
+ font-family: var(--wzl-font-ui);
13
+ font-size: var(--wzl-font-size-xs);
14
+ color: var(--wzl-fg);
15
+ }
16
+
17
+ .lk-control-matrix__corner {
18
+ width: var(--lk-control-matrix-label-width, 30%);
19
+ }
20
+
21
+ .lk-control-matrix__head,
22
+ .lk-control-matrix__label {
23
+ font-family: var(--wzl-font-display);
24
+ font-weight: var(--wzl-font-weight-light);
25
+ font-size: var(--wzl-font-size-sm);
26
+ line-height: var(--wzl-leading-snug);
27
+ color: var(--wzl-fg-muted);
28
+ text-transform: var(--wzl-params-label-case, uppercase);
29
+ letter-spacing: var(--wzl-params-label-tracking, var(--wzl-tracking-wide));
30
+ }
31
+
32
+ .lk-control-matrix__head {
33
+ padding: 0 0 var(--wzl-space-2);
34
+ text-align: center;
35
+ font-size: var(--wzl-font-size-2xs);
36
+ white-space: nowrap;
37
+ overflow: hidden;
38
+ text-overflow: ellipsis;
39
+ }
40
+
41
+ // The fallback scope every other column inherits from.
42
+ .lk-control-matrix__head.is-first {
43
+ color: var(--wzl-fg);
44
+ font-weight: var(--wzl-font-weight-medium);
45
+ }
46
+
47
+ .lk-control-matrix__head-text {
48
+ text-decoration: none;
49
+ cursor: default;
50
+ }
51
+
52
+ .lk-control-matrix__head-button {
53
+ width: 100%;
54
+ padding: 0;
55
+ border: 0;
56
+ border-radius: var(--wzl-radius-sm);
57
+ background: none;
58
+ color: inherit;
59
+ font: inherit;
60
+ letter-spacing: var(--wzl-params-label-tracking, var(--wzl-tracking-wide));
61
+ text-transform: var(--wzl-params-label-case, uppercase);
62
+ cursor: pointer;
63
+ overflow: hidden;
64
+ text-overflow: ellipsis;
65
+ }
66
+
67
+ .lk-control-matrix__head-button:hover {
68
+ color: var(--wzl-fg);
69
+ background: var(--wzl-surface-hover);
70
+ }
71
+
72
+ .lk-control-matrix__label {
73
+ padding: 0 var(--wzl-space-2) 0 0;
74
+ text-align: start;
75
+ white-space: nowrap;
76
+ }
77
+
78
+ // The text truncates; the help glyph after it never does.
79
+ .lk-control-matrix__label-text {
80
+ display: inline-block;
81
+ max-width: calc(100% - 1.4em);
82
+ overflow: hidden;
83
+ text-overflow: ellipsis;
84
+ vertical-align: middle;
85
+ }
86
+
87
+ .lk-control-matrix__cell {
88
+ padding: 1px;
89
+ height: var(--wzl-control-h-sm);
90
+ text-align: center;
91
+ vertical-align: middle;
92
+ }
93
+
94
+ .lk-control-matrix__cell.is-missing {
95
+ color: var(--wzl-fg-subtle);
96
+ }
97
+
98
+ // Inherited: the value in effect, but not chosen here.
99
+ .lk-control-matrix__cell.is-inherited > * {
100
+ opacity: 0.4;
101
+ }
102
+
103
+ // Every cell control fills its cell, so the cell is the click target.
104
+ .lk-control-matrix__value,
105
+ .lk-control-matrix__switch {
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ width: 100%;
110
+ height: var(--wzl-control-h-sm);
111
+ padding: 0 var(--wzl-space-1);
112
+ border: 1px solid transparent;
113
+ border-radius: var(--wzl-radius-sm);
114
+ background: none;
115
+ color: inherit;
116
+ font: inherit;
117
+ cursor: pointer;
118
+ }
119
+
120
+ .lk-control-matrix__value {
121
+ display: block;
122
+ overflow: hidden;
123
+ text-overflow: ellipsis;
124
+ white-space: nowrap;
125
+ line-height: calc(var(--wzl-control-h-sm) - 2px);
126
+ }
127
+
128
+ .lk-control-matrix__value.is-number {
129
+ .numeric();
130
+ text-align: end;
131
+ // A column of numbers is read down, so it keeps every digit: a glow of
132
+ // 0.35 cut to `0…` says nothing.
133
+ text-overflow: clip;
134
+ }
135
+
136
+ .lk-control-matrix__value:hover,
137
+ .lk-control-matrix__switch:hover {
138
+ border-color: var(--wzl-border);
139
+ background: var(--wzl-surface-hover);
140
+ }
141
+
142
+ .lk-control-matrix__value:focus-visible,
143
+ .lk-control-matrix__switch:focus-visible,
144
+ .lk-control-matrix__swatch:focus-visible,
145
+ .lk-control-matrix__head-button:focus-visible {
146
+ outline: 2px solid var(--wzl-focus-ring);
147
+ outline-offset: -1px;
148
+ }
149
+
150
+ // A small switch, drawn with the same parts as weasel-ui's.
151
+ .lk-control-matrix__track {
152
+ position: relative;
153
+ flex: 0 0 auto;
154
+ width: 20px;
155
+ height: 12px;
156
+ background: var(--wzl-surface-sunken);
157
+ border: 1px solid var(--wzl-border-strong);
158
+ border-radius: var(--wzl-radius-pill);
159
+ transition: background-color var(--wzl-motion-fast), border-color var(--wzl-motion-fast);
160
+ }
161
+
162
+ .lk-control-matrix__thumb {
163
+ position: absolute;
164
+ top: 1px;
165
+ left: 1px;
166
+ width: 8px;
167
+ height: 8px;
168
+ background: var(--wzl-fg);
169
+ border-radius: 50%;
170
+ transition: transform var(--wzl-motion-fast);
171
+ }
172
+
173
+ .lk-control-matrix__switch[aria-checked='true'] .lk-control-matrix__track {
174
+ background: var(--wzl-accent-strong);
175
+ border-color: var(--wzl-accent-strong);
176
+ }
177
+
178
+ .lk-control-matrix__switch[aria-checked='true'] .lk-control-matrix__thumb {
179
+ transform: translateX(8px);
180
+ background: var(--wzl-fg-on-accent);
181
+ }
182
+
183
+ .lk-control-matrix__swatch {
184
+ display: block;
185
+ width: 100%;
186
+ height: var(--wzl-control-h-xs);
187
+ padding: 0;
188
+ border: 1px solid var(--wzl-border);
189
+ border-radius: var(--wzl-radius-sm);
190
+ background: none;
191
+ cursor: pointer;
192
+ -webkit-appearance: none;
193
+ appearance: none;
194
+ }
195
+
196
+ .lk-control-matrix__swatch::-webkit-color-swatch-wrapper {
197
+ padding: 0;
198
+ }
199
+
200
+ .lk-control-matrix__swatch::-webkit-color-swatch {
201
+ border: 0;
202
+ border-radius: var(--wzl-radius-sm);
203
+ }
204
+
205
+ .lk-control-matrix__swatch::-moz-color-swatch {
206
+ border: 0;
207
+ border-radius: var(--wzl-radius-sm);
208
+ }
209
+
210
+ .lk-control-matrix__chip {
211
+ display: inline-block;
212
+ width: 22px;
213
+ height: var(--wzl-control-h-xs);
214
+ border: 1px solid var(--wzl-border);
215
+ border-radius: var(--wzl-radius-sm);
216
+ background: var(--lk-chip);
217
+ vertical-align: middle;
218
+ }
219
+
220
+ .lk-control-matrix__chip.is-unset {
221
+ border-style: dashed;
222
+ background: none;
223
+ }
224
+
225
+ .lk-control-matrix__popover {
226
+ outline: none;
227
+ }
228
+
229
+ .lk-control-matrix__editor {
230
+ display: flex;
231
+ flex-direction: column;
232
+ gap: var(--wzl-space-3);
233
+ inline-size: 240px;
234
+ padding: var(--wzl-space-4);
235
+ background: var(--wzl-surface-raised);
236
+ border: 1px solid var(--wzl-border);
237
+ border-radius: var(--wzl-radius-md);
238
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
239
+ outline: none;
240
+ }
241
+
242
+ .lk-control-matrix__editor-actions {
243
+ display: flex;
244
+ justify-content: flex-end;
245
+ }
@@ -0,0 +1,13 @@
1
+ // A sidebar-width frame for the ControlMatrix stories.
2
+ .lk-control-matrix-story {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: var(--wzl-space-5);
6
+ width: 360px;
7
+ }
8
+
9
+ .lk-control-matrix-story__note {
10
+ margin: 0;
11
+ color: var(--wzl-fg-muted);
12
+ font-size: var(--wzl-font-size-sm);
13
+ }
@@ -0,0 +1,135 @@
1
+ import type { Meta, StoryObj } from '@weasel-js/forge';
2
+ import { useMemo, useState } from 'react';
3
+ import { isAuto } from '../config/auto';
4
+ import { f } from '../config/builder';
5
+ import { valueAtPath, withValueAtPath } from '../config/path';
6
+ import { resolveConfigSchema } from '../config/resolve';
7
+ import { ControlMatrix, type ControlMatrixColumn } from './ControlMatrix';
8
+ import { ControlPanel } from './ControlPanel';
9
+ import './ControlMatrix.stories.less';
10
+
11
+ const meta: Meta<typeof ControlMatrix> = {
12
+ title: 'labkit/Controls/ControlMatrix',
13
+ component: ControlMatrix,
14
+ parameters: { layout: 'padded' },
15
+ };
16
+ export default meta;
17
+
18
+ type Story = StoryObj<typeof ControlMatrix>;
19
+
20
+ const look = () => ({
21
+ background: f.boolean(true).describe('Paint the box behind the primitive.'),
22
+ fill: f.color('#2f5d8a'),
23
+ border: f.number(1).range(0, 4).step(0.5).input().suffix('px'),
24
+ shading: f.enum('flat', [
25
+ { value: 'flat', label: 'Flat' },
26
+ { value: 'soft', label: 'Soft' },
27
+ { value: 'bevel', label: 'Bevel' },
28
+ ]),
29
+ glow: f.number(0).range(0, 1).step(0.05).describe('Halo strength around the primitive.'),
30
+ });
31
+
32
+ const primitives = ['window', 'page', 'slab', 'row', 'tile', 'arrow'] as const;
33
+ const LOOK_KEYS = Object.keys(look()) as (keyof ReturnType<typeof look>)[];
34
+
35
+ const config = f.schema({
36
+ looks: f.group({
37
+ defaults: f.group(look()),
38
+ window: f.group(look()),
39
+ page: f.group(look()),
40
+ slab: f.group(look()),
41
+ row: f.group(look()),
42
+ tile: f.group(look()),
43
+ // An arrow is a line: it has nothing to fill and no box behind it.
44
+ arrow: f.group({ border: look().border, shading: look().shading, glow: look().glow }),
45
+ }),
46
+ });
47
+
48
+ const columns: ControlMatrixColumn[] = [
49
+ { key: 'looks.defaults', label: 'Def', title: 'Defaults' },
50
+ { key: 'looks.window', label: 'Win', title: 'Window' },
51
+ { key: 'looks.page', label: 'Page', title: 'Page' },
52
+ { key: 'looks.slab', label: 'Slab', title: 'Slab' },
53
+ { key: 'looks.row', label: 'Row', title: 'Row' },
54
+ { key: 'looks.tile', label: 'Tile', title: 'Tile' },
55
+ { key: 'looks.arrow', label: 'Arr', title: 'Arrow' },
56
+ ];
57
+ const rows = LOOK_KEYS.map((key) => ({ key }));
58
+
59
+ /**
60
+ * The store the consumer keeps: Defaults always, a primitive's setting only
61
+ * once it is pinned. A missing value inherits.
62
+ */
63
+ function useLooks() {
64
+ const schema = useMemo(() => resolveConfigSchema(config, []), []);
65
+ const [stored, setStored] = useState<Record<string, unknown>>(() => ({
66
+ looks: {
67
+ defaults: config.defaults().looks.defaults,
68
+ window: { fill: '#6b3f8f', glow: 0.35 },
69
+ arrow: { border: 2 },
70
+ },
71
+ }));
72
+ const autoPaths = new Set<string>();
73
+ let shown: Record<string, unknown> = stored;
74
+ for (const p of primitives) {
75
+ for (const key of LOOK_KEYS) {
76
+ const path = `looks.${p}.${key}`;
77
+ if (valueAtPath(stored, path) !== undefined) continue;
78
+ autoPaths.add(path);
79
+ shown = withValueAtPath(shown, path, valueAtPath(stored, `looks.defaults.${key}`));
80
+ }
81
+ }
82
+ const setConfig = (path: string, value: unknown) =>
83
+ setStored((prev) => withValueAtPath(prev, path, isAuto(value) ? undefined : value));
84
+ return { schema, shown, autoPaths, setConfig };
85
+ }
86
+
87
+ /** Six primitives and their Defaults in a 360px sidebar. Click a cell to edit
88
+ * it, Option-click a pinned one to hand it back to Defaults. */
89
+ export const Looks: Story = {
90
+ render: function Render() {
91
+ const { schema, shown, autoPaths, setConfig } = useLooks();
92
+ const [focused, setFocused] = useState<string | null>(null);
93
+ return (
94
+ <div className="lk-control-matrix-story">
95
+ <ControlMatrix
96
+ title="Looks"
97
+ schema={schema}
98
+ columns={columns}
99
+ rows={rows}
100
+ config={shown}
101
+ auto={autoPaths}
102
+ setConfig={setConfig}
103
+ onColumnClick={(c) => setFocused(c.title ?? c.label)}
104
+ />
105
+ {focused ? <p className="lk-control-matrix-story__note">Clicked {focused}</p> : null}
106
+ </div>
107
+ );
108
+ },
109
+ };
110
+
111
+ /** The matrix under the panel it sits beside, to check the two read as one family. */
112
+ export const BesideControlPanel: Story = {
113
+ render: function Render() {
114
+ const { schema, shown, autoPaths, setConfig } = useLooks();
115
+ return (
116
+ <div className="lk-control-matrix-story">
117
+ <ControlPanel
118
+ title="Defaults"
119
+ schema={resolveConfigSchema(f.schema(look()), [])}
120
+ config={valueAtPath(shown, 'looks.defaults') as Record<string, unknown>}
121
+ setConfig={(path, value) => setConfig(`looks.defaults.${path}`, value)}
122
+ />
123
+ <ControlMatrix
124
+ title="Per primitive"
125
+ schema={schema}
126
+ columns={columns}
127
+ rows={rows}
128
+ config={shown}
129
+ auto={autoPaths}
130
+ setConfig={setConfig}
131
+ />
132
+ </div>
133
+ );
134
+ },
135
+ };
@@ -0,0 +1,206 @@
1
+ import { fireEvent, render, screen, within } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { auto } from '../config/auto';
4
+ import { f } from '../config/builder';
5
+ import { resolveConfigSchema } from '../config/resolve';
6
+ import { ControlMatrix, type ControlMatrixColumn } from './ControlMatrix';
7
+
8
+ const look = () => ({
9
+ background: f.boolean(true).label('Background'),
10
+ fill: f.color('#336699').label('Fill'),
11
+ glow: f.number(0.25).range(0, 1).step(0.05).label('Glow').describe('Halo around the shape.'),
12
+ shading: f.enum('flat', [
13
+ { value: 'flat', label: 'Flat' },
14
+ { value: 'soft', label: 'Soft' },
15
+ ]),
16
+ });
17
+
18
+ const schema = resolveConfigSchema(
19
+ f.schema({
20
+ looks: f.group({
21
+ defaults: f.group(look()),
22
+ window: f.group(look()),
23
+ // An arrow has no fill.
24
+ arrow: f.group({ background: look().background, glow: look().glow, shading: look().shading }),
25
+ }),
26
+ }),
27
+ [],
28
+ );
29
+
30
+ const columns: ControlMatrixColumn[] = [
31
+ { key: 'looks.defaults', label: 'Def', title: 'Defaults' },
32
+ { key: 'looks.window', label: 'Win', title: 'Window' },
33
+ { key: 'looks.arrow', label: 'Arr', title: 'Arrow' },
34
+ ];
35
+ const rows = [{ key: 'background' }, { key: 'fill' }, { key: 'glow' }, { key: 'shading' }];
36
+
37
+ const value = { background: true, fill: '#336699', glow: 0.25, shading: 'flat' };
38
+ const config = {
39
+ looks: {
40
+ defaults: value,
41
+ window: { ...value, glow: 0.5 },
42
+ arrow: { background: true, glow: 0.25, shading: 'soft' },
43
+ },
44
+ };
45
+
46
+ function setup(over: Partial<Parameters<typeof ControlMatrix>[0]> = {}) {
47
+ const setConfig = vi.fn();
48
+ const utils = render(
49
+ <ControlMatrix
50
+ schema={schema}
51
+ columns={columns}
52
+ rows={rows}
53
+ config={config}
54
+ auto={new Set(['looks.window.background', 'looks.window.fill', 'looks.arrow.glow'])}
55
+ setConfig={setConfig}
56
+ {...over}
57
+ />,
58
+ );
59
+ return { setConfig, ...utils };
60
+ }
61
+
62
+ const cell = (name: string) => screen.getByLabelText(name, { exact: false });
63
+
64
+ describe('<ControlMatrix>', () => {
65
+ it('draws a table of settings by scopes, labels from the schema', () => {
66
+ setup();
67
+ const table = screen.getByRole('table');
68
+ expect(
69
+ within(table)
70
+ .getAllByRole('columnheader')
71
+ .map((h) => h.textContent),
72
+ ).toEqual(['Def', 'Win', 'Arr']);
73
+ expect(
74
+ within(table)
75
+ .getAllByRole('rowheader')
76
+ .map((h) => h.textContent),
77
+ ).toEqual(['Background', 'Fill', 'Glowⓘ', 'Shading']);
78
+ expect(screen.getByRole('button', { name: 'About Glow' })).toBeTruthy();
79
+ });
80
+
81
+ it('shows numbers at their step precision and enums by label', () => {
82
+ setup();
83
+ expect(cell('Defaults Glow').textContent).toBe('0.25');
84
+ expect(cell('Window Glow').textContent).toBe('0.50');
85
+ expect(cell('Arrow Shading').textContent).toBe('Soft');
86
+ });
87
+
88
+ it('draws a dash for a scope with no such setting', () => {
89
+ setup();
90
+ const none = screen.getByLabelText('Arrow has no Fill');
91
+ expect(none.textContent).toBe('—');
92
+ expect(none.closest('td')?.className).toMatch(/is-missing/);
93
+ expect(none.closest('td')?.querySelector('button, input')).toBeNull();
94
+ });
95
+
96
+ it('ghosts an inherited cell and names where it inherits from', () => {
97
+ setup();
98
+ const inherited = screen.getByRole('switch', { name: 'Window Background, from Defaults' });
99
+ expect(inherited.closest('td')?.className).toMatch(/is-inherited/);
100
+ const pinned = screen.getByRole('switch', { name: 'Defaults Background' });
101
+ expect(pinned.closest('td')?.className).not.toMatch(/is-inherited/);
102
+ });
103
+
104
+ it('takes the inherit hint from a prop', () => {
105
+ setup({ inheritHint: (c) => `${c.title} follows the default` });
106
+ expect(
107
+ screen.getByRole('switch', { name: 'Window Background, Window follows the default' }),
108
+ ).toBeTruthy();
109
+ });
110
+
111
+ it('flips a boolean in place, which pins it', () => {
112
+ const { setConfig } = setup();
113
+ fireEvent.click(screen.getByRole('switch', { name: /^Window Background/ }));
114
+ expect(setConfig).toHaveBeenCalledWith('looks.window.background', false);
115
+ });
116
+
117
+ it('opens a color picker in place, which pins it', () => {
118
+ const { setConfig } = setup();
119
+ const swatch = cell('Window Fill') as HTMLInputElement;
120
+ expect(swatch.type).toBe('color');
121
+ fireEvent.change(swatch, { target: { value: '#ff0000' } });
122
+ expect(setConfig).toHaveBeenCalledWith('looks.window.fill', '#ff0000');
123
+ });
124
+
125
+ it('draws an unset color as unset, and clears a set one from its popover', () => {
126
+ const edge = resolveConfigSchema(
127
+ f.schema({
128
+ a: f.group({ edge: f.color('').label('Edge') }),
129
+ b: f.group({ edge: f.color('').label('Edge') }),
130
+ }),
131
+ [],
132
+ );
133
+ const setConfig = vi.fn();
134
+ render(
135
+ <ControlMatrix
136
+ schema={edge}
137
+ columns={[
138
+ { key: 'a', label: 'A', title: 'Alpha' },
139
+ { key: 'b', label: 'B', title: 'Beta' },
140
+ ]}
141
+ rows={[{ key: 'edge' }]}
142
+ config={{ a: { edge: null }, b: { edge: '#ff0000' } }}
143
+ auto={new Set()}
144
+ setConfig={setConfig}
145
+ />,
146
+ );
147
+ expect(screen.getByRole('button', { name: 'Alpha Edge, unset' })).toBeTruthy();
148
+ fireEvent.click(screen.getByRole('button', { name: 'Beta Edge' }));
149
+ fireEvent.click(screen.getByRole('button', { name: 'Clear' }));
150
+ expect(setConfig).toHaveBeenCalledWith('b.edge', null);
151
+ });
152
+
153
+ it('edits any other cell in a popover holding the panel control, which pins it', () => {
154
+ const { setConfig } = setup();
155
+ fireEvent.click(cell('Window Glow'));
156
+ const dialog = screen.getByRole('dialog', { name: 'Window Glow' });
157
+ fireEvent.change(within(dialog).getByRole('slider'), { target: { value: '0.75' } });
158
+ expect(setConfig).toHaveBeenCalledWith('looks.window.glow', 0.75);
159
+ });
160
+
161
+ it('unpins from the popover with Inherit, disabled while already inherited', () => {
162
+ const { setConfig, unmount } = setup();
163
+ fireEvent.click(cell('Window Glow'));
164
+ const inherit = screen.getByRole('button', { name: 'Inherit' });
165
+ expect(inherit).not.toBeDisabled();
166
+ fireEvent.click(inherit);
167
+ expect(setConfig).toHaveBeenCalledWith('looks.window.glow', auto);
168
+ unmount();
169
+
170
+ setup();
171
+ fireEvent.click(cell('Arrow Glow'));
172
+ expect(screen.getByRole('button', { name: 'Inherit' })).toBeDisabled();
173
+ });
174
+
175
+ it('has no Inherit for the fallback column', () => {
176
+ setup();
177
+ fireEvent.click(cell('Defaults Glow'));
178
+ expect(screen.getByRole('dialog', { name: 'Defaults Glow' })).toBeTruthy();
179
+ expect(screen.queryByRole('button', { name: 'Inherit' })).toBeNull();
180
+ });
181
+
182
+ it('unpins a pinned cell on Alt-click, without flipping or opening it', () => {
183
+ const { setConfig } = setup();
184
+ fireEvent.click(cell('Window Glow'), { altKey: true });
185
+ expect(setConfig).toHaveBeenCalledWith('looks.window.glow', auto);
186
+ expect(screen.queryByRole('dialog')).toBeNull();
187
+
188
+ setConfig.mockClear();
189
+ fireEvent.click(screen.getByRole('switch', { name: 'Defaults Background' }), { altKey: true });
190
+ fireEvent.click(screen.getByRole('switch', { name: /^Window Background/ }), { altKey: true });
191
+ // The fallback cannot inherit, and an inherited cell is already unpinned.
192
+ expect(setConfig).not.toHaveBeenCalled();
193
+ });
194
+
195
+ it('calls onColumnClick from a header button', () => {
196
+ const onColumnClick = vi.fn();
197
+ setup({ onColumnClick });
198
+ fireEvent.click(screen.getByRole('button', { name: 'Window' }));
199
+ expect(onColumnClick).toHaveBeenCalledWith(columns[1]);
200
+ });
201
+
202
+ it('draws plain headers without onColumnClick', () => {
203
+ setup();
204
+ expect(screen.queryByRole('button', { name: 'Window' })).toBeNull();
205
+ });
206
+ });