@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,4 +1,5 @@
1
1
  import { ToolButton, ToolGroup } from '../../passthrough/weasel-ui';
2
+ import { useRovingTabIndex } from '../../primitives/useRovingTabIndex';
2
3
  import type { TrialChromeContext, TrialContribution } from '../types';
3
4
 
4
5
  /** Props for `<PaletteRegion>`. */
@@ -10,13 +11,16 @@ export interface PaletteRegionProps {
10
11
  /** A trial's tool strip. Selection lives in the trial's or the lab's tool
11
12
  * slot; this region only reflects it. */
12
13
  export function PaletteRegion({ contributions, ctx }: PaletteRegionProps) {
14
+ const { ref, onKeyDown } = useRovingTabIndex<HTMLDivElement>('vertical');
13
15
  if (contributions.length === 0) return null;
14
16
  return (
15
17
  <div
18
+ ref={ref}
16
19
  className="lk-palette-region"
17
20
  role="toolbar"
18
21
  aria-label="Tools"
19
22
  aria-orientation="vertical"
23
+ onKeyDown={onKeyDown}
20
24
  >
21
25
  <ToolGroup orientation="vertical">
22
26
  {contributions.map((c) => {
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { useState } from 'react';
3
- import type { TrialChromeContext, TrialContribution } from '../types';
3
+ import type { SidebarSection, TrialChromeContext, TrialContribution } from '../types';
4
4
 
5
5
  /** Props for `<SidebarRegion>`. */
6
6
  export interface SidebarRegionProps {
@@ -11,29 +11,46 @@ export interface SidebarRegionProps {
11
11
  function Section({
12
12
  title,
13
13
  defaultCollapsed,
14
+ onUndock,
14
15
  children,
15
16
  }: {
16
17
  title: string;
17
18
  defaultCollapsed: boolean;
19
+ onUndock?: () => void;
18
20
  children: ReactNode;
19
21
  }) {
20
22
  const [collapsed, setCollapsed] = useState(defaultCollapsed);
21
23
  return (
22
24
  <section className="lk-sidebar-section">
23
- <button
24
- type="button"
25
- className="lk-sidebar-section__head"
26
- aria-expanded={!collapsed}
27
- onClick={() => setCollapsed((c) => !c)}
28
- >
29
- {title}
30
- </button>
25
+ <div className="lk-sidebar-section__bar">
26
+ <button
27
+ type="button"
28
+ className="lk-sidebar-section__head"
29
+ aria-expanded={!collapsed}
30
+ onClick={() => setCollapsed((c) => !c)}
31
+ >
32
+ {title}
33
+ </button>
34
+ {onUndock ? (
35
+ <button
36
+ type="button"
37
+ className="lk-sidebar-section__undock"
38
+ aria-label={`Undock ${title}`}
39
+ title={`Undock ${title}`}
40
+ onClick={onUndock}
41
+ >
42
+ ⧉
43
+ </button>
44
+ ) : null}
45
+ </div>
31
46
  {collapsed ? null : <div className="lk-sidebar-section__body">{children}</div>}
32
47
  </section>
33
48
  );
34
49
  }
35
50
 
36
- /** Lays a trial's `sidebar` contributions out as titled, collapsible sections. */
51
+ /** Lays a trial's `sidebar` contributions out as titled, collapsible sections.
52
+ * A section the trial has torn out is not rendered here — `Trial` portals its
53
+ * body into the workspace instead. */
37
54
  export function SidebarRegion({ contributions, ctx }: SidebarRegionProps) {
38
55
  if (contributions.length === 0) return null;
39
56
  return (
@@ -41,13 +58,20 @@ export function SidebarRegion({ contributions, ctx }: SidebarRegionProps) {
41
58
  {contributions.map((c) => {
42
59
  if (c.render) return <div key={c.id}>{c.render(ctx)}</div>;
43
60
  if (c.region !== 'sidebar' || !c.item) return null;
61
+ if (ctx.undockedPanels.includes(c.id)) return null;
62
+ const item = c.item as SidebarSection;
44
63
  return (
45
64
  <Section
46
65
  key={c.id}
47
- title={c.item.title}
48
- defaultCollapsed={c.item.defaultCollapsed ?? false}
66
+ title={item.title}
67
+ defaultCollapsed={item.defaultCollapsed ?? false}
68
+ onUndock={
69
+ item.undockable === false
70
+ ? undefined
71
+ : () => ctx.undockPanel(c.id, item.undockAs ?? 'tile')
72
+ }
49
73
  >
50
- {c.item.body}
74
+ {item.body}
51
75
  </Section>
52
76
  );
53
77
  })}
@@ -38,13 +38,14 @@ function groupsOf(contributions: readonly TrialContribution[]): Group[] {
38
38
  function renderEntry(c: TrialContribution, ctx: TrialChromeContext): ReactNode {
39
39
  if (c.render) return <span key={c.id}>{c.render(ctx)}</span>;
40
40
  if (c.region !== 'toolbar' || !c.item) return null;
41
- const { icon: Icon, label, shortcut, disabled, danger, showLabel } = c.item;
41
+ const { icon: Icon, label, shortcut, disabled, danger, showLabel, pressed } = c.item;
42
42
  return (
43
43
  <Toolbar.Button
44
44
  key={c.id}
45
45
  iconOnly={!showLabel}
46
46
  variant={danger ? 'danger' : 'default'}
47
47
  disabled={disabled}
48
+ pressed={pressed}
48
49
  aria-label={label}
49
50
  title={shortcut ? `${label} (${shortcut})` : label}
50
51
  onClick={c.item.onActivate}
@@ -1,3 +1,4 @@
1
+ import { useRovingTabIndex } from '../../primitives/useRovingTabIndex';
1
2
  import type { TrialChromeContext, TrialContribution } from '../types';
2
3
 
3
4
  /** Props for `<ViewportRegion>`. */
@@ -11,9 +12,16 @@ export interface ViewportRegionProps {
11
12
  * rather than in the toolbar, which acts on the trial itself.
12
13
  */
13
14
  export function ViewportRegion({ contributions, ctx }: ViewportRegionProps) {
15
+ const { ref, onKeyDown } = useRovingTabIndex<HTMLDivElement>();
14
16
  if (contributions.length === 0) return null;
15
17
  return (
16
- <div className="lk-viewport-controls" role="toolbar" aria-label="View">
18
+ <div
19
+ ref={ref}
20
+ className="lk-viewport-controls"
21
+ role="toolbar"
22
+ aria-label="View"
23
+ onKeyDown={onKeyDown}
24
+ >
17
25
  {contributions.map((c) => {
18
26
  if (c.render) return <span key={c.id}>{c.render(ctx)}</span>;
19
27
  if (c.region !== 'viewport' || !c.item) return null;
@@ -6,7 +6,12 @@ import { StatusRegion } from './StatusRegion';
6
6
  import { ViewportRegion } from './ViewportRegion';
7
7
 
8
8
  const Glyph = () => <svg />;
9
- const ctx = { trialId: 't1' } as unknown as TrialChromeContext;
9
+ const ctx = {
10
+ trialId: 't1',
11
+ undockedPanels: [],
12
+ undockPanel: () => {},
13
+ dockPanel: () => {},
14
+ } as unknown as TrialChromeContext;
10
15
 
11
16
  describe('SidebarRegion', () => {
12
17
  it('renders nothing when empty', () => {
@@ -19,6 +19,9 @@ export interface ToolbarItem {
19
19
  disabled?: boolean;
20
20
  /** Reddens on hover. For actions that discard work. */
21
21
  danger?: boolean;
22
+ /** A toggle rather than a command: renders `aria-pressed` and reads as held
23
+ * down while true. */
24
+ pressed?: boolean;
22
25
  /** Render the label beside the glyph rather than only in the tooltip. */
23
26
  showLabel?: boolean;
24
27
  onActivate: () => void;
@@ -37,6 +40,11 @@ export interface SidebarSection {
37
40
  title: string;
38
41
  /** Starts collapsed. The open/closed state itself is the region's. */
39
42
  defaultCollapsed?: boolean;
43
+ /** Offer the tear-out control. On by default; a section that only makes
44
+ * sense beside its trial sets this false. */
45
+ undockable?: boolean;
46
+ /** Where the tear-out control sends it. Default `'tile'`. */
47
+ undockAs?: 'tile' | 'floating';
40
48
  body: ReactNode;
41
49
  }
42
50
 
@@ -103,6 +111,11 @@ export interface TrialChromeContext {
103
111
  undo: () => void;
104
112
  redo: () => void;
105
113
 
114
+ /** Whether the trial's loupe is turned on. False for an instrument that
115
+ * declares none, whose chrome offers no way to turn one on. */
116
+ loupeOn: boolean;
117
+ toggleLoupe: () => void;
118
+
106
119
  /** The instrument's controls, resolved against the lab's rules. Always
107
120
  * populated: a legacy `configSchema()` is adapted into the same shape. */
108
121
  configSchema: ResolvedConfig;
@@ -112,6 +125,13 @@ export interface TrialChromeContext {
112
125
  config: unknown;
113
126
  setConfig: (key: string, value: unknown) => void;
114
127
 
128
+ /** Section ids this trial currently has torn out of its sidebar. */
129
+ undockedPanels: readonly string[];
130
+ /** Tear a sidebar section out into the workspace. */
131
+ undockPanel: (sectionId: string, as?: 'tile' | 'floating') => void;
132
+ /** Put a torn-out section back in the sidebar. */
133
+ dockPanel: (sectionId: string) => void;
134
+
115
135
  savedSnapshots: SavedSnapshot[];
116
136
  saveSnapshot: (name?: string) => void;
117
137
  loadSnapshot: (snapshotId: string) => void;
@@ -20,7 +20,13 @@ describe('builder', () => {
20
20
  it('keeps the subclass across a chain, so kind-specific methods survive', () => {
21
21
  const n = f.number(20).label('A').range(5, 80).describe('help').step(5);
22
22
  expect(n.kind).toBe('number');
23
- expect(n.annotations).toMatchObject({ name: 'A', description: 'help', min: 5, max: 80, step: 5 });
23
+ expect(n.annotations).toMatchObject({
24
+ name: 'A',
25
+ description: 'help',
26
+ min: 5,
27
+ max: 80,
28
+ step: 5,
29
+ });
24
30
  });
25
31
 
26
32
  it('keeps a custom node kind across a chain', () => {
@@ -29,11 +29,7 @@ abstract class BaseNode<T> implements ConfigNode<T> {
29
29
  /** Clone into the same subclass. Subclasses with extra constructor
30
30
  * arguments override this. */
31
31
  protected with(annotations: Annotations, options: NodeOptions): this {
32
- const Ctor = this.constructor as new (
33
- d: T,
34
- a: Annotations,
35
- o: NodeOptions,
36
- ) => this;
32
+ const Ctor = this.constructor as new (d: T, a: Annotations, o: NodeOptions) => this;
37
33
  return new Ctor(this.default, annotations, options);
38
34
  }
39
35
 
@@ -54,8 +54,7 @@ describe('resolveConfigSchema', () => {
54
54
  });
55
55
 
56
56
  it('a consumer rule claims an f.value kind before the built-in', () => {
57
- const colorByName: ConfigRule = (ctx) =>
58
- ctx.key.endsWith('Color') ? { kind: 'color' } : null;
57
+ const colorByName: ConfigRule = (ctx) => (ctx.key.endsWith('Color') ? { kind: 'color' } : null);
59
58
  const r = resolveConfigSchema(f.schema({ tintColor: f.value('#fff') }), [colorByName]);
60
59
  expect(leafAt(r, 'tintColor').kind).toBe('color');
61
60
  });
@@ -1,9 +1,9 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { PropertyRow } from '@weasel-js/ui';
2
3
  import { useState } from 'react';
3
4
  import { f } from '../config/builder';
4
5
  import { resolveConfigSchema } from '../config/resolve';
5
6
  import type { ConfigRule, ControlRenderer } from '../config/types';
6
- import { PropertyRow } from '../ui/properties/PropertyPanel';
7
7
  import { ControlPanel } from './ControlPanel';
8
8
  import type { ConfigField } from './types';
9
9
 
@@ -141,9 +141,48 @@ export const CustomControl: Story = {
141
141
  ),
142
142
  };
143
143
 
144
+ const described = f.schema({
145
+ frequency: f
146
+ .number(2)
147
+ .range(0.1, 10)
148
+ .step(0.1)
149
+ .describe('Cycles per second the oscillator runs at.'),
150
+ amplitude: f.number(0.5).range(0, 1).step(0.05),
151
+ showGrid: f.boolean(true).describe('Draw the alignment grid behind the plot.'),
152
+ wave: f
153
+ .enum('sine', [
154
+ { value: 'sine', label: 'Sine' },
155
+ { value: 'square', label: 'Square' },
156
+ ])
157
+ .describe('Waveform the generator emits.'),
158
+ title: f.string('My experiment'),
159
+ tint: f.color('#3a86ff').describe('Plot line color.'),
160
+ });
161
+
162
+ /** A leaf with a `describe(...)` gets an \u24d8 beside its label whose tooltip
163
+ * carries the text; leaves without one stay bare. */
164
+ export const Described: Story = {
165
+ render: () => {
166
+ const schema = resolveConfigSchema(described, []);
167
+ const [config, setConfig] = useState<Record<string, unknown>>(described.defaults());
168
+ return (
169
+ <ControlPanel
170
+ schema={schema}
171
+ config={config}
172
+ setConfig={(key, value) => setConfig((prev) => ({ ...prev, [key as string]: value }))}
173
+ />
174
+ );
175
+ },
176
+ };
177
+
144
178
  const conditional = f.schema({
145
179
  showGrid: f.boolean(true),
146
- cellSize: f.number(20).range(5, 80).step(5).label('Grid spacing').showIf((c) => c.showGrid === true),
180
+ cellSize: f
181
+ .number(20)
182
+ .range(5, 80)
183
+ .step(5)
184
+ .label('Grid spacing')
185
+ .showIf((c) => c.showGrid === true),
147
186
  });
148
187
 
149
188
  /** `showIf` hides the row while the value stays in config. Toggle the
@@ -1,10 +1,10 @@
1
1
  import { fireEvent, render, screen } from '@testing-library/react';
2
- import { afterEach, describe, expect, it, vi } from 'vitest';
3
2
  import { TOOL_PREF_KINDS, type ToolPrefKind } from '@weasel-js/core';
4
3
  import type { PrefLeaf } from '@weasel-js/ui';
5
- import type { ResolvedConfig } from '../config/types';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
6
5
  import { f } from '../config/builder';
7
6
  import { resolveConfigSchema } from '../config/resolve';
7
+ import type { ResolvedConfig } from '../config/types';
8
8
  import { ControlPanel } from './ControlPanel';
9
9
  import type { ConfigField } from './types';
10
10
 
@@ -188,6 +188,21 @@ describe('<ControlPanel> schema', () => {
188
188
  render(<ControlPanel schema={schema} config={{}} setConfig={vi.fn()} />);
189
189
  expect(screen.getByLabelText('Tint')).toHaveValue('#123456');
190
190
  });
191
+
192
+ it('hangs a described leaf\u2019s description off a help affordance, and leaves an undescribed leaf bare', () => {
193
+ const schema = resolveConfigSchema(
194
+ f.schema({
195
+ showGrid: f.boolean(true).describe('Draw the alignment grid.'),
196
+ snap: f.boolean(true),
197
+ }),
198
+ [],
199
+ );
200
+ render(
201
+ <ControlPanel schema={schema} config={{ showGrid: true, snap: true }} setConfig={vi.fn()} />,
202
+ );
203
+ expect(screen.getByRole('button', { name: 'About Show grid' })).toBeInTheDocument();
204
+ expect(screen.getAllByRole('button', { name: /^About / })).toHaveLength(1);
205
+ });
191
206
  });
192
207
 
193
208
  describe('<ControlPanel> renderers', () => {
@@ -205,7 +220,7 @@ describe('<ControlPanel> renderers', () => {
205
220
  expect(screen.queryByText('by-kind')).not.toBeInTheDocument();
206
221
  });
207
222
 
208
- it('a lab renderer for the path beats the node\'s own .render', () => {
223
+ it("a lab renderer for the path beats the node's own .render", () => {
209
224
  const schema = resolveConfigSchema(
210
225
  f.schema({ tint: f.color('#ffffff').render(() => <span>by-node</span>) }),
211
226
  [],
@@ -252,7 +267,7 @@ describe('<ControlPanel> renderers', () => {
252
267
 
253
268
  it('a renderer returning null collapses the row', () => {
254
269
  const schema = resolveConfigSchema(f.schema({ tint: f.color('#ffffff') }), []);
255
- const { container } = render(
270
+ render(
256
271
  <ControlPanel
257
272
  schema={schema}
258
273
  config={{ tint: '#ffffff' }}
@@ -260,7 +275,7 @@ describe('<ControlPanel> renderers', () => {
260
275
  renderers={{ color: () => null }}
261
276
  />,
262
277
  );
263
- expect(container.querySelector('.lk-property-row')).toBeNull();
278
+ expect(screen.queryByText('Tint')).toBeNull();
264
279
  });
265
280
  });
266
281
 
@@ -274,7 +289,11 @@ describe('<ControlPanel> visibility and sections', () => {
274
289
  [],
275
290
  );
276
291
  const { rerender } = render(
277
- <ControlPanel schema={schema} config={{ showGrid: true, cellSize: 20 }} setConfig={vi.fn()} />,
292
+ <ControlPanel
293
+ schema={schema}
294
+ config={{ showGrid: true, cellSize: 20 }}
295
+ setConfig={vi.fn()}
296
+ />,
278
297
  );
279
298
  expect(screen.getByLabelText('Cell size')).toBeInTheDocument();
280
299
  rerender(
@@ -302,11 +321,11 @@ describe('<ControlPanel> visibility and sections', () => {
302
321
  f.schema({ showGrid: f.boolean(true), seed: f.number(0).section('Advanced') }),
303
322
  [],
304
323
  );
305
- const { container } = render(
324
+ render(
306
325
  <ControlPanel schema={schema} config={{ showGrid: true, seed: 0 }} setConfig={vi.fn()} />,
307
326
  );
308
327
  expect(screen.getByText('Advanced')).toBeInTheDocument();
309
- const group = container.querySelector('.lk-property-group');
328
+ const group = screen.getByRole('heading', { name: 'Advanced' }).parentElement;
310
329
  expect(group).not.toBeNull();
311
330
  expect(group?.textContent).toContain('Seed');
312
331
  expect(group?.textContent).not.toContain('Show grid');
@@ -335,7 +354,10 @@ describe('<ControlPanel> built-in kind coverage', () => {
335
354
  schema={schemaWith(
336
355
  { kind: 'paint', name: 'Fill', description: '', default: null },
337
356
  {
338
- kind: 'object', name: 'Stroke', description: '', default: {},
357
+ kind: 'object',
358
+ name: 'Stroke',
359
+ description: '',
360
+ default: {},
339
361
  children: { width: { kind: 'number', name: 'Width', description: '', default: 1 } },
340
362
  },
341
363
  )}
@@ -360,3 +382,41 @@ describe('<ControlPanel> built-in kind coverage', () => {
360
382
  ).toThrow(/spline/);
361
383
  });
362
384
  });
385
+
386
+ describe('<ControlPanel> packing', () => {
387
+ const schema = resolveConfigSchema(
388
+ f.schema({ width: f.number(256), title: f.string('a'), tint: f.color('#ff0000') }),
389
+ [],
390
+ );
391
+ const config = { width: 256, title: 'a', tint: '#ff0000' };
392
+ // jsdom lays out no grid, so the class the grid rules key off is the proxy
393
+ // for "these rows pair" — a layout assertion is not available here.
394
+ const list = (container: HTMLElement) => container.querySelector('.lk-control-panel');
395
+ const rowOf = (name: string) => screen.getByLabelText(name).closest('label');
396
+
397
+ it('packs two rows to a line by default', () => {
398
+ const { container } = render(
399
+ <ControlPanel schema={schema} config={config} setConfig={vi.fn()} />,
400
+ );
401
+ expect(list(container)?.className).toMatch(/listPairs/);
402
+ expect(rowOf('Width')?.className).not.toMatch(/span/);
403
+ });
404
+
405
+ it('gives every row its own line under one-up', () => {
406
+ const { container } = render(
407
+ <ControlPanel schema={schema} config={config} setConfig={vi.fn()} pack="one-up" />,
408
+ );
409
+ expect(list(container)?.className).not.toMatch(/listPairs/);
410
+ });
411
+
412
+ it('spans the rows that need the width under auto, and pairs the rest', () => {
413
+ render(<ControlPanel schema={schema} config={config} setConfig={vi.fn()} pack="auto" />);
414
+ expect(rowOf('Title')?.className).toMatch(/span/);
415
+ expect(rowOf('Width')?.className).not.toMatch(/span/);
416
+ });
417
+
418
+ it('passes its layout down to the rows', () => {
419
+ render(<ControlPanel schema={schema} config={config} setConfig={vi.fn()} layout="inline" />);
420
+ expect(rowOf('Width')?.className).toMatch(/rowInline/);
421
+ });
422
+ });