@weasel-js/labkit 1.6.0 → 1.7.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 (222) hide show
  1. package/dist/_dts/{CanvasStackContext-DQgcH1ny.d.ts → CanvasStackContext-D2jnIwfk.d.ts} +1 -1
  2. package/dist/_dts/FloatingPanel-7j0IcXwz.d.ts +30 -0
  3. package/dist/_dts/{LayerList.d-2c9yup5Y.d.ts → LayerList.d-DiuEArKq.d.ts} +4 -4
  4. package/dist/_dts/PrefsRow.d-TOZ7BgD9.d.ts +79 -0
  5. package/dist/_dts/PropertyPanel.d-D-1paCbE.d.ts +141 -0
  6. package/dist/_dts/{Select.d-BchGMC2Y.d.ts → Select.d-Bensr_MT.d.ts} +15 -6
  7. package/dist/_dts/Stage-CCWfQQWM.d.ts +202 -0
  8. package/dist/_dts/index-B2Yj5aA0.d.ts +274 -0
  9. package/dist/_dts/{index-B0--8PEM.d.ts → index-CsYqUHD3.d.ts} +12 -13
  10. package/dist/_dts/{types-B7y8hX1D.d.ts → types-CRPSGFAp.d.ts} +33 -5
  11. package/dist/_dts/{frac-DTRq7Xpb.d.ts → types-CSm9jgol.d.ts} +700 -650
  12. package/dist/_dts/{useTrialState-CP4jfuKl.d.ts → useTrialState-CTZPgeZ7.d.ts} +2 -3
  13. package/dist/canvas/index.d.ts +40 -8
  14. package/dist/canvas/index.js +5 -3
  15. package/dist/chrome/index.d.ts +5 -6
  16. package/dist/chrome/index.js +9 -7
  17. package/dist/chunk-24W5VEJW.js +18 -0
  18. package/dist/chunk-24W5VEJW.js.map +1 -0
  19. package/dist/chunk-275JRRVD.js +728 -0
  20. package/dist/chunk-275JRRVD.js.map +1 -0
  21. package/dist/chunk-3RY6WL5T.js +38 -0
  22. package/dist/chunk-3RY6WL5T.js.map +1 -0
  23. package/dist/chunk-4LJSC7MB.js +143 -0
  24. package/dist/chunk-4LJSC7MB.js.map +1 -0
  25. package/dist/{chunk-3FI7Y4R4.js → chunk-7BO7HNGF.js} +5 -5
  26. package/dist/{chunk-3FI7Y4R4.js.map → chunk-7BO7HNGF.js.map} +1 -1
  27. package/dist/chunk-AWQG7FCC.js +19 -0
  28. package/dist/chunk-AWQG7FCC.js.map +1 -0
  29. package/dist/chunk-CZZPXUOU.js +3 -0
  30. package/dist/{chunk-SBXNQW4G.js.map → chunk-CZZPXUOU.js.map} +1 -1
  31. package/dist/{chunk-ZORVNOKX.js → chunk-FMK7UQ6V.js} +24 -145
  32. package/dist/chunk-FMK7UQ6V.js.map +1 -0
  33. package/dist/{chunk-BVL7Y574.js → chunk-FZ7YFQN7.js} +26 -10
  34. package/dist/chunk-FZ7YFQN7.js.map +1 -0
  35. package/dist/{chunk-USO6TERP.js → chunk-GOZLNM4L.js} +11 -9
  36. package/dist/chunk-GOZLNM4L.js.map +1 -0
  37. package/dist/{chunk-H5VIV7MQ.js → chunk-I5DUQ5HM.js} +88 -82
  38. package/dist/chunk-I5DUQ5HM.js.map +1 -0
  39. package/dist/chunk-J7OWWTCW.js +143 -0
  40. package/dist/chunk-J7OWWTCW.js.map +1 -0
  41. package/dist/{chunk-H6ZAOWNE.js → chunk-JVBYV6TL.js} +3 -3
  42. package/dist/{chunk-H6ZAOWNE.js.map → chunk-JVBYV6TL.js.map} +1 -1
  43. package/dist/{chunk-VQ47KWD5.js → chunk-KMJ7RWD3.js} +18 -7
  44. package/dist/chunk-KMJ7RWD3.js.map +1 -0
  45. package/dist/{chunk-ERFMC6WU.js → chunk-M7HP3FTK.js} +20 -11
  46. package/dist/chunk-M7HP3FTK.js.map +1 -0
  47. package/dist/chunk-MIM5R4ZT.js +14972 -0
  48. package/dist/chunk-MIM5R4ZT.js.map +1 -0
  49. package/dist/{chunk-HFRE4QQV.js → chunk-T5IBSPAM.js} +96 -32
  50. package/dist/chunk-T5IBSPAM.js.map +1 -0
  51. package/dist/chunk-UNAZSLWE.js +294 -0
  52. package/dist/chunk-UNAZSLWE.js.map +1 -0
  53. package/dist/{chunk-RS3HRQWU.js → chunk-WPLAANH4.js} +4 -3
  54. package/dist/chunk-WPLAANH4.js.map +1 -0
  55. package/dist/{chunk-AWH3SRPW.js → chunk-Y23G2VZL.js} +5 -37
  56. package/dist/chunk-Y23G2VZL.js.map +1 -0
  57. package/dist/{chunk-MOLIUNVH.js → chunk-YT75VYRD.js} +4 -4
  58. package/dist/{chunk-MOLIUNVH.js.map → chunk-YT75VYRD.js.map} +1 -1
  59. package/dist/chunk-ZIFPHIWZ.js +302 -0
  60. package/dist/chunk-ZIFPHIWZ.js.map +1 -0
  61. package/dist/config/index.d.ts +48 -40
  62. package/dist/config/index.js +4 -5
  63. package/dist/controls/index.d.ts +91 -7
  64. package/dist/controls/index.js +5 -4
  65. package/dist/dragdrop/index.d.ts +9 -7
  66. package/dist/dragdrop/index.js +3 -2
  67. package/dist/index.d.ts +294 -82
  68. package/dist/index.js +425 -402
  69. package/dist/index.js.map +1 -1
  70. package/dist/layers/index.d.ts +4 -5
  71. package/dist/layers/index.js +2 -1
  72. package/dist/loupe/index.d.ts +20 -17
  73. package/dist/loupe/index.js +5 -3
  74. package/dist/overview/index.d.ts +56 -0
  75. package/dist/overview/index.js +289 -0
  76. package/dist/overview/index.js.map +1 -0
  77. package/dist/passthrough/weasel-ui.d.ts +239 -69
  78. package/dist/passthrough/weasel-ui.js +3 -2
  79. package/dist/primitives/index.d.ts +5 -204
  80. package/dist/primitives/index.js +6 -4
  81. package/dist/state/index.d.ts +8 -8
  82. package/dist/state/index.js +4 -5
  83. package/dist/state/index.js.map +1 -1
  84. package/dist/styles.css +152 -82
  85. package/dist/undo/index.d.ts +4 -4
  86. package/package.json +17 -12
  87. package/src/annotations/AnnotationOverlay.tsx +2 -2
  88. package/src/annotations/Annotations.less +0 -10
  89. package/src/annotations/MarkList.tsx +4 -8
  90. package/src/annotations/index.ts +1 -0
  91. package/src/annotations/paint.test.ts +3 -14
  92. package/src/annotations/paint.ts +3 -19
  93. package/src/annotations/store.test.ts +26 -0
  94. package/src/annotations/store.ts +25 -6
  95. package/src/annotations/svgNodes.test.ts +6 -9
  96. package/src/annotations/svgNodes.ts +3 -3
  97. package/src/annotations/toolMap.test.ts +37 -2
  98. package/src/annotations/toolMap.ts +13 -0
  99. package/src/annotations/types.ts +27 -0
  100. package/src/canvas/AGENTS.md +9 -10
  101. package/src/canvas/CameraInput.test.tsx +136 -0
  102. package/src/canvas/CameraInput.tsx +322 -0
  103. package/src/canvas/CanvasStack.tsx +47 -58
  104. package/src/canvas/LinkedCursor.less +16 -0
  105. package/src/canvas/LinkedCursor.tsx +54 -0
  106. package/src/canvas/Stage.test.tsx +33 -4
  107. package/src/canvas/Stage.tsx +47 -37
  108. package/src/canvas/cameraRegistry.test.ts +55 -0
  109. package/src/canvas/cameraRegistry.ts +86 -0
  110. package/src/canvas/cameraView.test.ts +60 -0
  111. package/src/canvas/cameraView.ts +61 -0
  112. package/src/canvas/cameraZoom.test.ts +46 -0
  113. package/src/canvas/cameraZoom.ts +24 -0
  114. package/src/canvas/index.ts +23 -2
  115. package/src/canvas/sharedScope.test.tsx +94 -0
  116. package/src/chrome/ChromeRegions.stories.tsx +1 -0
  117. package/src/chrome/regions/PaletteRegion.less +0 -7
  118. package/src/chrome/regions/SidebarRegion.less +12 -2
  119. package/src/chrome/regions/SidebarRegion.test.tsx +9 -2
  120. package/src/config/builder.test.ts +2 -1
  121. package/src/config/builder.ts +15 -8
  122. package/src/config/entry.test.ts +3 -1
  123. package/src/config/fromConfigField.test.ts +14 -0
  124. package/src/config/fromConfigField.ts +19 -4
  125. package/src/config/index.ts +4 -1
  126. package/src/config/path.ts +2 -5
  127. package/src/config/resolve.ts +15 -1
  128. package/src/config/sectionTree.test.ts +12 -6
  129. package/src/config/sectionTree.ts +15 -5
  130. package/src/config/types.ts +34 -9
  131. package/src/controls/ControlPanel.less +1 -1
  132. package/src/controls/ControlPanel.stories.tsx +3 -2
  133. package/src/controls/ControlPanel.test.tsx +76 -13
  134. package/src/controls/ControlPanel.tsx +80 -11
  135. package/src/controls/inDialog.test.tsx +18 -0
  136. package/src/controls/inDialog.tsx +2 -10
  137. package/src/index.test.ts +1 -1
  138. package/src/index.ts +23 -5
  139. package/src/instrument/SineWave.smoke.test.tsx +2 -0
  140. package/src/instrument/types.ts +10 -1
  141. package/src/lab/Lab.nebula.test.tsx +11 -2
  142. package/src/lab/Lab.test.tsx +3 -1
  143. package/src/lab/Lab.tsx +87 -76
  144. package/src/lab/{LabFit.stories.less → LabFit.browser.test.less} +1 -1
  145. package/src/lab/{LabFit.stories.tsx → LabFit.browser.test.tsx} +41 -63
  146. package/src/lab/LabFullChrome.stories.tsx +1 -0
  147. package/src/lab/LabHeader.test.tsx +24 -14
  148. package/src/lab/LabHeader.tsx +11 -12
  149. package/src/lab/LabShell.less +8 -17
  150. package/src/lab/LabSwitcher.less +6 -6
  151. package/src/lab/LabZoom.test.tsx +223 -0
  152. package/src/lab/LabZoom.tsx +170 -0
  153. package/src/lab/index.ts +1 -0
  154. package/src/loupe/AGENTS.md +7 -6
  155. package/src/loupe/LoupeGestures.tsx +15 -10
  156. package/src/loupe/TrialLoupe.tsx +5 -5
  157. package/src/loupe/index.ts +1 -0
  158. package/src/loupe/loupeActions.ts +6 -2
  159. package/src/overview/AGENTS.md +38 -0
  160. package/src/overview/OverviewMarks.test.tsx +100 -0
  161. package/src/overview/OverviewMarks.tsx +93 -0
  162. package/src/overview/TrialOverview.less +53 -0
  163. package/src/overview/TrialOverview.stories.tsx +50 -0
  164. package/src/overview/TrialOverview.test.tsx +116 -0
  165. package/src/overview/TrialOverview.tsx +376 -0
  166. package/src/overview/index.ts +2 -0
  167. package/src/passthrough/weasel-ui.test.ts +2 -0
  168. package/src/passthrough/weasel-ui.ts +23 -4
  169. package/src/primitives/FloatingPanel.test.tsx +11 -0
  170. package/src/primitives/FloatingPanel.tsx +2 -5
  171. package/src/primitives/JobProgress.less +1 -1
  172. package/src/primitives/Legend.stories.tsx +1 -0
  173. package/src/primitives/Readout.browser.test.tsx +137 -0
  174. package/src/primitives/Readout.less +12 -0
  175. package/src/primitives/Readout.stories.tsx +58 -0
  176. package/src/primitives/Readout.test.tsx +93 -0
  177. package/src/primitives/Readout.tsx +56 -0
  178. package/src/primitives/index.ts +2 -0
  179. package/src/specimen/Specimen.stories.tsx +1 -0
  180. package/src/specimen/Specimen.tsx +21 -4
  181. package/src/state/Persistence.browser.test.tsx +47 -0
  182. package/src/state/labRecords.test.ts +4 -1
  183. package/src/state/store.test.ts +9 -5
  184. package/src/state/usePersistedState.test.tsx +1 -1
  185. package/src/state/useTrialState.test.tsx +1 -1
  186. package/src/styles.less +3 -0
  187. package/src/theme/Interstellar.stories.tsx +4 -6
  188. package/src/theme/base.less +21 -47
  189. package/src/theme/interstellar.test.ts +1 -1
  190. package/src/theme/interstellar.theme.json +10 -3
  191. package/src/tools/labTool.ts +6 -2
  192. package/src/trial/Trial.annotations.test.tsx +16 -0
  193. package/src/trial/Trial.less +0 -13
  194. package/src/trial/Trial.loupe.test.tsx +73 -1
  195. package/src/trial/Trial.stories.tsx +2 -0
  196. package/src/trial/Trial.tsx +23 -1
  197. package/src/trial/TrialChrome.tsx +21 -3
  198. package/src/trial/trialOps.test.ts +4 -4
  199. package/dist/_dts/PrefsRow.d-DFpPIeIC.d.ts +0 -39
  200. package/dist/_dts/inDialog-D5ZKGjiD.d.ts +0 -23
  201. package/dist/_dts/index-DINmCuLA.d.ts +0 -453
  202. package/dist/_dts/number.d-OeQYQoUO.d.ts +0 -28
  203. package/dist/_dts/usePanZoom-Bq73pwFe.d.ts +0 -117
  204. package/dist/chunk-2AYGEN57.js +0 -32
  205. package/dist/chunk-2AYGEN57.js.map +0 -1
  206. package/dist/chunk-AWH3SRPW.js.map +0 -1
  207. package/dist/chunk-BVL7Y574.js.map +0 -1
  208. package/dist/chunk-ERFMC6WU.js.map +0 -1
  209. package/dist/chunk-H5VIV7MQ.js.map +0 -1
  210. package/dist/chunk-HFRE4QQV.js.map +0 -1
  211. package/dist/chunk-RS3HRQWU.js.map +0 -1
  212. package/dist/chunk-SBXNQW4G.js +0 -3
  213. package/dist/chunk-TT5C5S6R.js +0 -368
  214. package/dist/chunk-TT5C5S6R.js.map +0 -1
  215. package/dist/chunk-USO6TERP.js.map +0 -1
  216. package/dist/chunk-VQ47KWD5.js.map +0 -1
  217. package/dist/chunk-WUJXJC5O.js +0 -14568
  218. package/dist/chunk-WUJXJC5O.js.map +0 -1
  219. package/dist/chunk-ZORVNOKX.js.map +0 -1
  220. package/src/canvas/usePanZoom.test.ts +0 -244
  221. package/src/canvas/usePanZoom.ts +0 -147
  222. package/src/state/Persistence.stories.tsx +0 -75
@@ -23,6 +23,7 @@
23
23
  --_s-tone: var(--wzl-tone, var(--_s-surface));
24
24
  --_s-accent: var(--wzl-tone);
25
25
  --_s-title-font: var(--wzl-font-display);
26
+ --_s-title-weight: var(--wzl-font-weight-bold);
26
27
  --_s-title-size: var(--wzl-font-size-sm);
27
28
  --_s-title-case: uppercase;
28
29
  --_s-title-tracking: 0.08em;
@@ -36,6 +37,7 @@
36
37
  --_s-tone: var(--wzl-tone, var(--wzl-stance-scope-tone, var(--_s-surface)));
37
38
  --_s-accent: var(--wzl-stance-scope-accent, var(--wzl-tone));
38
39
  --_s-title-font: var(--wzl-stance-scope-title-font, var(--wzl-font-display));
40
+ --_s-title-weight: var(--wzl-stance-scope-title-weight, var(--wzl-font-weight-bold));
39
41
  --_s-title-size: var(--wzl-stance-scope-title-size, var(--wzl-font-size-sm));
40
42
  --_s-title-case: var(--wzl-stance-scope-title-case, uppercase);
41
43
  --_s-title-tracking: var(--wzl-stance-scope-title-tracking, 0.08em);
@@ -49,6 +51,7 @@
49
51
  --_s-tone: var(--wzl-tone, var(--wzl-stance-aside-tone, var(--_s-surface)));
50
52
  --_s-accent: var(--wzl-stance-aside-accent, var(--wzl-tone));
51
53
  --_s-title-font: var(--wzl-stance-aside-title-font, var(--wzl-font-display));
54
+ --_s-title-weight: var(--wzl-stance-aside-title-weight, var(--wzl-font-weight-bold));
52
55
  --_s-title-size: var(--wzl-stance-aside-title-size, var(--wzl-font-size-sm));
53
56
  --_s-title-case: var(--wzl-stance-aside-title-case, uppercase);
54
57
  --_s-title-tracking: var(--wzl-stance-aside-title-tracking, 0.08em);
@@ -62,6 +65,7 @@
62
65
  --_s-tone: var(--wzl-tone, var(--wzl-stance-advanced-tone, var(--_s-surface)));
63
66
  --_s-accent: var(--wzl-stance-advanced-accent, var(--wzl-tone));
64
67
  --_s-title-font: var(--wzl-stance-advanced-title-font, var(--wzl-font-display));
68
+ --_s-title-weight: var(--wzl-stance-advanced-title-weight, var(--wzl-font-weight-bold));
65
69
  --_s-title-size: var(--wzl-stance-advanced-title-size, var(--wzl-font-size-sm));
66
70
  --_s-title-case: var(--wzl-stance-advanced-title-case, uppercase);
67
71
  --_s-title-tracking: var(--wzl-stance-advanced-title-tracking, 0.08em);
@@ -75,6 +79,7 @@
75
79
  --_s-tone: var(--wzl-tone, var(--wzl-stance-debug-tone, var(--_s-surface)));
76
80
  --_s-accent: var(--wzl-stance-debug-accent, var(--wzl-tone));
77
81
  --_s-title-font: var(--wzl-stance-debug-title-font, var(--wzl-font-display));
82
+ --_s-title-weight: var(--wzl-stance-debug-title-weight, var(--wzl-font-weight-bold));
78
83
  --_s-title-size: var(--wzl-stance-debug-title-size, var(--wzl-font-size-sm));
79
84
  --_s-title-case: var(--wzl-stance-debug-title-case, uppercase);
80
85
  --_s-title-tracking: var(--wzl-stance-debug-title-tracking, 0.08em);
@@ -88,6 +93,7 @@
88
93
  --_s-tone: var(--wzl-tone, var(--wzl-stance-danger-tone, var(--_s-surface)));
89
94
  --_s-accent: var(--wzl-stance-danger-accent, var(--wzl-tone));
90
95
  --_s-title-font: var(--wzl-stance-danger-title-font, var(--wzl-font-display));
96
+ --_s-title-weight: var(--wzl-stance-danger-title-weight, var(--wzl-font-weight-bold));
91
97
  --_s-title-size: var(--wzl-stance-danger-title-size, var(--wzl-font-size-sm));
92
98
  --_s-title-case: var(--wzl-stance-danger-title-case, uppercase);
93
99
  --_s-title-tracking: var(--wzl-stance-danger-title-tracking, 0.08em);
@@ -101,6 +107,7 @@
101
107
  --_s-tone: var(--wzl-tone, var(--wzl-stance-notice-tone, var(--_s-surface)));
102
108
  --_s-accent: var(--wzl-stance-notice-accent, var(--wzl-tone));
103
109
  --_s-title-font: var(--wzl-stance-notice-title-font, var(--wzl-font-display));
110
+ --_s-title-weight: var(--wzl-stance-notice-title-weight, var(--wzl-font-weight-bold));
104
111
  --_s-title-size: var(--wzl-stance-notice-title-size, var(--wzl-font-size-sm));
105
112
  --_s-title-case: var(--wzl-stance-notice-title-case, uppercase);
106
113
  --_s-title-tracking: var(--wzl-stance-notice-title-tracking, 0.08em);
@@ -114,6 +121,7 @@
114
121
  --_s-tone: var(--wzl-tone, var(--wzl-stance-important-tone, var(--_s-surface)));
115
122
  --_s-accent: var(--wzl-stance-important-accent, var(--wzl-tone));
116
123
  --_s-title-font: var(--wzl-stance-important-title-font, var(--wzl-font-display));
124
+ --_s-title-weight: var(--wzl-stance-important-title-weight, var(--wzl-font-weight-bold));
117
125
  --_s-title-size: var(--wzl-stance-important-title-size, var(--wzl-font-size-sm));
118
126
  --_s-title-case: var(--wzl-stance-important-title-case, uppercase);
119
127
  --_s-title-tracking: var(--wzl-stance-important-title-tracking, 0.08em);
@@ -127,6 +135,7 @@
127
135
  --_s-tone: var(--wzl-tone, var(--wzl-stance-preview-tone, var(--_s-surface)));
128
136
  --_s-accent: var(--wzl-stance-preview-accent, var(--wzl-tone));
129
137
  --_s-title-font: var(--wzl-stance-preview-title-font, var(--wzl-font-display));
138
+ --_s-title-weight: var(--wzl-stance-preview-title-weight, var(--wzl-font-weight-bold));
130
139
  --_s-title-size: var(--wzl-stance-preview-title-size, var(--wzl-font-size-sm));
131
140
  --_s-title-case: var(--wzl-stance-preview-title-case, uppercase);
132
141
  --_s-title-tracking: var(--wzl-stance-preview-title-tracking, 0.08em);
@@ -174,12 +183,13 @@
174
183
  .lk-sidebar-section__head {
175
184
  flex: 1;
176
185
  width: 100%;
177
- padding: var(--wzl-space-xs);
186
+ padding: var(--wzl-space-xs) var(--wzl-space-sm);
178
187
  border: 0;
179
188
  background: transparent;
180
189
  color: var(--_s-title-color);
181
190
  font-family: var(--_s-title-font);
182
191
  font-size: var(--_s-title-size);
192
+ font-weight: var(--_s-title-weight);
183
193
  letter-spacing: var(--_s-title-tracking);
184
194
  text-align: left;
185
195
  text-transform: var(--_s-title-case);
@@ -193,5 +203,5 @@
193
203
  // The section body is the only thing that insets a panel — the panels
194
204
  // themselves carry no padding, so this is the whole gutter.
195
205
  .lk-sidebar-section__body {
196
- padding: 0 var(--wzl-space-xs) var(--wzl-space-xs);
206
+ padding: 0 var(--wzl-space-sm) var(--wzl-space-sm);
197
207
  }
@@ -3,13 +3,20 @@ import { describe, expect, it } from 'vitest';
3
3
  import type { RegionContribution, TrialChromeContext } from '../types';
4
4
  import { SidebarRegion } from './SidebarRegion';
5
5
 
6
- const ctx = { collapsedSections: {}, setSectionCollapsed: () => {} } as unknown as TrialChromeContext;
6
+ const ctx = {
7
+ collapsedSections: {},
8
+ setSectionCollapsed: () => {},
9
+ } as unknown as TrialChromeContext;
7
10
 
8
11
  describe('<SidebarRegion> stance and tone', () => {
9
12
  it('puts a section’s stance and tone on the section', () => {
10
13
  const contributions: RegionContribution<TrialChromeContext>[] = [
11
14
  { id: 'a', region: 'sidebar', item: { title: 'Plain', body: 'x' } },
12
- { id: 'b', region: 'sidebar', item: { title: 'Debug', body: 'y', stance: 'debug', tone: '#224a63' } },
15
+ {
16
+ id: 'b',
17
+ region: 'sidebar',
18
+ item: { title: 'Debug', body: 'y', stance: 'debug', tone: '#224a63' },
19
+ },
13
20
  ];
14
21
  const { container } = render(<SidebarRegion contributions={contributions} ctx={ctx} />);
15
22
  const [plain, debug] = [...container.querySelectorAll<HTMLElement>('.lk-sidebar-section')];
@@ -1,3 +1,4 @@
1
+ import { compact } from '@weasel-js/quantity';
1
2
  import { describe, expect, it } from 'vitest';
2
3
  import { auto } from './auto';
3
4
  import { f } from './builder';
@@ -37,7 +38,7 @@ describe('builder', () => {
37
38
  });
38
39
 
39
40
  it('annotates a number with a display format', () => {
40
- expect(f.number(0).format('compact').annotations.format).toBe('compact');
41
+ expect(f.number(0).display(compact()).annotations.display).toEqual(compact());
41
42
  });
42
43
 
43
44
  it('keeps a custom node kind across a chain', () => {
@@ -1,5 +1,5 @@
1
- import type { PrefLeaf, PrefNumberFormat, PrefNumberUnit } from '@weasel-js/ui';
2
- import { type InDialogOptions, inDialog } from '../controls/inDialog';
1
+ import type { Display } from '@weasel-js/quantity';
2
+ import type { PrefLeaf, PrefNumberUnit } from '@weasel-js/ui';
3
3
  import { type Auto, isAuto } from './auto';
4
4
  import type {
5
5
  Annotations,
@@ -12,6 +12,7 @@ import type {
12
12
  ConfigSchema,
13
13
  ConfigShape,
14
14
  ControlRenderer,
15
+ InDialogOptions,
15
16
  InferConfig,
16
17
  NodeOptions,
17
18
  SectionOption,
@@ -89,13 +90,13 @@ export abstract class BaseNode<T> implements ConfigNode<T> {
89
90
 
90
91
  /** Draw this one row yourself, keeping the kind, default and validation. */
91
92
  render(renderer: ControlRenderer): this {
92
- return this.opt({ render: renderer });
93
+ return this.opt({ render: renderer, dialog: undefined });
93
94
  }
94
95
 
95
96
  /** Draw this row as a button that opens a dialog, with `body` as the
96
97
  * dialog's content — for an editor too big for the row. */
97
98
  dialog(body: ControlRenderer, opts?: InDialogOptions): this {
98
- return this.render(inDialog(body, opts));
99
+ return this.opt({ dialog: { ...opts, body }, render: undefined });
99
100
  }
100
101
 
101
102
  /**
@@ -141,10 +142,10 @@ export class NumberNode extends BaseNode<number> {
141
142
  return this.ann({ suffix });
142
143
  }
143
144
 
144
- /** Show the value in a named format — `'compact'` reads `2.00M`. Presentation
145
- * only: the stored value stays a plain number, and a typed `2.5m` reads back. */
146
- format(format: PrefNumberFormat): this {
147
- return this.ann({ format });
145
+ /** Show, speak and read the value through a display — `compact()` reads
146
+ * `2.00M`. Presentation only: the stored value stays a plain number. */
147
+ display(display: Display): this {
148
+ return this.ann({ display });
148
149
  }
149
150
 
150
151
  /**
@@ -286,6 +287,12 @@ export class GroupNode<S extends ConfigShape> implements ConfigBranch<S> {
286
287
  showIf(predicate: (config: Record<string, unknown>) => boolean): GroupNode<S> {
287
288
  return this.with(this.annotations, { ...this.options, showIf: predicate });
288
289
  }
290
+
291
+ /** Put a button on the group's heading that returns every value beneath it
292
+ * to its default, leaving the rest of the config alone. */
293
+ resettable(): GroupNode<S> {
294
+ return this.with(this.annotations, { ...this.options, resettable: true });
295
+ }
289
296
  }
290
297
 
291
298
  /** Every default in a shape, nested the way the shape is. */
@@ -39,7 +39,9 @@ describe('@weasel-js/labkit/config', () => {
39
39
  // A forge frame loads this entry for every story, and ui's barrel is 167 modules on the dev server.
40
40
  it('loads nothing from @weasel-js/ui at runtime', () => {
41
41
  const importers = [...walk(resolve(__dirname, 'index.ts'))].filter((file) =>
42
- [...readFileSync(file, 'utf8').matchAll(IMPORT)].some((m) => (m[1] ?? m[2] ?? m[3])?.startsWith('@weasel-js/ui')),
42
+ [...readFileSync(file, 'utf8').matchAll(IMPORT)].some((m) =>
43
+ (m[1] ?? m[2] ?? m[3])?.startsWith('@weasel-js/ui'),
44
+ ),
43
45
  );
44
46
  expect(importers).toEqual([]);
45
47
  });
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import type { ConfigField } from '../controls/types';
3
3
  import { fromConfigFields } from './fromConfigField';
4
+ import { schemaNodeAtPath } from './path';
4
5
  import { isLeafVisible } from './visible';
5
6
 
6
7
  const leaf = (fields: ConfigField[], k: string) =>
@@ -76,6 +77,19 @@ describe('fromConfigFields', () => {
76
77
  ]);
77
78
  expect(Object.keys(r.group.children)).toEqual(['z', 'a']);
78
79
  });
80
+
81
+ it('files a dotted key at the path it names, the one config reads and writes use', () => {
82
+ const r = fromConfigFields([
83
+ { type: 'number', key: 'pose.x', label: 'X', default: 0 },
84
+ { type: 'number', key: 'pose.y', label: 'Y', default: 0 },
85
+ { type: 'checkbox', key: 'on', label: 'On', default: true },
86
+ ]);
87
+ expect(schemaNodeAtPath(r.group, 'pose.x')).toMatchObject({ kind: 'number', name: 'X' });
88
+ expect(schemaNodeAtPath(r.group, 'pose.y')).toMatchObject({ kind: 'number', name: 'Y' });
89
+ // The intermediate is headless: a flat field list never asked for a heading.
90
+ expect(schemaNodeAtPath(r.group, 'pose')).toMatchObject({ name: '' });
91
+ expect(Object.keys(r.group.children)).toEqual(['pose', 'on']);
92
+ });
79
93
  });
80
94
 
81
95
  describe('isLeafVisible', () => {
@@ -1,4 +1,4 @@
1
- import type { PrefLeaf } from '@weasel-js/ui';
1
+ import type { PrefGroup, PrefLeaf } from '@weasel-js/ui';
2
2
  import type { ConfigField } from '../controls/types';
3
3
  import type { LeafPatch, ResolvedConfig } from './types';
4
4
 
@@ -53,9 +53,13 @@ function toLeaf(field: ConfigField): LeafPatch {
53
53
  *
54
54
  * Rules do not run here: a hand-written `ConfigField` already states its label
55
55
  * and control, which is everything the rule chain would have inferred.
56
+ *
57
+ * A dotted `key` is a config path, as it is to every read and write, so its
58
+ * leaf is filed under headless groups at that path rather than as one child
59
+ * whose name contains dots — which no path lookup would find.
56
60
  */
57
61
  export function fromConfigFields(fields: readonly ConfigField[]): ResolvedConfig {
58
- const children: Record<string, PrefLeaf> = {};
62
+ const root: PrefGroup = { name: '', children: {} };
59
63
  for (const field of fields) {
60
64
  const patch = toLeaf(field);
61
65
  for (const key of Object.keys(patch)) {
@@ -63,12 +67,23 @@ export function fromConfigFields(fields: readonly ConfigField[]): ResolvedConfig
63
67
  delete (patch as Record<string, unknown>)[key];
64
68
  }
65
69
  }
66
- children[field.key] = { ...patch, default: field.default } as PrefLeaf;
70
+ const segments = field.key.split('.');
71
+ const leafKey = segments.pop() as string;
72
+ let group = root;
73
+ for (const segment of segments) {
74
+ const existing = group.children[segment];
75
+ if (existing === undefined || !('children' in existing) || 'kind' in existing) {
76
+ group.children[segment] = { name: '', children: {} };
77
+ }
78
+ group = group.children[segment] as PrefGroup;
79
+ }
80
+ group.children[leafKey] = { ...patch, default: field.default } as PrefLeaf;
67
81
  }
68
82
  return {
69
- group: { name: '', children },
83
+ group: root,
70
84
  sections: [],
71
85
  showIf: new Map(),
72
86
  renderers: {},
87
+ dialogs: {},
73
88
  };
74
89
  }
@@ -23,8 +23,8 @@ export {
23
23
  withValueAtPath,
24
24
  } from './path';
25
25
  export { resolveConfigSchema } from './resolve';
26
- export { type SectionTree, sectionTree } from './sectionTree';
27
26
  export { applyRules, builtinRules, titleCase } from './rules';
27
+ export { type SectionTree, sectionTree } from './sectionTree';
28
28
  export type {
29
29
  Annotations,
30
30
  BranchAnnotations,
@@ -40,12 +40,15 @@ export type {
40
40
  ConfigSchema,
41
41
  ConfigShape,
42
42
  ControlRenderer,
43
+ DialogSpec,
43
44
  EntryValue,
45
+ InDialogOptions,
44
46
  InferConfig,
45
47
  LeafPatch,
46
48
  NodeOptions,
47
49
  NodeValue,
48
50
  ResolvedConfig,
51
+ SectionOption,
49
52
  SectionSpec,
50
53
  ValueAtPath,
51
54
  } from './types';
@@ -1,3 +1,4 @@
1
+ import { isPlainObject } from '@weasel-js/core';
1
2
  import type { PrefGroup, PrefLeaf } from '@weasel-js/ui';
2
3
 
3
4
  /**
@@ -9,11 +10,7 @@ import type { PrefGroup, PrefLeaf } from '@weasel-js/ui';
9
10
  /** A plain object a config tree can be walked into. An array, a `Date`, a
10
11
  * `Map` or a class instance is a leaf value: a walk that descended into one
11
12
  * would rebuild it as a bare `{}`. */
12
- export function isRecord(value: unknown): value is Record<string, unknown> {
13
- if (typeof value !== 'object' || value === null) return false;
14
- const proto = Object.getPrototypeOf(value);
15
- return proto === Object.prototype || proto === null;
16
- }
13
+ export const isRecord = isPlainObject;
17
14
 
18
15
  /** The value at a dotted path. `undefined` when a segment is missing or the
19
16
  * walk hits something that is not a record. */
@@ -7,6 +7,7 @@ import type {
7
7
  ConfigSchema,
8
8
  ConfigShape,
9
9
  ControlRenderer,
10
+ DialogSpec,
10
11
  LeafPatch,
11
12
  ResolvedConfig,
12
13
  SectionSpec,
@@ -25,6 +26,8 @@ interface Sink {
25
26
  sections: SectionSpec[];
26
27
  showIf: Map<string, (config: Record<string, unknown>) => boolean>;
27
28
  renderers: Record<string, ControlRenderer>;
29
+ dialogs: Record<string, DialogSpec>;
30
+ resettable: Set<string>;
28
31
  chain: readonly ConfigRule[];
29
32
  }
30
33
 
@@ -45,10 +48,19 @@ export function resolveConfigSchema<TC>(
45
48
  sections: [],
46
49
  showIf: new Map(),
47
50
  renderers: {},
51
+ dialogs: {},
52
+ resettable: new Set(),
48
53
  chain: [...rules, ...builtinRules],
49
54
  };
50
55
  const group = resolveShape(schema.nodes, '', '', sink);
51
- return { group, sections: sink.sections, showIf: sink.showIf, renderers: sink.renderers };
56
+ return {
57
+ group,
58
+ sections: sink.sections,
59
+ showIf: sink.showIf,
60
+ renderers: sink.renderers,
61
+ dialogs: sink.dialogs,
62
+ resettable: sink.resettable,
63
+ };
52
64
  }
53
65
 
54
66
  /** A section while it is still being filled. */
@@ -80,7 +92,9 @@ function resolveShape(shape: ConfigShape, at: string, name: string, sink: Sink):
80
92
  if (section.pack !== undefined && spec.pack === undefined) spec.pack = section.pack;
81
93
  }
82
94
  if (predicate) sink.showIf.set(path, predicate);
95
+ if (isConfigBranch(entry) && entry.options.resettable) sink.resettable.add(path);
83
96
  if (!isConfigBranch(entry) && entry.options.render) sink.renderers[path] = entry.options.render;
97
+ if (!isConfigBranch(entry) && entry.options.dialog) sink.dialogs[path] = entry.options.dialog;
84
98
  }
85
99
 
86
100
  return { name, children };
@@ -70,14 +70,19 @@ describe('sectionTree', () => {
70
70
  'weight',
71
71
  ]);
72
72
  const off = sectionTree(resolved, { lineNumbers: false, listing: { dim: 1, weight: 0 } });
73
- expect(Object.keys(groupAt(groupAt(off.group, 'view'), 'listing').children)).toEqual(['weight']);
73
+ expect(Object.keys(groupAt(groupAt(off.group, 'view'), 'listing').children)).toEqual([
74
+ 'weight',
75
+ ]);
74
76
  });
75
77
 
76
78
  it('drops a section nothing in it is showing, rather than opening it onto nothing', () => {
77
79
  const resolved = resolveConfigSchema(
78
80
  f.schema({
79
81
  mode: f.enum<string>('a', ['a', 'b']).section('View'),
80
- glow: f.number(1).section('Diff').showIf((c) => c.mode === 'b'),
82
+ glow: f
83
+ .number(1)
84
+ .section('Diff')
85
+ .showIf((c) => c.mode === 'b'),
81
86
  }),
82
87
  );
83
88
  expect(Object.keys(sectionTree(resolved, { mode: 'b', glow: 1 }).group.children)).toEqual([
@@ -91,7 +96,10 @@ describe('sectionTree', () => {
91
96
 
92
97
  it('keeps a hidden leaf out unless asked for it', () => {
93
98
  const resolved = resolveConfigSchema(
94
- f.schema({ seed: f.number(1).hidden().section('Debug'), route: f.boolean(true).section('Debug') }),
99
+ f.schema({
100
+ seed: f.number(1).hidden().section('Debug'),
101
+ route: f.boolean(true).section('Debug'),
102
+ }),
95
103
  );
96
104
  expect(Object.keys(groupAt(sectionTree(resolved, {}).group, 'debug').children)).toEqual([
97
105
  'route',
@@ -134,9 +142,7 @@ describe('sectionTree', () => {
134
142
  it('ignores a section nested under a group, which buckets rows in a pane', () => {
135
143
  const resolved = resolveConfigSchema(
136
144
  f.schema({
137
- cards: f
138
- .group({ floor: f.number(1).section('Size') })
139
- .section('View'),
145
+ cards: f.group({ floor: f.number(1).section('Size') }).section('View'),
140
146
  }),
141
147
  );
142
148
  const { group } = sectionTree(resolved, { cards: { floor: 1 } });
@@ -1,4 +1,4 @@
1
- import { isPrefLeaf, type PrefGroup } from '@weasel-js/ui';
1
+ import type { PrefGroup, PrefLeaf } from '@weasel-js/ui';
2
2
  import { valueAtPath } from './path';
3
3
  import type { ResolvedConfig } from './types';
4
4
  import { isLeafVisible } from './visible';
@@ -22,10 +22,20 @@ export interface SectionTree {
22
22
  pathAt: (railPath: string) => string;
23
23
  }
24
24
 
25
+ /** Structural rather than `isPrefLeaf`, so the config entry stays free of a
26
+ * runtime import from ui. */
27
+ function isLeaf(node: PrefLeaf | PrefGroup): node is PrefLeaf {
28
+ return 'kind' in node;
29
+ }
30
+
25
31
  /** A section label as an object key: no dots, since a dotted path is how every
26
32
  * caller addresses a node, and a label is free to hold one. */
27
33
  function slugOf(label: string, taken: ReadonlySet<string>): string {
28
- const base = label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'section';
34
+ const base =
35
+ label
36
+ .toLowerCase()
37
+ .replace(/[^a-z0-9]+/g, '-')
38
+ .replace(/^-|-$/g, '') || 'section';
29
39
  if (!taken.has(base)) return base;
30
40
  for (let n = 2; ; n += 1) if (!taken.has(`${base}-${n}`)) return `${base}-${n}`;
31
41
  }
@@ -44,7 +54,7 @@ function prune(
44
54
  for (const [key, child] of Object.entries(group.children)) {
45
55
  const path = at === '' ? key : `${at}.${key}`;
46
56
  if (!isLeafVisible(resolved, path, config, showHidden)) continue;
47
- if (isPrefLeaf(child)) {
57
+ if (isLeaf(child)) {
48
58
  children[key] = child;
49
59
  continue;
50
60
  }
@@ -81,7 +91,7 @@ export function sectionTree(
81
91
  claimed.add(path);
82
92
  const child = resolved.group.children[path];
83
93
  if (!child || !isLeafVisible(resolved, path, config, showHidden)) continue;
84
- if (isPrefLeaf(child)) {
94
+ if (isLeaf(child)) {
85
95
  kept[path] = child;
86
96
  } else {
87
97
  const subtree = prune(resolved, child, path, config, showHidden);
@@ -101,7 +111,7 @@ export function sectionTree(
101
111
  // gathers loose leaves into one item named for the root.
102
112
  for (const [key, child] of Object.entries(resolved.group.children)) {
103
113
  if (claimed.has(key) || !isLeafVisible(resolved, key, config, showHidden)) continue;
104
- if (isPrefLeaf(child)) {
114
+ if (isLeaf(child)) {
105
115
  children[key] = child;
106
116
  } else {
107
117
  const subtree = prune(resolved, child, key, config, showHidden);
@@ -1,10 +1,6 @@
1
- import type {
2
- PrefGroup,
3
- PrefLeaf,
4
- PrefNumberFormat,
5
- PrefNumberUnit,
6
- PrefRenderer,
7
- } from '@weasel-js/ui';
1
+ import type { Display } from '@weasel-js/quantity';
2
+ import type { PrefGroup, PrefLeaf, PrefNumberUnit, PrefRenderer } from '@weasel-js/ui';
3
+ import type { ReactNode } from 'react';
8
4
 
9
5
  /**
10
6
  * Renders the control cell for one config leaf. Identical to weasel-ui's
@@ -17,6 +13,9 @@ export type ControlRenderer = PrefRenderer;
17
13
  export interface ConfigOption {
18
14
  value: string;
19
15
  label: string;
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. */
18
+ icon?: string;
20
19
  }
21
20
 
22
21
  /** Everything a leaf can carry beyond its kind and default. A flat union of
@@ -31,8 +30,8 @@ export interface Annotations {
31
30
  max?: number;
32
31
  step?: number;
33
32
  suffix?: string;
34
- /** How a number's value is shown. `compact` abbreviates from a thousand up. */
35
- format?: PrefNumberFormat;
33
+ /** How a number's value shows, speaks and reads back — `compact()` reads `2.00M`. */
34
+ display?: Display;
36
35
  /** Display-unit conversion for a number stored in a canonical unit — the
37
36
  * value, its bounds and its step all convert at the control's edge. */
38
37
  unit?: PrefNumberUnit;
@@ -104,6 +103,26 @@ export interface ResolvedConfig {
104
103
  showIf: ReadonlyMap<string, (config: Record<string, unknown>) => boolean>;
105
104
  /** Node-level `.render` overrides, keyed by path. */
106
105
  renderers: Readonly<Record<string, ControlRenderer>>;
106
+ /** Node-level `.dialog` rows, keyed by path. */
107
+ dialogs: Readonly<Record<string, DialogSpec>>;
108
+ /** Paths of the groups marked `.resettable()`, whose heading carries a
109
+ * button that puts every value beneath it back to its default. */
110
+ resettable?: ReadonlySet<string>;
111
+ }
112
+
113
+ /** Options for a row drawn as a button that opens a dialog. */
114
+ export interface InDialogOptions {
115
+ /** What the row's button shows of the value. Defaults to a list joined with
116
+ * commas, or the value itself. */
117
+ summary?: (value: unknown) => ReactNode;
118
+ /** The dialog's heading. Defaults to the row's label. */
119
+ title?: ReactNode;
120
+ }
121
+
122
+ /** A `.dialog` row: `body` is the dialog's content, handed the context a row
123
+ * renderer gets. */
124
+ export interface DialogSpec extends InDialogOptions {
125
+ body: ControlRenderer;
107
126
  }
108
127
 
109
128
  /** How a node names the section it belongs to: the heading, and how that
@@ -121,6 +140,9 @@ export interface SectionOption {
121
140
  /** Per-node extras that do not belong on a `PrefLeaf`. */
122
141
  export interface NodeOptions extends BranchOptions {
123
142
  render?: ControlRenderer;
143
+ /** Draw the row as a button that opens a dialog. Set by `.dialog`; the
144
+ * config entry records it, and `ControlPanel` draws it. */
145
+ dialog?: DialogSpec;
124
146
  /** Errors for this leaf under `config`, the instrument's whole config. */
125
147
  validate?: (leaf: PrefLeaf, config: Record<string, unknown>) => string[];
126
148
  /** Computes this leaf's value while it is auto. Attaching one is what gives
@@ -141,6 +163,9 @@ export interface BranchOptions {
141
163
  /** Show this node only while the predicate holds. On a group it hides the
142
164
  * whole subtree; the values stay in config either way. */
143
165
  showIf?: (config: Record<string, unknown>) => boolean;
166
+ /** Put a button on this group's heading that returns every value beneath it
167
+ * to its default. Groups only. */
168
+ resettable?: boolean;
144
169
  }
145
170
 
146
171
  /** A group's own annotations: what it is called and, optionally, why. */
@@ -17,7 +17,7 @@
17
17
  .lk-pair-cell {
18
18
  display: inline-flex;
19
19
  align-items: center;
20
- gap: var(--wzl-space-3, 6px);
20
+ gap: var(--wzl-space-3);
21
21
  min-width: 0;
22
22
  }
23
23
 
@@ -1,5 +1,6 @@
1
1
  import { ANGLE_RADIANS, prefUnit } from '@weasel-js/core';
2
2
  import type { Meta, StoryObj } from '@weasel-js/forge';
3
+ import { compact } from '@weasel-js/quantity';
3
4
  import { type PrefNumberUnit, PropertyRow } from '@weasel-js/ui';
4
5
  import { useState } from 'react';
5
6
  import { f } from '../config/builder';
@@ -278,7 +279,7 @@ export const Presentation: Story = {
278
279
 
279
280
  const inline = f.schema({
280
281
  folderPad: f.number(1.1).range(0, 4).step(0.1).label('Folder pad'),
281
- glyphs: f.number(1_000_000).range(0, 2_000_000).format('compact'),
282
+ glyphs: f.number(1_000_000).range(0, 2_000_000).display(compact()),
282
283
  names: f.number(60).range(0, 100),
283
284
  labels: f.number(0).range(0, 100),
284
285
  placement: f.enum('spread', ['spread', 'stack', 'ring']),
@@ -337,7 +338,7 @@ const sidebarSliders = f.schema({
337
338
  pad: f.number(0.5).range(0, 4).step(0.25).label('Window pad'),
338
339
  folderPad: f.number(1).range(0, 4).step(0.1).label('Folder pad'),
339
340
  levels: f.number(2).range(0, 6).step(1).label('Levels'),
340
- glyphs: f.number(1_000_000).range(0, 2_000_000).step(2000).format('compact').label('Glyphs'),
341
+ glyphs: f.number(1_000_000).range(0, 2_000_000).step(2000).display(compact()).label('Glyphs'),
341
342
  names: f.number(40).range(0, 200).step(5).label('Names'),
342
343
  labels: f.number(0).range(0, 30).step(1).label('Labels'),
343
344
  text: f.number(100).range(50, 250).step(5).unit(shownIn('%')).label('Text'),