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.
- package/dist/Sh3.svelte +107 -39
- package/dist/__screenshots__/handheld.browser.test.ts/handheld-viewport-flip-e2e-viewport-override-flips-chrome-and-body-branches-1.png +0 -0
- package/dist/actions/CommandPalette.svelte +1 -2
- package/dist/actions/listActionsFromEntries.test.js +29 -0
- package/dist/actions/listActive.js +2 -0
- package/dist/actions/listeners.js +16 -1
- package/dist/actions/programmatic-dispatch.svelte.test.js +9 -2
- package/dist/actions/types.d.ts +8 -0
- package/dist/api.d.ts +8 -1
- package/dist/app/store/storeShard.svelte.js +1 -21
- package/dist/app/store/version.d.ts +11 -0
- package/dist/app/store/version.js +39 -0
- package/dist/app/store/version.test.d.ts +1 -0
- package/dist/app/store/version.test.js +44 -0
- package/dist/apps/lifecycle.d.ts +6 -0
- package/dist/apps/lifecycle.js +5 -2
- package/dist/apps/lifecycle.test.js +30 -0
- package/dist/apps/types.d.ts +12 -0
- package/dist/assets/iconIds.generated.d.ts +1 -1
- package/dist/assets/iconIds.generated.js +5 -0
- package/dist/assets/icons.svg +31 -0
- package/dist/auth/auth.svelte.js +18 -8
- package/dist/auth/types.d.ts +6 -0
- package/dist/chrome/CompactChrome.svelte +130 -0
- package/dist/chrome/CompactChrome.svelte.d.ts +3 -0
- package/dist/chrome/CompactChrome.svelte.test.d.ts +1 -0
- package/dist/chrome/CompactChrome.svelte.test.js +174 -0
- package/dist/chrome/MenuSheet.svelte +224 -0
- package/dist/chrome/MenuSheet.svelte.d.ts +7 -0
- package/dist/chrome/MenuSheet.svelte.test.d.ts +1 -0
- package/dist/chrome/MenuSheet.svelte.test.js +46 -0
- package/dist/createShell.d.ts +9 -0
- package/dist/createShell.js +20 -7
- package/dist/createShell.remoteAuth.test.d.ts +1 -0
- package/dist/createShell.remoteAuth.test.js +71 -0
- package/dist/documents/http-backend.js +12 -11
- package/dist/env/client.js +11 -5
- package/dist/files/types.d.ts +106 -0
- package/dist/files/types.js +1 -0
- package/dist/gestures/gestureRegistry.d.ts +6 -0
- package/dist/gestures/gestureRegistry.js +190 -0
- package/dist/gestures/gestureRegistry.test.d.ts +1 -0
- package/dist/gestures/gestureRegistry.test.js +119 -0
- package/dist/gestures/index.d.ts +6 -0
- package/dist/gestures/index.js +12 -0
- package/dist/gestures/pointerClaim.d.ts +7 -0
- package/dist/gestures/pointerClaim.js +36 -0
- package/dist/gestures/pointerClaim.test.d.ts +1 -0
- package/dist/gestures/pointerClaim.test.js +64 -0
- package/dist/gestures/types.d.ts +83 -0
- package/dist/gestures/types.js +1 -0
- package/dist/handheld.browser.test.d.ts +1 -0
- package/dist/handheld.browser.test.js +90 -0
- package/dist/host-entry.d.ts +1 -0
- package/dist/host-entry.js +1 -0
- package/dist/layout/LayoutRenderer.browser.test.js +15 -3
- package/dist/layout/LayoutRenderer.svelte +27 -3
- package/dist/layout/LayoutRenderer.svelte.d.ts +4 -1
- 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
- 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
- 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
- package/dist/layout/compact/CarouselTabs.svelte +361 -0
- package/dist/layout/compact/CarouselTabs.svelte.d.ts +10 -0
- package/dist/layout/compact/CarouselTabs.svelte.test.d.ts +1 -0
- package/dist/layout/compact/CarouselTabs.svelte.test.js +300 -0
- package/dist/layout/compact/CompactRenderer.svelte +53 -0
- package/dist/layout/compact/CompactRenderer.svelte.d.ts +3 -0
- package/dist/layout/compact/CompactRenderer.svelte.test.d.ts +1 -0
- package/dist/layout/compact/CompactRenderer.svelte.test.js +125 -0
- package/dist/layout/compact/derive.d.ts +3 -0
- package/dist/layout/compact/derive.js +157 -0
- package/dist/layout/compact/derive.test.d.ts +1 -0
- package/dist/layout/compact/derive.test.js +197 -0
- package/dist/layout/compact/drawerStore.svelte.d.ts +21 -0
- package/dist/layout/compact/drawerStore.svelte.js +75 -0
- package/dist/layout/compact/drawerStore.svelte.test.d.ts +1 -0
- package/dist/layout/compact/drawerStore.svelte.test.js +43 -0
- package/dist/layout/compact/enrichCarousels.d.ts +8 -0
- package/dist/layout/compact/enrichCarousels.js +44 -0
- package/dist/layout/compact/enrichCarousels.test.d.ts +1 -0
- package/dist/layout/compact/enrichCarousels.test.js +88 -0
- package/dist/layout/compact/resolveRole.d.ts +6 -0
- package/dist/layout/compact/resolveRole.js +13 -0
- package/dist/layout/compact/resolveRole.test.d.ts +1 -0
- package/dist/layout/compact/resolveRole.test.js +18 -0
- package/dist/layout/compact/types.d.ts +30 -0
- package/dist/layout/compact/types.js +15 -0
- package/dist/layout/drag.svelte.js +13 -0
- package/dist/layout/presets.compactVariant.test.d.ts +1 -0
- package/dist/layout/presets.compactVariant.test.js +27 -0
- package/dist/layout/presets.d.ts +12 -0
- package/dist/layout/presets.js +16 -0
- package/dist/layout/store.drawers.svelte.test.d.ts +1 -0
- package/dist/layout/store.drawers.svelte.test.js +49 -0
- package/dist/layout/store.schemaVersion.test.d.ts +1 -0
- package/dist/layout/store.schemaVersion.test.js +35 -0
- package/dist/layout/store.svelte.js +52 -2
- package/dist/layout/types.d.ts +51 -1
- package/dist/layout/types.js +1 -1
- package/dist/layout/types.test.d.ts +1 -0
- package/dist/layout/types.test.js +26 -0
- package/dist/overlays/DrawerSurface.svelte +141 -0
- package/dist/overlays/DrawerSurface.svelte.d.ts +12 -0
- package/dist/overlays/DrawerSurface.svelte.test.d.ts +1 -0
- package/dist/overlays/DrawerSurface.svelte.test.js +67 -0
- package/dist/overlays/ModalFrame.svelte +3 -1
- package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
- package/dist/overlays/OverlayRoots.svelte +12 -9
- package/dist/overlays/floatDismiss.js +5 -0
- package/dist/overlays/focusTrap.d.ts +11 -1
- package/dist/overlays/focusTrap.js +11 -9
- package/dist/overlays/modal.js +1 -0
- package/dist/overlays/popup.js +4 -0
- package/dist/overlays/types.d.ts +10 -1
- package/dist/primitives/Button.svelte +18 -0
- package/dist/primitives/Button.svelte.d.ts +6 -0
- package/dist/primitives/ResizableSplitter.svelte +71 -11
- package/dist/primitives/ResizableSplitter.svelte.d.ts +8 -0
- package/dist/primitives/ResizableSplitter.svelte.test.d.ts +1 -0
- package/dist/primitives/ResizableSplitter.svelte.test.js +74 -0
- package/dist/server-shard/types.d.ts +2 -1
- package/dist/sh3Api/headless.js +9 -1
- package/dist/sh3Api/headless.svelte.test.js +45 -1
- package/dist/sh3Runtime.svelte.d.ts +36 -0
- package/dist/sh3Runtime.svelte.js +33 -0
- package/dist/shards/activate.svelte.js +10 -0
- package/dist/shards/ctx-fetch.test.d.ts +1 -0
- package/dist/shards/ctx-fetch.test.js +66 -0
- package/dist/shards/types.d.ts +22 -1
- package/dist/tokens.css +3 -2
- package/dist/transport/apiFetch.d.ts +1 -0
- package/dist/transport/apiFetch.js +65 -0
- package/dist/transport/apiFetch.test.d.ts +1 -0
- package/dist/transport/apiFetch.test.js +37 -0
- package/dist/transport/authToken.d.ts +2 -0
- package/dist/transport/authToken.js +53 -0
- package/dist/transport/authToken.test.d.ts +1 -0
- package/dist/transport/authToken.test.js +33 -0
- package/dist/verbs/types.d.ts +5 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/viewport/classify.d.ts +8 -0
- package/dist/viewport/classify.js +20 -0
- package/dist/viewport/classify.test.d.ts +1 -0
- package/dist/viewport/classify.test.js +32 -0
- package/dist/viewport/store.browser.test.d.ts +1 -0
- package/dist/viewport/store.browser.test.js +33 -0
- package/dist/viewport/store.svelte.d.ts +9 -0
- package/dist/viewport/store.svelte.js +71 -0
- package/dist/viewport/store.svelte.test.d.ts +1 -0
- package/dist/viewport/store.svelte.test.js +54 -0
- package/dist/viewport/types.d.ts +9 -0
- package/dist/viewport/types.js +6 -0
- package/package.json +1 -1
|
@@ -1 +1,11 @@
|
|
|
1
|
-
export
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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)) {
|
package/dist/overlays/modal.js
CHANGED
|
@@ -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;
|
package/dist/overlays/popup.js
CHANGED
|
@@ -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;
|
package/dist/overlays/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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;
|
package/dist/sh3Api/headless.js
CHANGED
|
@@ -300,7 +300,15 @@ export function makeSh3Api(opts) {
|
|
|
300
300
|
},
|
|
301
301
|
listActions(actionOpts) {
|
|
302
302
|
const all = listActionsFromEntries(listActionEntriesFromRegistry(), getLiveDispatcherState());
|
|
303
|
-
|
|
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 {};
|