sh3-core 0.10.4 → 0.10.5

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.
@@ -54,6 +54,7 @@ export function makeSplitNode(children, overrides = {}) {
54
54
  sizes: (_b = overrides.sizes) !== null && _b !== void 0 ? _b : children.map(() => 1 / children.length),
55
55
  pinned: overrides.pinned,
56
56
  collapsed: overrides.collapsed,
57
+ fixed: overrides.fixed,
57
58
  children,
58
59
  };
59
60
  }
@@ -272,3 +272,81 @@ describe('LayoutRenderer browser — E.5 splitter collapse toggle', () => {
272
272
  expect((_a = root.collapsed) === null || _a === void 0 ? void 0 : _a[0]).toBe(true);
273
273
  });
274
274
  });
275
+ // ---------------------------------------------------------------------------
276
+ // E.6 — fixed[] slots: no collapse widget, frozen handles
277
+ // ---------------------------------------------------------------------------
278
+ describe('LayoutRenderer browser — E.6 fixed slots', () => {
279
+ beforeEach(() => { cleanupDOM(); resetFramework(); });
280
+ it('hides the collapse widget on a fixed pane but keeps it on panes with a non-fixed neighbor', async () => {
281
+ stubView();
282
+ registerApp(makeApp({
283
+ manifest: makeAppManifest({ id: 'e6a' }),
284
+ initialLayout: [
285
+ {
286
+ name: 'default',
287
+ tree: makeTree(makeSplitNode([
288
+ makeSlotNode('a', 'test:view'),
289
+ makeSlotNode('b', 'test:view'),
290
+ makeSlotNode('c', 'test:view'),
291
+ ], { fixed: [true, false, false] })),
292
+ },
293
+ ],
294
+ }));
295
+ await launchApp('e6a');
296
+ renderWithShell(LayoutRenderer, { path: [] });
297
+ await settle(30);
298
+ expect(document.querySelector('[data-testid="collapse-toggle-0"]')).toBeNull();
299
+ expect(document.querySelector('[data-testid="collapse-toggle-1"]')).not.toBeNull();
300
+ expect(document.querySelector('[data-testid="collapse-toggle-2"]')).not.toBeNull();
301
+ });
302
+ it('freezes the handle adjacent to a fixed pane: dblclick does not collapse', async () => {
303
+ var _a, _b, _c, _d;
304
+ stubView();
305
+ registerApp(makeApp({
306
+ manifest: makeAppManifest({ id: 'e6b' }),
307
+ initialLayout: [
308
+ {
309
+ name: 'default',
310
+ tree: makeTree(makeSplitNode([makeSlotNode('a', 'test:view'), makeSlotNode('b', 'test:view')], { fixed: [true, false] })),
311
+ },
312
+ ],
313
+ }));
314
+ await launchApp('e6b');
315
+ renderWithShell(LayoutRenderer, { path: [] });
316
+ await settle(30);
317
+ const handle = document.querySelector('[data-testid="splitter-handle-0"]');
318
+ expect(handle).not.toBeNull();
319
+ expect(handle.classList.contains('frozen')).toBe(true);
320
+ // pointer-events: none blocks Playwright clicks, so dispatch the event
321
+ // directly to verify the handler itself refuses to toggle.
322
+ handle.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }));
323
+ await settle(50);
324
+ const root = layoutStore.root;
325
+ if ((root === null || root === void 0 ? void 0 : root.type) !== 'split')
326
+ throw new Error('expected split root');
327
+ expect((_b = (_a = root.collapsed) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : false).toBe(false);
328
+ expect((_d = (_c = root.collapsed) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : false).toBe(false);
329
+ });
330
+ it('hides the collapse widget on a middle pane whose neighbors are both fixed', async () => {
331
+ stubView();
332
+ registerApp(makeApp({
333
+ manifest: makeAppManifest({ id: 'e6c' }),
334
+ initialLayout: [
335
+ {
336
+ name: 'default',
337
+ tree: makeTree(makeSplitNode([
338
+ makeSlotNode('a', 'test:view'),
339
+ makeSlotNode('b', 'test:view'),
340
+ makeSlotNode('c', 'test:view'),
341
+ ], { fixed: [true, false, true] })),
342
+ },
343
+ ],
344
+ }));
345
+ await launchApp('e6c');
346
+ renderWithShell(LayoutRenderer, { path: [] });
347
+ await settle(30);
348
+ expect(document.querySelector('[data-testid="collapse-toggle-0"]')).toBeNull();
349
+ expect(document.querySelector('[data-testid="collapse-toggle-1"]')).toBeNull();
350
+ expect(document.querySelector('[data-testid="collapse-toggle-2"]')).toBeNull();
351
+ });
352
+ });
@@ -169,6 +169,7 @@
169
169
  sizes={split.sizes}
170
170
  pinned={split.pinned}
171
171
  collapsed={split.collapsed}
172
+ fixed={split.fixed}
172
173
  count={split.children.length}
173
174
  pane={splitPane}
174
175
  onResize={(i, v) => (split.sizes[i] = v)}
@@ -21,6 +21,14 @@ export interface SplitNode {
21
21
  pinned?: SizeMode[];
22
22
  /** Per-child collapsed state. Omitted means all expanded. */
23
23
  collapsed?: boolean[];
24
+ /**
25
+ * Per-child fixed flag. A fixed child has no collapse widget and the
26
+ * resize handles on either side of it are frozen (non-interactive,
27
+ * rendered thinner). A non-fixed child whose every neighbor is fixed
28
+ * also loses its collapse widget — there's nowhere for the freed
29
+ * space to be used.
30
+ */
31
+ fixed?: boolean[];
24
32
  /** Ordered child nodes. Length must equal `sizes` length. */
25
33
  children: LayoutNode[];
26
34
  }
@@ -28,6 +28,7 @@
28
28
  sizes,
29
29
  pinned,
30
30
  collapsed,
31
+ fixed,
31
32
  count,
32
33
  pane,
33
34
  onResize,
@@ -46,6 +47,13 @@
46
47
  pinned?: SizeMode[];
47
48
  /** Per-pane collapsed state. Omitted entries default to false. */
48
49
  collapsed?: boolean[];
50
+ /**
51
+ * Per-pane fixed flag. A fixed pane has no collapse widget and
52
+ * the handles on either side of it are frozen (non-interactive,
53
+ * rendered thinner). A non-fixed pane whose every neighbor is
54
+ * fixed also loses its collapse widget.
55
+ */
56
+ fixed?: boolean[];
49
57
  /** Number of panes — `sizes.length` should match. */
50
58
  count: number;
51
59
  /** Snippet invoked once per pane with the pane index. */
@@ -67,6 +75,15 @@
67
75
 
68
76
  const modeOf = (i: number): SizeMode => pinned?.[i] ?? 'fr';
69
77
  const isCollapsed = (i: number): boolean => collapsed?.[i] ?? false;
78
+ const isFixed = (i: number): boolean => fixed?.[i] ?? false;
79
+ const isHandleFrozen = (i: number): boolean => isFixed(i) || isFixed(i + 1);
80
+
81
+ function canCollapse(i: number): boolean {
82
+ if (isFixed(i)) return false;
83
+ const left = i > 0 ? !isFixed(i - 1) : false;
84
+ const right = i < count - 1 ? !isFixed(i + 1) : false;
85
+ return left || right;
86
+ }
70
87
 
71
88
  /** CSS `flex` shorthand for pane i. */
72
89
  function flexFor(i: number): string {
@@ -88,8 +105,9 @@
88
105
  let drag: DragState | null = $state(null);
89
106
 
90
107
  function beginDrag(e: PointerEvent, handleIndex: number) {
91
- // Disable resize handles adjacent to collapsed panes.
108
+ // Disable resize handles adjacent to collapsed or fixed panes.
92
109
  if (isCollapsed(handleIndex) || isCollapsed(handleIndex + 1)) return;
110
+ if (isHandleFrozen(handleIndex)) return;
93
111
 
94
112
  e.preventDefault();
95
113
  (e.target as HTMLElement).setPointerCapture(e.pointerId);
@@ -198,12 +216,13 @@
198
216
  <span class="collapse-icon">{direction === 'horizontal' ? '▸' : '▾'}</span>
199
217
  </button>
200
218
  {:else}
201
- {#if onCollapseToggle}
219
+ {#if onCollapseToggle && canCollapse(i)}
202
220
  <button
203
221
  type="button"
204
222
  class="collapse-header expanded"
205
223
  class:horizontal={direction === 'horizontal'}
206
224
  class:vertical={direction === 'vertical'}
225
+ data-testid="collapse-toggle-{i}"
207
226
  onclick={() => onCollapseToggle?.(i, true)}
208
227
  aria-label="Collapse pane"
209
228
  >
@@ -221,12 +240,17 @@
221
240
  class="splitter-handle"
222
241
  class:dragging={drag?.handleIndex === i}
223
242
  class:disabled={isCollapsed(i) || isCollapsed(i + 1)}
243
+ class:frozen={isHandleFrozen(i)}
224
244
  data-testid="splitter-handle-{i}"
225
245
  onpointerdown={(e) => beginDrag(e, i)}
226
246
  onpointermove={moveDrag}
227
247
  onpointerup={endDrag}
228
248
  onpointercancel={endDrag}
229
- ondblclick={() => onCollapseToggle?.(i, !isCollapsed(i))}
249
+ ondblclick={() => {
250
+ if (isHandleFrozen(i)) return;
251
+ if (!canCollapse(i) && !isCollapsed(i)) return;
252
+ onCollapseToggle?.(i, !isCollapsed(i));
253
+ }}
230
254
  role="separator"
231
255
  aria-orientation={direction === 'horizontal' ? 'vertical' : 'horizontal'}
232
256
  ></div>
@@ -323,6 +347,15 @@
323
347
  cursor: default;
324
348
  pointer-events: none;
325
349
  }
350
+ .splitter-handle.frozen {
351
+ cursor: default;
352
+ pointer-events: none;
353
+ background: var(--shell-border);
354
+ opacity: 0.5;
355
+ }
356
+ .splitter-handle.frozen:hover {
357
+ background: var(--shell-border);
358
+ }
326
359
 
327
360
  .horizontal > .splitter-handle {
328
361
  width: 4px;
@@ -332,4 +365,6 @@
332
365
  height: 4px;
333
366
  cursor: row-resize;
334
367
  }
368
+ .horizontal > .splitter-handle.frozen { width: 1px; }
369
+ .vertical > .splitter-handle.frozen { height: 1px; }
335
370
  </style>
@@ -14,6 +14,13 @@ type $$ComponentProps = {
14
14
  pinned?: SizeMode[];
15
15
  /** Per-pane collapsed state. Omitted entries default to false. */
16
16
  collapsed?: boolean[];
17
+ /**
18
+ * Per-pane fixed flag. A fixed pane has no collapse widget and
19
+ * the handles on either side of it are frozen (non-interactive,
20
+ * rendered thinner). A non-fixed pane whose every neighbor is
21
+ * fixed also loses its collapse widget.
22
+ */
23
+ fixed?: boolean[];
17
24
  /** Number of panes — `sizes.length` should match. */
18
25
  count: number;
19
26
  /** Snippet invoked once per pane with the pane index. */
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export declare const VERSION = "0.10.4";
2
+ export declare const VERSION = "0.10.5";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export const VERSION = '0.10.4';
2
+ export const VERSION = '0.10.5';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"