@weasel-js/labkit 1.5.1 → 1.6.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 (160) hide show
  1. package/README.md +3 -7
  2. package/dist/_dts/{CanvasStackContext-CJNMS6mz.d.ts → CanvasStackContext-DQgcH1ny.d.ts} +1 -1
  3. package/dist/_dts/LayerList.d-2c9yup5Y.d.ts +83 -0
  4. package/dist/_dts/{PrefsForm.d-DPy9I8It.d.ts → PrefsRow.d-DFpPIeIC.d.ts} +16 -1
  5. package/dist/_dts/{Select.d-Gwr8fLpK.d.ts → Select.d-BchGMC2Y.d.ts} +129 -12
  6. package/dist/_dts/{frac-B4okC2lX.d.ts → frac-DTRq7Xpb.d.ts} +31 -13
  7. package/dist/_dts/inDialog-D5ZKGjiD.d.ts +23 -0
  8. package/dist/_dts/{index-CSJoj4LC.d.ts → index-B0--8PEM.d.ts} +5 -4
  9. package/dist/_dts/index-DINmCuLA.d.ts +453 -0
  10. package/dist/_dts/number.d-OeQYQoUO.d.ts +28 -0
  11. package/dist/_dts/{types-DzoNY1pO.d.ts → types-B7y8hX1D.d.ts} +6 -6
  12. package/dist/_dts/{usePanZoom-Pzf9f2jh.d.ts → usePanZoom-Bq73pwFe.d.ts} +4 -47
  13. package/dist/_dts/{useTrialState-uLlmaccn.d.ts → useTrialState-CP4jfuKl.d.ts} +1 -1
  14. package/dist/canvas/index.d.ts +6 -5
  15. package/dist/canvas/index.js +1 -1
  16. package/dist/chrome/index.d.ts +5 -4
  17. package/dist/chrome/index.js +7 -7
  18. package/dist/chunk-2RVCB2PT.js +3 -0
  19. package/dist/chunk-2RVCB2PT.js.map +1 -0
  20. package/dist/{chunk-UN4U4P63.js → chunk-3FI7Y4R4.js} +9 -9
  21. package/dist/chunk-3FI7Y4R4.js.map +1 -0
  22. package/dist/{chunk-7PFH3CNM.js → chunk-AWH3SRPW.js} +31 -5
  23. package/dist/chunk-AWH3SRPW.js.map +1 -0
  24. package/dist/{chunk-ZDHY47EV.js → chunk-BVL7Y574.js} +25 -9
  25. package/dist/chunk-BVL7Y574.js.map +1 -0
  26. package/dist/{chunk-IHUVQ3TX.js → chunk-H5VIV7MQ.js} +31 -27
  27. package/dist/chunk-H5VIV7MQ.js.map +1 -0
  28. package/dist/chunk-HFRE4QQV.js +400 -0
  29. package/dist/chunk-HFRE4QQV.js.map +1 -0
  30. package/dist/{chunk-IXTQI7JX.js → chunk-MOLIUNVH.js} +4 -4
  31. package/dist/{chunk-IXTQI7JX.js.map → chunk-MOLIUNVH.js.map} +1 -1
  32. package/dist/{chunk-CEIUBPPW.js → chunk-TT5C5S6R.js} +3 -86
  33. package/dist/chunk-TT5C5S6R.js.map +1 -0
  34. package/dist/chunk-USO6TERP.js +90 -0
  35. package/dist/chunk-USO6TERP.js.map +1 -0
  36. package/dist/{chunk-PHK4W562.js → chunk-VQ47KWD5.js} +3 -3
  37. package/dist/{chunk-PHK4W562.js.map → chunk-VQ47KWD5.js.map} +1 -1
  38. package/dist/chunk-WUJXJC5O.js +14568 -0
  39. package/dist/chunk-WUJXJC5O.js.map +1 -0
  40. package/dist/{chunk-EHGVROVV.js → chunk-ZORVNOKX.js} +6 -25
  41. package/dist/chunk-ZORVNOKX.js.map +1 -0
  42. package/dist/config/index.d.ts +53 -11
  43. package/dist/config/index.js +5 -5
  44. package/dist/controls/index.d.ts +6 -3
  45. package/dist/controls/index.js +4 -4
  46. package/dist/dragdrop/index.d.ts +4 -3
  47. package/dist/index.d.ts +139 -114
  48. package/dist/index.js +470 -479
  49. package/dist/index.js.map +1 -1
  50. package/dist/layers/index.d.ts +6 -35
  51. package/dist/layers/index.js +2 -3
  52. package/dist/loupe/index.d.ts +6 -5
  53. package/dist/passthrough/weasel-ui.d.ts +421 -215
  54. package/dist/passthrough/weasel-ui.js +1 -1
  55. package/dist/primitives/index.d.ts +2 -13
  56. package/dist/primitives/index.js +4 -4
  57. package/dist/state/index.d.ts +6 -5
  58. package/dist/state/index.js +5 -5
  59. package/dist/styles.css +410 -190
  60. package/dist/undo/index.d.ts +4 -3
  61. package/package.json +9 -13
  62. package/src/annotations/frac.test.ts +7 -0
  63. package/src/annotations/paint.test.ts +21 -0
  64. package/src/annotations/paint.ts +12 -0
  65. package/src/annotations/store.test.ts +31 -0
  66. package/src/annotations/types.ts +3 -2
  67. package/src/canvas/Stage.test.tsx +18 -0
  68. package/src/canvas/index.ts +0 -9
  69. package/src/chrome/labTypes.ts +1 -1
  70. package/src/chrome/regions/PaletteRegion.test.tsx +18 -0
  71. package/src/chrome/regions/PaletteRegion.tsx +5 -4
  72. package/src/chrome/regions/SidebarRegion.less +197 -0
  73. package/src/chrome/regions/SidebarRegion.test.tsx +20 -0
  74. package/src/chrome/regions/SidebarRegion.tsx +7 -2
  75. package/src/chrome/regions/TitleBarRegion.tsx +1 -1
  76. package/src/chrome/types.ts +13 -4
  77. package/src/config/autoConfig.ts +2 -2
  78. package/src/config/builder.ts +23 -5
  79. package/src/config/entry.test.ts +8 -0
  80. package/src/config/index.ts +2 -0
  81. package/src/config/rules.ts +2 -0
  82. package/src/config/sectionTree.test.ts +146 -0
  83. package/src/config/sectionTree.ts +124 -0
  84. package/src/config/types.ts +4 -4
  85. package/src/controls/AutoControls.stories.tsx +6 -6
  86. package/src/controls/ControlPanel.less +27 -0
  87. package/src/controls/ControlPanel.stories.tsx +95 -1
  88. package/src/controls/ControlPanel.test.tsx +82 -112
  89. package/src/controls/ControlPanel.tsx +159 -396
  90. package/src/controls/inDialog.test.tsx +82 -0
  91. package/src/controls/inDialog.tsx +46 -0
  92. package/src/controls/index.ts +1 -0
  93. package/src/index.test.ts +0 -2
  94. package/src/index.ts +14 -35
  95. package/src/instrument/types.ts +5 -2
  96. package/src/lab/Lab.nebula.test.tsx +31 -0
  97. package/src/lab/Lab.tsx +57 -18
  98. package/src/lab/LabBoundary.test.tsx +66 -0
  99. package/src/lab/LabBoundary.tsx +56 -0
  100. package/src/lab/LabHeader.tsx +4 -23
  101. package/src/lab/LabRoot.tsx +2 -3
  102. package/src/lab/LabShell.less +0 -1
  103. package/src/lab/LabShell.stories.tsx +15 -0
  104. package/src/lab/LabShell.test.tsx +18 -0
  105. package/src/lab/LabShell.tsx +14 -1
  106. package/src/lab/LabSwitcher.less +1 -1
  107. package/src/lab/LabSwitcher.test.tsx +41 -0
  108. package/src/lab/LabSwitcher.tsx +30 -8
  109. package/src/lab/index.ts +1 -0
  110. package/src/layers/AGENTS.md +18 -74
  111. package/src/layers/index.ts +4 -2
  112. package/src/passthrough/weasel-ui.ts +25 -10
  113. package/src/primitives/FpsMeter.less +6 -1
  114. package/src/primitives/FpsMeter.test.tsx +12 -4
  115. package/src/primitives/FpsMeter.tsx +6 -1
  116. package/src/primitives/JobProgress.stories.tsx +1 -1
  117. package/src/primitives/StatusBar.less +6 -0
  118. package/src/primitives/index.ts +0 -2
  119. package/src/specimen/Specimen.tsx +42 -56
  120. package/src/state/types.ts +8 -2
  121. package/src/state/view.test.ts +4 -4
  122. package/src/styles.less +4 -2
  123. package/src/theme/Interstellar.stories.tsx +8 -9
  124. package/src/theme/base.less +3 -20
  125. package/src/theme/interstellar.test.ts +6 -3
  126. package/src/theme/interstellar.theme.json +0 -56
  127. package/src/theme/interstellar.ts +2 -2
  128. package/src/trial/Trial.canvas.test.tsx +9 -5
  129. package/src/trial/Trial.config.test.tsx +1 -1
  130. package/src/trial/Trial.tsx +30 -8
  131. package/dist/_dts/index-BNdz-tNd.d.ts +0 -423
  132. package/dist/chunk-7PFH3CNM.js.map +0 -1
  133. package/dist/chunk-CEIUBPPW.js.map +0 -1
  134. package/dist/chunk-EH7UEQV7.js +0 -590
  135. package/dist/chunk-EH7UEQV7.js.map +0 -1
  136. package/dist/chunk-EHGVROVV.js.map +0 -1
  137. package/dist/chunk-IHUVQ3TX.js.map +0 -1
  138. package/dist/chunk-MDVTK5U3.js +0 -170
  139. package/dist/chunk-MDVTK5U3.js.map +0 -1
  140. package/dist/chunk-PJ7WEQBJ.js +0 -18
  141. package/dist/chunk-PJ7WEQBJ.js.map +0 -1
  142. package/dist/chunk-T3EC2DXD.js +0 -12628
  143. package/dist/chunk-T3EC2DXD.js.map +0 -1
  144. package/dist/chunk-UN4U4P63.js.map +0 -1
  145. package/dist/chunk-ZDHY47EV.js.map +0 -1
  146. package/dist/ui/layers/index.d.ts +0 -66
  147. package/dist/ui/layers/index.js +0 -3
  148. package/dist/ui/layers/index.js.map +0 -1
  149. package/src/canvas/useOrbit.test.ts +0 -167
  150. package/src/canvas/useOrbit.ts +0 -156
  151. package/src/lab/useSystemMode.ts +0 -30
  152. package/src/layers/LayerList.less +0 -93
  153. package/src/layers/LayerList.stories.tsx +0 -66
  154. package/src/layers/LayerList.test.tsx +0 -232
  155. package/src/layers/LayerList.tsx +0 -268
  156. package/src/primitives/Sidebar.less +0 -112
  157. package/src/primitives/Sidebar.stories.tsx +0 -38
  158. package/src/primitives/Sidebar.test.tsx +0 -74
  159. package/src/primitives/Sidebar.tsx +0 -35
  160. package/src/ui/layers/index.ts +0 -5
@@ -187,7 +187,11 @@ describe('<ControlPanel> schema', () => {
187
187
  [],
188
188
  );
189
189
  render(
190
- <ControlPanel schema={schema} config={{ showGrid: true, snap: false }} setConfig={setConfig} />,
190
+ <ControlPanel
191
+ schema={schema}
192
+ config={{ showGrid: true, snap: false }}
193
+ setConfig={setConfig}
194
+ />,
191
195
  );
192
196
  expect(screen.getByRole('checkbox', { name: 'Show grid' })).toBeInTheDocument();
193
197
  const sw = screen.getByRole('switch', { name: 'Snap' });
@@ -478,7 +482,27 @@ describe('<ControlPanel> format', () => {
478
482
  setConfig={vi.fn()}
479
483
  />,
480
484
  );
481
- expect(screen.getByDisplayValue('2.0M')).toBeInTheDocument();
485
+ expect(screen.getByDisplayValue('2.00M')).toBeInTheDocument();
486
+ });
487
+ });
488
+
489
+ describe('<ControlPanel> group descriptions', () => {
490
+ it('draws a named group’s describe() above its rows', () => {
491
+ const schema = resolveConfigSchema(
492
+ f.schema({
493
+ grid: f
494
+ .group({ cellSize: f.number(20) })
495
+ .label('Grid')
496
+ .describe('How the background lattice is spaced.'),
497
+ }),
498
+ [],
499
+ );
500
+ render(
501
+ <ControlPanel schema={schema} config={{ grid: { cellSize: 20 } }} setConfig={vi.fn()} />,
502
+ );
503
+ const note = screen.getByText('How the background lattice is spaced.');
504
+ const field = screen.getByLabelText('Cell size');
505
+ expect(note.compareDocumentPosition(field) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
482
506
  });
483
507
  });
484
508
 
@@ -675,7 +699,7 @@ describe('<ControlPanel> nested groups', () => {
675
699
  });
676
700
 
677
701
  describe('<ControlPanel> auto', () => {
678
- it('renders a pin dot whether or not it was given a set of auto paths', () => {
702
+ it('renders a label toggle whether or not it was given a set of auto paths', () => {
679
703
  render(
680
704
  <ControlPanel
681
705
  schema={resolveConfigSchema(f.schema({ gap: f.number(12).range(0, 48) }), [])}
@@ -708,7 +732,7 @@ describe('<ControlPanel> auto', () => {
708
732
  expect(setConfig).toHaveBeenCalledWith('gap', 12);
709
733
  });
710
734
 
711
- it("reads the resolver for an auto row's readout", () => {
735
+ it('reads out the bare word, resolver or not', () => {
712
736
  render(
713
737
  <ControlPanel
714
738
  schema={resolveConfigSchema(
@@ -725,19 +749,8 @@ describe('<ControlPanel> auto', () => {
725
749
  setConfig={() => {}}
726
750
  />,
727
751
  );
728
- expect(screen.getByText('auto · 18')).toBeInTheDocument();
729
- });
730
-
731
- it('reads "auto" alone for a path with no resolver', () => {
732
- render(
733
- <ControlPanel
734
- schema={resolveConfigSchema(f.schema({ cols: f.number(3).range(0, 8) }), [])}
735
- config={{ cols: 3 }}
736
- auto={new Set(['cols'])}
737
- setConfig={() => {}}
738
- />,
739
- );
740
752
  expect(screen.getByText('auto')).toBeInTheDocument();
753
+ expect(screen.queryByText(/auto · /)).toBeNull();
741
754
  });
742
755
 
743
756
  it('writes the sentinel when the dot turns a row auto', async () => {
@@ -794,11 +807,11 @@ describe('<ControlPanel> auto', () => {
794
807
  expect(screen.queryByRole('button', { name: /Seed/ })).toBeNull();
795
808
  });
796
809
 
797
- it('reports what the instrument reads when a resolver depends on another auto path', () => {
810
+ it('draws what the instrument reads when a resolver depends on another auto path', () => {
798
811
  // `width` is auto with no resolver, so the instrument sees `undefined` and
799
812
  // `gap` resolves from that. Re-running gap's resolver against the raw
800
- // config would instead read the pinned 640 and report a number nobody got.
801
- render(
813
+ // config would instead read the pinned 640 and draw a number nobody got.
814
+ const { container } = render(
802
815
  <ControlPanel
803
816
  schema={resolveConfigSchema(
804
817
  f.schema({
@@ -806,7 +819,7 @@ describe('<ControlPanel> auto', () => {
806
819
  gap: f
807
820
  .number(12)
808
821
  .range(0, 48)
809
- .auto((c) => (c.width === undefined ? -1 : (c.width as number) / 64)),
822
+ .auto((c) => (c.width === undefined ? 5 : (c.width as number) / 64)),
810
823
  }),
811
824
  [],
812
825
  )}
@@ -815,27 +828,7 @@ describe('<ControlPanel> auto', () => {
815
828
  setConfig={() => {}}
816
829
  />,
817
830
  );
818
- expect(screen.getByText('auto · -1')).toBeInTheDocument();
819
- expect(screen.queryByText('auto · 10')).toBeNull();
820
- });
821
-
822
- it('does not repeat a resolved value the control itself already shows', () => {
823
- render(
824
- <ControlPanel
825
- schema={resolveConfigSchema(
826
- f.schema({ caption: f.string('typed').auto(() => 'eight across') }),
827
- [],
828
- )}
829
- config={{ caption: 'typed' }}
830
- auto={new Set(['caption'])}
831
- setConfig={() => {}}
832
- />,
833
- );
834
- // The field below the readout is already showing it, and the readout slot
835
- // is one narrow column — a long string wraps it onto two lines.
836
- expect(screen.getByText('auto')).toBeInTheDocument();
837
- expect(screen.queryByText(/auto · /)).toBeNull();
838
- expect(screen.getByDisplayValue('eight across')).toBeInTheDocument();
831
+ expect(container.querySelector('input[type=range]')).toHaveValue('5');
839
832
  });
840
833
 
841
834
  it('draws an auto control at the resolved value, not the value underneath it', () => {
@@ -881,7 +874,7 @@ describe('<ControlPanel> auto', () => {
881
874
  });
882
875
 
883
876
  // Every kind, because the panel wraps some rows before rendering them and a
884
- // wrapper that drops the auto props leaves that kind silently unghosted.
877
+ // wrapper that drops the auto props leaves that kind silently pinned.
885
878
  it.each([
886
879
  ['number', f.number(3).range(0, 8)],
887
880
  ['slider', f.number(3).range(0, 8).slider()],
@@ -890,7 +883,7 @@ describe('<ControlPanel> auto', () => {
890
883
  ['radio', f.enum('a', ['a', 'b']).radio()],
891
884
  ['string', f.string('hi')],
892
885
  ['color', f.color('#3a86ff')],
893
- ])('marks a %s row auto and gives it a dot and a path', (_kind, node) => {
886
+ ])('marks a %s row auto and gives it a label toggle', (_kind, node) => {
894
887
  const { container } = render(
895
888
  <ControlPanel
896
889
  schema={resolveConfigSchema(f.schema({ thing: node.label('Thing') }), [])}
@@ -899,76 +892,11 @@ describe('<ControlPanel> auto', () => {
899
892
  setConfig={() => {}}
900
893
  />,
901
894
  );
902
- const row = container.querySelector('[data-auto-path="thing"]');
903
- expect(row).not.toBeNull();
904
- expect(row?.className).toMatch(/rowAuto/);
895
+ expect(container.querySelector('[class*=rowAuto]')).not.toBeNull();
905
896
  expect(screen.getByRole('button', { name: 'Pin Thing' })).toBeInTheDocument();
906
897
  });
907
898
  });
908
899
 
909
- describe('<ControlPanel> shift-click', () => {
910
- const panel = (props = {}) =>
911
- render(
912
- <ControlPanel
913
- schema={resolveConfigSchema(f.schema({ gap: f.number(12).range(0, 48) }), [])}
914
- config={{ gap: 12 }}
915
- auto={new Set()}
916
- setConfig={() => {}}
917
- {...props}
918
- />,
919
- );
920
-
921
- it('toggles a row on shift-pointerdown and suppresses the control it landed on', () => {
922
- const setConfig = vi.fn();
923
- const { container } = panel({ setConfig });
924
- const track = container.querySelector('input[type=range]') as HTMLInputElement;
925
- const ev = new PointerEvent('pointerdown', { bubbles: true, cancelable: true, shiftKey: true });
926
- track.dispatchEvent(ev);
927
-
928
- expect(setConfig).toHaveBeenCalledWith('gap', auto);
929
- // A PROXY, not the real claim. jsdom synthesizes neither a label's click
930
- // retargeting nor a range input's drag, so "the slider did not move" cannot
931
- // fail here. What is assertable is that the default was suppressed; the
932
- // browser half is covered by the story.
933
- expect(ev.defaultPrevented).toBe(true);
934
- });
935
-
936
- it('leaves an ordinary pointerdown alone', () => {
937
- const setConfig = vi.fn();
938
- const { container } = panel({ setConfig });
939
- container
940
- .querySelector('input[type=range]')
941
- ?.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true }));
942
- expect(setConfig).not.toHaveBeenCalled();
943
- });
944
-
945
- it('ignores shift-pointerdown on a manual row', () => {
946
- const setConfig = vi.fn();
947
- const { container } = panel({
948
- schema: resolveConfigSchema(f.schema({ seed: f.number(1).manual().range(0, 8) }), []),
949
- config: { seed: 1 },
950
- setConfig,
951
- });
952
- container
953
- .querySelector('input')
954
- ?.dispatchEvent(
955
- new PointerEvent('pointerdown', { bubbles: true, cancelable: true, shiftKey: true }),
956
- );
957
- expect(setConfig).not.toHaveBeenCalled();
958
- });
959
-
960
- it('pins a shift-clicked auto row back at its stored value', () => {
961
- const setConfig = vi.fn();
962
- const { container } = panel({ config: { gap: 24 }, auto: new Set(['gap']), setConfig });
963
- container
964
- .querySelector('input[type=range]')
965
- ?.dispatchEvent(
966
- new PointerEvent('pointerdown', { bubbles: true, cancelable: true, shiftKey: true }),
967
- );
968
- expect(setConfig).toHaveBeenCalledWith('gap', 24);
969
- });
970
- });
971
-
972
900
  describe('<ControlPanel> pair', () => {
973
901
  const paired = () =>
974
902
  resolveConfigSchema(
@@ -986,9 +914,13 @@ describe('<ControlPanel> pair', () => {
986
914
  const row = screen.getByText('Offset').closest('div') as HTMLElement;
987
915
  expect(within(row).getByLabelText('X')).toHaveValue(10);
988
916
  expect(within(row).getByLabelText('Y')).toHaveValue(20);
989
- // The pair names the row, so neither leaf's own name is drawn beside it.
990
- expect(screen.queryByText('X')).toBeNull();
991
- expect(screen.queryByText('Y')).toBeNull();
917
+ });
918
+
919
+ it('captions each cell of a pair with its own name, so a paired switch is not left unnamed', () => {
920
+ render(<ControlPanel schema={paired()} config={config} setConfig={vi.fn()} />);
921
+ const row = screen.getByText('Offset').closest('div') as HTMLElement;
922
+ expect(within(row).getByText('X')).toHaveClass('lk-pair-label');
923
+ expect(within(row).getByText('Y')).toHaveClass('lk-pair-label');
992
924
  });
993
925
 
994
926
  it('writes each cell of a paired row to its own path', () => {
@@ -998,6 +930,23 @@ describe('<ControlPanel> pair', () => {
998
930
  expect(setConfig).toHaveBeenCalledWith('y', 30);
999
931
  });
1000
932
 
933
+ it('keeps a segmented leaf segmented when it shares a row, and gives that row the width', () => {
934
+ const schema = resolveConfigSchema(
935
+ f.schema({
936
+ camera: f.enum<'2d' | '3d'>('3d', ['2d', '3d']).radio().label('').pair('Camera'),
937
+ hold: f.boolean(true).toggle().label('Hold front').pair('Camera'),
938
+ }),
939
+ [],
940
+ );
941
+ render(
942
+ <ControlPanel schema={schema} config={{ camera: '3d', hold: true }} setConfig={vi.fn()} />,
943
+ );
944
+ const row = screen.getByText('Camera').closest('div') as HTMLElement;
945
+ expect(within(row).getByRole('radio', { name: '2d' })).toBeInTheDocument();
946
+ expect(within(row).queryByRole('combobox')).toBeNull();
947
+ expect(row.className).toMatch(/span/);
948
+ });
949
+
1001
950
  it('leaves an unpaired sibling on its own row', () => {
1002
951
  render(<ControlPanel schema={paired()} config={config} setConfig={vi.fn()} />);
1003
952
  const row = screen.getByText('Offset').closest('div') as HTMLElement;
@@ -1121,3 +1070,24 @@ describe('<ControlPanel> color alpha', () => {
1121
1070
  expect(screen.queryByLabelText('Tint opacity')).toBeNull();
1122
1071
  });
1123
1072
  });
1073
+
1074
+ describe('<ControlPanel> title, stance and tone', () => {
1075
+ const fields: ConfigField[] = [{ key: 'on', label: 'On', type: 'checkbox', default: true }];
1076
+
1077
+ it('renders the bare list when given none of them', () => {
1078
+ const { container } = render(<ControlPanel fields={fields} config={{ on: true }} setConfig={vi.fn()} />);
1079
+ expect(container.firstElementChild?.classList.contains('lk-control-panel')).toBe(true);
1080
+ expect(container.querySelector('[data-stance], h2')).toBeNull();
1081
+ });
1082
+
1083
+ it('wraps its rows in a panel carrying them when asked', () => {
1084
+ const { container } = render(
1085
+ <ControlPanel title="View" stance="scope" tone={1} fields={fields} config={{ on: true }} setConfig={vi.fn()} />,
1086
+ );
1087
+ const panel = container.firstElementChild as HTMLElement;
1088
+ expect(panel.dataset.stance).toBe('scope');
1089
+ expect(panel.dataset.tone).toBe('1');
1090
+ expect(screen.getByRole('heading', { name: 'View' })).toBeTruthy();
1091
+ expect(panel.querySelector('.lk-control-panel')).not.toBeNull();
1092
+ });
1093
+ });