@weasel-js/labkit 1.3.0-pre.0 → 1.4.0-pre.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 (259) hide show
  1. package/dist/_dts/CanvasStackContext-DtAn_3O-.d.ts +42 -0
  2. package/dist/_dts/DrawCommand-B3bskUsC.d.ts +564 -0
  3. package/dist/_dts/{PrefsForm-BYa6cWnO.d.ts → PrefsForm-BkUJZx0A.d.ts} +4 -1
  4. package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +21 -0
  5. package/dist/_dts/frac-Cp3NivlC.d.ts +728 -0
  6. package/dist/_dts/index-BDVzvRzQ.d.ts +236 -0
  7. package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +87 -0
  8. package/dist/_dts/{DrawCommand-DBB45NfN.d.ts → types-C-gh9Ap-.d.ts} +1 -582
  9. package/dist/_dts/{types-AHlQxBNN.d.ts → types-D6s4b7if.d.ts} +2 -2
  10. package/dist/_dts/{types-1Sdxy_Pv.d.ts → types-DYMaEvM5.d.ts} +26 -1
  11. package/dist/_dts/{useTrialState-CsGMjhu9.d.ts → useTrialState-gmMvZqPc.d.ts} +7 -2
  12. package/dist/canvas/index.d.ts +43 -25
  13. package/dist/canvas/index.js +5 -3
  14. package/dist/chrome/index.d.ts +9 -6
  15. package/dist/chrome/index.js +7 -6
  16. package/dist/chunk-2AYGEN57.js +32 -0
  17. package/dist/chunk-2AYGEN57.js.map +1 -0
  18. package/dist/{chunk-RVNN4CHQ.js → chunk-3P5TTTA7.js} +16 -8
  19. package/dist/chunk-3P5TTTA7.js.map +1 -0
  20. package/dist/{chunk-NS54R52R.js → chunk-3ZELRRBV.js} +45 -40
  21. package/dist/chunk-3ZELRRBV.js.map +1 -0
  22. package/dist/{chunk-HFDVGF4X.js → chunk-4EY67BKJ.js} +32 -7
  23. package/dist/chunk-4EY67BKJ.js.map +1 -0
  24. package/dist/chunk-5TCEVMPI.js +140 -0
  25. package/dist/chunk-5TCEVMPI.js.map +1 -0
  26. package/dist/{chunk-O7NVSCLN.js → chunk-6JMU56CU.js} +133 -134
  27. package/dist/chunk-6JMU56CU.js.map +1 -0
  28. package/dist/{chunk-CPUJ3QXL.js → chunk-CQLQPQ4P.js} +2 -2
  29. package/dist/chunk-CQLQPQ4P.js.map +1 -0
  30. package/dist/chunk-CTRKTLYZ.js +33 -0
  31. package/dist/chunk-CTRKTLYZ.js.map +1 -0
  32. package/dist/chunk-EQIB2PGC.js +458 -0
  33. package/dist/chunk-EQIB2PGC.js.map +1 -0
  34. package/dist/chunk-HEORBLVT.js +7128 -0
  35. package/dist/chunk-HEORBLVT.js.map +1 -0
  36. package/dist/{chunk-V7PRSIRQ.js → chunk-JDDIZSLL.js} +4 -4
  37. package/dist/chunk-JDDIZSLL.js.map +1 -0
  38. package/dist/chunk-LITE5YFB.js +669 -0
  39. package/dist/chunk-LITE5YFB.js.map +1 -0
  40. package/dist/{chunk-Z6HJ5FEM.js → chunk-ROQDRXKW.js} +732 -315
  41. package/dist/chunk-ROQDRXKW.js.map +1 -0
  42. package/dist/chunk-SBXNQW4G.js +3 -0
  43. package/dist/chunk-SBXNQW4G.js.map +1 -0
  44. package/dist/{chunk-4TMMVIDM.js → chunk-XJ6N32QP.js} +50 -6
  45. package/dist/chunk-XJ6N32QP.js.map +1 -0
  46. package/dist/chunk-YQJNT4XQ.js +293 -0
  47. package/dist/chunk-YQJNT4XQ.js.map +1 -0
  48. package/dist/controls/index.d.ts +4 -28
  49. package/dist/controls/index.js +3 -3
  50. package/dist/dragdrop/index.d.ts +50 -5
  51. package/dist/dragdrop/index.js +3 -1
  52. package/dist/index.d.ts +660 -302
  53. package/dist/index.js +1793 -587
  54. package/dist/index.js.map +1 -1
  55. package/dist/layers/index.d.ts +6 -5
  56. package/dist/layers/index.js +4 -2
  57. package/dist/loupe/index.d.ts +187 -0
  58. package/dist/loupe/index.js +7 -0
  59. package/dist/loupe/index.js.map +1 -0
  60. package/dist/passthrough/weasel-canvas.d.ts +3 -1
  61. package/dist/passthrough/weasel-canvas.js +1 -1
  62. package/dist/passthrough/weasel-ui.d.ts +151 -91
  63. package/dist/passthrough/weasel-ui.js +3 -2
  64. package/dist/primitives/index.d.ts +3 -1
  65. package/dist/primitives/index.js +6 -5
  66. package/dist/state/index.d.ts +3 -3
  67. package/dist/state/index.js +2 -2
  68. package/dist/styles.css +229 -703
  69. package/dist/surface/index.d.ts +18 -2
  70. package/dist/surface/index.js +2 -2
  71. package/dist/ui/layers/index.d.ts +8 -2
  72. package/dist/ui/layers/index.js +2 -4
  73. package/dist/undo/index.d.ts +5 -4
  74. package/package.json +14 -7
  75. package/src/annotations/AnnotationOverlay.tsx +205 -0
  76. package/src/annotations/AnnotationTargets.tsx +37 -0
  77. package/src/annotations/Annotations.less +129 -0
  78. package/src/annotations/Annotations.meaning.test.tsx +111 -0
  79. package/src/annotations/Annotations.overlay.test.tsx +142 -0
  80. package/src/annotations/AnnotationsContext.ts +49 -0
  81. package/src/annotations/ExportMenu.test.tsx +86 -0
  82. package/src/annotations/ExportMenu.tsx +182 -0
  83. package/src/annotations/MarkList.tsx +75 -0
  84. package/src/annotations/capture.test.ts +140 -0
  85. package/src/annotations/capture.ts +237 -0
  86. package/src/annotations/drawOne.test.ts +56 -0
  87. package/src/annotations/drawOne.ts +47 -0
  88. package/src/annotations/frac.test.ts +59 -0
  89. package/src/annotations/frac.ts +66 -0
  90. package/src/annotations/history.test.ts +82 -0
  91. package/src/annotations/history.ts +118 -0
  92. package/src/annotations/index.ts +37 -0
  93. package/src/annotations/paint.test.ts +125 -0
  94. package/src/annotations/paint.ts +130 -0
  95. package/src/annotations/staleness.test.ts +56 -0
  96. package/src/annotations/staleness.ts +41 -0
  97. package/src/annotations/store.test.ts +175 -0
  98. package/src/annotations/store.ts +317 -0
  99. package/src/annotations/svgNodes.test.ts +77 -0
  100. package/src/annotations/svgNodes.ts +74 -0
  101. package/src/annotations/toolMap.test.ts +36 -0
  102. package/src/annotations/toolMap.ts +54 -0
  103. package/src/annotations/types.ts +222 -0
  104. package/src/annotations/view.test.ts +37 -0
  105. package/src/annotations/view.ts +40 -0
  106. package/src/canvas/AGENTS.md +55 -5
  107. package/src/canvas/CanvasStack.test.tsx +27 -1
  108. package/src/canvas/CanvasStack.tsx +29 -4
  109. package/src/canvas/CanvasStackContext.ts +23 -4
  110. package/src/canvas/camera.test.ts +78 -0
  111. package/src/canvas/camera.ts +62 -0
  112. package/src/canvas/canvasCoords.test.ts +26 -0
  113. package/src/canvas/canvasCoords.ts +20 -7
  114. package/src/canvas/index.ts +5 -1
  115. package/src/canvas/useLayerScheduler.ts +7 -3
  116. package/src/canvas/usePanZoom.test.ts +36 -5
  117. package/src/canvas/usePanZoom.ts +20 -15
  118. package/src/canvas/worldSpec.test.ts +63 -0
  119. package/src/canvas/worldSpec.ts +48 -0
  120. package/src/chrome/builtins.test.ts +31 -1
  121. package/src/chrome/builtins.tsx +63 -19
  122. package/src/chrome/regions/PaletteRegion.test.tsx +22 -1
  123. package/src/chrome/regions/PaletteRegion.tsx +4 -0
  124. package/src/chrome/regions/SidebarRegion.tsx +37 -13
  125. package/src/chrome/regions/ToolbarRegion.tsx +2 -1
  126. package/src/chrome/regions/ViewportRegion.tsx +9 -1
  127. package/src/chrome/regions/regions.test.tsx +6 -1
  128. package/src/chrome/types.ts +20 -0
  129. package/src/config/builder.test.ts +7 -1
  130. package/src/config/builder.ts +1 -5
  131. package/src/config/resolve.test.ts +1 -2
  132. package/src/controls/ControlPanel.stories.tsx +41 -2
  133. package/src/controls/ControlPanel.test.tsx +69 -9
  134. package/src/controls/ControlPanel.tsx +106 -25
  135. package/src/dragdrop/DragDropRuntime.tsx +8 -2
  136. package/src/dragdrop/dragDrop.test.tsx +29 -2
  137. package/src/dragdrop/index.ts +11 -0
  138. package/src/index.test.ts +22 -0
  139. package/src/index.ts +136 -4
  140. package/src/instrument/SineWave.smoke.test.tsx +1 -0
  141. package/src/instrument/types.ts +24 -1
  142. package/src/lab/Lab.surface.test.tsx +162 -0
  143. package/src/lab/Lab.tsx +102 -17
  144. package/src/lab/Lab.undock.test.tsx +85 -0
  145. package/src/lab/LabFullChrome.stories.tsx +20 -0
  146. package/src/lab/LabShell.less +11 -0
  147. package/src/lab/Workspace.less +73 -0
  148. package/src/lab/Workspace.surface.test.tsx +2 -0
  149. package/src/lab/Workspace.tsx +97 -8
  150. package/src/lab/index.ts +1 -1
  151. package/src/lab/panelHost.ts +37 -0
  152. package/src/layers/AGENTS.md +1 -1
  153. package/src/layers/LayerList.tsx +1 -1
  154. package/src/loupe/AGENTS.md +49 -0
  155. package/src/loupe/CanvasLoupe.tsx +83 -0
  156. package/src/loupe/DomLoupe.tsx +62 -0
  157. package/src/loupe/Loupe.less +38 -0
  158. package/src/loupe/LoupeBubble.tsx +32 -0
  159. package/src/loupe/TrialLoupe.tsx +93 -0
  160. package/src/loupe/canvasLens.test.ts +205 -0
  161. package/src/loupe/canvasLens.ts +141 -0
  162. package/src/loupe/index.ts +19 -0
  163. package/src/loupe/types.test.ts +29 -0
  164. package/src/loupe/types.ts +90 -0
  165. package/src/loupe/useHostSize.ts +30 -0
  166. package/src/loupe/useLoupe.test.tsx +197 -0
  167. package/src/loupe/useLoupe.ts +189 -0
  168. package/src/passthrough/weasel-ui.ts +25 -17
  169. package/src/primitives/FloatingPanel.test.tsx +76 -1
  170. package/src/primitives/FloatingPanel.tsx +19 -3
  171. package/src/primitives/ScaleIndicator.test.tsx +7 -2
  172. package/src/primitives/Sidebar.less +18 -0
  173. package/src/primitives/Toolbar.less +8 -0
  174. package/src/primitives/Toolbar.tsx +5 -0
  175. package/src/primitives/useRovingTabIndex.test.ts +21 -0
  176. package/src/primitives/useRovingTabIndex.ts +26 -17
  177. package/src/state/document.test.ts +23 -0
  178. package/src/state/document.ts +14 -2
  179. package/src/state/index.ts +2 -0
  180. package/src/state/store.test.ts +24 -0
  181. package/src/state/store.ts +35 -1
  182. package/src/state/types.ts +11 -0
  183. package/src/state/undock.test.ts +40 -0
  184. package/src/state/undock.ts +39 -0
  185. package/src/styles.less +2 -4
  186. package/src/surface/SurfaceContext.ts +4 -0
  187. package/src/surface/index.ts +8 -3
  188. package/src/surface/useSurfaceTile.test.tsx +2 -0
  189. package/src/surface/useSurfaceTile.ts +7 -1
  190. package/src/surface/useTiledSurface.test.tsx +58 -0
  191. package/src/surface/useTiledSurface.ts +38 -3
  192. package/src/theme/Interstellar.stories.tsx +1 -1
  193. package/src/trial/Trial.annotations.persist.test.tsx +166 -0
  194. package/src/trial/Trial.annotations.test.tsx +121 -0
  195. package/src/trial/Trial.canvas.test.tsx +76 -1
  196. package/src/trial/Trial.config.test.tsx +3 -8
  197. package/src/trial/Trial.less +19 -0
  198. package/src/trial/Trial.loupe.test.tsx +128 -0
  199. package/src/trial/Trial.stories.tsx +1 -0
  200. package/src/trial/Trial.test.tsx +12 -0
  201. package/src/trial/Trial.trialId.test.tsx +36 -0
  202. package/src/trial/Trial.tsx +215 -40
  203. package/src/trial/TrialChrome.tsx +35 -0
  204. package/src/trial/UndockedSections.tsx +65 -0
  205. package/src/trial/trialOps.test.ts +21 -0
  206. package/src/trial/trialOps.ts +5 -3
  207. package/src/ui/layers/index.ts +5 -1
  208. package/dist/_dts/index-DgcNdEdh.d.ts +0 -314
  209. package/dist/chunk-4TMMVIDM.js.map +0 -1
  210. package/dist/chunk-CPUJ3QXL.js.map +0 -1
  211. package/dist/chunk-HFDVGF4X.js.map +0 -1
  212. package/dist/chunk-NS54R52R.js.map +0 -1
  213. package/dist/chunk-O2BULFHX.js +0 -152
  214. package/dist/chunk-O2BULFHX.js.map +0 -1
  215. package/dist/chunk-O7NVSCLN.js.map +0 -1
  216. package/dist/chunk-PO6L3NSH.js +0 -366
  217. package/dist/chunk-PO6L3NSH.js.map +0 -1
  218. package/dist/chunk-PQJ5B2U2.js +0 -27
  219. package/dist/chunk-PQJ5B2U2.js.map +0 -1
  220. package/dist/chunk-RVNN4CHQ.js.map +0 -1
  221. package/dist/chunk-TLGRYALP.js +0 -6151
  222. package/dist/chunk-TLGRYALP.js.map +0 -1
  223. package/dist/chunk-V7PRSIRQ.js.map +0 -1
  224. package/dist/chunk-Z6HJ5FEM.js.map +0 -1
  225. package/dist/chunk-ZZAYVX4X.js +0 -505
  226. package/dist/chunk-ZZAYVX4X.js.map +0 -1
  227. package/src/primitives/DragHandleGlyph.tsx +0 -29
  228. package/src/ui/format.test.ts +0 -33
  229. package/src/ui/format.ts +0 -30
  230. package/src/ui/layers/LayerStack.less +0 -143
  231. package/src/ui/layers/LayerStack.stories.tsx +0 -45
  232. package/src/ui/layers/LayerStack.test.tsx +0 -170
  233. package/src/ui/layers/LayerStack.tsx +0 -208
  234. package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
  235. package/src/ui/properties/ColorRow.stories.tsx +0 -106
  236. package/src/ui/properties/CurveField.less +0 -48
  237. package/src/ui/properties/CurveField.stories.tsx +0 -20
  238. package/src/ui/properties/CurveField.test.tsx +0 -127
  239. package/src/ui/properties/CurveField.tsx +0 -184
  240. package/src/ui/properties/EffectCard.tsx +0 -324
  241. package/src/ui/properties/Gallery.stories.tsx +0 -178
  242. package/src/ui/properties/NumberRow.stories.tsx +0 -65
  243. package/src/ui/properties/PropertyGroup.less +0 -50
  244. package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
  245. package/src/ui/properties/PropertyGroup.test.tsx +0 -33
  246. package/src/ui/properties/PropertyGroup.tsx +0 -45
  247. package/src/ui/properties/PropertyList.stories.tsx +0 -83
  248. package/src/ui/properties/PropertyPanel.less +0 -606
  249. package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
  250. package/src/ui/properties/PropertyPanel.test.tsx +0 -193
  251. package/src/ui/properties/PropertyPanel.tsx +0 -469
  252. package/src/ui/properties/PropertyRow.stories.tsx +0 -53
  253. package/src/ui/properties/SelectRow.stories.tsx +0 -58
  254. package/src/ui/properties/SliderRow.stories.tsx +0 -74
  255. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -398
  256. package/src/ui/properties/TextRow.stories.tsx +0 -62
  257. package/src/ui/properties/ToggleRow.stories.tsx +0 -57
  258. package/src/ui/properties/index.ts +0 -39
  259. package/src/ui/properties/storyLayouts.tsx +0 -36
@@ -1,23 +1,34 @@
1
1
  import { isBuiltinToolPref } from '@weasel-js/core';
2
- import type { PrefLeaf } from '@weasel-js/ui';
3
- import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
4
- import { fromConfigFields } from '../config/fromConfigField';
5
- import type { ControlRenderer, ResolvedConfig } from '../config/types';
6
- import { isLeafVisible } from '../config/visible';
7
- import { PropertyGroup } from '../ui/properties/PropertyGroup';
8
2
  import {
9
3
  CheckboxRow,
10
4
  ColorRow,
11
5
  NumberRow,
6
+ type PrefLeaf,
7
+ PropertyGroup,
12
8
  PropertyList,
13
9
  PropertyRow,
10
+ type PropertyRowLayout,
14
11
  SelectRow,
12
+ SliderRow,
15
13
  TextRow,
16
14
  ToggleRow,
17
- SliderRow,
18
- } from '../ui/properties/PropertyPanel';
15
+ } from '@weasel-js/ui';
16
+ import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
17
+ import { fromConfigFields } from '../config/fromConfigField';
18
+ import type { ControlRenderer, ResolvedConfig } from '../config/types';
19
+ import { isLeafVisible } from '../config/visible';
19
20
  import type { ConfigField } from './types';
20
21
 
22
+ /** How a panel packs its rows into the two-column property grid.
23
+ * - `'auto'`: narrow controls (numbers, checkboxes, dropdowns, colours) sit
24
+ * two per row; the ones that need the width — text, sliders, segmented
25
+ * toggles — span it.
26
+ * - `'pairs'`: every row pairs. A control that needs the full width says so
27
+ * itself, with `<PropertyRow span>`.
28
+ * - `'one-up'`: one control per row, colours excepted.
29
+ */
30
+ export type ControlPack = 'auto' | 'pairs' | 'one-up';
31
+
21
32
  export interface ControlPanelProps<TC extends Record<string, unknown>> {
22
33
  /** The instrument's resolved config schema. */
23
34
  schema?: ResolvedConfig;
@@ -31,6 +42,11 @@ export interface ControlPanelProps<TC extends Record<string, unknown>> {
31
42
  * the row; entries override the built-in rows on collision.
32
43
  */
33
44
  renderers?: Record<string, ControlRenderer>;
45
+ /** How rows pack into the two-column grid. Defaults to `'pairs'`. */
46
+ pack?: ControlPack;
47
+ /** Where a row's label sits relative to its control. Defaults to `'block'`
48
+ * — above it, which is what leaves a paired row room for its value. */
49
+ layout?: PropertyRowLayout;
34
50
  /** Draw leaves marked `hidden`. */
35
51
  showHidden?: boolean;
36
52
  className?: string;
@@ -45,13 +61,12 @@ export function ControlPanel<TC extends Record<string, unknown>>({
45
61
  config,
46
62
  setConfig,
47
63
  renderers,
64
+ pack = 'pairs',
65
+ layout = 'block',
48
66
  showHidden = false,
49
67
  className,
50
68
  }: ControlPanelProps<TC>) {
51
- const resolved = useMemo(
52
- () => schema ?? fromConfigFields(fields ?? []),
53
- [schema, fields],
54
- );
69
+ const resolved = useMemo(() => schema ?? fromConfigFields(fields ?? []), [schema, fields]);
55
70
 
56
71
  const paths = Object.keys(resolved.group.children);
57
72
  const sectioned = new Set(resolved.sections.flatMap((s) => s.paths));
@@ -65,15 +80,21 @@ export function ControlPanel<TC extends Record<string, unknown>>({
65
80
  config={config}
66
81
  setConfig={setConfig}
67
82
  renderers={renderers}
83
+ pack={pack}
84
+ layout={layout}
68
85
  showHidden={showHidden}
69
86
  />
70
87
  );
71
88
 
89
+ const gridPack = pack === 'one-up' ? 'auto-color' : 'pairs';
72
90
  return (
73
- <PropertyList className={className ? `lk-control-panel ${className}` : 'lk-control-panel'}>
91
+ <PropertyList
92
+ pack={gridPack}
93
+ className={className ? `lk-control-panel ${className}` : 'lk-control-panel'}
94
+ >
74
95
  {loose.map(row)}
75
96
  {resolved.sections.map((section) => (
76
- <PropertyGroup key={section.label} title={section.label}>
97
+ <PropertyGroup key={section.label} title={section.label} pack={gridPack}>
77
98
  {section.paths.map(row)}
78
99
  </PropertyGroup>
79
100
  ))}
@@ -87,6 +108,8 @@ interface ControlRowProps<TC extends Record<string, unknown>> {
87
108
  config: TC;
88
109
  setConfig: (key: keyof TC, value: unknown) => void;
89
110
  renderers?: Record<string, ControlRenderer>;
111
+ pack: ControlPack;
112
+ layout: PropertyRowLayout;
90
113
  showHidden: boolean;
91
114
  }
92
115
 
@@ -102,6 +125,8 @@ function ControlRow<TC extends Record<string, unknown>>({
102
125
  config,
103
126
  setConfig,
104
127
  renderers,
128
+ pack,
129
+ layout,
105
130
  showHidden,
106
131
  }: ControlRowProps<TC>) {
107
132
  const leaf = resolved.group.children[path] as PrefLeaf | undefined;
@@ -114,14 +139,20 @@ function ControlRow<TC extends Record<string, unknown>>({
114
139
 
115
140
  // Most specific wins, and within a tier the lab's entry beats the
116
141
  // instrument's: controls[path] -> node .render -> controls[kind] -> built-in.
117
- const custom =
118
- renderers?.[path] ?? resolved.renderers[path] ?? renderers?.[leaf.kind];
142
+ const custom = renderers?.[path] ?? resolved.renderers[path] ?? renderers?.[leaf.kind];
143
+ // A custom row places itself like any other; one that needs the full width
144
+ // says so with `<PropertyRow span>`, which is the same opt-out a built-in has.
119
145
  if (custom) return custom({ path, pref: leaf, value, setValue: write });
120
146
 
121
147
  const label = leaf.name;
148
+ const description = leaf.description;
122
149
  const read = <T,>(): T => value as T;
150
+ // `auto` gives the whole width to the controls that read badly at half of a
151
+ // sidebar's: free text, a slider track, a segmented toggle. `pairs` doesn't.
152
+ const wide = pack === 'auto';
123
153
 
124
- if (!isBuiltinToolPref(leaf)) return <UnwiredRow label={label} kind={leaf.kind} />;
154
+ if (!isBuiltinToolPref(leaf))
155
+ return <UnwiredRow label={label} kind={leaf.kind} description={description} />;
125
156
 
126
157
  switch (leaf.kind) {
127
158
  case 'number': {
@@ -137,6 +168,9 @@ function ControlRow<TC extends Record<string, unknown>>({
137
168
  max={max}
138
169
  step={step}
139
170
  onChange={write}
171
+ layout={layout}
172
+ span={wide}
173
+ description={description}
140
174
  />
141
175
  );
142
176
  }
@@ -153,28 +187,58 @@ function ControlRow<TC extends Record<string, unknown>>({
153
187
  max={max}
154
188
  step={step}
155
189
  onChange={(n) => write(Math.min(hi, Math.max(lo, n)))}
190
+ layout={layout}
191
+ description={description}
156
192
  />
157
193
  );
158
194
  }
159
195
  case 'boolean':
160
- return <CheckboxRow label={label} value={read<boolean>()} onChange={write} />;
196
+ return (
197
+ <CheckboxRow
198
+ label={label}
199
+ value={read<boolean>()}
200
+ onChange={write}
201
+ description={description}
202
+ />
203
+ );
161
204
  case 'enum': {
162
205
  const options = extra<readonly { value: string; label: string }[]>(leaf, 'options') ?? [];
163
- const Row = extra<string>(leaf, 'control') === 'radio' ? ToggleRow : SelectRow;
206
+ const segmented = extra<string>(leaf, 'control') === 'radio';
207
+ const Row = segmented ? ToggleRow : SelectRow;
164
208
  return (
165
- <Row label={label} value={read<string>()} options={options} onChange={write} />
209
+ <Row
210
+ label={label}
211
+ value={read<string>()}
212
+ options={options}
213
+ onChange={write}
214
+ layout={layout}
215
+ span={segmented && wide}
216
+ description={description}
217
+ />
166
218
  );
167
219
  }
168
220
  case 'string':
169
- return <DebouncedTextRow leaf={leaf} label={label} value={read<string>()} write={write} />;
221
+ return (
222
+ <DebouncedTextRow
223
+ leaf={leaf}
224
+ label={label}
225
+ value={read<string>()}
226
+ write={write}
227
+ layout={layout}
228
+ span={wide}
229
+ description={description}
230
+ />
231
+ );
170
232
  case 'color':
171
- return <ColorRow label={label} value={read<string>()} onChange={write} />;
233
+ return (
234
+ <ColorRow label={label} value={read<string>()} onChange={write} description={description} />
235
+ );
172
236
  case 'paint':
173
237
  case 'object':
174
238
  // Declined: a hex swatch would write a solid over a gradient, and a flat
175
239
  // row would write one field into a half-built object. Override with
176
240
  // `render` to edit either.
177
- return <UnwiredRow label={label} kind={leaf.kind} />;
241
+ return <UnwiredRow label={label} kind={leaf.kind} description={description} />;
178
242
  default: {
179
243
  // Not reachable while every built-in kind has an arm — and a new kind
180
244
  // that lacks one is a compile error here, never a blank row.
@@ -188,9 +252,17 @@ function ControlRow<TC extends Record<string, unknown>>({
188
252
 
189
253
  /** A leaf this panel has no control for is named rather than dropped: a silent
190
254
  * gap reads as "this control does not exist". */
191
- function UnwiredRow({ label, kind }: { label: string; kind: string }) {
255
+ function UnwiredRow({
256
+ label,
257
+ kind,
258
+ description,
259
+ }: {
260
+ label: string;
261
+ kind: string;
262
+ description?: string;
263
+ }) {
192
264
  return (
193
- <PropertyRow label={label}>
265
+ <PropertyRow label={label} description={description}>
194
266
  <span className="lk-control-panel__unknown">no control for “{kind}”</span>
195
267
  </PropertyRow>
196
268
  );
@@ -204,11 +276,17 @@ function DebouncedTextRow({
204
276
  label,
205
277
  value,
206
278
  write,
279
+ layout,
280
+ span,
281
+ description,
207
282
  }: {
208
283
  leaf: PrefLeaf;
209
284
  label: string;
210
285
  value: string;
211
286
  write: (value: unknown) => void;
287
+ layout?: PropertyRowLayout;
288
+ span?: boolean;
289
+ description?: string;
212
290
  }) {
213
291
  const debounceMs = extra<number>(leaf, 'debounceMs') ?? 150;
214
292
  const [local, setLocal] = useState(value);
@@ -232,6 +310,9 @@ function DebouncedTextRow({
232
310
  return (
233
311
  <TextRow
234
312
  label={label}
313
+ layout={layout}
314
+ span={span}
315
+ description={description}
235
316
  value={local}
236
317
  placeholder={extra<string>(leaf, 'placeholder')}
237
318
  maxLength={extra<number>(leaf, 'maxLength')}
@@ -1,6 +1,7 @@
1
1
  import { useVisibleRaf } from '@weasel-js/core';
2
2
  import { type RefObject, useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { screenToWorld } from '../canvas/canvasCoords';
4
+ import { resolveFrame, type WorldSpec } from '../canvas/worldSpec';
4
5
  import type {
5
6
  DragDropCapability,
6
7
  DragFeedback,
@@ -20,6 +21,9 @@ export interface UseDragDropArgs<TS, TC> {
20
21
  capability: DragDropCapability<TS, TC>;
21
22
  canvasContainerRef: RefObject<HTMLElement | null>;
22
23
  view: ViewTransform;
24
+ /** The instrument's coordinate system, resolved against the container each
25
+ * time a screen point is converted — the container is what knows its size. */
26
+ worldSpec?: WorldSpec;
23
27
  state: TS;
24
28
  config: TC;
25
29
  setState: (next: TS | ((prev: TS) => TS)) => void;
@@ -39,6 +43,7 @@ export function useDragDrop<TS, TC>({
39
43
  config,
40
44
  setState,
41
45
  emit,
46
+ worldSpec,
42
47
  }: UseDragDropArgs<TS, TC>): UseDragDropResult {
43
48
  const [drag, setDrag] = useState<DragState | null>(null);
44
49
  const dragRef = useRef<DragState | null>(null);
@@ -67,9 +72,10 @@ export function useDragDrop<TS, TC>({
67
72
  const el = canvasContainerRef.current;
68
73
  if (!el) return null;
69
74
  const r = el.getBoundingClientRect();
70
- return screenToWorld({ x: screenPos.x - r.left, y: screenPos.y - r.top }, view);
75
+ const frame = resolveFrame(worldSpec, { width: r.width, height: r.height });
76
+ return screenToWorld({ x: screenPos.x - r.left, y: screenPos.y - r.top }, view, frame);
71
77
  },
72
- [canvasContainerRef, view],
78
+ [canvasContainerRef, view, worldSpec],
73
79
  );
74
80
 
75
81
  const frameLoop = useVisibleRaf(() => {
@@ -41,9 +41,19 @@ function StateProbe({ onState }: { onState: (state: DropState) => void }) {
41
41
  return null;
42
42
  }
43
43
 
44
- function renderLab(probe?: (state: DropState) => void) {
44
+ const centredInstrument = defineInstrument<DropState, Record<string, never>>({
45
+ ...testInstrument,
46
+ name: 'CentredDrop',
47
+ canvas: {
48
+ worldSpec: { origin: { x: 0.5, y: 0.5 }, yAxis: 'up' },
49
+ initialView: { zoom: 1, pan: { x: 0, y: 0 } },
50
+ layers: [{ id: 'main', draw: () => undefined }],
51
+ },
52
+ });
53
+
54
+ function renderLab(probe?: (state: DropState) => void, instrument = testInstrument) {
45
55
  return render(
46
- <Lab instruments={[testInstrument]} defaultInstrument="TestDrop">
56
+ <Lab instruments={[instrument]} defaultInstrument={instrument.name}>
47
57
  {probe ? <StateProbe onState={probe} /> : null}
48
58
  </Lab>,
49
59
  );
@@ -139,4 +149,21 @@ describe('DragDrop integration', () => {
139
149
  windowPointerUp(250, 250);
140
150
  expect(document.querySelector('.lk-drag-ghost')).toBeNull();
141
151
  });
152
+
153
+ it('drops through the instrument world spec, not the canvas top-left', () => {
154
+ let latest: DropState = { items: [] };
155
+ renderLab((s) => {
156
+ latest = s;
157
+ }, centredInstrument);
158
+ const palette = screen.getByRole('button', { name: 'Item A' });
159
+
160
+ act(() => {
161
+ pointerDown(palette, 0, 0);
162
+ });
163
+ // The host spans (100,100)-(500,500), so its centre is at (300,300).
164
+ // Dropping 50px right of centre and 50px above it, at zoom 1, y up.
165
+ windowPointerUp(350, 250);
166
+
167
+ expect(latest.items[0]).toMatchObject({ x: 50, y: 50 });
168
+ });
142
169
  });
@@ -1 +1,12 @@
1
1
  export type { DragDropCapability, DragFeedback, PaletteItem } from '../instrument/types';
2
+ export type {
3
+ DragOverlayProps,
4
+ DragState,
5
+ UseDragDropArgs,
6
+ UseDragDropResult,
7
+ } from './DragDropRuntime';
8
+ export { DragOverlay, useDragDrop } from './DragDropRuntime';
9
+ export type { DragGhostProps } from './DragGhost';
10
+ export { DragGhost } from './DragGhost';
11
+ export type { PaletteProps } from './Palette';
12
+ export { Palette } from './Palette';
package/src/index.test.ts CHANGED
@@ -60,11 +60,33 @@ describe('surface, job and orbit entry points', () => {
60
60
  expect(typeof kit.useSurfaceOptional).toBe('function');
61
61
  expect(typeof kit.toDeviceRect).toBe('function');
62
62
  expect(typeof kit.composeRects).toBe('function');
63
+ expect(typeof kit.createAnnotationStore).toBe('function');
64
+ expect(typeof kit.createAnnotationScene).toBe('function');
65
+ expect(typeof kit.annotationsFromJSON).toBe('function');
66
+ expect(typeof kit.seenFrom).toBe('function');
67
+ expect(typeof kit.fracToWorld).toBe('function');
63
68
  expect(typeof kit.useOrbit).toBe('function');
64
69
  expect(typeof kit.orbitAfterDrag).toBe('function');
65
70
  expect(typeof kit.useJob).toBe('function');
66
71
  expect(typeof kit.as2DView).toBe('function');
67
72
  expect(kit.SurfaceContext).toBeDefined();
73
+ expect(kit.SurfaceCanvasContext).toBeDefined();
74
+ expect(typeof kit.useSurfaceCanvas).toBe('function');
75
+ });
76
+ });
77
+
78
+ describe('the annotations capability', () => {
79
+ it('reaches a host through the package root', async () => {
80
+ const kit = await import('./index');
81
+ expect(typeof kit.useAnnotations).toBe('function');
82
+ expect(typeof kit.useAnnotationsOptional).toBe('function');
83
+ expect(typeof kit.AnnotationTargets).toBe('function');
84
+ expect(typeof kit.AnnotationOverlay).toBe('function');
85
+ expect(typeof kit.markCommands).toBe('function');
86
+ expect(typeof kit.annotationToolInfo).toBe('function');
87
+ expect(typeof kit.fitView).toBe('function');
88
+ expect(kit.ANNOTATION_TOOLS.map((t) => t.id)).toContain('arrow');
89
+ expect(typeof kit.MarkList).toBe('function');
68
90
  expect(kit.DEFAULT_VIEW).toEqual({ zoom: 1, pan: { x: 0, y: 0 } });
69
91
  });
70
92
  });
package/src/index.ts CHANGED
@@ -1,5 +1,136 @@
1
- export type { CanvasLayerDescriptor, CanvasStackContextValue, CanvasStackProps } from './canvas';
2
- export { CanvasStack, CanvasStackContext, screenToWorld, worldToScreen } from './canvas';
1
+ // The property-panel family and LayerStack live in `@weasel-js/ui` now; labkit
2
+ // re-exports them so existing `@weasel-js/labkit` imports keep resolving. Named,
3
+ // not `export *` — a star re-export of an external package emits no binding in
4
+ // the bundle.
5
+ export type {
6
+ CheckboxRowProps,
7
+ ColorRowProps,
8
+ CurveFieldProps,
9
+ CurveMark,
10
+ EffectCardListItem,
11
+ EffectCardListProps,
12
+ EffectCardProps,
13
+ LayerStackItem,
14
+ LayerStackProps,
15
+ NumberRowProps,
16
+ PropertyGroupProps,
17
+ PropertyListPack,
18
+ PropertyListProps,
19
+ PropertyOption,
20
+ PropertyPanelProps,
21
+ PropertyRowLayout,
22
+ PropertyRowProps,
23
+ PropertyRowVariant,
24
+ PropertySpanProps,
25
+ SelectRowProps,
26
+ SliderRowProps,
27
+ SubpanelProps,
28
+ TextRowProps,
29
+ ToggleRowProps,
30
+ } from '@weasel-js/ui';
31
+ export {
32
+ CheckboxRow,
33
+ ColorRow,
34
+ CurveField,
35
+ EffectCard,
36
+ EffectCardList,
37
+ LayerStack,
38
+ NumberRow,
39
+ PropertyGroup,
40
+ PropertyList,
41
+ PropertyPanel,
42
+ PropertyRow,
43
+ PropertySpan,
44
+ SelectRow,
45
+ SliderRow,
46
+ Subpanel,
47
+ TextRow,
48
+ ToggleRow,
49
+ } from '@weasel-js/ui';
50
+ export type {
51
+ Annotation,
52
+ AnnotationData,
53
+ AnnotationInit,
54
+ AnnotationKind,
55
+ AnnotationMeaning,
56
+ AnnotationPatch,
57
+ AnnotationQuery,
58
+ AnnotationStatus,
59
+ AnnotationStoreOptions,
60
+ AnnotationsApi,
61
+ AnnotationsCapability,
62
+ AnnotationTarget,
63
+ AnnotationTargetInfo,
64
+ CaptureDeps,
65
+ CaptureOptions,
66
+ CaptureResult,
67
+ CaptureSource,
68
+ FracPoint,
69
+ FracRect,
70
+ MarkScene,
71
+ SerializedAnnotations,
72
+ WorldRect,
73
+ } from './annotations';
74
+ export {
75
+ annotationsFromJSON,
76
+ createAnnotationScene,
77
+ createAnnotationStore,
78
+ fracContains,
79
+ fracIntersects,
80
+ fracToWorld,
81
+ isStale,
82
+ roundFrac,
83
+ seenFrom,
84
+ worldToFrac,
85
+ } from './annotations';
86
+ export type { AnnotationOverlayProps } from './annotations/AnnotationOverlay';
87
+ export { AnnotationOverlay } from './annotations/AnnotationOverlay';
88
+ export {
89
+ AnnotationsContext,
90
+ useAnnotations,
91
+ useAnnotationsOptional,
92
+ } from './annotations/AnnotationsContext';
93
+ export type { AnnotationTargetsProps } from './annotations/AnnotationTargets';
94
+ export { AnnotationTargets } from './annotations/AnnotationTargets';
95
+ export type { CaptureArgs, CapturePlan, ComposeSvgArgs } from './annotations/capture';
96
+ export { capturePlan, captureTarget, composeCaptureSvg } from './annotations/capture';
97
+ export type { MarkDrawOptions } from './annotations/drawOne';
98
+ export { createMarkDrawOne, resolveMarkStyle } from './annotations/drawOne';
99
+ export { ExportMenu } from './annotations/ExportMenu';
100
+ export type { MarkListProps } from './annotations/MarkList';
101
+ export { MarkList } from './annotations/MarkList';
102
+ export type { MarkStyle, PaintableMark } from './annotations/paint';
103
+ export { markCommands } from './annotations/paint';
104
+ export { markSvgNodes } from './annotations/svgNodes';
105
+ export type { AnnotationToolInfo } from './annotations/toolMap';
106
+ export {
107
+ ANNOTATION_TOOLS,
108
+ ANNOTATION_WEASEL_TOOLS,
109
+ annotationToolInfo,
110
+ } from './annotations/toolMap';
111
+ export type { ContentSize, PaneSize } from './annotations/view';
112
+ export { fitView, fromWeaselView, toWeaselView } from './annotations/view';
113
+ export type {
114
+ CanvasLayerDescriptor,
115
+ CanvasStackContextValue,
116
+ CanvasStackProps,
117
+ CanvasStackSurface,
118
+ ViewportSize,
119
+ WorldFrame,
120
+ WorldSpec,
121
+ ZoomAtOptions,
122
+ } from './canvas';
123
+ export {
124
+ applyCamera,
125
+ CanvasStack,
126
+ CanvasStackContext,
127
+ centerOn,
128
+ DEFAULT_FRAME,
129
+ resolveFrame,
130
+ screenToWorld,
131
+ worldToScreen,
132
+ zoomAt,
133
+ } from './canvas';
3
134
  export type { OrbitHandlers, OrbitView, UseOrbitOptions, Vec3 } from './canvas/useOrbit';
4
135
  export {
5
136
  clampPitch,
@@ -69,6 +200,7 @@ export * from './job';
69
200
  export * from './lab';
70
201
  export type { LayerListProps } from './layers';
71
202
  export { LayerList } from './layers';
203
+ export * from './loupe';
72
204
  export * from './primitives';
73
205
  export {
74
206
  createMemoryAdapter,
@@ -98,6 +230,8 @@ export type {
98
230
  TrialStateHandle,
99
231
  UndoStack,
100
232
  } from './state/types';
233
+ export type { UndockedPanel, UndockedPanels } from './state/undock';
234
+ export { dockPanel, panelKey, undockPanel } from './state/undock';
101
235
  export { useTrialState } from './state/useTrialState';
102
236
  export type { ViewTransform2D } from './state/view';
103
237
  export { as2DView, DEFAULT_VIEW } from './state/view';
@@ -105,7 +239,5 @@ export * from './surface';
105
239
  export { interstellarTheme } from './theme/interstellar';
106
240
  export type { ToolCapability, TrialTool } from './tools/types';
107
241
  export * from './trial';
108
- export * from './ui/layers';
109
- export * from './ui/properties';
110
242
  export type { EventBus, EventListener } from './undo';
111
243
  export { clearUndo, createEventBus, emptyStack, pushSnapshot, redo, undo } from './undo';
@@ -93,6 +93,7 @@ describe('SineWave instrument smoke test', () => {
93
93
  zoom: 1,
94
94
  setZoom: vi.fn(),
95
95
  activeToolId: null,
96
+ visibleLayers: [],
96
97
  },
97
98
  emit: vi.fn(),
98
99
  };
@@ -1,8 +1,11 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { AnnotationsCapability } from '../annotations/types';
3
+ import type { ViewportSize, WorldSpec } from '../canvas/worldSpec';
2
4
  import type { TrialContribution } from '../chrome/types';
3
5
  import type { ConfigSchema } from '../config/types';
4
6
  import type { ConfigField } from '../controls/types';
5
7
  import type { JobCapability, JobHandle } from '../job/types';
8
+ import type { LoupeDeclaration } from '../loupe/types';
6
9
  import type { ToolCapability } from '../tools/types';
7
10
 
8
11
  /** What an instrument's `render` is handed: its state and config, the setters
@@ -25,6 +28,10 @@ export interface RenderContext<TS = unknown, TC = unknown> {
25
28
  /** Resolved active tool: this trial's slot, or the lab's. Null when neither
26
29
  * holds one. */
27
30
  activeToolId: string | null;
31
+ /** The canvas layers currently shown, in declaration order. labkit skips a
32
+ * hidden layer's `draw`, so this is the only way to know what was
33
+ * painted — a legend listing rows the view did not draw needs it. */
34
+ visibleLayers: readonly string[];
28
35
  };
29
36
  emit: (event: string) => void;
30
37
  /** Present only when the instrument declares a `job`. */
@@ -45,7 +52,14 @@ export interface CanvasLayer<TS = unknown, TC = unknown> {
45
52
  * view starts. */
46
53
  export interface CanvasCapability<TS = unknown, TC = unknown> {
47
54
  layers: CanvasLayer<TS, TC>[];
48
- initialView?: { zoom: number; pan: { x: number; y: number } };
55
+ /** The coordinate system this instrument's world is in. Omitted, world (0,0)
56
+ * sits at the canvas top-left with y running down — what labkit assumed
57
+ * before an instrument could say otherwise. */
58
+ worldSpec?: WorldSpec;
59
+ /** Where the view starts. A function is called once the canvas knows its
60
+ * size, so an instrument can frame content it can only place in terms of
61
+ * the viewport; until then the trial's view is `null`. */
62
+ initialView?: ViewTransform | ((size: ViewportSize) => ViewTransform);
49
63
  /** Widens `usePanZoom`'s default clamp; the opening zoom stays reachable
50
64
  * regardless of these. */
51
65
  minZoom?: number;
@@ -127,6 +141,15 @@ export interface Instrument<TS = unknown, TC = unknown, TItem = unknown> {
127
141
  /** Tools this instrument offers. Declaring them gives the trial a palette
128
142
  * region and its own tool slot. */
129
143
  tools?: ToolCapability;
144
+ /** Regions of this instrument that accept marks, and optionally what a mark
145
+ * is allowed to mean. Declaring it is what makes the trial provide the
146
+ * annotation overlay and its chrome. */
147
+ annotations?: AnnotationsCapability<TS, TC>;
148
+ /** Magnification. `true` takes every default and re-draws the instrument's
149
+ * canvas layers through a zoomed camera; an instrument whose content is DOM
150
+ * gives a `render` that draws it again at a camera it is handed. A function
151
+ * is re-read as the config changes, so a setting can drive the lens. */
152
+ loupe?: LoupeDeclaration<TS, TC> | ((config: TC) => LoupeDeclaration<TS, TC>);
130
153
  /** Chrome this instrument contributes beyond what its capabilities imply. */
131
154
  chrome?: TrialContribution[];
132
155
  /** Work too slow to do during a render. The runtime starts it, aborts it on