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.
- package/README.md +16 -0
- package/custom-elements.json +271 -27
- package/dist/components/vf-button.d.ts +5 -1
- package/dist/components/vf-button.js +1 -1
- package/dist/components/vf-container.d.ts +3 -1
- package/dist/components/vf-container.js +34 -16
- package/dist/components/vf-desktop.d.ts +114 -12
- package/dist/components/vf-desktop.js +119 -58
- package/dist/components/vf-dialog.d.ts +7 -1
- package/dist/components/vf-icon-field.d.ts +1 -1
- package/dist/components/vf-icon.js +68 -61
- package/dist/components/vf-menu-bar.d.ts +5 -0
- package/dist/components/vf-menu-bar.js +67 -43
- package/dist/components/vf-window.d.ts +13 -0
- package/dist/components/vf-window.js +10 -1
- package/dist/events.d.ts +3 -2
- package/dist/events.js +3 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +44 -43
- package/dist/modal-dialog.d.ts +31 -4
- package/dist/modal-dialog.js +98 -82
- 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 +31 -23
- package/dist/shell/alert.d.ts +40 -0
- package/dist/shell/alert.js +41 -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 +156 -0
- package/dist/shell/finder.js +598 -0
- package/dist/shell/geometry.d.ts +196 -0
- package/dist/shell/geometry.js +241 -0
- package/dist/shell/index.d.ts +27 -0
- package/dist/shell/index.js +10 -0
- package/dist/shell/shell.d.ts +164 -0
- package/dist/shell/shell.js +234 -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 +234 -0
- package/dist/shell/windows.js +387 -0
- package/dist/text-control.d.ts +6 -0
- package/dist/text-control.js +2 -2
- package/dist/window-chrome.d.ts +43 -0
- package/dist/window-chrome.js +13 -0
- package/docs/SPEC.md +17 -16
- package/editor/vscode.html-custom-data.json +11 -6
- package/editor/web-types.json +55 -14
- package/package.json +10 -2
package/dist/position.js
CHANGED
|
@@ -70,25 +70,25 @@ var h = class {
|
|
|
70
70
|
return u(e.width, e.height, this.#s, t(this.#e));
|
|
71
71
|
}
|
|
72
72
|
hostConnected() {
|
|
73
|
-
this.#a && (this.#r || this.#l) && this.#
|
|
73
|
+
this.#a && (this.#r || this.#l) && this.#b();
|
|
74
74
|
}
|
|
75
75
|
hostDisconnected() {
|
|
76
|
-
this.#
|
|
76
|
+
this.#x();
|
|
77
77
|
}
|
|
78
78
|
hostUpdated() {
|
|
79
79
|
let e = this.#e.top ?? null, t = this.#e.left ?? null, n = this.#e.fixed === !0, r = this.#e.origin ?? null, i = this.#e.style;
|
|
80
80
|
if (r !== this.#o && (this.#o = r, this.#s = this.#p(r)), e === null && t === null && !n) {
|
|
81
81
|
if (!this.#a) return;
|
|
82
|
-
this.#r && this.#
|
|
82
|
+
this.#r && this.#y(), this.#x(), this.#a = !1, this.#r = !1, this.#l = !1, this.#u = 0, this.#d = 0, i.removeProperty("position"), i.removeProperty("top"), i.removeProperty("left"), i.removeProperty("right"), i.removeProperty("bottom"), i.removeProperty("margin"), this.#_(null, null);
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
85
|
if (this.#a && e === this.#t && t === this.#n && n === this.#r && r === this.#i) return;
|
|
86
86
|
let a = this.#a && this.#r, o = this.#s !== c;
|
|
87
87
|
if (this.#a = !0, this.#t = e, this.#n = t, this.#r = n, this.#i = r, this.#l = o, i.position = n ? "sticky" : "absolute", o) {
|
|
88
88
|
let e = this.#e.getBoundingClientRect();
|
|
89
|
-
(e.width > 0 || e.height > 0) && this.#
|
|
89
|
+
(e.width > 0 || e.height > 0) && this.#g(e.width, e.height);
|
|
90
90
|
} else this.#u = 0, this.#d = 0;
|
|
91
|
-
this.#m(), i.right = "auto", i.bottom = "auto", n ? a || this.#
|
|
91
|
+
this.#m(), i.right = "auto", i.bottom = "auto", n ? a || this.#v() : (a && this.#y(), i.margin = "0", this.#h()), n || o ? this.#b() : this.#x(), this.#_(t ?? 0, e ?? 0);
|
|
92
92
|
}
|
|
93
93
|
#p(e) {
|
|
94
94
|
if (e == null) return c;
|
|
@@ -97,49 +97,54 @@ var h = class {
|
|
|
97
97
|
}
|
|
98
98
|
#m() {
|
|
99
99
|
let e = this.#e.style;
|
|
100
|
-
e.top = r((this.#t ?? 0) - this.#d), e.left = r((this.#n ?? 0) - this.#u);
|
|
100
|
+
e.top = r((this.#t ?? 0) - this.#d), e.left = r((this.#n ?? 0) - this.#u), this.#h();
|
|
101
101
|
}
|
|
102
|
-
#h(
|
|
102
|
+
#h() {
|
|
103
|
+
!this.#l || this.#r || (this.#e.style.marginRight = r(-((this.#n ?? 0) - this.#u)));
|
|
104
|
+
}
|
|
105
|
+
#g(e, n) {
|
|
103
106
|
let r = u(e, n, this.#s, t(this.#e));
|
|
104
107
|
return r.x === this.#u && r.y === this.#d ? !1 : (this.#u = r.x, this.#d = r.y, !0);
|
|
105
108
|
}
|
|
106
|
-
#
|
|
109
|
+
#_(t, n) {
|
|
107
110
|
e(this.#e, "vf-placement-change", {
|
|
108
111
|
left: t,
|
|
109
112
|
top: n
|
|
110
113
|
}, { composed: !1 });
|
|
111
114
|
}
|
|
112
|
-
#
|
|
115
|
+
#v() {
|
|
113
116
|
let e = this.#e.style;
|
|
114
|
-
e.maxWidth = "fit-content", e.zIndex = "1", this.#
|
|
117
|
+
e.maxWidth = "fit-content", e.zIndex = "1", this.#S = !1;
|
|
115
118
|
let t = this.#e.getBoundingClientRect();
|
|
116
|
-
(t.width > 0 || t.height > 0) && this.#
|
|
119
|
+
(t.width > 0 || t.height > 0) && this.#C(t.width, t.height);
|
|
117
120
|
}
|
|
118
|
-
#
|
|
119
|
-
this.#
|
|
121
|
+
#y() {
|
|
122
|
+
this.#S = !1;
|
|
120
123
|
let e = this.#e.style;
|
|
121
124
|
e.removeProperty("display"), e.removeProperty("max-width"), e.removeProperty("z-index");
|
|
122
125
|
}
|
|
123
|
-
#
|
|
126
|
+
#b() {
|
|
124
127
|
this.#f ??= new ResizeObserver((e) => {
|
|
125
128
|
let t = e[e.length - 1]?.borderBoxSize?.[0], n = t ? {
|
|
126
129
|
width: t.inlineSize,
|
|
127
130
|
height: t.blockSize
|
|
128
131
|
} : this.#e.getBoundingClientRect();
|
|
129
|
-
this.#r && this.#
|
|
132
|
+
this.#r && this.#C(n.width, n.height);
|
|
133
|
+
let r = n.width > 0 || n.height > 0;
|
|
134
|
+
this.#l && r && this.#g(n.width, n.height) && (this.#m(), this.#_(this.#n ?? 0, this.#t ?? 0));
|
|
130
135
|
}), this.#f.observe(this.#e);
|
|
131
136
|
}
|
|
132
|
-
#
|
|
137
|
+
#x() {
|
|
133
138
|
this.#f?.disconnect(), this.#f = null;
|
|
134
139
|
}
|
|
135
|
-
#
|
|
136
|
-
#
|
|
140
|
+
#S = !1;
|
|
141
|
+
#C(e, t) {
|
|
137
142
|
let n = this.#e.style;
|
|
138
|
-
if (!this.#
|
|
143
|
+
if (!this.#S) {
|
|
139
144
|
let e = getComputedStyle(this.#e).display;
|
|
140
145
|
if (e) {
|
|
141
146
|
let t = d[e];
|
|
142
|
-
t && (n.display = t), this.#
|
|
147
|
+
t && (n.display = t), this.#S = !0;
|
|
143
148
|
}
|
|
144
149
|
}
|
|
145
150
|
n.margin = `0 ${-e}px ${-t}px 0`;
|
|
@@ -193,10 +198,13 @@ var _ = class {
|
|
|
193
198
|
};
|
|
194
199
|
}
|
|
195
200
|
#i() {
|
|
196
|
-
let
|
|
201
|
+
let t = this.#e, n = { bounds: null };
|
|
202
|
+
if (e(t, "vf-placement-bounds-request", n, { composed: !1 }), n.bounds) return n.bounds;
|
|
203
|
+
let r = t.offsetParent, a = r && r.clientWidth > 0 && r.clientHeight > 0 ? r : null;
|
|
197
204
|
return {
|
|
198
|
-
width: i(
|
|
199
|
-
height: i(
|
|
205
|
+
width: i(a?.clientWidth ?? window.innerWidth, t),
|
|
206
|
+
height: i(a?.clientHeight ?? window.innerHeight, t),
|
|
207
|
+
top: 0
|
|
200
208
|
};
|
|
201
209
|
}
|
|
202
210
|
}, v = /* @__PURE__ */ new Set([
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Finder's alert: a plain-framed `vf-dialog` with the site's caution art,
|
|
3
|
+
* a message and a row of buttons, sized to the message — composed from kit
|
|
4
|
+
* elements, as every alert in the kit is. Its buttons submit a
|
|
5
|
+
* `<form method="dialog">`, so it answers with the pressed button's `value`
|
|
6
|
+
* like any dialog an application holds.
|
|
7
|
+
*
|
|
8
|
+
* The layout follows the plain frame's body, its width and height less 10:
|
|
9
|
+
* the art and the message 16 in from the body's top-left, the buttons'
|
|
10
|
+
* bottom-right corner 16 in from the body's.
|
|
11
|
+
*/
|
|
12
|
+
import type { VfDialog } from '../index.js';
|
|
13
|
+
/** One button of an alert. */
|
|
14
|
+
export interface AlertButton {
|
|
15
|
+
label: string;
|
|
16
|
+
/** What the alert answers when it is pressed. */
|
|
17
|
+
value: string;
|
|
18
|
+
/** The default button, which Return presses. */
|
|
19
|
+
default?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface AlertOptions {
|
|
22
|
+
/** Default: OK, the default button. */
|
|
23
|
+
buttons?: AlertButton[];
|
|
24
|
+
/** The dialog's width, system px. Default 400. */
|
|
25
|
+
width?: number;
|
|
26
|
+
/** The 32×32 art beside the message, or null for none. */
|
|
27
|
+
art?: string | null;
|
|
28
|
+
/** The dialog's accessible name. Default "Alert". */
|
|
29
|
+
label?: string;
|
|
30
|
+
}
|
|
31
|
+
/** An alert, made but not yet shown. */
|
|
32
|
+
export interface ComposedAlert {
|
|
33
|
+
dialog: VfDialog;
|
|
34
|
+
/** Resolves once its parts have rendered, so `show()` opens it at once. Call it once the dialog is in the page. */
|
|
35
|
+
rendered(): Promise<unknown>;
|
|
36
|
+
/** Size the box to the message. Call it right after `show()`: a closed dialog lays out nothing. */
|
|
37
|
+
fit(): void;
|
|
38
|
+
}
|
|
39
|
+
/** Make an alert for `message`. */
|
|
40
|
+
export declare function composeAlert(message: string, options?: AlertOptions): ComposedAlert;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { effectiveScale as e } from "../scale.js";
|
|
2
|
+
import "../index.js";
|
|
3
|
+
//#region src/shell/alert.ts
|
|
4
|
+
var t = 10, n = 16, r = 32, i = 28;
|
|
5
|
+
function a(a, o = {}) {
|
|
6
|
+
let { buttons: s = [{
|
|
7
|
+
label: "OK",
|
|
8
|
+
value: "ok",
|
|
9
|
+
default: !0
|
|
10
|
+
}], width: c = 400, art: l = null, label: u = "Alert" } = o, d = document.createElement("vf-dialog");
|
|
11
|
+
d.frame = "plain", d.label = u, d.width = c, d.height = 120;
|
|
12
|
+
let f = document.createElement("form");
|
|
13
|
+
f.method = "dialog", f.noValidate = !0;
|
|
14
|
+
let p = l ? 64 : n;
|
|
15
|
+
if (l) {
|
|
16
|
+
let e = document.createElement("vf-img");
|
|
17
|
+
e.width = r, e.height = r, e.left = n, e.top = n;
|
|
18
|
+
let t = document.createElement("img");
|
|
19
|
+
t.src = l, t.alt = "", e.append(t), f.append(e);
|
|
20
|
+
}
|
|
21
|
+
let m = document.createElement("vf-paragraph");
|
|
22
|
+
m.face = "display", m.width = c - t - p - n, m.left = p, m.top = n, m.textContent = a, f.append(m);
|
|
23
|
+
let h = document.createElement("vf-button-group");
|
|
24
|
+
return h.origin = "bottom right", h.append(...s.map((e) => {
|
|
25
|
+
let t = document.createElement("vf-button");
|
|
26
|
+
return t.textContent = e.label, t.type = "submit", t.value = e.value, e.default && (t.variant = "default"), t;
|
|
27
|
+
})), f.append(h), d.append(f), {
|
|
28
|
+
dialog: d,
|
|
29
|
+
rendered: () => Promise.all([
|
|
30
|
+
d.updateComplete,
|
|
31
|
+
m.updateComplete,
|
|
32
|
+
h.updateComplete
|
|
33
|
+
]),
|
|
34
|
+
fit() {
|
|
35
|
+
let r = Math.ceil(m.getBoundingClientRect().height / e(m)), a = Math.max(l ? 48 : 0, n + r) + n + i + n;
|
|
36
|
+
d.height = a + t, h.left = c - t - n, h.top = a - n;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { a as composeAlert };
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalog: what is where. Pure and DOM-free — it runs under Node — and
|
|
3
|
+
* the source of truth for what a folder holds; the Finder renders it.
|
|
4
|
+
*
|
|
5
|
+
* - An item is a small record: a name, a kind, the container it sits in
|
|
6
|
+
* (`parent`, a folder's id, or null for the desktop) and, once placed, its
|
|
7
|
+
* position there in whole system px. A kind keeps small data of its own in
|
|
8
|
+
* `data`; a large payload stays in its application's own store, keyed by
|
|
9
|
+
* the item's id, and the kind's `copy` and `onRemove` keep that store in
|
|
10
|
+
* step.
|
|
11
|
+
* - Volumes are containers no one made: the Trash and a startup disk, each
|
|
12
|
+
* if the page wants one. They lead the listing, keep their names, and are
|
|
13
|
+
* never renamed, moved, copied or removed; nothing is made in the Trash.
|
|
14
|
+
* Deleting is a move into the Trash, and only Empty Trash removes. A
|
|
15
|
+
* volume's position is stored on a record of its own id.
|
|
16
|
+
* - A folder never goes into itself or a folder inside it. A parent with no
|
|
17
|
+
* record reads as the desktop.
|
|
18
|
+
* - An item whose kind no application registers is left out of the listing
|
|
19
|
+
* and kept in storage.
|
|
20
|
+
* - Storage is three calls over item records (list, put, remove). The
|
|
21
|
+
* catalog holds every record in memory, writes through to storage, and
|
|
22
|
+
* lists from memory; a position change writes after a short delay, since
|
|
23
|
+
* the resize rule moves every desktop icon on each resize event.
|
|
24
|
+
* - A seed runs once per storage: a persistent one stores a marker record
|
|
25
|
+
* (`#seeded`), so a site that starts in memory and switches its storage
|
|
26
|
+
* seeds once there.
|
|
27
|
+
*/
|
|
28
|
+
import type { Point } from './geometry.js';
|
|
29
|
+
/** The Trash's id. */
|
|
30
|
+
export declare const TRASH = "trash";
|
|
31
|
+
/** The startup disk's id. */
|
|
32
|
+
export declare const DISK = "disk";
|
|
33
|
+
/** A folder's kind. */
|
|
34
|
+
export declare const FOLDER = "folder";
|
|
35
|
+
/** The name a new folder takes, counted up where it is taken. */
|
|
36
|
+
export declare const UNTITLED_FOLDER = "untitled folder";
|
|
37
|
+
/** One item of the catalog. */
|
|
38
|
+
export interface Item {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
/** `folder`, a volume's (`trash`, `disk`), or a kind an application registers. */
|
|
42
|
+
kind: string;
|
|
43
|
+
/** A folder's id, a volume's, or null for the desktop. */
|
|
44
|
+
parent: string | null;
|
|
45
|
+
/** Its place in its container, whole system px; unset, the next free cell. */
|
|
46
|
+
left?: number;
|
|
47
|
+
top?: number;
|
|
48
|
+
createdAt: number;
|
|
49
|
+
modifiedAt: number;
|
|
50
|
+
/** The kind's own, small and serializable. */
|
|
51
|
+
data?: unknown;
|
|
52
|
+
}
|
|
53
|
+
/** The listing: volumes first, then every listed item by creation. */
|
|
54
|
+
export interface CatalogState {
|
|
55
|
+
/** Whether storage answers. Without it the catalog lists the volumes alone. */
|
|
56
|
+
available: boolean;
|
|
57
|
+
items: readonly Item[];
|
|
58
|
+
}
|
|
59
|
+
/** Where item records live. Any object with these three calls will do. */
|
|
60
|
+
export interface CatalogStorage {
|
|
61
|
+
list(): Promise<Item[]>;
|
|
62
|
+
put(item: Item): Promise<unknown>;
|
|
63
|
+
remove(id: string): Promise<unknown>;
|
|
64
|
+
}
|
|
65
|
+
/** What a kind tells the catalog about its items' payloads. */
|
|
66
|
+
export interface KindHooks {
|
|
67
|
+
/** Bytes, for Empty Trash's alert. */
|
|
68
|
+
size?(item: Item): number;
|
|
69
|
+
/** Copy `from`'s payload to `to`, a fresh copy of it. */
|
|
70
|
+
copy?(from: Item, to: Item): void | Promise<void>;
|
|
71
|
+
/** Empty Trash removed the item: drop its payload. */
|
|
72
|
+
onRemove?(item: Item): void | Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
/** The volumes a page wants, by name; `false` or unset is none (the Trash defaults on). */
|
|
75
|
+
export interface Volumes {
|
|
76
|
+
trash?: string | false;
|
|
77
|
+
disk?: string | false;
|
|
78
|
+
}
|
|
79
|
+
export interface CatalogOptions {
|
|
80
|
+
storage: CatalogStorage | null;
|
|
81
|
+
volumes?: Volumes;
|
|
82
|
+
/** Whether an application registers `kind`. Folders are always listed. Default: every kind. */
|
|
83
|
+
listed?(kind: string): boolean;
|
|
84
|
+
/** A kind's payload hooks. */
|
|
85
|
+
hooks?(kind: string): KindHooks | undefined;
|
|
86
|
+
now?(): number;
|
|
87
|
+
newId?(): string;
|
|
88
|
+
/** How long a position change waits before it is written, ms. Default 400. */
|
|
89
|
+
placeDelay?: number;
|
|
90
|
+
}
|
|
91
|
+
/** What an operation makes: the new item, or null where it was refused. */
|
|
92
|
+
export interface NewItem {
|
|
93
|
+
name?: string;
|
|
94
|
+
kind?: string;
|
|
95
|
+
parent?: string | null;
|
|
96
|
+
left?: number;
|
|
97
|
+
top?: number;
|
|
98
|
+
data?: unknown;
|
|
99
|
+
/** The creation time, for a batch made in order. Default now. */
|
|
100
|
+
at?: number;
|
|
101
|
+
}
|
|
102
|
+
/** A catalog dumped or restored: its item records. */
|
|
103
|
+
export interface CatalogArchive {
|
|
104
|
+
items: Item[];
|
|
105
|
+
}
|
|
106
|
+
/** Whether `kind` holds other items: a folder or a volume. */
|
|
107
|
+
export declare const isContainerKind: (kind: string) => boolean;
|
|
108
|
+
/** Whether `id` names a volume. */
|
|
109
|
+
export declare const isVolume: (id: string | null | undefined) => boolean;
|
|
110
|
+
/** The item with `id`, or null. */
|
|
111
|
+
export declare function itemOf(state: CatalogState, id: string | null | undefined): Item | null;
|
|
112
|
+
/** The container `parent` resolves to: that container while it is listed, else the desktop (null). */
|
|
113
|
+
export declare function containerOf(state: CatalogState, parent: string | null | undefined): string | null;
|
|
114
|
+
/** The items directly in a container (null: the desktop), in listing order. */
|
|
115
|
+
export declare function childrenOf(state: CatalogState, parent: string | null): Item[];
|
|
116
|
+
/** How many items a container holds directly. */
|
|
117
|
+
export declare function itemCount(state: CatalogState, parent: string | null): number;
|
|
118
|
+
/**
|
|
119
|
+
* Whether `ancestor` is on `id`'s chain of containers. An item is not inside
|
|
120
|
+
* itself; a looping chain stops at its first repeat.
|
|
121
|
+
*/
|
|
122
|
+
export declare function isInside(state: CatalogState, id: string, ancestor: string): boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Container `id` and the containers around it, innermost first. The desktop
|
|
125
|
+
* ends the chain, and so does an id with no container; a loop stops at its
|
|
126
|
+
* first repeat.
|
|
127
|
+
*/
|
|
128
|
+
export declare function enclosingFolders(state: CatalogState, id: string | null | undefined): string[];
|
|
129
|
+
/** Whether `id` is the Trash or anything inside it. */
|
|
130
|
+
export declare function isTrashed(state: CatalogState, id: string | null | undefined): boolean;
|
|
131
|
+
/** Everything under a container, each container before what it holds; a loop is walked once. */
|
|
132
|
+
export declare function descendantsOf(state: CatalogState, parent: string | null): Item[];
|
|
133
|
+
/** The first free folder name in a container: "untitled folder", "untitled folder 2", … */
|
|
134
|
+
export declare function nextFolderName(state: CatalogState, parent: string | null): string;
|
|
135
|
+
/**
|
|
136
|
+
* The name for a copy in a container: `name` itself while no item of its
|
|
137
|
+
* kind there has it, else its base (less " copy" and " copy N") plus " copy",
|
|
138
|
+
* then " copy 2", " copy 3", …
|
|
139
|
+
*/
|
|
140
|
+
export declare function copyName(state: CatalogState, parent: string | null, name: string, kind: string): string;
|
|
141
|
+
/** The catalog's operations, over its listing. */
|
|
142
|
+
export interface Catalog {
|
|
143
|
+
get(): CatalogState;
|
|
144
|
+
subscribe(fn: (state: CatalogState) => void): () => void;
|
|
145
|
+
item(id: string | null | undefined): Item | null;
|
|
146
|
+
refresh(): Promise<void>;
|
|
147
|
+
seed(fn: (catalog: Catalog) => void | Promise<void>): Promise<boolean>;
|
|
148
|
+
create(input?: NewItem): Promise<Item | null>;
|
|
149
|
+
rename(id: string, name: string): Promise<boolean>;
|
|
150
|
+
update(id: string, data: unknown): Promise<boolean>;
|
|
151
|
+
move(ids: readonly string[], parent: string | null, landings?: ReadonlyMap<string, Point | null>): Promise<string[]>;
|
|
152
|
+
place(positions: ReadonlyMap<string, Point>): void;
|
|
153
|
+
flush(): Promise<void>;
|
|
154
|
+
copy(ids: readonly string[], parent: string | null): Promise<Item[]>;
|
|
155
|
+
emptyTrash(): Promise<Item[]>;
|
|
156
|
+
trashSize(): number;
|
|
157
|
+
dump(): CatalogArchive;
|
|
158
|
+
clear(): Promise<void>;
|
|
159
|
+
import(archive: CatalogArchive, options?: {
|
|
160
|
+
mode?: 'merge' | 'replace';
|
|
161
|
+
}): Promise<number>;
|
|
162
|
+
dispose(): Promise<void>;
|
|
163
|
+
}
|
|
164
|
+
export declare function createCatalog(options: CatalogOptions): Catalog;
|
|
165
|
+
/** Storage that forgets everything on a reload. Its map is exposed for tests. */
|
|
166
|
+
export declare function memoryStorage(initial?: readonly Item[]): CatalogStorage & {
|
|
167
|
+
records: Map<string, Item>;
|
|
168
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
//#region src/shell/catalog.ts
|
|
2
|
+
var e = "trash", t = "disk", n = "folder", r = "untitled folder", i = "#seeded", a = (e) => e === "folder" || e === "trash" || e === "disk", o = (e) => e === "trash" || e === "disk";
|
|
3
|
+
function s(e, t) {
|
|
4
|
+
return t == null ? null : e.items.find((e) => e.id === t) ?? null;
|
|
5
|
+
}
|
|
6
|
+
function c(e, t) {
|
|
7
|
+
let n = s(e, t);
|
|
8
|
+
return n && a(n.kind) ? n.id : null;
|
|
9
|
+
}
|
|
10
|
+
function l(e, t) {
|
|
11
|
+
let n = c(e, t);
|
|
12
|
+
return e.items.filter((t) => t.id !== n && c(e, t.parent) === n);
|
|
13
|
+
}
|
|
14
|
+
function u(e, t) {
|
|
15
|
+
return l(e, t).length;
|
|
16
|
+
}
|
|
17
|
+
var d = (e, t) => c(e, s(e, t)?.parent ?? null);
|
|
18
|
+
function f(e, t, n) {
|
|
19
|
+
let r = /* @__PURE__ */ new Set(), i = d(e, t);
|
|
20
|
+
for (; i != null && !r.has(i);) {
|
|
21
|
+
if (i === n) return !0;
|
|
22
|
+
r.add(i), i = d(e, i);
|
|
23
|
+
}
|
|
24
|
+
return !1;
|
|
25
|
+
}
|
|
26
|
+
function p(e, t) {
|
|
27
|
+
let n = [], r = /* @__PURE__ */ new Set(), i = c(e, t ?? null);
|
|
28
|
+
for (; i != null && !r.has(i);) n.push(i), r.add(i), i = d(e, i);
|
|
29
|
+
return n;
|
|
30
|
+
}
|
|
31
|
+
function m(e, t) {
|
|
32
|
+
return t == null ? !1 : t === "trash" || f(e, t, "trash");
|
|
33
|
+
}
|
|
34
|
+
function h(e, t) {
|
|
35
|
+
let n = [], r = /* @__PURE__ */ new Set(), i = (t) => {
|
|
36
|
+
for (let o of l(e, t)) r.has(o.id) || (r.add(o.id), n.push(o), a(o.kind) && i(o.id));
|
|
37
|
+
};
|
|
38
|
+
return i(c(e, t)), n;
|
|
39
|
+
}
|
|
40
|
+
function g(e, t) {
|
|
41
|
+
let i = new Set(l(e, t).filter((e) => e.kind === n).map((e) => e.name));
|
|
42
|
+
if (!i.has("untitled folder")) return r;
|
|
43
|
+
for (let e = 2;; e++) {
|
|
44
|
+
let t = `${r} ${e}`;
|
|
45
|
+
if (!i.has(t)) return t;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var _ = (e) => e.replace(/ copy( \d+)?$/, "");
|
|
49
|
+
function v(e, t, n, r) {
|
|
50
|
+
let i = new Set(l(e, t).filter((e) => e.kind === r).map((e) => e.name));
|
|
51
|
+
if (!i.has(n)) return n;
|
|
52
|
+
let a = _(n);
|
|
53
|
+
if (!i.has(`${a} copy`)) return `${a} copy`;
|
|
54
|
+
for (let e = 2;; e++) {
|
|
55
|
+
let t = `${a} copy ${e}`;
|
|
56
|
+
if (!i.has(t)) return t;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
var y = (e, t) => e.createdAt - t.createdAt || (e.id < t.id ? -1 : 1);
|
|
60
|
+
function b(e, t) {
|
|
61
|
+
let n = { ...e };
|
|
62
|
+
return t ? (n.left = t.left, n.top = t.top) : (delete n.left, delete n.top), n;
|
|
63
|
+
}
|
|
64
|
+
function x(n) {
|
|
65
|
+
let { storage: r, volumes: l = {}, listed: u = () => !0, hooks: d = () => void 0, placeDelay: p = 400 } = n, _ = () => (n.now ?? Date.now)(), x = () => n.newId ? n.newId() : crypto.randomUUID(), S = [];
|
|
66
|
+
l.disk && S.push({
|
|
67
|
+
id: t,
|
|
68
|
+
name: l.disk,
|
|
69
|
+
kind: t,
|
|
70
|
+
parent: null,
|
|
71
|
+
createdAt: 0,
|
|
72
|
+
modifiedAt: 0
|
|
73
|
+
}), l.trash !== !1 && S.push({
|
|
74
|
+
id: e,
|
|
75
|
+
name: l.trash ?? "Trash",
|
|
76
|
+
kind: e,
|
|
77
|
+
parent: null,
|
|
78
|
+
createdAt: 0,
|
|
79
|
+
modifiedAt: 0
|
|
80
|
+
});
|
|
81
|
+
let C = /* @__PURE__ */ new Map(), w = !1, T = !1, E = O(), D = /* @__PURE__ */ new Set();
|
|
82
|
+
function O() {
|
|
83
|
+
let e = S.map((e) => {
|
|
84
|
+
let t = C.get(e.id);
|
|
85
|
+
return t?.left != null && t.top != null ? {
|
|
86
|
+
...e,
|
|
87
|
+
left: t.left,
|
|
88
|
+
top: t.top
|
|
89
|
+
} : e;
|
|
90
|
+
}), t = [...C.values()].filter((e) => !o(e.id) && (e.kind === "folder" || u(e.kind))).sort(y);
|
|
91
|
+
return {
|
|
92
|
+
available: w,
|
|
93
|
+
items: [...e, ...t]
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function k() {
|
|
97
|
+
E = O();
|
|
98
|
+
for (let e of [...D]) e(E);
|
|
99
|
+
}
|
|
100
|
+
let A = () => {
|
|
101
|
+
if (!r) throw Error("storage is unavailable");
|
|
102
|
+
return r;
|
|
103
|
+
};
|
|
104
|
+
async function j(e) {
|
|
105
|
+
await A().put(e), C.set(e.id, e);
|
|
106
|
+
}
|
|
107
|
+
let M = /* @__PURE__ */ new Set(), N = null;
|
|
108
|
+
async function P() {
|
|
109
|
+
N && clearTimeout(N), N = null;
|
|
110
|
+
let e = [...M];
|
|
111
|
+
if (M.clear(), !(!r || !w)) for (let t of e) {
|
|
112
|
+
let e = C.get(t);
|
|
113
|
+
e && await r.put(e).catch(() => {});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
let F = (e) => {
|
|
117
|
+
let t = S.find((t) => t.id === e);
|
|
118
|
+
return C.get(e) ?? {
|
|
119
|
+
...t,
|
|
120
|
+
name: ""
|
|
121
|
+
};
|
|
122
|
+
}, I = {
|
|
123
|
+
get: () => E,
|
|
124
|
+
subscribe(e) {
|
|
125
|
+
return D.add(e), () => {
|
|
126
|
+
D.delete(e);
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
item: (e) => s(E, e),
|
|
130
|
+
async refresh() {
|
|
131
|
+
if (!r) {
|
|
132
|
+
C = /* @__PURE__ */ new Map(), w = !1, k();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
let e = await r.list();
|
|
137
|
+
C = /* @__PURE__ */ new Map(), T = !1;
|
|
138
|
+
for (let t of e) t.id === i ? T = !0 : C.set(t.id, t);
|
|
139
|
+
w = !0;
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.warn("vintage-frames/shell: the catalog could not be read —", e), C = /* @__PURE__ */ new Map(), w = !1;
|
|
142
|
+
}
|
|
143
|
+
k();
|
|
144
|
+
},
|
|
145
|
+
async seed(e) {
|
|
146
|
+
return !r || !w || T ? !1 : (await e(I), await r.put({
|
|
147
|
+
id: i,
|
|
148
|
+
name: "",
|
|
149
|
+
kind: i,
|
|
150
|
+
parent: null,
|
|
151
|
+
createdAt: _(),
|
|
152
|
+
modifiedAt: _()
|
|
153
|
+
}), T = !0, !0);
|
|
154
|
+
},
|
|
155
|
+
async create(e = {}) {
|
|
156
|
+
let t = c(E, e.parent ?? null);
|
|
157
|
+
if (m(E, t)) return null;
|
|
158
|
+
let n = e.kind ?? "folder", r = e.at ?? _(), i = {
|
|
159
|
+
id: x(),
|
|
160
|
+
name: e.name ?? (n === "folder" ? g(E, t) : "untitled"),
|
|
161
|
+
kind: n,
|
|
162
|
+
parent: t,
|
|
163
|
+
createdAt: r,
|
|
164
|
+
modifiedAt: r
|
|
165
|
+
};
|
|
166
|
+
return e.data !== void 0 && (i.data = e.data), e.left != null && e.top != null && (i = b(i, {
|
|
167
|
+
left: e.left,
|
|
168
|
+
top: e.top
|
|
169
|
+
})), await j(i), k(), i;
|
|
170
|
+
},
|
|
171
|
+
async rename(e, t) {
|
|
172
|
+
let n = C.get(e);
|
|
173
|
+
return !n || o(e) || n.name === t ? !1 : (await j({
|
|
174
|
+
...n,
|
|
175
|
+
name: t,
|
|
176
|
+
modifiedAt: _()
|
|
177
|
+
}), k(), !0);
|
|
178
|
+
},
|
|
179
|
+
async update(e, t) {
|
|
180
|
+
let n = C.get(e);
|
|
181
|
+
return !n || o(e) ? !1 : (await j({
|
|
182
|
+
...n,
|
|
183
|
+
data: t,
|
|
184
|
+
modifiedAt: _()
|
|
185
|
+
}), k(), !0);
|
|
186
|
+
},
|
|
187
|
+
async move(e, t, n) {
|
|
188
|
+
let r = c(E, t), i = [];
|
|
189
|
+
for (let t of e) {
|
|
190
|
+
let e = C.get(t);
|
|
191
|
+
!e || o(t) || r === t || r != null && a(e.kind) && f(E, r, t) || c(E, e.parent) !== r && (M.delete(t), await j(b({
|
|
192
|
+
...e,
|
|
193
|
+
parent: r
|
|
194
|
+
}, n?.get(t) ?? null)), i.push(t));
|
|
195
|
+
}
|
|
196
|
+
return i.length && k(), i;
|
|
197
|
+
},
|
|
198
|
+
place(e) {
|
|
199
|
+
let t = !1;
|
|
200
|
+
for (let [n, r] of e) {
|
|
201
|
+
let e = o(n) ? S.some((e) => e.id === n) ? F(n) : null : C.get(n);
|
|
202
|
+
!e || e.left === r.left && e.top === r.top || (C.set(n, b(e, r)), M.add(n), t = !0);
|
|
203
|
+
}
|
|
204
|
+
t && (k(), N && clearTimeout(N), N = setTimeout(() => void P(), p));
|
|
205
|
+
},
|
|
206
|
+
flush: P,
|
|
207
|
+
async copy(e, t) {
|
|
208
|
+
let n = c(E, t);
|
|
209
|
+
if (m(E, n)) return [];
|
|
210
|
+
let r = [];
|
|
211
|
+
for (let t of e) {
|
|
212
|
+
let e = s(E, t);
|
|
213
|
+
if (!e || o(t)) continue;
|
|
214
|
+
let i = a(e.kind) ? h(E, t) : [], l = /* @__PURE__ */ new Map(), u = async (e, t, n) => {
|
|
215
|
+
let r = _(), i = {
|
|
216
|
+
...e,
|
|
217
|
+
id: x(),
|
|
218
|
+
name: n ? v(E, t, e.name, e.kind) : e.name,
|
|
219
|
+
parent: t,
|
|
220
|
+
createdAt: r,
|
|
221
|
+
modifiedAt: r
|
|
222
|
+
};
|
|
223
|
+
return n && (i = b(i, null)), l.set(e.id, i.id), await j(i), await d(e.kind)?.copy?.(e, i), i;
|
|
224
|
+
};
|
|
225
|
+
r.push(await u(e, n, !0));
|
|
226
|
+
for (let e of i) await u(e, l.get(c(E, e.parent) ?? "") ?? null, !1);
|
|
227
|
+
k();
|
|
228
|
+
}
|
|
229
|
+
return r;
|
|
230
|
+
},
|
|
231
|
+
async emptyTrash() {
|
|
232
|
+
let t = h(E, e);
|
|
233
|
+
for (let e of t) await A().remove(e.id), C.delete(e.id), M.delete(e.id), await d(e.kind)?.onRemove?.(e);
|
|
234
|
+
return t.length && k(), t;
|
|
235
|
+
},
|
|
236
|
+
trashSize() {
|
|
237
|
+
return h(E, e).reduce((e, t) => e + (d(t.kind)?.size?.(t) ?? 0), 0);
|
|
238
|
+
},
|
|
239
|
+
dump() {
|
|
240
|
+
return { items: [...C.values()] };
|
|
241
|
+
},
|
|
242
|
+
async clear() {
|
|
243
|
+
if (r) {
|
|
244
|
+
for (let e of await r.list()) e.id !== i && await r.remove(e.id);
|
|
245
|
+
C = /* @__PURE__ */ new Map(), M.clear(), k();
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
async import(e, { mode: t = "merge" } = {}) {
|
|
249
|
+
let n = e.items.filter((e) => e.id !== i && !o(e.id));
|
|
250
|
+
if (!n.length || !r) return 0;
|
|
251
|
+
let a = t === "replace", s = /* @__PURE__ */ new Map();
|
|
252
|
+
for (let e of n) s.set(e.id, a ? e.id : x());
|
|
253
|
+
let c = (e) => e == null || o(e) ? e : s.get(e) ?? null;
|
|
254
|
+
a && await I.clear();
|
|
255
|
+
for (let e of n) await j({
|
|
256
|
+
...e,
|
|
257
|
+
id: s.get(e.id),
|
|
258
|
+
parent: c(e.parent)
|
|
259
|
+
});
|
|
260
|
+
return k(), n.length;
|
|
261
|
+
},
|
|
262
|
+
async dispose() {
|
|
263
|
+
await P(), D.clear();
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
return I;
|
|
267
|
+
}
|
|
268
|
+
function S(e = []) {
|
|
269
|
+
let t = new Map(e.map((e) => [e.id, e]));
|
|
270
|
+
return {
|
|
271
|
+
records: t,
|
|
272
|
+
list: async () => [...t.values()],
|
|
273
|
+
put: async (e) => t.set(e.id, e),
|
|
274
|
+
remove: async (e) => t.delete(e)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
//#endregion
|
|
278
|
+
export { t as DISK, n as FOLDER, e as TRASH, r as UNTITLED_FOLDER, l as childrenOf, c as containerOf, v as copyName, x as createCatalog, h as descendantsOf, p as enclosingFolders, a as isContainerKind, f as isInside, m as isTrashed, o as isVolume, u as itemCount, s as itemOf, S as memoryStorage, g as nextFolderName };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The menu bar's clock: a label in the bar's `end` slot showing the time,
|
|
3
|
+
* updated on the minute. A press shows the date for a few seconds; a second
|
|
4
|
+
* press returns to the time early.
|
|
5
|
+
*/
|
|
6
|
+
/** How long a press shows the date, ms. */
|
|
7
|
+
export declare const DATE_HOLD_MS = 3000;
|
|
8
|
+
/** "7:27 PM": 12-hour local time. */
|
|
9
|
+
export declare function formatTime(d: Date): string;
|
|
10
|
+
/** "9/27/26": M/D/YY local date. */
|
|
11
|
+
export declare function formatDate(d: Date): string;
|
|
12
|
+
/** Render the clock into `label` now and on every minute boundary. Returns the stop function. */
|
|
13
|
+
export declare function startClock(label: HTMLElement, { now, holdMs }?: {
|
|
14
|
+
now?: () => number;
|
|
15
|
+
holdMs?: number;
|
|
16
|
+
}): () => void;
|