@weasel-js/labkit 1.4.4 → 1.5.1
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.
- package/README.md +54 -6
- package/dist/_dts/{CanvasStackContext-kTEZvEgE.d.ts → CanvasStackContext-CJNMS6mz.d.ts} +1 -1
- package/dist/_dts/{PrefsForm.d-DDHFANkc.d.ts → PrefsForm.d-DPy9I8It.d.ts} +4 -0
- package/dist/_dts/Select.d-Gwr8fLpK.d.ts +453 -0
- package/dist/_dts/{frac-z7ker2Vx.d.ts → frac-B4okC2lX.d.ts} +291 -31
- package/dist/_dts/{index-Blv9uQzu.d.ts → index-BNdz-tNd.d.ts} +105 -17
- package/dist/_dts/index-CSJoj4LC.d.ts +248 -0
- package/dist/_dts/{types-B9_zrHmb.d.ts → types-DzoNY1pO.d.ts} +91 -4
- package/dist/_dts/usePanZoom-Pzf9f2jh.d.ts +160 -0
- package/dist/_dts/useTrialState-uLlmaccn.d.ts +212 -0
- package/dist/canvas/index.d.ts +18 -129
- package/dist/canvas/index.js +4 -2
- package/dist/chrome/index.d.ts +5 -93
- package/dist/chrome/index.js +10 -6
- package/dist/chunk-7PFH3CNM.js +150 -0
- package/dist/chunk-7PFH3CNM.js.map +1 -0
- package/dist/{chunk-I6JCVE24.js → chunk-CEIUBPPW.js} +143 -17
- package/dist/chunk-CEIUBPPW.js.map +1 -0
- package/dist/chunk-EH7UEQV7.js +590 -0
- package/dist/chunk-EH7UEQV7.js.map +1 -0
- package/dist/{chunk-MOM3GOVY.js → chunk-EHGVROVV.js} +125 -27
- package/dist/chunk-EHGVROVV.js.map +1 -0
- package/dist/{chunk-TJ7QY3OC.js → chunk-ERFMC6WU.js} +160 -78
- package/dist/chunk-ERFMC6WU.js.map +1 -0
- package/dist/{chunk-CTRKTLYZ.js → chunk-H6ZAOWNE.js} +11 -11
- package/dist/chunk-H6ZAOWNE.js.map +1 -0
- package/dist/{chunk-64ZCN3DA.js → chunk-IHUVQ3TX.js} +250 -160
- package/dist/chunk-IHUVQ3TX.js.map +1 -0
- package/dist/{chunk-FQMJUVHJ.js → chunk-IJJUK4FU.js} +53 -46
- package/dist/chunk-IJJUK4FU.js.map +1 -0
- package/dist/chunk-IXTQI7JX.js +737 -0
- package/dist/chunk-IXTQI7JX.js.map +1 -0
- package/dist/{chunk-BKFVHKJH.js → chunk-MDVTK5U3.js} +4 -4
- package/dist/{chunk-BKFVHKJH.js.map → chunk-MDVTK5U3.js.map} +1 -1
- package/dist/chunk-PHK4W562.js +72 -0
- package/dist/chunk-PHK4W562.js.map +1 -0
- package/dist/chunk-PJ7WEQBJ.js +18 -0
- package/dist/chunk-PJ7WEQBJ.js.map +1 -0
- package/dist/chunk-RWNDDKOH.js +682 -0
- package/dist/chunk-RWNDDKOH.js.map +1 -0
- package/dist/chunk-T3EC2DXD.js +12628 -0
- package/dist/chunk-T3EC2DXD.js.map +1 -0
- package/dist/{chunk-SMHP6XZ4.js → chunk-UN4U4P63.js} +10 -8
- package/dist/chunk-UN4U4P63.js.map +1 -0
- package/dist/chunk-WIG6XHJ7.js +35 -0
- package/dist/chunk-WIG6XHJ7.js.map +1 -0
- package/dist/chunk-Z76KEZAX.js +15 -0
- package/dist/chunk-Z76KEZAX.js.map +1 -0
- package/dist/chunk-ZDHY47EV.js +363 -0
- package/dist/chunk-ZDHY47EV.js.map +1 -0
- package/dist/config/index.d.ts +276 -0
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controls/index.d.ts +3 -4
- package/dist/controls/index.js +4 -3
- package/dist/dragdrop/index.d.ts +3 -5
- package/dist/index.d.ts +266 -413
- package/dist/index.js +1775 -2251
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +4 -6
- package/dist/layers/index.js +2 -2
- package/dist/loupe/index.d.ts +69 -20
- package/dist/loupe/index.js +2 -2
- package/dist/passthrough/weasel-ui.d.ts +175 -309
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +55 -6
- package/dist/primitives/index.js +6 -4
- package/dist/state/index.d.ts +13 -8
- package/dist/state/index.js +18 -13
- package/dist/state/index.js.map +1 -1
- package/dist/styles.css +223 -76
- package/dist/surface/index.d.ts +44 -7
- package/dist/surface/index.js +2 -1
- package/dist/ui/layers/index.js +1 -1
- package/dist/undo/index.d.ts +3 -5
- package/package.json +15 -8
- package/src/annotations/AnnotationOverlay.tsx +56 -5
- package/src/annotations/Annotations.less +4 -0
- package/src/annotations/Annotations.overlay.test.tsx +105 -6
- package/src/annotations/drawOne.test.ts +1 -1
- package/src/annotations/drawOne.ts +2 -2
- package/src/annotations/paint.test.ts +2 -2
- package/src/annotations/paint.ts +1 -1
- package/src/annotations/preload.ts +6 -0
- package/src/annotations/store.test.ts +18 -0
- package/src/annotations/store.ts +5 -4
- package/src/annotations/svgNodes.test.ts +2 -2
- package/src/annotations/svgNodes.ts +1 -0
- package/src/annotations/toolMap.test.ts +9 -5
- package/src/annotations/toolMap.ts +6 -3
- package/src/annotations/types.ts +6 -3
- package/src/canvas/CameraWheelContext.ts +12 -0
- package/src/canvas/CanvasStack.stories.tsx +1 -1
- package/src/canvas/CanvasStack.test.tsx +25 -0
- package/src/canvas/CanvasStack.tsx +31 -4
- package/src/canvas/Stage.less +18 -0
- package/src/canvas/Stage.test.tsx +186 -0
- package/src/canvas/Stage.tsx +134 -0
- package/src/canvas/camera.test.ts +17 -2
- package/src/canvas/camera.ts +9 -9
- package/src/canvas/index.ts +4 -0
- package/src/canvas/usePanZoom.test.ts +28 -0
- package/src/canvas/usePanZoom.ts +21 -9
- package/src/chrome/ChromeRegions.stories.tsx +1 -4
- package/src/chrome/LabChrome.tsx +130 -0
- package/src/chrome/builtins.test.ts +1 -0
- package/src/chrome/builtins.tsx +21 -31
- package/src/chrome/index.ts +16 -0
- package/src/chrome/labRegions.test.tsx +208 -0
- package/src/chrome/labTypes.ts +50 -0
- package/src/chrome/merge.ts +7 -9
- package/src/chrome/regions/PaletteRegion.test.tsx +6 -4
- package/src/chrome/regions/PaletteRegion.tsx +16 -9
- package/src/chrome/regions/SidebarRegion.stories.tsx +44 -0
- package/src/chrome/regions/SidebarRegion.tsx +25 -13
- package/src/chrome/regions/StatusRegion.tsx +15 -8
- package/src/chrome/regions/ToolbarRegion.tsx +34 -16
- package/src/chrome/types.ts +50 -20
- package/src/config/auto.test.ts +12 -0
- package/src/config/auto.ts +15 -0
- package/src/config/autoConfig.test.ts +107 -0
- package/src/config/autoConfig.ts +153 -0
- package/src/config/builder.test.ts +42 -0
- package/src/config/builder.ts +68 -17
- package/src/config/declarationEmit.fixture.ts +11 -0
- package/src/config/declarationEmit.test.ts +51 -0
- package/src/config/entry.test.ts +46 -0
- package/src/config/index.ts +16 -1
- package/src/config/nodeClasses.test.ts +18 -0
- package/src/config/path.ts +7 -2
- package/src/config/resolve.test.ts +25 -0
- package/src/config/resolve.ts +17 -1
- package/src/config/types.ts +34 -2
- package/src/config/useResolvedConfig.ts +26 -0
- package/src/controls/AutoControls.stories.tsx +88 -0
- package/src/controls/ControlPanel.stories.tsx +42 -1
- package/src/controls/ControlPanel.test.tsx +530 -4
- package/src/controls/ControlPanel.tsx +562 -82
- package/src/fake-indexeddb-auto.d.ts +5 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +66 -5
- package/src/instrument/serializers.test.ts +22 -0
- package/src/instrument/serializers.ts +40 -1
- package/src/instrument/types.ts +25 -1
- package/src/lab/Lab.chrome.test.tsx +16 -0
- package/src/lab/Lab.less +4 -13
- package/src/lab/Lab.persist.test.tsx +147 -0
- package/src/lab/Lab.stories.tsx +1 -3
- package/src/lab/Lab.surface.test.tsx +71 -3
- package/src/lab/Lab.test.tsx +168 -16
- package/src/lab/Lab.tsx +434 -129
- package/src/lab/LabContext.ts +8 -0
- package/src/lab/LabFit.stories.less +18 -0
- package/src/lab/LabFit.stories.tsx +219 -0
- package/src/lab/LabFullChrome.stories.tsx +1 -4
- package/src/lab/LabHeader.test.tsx +40 -9
- package/src/lab/LabHeader.tsx +7 -14
- package/src/lab/LabPalette.tsx +13 -26
- package/src/lab/LabRoot.test.tsx +56 -0
- package/src/lab/LabRoot.tsx +47 -0
- package/src/lab/LabShell.less +73 -15
- package/src/lab/LabShell.stories.tsx +50 -1
- package/src/lab/LabShell.tsx +18 -27
- package/src/lab/LabSwitcher.less +18 -11
- package/src/lab/LabSwitcher.test.tsx +14 -9
- package/src/lab/LabSwitcher.tsx +8 -4
- package/src/lab/Workspace.less +1 -1
- package/src/lab/Workspace.stories.tsx +1 -1
- package/src/lab/Workspace.surface.test.tsx +1 -0
- package/src/lab/fitCheck.test.ts +57 -0
- package/src/lab/fitCheck.ts +125 -0
- package/src/lab/index.ts +2 -0
- package/src/layers/LayerList.stories.tsx +1 -1
- package/src/loupe/AGENTS.md +17 -5
- package/src/loupe/Loupe.less +1 -1
- package/src/loupe/LoupeGestures.tsx +67 -0
- package/src/loupe/TrialLoupe.tsx +43 -28
- package/src/loupe/canvasLens.ts +1 -1
- package/src/loupe/index.ts +4 -0
- package/src/loupe/loupeActions.ts +98 -0
- package/src/loupe/types.ts +1 -1
- package/src/loupe/useLoupe.test.tsx +60 -3
- package/src/loupe/useLoupe.ts +29 -49
- package/src/passthrough/weasel-ui.ts +9 -0
- package/src/primitives/FloatingPanel.stories.tsx +14 -2
- package/src/primitives/FloatingPanel.test.tsx +47 -16
- package/src/primitives/FloatingPanel.tsx +11 -25
- package/src/primitives/FpsMeter.stories.tsx +1 -1
- package/src/primitives/JobProgress.stories.tsx +1 -1
- package/src/primitives/Legend.stories.tsx +1 -1
- package/src/primitives/ScaleIndicator.stories.tsx +1 -1
- package/src/primitives/Sidebar.less +7 -0
- package/src/primitives/Sidebar.stories.tsx +1 -1
- package/src/primitives/Split.less +14 -0
- package/src/primitives/Split.test.tsx +133 -0
- package/src/primitives/Split.tsx +175 -0
- package/src/primitives/StatusBar.stories.tsx +1 -1
- package/src/primitives/Toolbar.stories.tsx +1 -1
- package/src/primitives/ZoomControl.tsx +5 -2
- package/src/primitives/index.ts +2 -0
- package/src/specimen/Specimen.less +95 -0
- package/src/specimen/Specimen.stories.tsx +13 -0
- package/src/specimen/Specimen.test.tsx +13 -0
- package/src/specimen/Specimen.tsx +888 -0
- package/src/state/Persistence.stories.tsx +75 -0
- package/src/state/Persistence.tsx +33 -0
- package/src/state/SingletonExperiment.test.tsx +47 -53
- package/src/state/SingletonExperiment.tsx +24 -13
- package/src/state/adapterContract.ts +116 -0
- package/src/state/adapters.test.ts +113 -61
- package/src/state/adapters.ts +328 -69
- package/src/state/document.test.ts +71 -96
- package/src/state/document.ts +66 -52
- package/src/state/helpers.test.ts +1 -1
- package/src/state/index.ts +8 -0
- package/src/state/labRecords.test.ts +105 -0
- package/src/state/labRecords.ts +160 -0
- package/src/state/openLabStore.test.ts +658 -0
- package/src/state/openLabStore.ts +403 -0
- package/src/state/records.test.ts +234 -0
- package/src/state/records.ts +219 -0
- package/src/state/store.test.ts +169 -635
- package/src/state/store.ts +146 -163
- package/src/state/toolSlot.test.ts +1 -1
- package/src/state/types.ts +65 -17
- package/src/state/useOpenOnce.ts +68 -0
- package/src/state/usePersistedState.test.tsx +125 -0
- package/src/state/usePersistedState.ts +66 -0
- package/src/state/useTrialState.test.tsx +87 -3
- package/src/state/useTrialState.ts +9 -1
- package/src/state/view.test.ts +49 -15
- package/src/state/view.ts +18 -1
- package/src/styles.less +3 -0
- package/src/surface/AGENTS.md +39 -7
- package/src/surface/SurfaceContext.ts +26 -3
- package/src/surface/index.ts +2 -0
- package/src/surface/useSurfaceTile.test.tsx +1 -0
- package/src/surface/useSurfaceTile.ts +6 -5
- package/src/surface/useTiledSurface.test.tsx +231 -1
- package/src/surface/useTiledSurface.ts +76 -15
- package/src/theme/Interstellar.stories.tsx +9 -9
- package/src/theme/interstellar.test.ts +17 -6
- package/src/theme/interstellar.theme.json +180 -0
- package/src/theme/interstellar.ts +8 -5
- package/src/tools/labTool.ts +11 -0
- package/src/trial/Trial.annotations.persist.test.tsx +100 -58
- package/src/trial/Trial.annotations.test.tsx +59 -7
- package/src/trial/Trial.canvas.test.tsx +32 -0
- package/src/trial/Trial.config.test.tsx +75 -2
- package/src/trial/Trial.job.test.tsx +3 -3
- package/src/trial/Trial.less +5 -26
- package/src/trial/Trial.stories.tsx +5 -3
- package/src/trial/Trial.test.tsx +31 -4
- package/src/trial/Trial.trialId.test.tsx +23 -1
- package/src/trial/Trial.tsx +139 -52
- package/src/trial/TrialBody.tsx +17 -141
- package/src/trial/TrialChrome.tsx +24 -7
- package/src/trial/trialOps.test.ts +101 -1
- package/src/trial/trialOps.ts +67 -7
- package/dist/_dts/types-BP2OCcpg.d.ts +0 -155
- package/dist/_dts/types-x92Kfeme.d.ts +0 -62
- package/dist/_dts/useTrialState-D6Vb3T-g.d.ts +0 -99
- package/dist/chunk-2P6PP5N4.js +0 -553
- package/dist/chunk-2P6PP5N4.js.map +0 -1
- package/dist/chunk-64ZCN3DA.js.map +0 -1
- package/dist/chunk-CTRKTLYZ.js.map +0 -1
- package/dist/chunk-E44SU6XS.js +0 -50
- package/dist/chunk-E44SU6XS.js.map +0 -1
- package/dist/chunk-FQMJUVHJ.js.map +0 -1
- package/dist/chunk-I6JCVE24.js.map +0 -1
- package/dist/chunk-MOM3GOVY.js.map +0 -1
- package/dist/chunk-SMHP6XZ4.js.map +0 -1
- package/dist/chunk-TJ7QY3OC.js.map +0 -1
- package/dist/chunk-U3IYHIAE.js +0 -343
- package/dist/chunk-U3IYHIAE.js.map +0 -1
- package/dist/chunk-W3ECWC2K.js +0 -7335
- package/dist/chunk-W3ECWC2K.js.map +0 -1
- package/src/theme/interstellar.tokens.json +0 -61
package/src/lab/LabShell.less
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
display: flex;
|
|
3
3
|
flex-direction: column;
|
|
4
4
|
width: 100%;
|
|
5
|
+
// Fills a container of definite height. Where the container has none, the
|
|
6
|
+
// percentage drops out and the shell takes the viewport's height instead.
|
|
5
7
|
height: 100vh;
|
|
8
|
+
height: 100dvh;
|
|
9
|
+
max-height: 100%;
|
|
6
10
|
background: var(--wzl-surface);
|
|
7
11
|
}
|
|
8
12
|
|
|
@@ -35,42 +39,100 @@
|
|
|
35
39
|
.lk-shell-header-actions {
|
|
36
40
|
margin-left: auto;
|
|
37
41
|
display: flex;
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
42
|
+
// Every control here runs at the one control height and shares a middle.
|
|
43
|
+
// The row still hands the header its first item's baseline, which is what
|
|
44
|
+
// lines the controls' labels up with the title.
|
|
45
|
+
align-items: center;
|
|
41
46
|
gap: var(--wzl-space-sm);
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
// A toolbar dropped into the header is a run of header controls, not a strip of
|
|
50
|
+
// its own: it takes the header's control height rather than the compact one
|
|
51
|
+
// trial toolbars use, and none of the strip's box.
|
|
52
|
+
.lk-shell-header-actions .lk-toolbar {
|
|
53
|
+
--wzl-control-h: inherit;
|
|
54
|
+
min-height: 0;
|
|
55
|
+
padding: 0;
|
|
56
|
+
background: none;
|
|
57
|
+
border: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
.lk-shell-body {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
45
63
|
flex: 1;
|
|
46
64
|
min-height: 0;
|
|
65
|
+
min-width: 0;
|
|
47
66
|
overflow: auto;
|
|
48
67
|
padding: var(--lk-workspace-pad);
|
|
49
68
|
}
|
|
50
69
|
|
|
51
|
-
// `.lk-shell-body` is a block container, so `flex: 1` here resolves to nothing
|
|
52
|
-
// and the workspace's `height: 100%` collapses to zero.
|
|
53
70
|
.lk-lab__body {
|
|
54
71
|
display: flex;
|
|
55
|
-
|
|
72
|
+
flex: 1;
|
|
56
73
|
min-height: 0;
|
|
57
74
|
position: relative;
|
|
58
75
|
|
|
59
|
-
> .lk-workspace
|
|
76
|
+
> .lk-workspace,
|
|
77
|
+
> .lk-workspace-host {
|
|
78
|
+
min-width: 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.lk-lab__panes {
|
|
83
|
+
flex: 1;
|
|
84
|
+
min-width: 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.lk-lab__sidebar {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
height: 100%;
|
|
91
|
+
overflow-y: auto;
|
|
92
|
+
background: var(--wzl-surface);
|
|
93
|
+
border-right: 1px solid var(--wzl-border);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.lk-lab__aside {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
height: 100%;
|
|
100
|
+
overflow-y: auto;
|
|
101
|
+
background: var(--wzl-surface);
|
|
102
|
+
border-left: 1px solid var(--wzl-border);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.lk-lab__main {
|
|
106
|
+
display: flex;
|
|
107
|
+
min-width: 0;
|
|
108
|
+
height: 100%;
|
|
109
|
+
|
|
110
|
+
> .lk-workspace,
|
|
111
|
+
> .lk-workspace-host {
|
|
112
|
+
flex: 1;
|
|
60
113
|
min-width: 0;
|
|
61
114
|
}
|
|
62
115
|
}
|
|
63
116
|
|
|
64
|
-
// The lab's
|
|
65
|
-
// annotate
|
|
66
|
-
//
|
|
117
|
+
// The lab's two shared drawing buffers, stacked around the trials: marks that
|
|
118
|
+
// annotate an instrument go over its DOM, an opaque renderer goes under it so
|
|
119
|
+
// the pane can still hold a label or a control. Both sit under the viewport
|
|
120
|
+
// chrome, whose z-overlay outranks them, and both are inert — every tile takes
|
|
121
|
+
// input from its own box.
|
|
67
122
|
.lk-lab__surface {
|
|
68
123
|
position: absolute;
|
|
69
124
|
inset: 0;
|
|
70
|
-
z-index: 1;
|
|
71
125
|
pointer-events: none;
|
|
72
126
|
}
|
|
73
127
|
|
|
128
|
+
.lk-lab__surface--under {
|
|
129
|
+
z-index: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.lk-lab__surface--over {
|
|
133
|
+
z-index: 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
74
136
|
.lk-shell-footer {
|
|
75
137
|
border-top: 1px solid var(--wzl-border);
|
|
76
138
|
padding: var(--wzl-space-xs) var(--wzl-space-md);
|
|
@@ -98,10 +160,6 @@
|
|
|
98
160
|
// against a shrink-to-fit header row it swallows the slack, which pushed the
|
|
99
161
|
// consumer's own header content down to min-content and wrapped the row to
|
|
100
162
|
// three lines. See the `.lk-root` prefix convention in theme/base.less.
|
|
101
|
-
.lk-root .lk-lab-header__add-select {
|
|
102
|
-
width: 160px;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
163
|
// The mode toggle is a preference, not a primary action: it sits quiet until
|
|
106
164
|
// the pointer is on it.
|
|
107
165
|
.lk-lab-header__mode {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@
|
|
1
|
+
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
|
+
import { expect, within } from '@weasel-js/forge/play';
|
|
3
|
+
import { Select, ToggleBar } from '../passthrough/weasel-ui';
|
|
4
|
+
import { Toolbar } from '../primitives/Toolbar';
|
|
2
5
|
import { LabShell } from './LabShell';
|
|
3
6
|
|
|
4
7
|
const meta: Meta<typeof LabShell> = {
|
|
@@ -37,3 +40,49 @@ export const WithFooter: Story = {
|
|
|
37
40
|
footer: <span>Status: ready</span>,
|
|
38
41
|
},
|
|
39
42
|
};
|
|
43
|
+
|
|
44
|
+
// jsdom has no layout, so this runs in the browser story projects.
|
|
45
|
+
export const HeaderControlsLineUp: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
title: 'My Lab',
|
|
48
|
+
header: (
|
|
49
|
+
<>
|
|
50
|
+
<button type="button">Add</button>
|
|
51
|
+
<ToggleBar
|
|
52
|
+
ariaLabel="Mode"
|
|
53
|
+
items={[
|
|
54
|
+
{ value: 'a', label: 'A' },
|
|
55
|
+
{ value: 'b', label: 'B' },
|
|
56
|
+
]}
|
|
57
|
+
value="a"
|
|
58
|
+
onChange={() => {}}
|
|
59
|
+
/>
|
|
60
|
+
<Select
|
|
61
|
+
aria-label="Font"
|
|
62
|
+
width="fit"
|
|
63
|
+
selectedKey="oswald"
|
|
64
|
+
options={[{ value: 'oswald', label: 'Oswald' }]}
|
|
65
|
+
/>
|
|
66
|
+
<Toolbar aria-label="Actions">
|
|
67
|
+
<Toolbar.Group>
|
|
68
|
+
<Toolbar.Button onClick={() => {}}>Undo</Toolbar.Button>
|
|
69
|
+
</Toolbar.Group>
|
|
70
|
+
</Toolbar>
|
|
71
|
+
</>
|
|
72
|
+
),
|
|
73
|
+
children: <p>Body content.</p>,
|
|
74
|
+
},
|
|
75
|
+
play: async ({ canvasElement }) => {
|
|
76
|
+
const canvas = within(canvasElement);
|
|
77
|
+
const boxes = [
|
|
78
|
+
canvas.getByRole('button', { name: 'Add' }),
|
|
79
|
+
canvas.getByRole('radiogroup', { name: 'Mode' }),
|
|
80
|
+
canvas.getByRole('button', { name: /Font/ }),
|
|
81
|
+
canvas.getByRole('button', { name: 'Undo' }),
|
|
82
|
+
].map((el) => el.getBoundingClientRect());
|
|
83
|
+
const heights = boxes.map((box) => Math.round(box.height * 2) / 2);
|
|
84
|
+
const middles = boxes.map((box) => Math.round((box.top + box.height / 2) * 2) / 2);
|
|
85
|
+
await expect(heights).toEqual(heights.map(() => heights[0]));
|
|
86
|
+
await expect(middles).toEqual(middles.map(() => middles[0]));
|
|
87
|
+
},
|
|
88
|
+
};
|
package/src/lab/LabShell.tsx
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { ThemeProvider, useThemeOptional } from '@weasel-js/theme/react';
|
|
2
1
|
import type { ReactNode } from 'react';
|
|
3
2
|
import type { LabMode } from '../state/types';
|
|
4
|
-
import {
|
|
3
|
+
import { LabRoot } from './LabRoot';
|
|
5
4
|
import type { LabPage } from './LabSwitcher';
|
|
6
5
|
import { LabSwitcher } from './LabSwitcher';
|
|
7
|
-
import { useResolvedMode } from './useSystemMode';
|
|
8
6
|
|
|
9
7
|
/** Props for `<LabShell>`. */
|
|
10
8
|
export interface LabShellProps {
|
|
@@ -27,34 +25,27 @@ export interface LabShellProps {
|
|
|
27
25
|
/** Page frame for a lab: a titled header, a body, and an optional footer,
|
|
28
26
|
* themed for the resolved color mode. Presentational only — use `<Lab>` when
|
|
29
27
|
* the trial runtime is wanted too. */
|
|
30
|
-
export function LabShell({
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
export function LabShell({
|
|
29
|
+
title,
|
|
30
|
+
children,
|
|
31
|
+
header,
|
|
32
|
+
footer,
|
|
33
|
+
mode = 'auto',
|
|
34
|
+
pages,
|
|
35
|
+
path,
|
|
36
|
+
}: LabShellProps) {
|
|
37
|
+
return (
|
|
38
|
+
<LabRoot mode={mode} className="lk-shell">
|
|
39
39
|
<header className="lk-shell-header">
|
|
40
|
-
{pages && pages.length > 0
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
{pages && pages.length > 0 ? (
|
|
41
|
+
<LabSwitcher title={title} pages={pages} path={path} className="lk-shell-title" />
|
|
42
|
+
) : (
|
|
43
|
+
<h1 className="lk-shell-title">{title}</h1>
|
|
44
|
+
)}
|
|
44
45
|
{header && <div className="lk-shell-header-actions">{header}</div>}
|
|
45
46
|
</header>
|
|
46
47
|
<main className="lk-shell-body">{children}</main>
|
|
47
48
|
{footer && <footer className="lk-shell-footer">{footer}</footer>}
|
|
48
|
-
</
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
// Inside <Lab> the theme is already applied on `.lk-lab`; wrapping again
|
|
52
|
-
// would only add a div.
|
|
53
|
-
return outer ? (
|
|
54
|
-
shell
|
|
55
|
-
) : (
|
|
56
|
-
<ThemeProvider theme={interstellarTheme} mode={resolved}>
|
|
57
|
-
{shell}
|
|
58
|
-
</ThemeProvider>
|
|
49
|
+
</LabRoot>
|
|
59
50
|
);
|
|
60
51
|
}
|
package/src/lab/LabSwitcher.less
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
.lk-switcher-title {
|
|
7
7
|
margin: 0;
|
|
8
8
|
font-size: var(--wzl-font-size-xl);
|
|
9
|
-
line-height: var(--wzl-
|
|
9
|
+
line-height: var(--wzl-leading-snug);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.lk-switcher-trigger {
|
|
@@ -21,13 +21,17 @@
|
|
|
21
21
|
border: 0;
|
|
22
22
|
padding: 0;
|
|
23
23
|
cursor: pointer;
|
|
24
|
+
// A title is a click target now, and a consumer's header is usually a
|
|
25
|
+
// crowded flex row -- left to wrap, `brick-icons corpus` breaks after the
|
|
26
|
+
// hyphen and the control becomes three ragged lines with a caret adrift.
|
|
27
|
+
white-space: nowrap;
|
|
24
28
|
|
|
25
|
-
&:hover { color: var(--wzl-
|
|
29
|
+
&:hover { color: var(--wzl-accent); }
|
|
26
30
|
|
|
27
31
|
&:focus-visible {
|
|
28
32
|
outline: 2px solid var(--wzl-focus-ring, currentColor);
|
|
29
33
|
outline-offset: 2px;
|
|
30
|
-
border-radius: var(--wzl-radius-sm,
|
|
34
|
+
border-radius: var(--wzl-radius-sm, 3px);
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -50,22 +54,25 @@
|
|
|
50
54
|
position: absolute;
|
|
51
55
|
top: calc(100% + var(--wzl-space-xs));
|
|
52
56
|
left: 0;
|
|
53
|
-
z-index: var(--wzl-z-
|
|
57
|
+
z-index: var(--wzl-z-overlay);
|
|
54
58
|
min-width: max(100%, 11rem);
|
|
55
59
|
display: flex;
|
|
56
60
|
flex-direction: column;
|
|
57
61
|
padding: var(--wzl-space-xs);
|
|
58
|
-
|
|
62
|
+
// `--wzl-surface-raised` is translucent by design -- it is for panels that
|
|
63
|
+
// blur what sits behind them. A menu has to be read, so it takes the opaque
|
|
64
|
+
// surface, the way `FloatingPanel` does.
|
|
65
|
+
background: var(--wzl-surface);
|
|
59
66
|
border: 1px solid var(--wzl-border);
|
|
60
|
-
border-radius: var(--wzl-radius-md,
|
|
61
|
-
box-shadow:
|
|
67
|
+
border-radius: var(--wzl-radius-md, 5px);
|
|
68
|
+
box-shadow: 0 6px 20px var(--wzl-shadow);
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
.lk-switcher-item {
|
|
65
72
|
padding: var(--wzl-space-xs) var(--wzl-space-sm);
|
|
66
73
|
border-radius: var(--wzl-radius-sm, 3px);
|
|
67
|
-
color: var(--wzl-
|
|
68
|
-
font-size: var(--wzl-font-size
|
|
74
|
+
color: var(--wzl-fg);
|
|
75
|
+
font-size: var(--wzl-font-size);
|
|
69
76
|
text-decoration: none;
|
|
70
77
|
white-space: nowrap;
|
|
71
78
|
|
|
@@ -80,6 +87,6 @@
|
|
|
80
87
|
// The open page stays in the list rather than being filtered out: a menu that
|
|
81
88
|
// drops its current entry shifts every other one as you move between pages.
|
|
82
89
|
.lk-switcher-item[aria-current='page'] {
|
|
83
|
-
color: var(--wzl-
|
|
84
|
-
font-weight: var(--wzl-font-weight-
|
|
90
|
+
color: var(--wzl-fg);
|
|
91
|
+
font-weight: var(--wzl-font-weight-bold);
|
|
85
92
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { currentPage, LabSwitcher } from './LabSwitcher';
|
|
4
3
|
import { LabShell } from './LabShell';
|
|
4
|
+
import { currentPage, LabSwitcher } from './LabSwitcher';
|
|
5
5
|
|
|
6
6
|
const PAGES = [
|
|
7
7
|
{ href: '/corpus', label: 'Wall' },
|
|
@@ -19,8 +19,7 @@ describe('currentPage', () => {
|
|
|
19
19
|
it('survives a query string, a trailing slash and a leftover extension', () => {
|
|
20
20
|
// A dev server maps `/stats` to `stats.html` and serves both spellings, so
|
|
21
21
|
// an old bookmark must still resolve to the same page.
|
|
22
|
-
for (const path of ['/stats?kind=base', '/stats/', '/stats.html',
|
|
23
|
-
'/lab/stats.html#top']) {
|
|
22
|
+
for (const path of ['/stats?kind=base', '/stats/', '/stats.html', '/lab/stats.html#top']) {
|
|
24
23
|
expect(currentPage(path, PAGES)).toBe(1);
|
|
25
24
|
}
|
|
26
25
|
});
|
|
@@ -34,7 +33,9 @@ describe('<LabSwitcher>', () => {
|
|
|
34
33
|
it('shows the title and nothing else until it is opened', () => {
|
|
35
34
|
render(<LabSwitcher title="corpus stats" pages={PAGES} path="/stats" />);
|
|
36
35
|
expect(screen.getByRole('button', { name: /corpus stats/ })).toHaveAttribute(
|
|
37
|
-
'aria-expanded',
|
|
36
|
+
'aria-expanded',
|
|
37
|
+
'false',
|
|
38
|
+
);
|
|
38
39
|
expect(screen.queryByRole('menu')).toBeNull();
|
|
39
40
|
});
|
|
40
41
|
|
|
@@ -54,8 +55,7 @@ describe('<LabSwitcher>', () => {
|
|
|
54
55
|
render(<LabSwitcher title="corpus stats" pages={PAGES} path="/stats" />);
|
|
55
56
|
open();
|
|
56
57
|
expect(screen.getByRole('menuitem', { name: 'Wall' }).tagName).toBe('A');
|
|
57
|
-
expect(screen.getByRole('menuitem', { name: 'Wall' })).toHaveAttribute(
|
|
58
|
-
'href', '/corpus');
|
|
58
|
+
expect(screen.getByRole('menuitem', { name: 'Wall' })).toHaveAttribute('href', '/corpus');
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
it('closes on Escape and on a press outside it', () => {
|
|
@@ -63,7 +63,8 @@ describe('<LabSwitcher>', () => {
|
|
|
63
63
|
<div>
|
|
64
64
|
<LabSwitcher title="corpus stats" pages={PAGES} path="/stats" />
|
|
65
65
|
<button type="button">elsewhere</button>
|
|
66
|
-
</div
|
|
66
|
+
</div>,
|
|
67
|
+
);
|
|
67
68
|
open();
|
|
68
69
|
fireEvent.keyDown(document, { key: 'Escape' });
|
|
69
70
|
expect(screen.queryByRole('menu')).toBeNull();
|
|
@@ -76,7 +77,7 @@ describe('<LabSwitcher>', () => {
|
|
|
76
77
|
it('stays a plain heading when there is nowhere to switch to', () => {
|
|
77
78
|
// A disclosure arrow promising a menu of the page you are already on is
|
|
78
79
|
// worse than no control.
|
|
79
|
-
render(<LabSwitcher title="corpus stats" pages={
|
|
80
|
+
render(<LabSwitcher title="corpus stats" pages={PAGES.slice(1, 2)} path="/stats" />);
|
|
80
81
|
expect(screen.getByRole('heading', { name: 'corpus stats' })).toBeInTheDocument();
|
|
81
82
|
expect(screen.queryByRole('button')).toBeNull();
|
|
82
83
|
});
|
|
@@ -84,7 +85,11 @@ describe('<LabSwitcher>', () => {
|
|
|
84
85
|
|
|
85
86
|
describe('<LabShell> with pages', () => {
|
|
86
87
|
it('turns its title into the switcher', () => {
|
|
87
|
-
render(
|
|
88
|
+
render(
|
|
89
|
+
<LabShell title="corpus stats" pages={PAGES} path="/stats">
|
|
90
|
+
body
|
|
91
|
+
</LabShell>,
|
|
92
|
+
);
|
|
88
93
|
expect(screen.getByRole('button', { name: /corpus stats/ })).toBeInTheDocument();
|
|
89
94
|
});
|
|
90
95
|
|
package/src/lab/LabSwitcher.tsx
CHANGED
|
@@ -25,8 +25,10 @@ export interface LabSwitcherProps {
|
|
|
25
25
|
* to `stats.html` serves both spellings, and a bookmark from before the URLs
|
|
26
26
|
* lost their extension still resolves to the same page. */
|
|
27
27
|
export function currentPage(path: string, pages: readonly LabPage[]): number {
|
|
28
|
-
const here = path
|
|
29
|
-
.replace(
|
|
28
|
+
const here = path
|
|
29
|
+
.replace(/[?#].*$/, '')
|
|
30
|
+
.replace(/\/$/, '')
|
|
31
|
+
.replace(/\.html$/, '');
|
|
30
32
|
return pages.findIndex((p) => here.endsWith(p.href.replace(/\.html$/, '')));
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -44,8 +46,10 @@ export function LabSwitcher({ title, pages, path, className }: LabSwitcherProps)
|
|
|
44
46
|
const [open, setOpen] = useState(false);
|
|
45
47
|
const root = useRef<HTMLDivElement>(null);
|
|
46
48
|
const menuId = useId();
|
|
47
|
-
const here = currentPage(
|
|
48
|
-
? '' : window.location.pathname),
|
|
49
|
+
const here = currentPage(
|
|
50
|
+
path ?? (typeof window === 'undefined' ? '' : window.location.pathname),
|
|
51
|
+
pages,
|
|
52
|
+
);
|
|
49
53
|
|
|
50
54
|
const close = useCallback(() => setOpen(false), []);
|
|
51
55
|
|
package/src/lab/Workspace.less
CHANGED
|
@@ -10,6 +10,7 @@ function fakeHandle(): SurfaceHandle {
|
|
|
10
10
|
invalidateAll: vi.fn(),
|
|
11
11
|
invalidateRects: vi.fn(),
|
|
12
12
|
registerTile: vi.fn(),
|
|
13
|
+
registerClear: vi.fn(() => () => {}),
|
|
13
14
|
registerPainter: vi.fn(() => () => {}),
|
|
14
15
|
containerRef: vi.fn(),
|
|
15
16
|
getContainer: vi.fn(() => null),
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { describeElement, findOvershoot } from './fitCheck';
|
|
3
|
+
|
|
4
|
+
const bound = { top: 0, left: 0, bottom: 600, right: 800 };
|
|
5
|
+
const box = (bottom: number, right: number) => ({ top: 0, left: 0, bottom, right });
|
|
6
|
+
|
|
7
|
+
describe('findOvershoot', () => {
|
|
8
|
+
it('returns null when everything is inside the bound', () => {
|
|
9
|
+
expect(findOvershoot(bound, [{ item: 'a', edges: box(600, 800) }])).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('ignores sub-pixel overshoot within the tolerance', () => {
|
|
13
|
+
expect(findOvershoot(bound, [{ item: 'a', edges: box(600.8, 800) }])).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('names the item reaching furthest past the bound', () => {
|
|
17
|
+
const hit = findOvershoot(bound, [
|
|
18
|
+
{ item: 'header', edges: box(640, 800) },
|
|
19
|
+
{ item: 'tile', edges: box(900, 800) },
|
|
20
|
+
{ item: 'footer', edges: box(700, 800) },
|
|
21
|
+
]);
|
|
22
|
+
expect(hit).toEqual({ item: 'tile', by: 300, axis: 'y' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('compares both axes and reports the worse one', () => {
|
|
26
|
+
const hit = findOvershoot(bound, [
|
|
27
|
+
{ item: 'tall', edges: box(650, 800) },
|
|
28
|
+
{ item: 'wide', edges: box(600, 1000) },
|
|
29
|
+
]);
|
|
30
|
+
expect(hit).toEqual({ item: 'wide', by: 200, axis: 'x' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('keeps the first item on a tie, which is the outermost in document order', () => {
|
|
34
|
+
const hit = findOvershoot(bound, [
|
|
35
|
+
{ item: 'shell', edges: box(616, 800) },
|
|
36
|
+
{ item: 'footer', edges: box(616, 800) },
|
|
37
|
+
]);
|
|
38
|
+
expect(hit?.item).toBe('shell');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('describeElement', () => {
|
|
43
|
+
it('writes the tag and every class', () => {
|
|
44
|
+
const el = document.createElement('DIV');
|
|
45
|
+
el.className = 'lk-shell-body extra';
|
|
46
|
+
expect(describeElement(el)).toBe('div.lk-shell-body.extra');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('places a classless element by its nearest classed ancestor', () => {
|
|
50
|
+
const outer = document.createElement('div');
|
|
51
|
+
outer.className = 'lk-trial';
|
|
52
|
+
const mid = document.createElement('div');
|
|
53
|
+
const leaf = document.createElement('canvas');
|
|
54
|
+
outer.appendChild(mid).appendChild(leaf);
|
|
55
|
+
expect(describeElement(leaf)).toBe('canvas in div.lk-trial');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/** An element's edges, in the same coordinate space as the bound it is tested against. */
|
|
4
|
+
export interface Edges {
|
|
5
|
+
top: number;
|
|
6
|
+
left: number;
|
|
7
|
+
bottom: number;
|
|
8
|
+
right: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Overshoot<T> {
|
|
12
|
+
item: T;
|
|
13
|
+
/** How far past the bound the item reaches, in px, on whichever axis is worse. */
|
|
14
|
+
by: number;
|
|
15
|
+
axis: 'x' | 'y';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The item reaching furthest past the bottom or right of `bound`. On a tie the
|
|
19
|
+
* first item wins, so given document order it names the outermost box. */
|
|
20
|
+
export function findOvershoot<T>(
|
|
21
|
+
bound: Edges,
|
|
22
|
+
items: Iterable<{ item: T; edges: Edges }>,
|
|
23
|
+
tolerance = 1,
|
|
24
|
+
): Overshoot<T> | null {
|
|
25
|
+
let worst: Overshoot<T> | null = null;
|
|
26
|
+
for (const { item, edges } of items) {
|
|
27
|
+
const y = edges.bottom - bound.bottom;
|
|
28
|
+
const x = edges.right - bound.right;
|
|
29
|
+
const by = Math.max(x, y);
|
|
30
|
+
if (by <= tolerance) continue;
|
|
31
|
+
if (!worst || by > worst.by) worst = { item, by, axis: y >= x ? 'y' : 'x' };
|
|
32
|
+
}
|
|
33
|
+
return worst;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tagAndClasses(el: Element): string {
|
|
37
|
+
return el.tagName.toLowerCase() + [...el.classList].map((c) => `.${c}`).join('');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `div.lk-shell-body` — the tag and classes, which is what a reader greps for.
|
|
41
|
+
* A classless element is placed by its nearest classed ancestor. */
|
|
42
|
+
export function describeElement(el: Element): string {
|
|
43
|
+
if (el.classList.length > 0) return tagAndClasses(el);
|
|
44
|
+
let up = el.parentElement;
|
|
45
|
+
while (up && up.classList.length === 0) up = up.parentElement;
|
|
46
|
+
return up ? `${tagAndClasses(el)} in ${tagAndClasses(up)}` : tagAndClasses(el);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function edgesOf(el: Element): Edges {
|
|
50
|
+
const r = el.getBoundingClientRect();
|
|
51
|
+
return { top: r.top, left: r.left, bottom: r.bottom, right: r.right };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function* candidates(root: Element): Iterable<{ item: Element; edges: Edges }> {
|
|
55
|
+
yield { item: root, edges: edgesOf(root) };
|
|
56
|
+
for (const el of root.querySelectorAll('*')) yield { item: el, edges: edgesOf(el) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function overflows(el: Element): boolean {
|
|
60
|
+
return el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Why a mounted lab scrolls, or null when it fits. The document only counts
|
|
64
|
+
* when the lab is what reaches past the viewport, so a lab embedded in a long
|
|
65
|
+
* page does not report the page's own scroll. */
|
|
66
|
+
export function explainLabScroll(lab: Element, shellBody: Element): string | null {
|
|
67
|
+
if (overflows(shellBody)) {
|
|
68
|
+
const r = shellBody.getBoundingClientRect();
|
|
69
|
+
const bound = {
|
|
70
|
+
top: r.top,
|
|
71
|
+
left: r.left,
|
|
72
|
+
bottom: r.top + shellBody.clientHeight,
|
|
73
|
+
right: r.left + shellBody.clientWidth,
|
|
74
|
+
};
|
|
75
|
+
const hit = findOvershoot(bound, candidates(shellBody));
|
|
76
|
+
return hit
|
|
77
|
+
? `.lk-shell-body scrolls: ${describeElement(hit.item)} reaches ${Math.round(hit.by)}px past it (${hit.axis})`
|
|
78
|
+
: '.lk-shell-body scrolls';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const doc = document.scrollingElement ?? document.documentElement;
|
|
82
|
+
if (!overflows(doc)) return null;
|
|
83
|
+
const r = lab.getBoundingClientRect();
|
|
84
|
+
const bound = {
|
|
85
|
+
top: r.top,
|
|
86
|
+
left: r.left,
|
|
87
|
+
bottom: r.top + Math.min(r.height, doc.clientHeight),
|
|
88
|
+
right: r.left + Math.min(r.width, doc.clientWidth),
|
|
89
|
+
};
|
|
90
|
+
const hit = findOvershoot(bound, candidates(lab));
|
|
91
|
+
return hit
|
|
92
|
+
? `the page scrolls: ${describeElement(hit.item)} reaches ${Math.round(hit.by)}px past the lab's box (${hit.axis})`
|
|
93
|
+
: null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const SETTLE_MS = 300;
|
|
97
|
+
|
|
98
|
+
/** Dev only: warns once, naming the culprit, when a mounted lab scrolls. */
|
|
99
|
+
export function useLabFitWarning(labBody: Element | null): void {
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (process.env.NODE_ENV === 'production') return;
|
|
102
|
+
if (!labBody || typeof ResizeObserver === 'undefined') return;
|
|
103
|
+
const lab = labBody.closest('.lk-lab');
|
|
104
|
+
const shellBody = labBody.closest('.lk-shell-body');
|
|
105
|
+
if (!lab || !shellBody) return;
|
|
106
|
+
|
|
107
|
+
// A shrinking window overflows for a frame or two before the workspace
|
|
108
|
+
// re-lays out, so only overflow that outlasts the resize is reported.
|
|
109
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
110
|
+
const observer = new ResizeObserver(() => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
timer = setTimeout(() => {
|
|
113
|
+
const why = explainLabScroll(lab, shellBody);
|
|
114
|
+
if (!why) return;
|
|
115
|
+
observer.disconnect();
|
|
116
|
+
console.warn(`[labkit] <Lab> should fit its container without scrolling, but ${why}.`);
|
|
117
|
+
}, SETTLE_MS);
|
|
118
|
+
});
|
|
119
|
+
for (const el of [document.documentElement, lab, shellBody, labBody]) observer.observe(el);
|
|
120
|
+
return () => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
observer.disconnect();
|
|
123
|
+
};
|
|
124
|
+
}, [labBody]);
|
|
125
|
+
}
|
package/src/lab/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type { LabContextValue } from './LabContext';
|
|
|
4
4
|
export { LabContext, useLabContext } from './LabContext';
|
|
5
5
|
export type { LabPaletteProps } from './LabPalette';
|
|
6
6
|
export { LabPalette } from './LabPalette';
|
|
7
|
+
export type { LabRootProps } from './LabRoot';
|
|
8
|
+
export { LabRoot } from './LabRoot';
|
|
7
9
|
export type { LabShellProps } from './LabShell';
|
|
8
10
|
export { LabShell } from './LabShell';
|
|
9
11
|
export type { LabPage, LabSwitcherProps } from './LabSwitcher';
|