vintage-frames 0.13.0 → 0.14.1
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/README.md +16 -0
- package/custom-elements.json +184 -20
- package/dist/components/vf-checkbox.d.ts +6 -5
- package/dist/components/vf-checkbox.js +41 -37
- package/dist/components/vf-container.d.ts +14 -14
- package/dist/components/vf-container.js +58 -28
- package/dist/components/vf-desktop.d.ts +114 -12
- package/dist/components/vf-desktop.js +119 -58
- package/dist/components/vf-fieldset.js +30 -22
- package/dist/components/vf-icon-field.d.ts +1 -1
- package/dist/components/vf-icon.js +111 -104
- package/dist/components/vf-list.js +30 -29
- package/dist/components/vf-menu-bar.d.ts +5 -0
- package/dist/components/vf-menu-bar.js +67 -43
- package/dist/components/vf-menu.js +24 -22
- package/dist/components/vf-number-field.js +32 -30
- package/dist/components/vf-progress-bar.js +28 -29
- package/dist/components/vf-radio.js +3 -4
- package/dist/components/vf-scroll-area.js +27 -26
- package/dist/components/vf-select.js +108 -90
- package/dist/components/vf-separator.d.ts +5 -5
- package/dist/components/vf-separator.js +36 -10
- package/dist/components/vf-swatch.js +47 -45
- package/dist/components/vf-text-area.js +37 -35
- package/dist/components/vf-text-field.js +20 -18
- package/dist/components/vf-window.d.ts +13 -0
- package/dist/components/vf-window.js +101 -72
- package/dist/index.d.ts +12 -1
- package/dist/index.js +61 -59
- package/dist/pattern-fill.d.ts +11 -8
- package/dist/pattern-fill.js +2 -4
- package/dist/position.d.ts +25 -7
- package/dist/position.js +6 -3
- package/dist/shell/alert.d.ts +32 -0
- package/dist/shell/alert.js +44 -0
- package/dist/shell/catalog.d.ts +168 -0
- package/dist/shell/catalog.js +278 -0
- package/dist/shell/clock.d.ts +16 -0
- package/dist/shell/clock.js +36 -0
- package/dist/shell/filing.d.ts +39 -0
- package/dist/shell/filing.js +68 -0
- package/dist/shell/finder.d.ts +116 -0
- package/dist/shell/finder.js +564 -0
- package/dist/shell/geometry.d.ts +196 -0
- package/dist/shell/geometry.js +241 -0
- package/dist/shell/index.d.ts +29 -0
- package/dist/shell/index.js +11 -0
- package/dist/shell/shell.d.ts +150 -0
- package/dist/shell/shell.js +213 -0
- package/dist/shell/state.d.ts +88 -0
- package/dist/shell/state.js +103 -0
- package/dist/shell/storage.d.ts +14 -0
- package/dist/shell/storage.js +41 -0
- package/dist/shell/windows.d.ts +215 -0
- package/dist/shell/windows.js +343 -0
- package/dist/styles/base.d.ts +3 -1
- package/dist/styles/recipes/field.d.ts +10 -6
- package/dist/styles/recipes/field.js +8 -7
- package/dist/styles/recipes/focus.d.ts +7 -3
- package/dist/styles/recipes/rule.d.ts +11 -8
- package/dist/styles/recipes/rule.js +31 -13
- package/dist/styles/recipes/scroll-rail.d.ts +7 -6
- package/dist/styles/recipes/scroll-rail.js +18 -18
- package/dist/styles/recipes/shadow.d.ts +10 -6
- package/dist/styles/recipes/shadow.js +4 -2
- package/dist/styles/recipes/stroke.d.ts +85 -0
- package/dist/styles/recipes/stroke.js +53 -0
- package/dist/styles/recipes/surface.d.ts +8 -4
- package/dist/styles/recipes/surface.js +23 -17
- package/dist/styles/recipes/title-bar.d.ts +7 -0
- package/dist/styles/recipes/title-bar.js +25 -22
- package/dist/styles/recipes/toggle.js +3 -3
- package/dist/window-chrome.d.ts +43 -0
- package/dist/window-chrome.js +13 -0
- package/dist/zoom.d.ts +9 -9
- package/docs/SPEC.md +34 -32
- package/editor/vscode.html-custom-data.json +12 -7
- package/editor/web-types.json +50 -14
- package/package.json +10 -2
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { onScaleChange as e } from "../scale.js";
|
|
2
|
+
import { VfWindow as t } from "../components/vf-window.js";
|
|
3
|
+
import "../index.js";
|
|
4
|
+
import { createCatalog as n } from "./catalog.js";
|
|
5
|
+
import { createWindowManager as r, deepActiveElement as i } from "./windows.js";
|
|
6
|
+
import { openWindowsOf as a } from "./state.js";
|
|
7
|
+
import { startClock as o } from "./clock.js";
|
|
8
|
+
import { showAlert as s } from "./alert.js";
|
|
9
|
+
//#region src/shell/shell.ts
|
|
10
|
+
function c(e) {
|
|
11
|
+
return e;
|
|
12
|
+
}
|
|
13
|
+
var l = "app", u = (e) => e instanceof HTMLInputElement || e instanceof HTMLTextAreaElement || e?.isContentEditable === !0;
|
|
14
|
+
function d(c, l) {
|
|
15
|
+
let d = l.apps, p = d.find((e) => e.catalog), m = l.defaultApp ?? p?.id ?? d[0]?.id ?? "", h = l.state ?? null, g = l.services ?? {}, _ = [], v = c.querySelector(":scope > vf-menu-bar");
|
|
16
|
+
if (!v) throw Error("vintage-frames/shell: the desktop has no vf-menu-bar");
|
|
17
|
+
let y = v.querySelector(":scope > vf-menu");
|
|
18
|
+
if (!y) throw Error("vintage-frames/shell: the menu bar has no system menu");
|
|
19
|
+
v.shortcuts = !0;
|
|
20
|
+
let b = h?.saved?.pattern;
|
|
21
|
+
b && (c.pattern = b);
|
|
22
|
+
let x = null;
|
|
23
|
+
if (l.fit === "viewport") {
|
|
24
|
+
let t = () => {
|
|
25
|
+
let e = {
|
|
26
|
+
width: c.width,
|
|
27
|
+
height: c.height
|
|
28
|
+
};
|
|
29
|
+
c.fitWithin(document.documentElement.clientWidth, document.documentElement.clientHeight), x?.resized(e);
|
|
30
|
+
};
|
|
31
|
+
t(), window.addEventListener("resize", t);
|
|
32
|
+
let n = e(t);
|
|
33
|
+
_.push(() => {
|
|
34
|
+
window.removeEventListener("resize", t), n();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
let S = r(c, {
|
|
38
|
+
defaultApp: m,
|
|
39
|
+
savedPin: (e) => h?.pin(e) ?? null
|
|
40
|
+
});
|
|
41
|
+
x = S;
|
|
42
|
+
let C = (e) => {
|
|
43
|
+
e.target === c && c.clearActive();
|
|
44
|
+
};
|
|
45
|
+
c.addEventListener("pointerdown", C), _.push(() => c.removeEventListener("pointerdown", C));
|
|
46
|
+
let w = {}, T = /* @__PURE__ */ new Map();
|
|
47
|
+
T.set("app", {
|
|
48
|
+
art: (e) => d.find((t) => t.id === f(e))?.icon ?? "",
|
|
49
|
+
open: (e, t) => void w[f(e) ?? ""]?.open?.({ from: t }),
|
|
50
|
+
editable: !1
|
|
51
|
+
});
|
|
52
|
+
for (let e of d) for (let [t, n] of Object.entries(e.kinds ?? {})) T.set(t, n);
|
|
53
|
+
let E = p?.catalog ? n({
|
|
54
|
+
storage: p.catalog.storage,
|
|
55
|
+
volumes: p.catalog.volumes,
|
|
56
|
+
listed: (e) => T.has(e),
|
|
57
|
+
hooks: (e) => T.get(e)
|
|
58
|
+
}) : null, D = () => !!document.querySelector("vf-dialog[open]"), O = () => u(i()), k = /* @__PURE__ */ new Map(), A = () => {
|
|
59
|
+
for (let [e, t] of k) {
|
|
60
|
+
let n = !t();
|
|
61
|
+
e.disabled !== n && (e.disabled = n);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
document.addEventListener("pointerdown", A, !0), document.addEventListener("keydown", A, !0), c.addEventListener("vf-select", A), c.addEventListener("vf-activate", A), _.push(() => document.removeEventListener("pointerdown", A, !0), () => document.removeEventListener("keydown", A, !0), () => c.removeEventListener("vf-select", A), () => c.removeEventListener("vf-activate", A), S.onWindows(A), S.onLayout(A)), E && _.push(E.subscribe(A));
|
|
65
|
+
let j = (e, t = {}) => s(c, e, {
|
|
66
|
+
art: l.caution ?? null,
|
|
67
|
+
...t
|
|
68
|
+
}), M = (e) => {
|
|
69
|
+
if (!e) return [];
|
|
70
|
+
let t = document.createElement("div");
|
|
71
|
+
t.innerHTML = e, customElements.upgrade(t);
|
|
72
|
+
let n = [...t.children].filter((e) => e.localName === "vf-menu");
|
|
73
|
+
for (let e of n) e.remove();
|
|
74
|
+
return n;
|
|
75
|
+
}, N = d.map((e) => ({
|
|
76
|
+
def: e,
|
|
77
|
+
menus: M(e.menus)
|
|
78
|
+
})), P = [];
|
|
79
|
+
for (let { def: e, menus: t } of N) {
|
|
80
|
+
let n = [], r = `vintage-frames/shell (${e.id})`, i = {
|
|
81
|
+
desktop: c,
|
|
82
|
+
windows: S,
|
|
83
|
+
menus: t,
|
|
84
|
+
systemMenu: y,
|
|
85
|
+
apps: w,
|
|
86
|
+
catalog: E,
|
|
87
|
+
services: g,
|
|
88
|
+
state: h,
|
|
89
|
+
kinds: T,
|
|
90
|
+
kind: (e) => T.get(e),
|
|
91
|
+
app: (e) => d.find((t) => t.id === e),
|
|
92
|
+
menu(e) {
|
|
93
|
+
let n = t.find((t) => t.dataset.menu === e);
|
|
94
|
+
if (!n) throw Error(`${r}: missing menu ${e}`);
|
|
95
|
+
return n;
|
|
96
|
+
},
|
|
97
|
+
item(e) {
|
|
98
|
+
for (let n of t) {
|
|
99
|
+
let t = n.querySelector(`vf-menu-item[value="${CSS.escape(e)}"]`);
|
|
100
|
+
if (t) return t;
|
|
101
|
+
}
|
|
102
|
+
throw Error(`${r}: missing menu item ${e}`);
|
|
103
|
+
},
|
|
104
|
+
onMenu(e) {
|
|
105
|
+
for (let n of t) i.on(n, "vf-menu-select", (t) => {
|
|
106
|
+
if (D()) return;
|
|
107
|
+
let { value: n, item: r } = t.detail;
|
|
108
|
+
e(n, r);
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
systemItem(e, t, r) {
|
|
112
|
+
let a = document.createElement("vf-menu-item");
|
|
113
|
+
return a.setAttribute("value", e), a.textContent = t, y.append(a), i.on(y, "vf-menu-select", (t) => {
|
|
114
|
+
!D() && t.detail.value === e && r();
|
|
115
|
+
}), n.push(() => a.remove()), a;
|
|
116
|
+
},
|
|
117
|
+
alert: j,
|
|
118
|
+
gate(e, t) {
|
|
119
|
+
k.set(e, t), n.push(() => k.delete(e));
|
|
120
|
+
let r = !t();
|
|
121
|
+
e.disabled !== r && (e.disabled = r);
|
|
122
|
+
},
|
|
123
|
+
typing: O,
|
|
124
|
+
modalOpen: D,
|
|
125
|
+
onFront(t) {
|
|
126
|
+
t(S.front === e.id), n.push(S.onFront((n) => t(n === e.id)));
|
|
127
|
+
},
|
|
128
|
+
on(e, t, r, i) {
|
|
129
|
+
e.addEventListener(t, r, i), n.push(() => e.removeEventListener(t, r, i));
|
|
130
|
+
},
|
|
131
|
+
onDispose(e) {
|
|
132
|
+
n.push(e);
|
|
133
|
+
}
|
|
134
|
+
}, a = e.init(i);
|
|
135
|
+
a && (w[e.id] = a), P.push(() => {
|
|
136
|
+
for (let e of n.splice(0).reverse()) e();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
let F = null, I = () => {
|
|
140
|
+
let e = N.find((e) => e.def.id === S.front) ?? N.find((e) => e.def.id === m) ?? null;
|
|
141
|
+
if (!(!e || e === F)) {
|
|
142
|
+
if (F) for (let e of F.menus) e.remove();
|
|
143
|
+
y.after(...e.menus), v.label = e.def.name, F = e, A();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
if (_.push(S.onFront(I)), I(), l.clock !== !1) {
|
|
147
|
+
let e = v.querySelector(":scope > [slot=\"end\"]"), t = !e;
|
|
148
|
+
e || (e = document.createElement("vf-label"), e.slot = "end", v.append(e));
|
|
149
|
+
let n = o(e);
|
|
150
|
+
_.push(() => {
|
|
151
|
+
n(), t && e.remove();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
let L = /* @__PURE__ */ new WeakSet(), R = () => {
|
|
155
|
+
if (E) for (let e of c.stackingOrder) {
|
|
156
|
+
if (!(e instanceof t)) continue;
|
|
157
|
+
let n = S.itemOf(e);
|
|
158
|
+
if (n == null) continue;
|
|
159
|
+
let r = E.item(n);
|
|
160
|
+
r ? (L.add(e), e.heading !== r.name && (e.heading = r.name)) : L.has(e) && E.get().available && S.close(e, { to: null });
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
return E && _.push(E.subscribe(R), S.onWindows(R)), h?.hold(), {
|
|
164
|
+
desktop: c,
|
|
165
|
+
windows: S,
|
|
166
|
+
catalog: E,
|
|
167
|
+
apps: w,
|
|
168
|
+
ready: (async () => {
|
|
169
|
+
try {
|
|
170
|
+
E && (await E.refresh(), p?.catalog?.seed && await E.seed(p.catalog.seed));
|
|
171
|
+
for (let { item: e, app: t } of a(h?.saved ?? null)) await w[t]?.open?.({ item: e });
|
|
172
|
+
let e = h?.saved?.active;
|
|
173
|
+
if (e) {
|
|
174
|
+
let t = S.windowFor(e);
|
|
175
|
+
t && c.bringToFront(t);
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
if (h) {
|
|
179
|
+
let e = h.start(() => ({
|
|
180
|
+
windows: S.snapshot(),
|
|
181
|
+
active: S.itemOf(c.activeWindow),
|
|
182
|
+
pattern: c.pattern ?? null
|
|
183
|
+
}), (e) => {
|
|
184
|
+
let t = [
|
|
185
|
+
S.onLayout(e),
|
|
186
|
+
S.onWindows(e),
|
|
187
|
+
...E ? [E.subscribe(e)] : []
|
|
188
|
+
];
|
|
189
|
+
return c.addEventListener("vf-activate", e), () => {
|
|
190
|
+
for (let e of t) e();
|
|
191
|
+
c.removeEventListener("vf-activate", e);
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
_.push(e), h.release();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
})(),
|
|
198
|
+
alert: j,
|
|
199
|
+
dispose() {
|
|
200
|
+
for (let e of P.splice(0).reverse()) e();
|
|
201
|
+
if (F) for (let e of F.menus) e.remove();
|
|
202
|
+
F = null;
|
|
203
|
+
for (let e of _.splice(0)) e();
|
|
204
|
+
S.dispose(), E?.dispose();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function f(e) {
|
|
209
|
+
let t = e.data;
|
|
210
|
+
return typeof t?.app == "string" ? t.app : null;
|
|
211
|
+
}
|
|
212
|
+
//#endregion
|
|
213
|
+
export { l as APP_KIND, f as appIdOf, d as createShell, c as defineApp };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The saved session: one versioned localStorage key holding each window's
|
|
3
|
+
* box as a nine-slice pin and its depth, the active window's item, the
|
|
4
|
+
* desktop pattern, and the site's own keys.
|
|
5
|
+
*
|
|
6
|
+
* - A window entry with a depth (`z`) was open at the write. One without is a
|
|
7
|
+
* box alone — a palette, or a window closed this session — which a reload
|
|
8
|
+
* never reopens and an open() still lands in.
|
|
9
|
+
* - Writes wait a moment and are held until the boot has reopened the
|
|
10
|
+
* session: a write while windows are still reopening would drop the depth
|
|
11
|
+
* of the ones not reached yet. Hiding or leaving the page writes at once.
|
|
12
|
+
* - `?fresh=1` neither reads nor writes.
|
|
13
|
+
*
|
|
14
|
+
* The parsing is pure, so it is tested under Node; the storage is the page's
|
|
15
|
+
* localStorage, reached only when a state is made.
|
|
16
|
+
*/
|
|
17
|
+
import type { Pin } from './geometry.js';
|
|
18
|
+
/** One saved window. */
|
|
19
|
+
export interface SavedWindow {
|
|
20
|
+
/** The application that owns it, which reopens it. */
|
|
21
|
+
app: string;
|
|
22
|
+
pin: Pin;
|
|
23
|
+
/** Its depth when saved, 0 the bottom; unset for a box alone. */
|
|
24
|
+
z?: number;
|
|
25
|
+
}
|
|
26
|
+
/** What the session holds. */
|
|
27
|
+
export interface SavedSession {
|
|
28
|
+
v: number;
|
|
29
|
+
/** Windows by the item they show. */
|
|
30
|
+
windows: Record<string, SavedWindow>;
|
|
31
|
+
/** The active window's item, or null. */
|
|
32
|
+
active: string | null;
|
|
33
|
+
/** The desktop pattern, or null for the desktop's own. */
|
|
34
|
+
pattern: string | null;
|
|
35
|
+
/** The site's own keys. */
|
|
36
|
+
extra: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A parsed blob as a session, its bad entries dropped, or null for anything
|
|
40
|
+
* that isn't one of this version.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readSession(parsed: unknown): SavedSession | null;
|
|
43
|
+
/** The windows that were open, deepest first: the order a boot reopens them in. */
|
|
44
|
+
export declare function openWindowsOf(session: SavedSession | null): {
|
|
45
|
+
item: string;
|
|
46
|
+
app: string;
|
|
47
|
+
pin: Pin;
|
|
48
|
+
}[];
|
|
49
|
+
/** What the shell reports at a write: the windows it knows and the active one. */
|
|
50
|
+
export interface SessionSnapshot {
|
|
51
|
+
windows: Record<string, SavedWindow>;
|
|
52
|
+
active: string | null;
|
|
53
|
+
pattern: string | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The session to write: every window known before keeps its box and loses
|
|
57
|
+
* its depth, and the snapshot's windows — the open ones with theirs — go
|
|
58
|
+
* over them.
|
|
59
|
+
*/
|
|
60
|
+
export declare function mergeSession(known: Record<string, SavedWindow>, snapshot: SessionSnapshot, extra: Record<string, unknown>): SavedSession;
|
|
61
|
+
/** A saved session the shell reads at boot and writes as the desktop changes. */
|
|
62
|
+
export interface ShellState {
|
|
63
|
+
/** The session read at boot, or null. */
|
|
64
|
+
readonly saved: SavedSession | null;
|
|
65
|
+
/** A saved window's pin, by item, or null. */
|
|
66
|
+
pin(item: string): Pin | null;
|
|
67
|
+
/** A site key's saved value, else its default. */
|
|
68
|
+
get<T = unknown>(key: string): T;
|
|
69
|
+
/** Set a site key and write at once, so an immediate reload still finds it. */
|
|
70
|
+
set(key: string, value: unknown): void;
|
|
71
|
+
/** Hold every write until {@link release}. */
|
|
72
|
+
hold(): void;
|
|
73
|
+
/** Release the hold and write what is on screen. */
|
|
74
|
+
release(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Start writing: `snapshot` is read at each write, after `subscribe`'s
|
|
77
|
+
* signal settles. Returns the stop function.
|
|
78
|
+
*/
|
|
79
|
+
start(snapshot: () => SessionSnapshot, subscribe: (fn: () => void) => () => void): () => void;
|
|
80
|
+
}
|
|
81
|
+
export interface LocalStorageStateOptions {
|
|
82
|
+
/** The site's own keys and their defaults: `{ greet: true, seeded: false }`. */
|
|
83
|
+
extra?: Record<string, unknown>;
|
|
84
|
+
/** Neither read nor write. Default: `?fresh=1` in the address. */
|
|
85
|
+
fresh?: boolean;
|
|
86
|
+
}
|
|
87
|
+
/** A session kept under one localStorage `key`. */
|
|
88
|
+
export declare function localStorageState(key: string, options?: LocalStorageStateOptions): ShellState;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { isPin as e } from "./geometry.js";
|
|
2
|
+
//#region src/shell/state.ts
|
|
3
|
+
var t = 1, n = 400;
|
|
4
|
+
function r(t) {
|
|
5
|
+
let n = t;
|
|
6
|
+
return !n || typeof n != "object" || typeof n.app != "string" || !n.app || !e(n.pin) ? null : Number.isInteger(n.z) ? {
|
|
7
|
+
app: n.app,
|
|
8
|
+
pin: n.pin,
|
|
9
|
+
z: n.z
|
|
10
|
+
} : {
|
|
11
|
+
app: n.app,
|
|
12
|
+
pin: n.pin
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function i(e) {
|
|
16
|
+
let n = e;
|
|
17
|
+
if (!n || typeof n != "object" || n.v !== t) return null;
|
|
18
|
+
let i = {};
|
|
19
|
+
if (n.windows && typeof n.windows == "object") for (let [e, t] of Object.entries(n.windows)) {
|
|
20
|
+
let n = r(t);
|
|
21
|
+
n && (i[e] = n);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
v: t,
|
|
25
|
+
windows: i,
|
|
26
|
+
active: typeof n.active == "string" && n.active ? n.active : null,
|
|
27
|
+
pattern: typeof n.pattern == "string" && n.pattern.trim() ? n.pattern : null,
|
|
28
|
+
extra: n.extra && typeof n.extra == "object" && !Array.isArray(n.extra) ? { ...n.extra } : {}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function a(e) {
|
|
32
|
+
return Object.entries(e?.windows ?? {}).filter(([, e]) => e.z != null).sort(([, e], [, t]) => e.z - t.z).map(([e, t]) => ({
|
|
33
|
+
item: e,
|
|
34
|
+
app: t.app,
|
|
35
|
+
pin: t.pin
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
function o(e, n, i) {
|
|
39
|
+
let a = {};
|
|
40
|
+
for (let [t, n] of Object.entries(e)) a[t] = {
|
|
41
|
+
app: n.app,
|
|
42
|
+
pin: n.pin
|
|
43
|
+
};
|
|
44
|
+
for (let [e, t] of Object.entries(n.windows)) {
|
|
45
|
+
let n = r(t);
|
|
46
|
+
n && (a[e] = n);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
v: t,
|
|
50
|
+
windows: a,
|
|
51
|
+
active: n.active,
|
|
52
|
+
pattern: n.pattern,
|
|
53
|
+
extra: { ...i }
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function s(e, t = {}) {
|
|
57
|
+
let r = t.fresh ?? new URLSearchParams(location.search).get("fresh") === "1", i = t.extra ?? {}, a = r ? null : c(e), s = {
|
|
58
|
+
...i,
|
|
59
|
+
...a?.extra ?? {}
|
|
60
|
+
}, l = { ...a?.windows ?? {} }, u = !1, d = null, f = () => {
|
|
61
|
+
if (r || u || !d) return;
|
|
62
|
+
let t = o(l, d(), s);
|
|
63
|
+
l = t.windows;
|
|
64
|
+
try {
|
|
65
|
+
localStorage.setItem(e, JSON.stringify(t));
|
|
66
|
+
} catch {}
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
saved: a,
|
|
70
|
+
pin: (e) => a?.windows[e]?.pin ?? null,
|
|
71
|
+
get: (e) => s[e],
|
|
72
|
+
set(e, t) {
|
|
73
|
+
s[e] = t, f();
|
|
74
|
+
},
|
|
75
|
+
hold() {
|
|
76
|
+
u = !0;
|
|
77
|
+
},
|
|
78
|
+
release() {
|
|
79
|
+
u = !1, f();
|
|
80
|
+
},
|
|
81
|
+
start(e, t) {
|
|
82
|
+
if (r) return () => {};
|
|
83
|
+
d = e;
|
|
84
|
+
let i = 0, a = () => {
|
|
85
|
+
clearTimeout(i), i = window.setTimeout(f, n);
|
|
86
|
+
}, o = () => {
|
|
87
|
+
document.visibilityState === "hidden" && f();
|
|
88
|
+
}, s = t(a);
|
|
89
|
+
return document.addEventListener("visibilitychange", o), window.addEventListener("pagehide", f), () => {
|
|
90
|
+
clearTimeout(i), s(), document.removeEventListener("visibilitychange", o), window.removeEventListener("pagehide", f), d = null;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function c(e) {
|
|
96
|
+
try {
|
|
97
|
+
return i(JSON.parse(localStorage.getItem(e) ?? "null"));
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { s as localStorageState, o as mergeSession, a as openWindowsOf, i as readSession };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog storage that keeps: IndexedDB, one object store of item records
|
|
3
|
+
* keyed by `id`.
|
|
4
|
+
*
|
|
5
|
+
* The database opens at the profile's own version, and one version up when
|
|
6
|
+
* the store is missing — opening at a fixed version throws `VersionError` on
|
|
7
|
+
* a profile already past it. It opens on first use; a failed open is retried
|
|
8
|
+
* on the next call. Every call rejects on failure (a private window), and the
|
|
9
|
+
* catalog then reports itself unavailable. A connection closes itself when
|
|
10
|
+
* another tab upgrades the database, and the next call reopens.
|
|
11
|
+
*/
|
|
12
|
+
import type { CatalogStorage } from './catalog.js';
|
|
13
|
+
/** A catalog storage in the IndexedDB database `name`, which keeps it across reloads. */
|
|
14
|
+
export declare function indexedDbStorage(name: string): CatalogStorage;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/shell/storage.ts
|
|
2
|
+
var e = "items", t = (e) => new Promise((t, n) => {
|
|
3
|
+
e.onsuccess = () => t(e.result), e.onerror = () => n(e.error ?? /* @__PURE__ */ Error("IndexedDB request failed"));
|
|
4
|
+
});
|
|
5
|
+
function n(t, n, r) {
|
|
6
|
+
return new Promise((i, a) => {
|
|
7
|
+
let o = n == null ? indexedDB.open(t) : indexedDB.open(t, n);
|
|
8
|
+
o.onupgradeneeded = () => {
|
|
9
|
+
o.result.objectStoreNames.contains(e) || o.result.createObjectStore(e, { keyPath: "id" });
|
|
10
|
+
}, o.onsuccess = () => {
|
|
11
|
+
let e = o.result;
|
|
12
|
+
e.onversionchange = () => {
|
|
13
|
+
r(), e.close();
|
|
14
|
+
}, i(e);
|
|
15
|
+
}, o.onerror = () => a(o.error ?? /* @__PURE__ */ Error("IndexedDB open failed")), o.onblocked = () => {
|
|
16
|
+
console.warn(`vintage-frames/shell: waiting for another tab to let go of the "${t}" database`);
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async function r(t, r) {
|
|
21
|
+
let i = await n(t, void 0, r);
|
|
22
|
+
if (!i.objectStoreNames.contains(e)) {
|
|
23
|
+
let e = i.version + 1;
|
|
24
|
+
i.close(), i = await n(t, e, r);
|
|
25
|
+
}
|
|
26
|
+
return i;
|
|
27
|
+
}
|
|
28
|
+
function i(n) {
|
|
29
|
+
let i = null, a = () => typeof indexedDB > "u" ? Promise.reject(/* @__PURE__ */ Error("IndexedDB is unavailable")) : (i ??= r(n, () => {
|
|
30
|
+
i = null;
|
|
31
|
+
}).catch((e) => {
|
|
32
|
+
throw i = null, e;
|
|
33
|
+
}), i), o = async (n, r) => t(r((await a()).transaction(e, n).objectStore(e)));
|
|
34
|
+
return {
|
|
35
|
+
list: () => o("readonly", (e) => e.getAll()),
|
|
36
|
+
put: (e) => o("readwrite", (t) => t.put(e)),
|
|
37
|
+
remove: (e) => o("readwrite", (t) => t.delete(e))
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { i as indexedDbStorage };
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The window manager. Applications make their windows; the manager places
|
|
3
|
+
* them, opens and closes them, follows the front application, carries them
|
|
4
|
+
* through a raster resize and arranges them.
|
|
5
|
+
*
|
|
6
|
+
* - `adopt()` takes a window an application made and appended: its
|
|
7
|
+
* application, the item it shows, how to place it, its resize policy, and
|
|
8
|
+
* what its close box means. `open()` raises the window for an item if one
|
|
9
|
+
* is open, and otherwise makes, appends, adopts and places one, and shows
|
|
10
|
+
* it out of `from`. `close()` hides a window into `to` — by default its
|
|
11
|
+
* home box, which the Finder provides: the item's icon, or the
|
|
12
|
+
* application's — then releases and removes it.
|
|
13
|
+
* - Placement, in order: this session's pin for the item, the saved pin,
|
|
14
|
+
* then the `place` policy — by default the first cascade slot no open
|
|
15
|
+
* window holds. The result is clamped into the desktop's window area.
|
|
16
|
+
* - The front application is the active window's, or the default one while
|
|
17
|
+
* none is active. `beforeFront` runs before a change, `onFront` reports it.
|
|
18
|
+
* - Palettes are an application's utility windows: shown while it is front
|
|
19
|
+
* and hidden otherwise, never closed or removed.
|
|
20
|
+
* - Opening a window moves keyboard focus into it; closing the active one
|
|
21
|
+
* moves focus to the window that becomes active, else to the closed
|
|
22
|
+
* window's icon, else to the desktop's icons. Focus left behind in a
|
|
23
|
+
* window that is no longer active — a title-bar press moves none — goes
|
|
24
|
+
* to the active window too.
|
|
25
|
+
* - `onWindows` reports a window opened or closed and a close's zoom rects
|
|
26
|
+
* landing, palettes aside; `isOpen()` and `hasWindows()` hold until they
|
|
27
|
+
* land, which is what the Finder's open ghost follows.
|
|
28
|
+
* - The resize rule re-pins every window with the nine-slice pin, re-reads a
|
|
29
|
+
* resizable window's pin when its size changed, floors it at the window's
|
|
30
|
+
* own size limits and caps it to the window area, so growing back restores
|
|
31
|
+
* every place.
|
|
32
|
+
*/
|
|
33
|
+
import { VfWindow } from '../index.js';
|
|
34
|
+
import type { VfDesktop, VfViewportBox } from '../index.js';
|
|
35
|
+
import type { Bands, Box, Pin, Policy, Size } from './geometry.js';
|
|
36
|
+
/** A box to write, any edge optional. */
|
|
37
|
+
export interface BoxInput {
|
|
38
|
+
left?: number;
|
|
39
|
+
top?: number;
|
|
40
|
+
width?: number;
|
|
41
|
+
height?: number;
|
|
42
|
+
}
|
|
43
|
+
/** A placement: a position, and a size where one is stated. */
|
|
44
|
+
export interface Placed extends BoxInput {
|
|
45
|
+
left: number;
|
|
46
|
+
top: number;
|
|
47
|
+
}
|
|
48
|
+
/** How a window is placed: its box on the desktop's live window area. */
|
|
49
|
+
export type PlacePolicy = (area: Box) => BoxInput & Point;
|
|
50
|
+
type Point = {
|
|
51
|
+
left: number;
|
|
52
|
+
top: number;
|
|
53
|
+
};
|
|
54
|
+
/** What adopting a window declares. */
|
|
55
|
+
export interface AdoptOptions {
|
|
56
|
+
/** The application shown while the window is active. */
|
|
57
|
+
app: string;
|
|
58
|
+
/** The item it shows: a catalog id, or an application's own key. */
|
|
59
|
+
item?: string | null;
|
|
60
|
+
/** Its placement on the live window area; adopt() and arrange() apply it. Unset, the owner places it. */
|
|
61
|
+
place?: PlacePolicy | null;
|
|
62
|
+
/** A saved pin to open at instead. */
|
|
63
|
+
pin?: Pin | null;
|
|
64
|
+
/** Its resize policy for the re-pin. Default: a resizable window floored at its size limits, else fixed. */
|
|
65
|
+
policy?: ((cur: Box) => Policy) | null;
|
|
66
|
+
/** A box held across a raster resize while the window is near it. */
|
|
67
|
+
keep?: ((area: Box) => Box) | null;
|
|
68
|
+
/**
|
|
69
|
+
* A palette: shown while its application is front — and while the
|
|
70
|
+
* predicate holds, for one the application hides for its own reasons —
|
|
71
|
+
* and hidden otherwise. Never closed by the manager.
|
|
72
|
+
*/
|
|
73
|
+
palette?: boolean | (() => boolean);
|
|
74
|
+
/**
|
|
75
|
+
* What the close box means. Unset, it closes the window. Set, the close box
|
|
76
|
+
* asks the application, which calls `close()` once it decides — after a
|
|
77
|
+
* Save prompt, say — or never.
|
|
78
|
+
*/
|
|
79
|
+
close?: ((win: VfWindow) => void) | null;
|
|
80
|
+
}
|
|
81
|
+
/** What opening a window for an item declares. */
|
|
82
|
+
export interface OpenOptions extends Omit<AdoptOptions, 'pin' | 'palette'> {
|
|
83
|
+
/** Makes the window, not yet appended. */
|
|
84
|
+
create: () => VfWindow;
|
|
85
|
+
/** The box it opens out of, in viewport CSS px: an icon's `cellRect()`. */
|
|
86
|
+
from?: VfViewportBox | null;
|
|
87
|
+
}
|
|
88
|
+
/** An application's arrangement group, for windows it places itself. */
|
|
89
|
+
export interface ArrangeGroup {
|
|
90
|
+
arrange(): void;
|
|
91
|
+
arranged(): boolean;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The box a closing window shrinks into, in viewport CSS px: `item` is the
|
|
95
|
+
* window's item, or null, and `app` its application. The Finder answers with
|
|
96
|
+
* the item's icon, the nearest open folder around it, or the application's
|
|
97
|
+
* icon.
|
|
98
|
+
*/
|
|
99
|
+
export type HomeBox = (item: string | null, app: string) => VfViewportBox | null;
|
|
100
|
+
/** Move the focus to the closed window's icon (`item` null: the desktop's first); resolves whether it moved. */
|
|
101
|
+
export type FocusHome = (item: string | null, app: string | null) => boolean;
|
|
102
|
+
export interface WindowManagerOptions {
|
|
103
|
+
/** The application front while no window is active. */
|
|
104
|
+
defaultApp: string;
|
|
105
|
+
/** A saved pin by item — the session the boot read. */
|
|
106
|
+
savedPin?: (item: string) => Pin | null;
|
|
107
|
+
}
|
|
108
|
+
/** The innermost focused element, through open shadow roots. */
|
|
109
|
+
export declare function deepActiveElement(): Element | null;
|
|
110
|
+
/**
|
|
111
|
+
* Move keyboard focus to the first thing in `win`'s content that takes it,
|
|
112
|
+
* once that content has rendered — and then only if `when`, read at that
|
|
113
|
+
* point, still holds. Resolves whether it did: a window with nothing
|
|
114
|
+
* focusable in its content keeps the focus where it was.
|
|
115
|
+
*/
|
|
116
|
+
export declare function focusInto(win: Element, { when }?: {
|
|
117
|
+
when?: () => boolean;
|
|
118
|
+
}): Promise<boolean>;
|
|
119
|
+
/** The window manager over a desktop. */
|
|
120
|
+
export declare function createWindowManager(desktop: VfDesktop, options: WindowManagerOptions): {
|
|
121
|
+
/** The desktop's window area, system px. */
|
|
122
|
+
readonly area: Box;
|
|
123
|
+
/** The front application's id. */
|
|
124
|
+
readonly front: string;
|
|
125
|
+
/**
|
|
126
|
+
* Take a window an application made and appended (a slotted child of the
|
|
127
|
+
* desktop: the clamp reads its lattice). It opens at the saved `pin`, or
|
|
128
|
+
* at its `place`; without either the owner placed it.
|
|
129
|
+
*/
|
|
130
|
+
adopt(win: VfWindow, opts: AdoptOptions): void;
|
|
131
|
+
/**
|
|
132
|
+
* Open the window for `item`: raise it if one is open, else make it,
|
|
133
|
+
* append it, adopt it and place it — this session's pin, then the saved
|
|
134
|
+
* one, then `place`, by default the first free cascade slot — and show it
|
|
135
|
+
* out of `from`. Keyboard focus moves into it. Returns the window.
|
|
136
|
+
*/
|
|
137
|
+
open(opts: OpenOptions): VfWindow;
|
|
138
|
+
/**
|
|
139
|
+
* Close a window into `to` — by default its home box, the Finder's icon
|
|
140
|
+
* for it — then release and remove it. The item's box is remembered for
|
|
141
|
+
* the session. Resolves when the zoom rects have landed.
|
|
142
|
+
*/
|
|
143
|
+
close(win: VfWindow, { to }?: {
|
|
144
|
+
to?: VfViewportBox | null;
|
|
145
|
+
}): Promise<boolean>;
|
|
146
|
+
/** The close box's route: the window's own close, or a plain close. */
|
|
147
|
+
requestClose(win: VfWindow): void;
|
|
148
|
+
/** The window showing `item`, or null. */
|
|
149
|
+
windowFor(item: string): VfWindow | null;
|
|
150
|
+
/** Every window of an application, back to front. */
|
|
151
|
+
windowsOf(app: string): VfWindow[];
|
|
152
|
+
/** A window's item, or null. */
|
|
153
|
+
itemOf(win: Element | null): string | null;
|
|
154
|
+
/** A window's application, or null when the manager doesn't hold it. */
|
|
155
|
+
appOf(win: Element | null): string | null;
|
|
156
|
+
/** Whether `win` is a palette. */
|
|
157
|
+
isPalette(win: Element | null): boolean;
|
|
158
|
+
/** Change the item a window shows: an untitled document's first save, say. */
|
|
159
|
+
setItem(win: VfWindow, item: string | null): void;
|
|
160
|
+
/** Whether a window shows `item`, or is still closing into its icon. */
|
|
161
|
+
isOpen(item: string): boolean;
|
|
162
|
+
/** Whether an application has a window open, palettes aside, or one still closing. */
|
|
163
|
+
hasWindows(app: string): boolean;
|
|
164
|
+
/** Re-read every palette's predicate: an application hid or showed one. */
|
|
165
|
+
palettesChanged(): void;
|
|
166
|
+
/** Write a box for a gesture the owner runs itself — a zoom, its group's arrange. */
|
|
167
|
+
write(win: VfWindow, box: Placed): void;
|
|
168
|
+
/** The box a placement would write, not written. */
|
|
169
|
+
clamped(win: VfWindow, box: BoxInput): Placed;
|
|
170
|
+
/** A window's `place` on the live area, clamped, or null. */
|
|
171
|
+
placed(win: VfWindow): Placed | null;
|
|
172
|
+
/** A window's pin on the current raster, for an owner that saves it. */
|
|
173
|
+
pinOf(win: VfWindow): Pin;
|
|
174
|
+
/** A pin on the live raster, clamped, under the window's policy or `policy`. */
|
|
175
|
+
fromPin(win: VfWindow, pin: Pin, policy?: Policy): Placed;
|
|
176
|
+
/** Widen the frame's bands, for an application whose windows dock along the edges. */
|
|
177
|
+
setFrameBands(next: Partial<Bands>): void;
|
|
178
|
+
/** Register an application's arrangement group. Returns the unregister. */
|
|
179
|
+
arrangeWith(app: string, group: ArrangeGroup): () => void;
|
|
180
|
+
/** View → Arrange Windows: every group's arrange, then every `place`. Nothing is raised. */
|
|
181
|
+
arrange(): void;
|
|
182
|
+
/** Whether every group is arranged and every shown window sits at its `place`. */
|
|
183
|
+
arranged(): boolean;
|
|
184
|
+
/**
|
|
185
|
+
* Carry every window from the `before` raster to the live one with the
|
|
186
|
+
* nine-slice pin, then tell the onRaster listeners. Nothing clamps a
|
|
187
|
+
* window's position, so growing back restores it.
|
|
188
|
+
*/
|
|
189
|
+
resized(before: Size): void;
|
|
190
|
+
/** The windows' saved boxes: each open one with its depth, the palettes as boxes alone. */
|
|
191
|
+
snapshot(): Record<string, {
|
|
192
|
+
app: string;
|
|
193
|
+
pin: Pin;
|
|
194
|
+
z?: number;
|
|
195
|
+
}>;
|
|
196
|
+
/** This session's pin for an item whose window closed, or null. */
|
|
197
|
+
rememberedPin(item: string): Pin | null;
|
|
198
|
+
/** Notify layout of a change the owner made itself. */
|
|
199
|
+
layoutChanged(): void;
|
|
200
|
+
/** Set where a window closes into and where focus goes after it: the Finder's. */
|
|
201
|
+
setHome(box: HomeBox, focus: FocusHome): () => void;
|
|
202
|
+
/** The layout signal: any geometry write, drag, grow or arrange. Returns the unsubscribe. */
|
|
203
|
+
onLayout(fn: () => void): () => void;
|
|
204
|
+
/** A window opened or closed, or finished closing, palettes aside; a window's item changed. */
|
|
205
|
+
onWindows(fn: () => void): () => void;
|
|
206
|
+
/** A raster resize, after every window has re-pinned. */
|
|
207
|
+
onRaster(fn: (before: Size, after: Size) => void): () => void;
|
|
208
|
+
/** An activation, before the front application changes: `fn(win)`, the new active window or null. */
|
|
209
|
+
beforeFront(fn: (win: HTMLElement | null) => void): () => void;
|
|
210
|
+
/** The front application changed. */
|
|
211
|
+
onFront(fn: (app: string) => void): () => void;
|
|
212
|
+
dispose(): void;
|
|
213
|
+
};
|
|
214
|
+
export type WindowManager = ReturnType<typeof createWindowManager>;
|
|
215
|
+
export {};
|