sh3-core 0.17.0 → 0.19.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 (154) hide show
  1. package/dist/Sh3.svelte +107 -39
  2. package/dist/__screenshots__/handheld.browser.test.ts/handheld-viewport-flip-e2e-viewport-override-flips-chrome-and-body-branches-1.png +0 -0
  3. package/dist/actions/CommandPalette.svelte +1 -2
  4. package/dist/actions/listActionsFromEntries.test.js +29 -0
  5. package/dist/actions/listActive.js +2 -0
  6. package/dist/actions/listeners.js +16 -1
  7. package/dist/actions/programmatic-dispatch.svelte.test.js +9 -2
  8. package/dist/actions/types.d.ts +8 -0
  9. package/dist/api.d.ts +8 -1
  10. package/dist/app/store/storeShard.svelte.js +1 -21
  11. package/dist/app/store/version.d.ts +11 -0
  12. package/dist/app/store/version.js +39 -0
  13. package/dist/app/store/version.test.d.ts +1 -0
  14. package/dist/app/store/version.test.js +44 -0
  15. package/dist/apps/lifecycle.d.ts +6 -0
  16. package/dist/apps/lifecycle.js +5 -2
  17. package/dist/apps/lifecycle.test.js +30 -0
  18. package/dist/apps/types.d.ts +12 -0
  19. package/dist/assets/iconIds.generated.d.ts +1 -1
  20. package/dist/assets/iconIds.generated.js +5 -0
  21. package/dist/assets/icons.svg +31 -0
  22. package/dist/auth/auth.svelte.js +18 -8
  23. package/dist/auth/types.d.ts +6 -0
  24. package/dist/chrome/CompactChrome.svelte +130 -0
  25. package/dist/chrome/CompactChrome.svelte.d.ts +3 -0
  26. package/dist/chrome/CompactChrome.svelte.test.d.ts +1 -0
  27. package/dist/chrome/CompactChrome.svelte.test.js +174 -0
  28. package/dist/chrome/MenuSheet.svelte +224 -0
  29. package/dist/chrome/MenuSheet.svelte.d.ts +7 -0
  30. package/dist/chrome/MenuSheet.svelte.test.d.ts +1 -0
  31. package/dist/chrome/MenuSheet.svelte.test.js +46 -0
  32. package/dist/createShell.d.ts +9 -0
  33. package/dist/createShell.js +20 -7
  34. package/dist/createShell.remoteAuth.test.d.ts +1 -0
  35. package/dist/createShell.remoteAuth.test.js +71 -0
  36. package/dist/documents/http-backend.js +12 -11
  37. package/dist/env/client.js +11 -5
  38. package/dist/files/types.d.ts +106 -0
  39. package/dist/files/types.js +1 -0
  40. package/dist/gestures/gestureRegistry.d.ts +6 -0
  41. package/dist/gestures/gestureRegistry.js +190 -0
  42. package/dist/gestures/gestureRegistry.test.d.ts +1 -0
  43. package/dist/gestures/gestureRegistry.test.js +119 -0
  44. package/dist/gestures/index.d.ts +6 -0
  45. package/dist/gestures/index.js +12 -0
  46. package/dist/gestures/pointerClaim.d.ts +7 -0
  47. package/dist/gestures/pointerClaim.js +36 -0
  48. package/dist/gestures/pointerClaim.test.d.ts +1 -0
  49. package/dist/gestures/pointerClaim.test.js +64 -0
  50. package/dist/gestures/types.d.ts +83 -0
  51. package/dist/gestures/types.js +1 -0
  52. package/dist/handheld.browser.test.d.ts +1 -0
  53. package/dist/handheld.browser.test.js +90 -0
  54. package/dist/host-entry.d.ts +1 -0
  55. package/dist/host-entry.js +1 -0
  56. package/dist/layout/LayoutRenderer.browser.test.js +15 -3
  57. package/dist/layout/LayoutRenderer.svelte +27 -3
  58. package/dist/layout/LayoutRenderer.svelte.d.ts +4 -1
  59. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-3-splitter-drag-updates-split-sizes-when-the-splitter-handle-is-dragged-1.png +0 -0
  60. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-5-splitter-collapse-toggle-toggles-collapsed-i--on-double-click-1.png +0 -0
  61. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-6-fixed-slots-hides-the-collapse-widget-on-a-fixed-pane-but-keeps-it-on-panes-with-a-non-fixed-neighbor-1.png +0 -0
  62. package/dist/layout/compact/CarouselTabs.svelte +361 -0
  63. package/dist/layout/compact/CarouselTabs.svelte.d.ts +10 -0
  64. package/dist/layout/compact/CarouselTabs.svelte.test.d.ts +1 -0
  65. package/dist/layout/compact/CarouselTabs.svelte.test.js +300 -0
  66. package/dist/layout/compact/CompactRenderer.svelte +53 -0
  67. package/dist/layout/compact/CompactRenderer.svelte.d.ts +3 -0
  68. package/dist/layout/compact/CompactRenderer.svelte.test.d.ts +1 -0
  69. package/dist/layout/compact/CompactRenderer.svelte.test.js +125 -0
  70. package/dist/layout/compact/derive.d.ts +3 -0
  71. package/dist/layout/compact/derive.js +157 -0
  72. package/dist/layout/compact/derive.test.d.ts +1 -0
  73. package/dist/layout/compact/derive.test.js +197 -0
  74. package/dist/layout/compact/drawerStore.svelte.d.ts +21 -0
  75. package/dist/layout/compact/drawerStore.svelte.js +75 -0
  76. package/dist/layout/compact/drawerStore.svelte.test.d.ts +1 -0
  77. package/dist/layout/compact/drawerStore.svelte.test.js +43 -0
  78. package/dist/layout/compact/enrichCarousels.d.ts +8 -0
  79. package/dist/layout/compact/enrichCarousels.js +44 -0
  80. package/dist/layout/compact/enrichCarousels.test.d.ts +1 -0
  81. package/dist/layout/compact/enrichCarousels.test.js +88 -0
  82. package/dist/layout/compact/resolveRole.d.ts +6 -0
  83. package/dist/layout/compact/resolveRole.js +13 -0
  84. package/dist/layout/compact/resolveRole.test.d.ts +1 -0
  85. package/dist/layout/compact/resolveRole.test.js +18 -0
  86. package/dist/layout/compact/types.d.ts +30 -0
  87. package/dist/layout/compact/types.js +15 -0
  88. package/dist/layout/drag.svelte.js +13 -0
  89. package/dist/layout/presets.compactVariant.test.d.ts +1 -0
  90. package/dist/layout/presets.compactVariant.test.js +27 -0
  91. package/dist/layout/presets.d.ts +12 -0
  92. package/dist/layout/presets.js +16 -0
  93. package/dist/layout/store.drawers.svelte.test.d.ts +1 -0
  94. package/dist/layout/store.drawers.svelte.test.js +49 -0
  95. package/dist/layout/store.schemaVersion.test.d.ts +1 -0
  96. package/dist/layout/store.schemaVersion.test.js +35 -0
  97. package/dist/layout/store.svelte.js +52 -2
  98. package/dist/layout/types.d.ts +51 -1
  99. package/dist/layout/types.js +1 -1
  100. package/dist/layout/types.test.d.ts +1 -0
  101. package/dist/layout/types.test.js +26 -0
  102. package/dist/overlays/DrawerSurface.svelte +141 -0
  103. package/dist/overlays/DrawerSurface.svelte.d.ts +12 -0
  104. package/dist/overlays/DrawerSurface.svelte.test.d.ts +1 -0
  105. package/dist/overlays/DrawerSurface.svelte.test.js +67 -0
  106. package/dist/overlays/ModalFrame.svelte +3 -1
  107. package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
  108. package/dist/overlays/OverlayRoots.svelte +12 -9
  109. package/dist/overlays/floatDismiss.js +5 -0
  110. package/dist/overlays/focusTrap.d.ts +11 -1
  111. package/dist/overlays/focusTrap.js +11 -9
  112. package/dist/overlays/modal.js +1 -0
  113. package/dist/overlays/popup.js +4 -0
  114. package/dist/overlays/types.d.ts +10 -1
  115. package/dist/primitives/Button.svelte +18 -0
  116. package/dist/primitives/Button.svelte.d.ts +6 -0
  117. package/dist/primitives/ResizableSplitter.svelte +71 -11
  118. package/dist/primitives/ResizableSplitter.svelte.d.ts +8 -0
  119. package/dist/primitives/ResizableSplitter.svelte.test.d.ts +1 -0
  120. package/dist/primitives/ResizableSplitter.svelte.test.js +74 -0
  121. package/dist/server-shard/types.d.ts +2 -1
  122. package/dist/sh3Api/headless.js +9 -1
  123. package/dist/sh3Api/headless.svelte.test.js +45 -1
  124. package/dist/sh3Runtime.svelte.d.ts +36 -0
  125. package/dist/sh3Runtime.svelte.js +33 -0
  126. package/dist/shards/activate.svelte.js +10 -0
  127. package/dist/shards/ctx-fetch.test.d.ts +1 -0
  128. package/dist/shards/ctx-fetch.test.js +66 -0
  129. package/dist/shards/types.d.ts +22 -1
  130. package/dist/tokens.css +3 -2
  131. package/dist/transport/apiFetch.d.ts +1 -0
  132. package/dist/transport/apiFetch.js +65 -0
  133. package/dist/transport/apiFetch.test.d.ts +1 -0
  134. package/dist/transport/apiFetch.test.js +37 -0
  135. package/dist/transport/authToken.d.ts +2 -0
  136. package/dist/transport/authToken.js +53 -0
  137. package/dist/transport/authToken.test.d.ts +1 -0
  138. package/dist/transport/authToken.test.js +33 -0
  139. package/dist/verbs/types.d.ts +5 -2
  140. package/dist/version.d.ts +1 -1
  141. package/dist/version.js +1 -1
  142. package/dist/viewport/classify.d.ts +8 -0
  143. package/dist/viewport/classify.js +20 -0
  144. package/dist/viewport/classify.test.d.ts +1 -0
  145. package/dist/viewport/classify.test.js +32 -0
  146. package/dist/viewport/store.browser.test.d.ts +1 -0
  147. package/dist/viewport/store.browser.test.js +33 -0
  148. package/dist/viewport/store.svelte.d.ts +9 -0
  149. package/dist/viewport/store.svelte.js +71 -0
  150. package/dist/viewport/store.svelte.test.d.ts +1 -0
  151. package/dist/viewport/store.svelte.test.js +54 -0
  152. package/dist/viewport/types.d.ts +9 -0
  153. package/dist/viewport/types.js +6 -0
  154. package/package.json +1 -1
@@ -1 +1,11 @@
1
- export declare function createFocusTrap(container: HTMLElement): () => void;
1
+ export interface FocusTrapOptions {
2
+ /**
3
+ * When false, skip moving focus into the container on install. Tab
4
+ * cycling and previous-focus restoration still work — this only
5
+ * suppresses the initial auto-focus. Used by the command palette on
6
+ * touch-only devices to keep the on-screen keyboard from popping
7
+ * up unprompted.
8
+ */
9
+ initialFocus?: boolean;
10
+ }
11
+ export declare function createFocusTrap(container: HTMLElement, options?: FocusTrapOptions): () => void;
@@ -20,7 +20,7 @@ const FOCUSABLE_SELECTOR = [
20
20
  'textarea:not([disabled])',
21
21
  '[tabindex]:not([tabindex="-1"])',
22
22
  ].join(',');
23
- export function createFocusTrap(container) {
23
+ export function createFocusTrap(container, options = {}) {
24
24
  const previouslyFocused = document.activeElement;
25
25
  function getFocusables() {
26
26
  return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
@@ -47,14 +47,16 @@ export function createFocusTrap(container) {
47
47
  }
48
48
  }
49
49
  container.addEventListener('keydown', onKeydown);
50
- // Defer initial focus to the next microtask so the container contents
51
- // (which may still be rendering if createFocusTrap was called mid-mount)
52
- // have a chance to appear in the DOM.
53
- queueMicrotask(() => {
54
- var _a;
55
- const focusables = getFocusables();
56
- ((_a = focusables[0]) !== null && _a !== void 0 ? _a : container).focus();
57
- });
50
+ if (options.initialFocus !== false) {
51
+ // Defer initial focus to the next microtask so the container contents
52
+ // (which may still be rendering if createFocusTrap was called mid-mount)
53
+ // have a chance to appear in the DOM.
54
+ queueMicrotask(() => {
55
+ var _a;
56
+ const focusables = getFocusables();
57
+ ((_a = focusables[0]) !== null && _a !== void 0 ? _a : container).focus();
58
+ });
59
+ }
58
60
  return () => {
59
61
  container.removeEventListener('keydown', onKeydown);
60
62
  if (previouslyFocused && document.contains(previouslyFocused)) {
@@ -132,6 +132,7 @@ function openModal(Content, props, options) {
132
132
  close: handle.close,
133
133
  boxStyle: options === null || options === void 0 ? void 0 : options.boxStyle,
134
134
  onBackdropClick: (options === null || options === void 0 ? void 0 : options.dismissOnBackdrop) ? handle.close : undefined,
135
+ initialFocus: options === null || options === void 0 ? void 0 : options.initialFocus,
135
136
  },
136
137
  });
137
138
  entry.host = host;
@@ -26,6 +26,7 @@
26
26
  * - close() is idempotent.
27
27
  */
28
28
  import { mount, unmount } from 'svelte';
29
+ import { getOwner } from '../gestures';
29
30
  import PopupFrame from './PopupFrame.svelte';
30
31
  import { getLayerRoot } from './roots';
31
32
  import { registerDismissable } from '../navigation/back-stack';
@@ -44,6 +45,9 @@ let current = null;
44
45
  function onDocumentPointerDown(e) {
45
46
  if (!current)
46
47
  return;
48
+ // Skip dismiss if a gesture owns this pointer.
49
+ if (getOwner(e.pointerId))
50
+ return;
47
51
  const target = e.target;
48
52
  if (target && current.host.contains(target))
49
53
  return;
@@ -1,4 +1,4 @@
1
- export type OverlayLayer = 'floating' | 'drag-preview' | 'popup' | 'modal' | 'toast' | 'command';
1
+ export type OverlayLayer = 'drawers' | 'floating' | 'drag-preview' | 'popup' | 'modal' | 'toast' | 'command';
2
2
  /** A handle returned by every overlay opener. Calling close() is idempotent. */
3
3
  export interface OverlayHandle {
4
4
  close(): void;
@@ -44,4 +44,13 @@ export interface ModalOptions {
44
44
  * expected dismissal gesture.
45
45
  */
46
46
  dismissOnBackdrop?: boolean;
47
+ /**
48
+ * When false, suppress the focus trap's initial auto-focus on the
49
+ * modal's first focusable descendant. Tab cycling within the trap
50
+ * still works, and previous-focus is still restored on close.
51
+ * Default true. Opt-out is for cases where focusing an input pops
52
+ * an on-screen keyboard the user didn't ask for (e.g. command
53
+ * palette on touch-only devices).
54
+ */
55
+ initialFocus?: boolean;
47
56
  }
@@ -21,6 +21,7 @@
21
21
  sprite,
22
22
  disabled = false,
23
23
  loading,
24
+ pressed,
24
25
  type = 'button',
25
26
  title,
26
27
  ariaLabel,
@@ -35,6 +36,12 @@
35
36
  disabled?: boolean;
36
37
  /** Controlled pending state. When true, spinner + disabled + aria-busy. */
37
38
  loading?: boolean;
39
+ /**
40
+ * Toggle state for toolbar/action buttons. When provided, sets
41
+ * aria-pressed and applies a pressed visual treatment. Orthogonal
42
+ * to `variant` — works with any variant.
43
+ */
44
+ pressed?: boolean;
38
45
  type?: 'button' | 'submit' | 'reset';
39
46
  title?: string;
40
47
  ariaLabel?: string;
@@ -77,8 +84,10 @@
77
84
  {type}
78
85
  class="sh3-btn sh3-btn--{variant}"
79
86
  class:sh3-btn--icon-only={iconOnly}
87
+ class:sh3-btn--pressed={pressed}
80
88
  disabled={disabled || pending}
81
89
  aria-busy={pending || undefined}
90
+ aria-pressed={pressed ?? undefined}
82
91
  {title}
83
92
  aria-label={ariaLabel ?? (iconOnly ? title : undefined)}
84
93
  onclick={handleClick}
@@ -134,6 +143,15 @@
134
143
  cursor: not-allowed;
135
144
  }
136
145
 
146
+ /* pressed=true: inset shadow + slight dimming to signal toggle-on state */
147
+ .sh3-btn--pressed {
148
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.35);
149
+ filter: brightness(0.88);
150
+ }
151
+ .sh3-btn--pressed:hover:not(:disabled) {
152
+ filter: brightness(0.96);
153
+ }
154
+
137
155
  .sh3-btn--alert {
138
156
  background: var(--sh3-error);
139
157
  color: var(--sh3-fg-on-error);
@@ -9,6 +9,12 @@ type $$ComponentProps = {
9
9
  disabled?: boolean;
10
10
  /** Controlled pending state. When true, spinner + disabled + aria-busy. */
11
11
  loading?: boolean;
12
+ /**
13
+ * Toggle state for toolbar/action buttons. When provided, sets
14
+ * aria-pressed and applies a pressed visual treatment. Orthogonal
15
+ * to `variant` — works with any variant.
16
+ */
17
+ pressed?: boolean;
12
18
  type?: 'button' | 'submit' | 'reset';
13
19
  title?: string;
14
20
  ariaLabel?: string;
@@ -19,6 +19,8 @@
19
19
 
20
20
  import type { Snippet } from 'svelte';
21
21
  import type { SizeMode, SplitDirection } from '../layout/types';
22
+ import { claim, revoke } from '../gestures/pointerClaim';
23
+ import { ancestorCount } from '../gestures';
22
24
 
23
25
  const MIN_PX = 40;
24
26
  const COLLAPSED_PX = 28;
@@ -33,6 +35,7 @@
33
35
  pane,
34
36
  onResize,
35
37
  onCollapseToggle,
38
+ compact = false,
36
39
  }: {
37
40
  direction: SplitDirection;
38
41
  /**
@@ -69,6 +72,14 @@
69
72
  onResize?: (index: number, value: number) => void;
70
73
  /** Called when a collapsed pane's header is clicked to toggle. */
71
74
  onCollapseToggle?: (index: number, collapsed: boolean) => void;
75
+ /**
76
+ * Compact-mode rendering: drops the per-pane collapse arrows (the
77
+ * 16px side rail eats too much room on a phone) and freezes the
78
+ * resize handles into thin visual separators with no drag gesture.
79
+ * Existing collapsed panes stay visually collapsed but lose the
80
+ * expand affordance until the viewport flips back.
81
+ */
82
+ compact?: boolean;
72
83
  } = $props();
73
84
 
74
85
  let container: HTMLDivElement;
@@ -103,12 +114,19 @@
103
114
  };
104
115
 
105
116
  let drag: DragState | null = $state(null);
117
+ let activeDragPointerId: number | null = null;
106
118
 
107
119
  function beginDrag(e: PointerEvent, handleIndex: number) {
108
120
  // Disable resize handles adjacent to collapsed or fixed panes.
121
+ if (compact) return;
109
122
  if (isCollapsed(handleIndex) || isCollapsed(handleIndex + 1)) return;
110
123
  if (isHandleFrozen(handleIndex)) return;
111
124
 
125
+ const depth = container ? ancestorCount(container) : 0;
126
+ const claimGranted = claim(e.pointerId, { ownerId: 'sh3:splitter', axis: 'xy', priority: 'normal', depth });
127
+ if (!claimGranted) return;
128
+ activeDragPointerId = e.pointerId;
129
+
112
130
  e.preventDefault();
113
131
  (e.target as HTMLElement).setPointerCapture(e.pointerId);
114
132
 
@@ -188,6 +206,10 @@
188
206
  function endDrag(e: PointerEvent) {
189
207
  if (!drag) return;
190
208
  (e.target as HTMLElement).releasePointerCapture(e.pointerId);
209
+ if (activeDragPointerId !== null) {
210
+ revoke(activeDragPointerId, 'sh3:splitter');
211
+ activeDragPointerId = null;
212
+ }
191
213
  drag = null;
192
214
  }
193
215
  </script>
@@ -196,6 +218,7 @@
196
218
  class="splitter"
197
219
  class:horizontal={direction === 'horizontal'}
198
220
  class:vertical={direction === 'vertical'}
221
+ class:compact
199
222
  bind:this={container}
200
223
  >
201
224
  {#each Array(count) as _, i (i)}
@@ -205,18 +228,33 @@
205
228
  style="flex: {flexFor(i)};"
206
229
  >
207
230
  {#if isCollapsed(i)}
208
- <button
209
- type="button"
210
- class="collapse-header"
211
- class:horizontal={direction === 'horizontal'}
212
- class:vertical={direction === 'vertical'}
213
- onclick={() => onCollapseToggle?.(i, false)}
214
- aria-label="Expand pane"
215
- >
216
- <span class="collapse-icon">{direction === 'horizontal' ? '▸' : '▾'}</span>
217
- </button>
231
+ {#if compact}
232
+ <!--
233
+ Compact mode strands collapsed panes — the toggle isn't shown
234
+ so they can't be expanded until the viewport flips back to
235
+ desktop. Render an inert placeholder rather than a disabled
236
+ button so taps don't register at all.
237
+ -->
238
+ <div
239
+ class="collapse-header inert"
240
+ class:horizontal={direction === 'horizontal'}
241
+ class:vertical={direction === 'vertical'}
242
+ aria-hidden="true"
243
+ ></div>
244
+ {:else}
245
+ <button
246
+ type="button"
247
+ class="collapse-header"
248
+ class:horizontal={direction === 'horizontal'}
249
+ class:vertical={direction === 'vertical'}
250
+ onclick={() => onCollapseToggle?.(i, false)}
251
+ aria-label="Expand pane"
252
+ >
253
+ <span class="collapse-icon">{direction === 'horizontal' ? '▸' : '▾'}</span>
254
+ </button>
255
+ {/if}
218
256
  {:else}
219
- {#if onCollapseToggle && canCollapse(i)}
257
+ {#if !compact && onCollapseToggle && canCollapse(i)}
220
258
  <button
221
259
  type="button"
222
260
  class="collapse-header expanded"
@@ -367,4 +405,26 @@
367
405
  }
368
406
  .horizontal > .splitter-handle.frozen { width: 1px; }
369
407
  .vertical > .splitter-handle.frozen { height: 1px; }
408
+
409
+ /*
410
+ * Compact mode: handles are pure visual separators — 1px, no cursor
411
+ * change, no pointer events (so taps fall through to the panes
412
+ * beneath). Mirrors how the .frozen variant degrades a fixed-pane
413
+ * boundary, but applies to every handle in the splitter.
414
+ */
415
+ .splitter.compact > .splitter-handle {
416
+ pointer-events: none;
417
+ cursor: default;
418
+ opacity: 0.6;
419
+ }
420
+ .splitter.compact.horizontal > .splitter-handle { width: 1px; }
421
+ .splitter.compact.vertical > .splitter-handle { height: 1px; }
422
+ .splitter.compact > .splitter-handle:hover {
423
+ background: var(--sh3-border);
424
+ }
425
+ .collapse-header.inert {
426
+ pointer-events: none;
427
+ background: var(--sh3-bg-elevated);
428
+ flex: 0 0 auto;
429
+ }
370
430
  </style>
@@ -36,6 +36,14 @@ type $$ComponentProps = {
36
36
  onResize?: (index: number, value: number) => void;
37
37
  /** Called when a collapsed pane's header is clicked to toggle. */
38
38
  onCollapseToggle?: (index: number, collapsed: boolean) => void;
39
+ /**
40
+ * Compact-mode rendering: drops the per-pane collapse arrows (the
41
+ * 16px side rail eats too much room on a phone) and freezes the
42
+ * resize handles into thin visual separators with no drag gesture.
43
+ * Existing collapsed panes stay visually collapsed but lose the
44
+ * expand affordance until the viewport flips back.
45
+ */
46
+ compact?: boolean;
39
47
  };
40
48
  declare const ResizableSplitter: import("svelte").Component<$$ComponentProps, {}, "">;
41
49
  type ResizableSplitter = ReturnType<typeof ResizableSplitter>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ /*
2
+ * Compact-mode behavior pin for ResizableSplitter — when `compact=true`,
3
+ * the per-pane collapse arrow is not rendered, the root carries the
4
+ * `.compact` class (which CSS uses to thin handles + freeze pointer
5
+ * events), and pointer-down on a handle does not start a drag.
6
+ *
7
+ * Resize / drag / collapse interactions in desktop mode are covered by
8
+ * LayoutRenderer.browser.test.ts; this file pins the new gate only.
9
+ */
10
+ import { describe, it, expect, afterEach } from 'vitest';
11
+ import { claim, __resetForTest as resetClaims } from '../gestures/pointerClaim';
12
+ import { mount, unmount, flushSync } from 'svelte';
13
+ import ResizableSplitter from './ResizableSplitter.svelte';
14
+ const SplitterAny = ResizableSplitter;
15
+ let mounted = null;
16
+ let host = null;
17
+ function renderHost(props) {
18
+ host = document.createElement('div');
19
+ host.style.cssText = 'position: relative; width: 600px; height: 400px;';
20
+ document.body.appendChild(host);
21
+ mounted = mount(SplitterAny, { target: host, props });
22
+ flushSync();
23
+ return host;
24
+ }
25
+ afterEach(() => {
26
+ if (mounted) {
27
+ unmount(mounted);
28
+ mounted = null;
29
+ }
30
+ if (host) {
31
+ host.remove();
32
+ host = null;
33
+ }
34
+ resetClaims();
35
+ });
36
+ describe('ResizableSplitter compact-mode (dom)', () => {
37
+ const baseProps = (extra = {}) => (Object.assign({ direction: 'horizontal', sizes: [0.5, 0.5], count: 2, pane: () => null, onResize: () => { }, onCollapseToggle: () => { } }, extra));
38
+ it('desktop mode: collapse-toggle buttons render for each pane', () => {
39
+ const el = renderHost(baseProps({ compact: false }));
40
+ expect(el.querySelectorAll('[data-testid^="collapse-toggle-"]').length).toBe(2);
41
+ expect(el.querySelector('.splitter').classList.contains('compact')).toBe(false);
42
+ });
43
+ it('compact mode: no collapse-toggle buttons render', () => {
44
+ const el = renderHost(baseProps({ compact: true }));
45
+ expect(el.querySelectorAll('[data-testid^="collapse-toggle-"]').length).toBe(0);
46
+ });
47
+ it('compact mode: root carries .compact class', () => {
48
+ const el = renderHost(baseProps({ compact: true }));
49
+ expect(el.querySelector('.splitter').classList.contains('compact')).toBe(true);
50
+ });
51
+ it('compact mode: pointerdown on a handle does not begin a drag (no .dragging class)', () => {
52
+ const el = renderHost(baseProps({ compact: true }));
53
+ const handle = el.querySelector('.splitter-handle');
54
+ expect(handle).not.toBeNull();
55
+ // Simulate pointerdown — beginDrag should bail before adding .dragging.
56
+ const evt = new PointerEvent('pointerdown', { bubbles: true, pointerId: 1 });
57
+ handle.dispatchEvent(evt);
58
+ flushSync();
59
+ expect(handle.classList.contains('dragging')).toBe(false);
60
+ });
61
+ });
62
+ describe('ResizableSplitter PointerClaim integration', () => {
63
+ const baseProps = (extra = {}) => (Object.assign({ direction: 'horizontal', sizes: [0.5, 0.5], count: 2, pane: () => null, onResize: () => { }, onCollapseToggle: () => { } }, extra));
64
+ it('does not begin drag when pointer is already claimed', () => {
65
+ const el = renderHost(baseProps({ compact: false }));
66
+ const handle = el.querySelector('.splitter-handle');
67
+ expect(handle).not.toBeNull();
68
+ claim(1, { ownerId: 'app:pan', axis: 'xy', priority: 'normal', depth: 99 });
69
+ const evt = new PointerEvent('pointerdown', { bubbles: true, pointerId: 1, isPrimary: true });
70
+ handle.dispatchEvent(evt);
71
+ flushSync();
72
+ expect(handle.classList.contains('dragging')).toBe(false);
73
+ });
74
+ });
@@ -33,7 +33,8 @@ export interface TenantDocumentAPI {
33
33
  shardId: string;
34
34
  path: string;
35
35
  content: string | Uint8Array;
36
- incomingVersion: number;
36
+ /** The exact version the store will write. Not auto-incremented. */
37
+ assignedVersion: number;
37
38
  expectedLocalVersion: number;
38
39
  origin: string;
39
40
  deleted?: boolean;
@@ -300,7 +300,15 @@ export function makeSh3Api(opts) {
300
300
  },
301
301
  listActions(actionOpts) {
302
302
  const all = listActionsFromEntries(listActionEntriesFromRegistry(), getLiveDispatcherState());
303
- return (actionOpts === null || actionOpts === void 0 ? void 0 : actionOpts.activeOnly) ? all.filter((a) => a.active) : all;
303
+ let out = all;
304
+ if ((actionOpts === null || actionOpts === void 0 ? void 0 : actionOpts.submenuOf) !== undefined) {
305
+ const parent = actionOpts.submenuOf;
306
+ out = out.filter((a) => a.submenuOf === parent);
307
+ }
308
+ if (actionOpts === null || actionOpts === void 0 ? void 0 : actionOpts.activeOnly) {
309
+ out = out.filter((a) => a.active);
310
+ }
311
+ return out;
304
312
  },
305
313
  runAction(id, runOpts) {
306
314
  return dispatchActionProgrammatic(id, runOpts);
@@ -1,5 +1,8 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
2
  import { makeSh3Api } from './headless';
3
+ import { registerAction, __resetActionsRegistryForTest, } from '../actions/registry';
4
+ import { __resetContributionsForTest } from '../contributions/registry';
5
+ import { __resetDispatcherStateForTest } from '../actions/state.svelte';
3
6
  function makeMockZoneManager() {
4
7
  const data = {
5
8
  ephemeral: {},
@@ -57,3 +60,44 @@ describe('sh3Api readZone', () => {
57
60
  expect(api.readZone('not-a-shard', 'workspace')).toBe(null);
58
61
  });
59
62
  });
63
+ describe('sh3Api listActions submenu filter', () => {
64
+ beforeEach(() => {
65
+ __resetContributionsForTest();
66
+ __resetActionsRegistryForTest();
67
+ __resetDispatcherStateForTest();
68
+ });
69
+ it('returns only children of the named parent when { submenuOf } is set', () => {
70
+ registerAction({ id: 'theme.set', label: 'Theme', scope: 'home', submenu: true }, 'shard.x');
71
+ registerAction({
72
+ id: 'theme.set:dark', label: 'Dark', scope: 'home',
73
+ submenuOf: 'theme.set', run: () => { },
74
+ }, 'shard.x');
75
+ registerAction({
76
+ id: 'theme.set:light', label: 'Light', scope: 'home',
77
+ submenuOf: 'theme.set', run: () => { },
78
+ }, 'shard.x');
79
+ registerAction({ id: 'unrelated', label: 'U', scope: 'home', run: () => { } }, 'shard.x');
80
+ const api = makeSh3Api({ callerKind: 'verb' });
81
+ const ids = api.listActions({ submenuOf: 'theme.set' }).map((d) => d.id);
82
+ expect(ids.sort()).toEqual(['theme.set:dark', 'theme.set:light']);
83
+ });
84
+ it('returns [] when no children match the parent id', () => {
85
+ registerAction({ id: 'home.go', label: 'Go', scope: 'home', run: () => { } }, 'shard.x');
86
+ const api = makeSh3Api({ callerKind: 'verb' });
87
+ expect(api.listActions({ submenuOf: 'nope' })).toEqual([]);
88
+ });
89
+ it('combines with { activeOnly } — both predicates must hold', () => {
90
+ registerAction({ id: 'p', label: 'P', scope: 'home', submenu: true }, 'shard.x');
91
+ // active child (home is active by default in the test state)
92
+ registerAction({ id: 'p:a', label: 'A', scope: 'home',
93
+ submenuOf: 'p', run: () => { } }, 'shard.x');
94
+ // inactive child (app scope, no active app)
95
+ registerAction({ id: 'p:b', label: 'B', scope: 'app',
96
+ submenuOf: 'p', run: () => { } }, 'shard.x');
97
+ const api = makeSh3Api({ callerKind: 'verb' });
98
+ const ids = api
99
+ .listActions({ submenuOf: 'p', activeOnly: true })
100
+ .map((d) => d.id);
101
+ expect(ids).toEqual(['p:a']);
102
+ });
103
+ });
@@ -10,6 +10,8 @@ import type { ColorApi } from './color/api';
10
10
  import { type OpenContextMenuOpts, type OpenPaletteOpts } from './actions/listeners';
11
11
  import type { ActiveActionDescriptor } from './actions/types';
12
12
  import { type DispatchToTerminalResult } from './shell-shard/dispatch-to-terminal';
13
+ import type { ViewportInfo, ViewportClass } from './viewport/types';
14
+ import type { DrawerAnchor, DrawerStateMap } from './layout/compact/types';
13
15
  /**
14
16
  * The process-wide sh3 singleton exposed to shards and the sh3's own
15
17
  * internal code. Provides state zone creation and overlay managers.
@@ -39,6 +41,17 @@ export interface Sh3 {
39
41
  color: ColorApi;
40
42
  /** Actions facade — rebind keys, query bindings, open menus/palette. */
41
43
  actions: Sh3ActionsApi;
44
+ /**
45
+ * Reactive viewport classification. Subscribers fire on class change
46
+ * (desktop ↔ compact). Use `override(cls)` to pin a class for
47
+ * playgrounds and debug; pass null to restore auto-derivation.
48
+ */
49
+ readonly viewport: Sh3Viewport;
50
+ /**
51
+ * Compact-mode drawer surface controls. Inert on desktop — mutating
52
+ * methods throw rather than silently no-op so misuse is caught early.
53
+ */
54
+ readonly drawers: Sh3Drawers;
42
55
  /**
43
56
  * Dispatch `line` through a Terminal view's normal submit path. Used by
44
57
  * views outside a verb context (floating pickers, dialogs) to drive a
@@ -51,6 +64,29 @@ export interface Sh3 {
51
64
  */
52
65
  dispatchToTerminal(line: string): DispatchToTerminalResult;
53
66
  }
67
+ /**
68
+ * Compact-mode drawer surface controls. Mutating methods throw on desktop
69
+ * so misuse surfaces as a loud error instead of a silent no-op.
70
+ */
71
+ export interface Sh3Drawers {
72
+ readonly state: DrawerStateMap;
73
+ open(anchor: DrawerAnchor): void;
74
+ close(anchor: DrawerAnchor): void;
75
+ toggle(anchor: DrawerAnchor): void;
76
+ activate(anchor: DrawerAnchor, slotId: string): void;
77
+ }
78
+ /**
79
+ * Reactive viewport classification surface. See viewport/store.svelte.ts.
80
+ */
81
+ export interface Sh3Viewport {
82
+ /** Reactive — read directly inside an effect, or use `subscribe()`. */
83
+ readonly current: ViewportInfo;
84
+ subscribe(cb: (i: ViewportInfo) => void): () => void;
85
+ /** Pin the class. Pass null to restore auto. Debug/playground only. */
86
+ override(cls: ViewportClass | null): void;
87
+ /** Currently-pinned override (null = auto). */
88
+ readonly pinned: ViewportClass | null;
89
+ }
54
90
  /**
55
91
  * API for managing action bindings and triggering menus/palette
56
92
  * programmatically (e.g. from a future settings UI shard).
@@ -28,6 +28,8 @@ import { setUserBindings, getLiveDispatcherState, onActiveChange as onActiveChan
28
28
  import { listActions, onActionsChange } from './actions/registry';
29
29
  import { listActiveFromEntries } from './actions/listActive';
30
30
  import { makeDispatchToTerminal } from './shell-shard/dispatch-to-terminal';
31
+ import { viewportStore } from './viewport/store.svelte';
32
+ import { drawerStore } from './layout/compact/drawerStore.svelte';
31
33
  const sh3Actions = {
32
34
  async rebind(appId, actionId, shortcut) {
33
35
  await saveUserBinding(appId, actionId, shortcut);
@@ -77,4 +79,35 @@ export const sh3 = {
77
79
  color: colorApi,
78
80
  actions: sh3Actions,
79
81
  dispatchToTerminal: makeDispatchToTerminal({ headless: false }),
82
+ viewport: {
83
+ get current() { return viewportStore.current; },
84
+ subscribe: (cb) => viewportStore.subscribe(cb),
85
+ override: (cls) => viewportStore.override(cls),
86
+ get pinned() { return viewportStore.pinned; },
87
+ },
88
+ drawers: {
89
+ get state() { return drawerStore.state; },
90
+ open: (anchor) => {
91
+ assertCompact('open');
92
+ drawerStore.open(anchor);
93
+ },
94
+ close: (anchor) => {
95
+ assertCompact('close');
96
+ drawerStore.close(anchor);
97
+ },
98
+ toggle: (anchor) => {
99
+ assertCompact('toggle');
100
+ drawerStore.toggle(anchor);
101
+ },
102
+ activate: (anchor, slotId) => {
103
+ assertCompact('activate');
104
+ drawerStore.activate(anchor, slotId);
105
+ },
106
+ },
80
107
  };
108
+ function assertCompact(method) {
109
+ const cls = viewportStore.current.class;
110
+ if (cls !== 'compact') {
111
+ throw new Error(`Sh3.drawers.${method}: viewport class is "${cls}"; drawers exist only on compact`);
112
+ }
113
+ }
@@ -21,6 +21,8 @@ import { registerView, unregisterView, registerVerb as fwRegisterVerb, unregiste
21
21
  import { makeSh3Api } from '../sh3Api/headless';
22
22
  import { createDocumentHandle, getTenantId, getDocumentBackend } from '../documents';
23
23
  import { fetchEnvState, putEnvState } from '../env/client';
24
+ import { getEnvServerUrl } from '../env/index';
25
+ import { apiFetch } from '../transport/apiFetch';
24
26
  import { isAdmin as checkIsAdmin } from '../auth/index';
25
27
  import { createZoneManager } from '../state/manage';
26
28
  import { PERMISSION_STATE_MANAGE } from '../state/types';
@@ -157,6 +159,14 @@ export async function activateShard(id, opts) {
157
159
  entry.cleanupFns.push(() => handle.dispose());
158
160
  return handle;
159
161
  },
162
+ fetch(path, init) {
163
+ const isAbsolute = path.startsWith('http://') || path.startsWith('https://');
164
+ if (isAbsolute)
165
+ return apiFetch(path, init);
166
+ const base = getEnvServerUrl();
167
+ const sep = path.startsWith('/') ? '' : '/';
168
+ return apiFetch(`${base}${sep}${path}`, init);
169
+ },
160
170
  env(defaults) {
161
171
  if (envState.proxy) {
162
172
  console.warn(`[sh3] Shard "${id}" called ctx.env() more than once; extra calls are ignored.`);
@@ -0,0 +1 @@
1
+ export {};