sh3-core 0.17.0 → 0.17.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/Sh3.svelte +48 -35
- 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/listActionsFromEntries.test.js +29 -0
- package/dist/actions/listActive.js +2 -0
- package/dist/actions/listeners.js +4 -0
- package/dist/actions/programmatic-dispatch.svelte.test.js +9 -2
- package/dist/actions/types.d.ts +8 -0
- package/dist/api.d.ts +4 -1
- package/dist/chrome/CompactChrome.svelte +96 -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 +67 -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/handheld.browser.test.d.ts +1 -0
- package/dist/handheld.browser.test.js +90 -0
- package/dist/layout/LayoutRenderer.svelte +12 -1
- package/dist/layout/LayoutRenderer.svelte.d.ts +2 -1
- 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 +76 -0
- package/dist/layout/compact/derive.d.ts +3 -0
- package/dist/layout/compact/derive.js +155 -0
- package/dist/layout/compact/derive.test.d.ts +1 -0
- package/dist/layout/compact/derive.test.js +160 -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/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 +27 -0
- package/dist/layout/compact/types.js +15 -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 +43 -1
- package/dist/layout/types.js +1 -1
- 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/OverlayRoots.svelte +12 -9
- package/dist/overlays/types.d.ts +1 -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/types.d.ts +9 -1
- package/dist/tokens.css +3 -2
- 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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* One drawer frame anchored to an edge. Renders nothing when closed.
|
|
4
|
+
* Open state slides the panel in over a backdrop. Tapping the backdrop
|
|
5
|
+
* fires onClose; tapping the close button does the same.
|
|
6
|
+
*
|
|
7
|
+
* Multi-slot drawers render a tab strip in the header. Single-slot
|
|
8
|
+
* drawers show the slot label only.
|
|
9
|
+
*
|
|
10
|
+
* Slot rendering goes through the standard SlotContainer path so the
|
|
11
|
+
* pooled host (and the mounted view) survives mount/unmount via the
|
|
12
|
+
* slot host pool — same mechanism as tab-drag re-parents.
|
|
13
|
+
*/
|
|
14
|
+
import type { DrawerAnchor, DrawerSpec } from '../layout/compact/types';
|
|
15
|
+
import type { SlotNode } from '../layout/types';
|
|
16
|
+
import SlotContainer from '../layout/SlotContainer.svelte';
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
anchor,
|
|
20
|
+
spec,
|
|
21
|
+
open,
|
|
22
|
+
activeSlotId,
|
|
23
|
+
onClose,
|
|
24
|
+
onActivate,
|
|
25
|
+
}: {
|
|
26
|
+
anchor: DrawerAnchor;
|
|
27
|
+
spec: DrawerSpec;
|
|
28
|
+
open: boolean;
|
|
29
|
+
activeSlotId: string | null;
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
onActivate: (slotId: string) => void;
|
|
32
|
+
} = $props();
|
|
33
|
+
|
|
34
|
+
const activeSlot = $derived(
|
|
35
|
+
spec.slots.find((s) => s.slotId === activeSlotId) ?? spec.slots[0],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const slotNode: SlotNode = $derived({
|
|
39
|
+
type: 'slot',
|
|
40
|
+
slotId: activeSlot.slotId,
|
|
41
|
+
viewId: activeSlot.viewId,
|
|
42
|
+
role: activeSlot.role,
|
|
43
|
+
});
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
{#if open}
|
|
47
|
+
<div
|
|
48
|
+
class="drawer-backdrop"
|
|
49
|
+
onclick={onClose}
|
|
50
|
+
onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}
|
|
51
|
+
role="presentation"
|
|
52
|
+
></div>
|
|
53
|
+
<div
|
|
54
|
+
class="drawer drawer-{anchor}"
|
|
55
|
+
data-sh3-region="drawer"
|
|
56
|
+
data-sh3-anchor={anchor}
|
|
57
|
+
>
|
|
58
|
+
<header>
|
|
59
|
+
<span class="title">{activeSlot.label}</span>
|
|
60
|
+
<button
|
|
61
|
+
class="close"
|
|
62
|
+
onclick={onClose}
|
|
63
|
+
aria-label="Close drawer"
|
|
64
|
+
>×</button>
|
|
65
|
+
</header>
|
|
66
|
+
{#if spec.slots.length > 1}
|
|
67
|
+
<div class="tab-strip" role="tablist">
|
|
68
|
+
{#each spec.slots as s (s.slotId)}
|
|
69
|
+
<button
|
|
70
|
+
role="tab"
|
|
71
|
+
aria-selected={s.slotId === activeSlot.slotId}
|
|
72
|
+
class:active={s.slotId === activeSlot.slotId}
|
|
73
|
+
onclick={() => onActivate(s.slotId)}
|
|
74
|
+
>
|
|
75
|
+
{s.label}
|
|
76
|
+
</button>
|
|
77
|
+
{/each}
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
80
|
+
<div class="body">
|
|
81
|
+
<SlotContainer node={slotNode} label={activeSlot.label} />
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
{/if}
|
|
85
|
+
|
|
86
|
+
<style>
|
|
87
|
+
.drawer-backdrop {
|
|
88
|
+
position: absolute;
|
|
89
|
+
inset: 0;
|
|
90
|
+
background: var(--sh3-overlay-backdrop, rgba(0, 0, 0, 0.35));
|
|
91
|
+
pointer-events: auto;
|
|
92
|
+
}
|
|
93
|
+
.drawer {
|
|
94
|
+
position: absolute;
|
|
95
|
+
background: var(--sh3-bg);
|
|
96
|
+
color: var(--sh3-fg);
|
|
97
|
+
box-shadow: var(--sh3-shadow-md, 0 0 16px rgba(0, 0, 0, 0.2));
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
border: 1px solid var(--sh3-border);
|
|
101
|
+
pointer-events: auto;
|
|
102
|
+
}
|
|
103
|
+
.drawer-left { top: 0; bottom: 0; left: 0; width: min(360px, 80vw); }
|
|
104
|
+
.drawer-right { top: 0; bottom: 0; right: 0; width: min(360px, 80vw); }
|
|
105
|
+
.drawer-top { left: 0; right: 0; top: 0; height: min(50vh, 360px); }
|
|
106
|
+
header {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
padding: var(--sh3-pad-sm) var(--sh3-pad-md);
|
|
110
|
+
gap: var(--sh3-pad-md);
|
|
111
|
+
border-bottom: 1px solid var(--sh3-border);
|
|
112
|
+
background: var(--sh3-bg-elevated);
|
|
113
|
+
}
|
|
114
|
+
.title { font-weight: 600; }
|
|
115
|
+
.close {
|
|
116
|
+
margin-left: auto;
|
|
117
|
+
background: none;
|
|
118
|
+
border: none;
|
|
119
|
+
font-size: var(--sh3-font-lg);
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
color: var(--sh3-fg-muted);
|
|
122
|
+
padding: 0 var(--sh3-pad-sm);
|
|
123
|
+
}
|
|
124
|
+
.close:hover { color: var(--sh3-fg); }
|
|
125
|
+
.tab-strip {
|
|
126
|
+
display: flex;
|
|
127
|
+
gap: 2px;
|
|
128
|
+
padding: var(--sh3-pad-xs) var(--sh3-pad-sm) 0;
|
|
129
|
+
background: var(--sh3-bg-sunken);
|
|
130
|
+
}
|
|
131
|
+
.tab-strip button {
|
|
132
|
+
padding: var(--sh3-pad-xs) var(--sh3-pad-sm);
|
|
133
|
+
border: 1px solid var(--sh3-border);
|
|
134
|
+
background: var(--sh3-bg-elevated);
|
|
135
|
+
border-bottom: none;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
color: var(--sh3-fg);
|
|
138
|
+
}
|
|
139
|
+
.tab-strip button.active { background: var(--sh3-bg); }
|
|
140
|
+
.body { flex: 1; min-height: 0; overflow: auto; }
|
|
141
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DrawerAnchor, DrawerSpec } from '../layout/compact/types';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
anchor: DrawerAnchor;
|
|
4
|
+
spec: DrawerSpec;
|
|
5
|
+
open: boolean;
|
|
6
|
+
activeSlotId: string | null;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
onActivate: (slotId: string) => void;
|
|
9
|
+
};
|
|
10
|
+
declare const DrawerSurface: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type DrawerSurface = ReturnType<typeof DrawerSurface>;
|
|
12
|
+
export default DrawerSurface;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* DOM smoke test for DrawerSurface — when open with a single-slot spec,
|
|
3
|
+
* the drawer renders the slot's label in its header. When closed, it
|
|
4
|
+
* renders nothing.
|
|
5
|
+
*
|
|
6
|
+
* Re-parent contract testing (slot DOM container actually moves, doesn't
|
|
7
|
+
* remount) is handled by the browser-mode handheld-flip test.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
10
|
+
import { mount, unmount, flushSync } from 'svelte';
|
|
11
|
+
import DrawerSurface from './DrawerSurface.svelte';
|
|
12
|
+
const DrawerSurfaceAny = DrawerSurface;
|
|
13
|
+
const spec = {
|
|
14
|
+
slots: [{ slotId: 'sb', viewId: 'v:sb', label: 'Files', role: 'sidebar' }],
|
|
15
|
+
};
|
|
16
|
+
const multiSpec = {
|
|
17
|
+
slots: [
|
|
18
|
+
{ slotId: 'sb', viewId: 'v:sb', label: 'Files', role: 'sidebar' },
|
|
19
|
+
{ slotId: 'pin', viewId: 'v:pin', label: 'Pinned', role: 'sidebar' },
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
let mounted = null;
|
|
23
|
+
let host = null;
|
|
24
|
+
function renderHost(props) {
|
|
25
|
+
host = document.createElement('div');
|
|
26
|
+
host.style.position = 'relative';
|
|
27
|
+
document.body.appendChild(host);
|
|
28
|
+
mounted = mount(DrawerSurfaceAny, { target: host, props });
|
|
29
|
+
flushSync();
|
|
30
|
+
return host;
|
|
31
|
+
}
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
if (mounted) {
|
|
34
|
+
unmount(mounted);
|
|
35
|
+
mounted = null;
|
|
36
|
+
}
|
|
37
|
+
if (host) {
|
|
38
|
+
host.remove();
|
|
39
|
+
host = null;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
describe('DrawerSurface (dom)', () => {
|
|
43
|
+
it('renders nothing when closed', () => {
|
|
44
|
+
const el = renderHost({
|
|
45
|
+
anchor: 'left', spec, open: false, activeSlotId: null,
|
|
46
|
+
onClose: () => { }, onActivate: () => { },
|
|
47
|
+
});
|
|
48
|
+
expect(el.querySelector('[data-sh3-region="drawer"]')).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
it('renders header with slot label when open', () => {
|
|
51
|
+
const el = renderHost({
|
|
52
|
+
anchor: 'left', spec, open: true, activeSlotId: 'sb',
|
|
53
|
+
onClose: () => { }, onActivate: () => { },
|
|
54
|
+
});
|
|
55
|
+
const header = el.querySelector('[data-sh3-region="drawer"] header');
|
|
56
|
+
expect(header === null || header === void 0 ? void 0 : header.textContent).toContain('Files');
|
|
57
|
+
});
|
|
58
|
+
it('multi-slot drawer renders a tab strip with one button per slot', () => {
|
|
59
|
+
const el = renderHost({
|
|
60
|
+
anchor: 'left', spec: multiSpec, open: true, activeSlotId: 'pin',
|
|
61
|
+
onClose: () => { }, onActivate: () => { },
|
|
62
|
+
});
|
|
63
|
+
const tabs = el.querySelectorAll('[data-sh3-region="drawer"] .tab-strip button');
|
|
64
|
+
expect(tabs.length).toBe(2);
|
|
65
|
+
expect(tabs[1].classList.contains('active')).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -23,13 +23,16 @@
|
|
|
23
23
|
|
|
24
24
|
// Layer metadata — order matches the stack in docs/design/layout.md.
|
|
25
25
|
// Index 0 here is layer 1 (floating panels); layer 0 is the content area.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
{ layer:
|
|
30
|
-
{ layer:
|
|
31
|
-
{ layer:
|
|
32
|
-
{ layer:
|
|
26
|
+
// The 'drawers' layer (compact-mode side panels) sits between docked (0)
|
|
27
|
+
// and floating (1); its z-index comes from --sh3-z-layer-drawers.
|
|
28
|
+
const overlayLayers: { layer: number | string; name: OverlayLayer; zToken: string }[] = [
|
|
29
|
+
{ layer: 'drawers', name: 'drawers', zToken: '--sh3-z-layer-drawers' },
|
|
30
|
+
{ layer: 1, name: 'floating', zToken: '--sh3-z-layer-1' },
|
|
31
|
+
{ layer: 2, name: 'drag-preview', zToken: '--sh3-z-layer-2' },
|
|
32
|
+
{ layer: 3, name: 'popup', zToken: '--sh3-z-layer-3' },
|
|
33
|
+
{ layer: 4, name: 'modal', zToken: '--sh3-z-layer-4' },
|
|
34
|
+
{ layer: 5, name: 'toast', zToken: '--sh3-z-layer-5' },
|
|
35
|
+
{ layer: 6, name: 'command', zToken: '--sh3-z-layer-6' },
|
|
33
36
|
];
|
|
34
37
|
|
|
35
38
|
const overlayRoots: Partial<Record<OverlayLayer, HTMLDivElement>> = $state({});
|
|
@@ -55,12 +58,12 @@
|
|
|
55
58
|
</script>
|
|
56
59
|
|
|
57
60
|
<div class="sh3-overlays" aria-hidden="true">
|
|
58
|
-
{#each overlayLayers as { layer, name } (layer)}
|
|
61
|
+
{#each overlayLayers as { layer, name, zToken } (layer)}
|
|
59
62
|
<div
|
|
60
63
|
class="sh3-overlay-root"
|
|
61
64
|
data-sh3-overlay={name}
|
|
62
65
|
data-sh3-layer={layer}
|
|
63
|
-
style="z-index: var(
|
|
66
|
+
style="z-index: var({zToken});"
|
|
64
67
|
bind:this={overlayRoots[name]}
|
|
65
68
|
>
|
|
66
69
|
{#if name === 'floating'}
|
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;
|
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
|
+
}
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { Sh3Api } from '../verbs/types';
|
|
|
8
8
|
import type { ShardContextKeys } from '../keys/types';
|
|
9
9
|
import type { ContributionsApi } from '../contributions/types';
|
|
10
10
|
import type { ActionsApi } from '../actions/types';
|
|
11
|
-
import type { TreeRootRef } from '../layout/types';
|
|
11
|
+
import type { TreeRootRef, SlotRole } from '../layout/types';
|
|
12
12
|
export { PERMISSION_KEYS_MINT, type ShardContextKeys, type ApiKeyPublic, type MintOpts, ScopeEscalationError, ConsentDeniedError } from '../keys/types';
|
|
13
13
|
/**
|
|
14
14
|
* The object returned by `ViewFactory.mount`. The framework calls
|
|
@@ -38,6 +38,14 @@ export interface ViewHandle {
|
|
|
38
38
|
closable?: boolean | {
|
|
39
39
|
canClose(): Promise<boolean>;
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* View-level slot-role default. The compact renderer reads this when
|
|
43
|
+
* the containing slot's `role` is unset; slot-level always wins.
|
|
44
|
+
*
|
|
45
|
+
* Lets a view declare "I'm a sidebar by nature" without forcing the
|
|
46
|
+
* app author to know. See `layout/compact/resolveRole.ts`.
|
|
47
|
+
*/
|
|
48
|
+
defaultRole?: SlotRole;
|
|
41
49
|
}
|
|
42
50
|
/**
|
|
43
51
|
* Context passed to `ViewFactory.mount` so the view knows which layout
|
package/dist/tokens.css
CHANGED
|
@@ -79,8 +79,9 @@
|
|
|
79
79
|
* source of truth for the layer stack. No component outside the overlay
|
|
80
80
|
* layer managers is permitted to write a z-index.
|
|
81
81
|
*/
|
|
82
|
-
--sh3-z-layer-0: 0; /* docked layout (content area)
|
|
83
|
-
--sh3-z-layer-
|
|
82
|
+
--sh3-z-layer-0: 0; /* docked layout (content area) */
|
|
83
|
+
--sh3-z-layer-drawers: 50; /* compact-mode drawer surfaces */
|
|
84
|
+
--sh3-z-layer-1: 100; /* floating panels (deferred) */
|
|
84
85
|
--sh3-z-layer-2: 200; /* drag preview */
|
|
85
86
|
--sh3-z-layer-3: 300; /* popups, context menus */
|
|
86
87
|
--sh3-z-layer-4: 400; /* modals */
|
package/dist/verbs/types.d.ts
CHANGED
|
@@ -128,11 +128,14 @@ export interface Sh3Api {
|
|
|
128
128
|
scrollback: ScrollbackEntry[];
|
|
129
129
|
}>;
|
|
130
130
|
/**
|
|
131
|
-
* Read-only snapshot of every action registered across every shard.
|
|
132
|
-
*
|
|
131
|
+
* Read-only snapshot of every action registered across every shard.
|
|
132
|
+
* - `activeOnly`: filter to currently-dispatchable actions.
|
|
133
|
+
* - `submenuOf`: restrict to children of the named parent action id
|
|
134
|
+
* (mirrors the palette sub-drill filter).
|
|
133
135
|
*/
|
|
134
136
|
listActions(opts?: {
|
|
135
137
|
activeOnly?: boolean;
|
|
138
|
+
submenuOf?: string;
|
|
136
139
|
}): ActionDescriptor[];
|
|
137
140
|
/**
|
|
138
141
|
* Programmatically dispatch a registered action by id. Same semantics as
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export declare const VERSION = "0.17.
|
|
2
|
+
export declare const VERSION = "0.17.2";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Auto-generated from package.json — do not edit manually. */
|
|
2
|
-
export const VERSION = '0.17.
|
|
2
|
+
export const VERSION = '0.17.2';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* classify — derives the viewport class from multi-signal input.
|
|
3
|
+
*
|
|
4
|
+
* Why multi-signal: a 6.7" phone reports ~393 CSS px wide *with* coarse
|
|
5
|
+
* pointer + high DPR; a narrow desktop window reports the same width
|
|
6
|
+
* *with* fine pointer + DPR 1-2. CSS pixels alone undercount physical
|
|
7
|
+
* compactness on high-DPI mobile, so width is one signal among three.
|
|
8
|
+
*
|
|
9
|
+
* The thresholds (720, 1100) are not load-bearing — they're tunable in
|
|
10
|
+
* a single place. Adjust with a corresponding test row.
|
|
11
|
+
*/
|
|
12
|
+
export function classify(i) {
|
|
13
|
+
if (i.coarsePointer && i.noHover)
|
|
14
|
+
return 'compact';
|
|
15
|
+
if (i.width < 720)
|
|
16
|
+
return 'compact';
|
|
17
|
+
if (i.coarsePointer && i.dpr >= 2 && i.width < 1100)
|
|
18
|
+
return 'compact';
|
|
19
|
+
return 'desktop';
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Table-driven tests for the viewport classifier. Each row is a real
|
|
3
|
+
* device (or a deliberately-chosen edge case) with the signal tuple
|
|
4
|
+
* expected from a real browser/matchMedia in that device's typical
|
|
5
|
+
* orientation.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { classify } from './classify';
|
|
9
|
+
const ROWS = [
|
|
10
|
+
{ name: 'phone portrait', width: 393, coarsePointer: true, noHover: true, dpr: 3, expected: 'compact' },
|
|
11
|
+
{ name: 'phone landscape', width: 852, coarsePointer: true, noHover: true, dpr: 3, expected: 'compact' },
|
|
12
|
+
{ name: 'tablet portrait', width: 768, coarsePointer: true, noHover: true, dpr: 2, expected: 'compact' },
|
|
13
|
+
{ name: 'tablet landscape', width: 1024, coarsePointer: true, noHover: true, dpr: 2, expected: 'compact' },
|
|
14
|
+
{ name: 'desktop wide', width: 1920, coarsePointer: false, noHover: false, dpr: 1, expected: 'desktop' },
|
|
15
|
+
{ name: 'desktop narrow window', width: 700, coarsePointer: false, noHover: false, dpr: 1, expected: 'compact' },
|
|
16
|
+
{ name: 'iPad with mouse attached', width: 1024, coarsePointer: false, noHover: false, dpr: 2, expected: 'desktop' },
|
|
17
|
+
{ name: 'small laptop', width: 1366, coarsePointer: false, noHover: false, dpr: 1, expected: 'desktop' },
|
|
18
|
+
{ name: 'phone with stylus (hover ok)', width: 412, coarsePointer: true, noHover: false, dpr: 3, expected: 'compact' },
|
|
19
|
+
];
|
|
20
|
+
describe('classify', () => {
|
|
21
|
+
for (const row of ROWS) {
|
|
22
|
+
it(`${row.name} → ${row.expected}`, () => {
|
|
23
|
+
const result = classify({
|
|
24
|
+
width: row.width,
|
|
25
|
+
coarsePointer: row.coarsePointer,
|
|
26
|
+
noHover: row.noHover,
|
|
27
|
+
dpr: row.dpr,
|
|
28
|
+
});
|
|
29
|
+
expect(result).toBe(row.expected);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Real-signal viewport classification — boots the viewport store under
|
|
3
|
+
* Chromium and asserts initial class + override behavior. happy-dom's
|
|
4
|
+
* matchMedia stub is shallow, so this is the contract pin against a
|
|
5
|
+
* real browser engine.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
8
|
+
import { viewportStore } from './store.svelte';
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
viewportStore.__reset();
|
|
11
|
+
});
|
|
12
|
+
describe('viewport store under real browser', () => {
|
|
13
|
+
it('returns a viewport class consistent with the test runner window', () => {
|
|
14
|
+
const info = viewportStore.current;
|
|
15
|
+
expect(info.class).toMatch(/desktop|compact/);
|
|
16
|
+
expect(info.width).toBeGreaterThan(0);
|
|
17
|
+
expect(info.height).toBeGreaterThan(0);
|
|
18
|
+
});
|
|
19
|
+
it('override(compact) immediately flips class', () => {
|
|
20
|
+
const fires = [];
|
|
21
|
+
const unsub = viewportStore.subscribe((i) => fires.push(i.class));
|
|
22
|
+
viewportStore.override('compact');
|
|
23
|
+
expect(viewportStore.current.class).toBe('compact');
|
|
24
|
+
expect(fires).toContain('compact');
|
|
25
|
+
unsub();
|
|
26
|
+
});
|
|
27
|
+
it('override(null) restores auto-derived class', () => {
|
|
28
|
+
viewportStore.override('compact');
|
|
29
|
+
expect(viewportStore.pinned).toBe('compact');
|
|
30
|
+
viewportStore.override(null);
|
|
31
|
+
expect(viewportStore.pinned).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ViewportClass, ViewportInfo } from './types';
|
|
2
|
+
export declare const viewportStore: {
|
|
3
|
+
readonly current: ViewportInfo;
|
|
4
|
+
subscribe(cb: (i: ViewportInfo) => void): () => void;
|
|
5
|
+
override(cls: ViewportClass | null): void;
|
|
6
|
+
readonly pinned: ViewportClass | null;
|
|
7
|
+
/** Test-only reset hook. Not exported from index.ts. */
|
|
8
|
+
__reset(): void;
|
|
9
|
+
};
|