sh3-core 0.15.0 → 0.15.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/actions/ctx-actions.svelte.test.js +111 -0
- package/dist/actions/dispatcher.svelte.js +23 -2
- package/dist/actions/dispatcher.test.js +33 -0
- package/dist/actions/listActionsFromEntries.test.js +78 -0
- package/dist/actions/listActive.d.ts +2 -1
- package/dist/actions/listActive.js +43 -17
- package/dist/actions/listeners.d.ts +16 -0
- package/dist/actions/listeners.js +68 -14
- package/dist/actions/programmatic-dispatch.svelte.test.d.ts +1 -0
- package/dist/actions/programmatic-dispatch.svelte.test.js +98 -0
- package/dist/actions/types.d.ts +37 -0
- package/dist/api.d.ts +1 -1
- package/dist/app/store/verbs.js +4 -0
- package/dist/app-appearance/appearanceShard.svelte.js +19 -6
- package/dist/app-appearance/appearanceState.svelte.js +3 -3
- package/dist/host.js +2 -1
- package/dist/layouts-shard/LayoutSaveModal.svelte +145 -0
- package/dist/layouts-shard/LayoutSaveModal.svelte.d.ts +12 -0
- package/dist/layouts-shard/LayoutsSection.svelte +142 -0
- package/dist/layouts-shard/LayoutsSection.svelte.d.ts +3 -0
- package/dist/layouts-shard/filter.d.ts +3 -0
- package/dist/layouts-shard/filter.js +66 -0
- package/dist/layouts-shard/filter.test.d.ts +1 -0
- package/dist/layouts-shard/filter.test.js +123 -0
- package/dist/layouts-shard/index.d.ts +1 -0
- package/dist/layouts-shard/index.js +1 -0
- package/dist/layouts-shard/layoutsApi.d.ts +12 -0
- package/dist/layouts-shard/layoutsApi.js +41 -0
- package/dist/layouts-shard/layoutsApi.test.d.ts +1 -0
- package/dist/layouts-shard/layoutsApi.test.js +74 -0
- package/dist/layouts-shard/layoutsShard.svelte.d.ts +11 -0
- package/dist/layouts-shard/layoutsShard.svelte.js +231 -0
- package/dist/layouts-shard/layoutsShard.svelte.test.d.ts +1 -0
- package/dist/layouts-shard/layoutsShard.svelte.test.js +215 -0
- package/dist/layouts-shard/layoutsState.svelte.d.ts +9 -0
- package/dist/layouts-shard/layoutsState.svelte.js +50 -0
- package/dist/layouts-shard/layoutsState.test.d.ts +1 -0
- package/dist/layouts-shard/layoutsState.test.js +43 -0
- package/dist/layouts-shard/types.d.ts +21 -0
- package/dist/layouts-shard/types.js +6 -0
- package/dist/{app-appearance/AppAppearanceModal.svelte → overlays/EntityAppearanceModal.svelte} +36 -31
- package/dist/overlays/EntityAppearanceModal.svelte.d.ts +19 -0
- package/dist/overlays/EntityAppearanceModal.test.d.ts +1 -0
- package/dist/overlays/EntityAppearanceModal.test.js +57 -0
- package/dist/overlays/FloatFrame.svelte +149 -8
- package/dist/overlays/FloatFrame.svelte.d.ts +1 -1
- package/dist/overlays/FloatLayer.svelte +2 -2
- package/dist/overlays/float.d.ts +38 -1
- package/dist/overlays/float.js +82 -0
- package/dist/overlays/float.test.js +394 -0
- package/dist/overlays/floatMaximized.svelte.d.ts +4 -0
- package/dist/overlays/floatMaximized.svelte.js +30 -0
- package/dist/runtime/runVerb-shell.test.d.ts +1 -0
- package/dist/runtime/runVerb-shell.test.js +231 -0
- package/dist/sh3core-shard/ShellHome.svelte +3 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.d.ts +7 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.js +23 -0
- package/dist/shards/activate-runtime.test.js +24 -2
- package/dist/shards/activate.svelte.js +18 -4
- package/dist/shards/types.d.ts +44 -4
- package/dist/shell-shard/CommandLine.svelte +143 -0
- package/dist/shell-shard/CommandLine.svelte.d.ts +26 -0
- package/dist/shell-shard/CommandLine.svelte.test.d.ts +1 -0
- package/dist/shell-shard/CommandLine.svelte.test.js +43 -0
- package/dist/shell-shard/InputLine.svelte +17 -40
- package/dist/shell-shard/InputLine.svelte.d.ts +2 -0
- package/dist/shell-shard/ScrollbackView.svelte +10 -3
- package/dist/shell-shard/ScrollbackView.svelte.d.ts +1 -0
- package/dist/shell-shard/Terminal.svelte +94 -22
- package/dist/shell-shard/buffer-store.d.ts +15 -0
- package/dist/shell-shard/buffer-store.js +124 -0
- package/dist/shell-shard/buffer-store.svelte.test.d.ts +1 -0
- package/dist/shell-shard/buffer-store.svelte.test.js +107 -0
- package/dist/shell-shard/buffer-zone-state.svelte.d.ts +38 -0
- package/dist/shell-shard/buffer-zone-state.svelte.js +31 -0
- package/dist/shell-shard/contract.d.ts +7 -0
- package/dist/shell-shard/dispatch-custom.test.js +3 -1
- package/dist/shell-shard/dispatch-gating.test.js +6 -2
- package/dist/shell-shard/dispatch-invoke.test.js +10 -8
- package/dist/shell-shard/dispatch.d.ts +7 -2
- package/dist/shell-shard/dispatch.js +23 -27
- package/dist/shell-shard/display-cwd.d.ts +1 -0
- package/dist/shell-shard/display-cwd.js +27 -0
- package/dist/shell-shard/display-cwd.test.d.ts +1 -0
- package/dist/shell-shard/display-cwd.test.js +29 -0
- package/dist/shell-shard/entries/StatusEntry.svelte +2 -0
- package/dist/shell-shard/manifest.js +2 -1
- package/dist/shell-shard/manifest.test.d.ts +1 -0
- package/dist/shell-shard/manifest.test.js +8 -0
- package/dist/shell-shard/mode-buffer.svelte.d.ts +8 -0
- package/dist/shell-shard/mode-buffer.svelte.js +19 -0
- package/dist/shell-shard/mode-buffer.svelte.test.d.ts +1 -0
- package/dist/shell-shard/mode-buffer.svelte.test.js +25 -0
- package/dist/shell-shard/modes/builtin.js +2 -0
- package/dist/shell-shard/modes/types.d.ts +8 -0
- package/dist/shell-shard/protocol.d.ts +12 -6
- package/dist/shell-shard/replay.d.ts +3 -0
- package/dist/shell-shard/replay.js +44 -0
- package/dist/shell-shard/replay.svelte.test.d.ts +1 -0
- package/dist/shell-shard/replay.svelte.test.js +47 -0
- package/dist/shell-shard/rich-registry.d.ts +5 -0
- package/dist/shell-shard/rich-registry.js +25 -0
- package/dist/shell-shard/rich-registry.test.d.ts +1 -0
- package/dist/shell-shard/rich-registry.test.js +31 -0
- package/dist/shell-shard/scrollback.svelte.d.ts +2 -0
- package/dist/shell-shard/scrollback.svelte.js +23 -0
- package/dist/shell-shard/scrollback.svelte.test.d.ts +1 -0
- package/dist/shell-shard/scrollback.svelte.test.js +51 -0
- package/dist/shell-shard/session-client.svelte.d.ts +18 -2
- package/dist/shell-shard/session-client.svelte.js +21 -4
- package/dist/shell-shard/shellApi.d.ts +2 -1
- package/dist/shell-shard/shellApi.js +32 -3
- package/dist/shell-shard/shellApi.svelte.test.d.ts +1 -0
- package/dist/shell-shard/shellApi.svelte.test.js +59 -0
- package/dist/shell-shard/shellShard.svelte.js +11 -1
- package/dist/shell-shard/terminal-dispatch.test.js +3 -1
- package/dist/shell-shard/verbs/apps.js +9 -0
- package/dist/shell-shard/verbs/env.js +4 -0
- package/dist/shell-shard/verbs/help.js +9 -1
- package/dist/shell-shard/verbs/help.svelte.test.d.ts +1 -0
- package/dist/shell-shard/verbs/help.svelte.test.js +53 -0
- package/dist/shell-shard/verbs/history.js +8 -1
- package/dist/shell-shard/verbs/index.js +0 -8
- package/dist/shell-shard/verbs/shards.js +5 -0
- package/dist/shell-shard/verbs/views.js +9 -0
- package/dist/shell-shard/verbs/zones.js +9 -0
- 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/app-appearance/AppAppearanceModal.svelte.d.ts +0 -8
- package/dist/shell-shard/verbs/cat.d.ts +0 -2
- package/dist/shell-shard/verbs/cat.js +0 -34
- package/dist/shell-shard/verbs/cd.test.js +0 -56
- package/dist/shell-shard/verbs/ls.d.ts +0 -2
- package/dist/shell-shard/verbs/ls.js +0 -29
- package/dist/shell-shard/verbs/ls.test.js +0 -49
- package/dist/shell-shard/verbs/session.d.ts +0 -4
- package/dist/shell-shard/verbs/session.js +0 -97
- /package/dist/{shell-shard/verbs/cd.test.d.ts → actions/ctx-actions.svelte.test.d.ts} +0 -0
- /package/dist/{shell-shard/verbs/ls.test.d.ts → actions/listActionsFromEntries.test.d.ts} +0 -0
package/dist/{app-appearance/AppAppearanceModal.svelte → overlays/EntityAppearanceModal.svelte}
RENAMED
|
@@ -1,58 +1,65 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* EntityAppearanceModal — generalization of the per-entity Customize
|
|
4
|
+
* modal. Used by __app-appearance__ for apps and __layouts__ for saved
|
|
5
|
+
* layouts. Purely presentational: callers pass an
|
|
6
|
+
* onSave({ label, icon, color }) callback and decide what an empty
|
|
7
|
+
* label means (apps treat empty as "use manifest"; layouts mark the
|
|
8
|
+
* input required via requireLabel: true).
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import { untrack } from 'svelte';
|
|
10
12
|
import IconPicker from '../primitives/widgets/IconPicker.svelte';
|
|
11
13
|
import ColorSwatch from '../primitives/widgets/ColorSwatch.svelte';
|
|
12
14
|
import iconsUrl from '../assets/icons.svg';
|
|
13
|
-
import { listRegisteredApps } from '../api';
|
|
14
|
-
import { getAppearance, setAppearance } from './appearanceState.svelte';
|
|
15
15
|
|
|
16
16
|
interface Props {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
entityLabel: string;
|
|
18
|
+
initialAppearance?: { icon?: string; color?: string };
|
|
19
|
+
defaultIcon: string;
|
|
20
|
+
requireLabel: boolean;
|
|
21
|
+
onSave(next: { label: string; icon?: string; color?: string }): void;
|
|
22
|
+
onReset(): void;
|
|
19
23
|
close: () => void;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
let {
|
|
26
|
+
let {
|
|
27
|
+
entityLabel,
|
|
28
|
+
initialAppearance,
|
|
29
|
+
defaultIcon,
|
|
30
|
+
requireLabel,
|
|
31
|
+
onSave,
|
|
32
|
+
onReset,
|
|
33
|
+
close,
|
|
34
|
+
}: Props = $props();
|
|
23
35
|
|
|
24
|
-
const initial = untrack(() =>
|
|
25
|
-
const manifestIcon = untrack(
|
|
26
|
-
() => listRegisteredApps().find((m) => m.id === appId)?.icon,
|
|
27
|
-
);
|
|
36
|
+
const initial = untrack(() => initialAppearance);
|
|
28
37
|
|
|
29
38
|
let icon = $state<string | undefined>(initial?.icon);
|
|
30
39
|
let color = $state<string | undefined>(initial?.color);
|
|
31
|
-
let label = $state<string>(
|
|
40
|
+
let label = $state<string>(untrack(() => entityLabel));
|
|
32
41
|
let pickerOpen = $state<boolean>(initial?.icon !== undefined);
|
|
33
42
|
|
|
43
|
+
const trimmed = $derived(label.trim());
|
|
44
|
+
const saveDisabled = $derived(requireLabel && trimmed === '');
|
|
45
|
+
const effectiveLabel = $derived(trimmed === '' ? entityLabel : trimmed);
|
|
46
|
+
const effectiveIcon = $derived(icon ?? defaultIcon);
|
|
34
47
|
const hasOverride = $derived(initial !== undefined);
|
|
35
|
-
const effectiveLabel = $derived(label.trim() === '' ? appLabel : label.trim());
|
|
36
|
-
const effectiveIcon = $derived(icon ?? manifestIcon ?? 'box');
|
|
37
48
|
|
|
38
49
|
function save() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
icon,
|
|
42
|
-
color,
|
|
43
|
-
label: trimmed === '' ? undefined : trimmed,
|
|
44
|
-
});
|
|
50
|
+
if (saveDisabled) return;
|
|
51
|
+
onSave({ label: trimmed, icon, color });
|
|
45
52
|
close();
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
function reset() {
|
|
49
|
-
|
|
56
|
+
onReset();
|
|
50
57
|
close();
|
|
51
58
|
}
|
|
52
59
|
</script>
|
|
53
60
|
|
|
54
61
|
<div class="app-appearance">
|
|
55
|
-
<h2>Customize {
|
|
62
|
+
<h2>Customize {entityLabel}</h2>
|
|
56
63
|
|
|
57
64
|
<div class="preview">
|
|
58
65
|
<div
|
|
@@ -67,11 +74,11 @@
|
|
|
67
74
|
</div>
|
|
68
75
|
</div>
|
|
69
76
|
|
|
70
|
-
<label class="row"><span>Name <em>(empty = default)</em
|
|
77
|
+
<label class="row"><span>Name {#if !requireLabel}<em>(empty = default)</em>{/if}</span>
|
|
71
78
|
<input
|
|
72
79
|
type="text"
|
|
73
80
|
bind:value={label}
|
|
74
|
-
placeholder={
|
|
81
|
+
placeholder={entityLabel}
|
|
75
82
|
class="name-input"
|
|
76
83
|
/>
|
|
77
84
|
</label>
|
|
@@ -95,9 +102,9 @@
|
|
|
95
102
|
</label>
|
|
96
103
|
|
|
97
104
|
<div class="actions">
|
|
98
|
-
<button type="button" class="primary" onclick={save}>Save</button>
|
|
99
|
-
<button type="button" onclick={reset} disabled={!hasOverride}>Reset</button>
|
|
100
|
-
<button type="button" onclick={close}>Cancel</button>
|
|
105
|
+
<button type="button" class="primary" onclick={save} disabled={saveDisabled}>Save</button>
|
|
106
|
+
<button type="button" data-role="reset" onclick={reset} disabled={!hasOverride}>Reset</button>
|
|
107
|
+
<button type="button" onclick={() => close()}>Cancel</button>
|
|
101
108
|
</div>
|
|
102
109
|
</div>
|
|
103
110
|
|
|
@@ -158,8 +165,6 @@
|
|
|
158
165
|
-webkit-line-clamp: 2;
|
|
159
166
|
line-clamp: 2;
|
|
160
167
|
}
|
|
161
|
-
.preview-card-icon { width: 24px; height: 24px; color: var(--shell-fg); }
|
|
162
|
-
.preview-card-label { font-weight: 600; padding: 0 4px; line-height: 1.2; }
|
|
163
168
|
.actions { display: flex; gap: 8px; margin-top: 16px; }
|
|
164
169
|
.actions button {
|
|
165
170
|
background: var(--shell-bg-elevated);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
entityLabel: string;
|
|
3
|
+
initialAppearance?: {
|
|
4
|
+
icon?: string;
|
|
5
|
+
color?: string;
|
|
6
|
+
};
|
|
7
|
+
defaultIcon: string;
|
|
8
|
+
requireLabel: boolean;
|
|
9
|
+
onSave(next: {
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
color?: string;
|
|
13
|
+
}): void;
|
|
14
|
+
onReset(): void;
|
|
15
|
+
close: () => void;
|
|
16
|
+
}
|
|
17
|
+
declare const EntityAppearanceModal: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type EntityAppearanceModal = ReturnType<typeof EntityAppearanceModal>;
|
|
19
|
+
export default EntityAppearanceModal;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { mount, flushSync } from 'svelte';
|
|
3
|
+
import EntityAppearanceModal from './EntityAppearanceModal.svelte';
|
|
4
|
+
function mountModal(props = {}) {
|
|
5
|
+
const target = document.createElement('div');
|
|
6
|
+
document.body.appendChild(target);
|
|
7
|
+
const onSave = vi.fn();
|
|
8
|
+
const onReset = vi.fn();
|
|
9
|
+
const close = vi.fn();
|
|
10
|
+
const instance = mount(EntityAppearanceModal, {
|
|
11
|
+
target,
|
|
12
|
+
props: Object.assign({ entityLabel: 'My App', defaultIcon: 'box', requireLabel: false, onSave,
|
|
13
|
+
onReset,
|
|
14
|
+
close }, props),
|
|
15
|
+
});
|
|
16
|
+
return { target, onSave, onReset, close, instance };
|
|
17
|
+
}
|
|
18
|
+
describe('EntityAppearanceModal', () => {
|
|
19
|
+
it('renders the entityLabel as title placeholder', () => {
|
|
20
|
+
const { target } = mountModal();
|
|
21
|
+
const input = target.querySelector('input.name-input');
|
|
22
|
+
expect(input.placeholder).toBe('My App');
|
|
23
|
+
});
|
|
24
|
+
it('shows the "(empty = default)" hint when requireLabel is false', () => {
|
|
25
|
+
const { target } = mountModal({ requireLabel: false });
|
|
26
|
+
expect(target.textContent).toContain('(empty = default)');
|
|
27
|
+
});
|
|
28
|
+
it('hides the "(empty = default)" hint when requireLabel is true', () => {
|
|
29
|
+
const { target } = mountModal({ requireLabel: true });
|
|
30
|
+
expect(target.textContent).not.toContain('(empty = default)');
|
|
31
|
+
});
|
|
32
|
+
it('disables Save when requireLabel is true and the input is empty', async () => {
|
|
33
|
+
const { target } = mountModal({ requireLabel: true, entityLabel: '' });
|
|
34
|
+
flushSync();
|
|
35
|
+
const save = target.querySelector('button.primary');
|
|
36
|
+
expect(save.disabled).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it('calls onSave with the trimmed label, icon, color', async () => {
|
|
39
|
+
const { target, onSave } = mountModal({
|
|
40
|
+
initialAppearance: { icon: 'cog', color: '#ff0000' },
|
|
41
|
+
});
|
|
42
|
+
const input = target.querySelector('input.name-input');
|
|
43
|
+
input.value = ' Renamed ';
|
|
44
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
45
|
+
flushSync();
|
|
46
|
+
target.querySelector('button.primary').click();
|
|
47
|
+
expect(onSave).toHaveBeenCalledWith({ label: 'Renamed', icon: 'cog', color: '#ff0000' });
|
|
48
|
+
});
|
|
49
|
+
it('calls onReset and close when Reset is clicked', async () => {
|
|
50
|
+
const { target, onReset, close } = mountModal({
|
|
51
|
+
initialAppearance: { icon: 'cog' },
|
|
52
|
+
});
|
|
53
|
+
target.querySelector('button[data-role="reset"]').click();
|
|
54
|
+
expect(onReset).toHaveBeenCalled();
|
|
55
|
+
expect(close).toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -2,34 +2,69 @@
|
|
|
2
2
|
Single floating panel frame.
|
|
3
3
|
|
|
4
4
|
Renders:
|
|
5
|
-
- Header bar (title + close
|
|
5
|
+
- Header bar (title + maximize/close buttons, receives pointerdown for drag).
|
|
6
6
|
- Body that mounts the float's content subtree via LayoutRenderer
|
|
7
7
|
using rootRef={{ kind: 'float', floatId: entry.id }} so the
|
|
8
8
|
renderer reads from layoutStore.tree.floats[...].content instead
|
|
9
9
|
of layoutStore.root.
|
|
10
|
+
- Bottom-right resize grip (always rendered, including on dismissable
|
|
11
|
+
pickers — its pointerdown is inside the frame so the dismiss listener
|
|
12
|
+
doesn't fire).
|
|
10
13
|
|
|
11
14
|
Behavior:
|
|
12
15
|
- Pointer drag on header mutates entry.position in place. The entry
|
|
13
16
|
is a live reference from layoutStore.tree.floats, so mutation
|
|
14
17
|
reactivity flows through the workspace-zone proxy.
|
|
18
|
+
- Pointer drag on the resize grip mutates entry.size, clamped at
|
|
19
|
+
computeMinSize(entry.content).
|
|
15
20
|
- Click anywhere on the frame raises it (calls floatManager.focus).
|
|
16
|
-
- Close button calls floatManager.close.
|
|
21
|
+
- Close button calls floatManager.close. Maximize button toggles.
|
|
22
|
+
- Header double-click toggles maximize (excluding clicks on the close /
|
|
23
|
+
maximize buttons).
|
|
24
|
+
- Drag or resize while maximized implicitly un-maximizes (forgets the
|
|
25
|
+
saved prev rect; keeps the current rect and proceeds). See spec
|
|
26
|
+
docs/superpowers/specs/2026-05-07-float-resize-maximize-design.md.
|
|
17
27
|
-->
|
|
18
28
|
<script lang="ts">
|
|
19
29
|
import LayoutRenderer from '../layout/LayoutRenderer.svelte';
|
|
20
30
|
import { floatManager, getFloatParentHost } from './float';
|
|
21
31
|
import { registerDismissableFrame, unregisterDismissableFrame } from './floatDismiss';
|
|
32
|
+
import { computeMinSize } from '../layout/floats';
|
|
22
33
|
import type { FloatEntry } from '../layout/types';
|
|
34
|
+
import { shell } from '../shellRuntime.svelte';
|
|
35
|
+
import { makeSelectionApi } from '../actions/selection.svelte';
|
|
36
|
+
|
|
37
|
+
const floatHeaderSelection = makeSelectionApi('__layouts__');
|
|
38
|
+
|
|
39
|
+
function openHeaderContextMenu(e: MouseEvent): void {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
floatHeaderSelection.set({ type: 'float-header', ref: { floatId: entry.id } });
|
|
42
|
+
shell.actions.openContextMenu({
|
|
43
|
+
x: e.clientX,
|
|
44
|
+
y: e.clientY,
|
|
45
|
+
scope: { element: 'float-header' },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
23
48
|
|
|
24
49
|
interface Props {
|
|
25
50
|
entry: FloatEntry;
|
|
26
51
|
}
|
|
27
|
-
|
|
52
|
+
// `entry` is the live workspace-zone proxy from `layoutStore.floats`; the
|
|
53
|
+
// drag/resize/dismiss-listener paths mutate `entry.position` and
|
|
54
|
+
// `entry.size` in place, which is the canonical reactive flow. Marking
|
|
55
|
+
// it `$bindable()` opts into that mutation so Svelte 5 doesn't emit the
|
|
56
|
+
// ownership_invalid_mutation warning. The parent (FloatLayer) uses
|
|
57
|
+
// `bind:entry` to acknowledge the contract.
|
|
58
|
+
let { entry = $bindable() }: Props = $props();
|
|
28
59
|
|
|
29
60
|
let dragging = $state(false);
|
|
30
61
|
let dragOffset = { x: 0, y: 0 };
|
|
62
|
+
let resizing = $state(false);
|
|
63
|
+
let resizeStart = { pointer: { x: 0, y: 0 }, size: { w: 0, h: 0 }, min: { w: 0, h: 0 } };
|
|
31
64
|
let frameEl: HTMLDivElement | undefined = $state();
|
|
32
65
|
|
|
66
|
+
const isMaximized = $derived(floatManager.isMaximized(entry.id));
|
|
67
|
+
|
|
33
68
|
$effect(() => {
|
|
34
69
|
if (!entry.dismissable) return;
|
|
35
70
|
if (!frameEl) return;
|
|
@@ -62,8 +97,9 @@
|
|
|
62
97
|
function onHeaderPointerDown(e: PointerEvent): void {
|
|
63
98
|
if (e.button !== 0) return;
|
|
64
99
|
if ((e.target as HTMLElement).closest('.sh3-float-close')) return;
|
|
100
|
+
if ((e.target as HTMLElement).closest('.sh3-float-maximize')) return;
|
|
65
101
|
const target = e.currentTarget as HTMLElement;
|
|
66
|
-
target.setPointerCapture(e.pointerId);
|
|
102
|
+
target.setPointerCapture?.(e.pointerId);
|
|
67
103
|
dragging = true;
|
|
68
104
|
dragOffset = { x: e.clientX - entry.position.x, y: e.clientY - entry.position.y };
|
|
69
105
|
floatManager.focus(entry.id);
|
|
@@ -71,6 +107,11 @@
|
|
|
71
107
|
|
|
72
108
|
function onHeaderPointerMove(e: PointerEvent): void {
|
|
73
109
|
if (!dragging) return;
|
|
110
|
+
// Implicit un-maximize on first drag movement (no-op if not maximized).
|
|
111
|
+
// We unmaximize on move rather than pointerdown so a casual click —
|
|
112
|
+
// which precedes a dblclick — does not destroy the saved prev rect
|
|
113
|
+
// and break the dblclick toggle.
|
|
114
|
+
floatManager.unmaximize(entry.id);
|
|
74
115
|
entry.position.x = e.clientX - dragOffset.x;
|
|
75
116
|
entry.position.y = e.clientY - dragOffset.y;
|
|
76
117
|
}
|
|
@@ -79,7 +120,45 @@
|
|
|
79
120
|
if (!dragging) return;
|
|
80
121
|
dragging = false;
|
|
81
122
|
const target = e.currentTarget as HTMLElement;
|
|
82
|
-
if (target.hasPointerCapture(e.pointerId)) {
|
|
123
|
+
if (target.hasPointerCapture?.(e.pointerId)) {
|
|
124
|
+
target.releasePointerCapture(e.pointerId);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function onHeaderDblClick(e: MouseEvent): void {
|
|
129
|
+
if ((e.target as HTMLElement).closest('.sh3-float-close')) return;
|
|
130
|
+
if ((e.target as HTMLElement).closest('.sh3-float-maximize')) return;
|
|
131
|
+
floatManager.toggleMaximize(entry.id);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function onGripPointerDown(e: PointerEvent): void {
|
|
135
|
+
if (e.button !== 0) return;
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
const target = e.currentTarget as HTMLElement;
|
|
138
|
+
target.setPointerCapture?.(e.pointerId);
|
|
139
|
+
resizing = true;
|
|
140
|
+
resizeStart = {
|
|
141
|
+
pointer: { x: e.clientX, y: e.clientY },
|
|
142
|
+
size: { w: entry.size.w, h: entry.size.h },
|
|
143
|
+
min: computeMinSize(entry.content),
|
|
144
|
+
};
|
|
145
|
+
floatManager.focus(entry.id);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function onGripPointerMove(e: PointerEvent): void {
|
|
149
|
+
if (!resizing) return;
|
|
150
|
+
floatManager.unmaximize(entry.id);
|
|
151
|
+
const dx = e.clientX - resizeStart.pointer.x;
|
|
152
|
+
const dy = e.clientY - resizeStart.pointer.y;
|
|
153
|
+
entry.size.w = Math.max(resizeStart.min.w, resizeStart.size.w + dx);
|
|
154
|
+
entry.size.h = Math.max(resizeStart.min.h, resizeStart.size.h + dy);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function onGripPointerUp(e: PointerEvent): void {
|
|
158
|
+
if (!resizing) return;
|
|
159
|
+
resizing = false;
|
|
160
|
+
const target = e.currentTarget as HTMLElement;
|
|
161
|
+
if (target.hasPointerCapture?.(e.pointerId)) {
|
|
83
162
|
target.releasePointerCapture(e.pointerId);
|
|
84
163
|
}
|
|
85
164
|
}
|
|
@@ -88,6 +167,11 @@
|
|
|
88
167
|
floatManager.focus(entry.id);
|
|
89
168
|
}
|
|
90
169
|
|
|
170
|
+
function onMaximize(e: MouseEvent): void {
|
|
171
|
+
e.stopPropagation();
|
|
172
|
+
floatManager.toggleMaximize(entry.id);
|
|
173
|
+
}
|
|
174
|
+
|
|
91
175
|
function onClose(e: MouseEvent): void {
|
|
92
176
|
e.stopPropagation();
|
|
93
177
|
floatManager.close(entry.id);
|
|
@@ -113,18 +197,40 @@
|
|
|
113
197
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
114
198
|
<header
|
|
115
199
|
class="sh3-float-header"
|
|
200
|
+
data-sh3-scope="element:float-header"
|
|
201
|
+
data-float-id={entry.id}
|
|
116
202
|
onpointerdown={onHeaderPointerDown}
|
|
117
203
|
onpointermove={onHeaderPointerMove}
|
|
118
204
|
onpointerup={onHeaderPointerUp}
|
|
119
205
|
onpointercancel={onHeaderPointerUp}
|
|
206
|
+
ondblclick={onHeaderDblClick}
|
|
207
|
+
oncontextmenu={openHeaderContextMenu}
|
|
120
208
|
>
|
|
121
209
|
<span class="sh3-float-title">{entry.title}</span>
|
|
122
|
-
<
|
|
210
|
+
<span class="sh3-float-header-actions">
|
|
211
|
+
<button
|
|
212
|
+
class="sh3-float-maximize"
|
|
213
|
+
onclick={onMaximize}
|
|
214
|
+
aria-label={isMaximized ? 'Restore float' : 'Maximize float'}
|
|
215
|
+
aria-pressed={isMaximized}
|
|
216
|
+
>{isMaximized ? '\u{1F5D7}' : '\u{1F5D6}'}</button>
|
|
217
|
+
<button class="sh3-float-close" onclick={onClose} aria-label="Close float">×</button>
|
|
218
|
+
</span>
|
|
123
219
|
</header>
|
|
124
220
|
{/if}
|
|
125
221
|
<div class="sh3-float-body">
|
|
126
222
|
<LayoutRenderer rootRef={{ kind: 'float', floatId: entry.id }} path={[]} />
|
|
127
223
|
</div>
|
|
224
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
225
|
+
<div
|
|
226
|
+
class="sh3-float-resize-grip"
|
|
227
|
+
role="presentation"
|
|
228
|
+
aria-hidden="true"
|
|
229
|
+
onpointerdown={onGripPointerDown}
|
|
230
|
+
onpointermove={onGripPointerMove}
|
|
231
|
+
onpointerup={onGripPointerUp}
|
|
232
|
+
onpointercancel={onGripPointerUp}
|
|
233
|
+
></div>
|
|
128
234
|
</div>
|
|
129
235
|
|
|
130
236
|
<style>
|
|
@@ -159,15 +265,26 @@
|
|
|
159
265
|
text-overflow: ellipsis;
|
|
160
266
|
white-space: nowrap;
|
|
161
267
|
}
|
|
268
|
+
.sh3-float-header-actions {
|
|
269
|
+
display: inline-flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
gap: 2px;
|
|
272
|
+
flex-shrink: 0;
|
|
273
|
+
}
|
|
274
|
+
.sh3-float-maximize,
|
|
162
275
|
.sh3-float-close {
|
|
163
276
|
background: transparent;
|
|
164
277
|
border: none;
|
|
165
278
|
color: var(--shell-fg);
|
|
166
|
-
font-size: 16px;
|
|
167
279
|
line-height: 1;
|
|
168
280
|
cursor: pointer;
|
|
169
281
|
padding: 0 4px;
|
|
170
|
-
|
|
282
|
+
}
|
|
283
|
+
.sh3-float-maximize {
|
|
284
|
+
font-size: 12px;
|
|
285
|
+
}
|
|
286
|
+
.sh3-float-close {
|
|
287
|
+
font-size: 16px;
|
|
171
288
|
}
|
|
172
289
|
.sh3-float-body {
|
|
173
290
|
flex: 1;
|
|
@@ -175,4 +292,28 @@
|
|
|
175
292
|
overflow: hidden;
|
|
176
293
|
min-height: 0;
|
|
177
294
|
}
|
|
295
|
+
.sh3-float-resize-grip {
|
|
296
|
+
position: absolute;
|
|
297
|
+
right: 0;
|
|
298
|
+
bottom: 0;
|
|
299
|
+
width: 16px;
|
|
300
|
+
height: 16px;
|
|
301
|
+
cursor: nwse-resize;
|
|
302
|
+
/* Subtle visual hint without being obtrusive — two diagonal lines made
|
|
303
|
+
from a CSS gradient stripe. */
|
|
304
|
+
background:
|
|
305
|
+
linear-gradient(
|
|
306
|
+
135deg,
|
|
307
|
+
transparent 0,
|
|
308
|
+
transparent 6px,
|
|
309
|
+
var(--shell-border-strong) 6px,
|
|
310
|
+
var(--shell-border-strong) 7px,
|
|
311
|
+
transparent 7px,
|
|
312
|
+
transparent 10px,
|
|
313
|
+
var(--shell-border-strong) 10px,
|
|
314
|
+
var(--shell-border-strong) 11px,
|
|
315
|
+
transparent 11px
|
|
316
|
+
);
|
|
317
|
+
border-bottom-right-radius: var(--shell-radius);
|
|
318
|
+
}
|
|
178
319
|
</style>
|
|
@@ -2,6 +2,6 @@ import type { FloatEntry } from '../layout/types';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
entry: FloatEntry;
|
|
4
4
|
}
|
|
5
|
-
declare const FloatFrame: import("svelte").Component<Props, {}, "">;
|
|
5
|
+
declare const FloatFrame: import("svelte").Component<Props, {}, "entry">;
|
|
6
6
|
type FloatFrame = ReturnType<typeof FloatFrame>;
|
|
7
7
|
export default FloatFrame;
|
package/dist/overlays/float.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FloatEntry } from '../layout/types';
|
|
1
|
+
import type { LayoutNode, FloatEntry } from '../layout/types';
|
|
2
2
|
import type { Size } from '../layout/floats';
|
|
3
3
|
export interface FloatOptions {
|
|
4
4
|
title?: string;
|
|
@@ -29,9 +29,46 @@ export interface FloatOptions {
|
|
|
29
29
|
}
|
|
30
30
|
export interface FloatManager {
|
|
31
31
|
open(viewId: string, options?: FloatOptions): string;
|
|
32
|
+
/**
|
|
33
|
+
* Open a float with a pre-built content tree. Used by save/restore flows
|
|
34
|
+
* (e.g. saved layouts) where the caller has already arranged the
|
|
35
|
+
* structure rather than starting from a single view id. The `content`
|
|
36
|
+
* tree is stored verbatim — the caller is responsible for cloning if it
|
|
37
|
+
* shouldn't be aliased to a source.
|
|
38
|
+
*/
|
|
39
|
+
openWithContent(options: {
|
|
40
|
+
content: LayoutNode;
|
|
41
|
+
size: Size;
|
|
42
|
+
title?: string;
|
|
43
|
+
position?: {
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
};
|
|
47
|
+
}): string;
|
|
32
48
|
close(floatId: string): void;
|
|
33
49
|
list(): FloatEntry[];
|
|
34
50
|
focus(floatId: string): void;
|
|
51
|
+
/**
|
|
52
|
+
* Snapshot the current rect, override with the float layer bounds, and
|
|
53
|
+
* raise. No-op if the float is already maximized or unknown. Bounds are
|
|
54
|
+
* frozen at maximize time — shell resize while maximized does not refit.
|
|
55
|
+
*/
|
|
56
|
+
maximize(floatId: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Roll the rect back to the snapshot taken at maximize time. No-op if
|
|
59
|
+
* the float was not maximized.
|
|
60
|
+
*/
|
|
61
|
+
restore(floatId: string): void;
|
|
62
|
+
/** `isMaximized(id) ? restore(id) : maximize(id)`. */
|
|
63
|
+
toggleMaximize(floatId: string): void;
|
|
64
|
+
/**
|
|
65
|
+
* Forget the saved rect snapshot without rolling back. Used by drag /
|
|
66
|
+
* resize handlers to "exit maximize but keep the current rect" — distinct
|
|
67
|
+
* from `restore`, which would snap back. Safe no-op when the float was
|
|
68
|
+
* not maximized.
|
|
69
|
+
*/
|
|
70
|
+
unmaximize(floatId: string): void;
|
|
71
|
+
isMaximized(floatId: string): boolean;
|
|
35
72
|
}
|
|
36
73
|
/**
|
|
37
74
|
* Bind the manager to the active LayoutTree's `floats` array. Called
|
package/dist/overlays/float.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { computeMinSize, cascadePosition, generateFloatId } from '../layout/floats';
|
|
30
30
|
import { findEnclosingOverlayHost } from './parentHost';
|
|
31
|
+
import { setMaximizedReactive, readMaximizedReactive, __resetMaximizedReactiveForTest, } from './floatMaximized.svelte';
|
|
31
32
|
// ----- storage binding ---------------------------------------------------
|
|
32
33
|
let fallbackFloats = [];
|
|
33
34
|
let boundFloats = null;
|
|
@@ -51,6 +52,8 @@ export function __resetFloatManagerForTest() {
|
|
|
51
52
|
boundFloats = null;
|
|
52
53
|
getTreeBounds = () => ({ w: 1600, h: 900 });
|
|
53
54
|
parentHosts.clear();
|
|
55
|
+
maximizedRects.clear();
|
|
56
|
+
__resetMaximizedReactiveForTest();
|
|
54
57
|
}
|
|
55
58
|
function activeStore() {
|
|
56
59
|
return boundFloats !== null && boundFloats !== void 0 ? boundFloats : fallbackFloats;
|
|
@@ -63,6 +66,12 @@ const parentHosts = new Map();
|
|
|
63
66
|
export function getFloatParentHost(id) {
|
|
64
67
|
return parentHosts.get(id);
|
|
65
68
|
}
|
|
69
|
+
// ----- maximize sidecar --------------------------------------------------
|
|
70
|
+
// Presence in this map ⇒ the float is currently maximized; the value is the
|
|
71
|
+
// rect to restore to. Lives outside FloatEntry so the layout schema doesn't
|
|
72
|
+
// need to bump for an in-memory, non-persisted concern. Reactivity is mirrored
|
|
73
|
+
// into floatMaximized.svelte.ts so Svelte components observe state changes.
|
|
74
|
+
const maximizedRects = new Map();
|
|
66
75
|
// ----- slot id minting ---------------------------------------------------
|
|
67
76
|
let floatSlotCounter = 0;
|
|
68
77
|
function mintFloatSlotId(viewId) {
|
|
@@ -128,6 +137,21 @@ function openFloat(viewId, options = {}) {
|
|
|
128
137
|
store.push(entry);
|
|
129
138
|
return id;
|
|
130
139
|
}
|
|
140
|
+
function openFloatWithContent(options) {
|
|
141
|
+
var _a;
|
|
142
|
+
const store = activeStore();
|
|
143
|
+
const id = generateFloatId();
|
|
144
|
+
const position = (_a = options.position) !== null && _a !== void 0 ? _a : cascadePosition(store, getTreeBounds());
|
|
145
|
+
const entry = {
|
|
146
|
+
id,
|
|
147
|
+
content: options.content,
|
|
148
|
+
position,
|
|
149
|
+
size: options.size,
|
|
150
|
+
title: options.title,
|
|
151
|
+
};
|
|
152
|
+
store.push(entry);
|
|
153
|
+
return id;
|
|
154
|
+
}
|
|
131
155
|
function closeFloat(floatId) {
|
|
132
156
|
const store = activeStore();
|
|
133
157
|
const idx = store.findIndex((f) => f.id === floatId);
|
|
@@ -135,6 +159,8 @@ function closeFloat(floatId) {
|
|
|
135
159
|
return;
|
|
136
160
|
store.splice(idx, 1);
|
|
137
161
|
parentHosts.delete(floatId);
|
|
162
|
+
maximizedRects.delete(floatId);
|
|
163
|
+
setMaximizedReactive(floatId, false);
|
|
138
164
|
}
|
|
139
165
|
function listFloats() {
|
|
140
166
|
// Return a snapshot so callers can iterate without racing mutations.
|
|
@@ -148,9 +174,65 @@ function focusFloat(floatId) {
|
|
|
148
174
|
const [entry] = store.splice(idx, 1);
|
|
149
175
|
store.push(entry);
|
|
150
176
|
}
|
|
177
|
+
function maximizeFloat(id) {
|
|
178
|
+
if (maximizedRects.has(id))
|
|
179
|
+
return;
|
|
180
|
+
const entry = activeStore().find((f) => f.id === id);
|
|
181
|
+
if (!entry)
|
|
182
|
+
return;
|
|
183
|
+
maximizedRects.set(id, {
|
|
184
|
+
position: { x: entry.position.x, y: entry.position.y },
|
|
185
|
+
size: { w: entry.size.w, h: entry.size.h },
|
|
186
|
+
});
|
|
187
|
+
setMaximizedReactive(id, true);
|
|
188
|
+
const bounds = getTreeBounds();
|
|
189
|
+
entry.position.x = 0;
|
|
190
|
+
entry.position.y = 0;
|
|
191
|
+
entry.size.w = bounds.w;
|
|
192
|
+
entry.size.h = bounds.h;
|
|
193
|
+
focusFloat(id);
|
|
194
|
+
}
|
|
195
|
+
function restoreFloat(id) {
|
|
196
|
+
const prev = maximizedRects.get(id);
|
|
197
|
+
if (!prev)
|
|
198
|
+
return;
|
|
199
|
+
const entry = activeStore().find((f) => f.id === id);
|
|
200
|
+
if (!entry) {
|
|
201
|
+
maximizedRects.delete(id);
|
|
202
|
+
setMaximizedReactive(id, false);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
entry.position.x = prev.position.x;
|
|
206
|
+
entry.position.y = prev.position.y;
|
|
207
|
+
entry.size.w = prev.size.w;
|
|
208
|
+
entry.size.h = prev.size.h;
|
|
209
|
+
maximizedRects.delete(id);
|
|
210
|
+
setMaximizedReactive(id, false);
|
|
211
|
+
}
|
|
212
|
+
function toggleMaximizeFloat(id) {
|
|
213
|
+
if (maximizedRects.has(id))
|
|
214
|
+
restoreFloat(id);
|
|
215
|
+
else
|
|
216
|
+
maximizeFloat(id);
|
|
217
|
+
}
|
|
218
|
+
function unmaximizeFloat(id) {
|
|
219
|
+
if (!maximizedRects.has(id))
|
|
220
|
+
return;
|
|
221
|
+
maximizedRects.delete(id);
|
|
222
|
+
setMaximizedReactive(id, false);
|
|
223
|
+
}
|
|
224
|
+
function isMaximizedFloat(id) {
|
|
225
|
+
return readMaximizedReactive(id);
|
|
226
|
+
}
|
|
151
227
|
export const floatManager = {
|
|
152
228
|
open: openFloat,
|
|
229
|
+
openWithContent: openFloatWithContent,
|
|
153
230
|
close: closeFloat,
|
|
154
231
|
list: listFloats,
|
|
155
232
|
focus: focusFloat,
|
|
233
|
+
maximize: maximizeFloat,
|
|
234
|
+
restore: restoreFloat,
|
|
235
|
+
toggleMaximize: toggleMaximizeFloat,
|
|
236
|
+
unmaximize: unmaximizeFloat,
|
|
237
|
+
isMaximized: isMaximizedFloat,
|
|
156
238
|
};
|