@weasel-js/labkit 1.5.2 → 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.
- package/README.md +3 -7
- package/dist/_dts/{CanvasStackContext-CMMo180l.d.ts → CanvasStackContext-DQgcH1ny.d.ts} +1 -1
- package/dist/_dts/LayerList.d-2c9yup5Y.d.ts +83 -0
- package/dist/_dts/{PrefsForm.d-DPy9I8It.d.ts → PrefsRow.d-DFpPIeIC.d.ts} +16 -1
- package/dist/_dts/{Select.d-ZTiwzYyT.d.ts → Select.d-BchGMC2Y.d.ts} +102 -11
- package/dist/_dts/{frac-B4hFJMiN.d.ts → frac-DTRq7Xpb.d.ts} +18 -10
- package/dist/_dts/inDialog-D5ZKGjiD.d.ts +23 -0
- package/dist/_dts/{index-DlKolEIZ.d.ts → index-B0--8PEM.d.ts} +2 -2
- package/dist/_dts/index-DINmCuLA.d.ts +453 -0
- package/dist/_dts/number.d-OeQYQoUO.d.ts +28 -0
- package/dist/_dts/{types-Db-3NNk1.d.ts → types-B7y8hX1D.d.ts} +2 -2
- package/dist/_dts/{usePanZoom-SSTZe410.d.ts → usePanZoom-Bq73pwFe.d.ts} +2 -2
- package/dist/_dts/{useTrialState-pnAE-UBD.d.ts → useTrialState-CP4jfuKl.d.ts} +1 -1
- package/dist/canvas/index.d.ts +6 -5
- package/dist/chrome/index.d.ts +5 -4
- package/dist/chrome/index.js +7 -7
- package/dist/chunk-2RVCB2PT.js +3 -0
- package/dist/chunk-2RVCB2PT.js.map +1 -0
- package/dist/{chunk-753Y6HE6.js → chunk-3FI7Y4R4.js} +4 -4
- package/dist/chunk-3FI7Y4R4.js.map +1 -0
- package/dist/{chunk-REOBGYMC.js → chunk-AWH3SRPW.js} +31 -5
- package/dist/chunk-AWH3SRPW.js.map +1 -0
- package/dist/{chunk-A2ACAGW6.js → chunk-BVL7Y574.js} +22 -5
- package/dist/chunk-BVL7Y574.js.map +1 -0
- package/dist/{chunk-CKUKF72R.js → chunk-H5VIV7MQ.js} +26 -22
- package/dist/chunk-H5VIV7MQ.js.map +1 -0
- package/dist/chunk-HFRE4QQV.js +400 -0
- package/dist/chunk-HFRE4QQV.js.map +1 -0
- package/dist/{chunk-LRI3MBGR.js → chunk-MOLIUNVH.js} +4 -4
- package/dist/{chunk-LRI3MBGR.js.map → chunk-MOLIUNVH.js.map} +1 -1
- package/dist/chunk-USO6TERP.js +90 -0
- package/dist/chunk-USO6TERP.js.map +1 -0
- package/dist/{chunk-EMXAIRUJ.js → chunk-VQ47KWD5.js} +3 -3
- package/dist/{chunk-EMXAIRUJ.js.map → chunk-VQ47KWD5.js.map} +1 -1
- package/dist/chunk-WUJXJC5O.js +14568 -0
- package/dist/chunk-WUJXJC5O.js.map +1 -0
- package/dist/{chunk-KJCUERQF.js → chunk-ZORVNOKX.js} +6 -25
- package/dist/chunk-ZORVNOKX.js.map +1 -0
- package/dist/config/index.d.ts +50 -7
- package/dist/config/index.js +5 -5
- package/dist/controls/index.d.ts +6 -3
- package/dist/controls/index.js +4 -4
- package/dist/dragdrop/index.d.ts +4 -3
- package/dist/index.d.ts +83 -102
- package/dist/index.js +430 -458
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +6 -35
- package/dist/layers/index.js +2 -3
- package/dist/loupe/index.d.ts +6 -5
- package/dist/passthrough/weasel-ui.d.ts +407 -204
- package/dist/passthrough/weasel-ui.js +1 -1
- package/dist/primitives/index.d.ts +2 -13
- package/dist/primitives/index.js +4 -4
- package/dist/state/index.d.ts +6 -5
- package/dist/state/index.js +5 -5
- package/dist/styles.css +298 -174
- package/dist/undo/index.d.ts +4 -3
- package/package.json +9 -13
- package/src/annotations/frac.test.ts +7 -0
- package/src/annotations/paint.test.ts +21 -0
- package/src/annotations/paint.ts +12 -0
- package/src/annotations/store.test.ts +31 -0
- package/src/annotations/types.ts +3 -2
- package/src/canvas/Stage.test.tsx +18 -0
- package/src/chrome/regions/SidebarRegion.less +197 -0
- package/src/chrome/regions/SidebarRegion.test.tsx +20 -0
- package/src/chrome/regions/SidebarRegion.tsx +7 -2
- package/src/chrome/types.ts +5 -2
- package/src/config/autoConfig.ts +2 -2
- package/src/config/builder.ts +20 -1
- package/src/config/entry.test.ts +8 -0
- package/src/config/index.ts +2 -0
- package/src/config/rules.ts +2 -0
- package/src/config/sectionTree.test.ts +146 -0
- package/src/config/sectionTree.ts +124 -0
- package/src/controls/ControlPanel.less +4 -0
- package/src/controls/ControlPanel.stories.tsx +55 -2
- package/src/controls/ControlPanel.test.tsx +30 -3
- package/src/controls/ControlPanel.tsx +116 -337
- package/src/controls/inDialog.test.tsx +82 -0
- package/src/controls/inDialog.tsx +46 -0
- package/src/controls/index.ts +1 -0
- package/src/index.ts +11 -26
- package/src/instrument/types.ts +5 -2
- package/src/lab/Lab.nebula.test.tsx +31 -0
- package/src/lab/Lab.tsx +38 -15
- package/src/lab/LabBoundary.test.tsx +66 -0
- package/src/lab/LabBoundary.tsx +56 -0
- package/src/lab/LabHeader.tsx +4 -23
- package/src/lab/LabRoot.tsx +2 -3
- package/src/lab/LabShell.less +0 -1
- package/src/lab/LabShell.stories.tsx +15 -0
- package/src/lab/LabShell.test.tsx +18 -0
- package/src/lab/LabShell.tsx +14 -1
- package/src/lab/LabSwitcher.less +1 -1
- package/src/lab/LabSwitcher.test.tsx +41 -0
- package/src/lab/LabSwitcher.tsx +30 -8
- package/src/lab/index.ts +1 -0
- package/src/layers/AGENTS.md +18 -74
- package/src/layers/index.ts +4 -2
- package/src/passthrough/weasel-ui.ts +25 -10
- package/src/primitives/index.ts +0 -2
- package/src/specimen/Specimen.tsx +42 -56
- package/src/state/types.ts +2 -1
- package/src/styles.less +4 -2
- package/src/theme/Interstellar.stories.tsx +8 -9
- package/src/theme/base.less +3 -20
- package/src/theme/interstellar.test.ts +6 -3
- package/src/theme/interstellar.theme.json +0 -56
- package/src/theme/interstellar.ts +2 -2
- package/src/trial/Trial.canvas.test.tsx +1 -1
- package/src/trial/Trial.tsx +30 -8
- package/dist/_dts/index-8LiKe3A-.d.ts +0 -410
- package/dist/chunk-6BUQ77ZV.js +0 -18
- package/dist/chunk-6BUQ77ZV.js.map +0 -1
- package/dist/chunk-753Y6HE6.js.map +0 -1
- package/dist/chunk-A2ACAGW6.js.map +0 -1
- package/dist/chunk-CKUKF72R.js.map +0 -1
- package/dist/chunk-D35PFTS7.js +0 -170
- package/dist/chunk-D35PFTS7.js.map +0 -1
- package/dist/chunk-FKLT3ITC.js +0 -590
- package/dist/chunk-FKLT3ITC.js.map +0 -1
- package/dist/chunk-KJCUERQF.js.map +0 -1
- package/dist/chunk-LASCBWW7.js +0 -13456
- package/dist/chunk-LASCBWW7.js.map +0 -1
- package/dist/chunk-REOBGYMC.js.map +0 -1
- package/dist/ui/layers/index.d.ts +0 -66
- package/dist/ui/layers/index.js +0 -3
- package/dist/ui/layers/index.js.map +0 -1
- package/src/lab/useSystemMode.ts +0 -30
- package/src/layers/LayerList.less +0 -93
- package/src/layers/LayerList.stories.tsx +0 -66
- package/src/layers/LayerList.test.tsx +0 -232
- package/src/layers/LayerList.tsx +0 -268
- package/src/primitives/Sidebar.less +0 -112
- package/src/primitives/Sidebar.stories.tsx +0 -38
- package/src/primitives/Sidebar.test.tsx +0 -74
- package/src/primitives/Sidebar.tsx +0 -35
- package/src/ui/layers/index.ts +0 -5
package/src/layers/LayerList.tsx
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import { openPointerSession, type PointerSession } from '@weasel-js/core';
|
|
2
|
-
import { Disclosure } from '@weasel-js/ui';
|
|
3
|
-
import {
|
|
4
|
-
Fragment,
|
|
5
|
-
type ReactNode,
|
|
6
|
-
type PointerEvent as ReactPointerEvent,
|
|
7
|
-
useEffect,
|
|
8
|
-
useId,
|
|
9
|
-
useRef,
|
|
10
|
-
useState,
|
|
11
|
-
} from 'react';
|
|
12
|
-
import type { LayerDescriptor } from '../instrument/types';
|
|
13
|
-
import { DragHandleGlyph } from '../passthrough/weasel-ui';
|
|
14
|
-
|
|
15
|
-
/** A layer that may itself contain layers. A plain `LayerDescriptor` is one
|
|
16
|
-
* of these with no children, so a flat list needs no change. */
|
|
17
|
-
export interface LayerTreeNode extends LayerDescriptor {
|
|
18
|
-
children?: LayerTreeNode[];
|
|
19
|
-
/** Render this node's children hidden on first paint. Ignored once
|
|
20
|
-
* `collapsedIds` is supplied. */
|
|
21
|
-
defaultCollapsed?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Props for `<LayerList>`. */
|
|
25
|
-
export interface LayerListProps {
|
|
26
|
-
layers: LayerTreeNode[];
|
|
27
|
-
visibility: Record<string, boolean>;
|
|
28
|
-
/** The whole tree, with the dragged node moved. Reordering is scoped to a
|
|
29
|
-
* node's siblings: a drag never changes which parent a layer sits under. */
|
|
30
|
-
onReorder: (newOrder: LayerTreeNode[]) => void;
|
|
31
|
-
onToggle: (id: string, visible: boolean) => void;
|
|
32
|
-
/** Ids whose children are hidden. Supplying it makes collapse controlled;
|
|
33
|
-
* without it the list keeps its own, seeded from `defaultCollapsed`. */
|
|
34
|
-
collapsedIds?: readonly string[];
|
|
35
|
-
onCollapsedChange?: (ids: string[]) => void;
|
|
36
|
-
className?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface DragState {
|
|
40
|
-
/** Id of the dragged row's parent, or null at the top level. */
|
|
41
|
-
parentId: string | null;
|
|
42
|
-
fromIndex: number;
|
|
43
|
-
toIndex: number;
|
|
44
|
-
startY: number;
|
|
45
|
-
/** Row height plus row gap, measured when the drag starts. */
|
|
46
|
-
pitch: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Which row the drag currently marks. */
|
|
50
|
-
interface DragMark {
|
|
51
|
-
parentId: string | null;
|
|
52
|
-
index: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function findNode(nodes: LayerTreeNode[], id: string): LayerTreeNode | null {
|
|
56
|
-
for (const node of nodes) {
|
|
57
|
-
if (node.id === id) return node;
|
|
58
|
-
const hit = node.children ? findNode(node.children, id) : null;
|
|
59
|
-
if (hit) return hit;
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function siblingsOf(layers: LayerTreeNode[], parentId: string | null): LayerTreeNode[] {
|
|
65
|
-
if (parentId === null) return layers;
|
|
66
|
-
return findNode(layers, parentId)?.children ?? [];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function withChildren(
|
|
70
|
-
nodes: LayerTreeNode[],
|
|
71
|
-
parentId: string | null,
|
|
72
|
-
next: LayerTreeNode[],
|
|
73
|
-
): LayerTreeNode[] {
|
|
74
|
-
if (parentId === null) return next;
|
|
75
|
-
return nodes.map((node) => {
|
|
76
|
-
if (node.id === parentId) return { ...node, children: next };
|
|
77
|
-
if (node.children) return { ...node, children: withChildren(node.children, parentId, next) };
|
|
78
|
-
return node;
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function collectCollapsed(nodes: LayerTreeNode[], into: Set<string>): Set<string> {
|
|
83
|
-
for (const node of nodes) {
|
|
84
|
-
if (node.defaultCollapsed) into.add(node.id);
|
|
85
|
-
if (node.children) collectCollapsed(node.children, into);
|
|
86
|
-
}
|
|
87
|
-
return into;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function anyNested(nodes: LayerTreeNode[]): boolean {
|
|
91
|
-
return nodes.some((n) => (n.children?.length ?? 0) > 0);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** A reorderable list of layers with per-layer visibility toggles. Layers
|
|
95
|
-
* marked `alwaysOn` are pinned and cannot be reordered or hidden. Layers with
|
|
96
|
-
* `children` render as an expandable subtree. */
|
|
97
|
-
export function LayerList({
|
|
98
|
-
layers,
|
|
99
|
-
visibility,
|
|
100
|
-
onReorder,
|
|
101
|
-
onToggle,
|
|
102
|
-
collapsedIds,
|
|
103
|
-
onCollapsedChange,
|
|
104
|
-
className,
|
|
105
|
-
}: LayerListProps) {
|
|
106
|
-
const [dragMark, setDragMark] = useState<DragMark | null>(null);
|
|
107
|
-
const [ownCollapsed, setOwnCollapsed] = useState<Set<string>>(() =>
|
|
108
|
-
collectCollapsed(layers, new Set()),
|
|
109
|
-
);
|
|
110
|
-
const dragRef = useRef<DragState | null>(null);
|
|
111
|
-
const sessionRef = useRef<PointerSession | null>(null);
|
|
112
|
-
const uid = useId();
|
|
113
|
-
// The session's callbacks outlive the render that opened them, so the commit
|
|
114
|
-
// reads its inputs here rather than from that render's closure.
|
|
115
|
-
const commitRef = useRef({ layers, onReorder });
|
|
116
|
-
commitRef.current = { layers, onReorder };
|
|
117
|
-
|
|
118
|
-
useEffect(() => () => sessionRef.current?.cancel(), []);
|
|
119
|
-
|
|
120
|
-
const collapsed = collapsedIds ? new Set(collapsedIds) : ownCollapsed;
|
|
121
|
-
|
|
122
|
-
const toggleCollapsed = (id: string) => {
|
|
123
|
-
const next = new Set(collapsed);
|
|
124
|
-
if (next.has(id)) next.delete(id);
|
|
125
|
-
else next.add(id);
|
|
126
|
-
if (!collapsedIds) setOwnCollapsed(next);
|
|
127
|
-
onCollapsedChange?.([...next]);
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
if (layers.length === 0) {
|
|
131
|
-
return (
|
|
132
|
-
<div className={className ? `lk-layer-list ${className}` : 'lk-layer-list'}>
|
|
133
|
-
<div className="lk-layer-list__empty">No layers</div>
|
|
134
|
-
</div>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const clearDrag = () => {
|
|
139
|
-
dragRef.current = null;
|
|
140
|
-
sessionRef.current = null;
|
|
141
|
-
setDragMark(null);
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const startDrag = (e: ReactPointerEvent<HTMLElement>, parentId: string | null, index: number) => {
|
|
145
|
-
// Measured at grab time rather than hardcoded: a restyle that changes row
|
|
146
|
-
// height or gap would otherwise silently skew every drag distance.
|
|
147
|
-
const row = e.currentTarget.closest('.lk-layer-list__row');
|
|
148
|
-
const list = row?.parentElement;
|
|
149
|
-
const gap = list ? Number.parseFloat(getComputedStyle(list).rowGap) || 0 : 0;
|
|
150
|
-
const pitch = row ? row.getBoundingClientRect().height + gap : 0;
|
|
151
|
-
const drag: DragState = {
|
|
152
|
-
parentId,
|
|
153
|
-
fromIndex: index,
|
|
154
|
-
toIndex: index,
|
|
155
|
-
startY: e.clientY,
|
|
156
|
-
pitch: pitch > 0 ? pitch : 1,
|
|
157
|
-
};
|
|
158
|
-
dragRef.current = drag;
|
|
159
|
-
setDragMark({ parentId, index });
|
|
160
|
-
sessionRef.current = openPointerSession(e.currentTarget, e, {
|
|
161
|
-
onMove: (ev) => {
|
|
162
|
-
const delta = Math.round((ev.clientY - drag.startY) / drag.pitch);
|
|
163
|
-
const siblings = siblingsOf(commitRef.current.layers, parentId);
|
|
164
|
-
const last = siblings.filter((l) => !l.alwaysOn).length - 1;
|
|
165
|
-
drag.toIndex = Math.min(last, Math.max(0, drag.fromIndex + delta));
|
|
166
|
-
setDragMark({ parentId, index: drag.toIndex });
|
|
167
|
-
},
|
|
168
|
-
onEnd: () => {
|
|
169
|
-
const { layers: tree, onReorder: commit } = commitRef.current;
|
|
170
|
-
const { fromIndex, toIndex } = drag;
|
|
171
|
-
clearDrag();
|
|
172
|
-
if (toIndex === fromIndex) return;
|
|
173
|
-
const siblings = siblingsOf(tree, parentId);
|
|
174
|
-
const rows = siblings.filter((l) => !l.alwaysOn);
|
|
175
|
-
const locked = siblings.filter((l) => l.alwaysOn);
|
|
176
|
-
const next = [...rows];
|
|
177
|
-
const [moved] = next.splice(fromIndex, 1);
|
|
178
|
-
if (moved) next.splice(toIndex, 0, moved);
|
|
179
|
-
commit(withChildren(tree, parentId, [...next, ...locked]));
|
|
180
|
-
},
|
|
181
|
-
// An interrupted drag never named a destination, so it reorders nothing.
|
|
182
|
-
onCancel: clearDrag,
|
|
183
|
-
});
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
// A twisty column on every row keeps labels aligned across depths, but a
|
|
187
|
-
// flat list would only be paying 20px for it.
|
|
188
|
-
const nested = anyNested(layers);
|
|
189
|
-
|
|
190
|
-
function renderRow(
|
|
191
|
-
layer: LayerTreeNode,
|
|
192
|
-
parentId: string | null,
|
|
193
|
-
index: number,
|
|
194
|
-
path: string,
|
|
195
|
-
pinned: boolean,
|
|
196
|
-
): ReactNode {
|
|
197
|
-
const kids = layer.children ?? [];
|
|
198
|
-
const open = kids.length > 0 && !collapsed.has(layer.id);
|
|
199
|
-
const bodyId = `${uid}${path}`;
|
|
200
|
-
const dragging = !pinned && dragMark?.parentId === parentId && dragMark.index === index;
|
|
201
|
-
const cls = ['lk-layer-list__row'];
|
|
202
|
-
if (dragging) cls.push('lk-layer-list__row--dragging');
|
|
203
|
-
if (pinned) cls.push('lk-layer-list__row--pinned');
|
|
204
|
-
return (
|
|
205
|
-
<Fragment key={layer.id}>
|
|
206
|
-
<div className={cls.join(' ')}>
|
|
207
|
-
{nested &&
|
|
208
|
-
(kids.length > 0 ? (
|
|
209
|
-
<Disclosure
|
|
210
|
-
open={open}
|
|
211
|
-
onToggle={() => toggleCollapsed(layer.id)}
|
|
212
|
-
label={`${layer.label} sublayers`}
|
|
213
|
-
controls={bodyId}
|
|
214
|
-
/>
|
|
215
|
-
) : (
|
|
216
|
-
<span className="lk-layer-list__twisty-gap" />
|
|
217
|
-
))}
|
|
218
|
-
{pinned ? (
|
|
219
|
-
<span className="lk-layer-list__lock" role="img" aria-label="Always on">
|
|
220
|
-
🔒
|
|
221
|
-
</span>
|
|
222
|
-
) : (
|
|
223
|
-
<>
|
|
224
|
-
<button
|
|
225
|
-
type="button"
|
|
226
|
-
className="lk-layer-list__handle"
|
|
227
|
-
aria-label={`Reorder ${layer.label}`}
|
|
228
|
-
onPointerDown={(e) => startDrag(e, parentId, index)}
|
|
229
|
-
>
|
|
230
|
-
<DragHandleGlyph size={13} />
|
|
231
|
-
</button>
|
|
232
|
-
<input
|
|
233
|
-
className="lk-layer-list__check"
|
|
234
|
-
type="checkbox"
|
|
235
|
-
checked={visibility[layer.id] !== false}
|
|
236
|
-
onChange={(e) => onToggle(layer.id, e.target.checked)}
|
|
237
|
-
aria-label={`Toggle ${layer.label}`}
|
|
238
|
-
/>
|
|
239
|
-
</>
|
|
240
|
-
)}
|
|
241
|
-
<span className="lk-layer-list__label">{layer.label}</span>
|
|
242
|
-
</div>
|
|
243
|
-
{open && (
|
|
244
|
-
<div id={bodyId} className="lk-layer-list__children">
|
|
245
|
-
{renderLevel(kids, layer.id, path)}
|
|
246
|
-
</div>
|
|
247
|
-
)}
|
|
248
|
-
</Fragment>
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function renderLevel(nodes: LayerTreeNode[], parentId: string | null, path: string): ReactNode {
|
|
253
|
-
const reorderable = nodes.filter((l) => !l.alwaysOn);
|
|
254
|
-
const pinned = nodes.filter((l) => l.alwaysOn);
|
|
255
|
-
return (
|
|
256
|
-
<>
|
|
257
|
-
{reorderable.map((layer, i) => renderRow(layer, parentId, i, `${path}-${i}`, false))}
|
|
258
|
-
{pinned.map((layer, i) => renderRow(layer, parentId, i, `${path}-p${i}`, true))}
|
|
259
|
-
</>
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return (
|
|
264
|
-
<div className={className ? `lk-layer-list ${className}` : 'lk-layer-list'}>
|
|
265
|
-
{renderLevel(layers, null, '')}
|
|
266
|
-
</div>
|
|
267
|
-
);
|
|
268
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
.lk-sidebar {
|
|
2
|
-
display: flex;
|
|
3
|
-
flex-direction: column;
|
|
4
|
-
width: 280px;
|
|
5
|
-
flex: 1 1 auto;
|
|
6
|
-
background: var(--wzl-surface-raised);
|
|
7
|
-
border-right: 1px solid var(--wzl-border);
|
|
8
|
-
min-height: 0;
|
|
9
|
-
transition: width 150ms ease;
|
|
10
|
-
|
|
11
|
-
&--collapsed {
|
|
12
|
-
width: 32px;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
.lk-sidebar-header {
|
|
17
|
-
display: flex;
|
|
18
|
-
align-items: center;
|
|
19
|
-
padding: var(--wzl-space-xs) var(--wzl-space-sm);
|
|
20
|
-
border-bottom: 1px solid var(--wzl-line-subtle);
|
|
21
|
-
min-height: var(--wzl-control-h);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
.lk-sidebar-title {
|
|
25
|
-
flex: 1;
|
|
26
|
-
font-weight: var(--wzl-font-weight-bold);
|
|
27
|
-
color: var(--wzl-fg);
|
|
28
|
-
font-size: var(--wzl-font-size);
|
|
29
|
-
|
|
30
|
-
.lk-sidebar--collapsed & {
|
|
31
|
-
display: none;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.lk-sidebar-toggle {
|
|
36
|
-
height: 24px;
|
|
37
|
-
width: 24px;
|
|
38
|
-
padding: 0;
|
|
39
|
-
background: transparent;
|
|
40
|
-
border: 1px solid transparent;
|
|
41
|
-
border-radius: var(--wzl-radius-sm);
|
|
42
|
-
color: var(--wzl-fg-muted);
|
|
43
|
-
cursor: pointer;
|
|
44
|
-
|
|
45
|
-
&:hover {
|
|
46
|
-
background: var(--wzl-surface);
|
|
47
|
-
color: var(--wzl-fg);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.lk-sidebar-body {
|
|
52
|
-
flex: 1;
|
|
53
|
-
overflow: auto;
|
|
54
|
-
padding: var(--wzl-space-sm);
|
|
55
|
-
|
|
56
|
-
.lk-sidebar--collapsed & {
|
|
57
|
-
display: none;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.lk-sidebar-section {
|
|
62
|
-
border-bottom: var(--wzl-border-w) solid var(--wzl-line-subtle);
|
|
63
|
-
|
|
64
|
-
// The last section runs to the bottom of its pane. Stopped at its content,
|
|
65
|
-
// its rule and the bare pane under it read as a second, empty zone.
|
|
66
|
-
&:last-child {
|
|
67
|
-
flex-grow: 1;
|
|
68
|
-
border-bottom: 0;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.lk-sidebar-section__bar {
|
|
73
|
-
display: flex;
|
|
74
|
-
align-items: center;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.lk-sidebar-section__undock {
|
|
78
|
-
padding: 0 var(--wzl-space-xs);
|
|
79
|
-
border: 0;
|
|
80
|
-
background: transparent;
|
|
81
|
-
color: var(--wzl-fg-subtle);
|
|
82
|
-
cursor: pointer;
|
|
83
|
-
|
|
84
|
-
&:hover {
|
|
85
|
-
color: var(--wzl-fg);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.lk-sidebar-section__head {
|
|
90
|
-
flex: 1;
|
|
91
|
-
width: 100%;
|
|
92
|
-
padding: var(--wzl-space-xs);
|
|
93
|
-
border: 0;
|
|
94
|
-
background: transparent;
|
|
95
|
-
color: var(--wzl-fg-muted);
|
|
96
|
-
font-family: var(--wzl-font-display);
|
|
97
|
-
font-size: var(--wzl-font-size-sm);
|
|
98
|
-
letter-spacing: 0.08em;
|
|
99
|
-
text-align: left;
|
|
100
|
-
text-transform: uppercase;
|
|
101
|
-
cursor: pointer;
|
|
102
|
-
|
|
103
|
-
&:hover {
|
|
104
|
-
color: var(--wzl-fg);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// The section body is the only thing that insets a panel — the panels
|
|
109
|
-
// themselves carry no padding, so this is the whole gutter.
|
|
110
|
-
.lk-sidebar-section__body {
|
|
111
|
-
padding: 0 var(--wzl-space-xs) var(--wzl-space-xs);
|
|
112
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@weasel-js/forge';
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { Sidebar } from './Sidebar';
|
|
4
|
-
|
|
5
|
-
const meta: Meta<typeof Sidebar> = {
|
|
6
|
-
title: 'labkit/Primitives/Sidebar',
|
|
7
|
-
component: Sidebar,
|
|
8
|
-
};
|
|
9
|
-
export default meta;
|
|
10
|
-
|
|
11
|
-
type Story = StoryObj<typeof Sidebar>;
|
|
12
|
-
|
|
13
|
-
export const Default: Story = {
|
|
14
|
-
render: () => (
|
|
15
|
-
<div style={{ display: 'flex', height: 400 }}>
|
|
16
|
-
<div style={{ flex: 1 }}>main content area</div>
|
|
17
|
-
<Sidebar title="Controls">
|
|
18
|
-
<p>One slider here</p>
|
|
19
|
-
<p>Another slider</p>
|
|
20
|
-
</Sidebar>
|
|
21
|
-
</div>
|
|
22
|
-
),
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const Collapsible: Story = {
|
|
26
|
-
render: function Render() {
|
|
27
|
-
const [collapsed, setCollapsed] = useState(false);
|
|
28
|
-
return (
|
|
29
|
-
<div style={{ display: 'flex', height: 400 }}>
|
|
30
|
-
<div style={{ flex: 1, padding: 16 }}>main content area</div>
|
|
31
|
-
<Sidebar title="Controls" collapsed={collapsed} onToggle={() => setCollapsed((c) => !c)}>
|
|
32
|
-
<p>One slider here</p>
|
|
33
|
-
<p>Another slider</p>
|
|
34
|
-
</Sidebar>
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
37
|
-
},
|
|
38
|
-
};
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
-
import { describe, expect, test } from 'vitest';
|
|
3
|
-
import { Sidebar } from './Sidebar';
|
|
4
|
-
|
|
5
|
-
describe('Sidebar', () => {
|
|
6
|
-
test('renders children when expanded (default)', () => {
|
|
7
|
-
render(
|
|
8
|
-
<Sidebar>
|
|
9
|
-
<p>content</p>
|
|
10
|
-
</Sidebar>,
|
|
11
|
-
);
|
|
12
|
-
expect(screen.getByText('content')).toBeInTheDocument();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test('renders title when provided', () => {
|
|
16
|
-
render(
|
|
17
|
-
<Sidebar title="Controls">
|
|
18
|
-
<p>x</p>
|
|
19
|
-
</Sidebar>,
|
|
20
|
-
);
|
|
21
|
-
expect(screen.getByText('Controls')).toBeInTheDocument();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('marks the sidebar collapsed and nests body inside the collapsed root', () => {
|
|
25
|
-
const { container } = render(
|
|
26
|
-
<Sidebar collapsed>
|
|
27
|
-
<p>content</p>
|
|
28
|
-
</Sidebar>,
|
|
29
|
-
);
|
|
30
|
-
const root = container.querySelector('.lk-sidebar');
|
|
31
|
-
const body = container.querySelector('.lk-sidebar-body');
|
|
32
|
-
expect(root).not.toBeNull();
|
|
33
|
-
expect(body).not.toBeNull();
|
|
34
|
-
expect(root?.classList.contains('lk-sidebar--collapsed')).toBe(true);
|
|
35
|
-
// Body is still rendered (children stay in DOM), but lives inside the collapsed
|
|
36
|
-
// root so the CSS rule `.lk-sidebar--collapsed .lk-sidebar-body { display:none }` applies.
|
|
37
|
-
expect(body?.parentElement).toBe(root);
|
|
38
|
-
expect(body?.textContent).toBe('content');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('toggle button calls onToggle when clicked', () => {
|
|
42
|
-
let toggled = false;
|
|
43
|
-
render(
|
|
44
|
-
<Sidebar
|
|
45
|
-
title="C"
|
|
46
|
-
onToggle={() => {
|
|
47
|
-
toggled = true;
|
|
48
|
-
}}
|
|
49
|
-
>
|
|
50
|
-
<p>x</p>
|
|
51
|
-
</Sidebar>,
|
|
52
|
-
);
|
|
53
|
-
screen.getByRole('button', { name: /collapse|expand/i }).click();
|
|
54
|
-
expect(toggled).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('shows expand label when collapsed', () => {
|
|
58
|
-
render(
|
|
59
|
-
<Sidebar title="C" collapsed onToggle={() => {}}>
|
|
60
|
-
<p>x</p>
|
|
61
|
-
</Sidebar>,
|
|
62
|
-
);
|
|
63
|
-
expect(screen.getByRole('button', { name: /expand/i })).toBeInTheDocument();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('shows collapse label when expanded', () => {
|
|
67
|
-
render(
|
|
68
|
-
<Sidebar title="C" onToggle={() => {}}>
|
|
69
|
-
<p>x</p>
|
|
70
|
-
</Sidebar>,
|
|
71
|
-
);
|
|
72
|
-
expect(screen.getByRole('button', { name: /collapse/i })).toBeInTheDocument();
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
/** Props for `<Sidebar>`. */
|
|
4
|
-
export interface SidebarProps {
|
|
5
|
-
children: ReactNode;
|
|
6
|
-
title?: string;
|
|
7
|
-
collapsed?: boolean;
|
|
8
|
-
onToggle?: () => void;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** A collapsible side panel with an optional title. Collapse state is the
|
|
12
|
-
* caller's to hold. */
|
|
13
|
-
export function Sidebar({ children, title, collapsed = false, onToggle }: SidebarProps) {
|
|
14
|
-
const className = `lk-sidebar${collapsed ? ' lk-sidebar--collapsed' : ''}`;
|
|
15
|
-
return (
|
|
16
|
-
<aside className={className}>
|
|
17
|
-
{(title || onToggle) && (
|
|
18
|
-
<div className="lk-sidebar-header">
|
|
19
|
-
{title && <span className="lk-sidebar-title">{title}</span>}
|
|
20
|
-
{onToggle && (
|
|
21
|
-
<button
|
|
22
|
-
type="button"
|
|
23
|
-
className="lk-sidebar-toggle"
|
|
24
|
-
onClick={onToggle}
|
|
25
|
-
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
26
|
-
>
|
|
27
|
-
{collapsed ? '›' : '‹'}
|
|
28
|
-
</button>
|
|
29
|
-
)}
|
|
30
|
-
</div>
|
|
31
|
-
)}
|
|
32
|
-
<div className="lk-sidebar-body">{children}</div>
|
|
33
|
-
</aside>
|
|
34
|
-
);
|
|
35
|
-
}
|
package/src/ui/layers/index.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
// LayerStack lives in `@weasel-js/ui` now; labkit re-exports it so existing
|
|
2
|
-
// `@weasel-js/labkit/ui/layers` imports keep resolving. Named, not `export *`
|
|
3
|
-
// — a star re-export of an external package emits no binding in the bundle.
|
|
4
|
-
export type { LayerStackItem, LayerStackProps } from '@weasel-js/ui';
|
|
5
|
-
export { LayerStack } from '@weasel-js/ui';
|