vintage-frames 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +16 -0
  2. package/custom-elements.json +271 -27
  3. package/dist/components/vf-button.d.ts +5 -1
  4. package/dist/components/vf-button.js +1 -1
  5. package/dist/components/vf-container.d.ts +3 -1
  6. package/dist/components/vf-container.js +34 -16
  7. package/dist/components/vf-desktop.d.ts +114 -12
  8. package/dist/components/vf-desktop.js +119 -58
  9. package/dist/components/vf-dialog.d.ts +7 -1
  10. package/dist/components/vf-icon-field.d.ts +1 -1
  11. package/dist/components/vf-icon.js +68 -61
  12. package/dist/components/vf-menu-bar.d.ts +5 -0
  13. package/dist/components/vf-menu-bar.js +67 -43
  14. package/dist/components/vf-window.d.ts +13 -0
  15. package/dist/components/vf-window.js +10 -1
  16. package/dist/events.d.ts +3 -2
  17. package/dist/events.js +3 -3
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.js +44 -43
  20. package/dist/modal-dialog.d.ts +31 -4
  21. package/dist/modal-dialog.js +98 -82
  22. package/dist/pattern-fill.d.ts +11 -8
  23. package/dist/pattern-fill.js +2 -4
  24. package/dist/position.d.ts +25 -7
  25. package/dist/position.js +31 -23
  26. package/dist/shell/alert.d.ts +40 -0
  27. package/dist/shell/alert.js +41 -0
  28. package/dist/shell/catalog.d.ts +168 -0
  29. package/dist/shell/catalog.js +278 -0
  30. package/dist/shell/clock.d.ts +16 -0
  31. package/dist/shell/clock.js +36 -0
  32. package/dist/shell/filing.d.ts +39 -0
  33. package/dist/shell/filing.js +68 -0
  34. package/dist/shell/finder.d.ts +156 -0
  35. package/dist/shell/finder.js +598 -0
  36. package/dist/shell/geometry.d.ts +196 -0
  37. package/dist/shell/geometry.js +241 -0
  38. package/dist/shell/index.d.ts +27 -0
  39. package/dist/shell/index.js +10 -0
  40. package/dist/shell/shell.d.ts +164 -0
  41. package/dist/shell/shell.js +234 -0
  42. package/dist/shell/state.d.ts +88 -0
  43. package/dist/shell/state.js +103 -0
  44. package/dist/shell/storage.d.ts +14 -0
  45. package/dist/shell/storage.js +41 -0
  46. package/dist/shell/windows.d.ts +234 -0
  47. package/dist/shell/windows.js +387 -0
  48. package/dist/text-control.d.ts +6 -0
  49. package/dist/text-control.js +2 -2
  50. package/dist/window-chrome.d.ts +43 -0
  51. package/dist/window-chrome.js +13 -0
  52. package/docs/SPEC.md +17 -16
  53. package/editor/vscode.html-custom-data.json +11 -6
  54. package/editor/web-types.json +55 -14
  55. package/package.json +10 -2
@@ -0,0 +1,234 @@
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
+ //#region src/shell/shell.ts
9
+ function s(e) {
10
+ return e;
11
+ }
12
+ var c = "app", l = (e) => e instanceof HTMLInputElement || e instanceof HTMLTextAreaElement || e?.isContentEditable === !0;
13
+ function u(s, c) {
14
+ let u = c.apps, f = u.find((e) => e.catalog), p = c.defaultApp ?? f?.id ?? u[0]?.id ?? "", m = c.state ?? null, h = c.services ?? {}, g = [], _ = s.querySelector(":scope > vf-menu-bar");
15
+ if (!_) throw Error("vintage-frames/shell: the desktop has no vf-menu-bar");
16
+ let v = _.querySelector(":scope > vf-menu");
17
+ if (!v) throw Error("vintage-frames/shell: the menu bar has no system menu");
18
+ _.shortcuts = !0;
19
+ let y = m?.saved?.pattern;
20
+ y && (s.pattern = y);
21
+ let b = null;
22
+ if (c.fit === "viewport") {
23
+ let t = () => {
24
+ let e = {
25
+ width: s.width,
26
+ height: s.height
27
+ };
28
+ s.fitWithin(document.documentElement.clientWidth, document.documentElement.clientHeight), b?.resized(e);
29
+ };
30
+ t(), window.addEventListener("resize", t);
31
+ let n = e(t);
32
+ g.push(() => {
33
+ window.removeEventListener("resize", t), n();
34
+ });
35
+ }
36
+ let x = r(s, {
37
+ defaultApp: p,
38
+ savedPin: (e) => m?.pin(e) ?? null
39
+ });
40
+ b = x;
41
+ let S = (e) => {
42
+ e.target === s && s.clearActive();
43
+ };
44
+ s.addEventListener("pointerdown", S), g.push(() => s.removeEventListener("pointerdown", S));
45
+ let C = {}, w = /* @__PURE__ */ new Map();
46
+ w.set("app", {
47
+ art: (e) => u.find((t) => t.id === d(e))?.icon ?? "",
48
+ open: (e, t) => void C[d(e) ?? ""]?.open?.({ from: t }),
49
+ editable: !1
50
+ });
51
+ for (let e of u) for (let [t, n] of Object.entries(e.kinds ?? {})) w.set(t, n);
52
+ let T = f?.catalog ? n({
53
+ storage: f.catalog.storage,
54
+ volumes: f.catalog.volumes,
55
+ listed: (e) => w.has(e),
56
+ hooks: (e) => w.get(e)
57
+ }) : null, E = () => !!document.querySelector("vf-dialog[open]"), D = () => l(i()), O = /* @__PURE__ */ new Map(), k = () => {
58
+ for (let [e, t] of O) {
59
+ let n = !t();
60
+ e.disabled !== n && (e.disabled = n);
61
+ }
62
+ };
63
+ document.addEventListener("pointerdown", k, !0), document.addEventListener("keydown", k, !0), s.addEventListener("vf-select", k), s.addEventListener("vf-activate", k), g.push(() => document.removeEventListener("pointerdown", k, !0), () => document.removeEventListener("keydown", k, !0), () => s.removeEventListener("vf-select", k), () => s.removeEventListener("vf-activate", k), x.onWindows(k), x.onLayout(k)), T && g.push(T.subscribe(k));
64
+ let A = (e) => {
65
+ if (!e) return [];
66
+ let t = document.createElement("div");
67
+ t.innerHTML = e, customElements.upgrade(t);
68
+ let n = [...t.children].filter((e) => e instanceof HTMLElement);
69
+ for (let e of n) e.remove();
70
+ return n;
71
+ }, j = u.map((e) => ({
72
+ def: e,
73
+ menus: A(e.menus).filter((e) => e.localName === "vf-menu"),
74
+ dialogs: A(e.dialogs).filter((e) => e.dataset.dialog != null)
75
+ })), M = [];
76
+ for (let { def: e, menus: t, dialogs: n } of j) {
77
+ let r = [], i = `vintage-frames/shell (${e.id})`, a = /* @__PURE__ */ new Set();
78
+ for (let t of n) x.holdDialog(t, { app: e.id }), a.add(t);
79
+ r.push(() => {
80
+ for (let e of a) x.releaseDialog(e), e.remove();
81
+ a.clear();
82
+ });
83
+ let o = {
84
+ desktop: s,
85
+ windows: x,
86
+ menus: t,
87
+ systemMenu: v,
88
+ apps: C,
89
+ catalog: T,
90
+ services: h,
91
+ state: m,
92
+ kinds: w,
93
+ kind: (e) => w.get(e),
94
+ app: (e) => u.find((t) => t.id === e),
95
+ menu(e) {
96
+ let n = t.find((t) => t.dataset.menu === e);
97
+ if (!n) throw Error(`${i}: missing menu ${e}`);
98
+ return n;
99
+ },
100
+ item(e) {
101
+ for (let n of t) {
102
+ let t = n.querySelector(`vf-menu-item[value="${CSS.escape(e)}"]`);
103
+ if (t) return t;
104
+ }
105
+ throw Error(`${i}: missing menu item ${e}`);
106
+ },
107
+ onMenu(e) {
108
+ for (let n of t) o.on(n, "vf-menu-select", (t) => {
109
+ if (E()) return;
110
+ let { value: n, item: r } = t.detail;
111
+ e(n, r);
112
+ });
113
+ },
114
+ systemItem(e, t, n) {
115
+ let i = document.createElement("vf-menu-item");
116
+ return i.setAttribute("value", e), i.textContent = t, v.append(i), o.on(v, "vf-menu-select", (t) => {
117
+ !E() && t.detail.value === e && n();
118
+ }), r.push(() => i.remove()), i;
119
+ },
120
+ dialog(e) {
121
+ let t = n.find((t) => t.dataset.dialog === e);
122
+ if (!t) throw Error(`${i}: missing dialog ${e}`);
123
+ return t;
124
+ },
125
+ hold(t) {
126
+ return x.holdDialog(t, { app: e.id }), a.add(t), t;
127
+ },
128
+ release(e) {
129
+ a.delete(e) && (x.releaseDialog(e), e.remove());
130
+ },
131
+ ask(t) {
132
+ if (x.appOf(t) !== e.id) throw Error(`${i}: ask() takes a dialog the application holds`);
133
+ return new Promise((e) => {
134
+ let n = (r) => {
135
+ r.target === t && (t.removeEventListener("vf-close", n), e(r.detail.returnValue));
136
+ };
137
+ t.addEventListener("vf-close", n), t.show();
138
+ });
139
+ },
140
+ gate(e, t) {
141
+ O.set(e, t), r.push(() => O.delete(e));
142
+ let n = !t();
143
+ e.disabled !== n && (e.disabled = n);
144
+ },
145
+ typing: D,
146
+ modalOpen: E,
147
+ onFront(t) {
148
+ t(x.front === e.id), r.push(x.onFront((n) => t(n === e.id)));
149
+ },
150
+ on(e, t, n, i) {
151
+ e.addEventListener(t, n, i), r.push(() => e.removeEventListener(t, n, i));
152
+ },
153
+ onDispose(e) {
154
+ r.push(e);
155
+ }
156
+ }, c = e.init(o);
157
+ c && (C[e.id] = c), M.push(() => {
158
+ for (let e of r.splice(0).reverse()) e();
159
+ });
160
+ }
161
+ let N = null, P = () => {
162
+ let e = x.asking ?? x.front, t = j.find((t) => t.def.id === e) ?? j.find((e) => e.def.id === p) ?? null;
163
+ if (!(!t || t === N)) {
164
+ if (N) for (let e of N.menus) e.remove();
165
+ v.after(...t.menus), _.label = t.def.name, N = t, k();
166
+ }
167
+ };
168
+ if (g.push(x.onFront(P), x.onDialogs(P)), P(), c.clock !== !1) {
169
+ let e = _.querySelector(":scope > [slot=\"end\"]"), t = !e;
170
+ e || (e = document.createElement("vf-label"), e.slot = "end", _.append(e));
171
+ let n = o(e);
172
+ g.push(() => {
173
+ n(), t && e.remove();
174
+ });
175
+ }
176
+ let F = /* @__PURE__ */ new WeakSet(), I = () => {
177
+ if (T) for (let e of s.stackingOrder) {
178
+ if (!(e instanceof t)) continue;
179
+ let n = x.itemOf(e);
180
+ if (n == null) continue;
181
+ let r = T.item(n);
182
+ r ? (F.add(e), e.heading !== r.name && (e.heading = r.name)) : F.has(e) && T.get().available && x.close(e, { to: null });
183
+ }
184
+ };
185
+ return T && g.push(T.subscribe(I), x.onWindows(I)), m?.hold(), {
186
+ desktop: s,
187
+ windows: x,
188
+ catalog: T,
189
+ apps: C,
190
+ ready: (async () => {
191
+ try {
192
+ T && (await T.refresh(), f?.catalog?.seed && await T.seed(f.catalog.seed));
193
+ for (let { item: e, app: t } of a(m?.saved ?? null)) await C[t]?.open?.({ item: e });
194
+ let e = m?.saved?.active;
195
+ if (e) {
196
+ let t = x.windowFor(e);
197
+ t && s.bringToFront(t);
198
+ }
199
+ } finally {
200
+ if (m) {
201
+ let e = m.start(() => ({
202
+ windows: x.snapshot(),
203
+ active: x.itemOf(s.activeWindow),
204
+ pattern: s.pattern ?? null
205
+ }), (e) => {
206
+ let t = [
207
+ x.onLayout(e),
208
+ x.onWindows(e),
209
+ ...T ? [T.subscribe(e)] : []
210
+ ];
211
+ return s.addEventListener("vf-activate", e), () => {
212
+ for (let e of t) e();
213
+ s.removeEventListener("vf-activate", e);
214
+ };
215
+ });
216
+ g.push(e), m.release();
217
+ }
218
+ }
219
+ })(),
220
+ dispose() {
221
+ for (let e of M.splice(0).reverse()) e();
222
+ if (N) for (let e of N.menus) e.remove();
223
+ N = null;
224
+ for (let e of g.splice(0)) e();
225
+ x.dispose(), T?.dispose();
226
+ }
227
+ };
228
+ }
229
+ function d(e) {
230
+ let t = e.data;
231
+ return typeof t?.app == "string" ? t.app : null;
232
+ }
233
+ //#endregion
234
+ export { c as APP_KIND, d as appIdOf, u as createShell, s 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 };