sh3-core 0.10.4 → 0.11.2
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/Shell.svelte +12 -31
- package/dist/__test__/fixtures.js +1 -0
- package/dist/__test__/reset.js +6 -0
- package/dist/actions/CommandPalette.svelte +68 -0
- package/dist/actions/CommandPalette.svelte.d.ts +11 -0
- package/dist/actions/ContextMenu.svelte +97 -0
- package/dist/actions/ContextMenu.svelte.d.ts +9 -0
- package/dist/actions/bindings-store.d.ts +8 -0
- package/dist/actions/bindings-store.js +27 -0
- package/dist/actions/bindings-store.test.d.ts +1 -0
- package/dist/actions/bindings-store.test.js +25 -0
- package/dist/actions/bindings.d.ts +4 -0
- package/dist/actions/bindings.js +17 -0
- package/dist/actions/bindings.test.d.ts +1 -0
- package/dist/actions/bindings.test.js +30 -0
- package/dist/actions/contextMenuModel.d.ts +16 -0
- package/dist/actions/contextMenuModel.js +71 -0
- package/dist/actions/contextMenuModel.test.d.ts +1 -0
- package/dist/actions/contextMenuModel.test.js +44 -0
- package/dist/actions/dispatcher.svelte.d.ts +34 -0
- package/dist/actions/dispatcher.svelte.js +117 -0
- package/dist/actions/dispatcher.test.d.ts +1 -0
- package/dist/actions/dispatcher.test.js +155 -0
- package/dist/actions/listeners.d.ts +11 -0
- package/dist/actions/listeners.js +180 -0
- package/dist/actions/listeners.test.d.ts +1 -0
- package/dist/actions/listeners.test.js +149 -0
- package/dist/actions/palette-scorer.d.ts +11 -0
- package/dist/actions/palette-scorer.js +49 -0
- package/dist/actions/palette-scorer.test.d.ts +1 -0
- package/dist/actions/palette-scorer.test.js +40 -0
- package/dist/actions/paletteModel.d.ts +4 -0
- package/dist/actions/paletteModel.js +40 -0
- package/dist/actions/paletteModel.test.d.ts +1 -0
- package/dist/actions/paletteModel.test.js +33 -0
- package/dist/actions/registry.d.ts +10 -0
- package/dist/actions/registry.js +36 -0
- package/dist/actions/registry.test.d.ts +1 -0
- package/dist/actions/registry.test.js +49 -0
- package/dist/actions/selection.svelte.d.ts +8 -0
- package/dist/actions/selection.svelte.js +44 -0
- package/dist/actions/selection.test.d.ts +1 -0
- package/dist/actions/selection.test.js +51 -0
- package/dist/actions/shardContext.test.d.ts +1 -0
- package/dist/actions/shardContext.test.js +41 -0
- package/dist/actions/shellActions.test.d.ts +1 -0
- package/dist/actions/shellActions.test.js +22 -0
- package/dist/actions/shortcuts.d.ts +5 -0
- package/dist/actions/shortcuts.js +87 -0
- package/dist/actions/shortcuts.test.d.ts +1 -0
- package/dist/actions/shortcuts.test.js +49 -0
- package/dist/actions/state.svelte.d.ts +16 -0
- package/dist/actions/state.svelte.js +76 -0
- package/dist/actions/state.test.d.ts +1 -0
- package/dist/actions/state.test.js +40 -0
- package/dist/actions/syncMountedViewIds.test.d.ts +1 -0
- package/dist/actions/syncMountedViewIds.test.js +97 -0
- package/dist/actions/types.d.ts +56 -0
- package/dist/actions/types.js +7 -0
- package/dist/api.d.ts +2 -2
- package/dist/api.js +1 -1
- package/dist/apps/lifecycle.js +13 -3
- package/dist/createShell.js +4 -1
- package/dist/host.js +6 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/layout/LayoutRenderer.browser.test.js +78 -0
- package/dist/layout/LayoutRenderer.svelte +1 -0
- package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-6-fixed-slots-freezes-the-handle-adjacent-to-a-fixed-pane--dblclick-does-not-collapse-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-1.png +0 -0
- package/dist/layout/inspection.d.ts +11 -1
- package/dist/layout/inspection.js +13 -1
- package/dist/layout/ops-locate.test.d.ts +1 -0
- package/dist/layout/ops-locate.test.js +103 -0
- package/dist/layout/ops.d.ts +8 -0
- package/dist/layout/ops.js +27 -0
- package/dist/layout/slotHostPool.svelte.js +24 -0
- package/dist/layout/slotHostPool.test.js +14 -0
- package/dist/layout/types.d.ts +15 -0
- package/dist/overlays/FloatFrame.svelte +23 -11
- package/dist/overlays/ModalFrame.svelte +9 -1
- package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
- package/dist/overlays/__test__/DummyFrame.svelte +6 -0
- package/dist/overlays/__test__/DummyFrame.svelte.d.ts +6 -0
- package/dist/overlays/float.d.ts +6 -0
- package/dist/overlays/float.js +24 -9
- package/dist/overlays/float.test.js +175 -0
- package/dist/overlays/floatDismiss.d.ts +8 -0
- package/dist/overlays/floatDismiss.js +68 -0
- package/dist/overlays/modal.js +5 -1
- package/dist/overlays/modal.test.d.ts +1 -0
- package/dist/overlays/modal.test.js +55 -0
- package/dist/overlays/popup.d.ts +2 -0
- package/dist/overlays/popup.js +24 -4
- package/dist/overlays/popup.test.d.ts +1 -0
- package/dist/overlays/popup.test.js +95 -0
- package/dist/overlays/types.d.ts +17 -1
- package/dist/primitives/Button.svelte +144 -0
- package/dist/primitives/Button.svelte.d.ts +18 -0
- package/dist/primitives/ResizableSplitter.svelte +38 -3
- package/dist/primitives/ResizableSplitter.svelte.d.ts +7 -0
- package/dist/primitives/icon-context.d.ts +15 -0
- package/dist/primitives/icon-context.js +29 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.js +50 -0
- package/dist/shards/activate.svelte.js +14 -0
- package/dist/shards/types.d.ts +19 -0
- package/dist/shards/types.js +5 -4
- package/dist/shell-shard/locateSlot.test.d.ts +1 -0
- package/dist/shell-shard/locateSlot.test.js +101 -0
- package/dist/shell-shard/shellShard.svelte.d.ts +7 -0
- package/dist/shell-shard/shellShard.svelte.js +34 -1
- package/dist/shellRuntime.svelte.d.ts +19 -0
- package/dist/shellRuntime.svelte.js +30 -0
- package/dist/tokens.css +11 -1
- package/dist/verbs/types.d.ts +9 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/apps/terminal/manifest.d.ts +0 -8
- package/dist/apps/terminal/manifest.js +0 -14
- package/dist/apps/terminal/terminal-app.d.ts +0 -7
- package/dist/apps/terminal/terminal-app.js +0 -14
package/dist/overlays/popup.d.ts
CHANGED
package/dist/overlays/popup.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* - Popups do NOT stack. Opening a second popup dismisses the first.
|
|
10
10
|
* - Clicking outside the popup dismisses it.
|
|
11
11
|
* - Pressing Escape dismisses it.
|
|
12
|
-
* - The caller provides
|
|
13
|
-
* itself relative to the anchor's
|
|
12
|
+
* - The caller provides a PopupAnchor (HTMLElement or {x,y} point);
|
|
13
|
+
* the popup positions itself relative to the anchor's viewport rect.
|
|
14
14
|
*
|
|
15
15
|
* Implementation notes:
|
|
16
16
|
* - The manager keeps at most one active entry. show() closes any
|
|
@@ -28,6 +28,17 @@
|
|
|
28
28
|
import { mount, unmount } from 'svelte';
|
|
29
29
|
import PopupFrame from './PopupFrame.svelte';
|
|
30
30
|
import { getLayerRoot } from './roots';
|
|
31
|
+
/**
|
|
32
|
+
* Convert a PopupAnchor to a DOMRect.
|
|
33
|
+
* - HTMLElement: uses its live bounding rect.
|
|
34
|
+
* - { x, y } virtual point: zero-size rect at the viewport coordinates so
|
|
35
|
+
* PopupFrame places itself at bottom-start of the cursor position.
|
|
36
|
+
*/
|
|
37
|
+
function anchorRect(anchor) {
|
|
38
|
+
if (anchor instanceof HTMLElement)
|
|
39
|
+
return anchor.getBoundingClientRect();
|
|
40
|
+
return new DOMRect(anchor.x, anchor.y, 0, 0);
|
|
41
|
+
}
|
|
31
42
|
let current = null;
|
|
32
43
|
function onDocumentPointerDown(e) {
|
|
33
44
|
if (!current)
|
|
@@ -75,7 +86,7 @@ function showPopup(Content, options, props) {
|
|
|
75
86
|
host.style.inset = '0';
|
|
76
87
|
host.style.pointerEvents = 'none'; // only the frame captures pointer events
|
|
77
88
|
root.appendChild(host);
|
|
78
|
-
const
|
|
89
|
+
const rect = anchorRect(options.anchor);
|
|
79
90
|
const entry = {};
|
|
80
91
|
const handle = {
|
|
81
92
|
close: () => removeEntry(entry),
|
|
@@ -85,7 +96,7 @@ function showPopup(Content, options, props) {
|
|
|
85
96
|
props: {
|
|
86
97
|
Content: Content,
|
|
87
98
|
contentProps: (props !== null && props !== void 0 ? props : {}),
|
|
88
|
-
anchorRect,
|
|
99
|
+
anchorRect: rect,
|
|
89
100
|
close: handle.close,
|
|
90
101
|
},
|
|
91
102
|
});
|
|
@@ -106,3 +117,12 @@ export const popupManager = {
|
|
|
106
117
|
show: showPopup,
|
|
107
118
|
close: closeCurrent,
|
|
108
119
|
};
|
|
120
|
+
/** @internal — test helper only. Closes any active popup and resets state. */
|
|
121
|
+
export function __resetPopupManagerForTest() {
|
|
122
|
+
if (current) {
|
|
123
|
+
removeDismissListeners();
|
|
124
|
+
unmount(current.frame);
|
|
125
|
+
current.host.remove();
|
|
126
|
+
current = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { popupManager, __resetPopupManagerForTest } from './popup';
|
|
4
|
+
import { registerLayerRoot, unregisterLayerRoot } from './roots';
|
|
5
|
+
import DummyFrame from './__test__/DummyFrame.svelte';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
function makeLayerRoot() {
|
|
10
|
+
const el = document.createElement('div');
|
|
11
|
+
el.style.position = 'relative';
|
|
12
|
+
document.body.appendChild(el);
|
|
13
|
+
registerLayerRoot('popup', el);
|
|
14
|
+
return el;
|
|
15
|
+
}
|
|
16
|
+
function teardownLayerRoot(el) {
|
|
17
|
+
unregisterLayerRoot('popup');
|
|
18
|
+
el.remove();
|
|
19
|
+
}
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// P.1 — virtual-point anchor (context-menu use case)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
describe('popup — P.1 virtual-point anchor', () => {
|
|
24
|
+
let layerRoot;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
// Give the viewport a real size so the overflow clamp in PopupFrame
|
|
27
|
+
// doesn't crush our anchor coordinates to the clamped minimum.
|
|
28
|
+
vi.stubGlobal('innerWidth', 2000);
|
|
29
|
+
vi.stubGlobal('innerHeight', 2000);
|
|
30
|
+
layerRoot = makeLayerRoot();
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
__resetPopupManagerForTest();
|
|
34
|
+
teardownLayerRoot(layerRoot);
|
|
35
|
+
vi.unstubAllGlobals();
|
|
36
|
+
});
|
|
37
|
+
it('accepts { x, y } as anchor without throwing', () => {
|
|
38
|
+
expect(() => {
|
|
39
|
+
popupManager.show(DummyFrame, { anchor: { x: 100, y: 200 } }, {});
|
|
40
|
+
}).not.toThrow();
|
|
41
|
+
});
|
|
42
|
+
it('mounts the popup host inside the layer root', () => {
|
|
43
|
+
popupManager.show(DummyFrame, { anchor: { x: 100, y: 200 } }, {});
|
|
44
|
+
const host = layerRoot.querySelector('.sh3-popup-host');
|
|
45
|
+
expect(host).not.toBeNull();
|
|
46
|
+
});
|
|
47
|
+
it('renders the dummy frame content inside the host', async () => {
|
|
48
|
+
popupManager.show(DummyFrame, { anchor: { x: 100, y: 200 } }, {});
|
|
49
|
+
await tick();
|
|
50
|
+
const content = layerRoot.querySelector('.dummy-popup-content');
|
|
51
|
+
expect(content).not.toBeNull();
|
|
52
|
+
});
|
|
53
|
+
it('positions the popup frame at bottom-start of the virtual point', async () => {
|
|
54
|
+
popupManager.show(DummyFrame, { anchor: { x: 100, y: 200 } }, {});
|
|
55
|
+
await tick();
|
|
56
|
+
// PopupFrame sets top = anchor.bottom + 4 = 200 + 4 = 204 (zero-size rect: bottom === y).
|
|
57
|
+
// left = anchor.left = 100 (no overflow with 2000px viewport).
|
|
58
|
+
const frame = layerRoot.querySelector('.popup-frame');
|
|
59
|
+
expect(frame).not.toBeNull();
|
|
60
|
+
expect(frame.style.top).toBe('204px');
|
|
61
|
+
expect(frame.style.left).toBe('100px');
|
|
62
|
+
});
|
|
63
|
+
it('close() removes the host from the DOM', async () => {
|
|
64
|
+
popupManager.show(DummyFrame, { anchor: { x: 50, y: 80 } }, {});
|
|
65
|
+
await tick();
|
|
66
|
+
expect(layerRoot.querySelector('.sh3-popup-host')).not.toBeNull();
|
|
67
|
+
popupManager.close();
|
|
68
|
+
await tick();
|
|
69
|
+
expect(layerRoot.querySelector('.sh3-popup-host')).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// P.2 — HTMLElement anchor still works (regression guard)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
describe('popup — P.2 HTMLElement anchor regression', () => {
|
|
76
|
+
let layerRoot;
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
vi.stubGlobal('innerWidth', 2000);
|
|
79
|
+
vi.stubGlobal('innerHeight', 2000);
|
|
80
|
+
layerRoot = makeLayerRoot();
|
|
81
|
+
});
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
__resetPopupManagerForTest();
|
|
84
|
+
teardownLayerRoot(layerRoot);
|
|
85
|
+
vi.unstubAllGlobals();
|
|
86
|
+
});
|
|
87
|
+
it('accepts an HTMLElement anchor without throwing', () => {
|
|
88
|
+
const anchor = document.createElement('button');
|
|
89
|
+
document.body.appendChild(anchor);
|
|
90
|
+
expect(() => {
|
|
91
|
+
popupManager.show(DummyFrame, { anchor }, {});
|
|
92
|
+
}).not.toThrow();
|
|
93
|
+
anchor.remove();
|
|
94
|
+
});
|
|
95
|
+
});
|
package/dist/overlays/types.d.ts
CHANGED
|
@@ -9,8 +9,17 @@ export type ToastHandle = OverlayHandle;
|
|
|
9
9
|
export type ToastLevel = 'info' | 'warn' | 'error' | 'success';
|
|
10
10
|
/** Where a popup should sit relative to its anchor. Phase 5 ships bottom-start. */
|
|
11
11
|
export type PopupPlacement = 'bottom-start';
|
|
12
|
+
/**
|
|
13
|
+
* Anchor for a popup — either an HTMLElement whose bounding rect is used, or
|
|
14
|
+
* a virtual point `{ x, y }` in viewport coordinates (used by context menus
|
|
15
|
+
* that anchor at pointer coordinates).
|
|
16
|
+
*/
|
|
17
|
+
export type PopupAnchor = HTMLElement | {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
};
|
|
12
21
|
export interface PopupOptions {
|
|
13
|
-
anchor:
|
|
22
|
+
anchor: PopupAnchor;
|
|
14
23
|
placement?: PopupPlacement;
|
|
15
24
|
}
|
|
16
25
|
export interface ToastOptions {
|
|
@@ -28,4 +37,11 @@ export interface ModalOptions {
|
|
|
28
37
|
* views like non-centered palettes or inspectors.
|
|
29
38
|
*/
|
|
30
39
|
boxStyle?: string;
|
|
40
|
+
/**
|
|
41
|
+
* When true, a click on the frame area outside the dialog box closes the
|
|
42
|
+
* modal. Default false — content owns its own close UI. Opt-in is for
|
|
43
|
+
* picker-style modals like the command palette where outside-click is the
|
|
44
|
+
* expected dismissal gesture.
|
|
45
|
+
*/
|
|
46
|
+
dismissOnBackdrop?: boolean;
|
|
31
47
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* Button — themed button primitive for shards and apps.
|
|
4
|
+
*
|
|
5
|
+
* Icons resolve via a three-tier lookup (see primitives/icon-context.ts).
|
|
6
|
+
* `icon` accepts either a sprite symbol id ("save") or a direct URL
|
|
7
|
+
* ending in .svg / containing a slash ("./foo.svg"); the URL form
|
|
8
|
+
* bypasses the active sprite entirely.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Snippet } from 'svelte';
|
|
12
|
+
import coreSpriteUrl from '../assets/icons.svg';
|
|
13
|
+
import { getIconSprite, type ButtonVariant } from './icon-context';
|
|
14
|
+
|
|
15
|
+
let {
|
|
16
|
+
variant = 'default',
|
|
17
|
+
icon,
|
|
18
|
+
sprite,
|
|
19
|
+
disabled = false,
|
|
20
|
+
type = 'button',
|
|
21
|
+
title,
|
|
22
|
+
ariaLabel,
|
|
23
|
+
onclick,
|
|
24
|
+
children,
|
|
25
|
+
}: {
|
|
26
|
+
variant?: ButtonVariant;
|
|
27
|
+
/** Sprite symbol id, or a direct .svg URL. */
|
|
28
|
+
icon?: string;
|
|
29
|
+
/** Override the sprite sheet URL for this button only. */
|
|
30
|
+
sprite?: string;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
type?: 'button' | 'submit' | 'reset';
|
|
33
|
+
title?: string;
|
|
34
|
+
ariaLabel?: string;
|
|
35
|
+
onclick?: (event: MouseEvent) => void;
|
|
36
|
+
children?: Snippet;
|
|
37
|
+
} = $props();
|
|
38
|
+
|
|
39
|
+
const contextSprite = getIconSprite();
|
|
40
|
+
|
|
41
|
+
function isDirectUrl(value: string): boolean {
|
|
42
|
+
return value.includes('/') || value.endsWith('.svg');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const iconHref = $derived.by(() => {
|
|
46
|
+
if (!icon) return undefined;
|
|
47
|
+
if (isDirectUrl(icon)) return icon;
|
|
48
|
+
const base = sprite ?? contextSprite ?? coreSpriteUrl;
|
|
49
|
+
return `${base}#${icon}`;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const iconOnly = $derived(variant === 'icon' || (!!icon && !children));
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<button
|
|
56
|
+
{type}
|
|
57
|
+
class="sh3-btn sh3-btn--{variant}"
|
|
58
|
+
class:sh3-btn--icon-only={iconOnly}
|
|
59
|
+
{disabled}
|
|
60
|
+
{title}
|
|
61
|
+
aria-label={ariaLabel ?? (iconOnly ? title : undefined)}
|
|
62
|
+
{onclick}
|
|
63
|
+
>
|
|
64
|
+
{#if iconHref}
|
|
65
|
+
<svg class="sh3-btn__icon" aria-hidden="true">
|
|
66
|
+
<use href={iconHref} />
|
|
67
|
+
</svg>
|
|
68
|
+
{/if}
|
|
69
|
+
{#if children}
|
|
70
|
+
<span class="sh3-btn__label">{@render children()}</span>
|
|
71
|
+
{/if}
|
|
72
|
+
</button>
|
|
73
|
+
|
|
74
|
+
<style>
|
|
75
|
+
.sh3-btn {
|
|
76
|
+
appearance: none;
|
|
77
|
+
display: inline-flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
gap: var(--shell-pad-sm);
|
|
81
|
+
padding: 6px 14px;
|
|
82
|
+
background: var(--shell-accent);
|
|
83
|
+
color: var(--shell-fg-on-accent);
|
|
84
|
+
border: none;
|
|
85
|
+
border-radius: var(--shell-radius);
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
font-family: inherit;
|
|
88
|
+
font-size: 0.875rem;
|
|
89
|
+
line-height: var(--shell-line);
|
|
90
|
+
}
|
|
91
|
+
.sh3-btn:hover:not(:disabled) { filter: brightness(1.12); }
|
|
92
|
+
.sh3-btn:active:not(:disabled) { filter: brightness(0.92); }
|
|
93
|
+
.sh3-btn:focus-visible {
|
|
94
|
+
box-shadow: var(--shell-focus-ring);
|
|
95
|
+
outline: none;
|
|
96
|
+
}
|
|
97
|
+
.sh3-btn:disabled {
|
|
98
|
+
opacity: 0.55;
|
|
99
|
+
cursor: not-allowed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.sh3-btn--alert {
|
|
103
|
+
background: var(--shell-error);
|
|
104
|
+
color: var(--shell-fg-on-error);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.sh3-btn--ghost {
|
|
108
|
+
background: transparent;
|
|
109
|
+
color: var(--shell-fg);
|
|
110
|
+
border: 1px solid var(--shell-border);
|
|
111
|
+
}
|
|
112
|
+
.sh3-btn--ghost:hover:not(:disabled) {
|
|
113
|
+
background: var(--shell-bg-elevated);
|
|
114
|
+
filter: none;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.sh3-btn--icon {
|
|
118
|
+
background: transparent;
|
|
119
|
+
color: var(--shell-fg-muted);
|
|
120
|
+
padding: var(--shell-pad-sm);
|
|
121
|
+
}
|
|
122
|
+
.sh3-btn--icon:hover:not(:disabled) {
|
|
123
|
+
background: var(--shell-bg-elevated);
|
|
124
|
+
color: var(--shell-fg);
|
|
125
|
+
filter: none;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.sh3-btn--icon-only {
|
|
129
|
+
padding: var(--shell-pad-sm);
|
|
130
|
+
width: 26px;
|
|
131
|
+
height: 26px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.sh3-btn__icon {
|
|
135
|
+
width: 16px;
|
|
136
|
+
height: 16px;
|
|
137
|
+
flex-shrink: 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.sh3-btn__label {
|
|
141
|
+
display: inline-flex;
|
|
142
|
+
white-space: nowrap;
|
|
143
|
+
}
|
|
144
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type ButtonVariant } from './icon-context';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
variant?: ButtonVariant;
|
|
5
|
+
/** Sprite symbol id, or a direct .svg URL. */
|
|
6
|
+
icon?: string;
|
|
7
|
+
/** Override the sprite sheet URL for this button only. */
|
|
8
|
+
sprite?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
type?: 'button' | 'submit' | 'reset';
|
|
11
|
+
title?: string;
|
|
12
|
+
ariaLabel?: string;
|
|
13
|
+
onclick?: (event: MouseEvent) => void;
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
};
|
|
16
|
+
declare const Button: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
17
|
+
type Button = ReturnType<typeof Button>;
|
|
18
|
+
export default Button;
|
|
@@ -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={() =>
|
|
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. */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual variants for <Button>. Exported from this module (rather than
|
|
3
|
+
* from Button.svelte itself) so TypeDoc, which only sees `.svelte`
|
|
4
|
+
* imports through the ambient `*.svelte` asset declaration, can
|
|
5
|
+
* discover it. Button.svelte re-imports this type.
|
|
6
|
+
*/
|
|
7
|
+
export type ButtonVariant = 'default' | 'alert' | 'ghost' | 'icon';
|
|
8
|
+
/**
|
|
9
|
+
* Register an icon sprite URL for this subtree. Call from a view's
|
|
10
|
+
* <script> block to have every descendant <Button icon="..."/> resolve
|
|
11
|
+
* the icon name against this sprite.
|
|
12
|
+
*/
|
|
13
|
+
export declare function provideIcons(spriteUrl: string): void;
|
|
14
|
+
/** Read the currently-active sprite URL, or undefined if none set. */
|
|
15
|
+
export declare function getIconSprite(): string | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Icon sprite context — lets a view opt all descendant <Button> (and any
|
|
3
|
+
* future icon-consuming primitive) into using a different sprite sheet
|
|
4
|
+
* than sh3-core's default assets/icons.svg.
|
|
5
|
+
*
|
|
6
|
+
* Resolution order inside Button is:
|
|
7
|
+
* 1. `sprite` prop on the Button (explicit one-off override)
|
|
8
|
+
* 2. nearest `provideIcons(url)` call up the Svelte tree (this module)
|
|
9
|
+
* 3. sh3-core's bundled sprite (fallback)
|
|
10
|
+
*
|
|
11
|
+
* This is tree-scoped (Svelte context), so it does not cross custom
|
|
12
|
+
* element / iframe boundaries. Shards that render inside such boundaries
|
|
13
|
+
* should pass `sprite` explicitly or call provideIcons() from their own
|
|
14
|
+
* root component.
|
|
15
|
+
*/
|
|
16
|
+
import { getContext, setContext } from 'svelte';
|
|
17
|
+
const ICON_SPRITE_KEY = Symbol('sh3.icon-sprite');
|
|
18
|
+
/**
|
|
19
|
+
* Register an icon sprite URL for this subtree. Call from a view's
|
|
20
|
+
* <script> block to have every descendant <Button icon="..."/> resolve
|
|
21
|
+
* the icon name against this sprite.
|
|
22
|
+
*/
|
|
23
|
+
export function provideIcons(spriteUrl) {
|
|
24
|
+
setContext(ICON_SPRITE_KEY, spriteUrl);
|
|
25
|
+
}
|
|
26
|
+
/** Read the currently-active sprite URL, or undefined if none set. */
|
|
27
|
+
export function getIconSprite() {
|
|
28
|
+
return getContext(ICON_SPRITE_KEY);
|
|
29
|
+
}
|
|
@@ -25,6 +25,9 @@ import { mount, unmount } from 'svelte';
|
|
|
25
25
|
import ShellHome from './ShellHome.svelte';
|
|
26
26
|
import KeysAndPeers from '../shell/views/KeysAndPeers.svelte';
|
|
27
27
|
import { VERSION } from '../version';
|
|
28
|
+
import { __setBindingsZone } from '../actions/bindings-store';
|
|
29
|
+
import { registeredApps } from '../apps/registry.svelte';
|
|
30
|
+
import { launchApp } from '../apps/lifecycle';
|
|
28
31
|
export const sh3coreShard = {
|
|
29
32
|
manifest: {
|
|
30
33
|
id: '__sh3core__',
|
|
@@ -36,6 +39,24 @@ export const sh3coreShard = {
|
|
|
36
39
|
],
|
|
37
40
|
},
|
|
38
41
|
activate(ctx) {
|
|
42
|
+
const zones = ctx.state({
|
|
43
|
+
user: {
|
|
44
|
+
bindings: {},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
__setBindingsZone(zones.user);
|
|
48
|
+
ctx.actions.register({
|
|
49
|
+
id: 'sh3.palette.open',
|
|
50
|
+
label: 'Command Palette…',
|
|
51
|
+
scope: ['home', 'app'],
|
|
52
|
+
defaultShortcut: 'Mod+K',
|
|
53
|
+
contextItem: false,
|
|
54
|
+
paletteItem: false,
|
|
55
|
+
run(_dispatchCtx) {
|
|
56
|
+
// Lazy import to avoid circular module load at boot.
|
|
57
|
+
import('../actions/listeners').then(({ openPalette }) => openPalette());
|
|
58
|
+
},
|
|
59
|
+
});
|
|
39
60
|
const factory = {
|
|
40
61
|
mount(container, _context) {
|
|
41
62
|
const instance = mount(ShellHome, { target: container });
|
|
@@ -55,6 +76,35 @@ export const sh3coreShard = {
|
|
|
55
76
|
};
|
|
56
77
|
ctx.registerView('sh3core:home', factory);
|
|
57
78
|
ctx.registerView('shell:keys-and-peers', keysFactory);
|
|
79
|
+
// Dynamic launcher actions: one "Launch <App>" per registered app, kept
|
|
80
|
+
// in sync as packages install/uninstall via the registry. Re-launching
|
|
81
|
+
// the active app is harmless (lifecycle treats it as a resume).
|
|
82
|
+
const launcherUnregisters = new Map();
|
|
83
|
+
$effect.root(() => {
|
|
84
|
+
$effect(() => {
|
|
85
|
+
const currentIds = new Set();
|
|
86
|
+
for (const [id, app] of registeredApps) {
|
|
87
|
+
currentIds.add(id);
|
|
88
|
+
if (launcherUnregisters.has(id))
|
|
89
|
+
continue;
|
|
90
|
+
const off = ctx.actions.register({
|
|
91
|
+
id: `sh3.app.launch:${id}`,
|
|
92
|
+
label: `Launch ${app.manifest.label}`,
|
|
93
|
+
scope: ['home', 'app'],
|
|
94
|
+
run() {
|
|
95
|
+
void launchApp(id);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
launcherUnregisters.set(id, off);
|
|
99
|
+
}
|
|
100
|
+
for (const id of [...launcherUnregisters.keys()]) {
|
|
101
|
+
if (!currentIds.has(id)) {
|
|
102
|
+
launcherUnregisters.get(id)();
|
|
103
|
+
launcherUnregisters.delete(id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
58
108
|
},
|
|
59
109
|
autostart() {
|
|
60
110
|
// Intentionally empty. Defining this field is what puts the sh3core
|
|
@@ -29,6 +29,9 @@ import { createShardKeysApi } from '../keys/client';
|
|
|
29
29
|
import { PERMISSION_KEYS_MINT } from '../keys/types';
|
|
30
30
|
import { subscribe } from '../keys/revocation-bus.svelte';
|
|
31
31
|
import { register as contributionsRegister, list as contributionsList, listPoints as contributionsListPoints, onChange as contributionsOnChange, } from '../contributions';
|
|
32
|
+
import { registerAction } from '../actions/registry';
|
|
33
|
+
import { makeSelectionApi, clearSelectionForShard } from '../actions/selection.svelte';
|
|
34
|
+
import { openContextMenu as shellOpenContextMenu, openPalette as shellOpenPalette } from '../actions/listeners';
|
|
32
35
|
/**
|
|
33
36
|
* Reactive registry of every shard known to the host. Keys are shard ids.
|
|
34
37
|
* Populated once at boot by the glob-discovery loop in main.ts (through
|
|
@@ -171,6 +174,16 @@ export async function activateShard(id) {
|
|
|
171
174
|
})
|
|
172
175
|
: undefined,
|
|
173
176
|
contributions,
|
|
177
|
+
actions: {
|
|
178
|
+
register(action) {
|
|
179
|
+
const dispose = registerAction(action, id);
|
|
180
|
+
entry.cleanupFns.push(async () => dispose());
|
|
181
|
+
return dispose;
|
|
182
|
+
},
|
|
183
|
+
selection: makeSelectionApi(id),
|
|
184
|
+
openContextMenu(opts) { shellOpenContextMenu(opts); },
|
|
185
|
+
openPalette(opts) { shellOpenPalette(opts); },
|
|
186
|
+
},
|
|
174
187
|
};
|
|
175
188
|
entry.ctx = ctx;
|
|
176
189
|
// Wire onKeyRevoked hook: subscribe to the revocation bus for this shard.
|
|
@@ -228,6 +241,7 @@ export function deactivateShard(id) {
|
|
|
228
241
|
fwUnregisterVerb(name);
|
|
229
242
|
for (const viewId of entry.viewIds)
|
|
230
243
|
unregisterView(viewId);
|
|
244
|
+
clearSelectionForShard(id);
|
|
231
245
|
active.delete(id);
|
|
232
246
|
activeShards.delete(id);
|
|
233
247
|
}
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type { EnvState } from '../env/types';
|
|
|
6
6
|
import type { Verb } from '../verbs/types';
|
|
7
7
|
import type { ShardContextKeys } from '../keys/types';
|
|
8
8
|
import type { ContributionsApi } from '../contributions/types';
|
|
9
|
+
import type { ActionsApi } from '../actions/types';
|
|
10
|
+
import type { TreeRootRef } from '../layout/types';
|
|
9
11
|
export { PERMISSION_KEYS_MINT, type ShardContextKeys, type ApiKeyPublic, type MintOpts, ScopeEscalationError, ConsentDeniedError } from '../keys/types';
|
|
10
12
|
/**
|
|
11
13
|
* The object returned by `ViewFactory.mount`. The framework calls
|
|
@@ -59,6 +61,18 @@ export interface MountContext {
|
|
|
59
61
|
* Call this whenever the view's save-state changes.
|
|
60
62
|
*/
|
|
61
63
|
setDirty(dirty: boolean): void;
|
|
64
|
+
/**
|
|
65
|
+
* Snapshot of this view's current host location. Returns
|
|
66
|
+
* `{ kind: 'docked' }` when the containing slot is anywhere in the
|
|
67
|
+
* active docked tree, `{ kind: 'float', floatId }` when it lives
|
|
68
|
+
* inside a float's subtree, or `null` when the slot is no longer
|
|
69
|
+
* present in the active layout.
|
|
70
|
+
*
|
|
71
|
+
* Not reactive — each call walks the current layout. Call it when
|
|
72
|
+
* you need to decide something (e.g. toggle a pop-out button,
|
|
73
|
+
* branch in a context-menu handler).
|
|
74
|
+
*/
|
|
75
|
+
location(): TreeRootRef | null;
|
|
62
76
|
}
|
|
63
77
|
/**
|
|
64
78
|
* The shard-side adapter that knows how to bring a view to life inside a
|
|
@@ -232,6 +246,11 @@ export interface ShardContext {
|
|
|
232
246
|
* docs/sh3-rfcs/2026-04-20-shard-contribution-points.md.
|
|
233
247
|
*/
|
|
234
248
|
contributions: ContributionsApi;
|
|
249
|
+
/**
|
|
250
|
+
* Register UI actions (keyboard shortcuts, context-menu items, palette
|
|
251
|
+
* entries). Actions are auto-unregistered when the shard deactivates.
|
|
252
|
+
*/
|
|
253
|
+
actions: ActionsApi;
|
|
235
254
|
}
|
|
236
255
|
/**
|
|
237
256
|
* A shard module. Shards are the fundamental unit of contribution in SH3.
|