vintage-frames 0.14.1 → 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/custom-elements.json +94 -14
- package/dist/components/vf-button.d.ts +5 -1
- package/dist/components/vf-button.js +1 -1
- package/dist/components/vf-dialog.d.ts +7 -1
- package/dist/events.d.ts +3 -2
- package/dist/events.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/modal-dialog.d.ts +31 -4
- package/dist/modal-dialog.js +98 -82
- package/dist/position.js +25 -20
- package/dist/shell/alert.d.ts +17 -9
- package/dist/shell/alert.js +25 -28
- package/dist/shell/finder.d.ts +41 -1
- package/dist/shell/finder.js +212 -178
- package/dist/shell/index.d.ts +1 -3
- package/dist/shell/index.js +5 -6
- package/dist/shell/shell.d.ts +25 -11
- package/dist/shell/shell.js +143 -122
- package/dist/shell/windows.d.ts +21 -2
- package/dist/shell/windows.js +169 -125
- package/dist/text-control.d.ts +6 -0
- package/dist/text-control.js +2 -2
- package/docs/SPEC.md +5 -5
- package/editor/vscode.html-custom-data.json +2 -2
- package/editor/web-types.json +11 -6
- package/package.json +1 -1
package/dist/shell/windows.js
CHANGED
|
@@ -27,19 +27,29 @@ async function g(e, { when: t } = {}) {
|
|
|
27
27
|
for (let t of n) if (!(t.closest("[hidden]") || !t.checkVisibility()) && !(t.tabIndex < 0 && !t.localName.startsWith("vf-")) && (t.focus({ preventScroll: !0 }), h(e))) return !0;
|
|
28
28
|
return !1;
|
|
29
29
|
}
|
|
30
|
-
function
|
|
31
|
-
let { defaultApp:
|
|
32
|
-
e.addEventListener(t, n),
|
|
33
|
-
},
|
|
30
|
+
function ee(m, ee) {
|
|
31
|
+
let { defaultApp: te, savedPin: ne = () => null } = ee, _ = [], v = (e, t, n) => {
|
|
32
|
+
e.addEventListener(t, n), _.push(() => e.removeEventListener(t, n));
|
|
33
|
+
}, y = () => ({
|
|
34
34
|
width: m.width,
|
|
35
35
|
height: m.height
|
|
36
|
-
}),
|
|
37
|
-
if (!
|
|
38
|
-
},
|
|
39
|
-
for (let e of [...
|
|
40
|
-
},
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
}), b = () => m.windowArea, x = /* @__PURE__ */ new Map(), S = /* @__PURE__ */ new Map(), C = /* @__PURE__ */ new Map(), w = /* @__PURE__ */ new Map(), T = /* @__PURE__ */ new Map(), E = /* @__PURE__ */ new WeakMap(), D = {}, O = () => a(b().top, D), k = /* @__PURE__ */ new Set(), A = !1, j = () => {
|
|
37
|
+
if (!A) for (let e of [...k]) e();
|
|
38
|
+
}, M = /* @__PURE__ */ new Set(), N = () => {
|
|
39
|
+
for (let e of [...M]) e();
|
|
40
|
+
}, P = /* @__PURE__ */ new Set(), F = /* @__PURE__ */ new Set(), I = /* @__PURE__ */ new Set(), L = /* @__PURE__ */ new Set(), R = /* @__PURE__ */ new Map(), z = [], re = (e) => {
|
|
41
|
+
for (let t = e; t; t = t.parentElement) if (R.has(t)) return t;
|
|
42
|
+
return null;
|
|
43
|
+
}, B = (e) => e.isConnected && (e.matches("vf-dialog[open]") || !!e.querySelector("vf-dialog[open]")), V = () => {
|
|
44
|
+
let e = z.filter((e) => R.has(e) && B(e));
|
|
45
|
+
for (let t of R.keys()) !e.includes(t) && B(t) && e.push(t);
|
|
46
|
+
if (!(e.length === z.length && e.every((e, t) => e === z[t]))) {
|
|
47
|
+
z = e;
|
|
48
|
+
for (let e of [...L]) e();
|
|
49
|
+
}
|
|
50
|
+
}, H = () => null, U = () => !1;
|
|
51
|
+
function W(n, r) {
|
|
52
|
+
let i = b(), a = t(n), o = (e) => Math.floor(e / a) * a, s = Math.ceil(i.top / a) * a, c = i.left + i.width, l = i.top + i.height, { width: u, height: d } = r;
|
|
43
53
|
n.resizable && (u != null && Number.isFinite(u) && (u = Math.min(u, o(i.width))), d != null && Number.isFinite(d) && (d = Math.min(d, o(Math.max(0, l - s)))));
|
|
44
54
|
let p = u ?? n.width ?? 0, m = d ?? n.height ?? 0;
|
|
45
55
|
return {
|
|
@@ -49,13 +59,13 @@ function _(m, _) {
|
|
|
49
59
|
height: d
|
|
50
60
|
};
|
|
51
61
|
}
|
|
52
|
-
let
|
|
53
|
-
t.left = e(n.left, t), t.top = e(n.top, t), n.width != null && Number.isFinite(n.width) && (t.width = n.width), n.height != null && Number.isFinite(n.height) && (t.height = n.height),
|
|
54
|
-
},
|
|
55
|
-
let t =
|
|
56
|
-
return t ?
|
|
57
|
-
},
|
|
58
|
-
let n =
|
|
62
|
+
let G = (t, n) => {
|
|
63
|
+
t.left = e(n.left, t), t.top = e(n.top, t), n.width != null && Number.isFinite(n.width) && (t.width = n.width), n.height != null && Number.isFinite(n.height) && (t.height = n.height), E.delete(t);
|
|
64
|
+
}, K = (e) => {
|
|
65
|
+
let t = x.get(e)?.place;
|
|
66
|
+
return t ? W(e, t(b())) : null;
|
|
67
|
+
}, q = (e, t) => {
|
|
68
|
+
let n = x.get(e)?.policy;
|
|
59
69
|
if (n) return n(t);
|
|
60
70
|
if (!e.resizable) return { size: {
|
|
61
71
|
width: t.width,
|
|
@@ -66,7 +76,7 @@ function _(m, _) {
|
|
|
66
76
|
width: r,
|
|
67
77
|
height: i
|
|
68
78
|
} };
|
|
69
|
-
},
|
|
79
|
+
}, J = (e, t, n = q(e, p(e))) => W(e, c(t, y(), O(), n)), ie = (e, t) => {
|
|
70
80
|
let n = p(e);
|
|
71
81
|
return [
|
|
72
82
|
"left",
|
|
@@ -77,71 +87,71 @@ function _(m, _) {
|
|
|
77
87
|
let r = t[e];
|
|
78
88
|
return r == null || !Number.isFinite(r) || n[e] === r;
|
|
79
89
|
});
|
|
80
|
-
},
|
|
81
|
-
let t = [...
|
|
82
|
-
return r(l(
|
|
83
|
-
},
|
|
84
|
-
function
|
|
90
|
+
}, ae = (e) => {
|
|
91
|
+
let t = [...x].filter(([t, n]) => t !== e && !t.hidden && !n.palette).map(([e]) => p(e));
|
|
92
|
+
return r(l(b()), t).slot;
|
|
93
|
+
}, Y = te, X = (e) => e instanceof n && x.get(e)?.app || te;
|
|
94
|
+
function oe() {
|
|
85
95
|
let e = !1;
|
|
86
|
-
for (let [t, n] of
|
|
96
|
+
for (let [t, n] of x) {
|
|
87
97
|
if (!n.palette) continue;
|
|
88
|
-
let r = n.app ===
|
|
98
|
+
let r = n.app === Y && n.palette();
|
|
89
99
|
t.hidden !== !r && (t.hidden = !r, r && m.bringToFront(t), e = !0);
|
|
90
100
|
}
|
|
91
|
-
e &&
|
|
101
|
+
e && j();
|
|
92
102
|
}
|
|
93
|
-
let
|
|
103
|
+
let se = (e) => {
|
|
94
104
|
let t = document.activeElement?.closest("vf-window");
|
|
95
|
-
!(t instanceof n) || t === e || !
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
for (let t of [...
|
|
99
|
-
let t =
|
|
100
|
-
if (t !==
|
|
101
|
-
|
|
102
|
-
for (let e of [...
|
|
105
|
+
!(t instanceof n) || t === e || !x.has(t) || x.get(t).palette || (e ? Z(e) : U(null, null));
|
|
106
|
+
}, Z = (e) => void g(e, { when: () => m.activeWindow === e }), ce = (e) => {
|
|
107
|
+
se(e);
|
|
108
|
+
for (let t of [...F]) t(e);
|
|
109
|
+
let t = X(e);
|
|
110
|
+
if (t !== Y) {
|
|
111
|
+
Y = t, oe();
|
|
112
|
+
for (let e of [...I]) e(Y);
|
|
103
113
|
}
|
|
104
114
|
};
|
|
105
|
-
|
|
115
|
+
v(m, "vf-activate", (e) => ce(e.detail.window)), v(m, "vf-close", (e) => {
|
|
106
116
|
let t = e.target;
|
|
107
|
-
t instanceof n &&
|
|
117
|
+
t instanceof n && x.has(t) && $.requestClose(t);
|
|
108
118
|
});
|
|
109
|
-
let
|
|
110
|
-
!(e.target instanceof n) || !
|
|
111
|
-
|
|
119
|
+
let Q = 0, le = (e) => {
|
|
120
|
+
!(e.target instanceof n) || !x.has(e.target) || Q || (Q = window.setTimeout(() => {
|
|
121
|
+
Q = 0, j();
|
|
112
122
|
}, 0));
|
|
113
123
|
};
|
|
114
|
-
m.addEventListener("vf-placement-change",
|
|
115
|
-
e.detail?.commit &&
|
|
124
|
+
m.addEventListener("vf-placement-change", le), _.push(() => m.removeEventListener("vf-placement-change", le)), v(m, "vf-resize", (e) => {
|
|
125
|
+
e.detail?.commit && j();
|
|
116
126
|
});
|
|
117
|
-
let
|
|
118
|
-
let t =
|
|
119
|
-
t && (
|
|
120
|
-
},
|
|
121
|
-
let l = p(n), u =
|
|
127
|
+
let ue = (e) => {
|
|
128
|
+
let t = x.get(e);
|
|
129
|
+
t && (x.delete(e), E.delete(e), t.palette || N(), j());
|
|
130
|
+
}, de = (n, r, i, a) => {
|
|
131
|
+
let l = p(n), u = x.get(n);
|
|
122
132
|
if (u?.keep && o(l, u.keep(a))) {
|
|
123
|
-
|
|
133
|
+
G(n, W(n, u.keep(b())));
|
|
124
134
|
return;
|
|
125
135
|
}
|
|
126
|
-
let d =
|
|
136
|
+
let d = E.get(n), f = !!d && n.resizable && (d.width !== l.width || d.height !== l.height), m = !d || d.left !== l.left || d.top !== l.top || f || !d ? s(l, r, O()) : d.pin, h = c(m, i, O(), q(n, l));
|
|
127
137
|
if (n.left = e(h.left, n), n.top = e(h.top, n), n.resizable) {
|
|
128
|
-
let r = t(n), i =
|
|
138
|
+
let r = t(n), i = b(), a = Math.ceil(i.top / r) * r, o = Math.floor(i.width / r) * r, s = Math.floor(Math.max(0, i.top + i.height - a) / r) * r;
|
|
129
139
|
n.width = Math.min(e(h.width, n), o), n.height = Math.min(e(h.height, n), s);
|
|
130
140
|
}
|
|
131
|
-
|
|
141
|
+
E.set(n, {
|
|
132
142
|
pin: m,
|
|
133
143
|
...p(n)
|
|
134
144
|
});
|
|
135
145
|
}, $ = {
|
|
136
146
|
get area() {
|
|
137
|
-
return
|
|
147
|
+
return b();
|
|
138
148
|
},
|
|
139
149
|
get front() {
|
|
140
|
-
return
|
|
150
|
+
return Y;
|
|
141
151
|
},
|
|
142
152
|
adopt(e, t) {
|
|
143
153
|
let n = t.palette;
|
|
144
|
-
if (
|
|
154
|
+
if (x.set(e, {
|
|
145
155
|
app: t.app,
|
|
146
156
|
item: t.item ?? null,
|
|
147
157
|
place: t.place ?? null,
|
|
@@ -149,151 +159,180 @@ function _(m, _) {
|
|
|
149
159
|
keep: t.keep ?? null,
|
|
150
160
|
palette: n ? n === !0 ? () => !0 : n : null,
|
|
151
161
|
close: t.close ?? null
|
|
152
|
-
}), t.pin)
|
|
162
|
+
}), t.pin) G(e, J(e, t.pin));
|
|
153
163
|
else {
|
|
154
|
-
let t =
|
|
155
|
-
t &&
|
|
164
|
+
let t = K(e);
|
|
165
|
+
t && G(e, t);
|
|
156
166
|
}
|
|
157
167
|
if (n) {
|
|
158
|
-
let t =
|
|
159
|
-
e.hidden = !(t.app ===
|
|
168
|
+
let t = x.get(e);
|
|
169
|
+
e.hidden = !(t.app === Y && t.palette());
|
|
160
170
|
}
|
|
161
|
-
m.activeWindow === e &&
|
|
171
|
+
m.activeWindow === e && ce(e), n || N(), j();
|
|
162
172
|
},
|
|
163
173
|
open(e) {
|
|
164
174
|
let t = e.item == null ? null : $.windowFor(e.item);
|
|
165
|
-
if (t) return m.bringToFront(t),
|
|
175
|
+
if (t) return m.bringToFront(t), Z(t), t;
|
|
166
176
|
let n = e.create(), r = {
|
|
167
177
|
width: n.width ?? 0,
|
|
168
178
|
height: n.height ?? 0
|
|
169
|
-
}, a =
|
|
179
|
+
}, a = ae();
|
|
170
180
|
m.append(n);
|
|
171
181
|
let o = e.item ?? null;
|
|
172
182
|
return $.adopt(n, {
|
|
173
183
|
app: e.app,
|
|
174
184
|
item: o,
|
|
175
185
|
place: e.place ?? ((e) => i(e, r, l(e), a)),
|
|
176
|
-
pin: o == null ? null :
|
|
186
|
+
pin: o == null ? null : C.get(o)?.pin ?? ne(o),
|
|
177
187
|
policy: e.policy ?? null,
|
|
178
188
|
keep: e.keep ?? null,
|
|
179
189
|
close: e.close ?? null
|
|
180
|
-
}), o != null &&
|
|
190
|
+
}), o != null && C.delete(o), m.bringToFront(n), e.from && n.show({ from: e.from }), Z(n), n;
|
|
181
191
|
},
|
|
182
192
|
close(e, { to: t } = {}) {
|
|
183
|
-
let n =
|
|
184
|
-
r != null && i != null &&
|
|
193
|
+
let n = x.get(e), r = n?.item ?? null, i = n?.app ?? null, a = t === void 0 ? i == null ? null : H(r, i) : t, o = m.activeWindow === e || h(e);
|
|
194
|
+
r != null && i != null && C.set(r, {
|
|
185
195
|
app: i,
|
|
186
|
-
pin: s(p(e),
|
|
196
|
+
pin: s(p(e), y(), O())
|
|
187
197
|
});
|
|
188
198
|
let c = e.hide({ to: a });
|
|
189
|
-
r != null && u(
|
|
199
|
+
r != null && u(w, r), i != null && !n?.palette && u(T, i);
|
|
190
200
|
let l = () => {
|
|
191
|
-
r != null && d(
|
|
201
|
+
r != null && d(w, r), i != null && !n?.palette && d(T, i), N();
|
|
192
202
|
};
|
|
193
|
-
return c.then(l, l),
|
|
203
|
+
return c.then(l, l), ue(e), e.remove(), o && queueMicrotask(() => {
|
|
194
204
|
let e = m.activeWindow;
|
|
195
|
-
e ?
|
|
205
|
+
e ? Z(e) : U(r, i) || U(null, null);
|
|
196
206
|
}), c;
|
|
197
207
|
},
|
|
198
208
|
requestClose(e) {
|
|
199
|
-
let t =
|
|
209
|
+
let t = x.get(e)?.close;
|
|
200
210
|
t ? t(e) : $.close(e);
|
|
201
211
|
},
|
|
202
212
|
windowFor(e) {
|
|
203
|
-
for (let [t, n] of
|
|
213
|
+
for (let [t, n] of x) if (n.item === e) return t;
|
|
204
214
|
return null;
|
|
205
215
|
},
|
|
206
216
|
windowsOf(e) {
|
|
207
|
-
return m.stackingOrder.filter((t) => t instanceof n &&
|
|
217
|
+
return m.stackingOrder.filter((t) => t instanceof n && x.get(t)?.app === e);
|
|
208
218
|
},
|
|
209
219
|
itemOf(e) {
|
|
210
|
-
return e instanceof n ?
|
|
220
|
+
return e instanceof n ? x.get(e)?.item ?? null : null;
|
|
211
221
|
},
|
|
212
222
|
appOf(e) {
|
|
213
|
-
|
|
223
|
+
if (e instanceof n) return x.get(e)?.app ?? null;
|
|
224
|
+
let t = re(e);
|
|
225
|
+
return t ? R.get(t).app : null;
|
|
226
|
+
},
|
|
227
|
+
holdDialog(e, { app: t }) {
|
|
228
|
+
let n = R.get(e);
|
|
229
|
+
if (n) n.app = t;
|
|
230
|
+
else {
|
|
231
|
+
let n = new MutationObserver(V);
|
|
232
|
+
n.observe(e, {
|
|
233
|
+
attributes: !0,
|
|
234
|
+
attributeFilter: ["open"],
|
|
235
|
+
subtree: !0
|
|
236
|
+
}), R.set(e, {
|
|
237
|
+
app: t,
|
|
238
|
+
observer: n
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
m.contains(e) || m.append(e), V();
|
|
242
|
+
},
|
|
243
|
+
releaseDialog(e) {
|
|
244
|
+
let t = R.get(e);
|
|
245
|
+
t && (t.observer.disconnect(), R.delete(e), V());
|
|
246
|
+
},
|
|
247
|
+
dialogsOf(e) {
|
|
248
|
+
return [...R].filter(([, t]) => t.app === e).map(([e]) => e);
|
|
249
|
+
},
|
|
250
|
+
get asking() {
|
|
251
|
+
let e = z[z.length - 1];
|
|
252
|
+
return e ? R.get(e).app : null;
|
|
214
253
|
},
|
|
215
254
|
isPalette(e) {
|
|
216
|
-
return e instanceof n && !!
|
|
255
|
+
return e instanceof n && !!x.get(e)?.palette;
|
|
217
256
|
},
|
|
218
257
|
setItem(e, t) {
|
|
219
|
-
let n =
|
|
220
|
-
!n || n.item === t || (n.item = t,
|
|
258
|
+
let n = x.get(e);
|
|
259
|
+
!n || n.item === t || (n.item = t, N());
|
|
221
260
|
},
|
|
222
261
|
isOpen(e) {
|
|
223
|
-
return
|
|
262
|
+
return w.has(e) || $.windowFor(e) !== null;
|
|
224
263
|
},
|
|
225
264
|
hasWindows(e) {
|
|
226
|
-
if (
|
|
227
|
-
for (let [, t] of
|
|
265
|
+
if (T.has(e)) return !0;
|
|
266
|
+
for (let [, t] of x) if (t.app === e && !t.palette) return !0;
|
|
228
267
|
return !1;
|
|
229
268
|
},
|
|
230
269
|
palettesChanged() {
|
|
231
|
-
|
|
270
|
+
oe();
|
|
232
271
|
},
|
|
233
272
|
write(e, t) {
|
|
234
|
-
|
|
273
|
+
G(e, t), j();
|
|
235
274
|
},
|
|
236
275
|
clamped(e, t) {
|
|
237
|
-
return
|
|
276
|
+
return W(e, t);
|
|
238
277
|
},
|
|
239
278
|
placed(e) {
|
|
240
|
-
return
|
|
279
|
+
return K(e);
|
|
241
280
|
},
|
|
242
281
|
pinOf(e) {
|
|
243
|
-
return s(p(e),
|
|
282
|
+
return s(p(e), y(), O());
|
|
244
283
|
},
|
|
245
284
|
fromPin(e, t, n) {
|
|
246
|
-
return
|
|
285
|
+
return J(e, t, n);
|
|
247
286
|
},
|
|
248
287
|
setFrameBands(e) {
|
|
249
|
-
|
|
288
|
+
D = { ...e }, E = /* @__PURE__ */ new WeakMap();
|
|
250
289
|
},
|
|
251
290
|
arrangeWith(e, t) {
|
|
252
|
-
return
|
|
253
|
-
|
|
291
|
+
return S.set(e, t), j(), () => {
|
|
292
|
+
S.get(e) === t && S.delete(e), j();
|
|
254
293
|
};
|
|
255
294
|
},
|
|
256
295
|
arrange() {
|
|
257
|
-
|
|
296
|
+
A = !0;
|
|
258
297
|
try {
|
|
259
|
-
for (let e of
|
|
260
|
-
for (let e of
|
|
261
|
-
let t =
|
|
262
|
-
t &&
|
|
298
|
+
for (let e of S.values()) e.arrange();
|
|
299
|
+
for (let e of x.keys()) {
|
|
300
|
+
let t = K(e);
|
|
301
|
+
t && G(e, t);
|
|
263
302
|
}
|
|
264
303
|
} finally {
|
|
265
|
-
|
|
304
|
+
A = !1;
|
|
266
305
|
}
|
|
267
|
-
|
|
306
|
+
j();
|
|
268
307
|
},
|
|
269
308
|
arranged() {
|
|
270
|
-
for (let e of
|
|
271
|
-
for (let e of
|
|
272
|
-
let t =
|
|
273
|
-
if (t && !e.hidden && !
|
|
309
|
+
for (let e of S.values()) if (!e.arranged()) return !1;
|
|
310
|
+
for (let e of x.keys()) {
|
|
311
|
+
let t = K(e);
|
|
312
|
+
if (t && !e.hidden && !ie(e, t)) return !1;
|
|
274
313
|
}
|
|
275
314
|
return !0;
|
|
276
315
|
},
|
|
277
316
|
resized(e) {
|
|
278
|
-
let t =
|
|
317
|
+
let t = y();
|
|
279
318
|
if (e.width === t.width && e.height === t.height) return;
|
|
280
|
-
let n =
|
|
319
|
+
let n = b(), r = {
|
|
281
320
|
left: n.left,
|
|
282
321
|
top: n.top,
|
|
283
322
|
width: e.width - n.left,
|
|
284
323
|
height: Math.max(0, e.height - n.top)
|
|
285
324
|
};
|
|
286
|
-
for (let n of
|
|
287
|
-
for (let n of [...
|
|
288
|
-
|
|
325
|
+
for (let n of x.keys()) de(n, e, t, r);
|
|
326
|
+
for (let n of [...P]) n(e, t);
|
|
327
|
+
j();
|
|
289
328
|
},
|
|
290
329
|
snapshot() {
|
|
291
330
|
let e = {};
|
|
292
|
-
for (let [t, n] of
|
|
331
|
+
for (let [t, n] of C) e[t] = { ...n };
|
|
293
332
|
let t = m.stackingOrder;
|
|
294
|
-
for (let [n, r] of
|
|
333
|
+
for (let [n, r] of x) {
|
|
295
334
|
if (r.item == null) continue;
|
|
296
|
-
let i = s(p(n),
|
|
335
|
+
let i = s(p(n), y(), O());
|
|
297
336
|
e[r.item] = r.palette ? {
|
|
298
337
|
app: r.app,
|
|
299
338
|
pin: i
|
|
@@ -306,38 +345,43 @@ function _(m, _) {
|
|
|
306
345
|
return e;
|
|
307
346
|
},
|
|
308
347
|
rememberedPin(e) {
|
|
309
|
-
return
|
|
348
|
+
return C.get(e)?.pin ?? null;
|
|
310
349
|
},
|
|
311
350
|
layoutChanged() {
|
|
312
|
-
|
|
351
|
+
j();
|
|
313
352
|
},
|
|
314
353
|
setHome(e, t) {
|
|
315
|
-
return
|
|
316
|
-
|
|
354
|
+
return H = e, U = t, () => {
|
|
355
|
+
H = () => null, U = () => !1;
|
|
317
356
|
};
|
|
318
357
|
},
|
|
319
358
|
onLayout(e) {
|
|
320
|
-
return
|
|
359
|
+
return k.add(e), () => void k.delete(e);
|
|
321
360
|
},
|
|
322
361
|
onWindows(e) {
|
|
323
|
-
return
|
|
362
|
+
return M.add(e), () => void M.delete(e);
|
|
324
363
|
},
|
|
325
364
|
onRaster(e) {
|
|
326
|
-
return
|
|
365
|
+
return P.add(e), () => void P.delete(e);
|
|
327
366
|
},
|
|
328
367
|
beforeFront(e) {
|
|
329
|
-
return
|
|
368
|
+
return F.add(e), () => void F.delete(e);
|
|
330
369
|
},
|
|
331
370
|
onFront(e) {
|
|
332
|
-
return
|
|
371
|
+
return I.add(e), () => void I.delete(e);
|
|
372
|
+
},
|
|
373
|
+
onDialogs(e) {
|
|
374
|
+
return L.add(e), () => void L.delete(e);
|
|
333
375
|
},
|
|
334
376
|
dispose() {
|
|
335
|
-
clearTimeout(
|
|
336
|
-
for (let e of
|
|
337
|
-
|
|
377
|
+
clearTimeout(Q);
|
|
378
|
+
for (let e of _) e();
|
|
379
|
+
k.clear(), M.clear(), P.clear(), F.clear(), I.clear(), L.clear();
|
|
380
|
+
for (let { observer: e } of R.values()) e.disconnect();
|
|
381
|
+
R.clear(), z = [], S.clear(), x.clear();
|
|
338
382
|
}
|
|
339
383
|
};
|
|
340
|
-
return
|
|
384
|
+
return Y = X(m.activeWindow), $;
|
|
341
385
|
}
|
|
342
386
|
//#endregion
|
|
343
|
-
export {
|
|
387
|
+
export { ee as createWindowManager, m as deepActiveElement, g as focusInto };
|
package/dist/text-control.d.ts
CHANGED
|
@@ -117,6 +117,12 @@ export declare class VfTextControlBase extends VfShadowRoleControl {
|
|
|
117
117
|
* disabled default button means no submission at all, as in HTML; a form
|
|
118
118
|
* with no submit button falls back to the bare call, also as in HTML.
|
|
119
119
|
*
|
|
120
|
+
* One departure: a submit `vf-button variant="default"`, the button with
|
|
121
|
+
* the ring, is the default button when the form has one, ahead of the
|
|
122
|
+
* first in tree order. A Cancel/OK row puts Cancel first, and Return
|
|
123
|
+
* should press the button the ring marks — the one a modal's own Return
|
|
124
|
+
* rule presses (`VfModalDialog`).
|
|
125
|
+
*
|
|
120
126
|
* Returns whether a form owner took the press — true even when a disabled
|
|
121
127
|
* default button meant no submission, since HTML gave the press to the form
|
|
122
128
|
* either way. The callers cancel the keydown on true, so a modal shell
|
package/dist/text-control.js
CHANGED
|
@@ -57,8 +57,8 @@ var l = class extends a {
|
|
|
57
57
|
requestImplicitSubmit() {
|
|
58
58
|
let e = this.internals.form;
|
|
59
59
|
if (!e) return !1;
|
|
60
|
-
let t = [...e.elements].
|
|
61
|
-
return
|
|
60
|
+
let t = [...e.elements].filter((e) => e instanceof HTMLButtonElement && e.type === "submit" || e instanceof HTMLInputElement && (e.type === "submit" || e.type === "image") || e.localName === "vf-button" && !e.href && e.type?.toLowerCase() === "submit"), n = t.find((e) => e.localName === "vf-button" && e.variant === "default") ?? t[0];
|
|
61
|
+
return n ? (n.matches(":disabled") || n.click(), !0) : (e.requestSubmit(), !0);
|
|
62
62
|
}
|
|
63
63
|
emitValue(t, n = { value: this.value }) {
|
|
64
64
|
e(this, t, n);
|
package/docs/SPEC.md
CHANGED
|
@@ -46,7 +46,7 @@ Modern requirements that we deliberately keep (accessibility over purity):
|
|
|
46
46
|
- **The name/description bridge** (`VfShadowRoleControl`, src/form-control.ts). On the controls whose role lives on a shadow-internal node (the three fields, `vf-select`, `vf-swatch`, `vf-button`), a host-level `aria-labelledby`, `aria-label` or associated `<label for>` resolves — in html-aam precedence — to the inner focusable element's `aria-label` whenever the `label` property is empty (`hostLabel`). Those six extend `VfShadowRoleControl`; the host-role controls (`vf-checkbox`, `vf-radio-group`, `vf-slider`) extend the plain `VfFormControl` and so never *carry* the bridge's API at all — an inherited `description` that renders nothing is the advertised-but-inert shape this split removes, and `verify:manifest` enforces it (a tag whose manifest lists `description` must call `renderDescription()`). `vf-button` takes the ARIA half alone (`hostAriaLabel`): a `<button>` is not a labelable element, so no caption names a native one and none names this one either — its slotted content is the name a bridge doesn't override. A `description` property (or, when it's empty, a host-level `aria-describedby`) renders as a hidden span in the control's own shadow root with the inner control's `aria-describedby` pointing at it — the shadow-internal IDREF idiom `vf-dialog`'s title patch uses — and a failing constraint's `validationMessage` joins the same node. Referenced text is flattened at render time, so an edit to a referenced element's *text* lands on the next render rather than instantly — the one divergence from native. Controls whose role sits on the host (the toggles, slider, radio group, bars) need none of this: the platform reads their host attributes directly.
|
|
47
47
|
- **Constraint validation** (`VfFormControl`): a reflected `required` fails validation with `valueMissing` while the control is empty by its own definition (fields/select `value === ''`, checkbox unchecked, radio group unselected — each with its native counterpart's message), through one `syncValidity()` funnel run before each render. The native surface is complete — `checkValidity`/`reportValidity`/`validity`/`validationMessage`/ `willValidate`/`setCustomValidity` — `:invalid` matches on the host, `form.reportValidity()` blocks, and `disabled`/`readonly` bar validation per HTML's own rules. One divergence the platform imposes: `willValidate` is `true` on a `vf-button type="button"`, where a native button is barred from constraint validation. A form-associated custom element cannot opt out of the candidate set, so this is a platform limit. AT wiring is `aria-required`/`aria-invalid` on the inner control (plus internals mirrors for host-role controls) — never a forwarded native `required`, which would put UA `:user-invalid` styling on the artwork. Enter's implicit submission routes through the browser's validation, so it cannot submit past a failing constraint.
|
|
48
48
|
- Components must render nothing surprising outside their box: no margins on `:host` by default.
|
|
49
|
-
- **Explicit placement** (`VfPositioned`, src/position.ts): every component takes `top`/`left` in whole system px — set either and the host is absolutely positioned within its parent (`position: absolute` plus both offsets as live `calc(var(--vf-scale, 1) * Npx)` inline styles; the unset coordinate is 0; `right`/`bottom` released to `auto` and `margin` zeroed). Set neither and the element renders in flow; removing both unwinds every inline declaration. The DITL model: a dialog can be laid out by positioning its items just as validly as by stacking them. **No component is excluded** — the rows a container normally owns (`vf-option`, `vf-menu-item`, `vf-list-item`) and a bar's `vf-menu` take the pair on the same terms, because a consumer may stand one of them up on its own and the kit does not get to rule that out. Placing such a row *inside* its managing parent takes it out of that parent's flow, so the parent stops measuring it (a `vf-select`/`vf-menu` panel is as wide as its widest **flowed** row, and `popup-overflow`'s clamp counts the same set) and the rows after it close the gap — the documented consequence of the placement, not a failure of it; each component's own doc comment states what its container stops doing. Containers are deliberate anchors — the desktop raster, a window's or a dialog's *content region* (the frame's inner edge below any title bar, with no inset of its own — flow content starts there too), a stack's box, a fieldset's border interior, a scroll area's scrolled plane. The style writing rides a ReactiveController (`hostUpdated`), not an `updated()` override — component subclasses routinely skip `super.updated()` — and re-applies only when the property values changed, so an unrelated update never re-asserts a coordinate. **`origin`** (reflected string: one of nine keywords, vertical then horizontal — `top left` the default, `top center`, `top right`, `center left`, `center`, `center right`, `bottom left`, `bottom center`, `bottom right`) names which point of the host's own box the pair places. With a non-default origin the controller measures the host's border box in whole system px (w × h) and writes `left` as `calc(var(--vf-scale, 1) * (L − ⌈w·fx⌉)px)` with `fx` 0, ½ or 1, `top` likewise — the ceiling sends an odd size's leftover half toward the start, the tie rule `vf-stack`'s centering and the title patch use — and keeps the offset current with a ResizeObserver (a relabel, a late font; a zoom step changes CSS px, not system px, so the same offset comes back and nothing is rewritten), each rewrite announced as `vf-placement-change`. Never a transform: a percentage of an odd width is half a pixel, and a transform would make the host the containing block for a `position: fixed` descendant (`vf-select`'s list). `top left` is the plain path — no observer, no measurement, the same inline style. The pair is the origin point throughout: `PlacementController.seed()` returns it (the corner read from flow plus the offset, for a host with no stated pair), `moveTo()` takes the offset off before the clamp and puts it back after, so a gesture writes the point's pair and the clamp holds the box; `placementAt(clientX, clientY, child?)` on the three shadow-anchored containers folds `child`'s offset in; `fixed` folds it into its sticky thresholds. `vf-dialog` has no `origin` — its pair is the viewport's, and unset centers it. An unknown value places as `top left` and warns once per element; on its own the attribute places nothing. **`fixed`** (bare attribute, reflected) holds the placement against the *visible* region of the nearest scrolling ancestor instead of its scrolled plane: `position: sticky` in place of `absolute`, the same live offsets, the flag alone at (0,0). Sticky boxes stay in flow, so the controller erases the footprint — the host is blockified (the outer display change `absolute` makes on its own: `inline-block` → `block`, `inline-flex` → `flex`, …), `max-width: fit-content`, `z-index: 1` (over the plane's placed children), and a negative right/bottom margin equal to its own border box, kept exact by a ResizeObserver — a 0×0 margin box that flow content lays out as if it weren't there. Two engine facts stated as rules: sticky only ever pushes a box *down* from where the flow put it, so a fixed child precedes the flow content in its parent; and a scroll container that never scrolls (a plain window body is `overflow: hidden`) holds it exactly where placement would. Removing the flag returns the host to ordinary placement with the recipe unwound. `npm run verify:position`.
|
|
49
|
+
- **Explicit placement** (`VfPositioned`, src/position.ts): every component takes `top`/`left` in whole system px — set either and the host is absolutely positioned within its parent (`position: absolute` plus both offsets as live `calc(var(--vf-scale, 1) * Npx)` inline styles; the unset coordinate is 0; `right`/`bottom` released to `auto` and `margin` zeroed). Set neither and the element renders in flow; removing both unwinds every inline declaration. The DITL model: a dialog can be laid out by positioning its items just as validly as by stacking them. **No component is excluded** — the rows a container normally owns (`vf-option`, `vf-menu-item`, `vf-list-item`) and a bar's `vf-menu` take the pair on the same terms, because a consumer may stand one of them up on its own and the kit does not get to rule that out. Placing such a row *inside* its managing parent takes it out of that parent's flow, so the parent stops measuring it (a `vf-select`/`vf-menu` panel is as wide as its widest **flowed** row, and `popup-overflow`'s clamp counts the same set) and the rows after it close the gap — the documented consequence of the placement, not a failure of it; each component's own doc comment states what its container stops doing. Containers are deliberate anchors — the desktop raster, a window's or a dialog's *content region* (the frame's inner edge below any title bar, with no inset of its own — flow content starts there too), a stack's box, a fieldset's border interior, a scroll area's scrolled plane. The style writing rides a ReactiveController (`hostUpdated`), not an `updated()` override — component subclasses routinely skip `super.updated()` — and re-applies only when the property values changed, so an unrelated update never re-asserts a coordinate. **`origin`** (reflected string: one of nine keywords, vertical then horizontal — `top left` the default, `top center`, `top right`, `center left`, `center`, `center right`, `bottom left`, `bottom center`, `bottom right`) names which point of the host's own box the pair places. With a non-default origin the controller measures the host's border box in whole system px (w × h) and writes `left` as `calc(var(--vf-scale, 1) * (L − ⌈w·fx⌉)px)` with `fx` 0, ½ or 1, `top` likewise — the ceiling sends an odd size's leftover half toward the start, the tie rule `vf-stack`'s centering and the title patch use — and keeps the offset current with a ResizeObserver (a relabel, a late font; a zoom step changes CSS px, not system px, so the same offset comes back and nothing is rewritten), each rewrite announced as `vf-placement-change`. A box with no layout (0×0, inside a closed dialog) keeps the offset it had, so it shows again in place. The width measured is the box's own wherever its corner lands: a placed box without a declared width shrinks to the room between its `left` and its parent's far edge, and the offset moves that `left`, so a first measurement with the corner at the stated point squeezed the box, and each re-measure freed a little more room — a row walked for frames after every show and could stop short. A negative right margin as wide as the written `left` gives the box its parent's whole width wherever it sits, so the first measurement is the last; a declared width is untouched (not for `fixed`, whose margins are its footprint). Never a transform: a percentage of an odd width is half a pixel, and a transform would make the host the containing block for a `position: fixed` descendant (`vf-select`'s list). `top left` is the plain path — no observer, no measurement, the same inline style. The pair is the origin point throughout: `PlacementController.seed()` returns it (the corner read from flow plus the offset, for a host with no stated pair), `moveTo()` takes the offset off before the clamp and puts it back after, so a gesture writes the point's pair and the clamp holds the box; `placementAt(clientX, clientY, child?)` on the three shadow-anchored containers folds `child`'s offset in; `fixed` folds it into its sticky thresholds. `vf-dialog` has no `origin` — its pair is the viewport's, and unset centers it. An unknown value places as `top left` and warns once per element; on its own the attribute places nothing. **`fixed`** (bare attribute, reflected) holds the placement against the *visible* region of the nearest scrolling ancestor instead of its scrolled plane: `position: sticky` in place of `absolute`, the same live offsets, the flag alone at (0,0). Sticky boxes stay in flow, so the controller erases the footprint — the host is blockified (the outer display change `absolute` makes on its own: `inline-block` → `block`, `inline-flex` → `flex`, …), `max-width: fit-content`, `z-index: 1` (over the plane's placed children), and a negative right/bottom margin equal to its own border box, kept exact by a ResizeObserver — a 0×0 margin box that flow content lays out as if it weren't there. Two engine facts stated as rules: sticky only ever pushes a box *down* from where the flow put it, so a fixed child precedes the flow content in its parent; and a scroll container that never scrolls (a plain window body is `overflow: hidden`) holds it exactly where placement would. Removing the flag returns the host to ordinary placement with the recipe unwound. `npm run verify:position`.
|
|
50
50
|
- `vf-dialog` takes the pair in **viewport** coordinates: `showModal()` puts the box in the top layer, whose containing block is the viewport rather than the nearest positioned ancestor. Unset means centered — recomputed on open and on every box/viewport/scale change, so a modal whose content upgrades after opening is never stranded; `position` is left to the UA and only the four inset/margin declarations are written.
|
|
51
51
|
- **Gestures write through the same properties** (`PlacementController`): a `vf-window`/`vf-dialog` title-bar drag and a `vf-icon` drag or arrow nudge state `top`/`left`, and `vf-window`'s grow box states `width`/`height` (`VfSized`) — all in whole system px, so a moved element is placed the way an authored one is and holds its spot through a zoom. Writing resolved CSS px instead was the bug: `--vf-scale` moved under the constant and every zoom step re-read it as a different number of system px (by the ratio the scale itself moved — visible even at zoom levels where the target does not change, and nothing else on the page does). Values are snapped to `snapSys` at gesture time and never re-snapped afterwards: re-rounding onto each new lattice compounds (62 → 63 → 64), and whole system px is whole device px at every rung regardless. `npm run verify:zoom` group (e). Every placement write the controller makes is announced as `vf-placement-change` (bubbles, `composed: false`, `{ left, top }`, nulls on a return to flow), a public event: a page hears every move through it — a title-bar drag's steps and its release (an `outline-drag` writes once, on the release), an arrow nudge, a drop, a walk's landing, an `origin` re-measure and its own property writes — and so does a `vf-scroll-area`, which re-measures under a child dropped, nudged or set past its viewport with no code on the page. It is dispatched from the host's update with the new pair already written, so a handler that writes the pair again takes effect on the next update. `npm run verify:position` (EVENT), `npm run verify:window` (PLACEMENT EVENT). `npm run verify:icon-drag` (RE-MEASURE).
|
|
52
52
|
- **The movable contract.** A host that moves under a gesture states its own rectangle, and its positioning parent is a box with a size. Both halves are the consumer's — a component can supply neither for itself — and both used to fail quietly, so each is a one-time `console.warn` (`warnMovableContract`, latched per element like `vf-window`'s size warning). `npm run verify:position` group CONTRACT.
|
|
@@ -233,18 +233,18 @@ The desktop-window shell: the classic document window (see DragThing screenshot)
|
|
|
233
233
|
|
|
234
234
|
#### `vf-dialog` (`VfDialog`, vf-dialog.ts)
|
|
235
235
|
The modal-dialog shell: movable modal by default (the dBoxProc double frame with the striped title bar set into it — traced from a 2× System 7 capture of a movable modal), the modal dialog box with `frame="plain"` (the bare double frame; see the Group A recipe table).
|
|
236
|
-
- **Attributes/props:** `open: boolean` (reflect), `heading: string`, `top: number` / `left: number` (whole system px, in **viewport** coordinates — see §1 Explicit placement; unset means centered), `width: number` / `height: number` (**declare them both** — whole system px, the same fixed box `vf-window` is. A native `<dialog>` is `width: fit-content` measured against the space left beside its own offsets, and stating an offset is how the movable modal is positioned, so an undeclared dialog squeezes itself and reflows as it is dragged toward an edge. The two fall back differently — width to 260 system px, height to the content — and it names whichever are missing, once, on the open that first shows it), `label: string` (accessible name for a dialog with no `heading`), `closable: boolean` (default **false** — the bare movable-modal bar; the close box is opt-in because the HIG's Chapter 6 text denies a movable modal one while its Figure 5-1 grants it — the parameter enables either reading), `light-dismiss: boolean` (`lightDismiss`; default **false** — a click outside the frame closes the dialog with `vf-close` reason `'outside'`. Off by default because the classic modal ignored an outside click, and a dialog that asks a question should keep ignoring it; opt in for the About box and the splash, which the classic Mac did dismiss on a click. Reflected, and read at the release, so it can be toggled on an open dialog), `frame?: 'plain'
|
|
237
|
-
- **Implementation:** wraps a native `<dialog>` (for top-layer + focus trap). `show()` → `showModal()`; `close()` closes. Keep `open` attr in sync both directions. Drag the title bar to move it (shared `DragController` with `vf-window`), stating `top`/`left` in system px; drags starting on the close widget are ignored (same composedPath guard as `vf-window`). Escape → close + `vf-close` detail `{ reason: 'escape' }`; close box/programmatic/close() → `{ reason: 'close' }`; a click outside under `light-dismiss` → `{ reason: 'outside' }`. No backdrop dimming: `::backdrop { background: transparent; }`. **Light dismiss is target identity, not geometry** (`VfModalDialog`): the `::backdrop` hit-tests as its `<dialog>`, and the frame fills the dialog's box, so the dialog is a pointer event's *target* exactly when the pointer was outside the frame — the listeners sit on the `<dialog>` itself, attached while open. It takes both halves of the click: a `pointerdown` on the backdrop arms (any other press disarms, so a right-click macOS never releases can't leave a stale arm), and a `pointerup` on the backdrop with the same `pointerId` closes — the platform's own `closedby="any"` two-step, so a press that starts on a control and slides off, or a title-bar drag released past the frame, leaves the dialog open. Deliberately not the `click` event: UI Events dispatches a press-drag-release click at the common ancestor of the two targets, which for a press on the frame released outside is the dialog itself. The platform's `closedby` attribute is not used because its dismissal arrives as `cancel`, which the base already reads as Escape. The press is consumed either way — a modal's backdrop lets nothing beneath it see the click — which is what lets a page dismiss its About box without also selecting the icon under the pointer. `npm run verify:dialog`. **Keyboard: the Dialog Manager's two rules** (`VfModalDialog`). *Initial focus:* right after `showModal()`'s own focusing steps, focus is handed to `initialFocusTarget` — a slotted control carrying `autofocus`, else the first enabled text-entry control (`vf-text-field`, `vf-number-field`, `vf-text-area`, or a native text input/textarea), else the default button (`defaultButton`: the first enabled `vf-button[variant="default"]`, looked for among the slotted content and then the shadow tree), else nothing, leaving the browser's pick. The classic dialog put the insertion point in its first text item and gave nothing else focus; the web needs a focused control, and the default button is the one that makes Return and Space work without a Tab. Left alone, `showModal()` focuses the first focusable thing in flat-tree order — Cancel in a Cancel/OK row, or a link in the body — so Return pressed the wrong thing. Both getters are `protected`, for a consumer's own modal to override. *Return/Enter:* a `keydown` listener on the `<dialog>`, bubble phase, attached while open with the light-dismiss trio. An uncancelled, unmodified Enter activates `defaultButton` (its `click()`, so a `type="submit"` default runs its proxy path) and cancels the keydown, which is what keeps a focused Cancel from firing itself — Space presses the focused control, as on the classic Mac. Anything that took Enter for itself cancelled the keydown first and is left alone: `vf-select`'s list, `vf-list`, an icon's rename, and a text field whose form ran its implicit submission (`requestImplicitSubmit` now reports whether a form owner took the press, and the fields cancel on true — so the submission runs once, not once per layer). A link keeps its own Enter. A multi-line editor (`<textarea>`, contenteditable) keeps Return for the newline; only `code === 'NumpadEnter'` fires the button there — the split a TextEdit item drew between the two keys. With no enabled default button the press is left native. Escape is unchanged (the `cancel` path above). **Removal while open is a close path** (`VfModalDialog.disconnectedCallback`): HTML's dialog removing steps skip the close algorithm entirely, which is exactly what a framework unmount does — the teardown routes through the same native-`close` funnel (`vf-close` fires on the removed element; nothing bubbles, it left the tree), `open` and the written origin reconcile so a re-append mounts closed, and focus returns to the element focused at open time. **The placement re-settles while open** whenever the dialog's own box resizes — slotted content upgrading after `showModal()`, `--vf-scale` moving under zoom — the viewport resizes, or the scale changes. What survives depends on how the modal got where it is: an unplaced one re-centers (it never claimed a spot, and the stranded case is exactly this one), while a dragged or authored origin is kept and only re-clamped on screen.
|
|
236
|
+
- **Attributes/props:** `open: boolean` (reflect), `heading: string`, `top: number` / `left: number` (whole system px, in **viewport** coordinates — see §1 Explicit placement; unset means centered), `width: number` / `height: number` (**declare them both** — whole system px, the same fixed box `vf-window` is. A native `<dialog>` is `width: fit-content` measured against the space left beside its own offsets, and stating an offset is how the movable modal is positioned, so an undeclared dialog squeezes itself and reflows as it is dragged toward an edge. The two fall back differently — width to 260 system px, height to the content — and it names whichever are missing, once, on the open that first shows it), `label: string` (accessible name for a dialog with no `heading`), `closable: boolean` (default **false** — the bare movable-modal bar; the close box is opt-in because the HIG's Chapter 6 text denies a movable modal one while its Figure 5-1 grants it — the parameter enables either reading), `light-dismiss: boolean` (`lightDismiss`; default **false** — a click outside the frame closes the dialog with `vf-close` reason `'outside'`. Off by default because the classic modal ignored an outside click, and a dialog that asks a question should keep ignoring it; opt in for the About box and the splash, which the classic Mac did dismiss on a click. Reflected, and read at the release, so it can be toggled on an open dialog), `frame?: 'plain'`, `returnValue: string` (property only, as on `<dialog>`: the dialog's answer, cleared on each open).
|
|
237
|
+
- **Implementation:** wraps a native `<dialog>` (for top-layer + focus trap). `show()` → `showModal()`; `close()` closes. Keep `open` attr in sync both directions. Drag the title bar to move it (shared `DragController` with `vf-window`), stating `top`/`left` in system px; drags starting on the close widget are ignored (same composedPath guard as `vf-window`). Escape → close + `vf-close` detail `{ reason: 'escape' }`; close box/programmatic/close() → `{ reason: 'close' }`; a click outside under `light-dismiss` → `{ reason: 'outside' }`. **Answers** (`VfModalDialog`): `close(value)` closes with `value` as `returnValue`, and a `<form method="dialog">` inside the dialog closes it on submit with the submitter's `value`, a submitter's `formmethod="dialog"` too. The platform's own rule looks for a `<dialog>` around the form, and this one is in the shadow root, so the base listens for `submit` on the host and closes at the end of the submit's path (`deferActivation`), not at all if anything cancelled it. `vf-button`'s submit proxy carries its `value` with or without a `name`. `vf-close`'s detail carries `returnValue`: the value that close carried, or null (Escape, a click outside, the close box, `close()` alone, removal). Return in a text field runs the form's implicit submission through the ringed `variant="default"` submit button when the form has one, ahead of the first in tree order, so a Cancel/OK row answers OK (`requestImplicitSubmit`). `npm run verify:dialog`, ANSWERS. No backdrop dimming: `::backdrop { background: transparent; }`. **Light dismiss is target identity, not geometry** (`VfModalDialog`): the `::backdrop` hit-tests as its `<dialog>`, and the frame fills the dialog's box, so the dialog is a pointer event's *target* exactly when the pointer was outside the frame — the listeners sit on the `<dialog>` itself, attached while open. It takes both halves of the click: a `pointerdown` on the backdrop arms (any other press disarms, so a right-click macOS never releases can't leave a stale arm), and a `pointerup` on the backdrop with the same `pointerId` closes — the platform's own `closedby="any"` two-step, so a press that starts on a control and slides off, or a title-bar drag released past the frame, leaves the dialog open. Deliberately not the `click` event: UI Events dispatches a press-drag-release click at the common ancestor of the two targets, which for a press on the frame released outside is the dialog itself. The platform's `closedby` attribute is not used because its dismissal arrives as `cancel`, which the base already reads as Escape. The press is consumed either way — a modal's backdrop lets nothing beneath it see the click — which is what lets a page dismiss its About box without also selecting the icon under the pointer. `npm run verify:dialog`. **Keyboard: the Dialog Manager's two rules** (`VfModalDialog`). *Initial focus:* right after `showModal()`'s own focusing steps, focus is handed to `initialFocusTarget` — a slotted control carrying `autofocus`, else the first enabled text-entry control (`vf-text-field`, `vf-number-field`, `vf-text-area`, or a native text input/textarea), else the default button (`defaultButton`: the first enabled `vf-button[variant="default"]`, looked for among the slotted content and then the shadow tree), else nothing, leaving the browser's pick. The classic dialog put the insertion point in its first text item and gave nothing else focus; the web needs a focused control, and the default button is the one that makes Return and Space work without a Tab. Left alone, `showModal()` focuses the first focusable thing in flat-tree order — Cancel in a Cancel/OK row, or a link in the body — so Return pressed the wrong thing. Both getters are `protected`, for a consumer's own modal to override. *Return/Enter:* a `keydown` listener on the `<dialog>`, bubble phase, attached while open with the light-dismiss trio. An uncancelled, unmodified Enter activates `defaultButton` (its `click()`, so a `type="submit"` default runs its proxy path) and cancels the keydown, which is what keeps a focused Cancel from firing itself — Space presses the focused control, as on the classic Mac. Anything that took Enter for itself cancelled the keydown first and is left alone: `vf-select`'s list, `vf-list`, an icon's rename, and a text field whose form ran its implicit submission (`requestImplicitSubmit` now reports whether a form owner took the press, and the fields cancel on true — so the submission runs once, not once per layer). A link keeps its own Enter. A multi-line editor (`<textarea>`, contenteditable) keeps Return for the newline; only `code === 'NumpadEnter'` fires the button there — the split a TextEdit item drew between the two keys. With no enabled default button the press is left native. Escape is unchanged (the `cancel` path above). **Removal while open is a close path** (`VfModalDialog.disconnectedCallback`): HTML's dialog removing steps skip the close algorithm entirely, which is exactly what a framework unmount does — the teardown routes through the same native-`close` funnel (`vf-close` fires on the removed element; nothing bubbles, it left the tree), `open` and the written origin reconcile so a re-append mounts closed, and focus returns to the element focused at open time. **The placement re-settles while open** whenever the dialog's own box resizes — slotted content upgrading after `showModal()`, `--vf-scale` moving under zoom — the viewport resizes, or the scale changes. What survives depends on how the modal got where it is: an unplaced one re-centers (it never claimed a spot, and the stranded case is exactly this one), while a dragged or authored origin is kept and only re-clamped on screen.
|
|
238
238
|
- **Visual (default chrome):** `vfModalFrame` with `vfTitleBar` set into it (§4) — the bar is literally the recipe `vf-window` uses, so it is identical by construction (stripes + centered title) rather than by matching copies; the frame is the modal double frame, **not** the window's shadowed `vfChromeFrame`. Outside → in: 1px outer rule; the 18px bar directly under it, 2px in at either end with its stripes starting 2px from the rule; the 2px band as the bar's floor (the bar's rule + the inner box's 1px top border); 2px gap + 2px band down the sides and along the bottom; no shadow. The body begins 20px below the dialog's top and 5px inside its other three edges. The bar takes the default `--vf-title-inset` (16px) — 60px when `closable`, the same clearance as `vf-window`, since the centered title needs symmetric room for the widget — and sets `touch-action: none` unconditionally, having no immovable state. `closable` renders the shared close box (`vfWindowWidgets` + `closeBox()` — byte-identical to `vf-window`'s, per the `moveable modal dialog.png` reference). Body is WHITE (`--vf-surface: #fff`) and is the content region, with no inset of its own: the positioning anchor, so a child placed with `top`/`left` measures from the frame's inner edge, and flow content starts at the same corner. Both chromes are full-height flex columns and the body takes the slack, for the same reason `vf-window`'s frame is: the declared `height` lands on the `<dialog>` (see `dialogSize`), and the recipes are skin only. The frame is the flex child of the `<dialog>` itself (`dialog[open]` is a flex column in `modalDialogStyles` — `[open]`-scoped, or it would out-cascade the UA's `dialog:not([open]) { display: none }`), not a `height: 100%` block: a percentage can't resolve against the undeclared-height dialog that only the UA's `dialog:modal` max-height caps, and that spill was how a viewport-tall modal stranded its buttons off-screen. **The box never grows, but over-stuffed content scrolls instead of clipping**: the body holds a `.content` scroll region (the default slot; part `content`). Placed children anchor to the body, outside that scroller, so they hold still while it scrolls. While the content fits, nothing matches. Once it overflows (`ScrollStateController`, the always-a-rail machinery), the region reserves the rail's 15px as its own right padding, wears the drawn `vfScrollRail` rail (§4) flush against the frame's inner band, which supplies the cell's outer line, and becomes a keyboard stop (`tabindex="0"`, `role="group"`, the kit's dotted ring) so the copy is scrollable without a pointer. The rail rides the body as an overlay pinned to its right edge, deliberately out of the layout flow: a rail *column*'s two fixed 15px arrow cells would hand the region a 32px minimum height, and a short dialog would then measure as fitting with the rail shown and overflowing without it — flip-flopping forever. The drop-open exemption is unchanged.
|
|
239
239
|
- **The CSS and the controller state the same thing.** `.content` is `overflow-y: hidden` until the controller flags overflow, then `overflow-y: scroll`. It is deliberately never `auto`: the controller ignores the body face's negative half-leading (`LEADING_SPILL_SYS`, src/scroll-state.ts — `vf-paragraph` sets a 12-system-px line box under a 16-system-px em, so the inline box spills 2 inkless system px past the block box and `scrollHeight` counts it), and `auto` has no way to know that. With `auto` the two disagreed, and a fixed info dialog whose copy ends in a `vf-paragraph` rubber-banded under the wheel with no rail shown — 6 CSS px at scale 3. `hidden` still scrolls programmatically, so `scrollIntoView` on a focused control is unaffected. `npm run verify:contract`, OVERFLOW group.
|
|
240
240
|
- **Visual (`frame="plain"`):** the same `vfModalFrame` (§4 — 1px outer, 2px gap, 2px inner band, no shadow, per `Windows/modal dialog.png`) with no title bar, and immovable like the original dBoxProc dialog (nothing renders a drag handle). The body begins 5px inside every edge. With no bar, `heading` only names the dialog and `closable` is ignored; a title in the body is the composer's, placed like any other item.
|
|
241
241
|
- **A11y:** the default chrome is named by its title patch (`id="title"`) via `aria-labelledby` when `heading` is set. Otherwise — no heading, or the plain frame, which has no patch to point at — it falls back to `aria-label`: `label` if given, else `heading`, else `'Dialog'`. An explicit `label` wins over `heading`. The close box is labeled `Close ${heading}` like `vf-window`'s.
|
|
242
242
|
- **Slots:** default.
|
|
243
243
|
- **Parts:** `frame`, `title-bar` and `title` (default chrome), `close-box` (when `closable`), `body`, `content`.
|
|
244
|
-
- **Events:** `vf-close` (detail `{ reason: 'escape' | 'close' | 'outside' }`).
|
|
244
|
+
- **Events:** `vf-close` (detail `{ reason: 'escape' | 'close' | 'outside', returnValue: string | null }`).
|
|
245
245
|
|
|
246
246
|
#### The alert box (composed — no component)
|
|
247
|
-
System 7's fixed modal alert (double frame: 2px outer, 2px gap, 1px inner rule, *with* the hard shadow) is deliberately not shipped as a component. What separates an alert from a modal dialog is a *picture* — the 32×32 icon — and pictures are the consumer's assets, never the library's (see the glyph-sprites note at the top of this spec). An alert box is composed from the shells above: `vf-dialog frame="plain"` with `label` stated (there is no title bar to name it), a row `vf-stack` slotting the consumer's own 32×32 art through `vf-img`, display-face copy (System 7 alerts used chrome type), and a `vf-button-group`, each placed with `top`/`left
|
|
247
|
+
System 7's fixed modal alert (double frame: 2px outer, 2px gap, 1px inner rule, *with* the hard shadow) is deliberately not shipped as a component. What separates an alert from a modal dialog is a *picture* — the 32×32 icon — and pictures are the consumer's assets, never the library's (see the glyph-sprites note at the top of this spec). An alert box is composed from the shells above: `vf-dialog frame="plain"` with `label` stated (there is no title bar to name it), a row `vf-stack` slotting the consumer's own 32×32 art through `vf-img`, display-face copy (System 7 alerts used chrome type), and a `vf-button-group`, each placed with `top`/`left`, all inside a `<form method="dialog">` whose buttons are `type="submit"` with a `value` each, so the alert answers with the pressed button's value. The reference page carries the live recipe, composed from `demo/icons/alert.png`.
|
|
248
248
|
|
|
249
249
|
#### `vf-separator` (`VfSeparator`, vf-separator.ts)
|
|
250
250
|
- 1px black rule. `vertical: boolean` attr → 1px wide, auto height. Horizontal default: `display: block; height: 1px`, the line the inner element's own background, `var(--vf-separator-color, var(--vf-black, #000))`. `--vf-separator-style` is `solid` (default) or `dotted` — one system px on, one off, from the rule's start, a `repeating-linear-gradient` selected by a style query on the token. Inside menus it renders as the classic dimmed **dotted** rule spanning the full panel width (see Menus.png): the menu panel sets the two props to `--vf-disabled` / `dotted`, and spends a full `--vf-menu-row-height` on the divider: 8px margin above the rule, 7 below. Under forced colors the line is a border in the token's style, as every stroke is. `role="separator"`.
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
"name": "value",
|
|
67
|
-
"description": "Value submitted under `name` when `type=\"submit\"`.",
|
|
67
|
+
"description": "Value submitted under `name` when `type=\"submit\"`. In a\n`<form method=\"dialog\">` inside a `vf-dialog`, the dialog's answer\n(`returnValue`), with or without a `name`.",
|
|
68
68
|
"values": []
|
|
69
69
|
},
|
|
70
70
|
{
|
|
@@ -303,7 +303,7 @@
|
|
|
303
303
|
},
|
|
304
304
|
{
|
|
305
305
|
"name": "vf-dialog",
|
|
306
|
-
"description": "`<vf-dialog>` — the System 7 modal dialog shell.\n\nTwo chromes, one modal lifecycle (native `<dialog>` for top-layer rendering\nand focus trapping, with a fully transparent backdrop — no dimming). Both\nare the same dBoxProc double frame — 1px outer rule, 2px gap, 2px inner\nband, no shadow (vfModalFrame):\n\n- **Default:** the movable modal (movableDBoxProc) — the striped title bar\n set into the top of that frame, with a centered title over a white body.\n Drag the title bar to move it. `closable` adds the standard close box\n (left of the bar) — the HIG's own figures disagree on whether a movable\n modal carries one (Figure 5-1 says yes, Figure 6-1 and the Chapter 6 text\n say no), so the component enables either reading rather than enforcing\n one.\n- **`frame=\"plain\"`:** the modal dialog box — the bare frame, no title bar —\n and immovable, like the original. With no bar, `heading` only names the\n dialog and `closable` is ignored.\n\nOpen it with `show()` (or set the `open` attribute/property); close with\n`close()`. Escape closes it and fires `vf-close` with\n`{ reason: 'escape' }`; the close box and programmatic closing fire\n`{ reason: 'close' }`. With `light-dismiss`, a click outside the frame\ncloses it too, with `{ reason: 'outside' }` — for the About box; off by\ndefault, since the classic modal ignored an outside click.\n\nKeyboard, the classic Dialog Manager's two rules (VfModalDialog):\non open, focus goes to the first text-entry control — a slotted control\nwith `autofocus` first — or, with none, to the default button\n(`vf-button variant=\"default\"`). Return or Enter activates the default\nbutton from anywhere in the dialog, a focused Cancel included; Space\npresses the focused control. A link keeps its own Enter, and in a\nmulti-line editor Return inserts the newline while the keypad's Enter\nactivates the button.\n\nSlotted children placed with `top`/`left` measure from the content region's\ncorner — the frame's inner edge, below the title bar — and flow content\nstarts at the same corner.\n---\n\n\n### **Events:**\n - **vf-close** - Dialog closed. Detail `{ reason: 'escape' | 'close' | 'outside' }` — `'outside'` only under `light-dismiss`.\n\n### **Slots:**\n - _default_ - Default slot: the dialog's content.\n\n### **CSS Properties:**\n - **--vf-dots-pattern** - the windoid bar's dot-grid dither — a 2×2 tile, one black pixel at the origin (`vfDots`; override the whole pattern like `--vf-desktop-pattern`) _(default: undefined)_\n- **--vf-titlebar-height** - window/dialog title bars _(default: undefined)_\n- **--vf-scrollbar-thumb** - scrollbar thumb/elevator (white) _(default: undefined)_\n- **--vf-scrollbar-track** - the scroll trough's base color under the dot-dither (white) _(default: undefined)_\n\n### **CSS Parts:**\n - **frame** - The outer frame (the double frame's 1px rule; the bar and the inner band sit inside it).\n- **title-bar** - The striped title bar (default chrome only).\n- **title** - The centered title patch (default chrome only).\n- **close-box** - The close widget (`closable`, default chrome only).\n- **body** - The white content region.\n- **content** - The scrolling region inside the body (the slotted content). Inert while the content fits; over-stuffed, it scrolls under a System 7 rail and becomes a keyboard stop.",
|
|
306
|
+
"description": "`<vf-dialog>` — the System 7 modal dialog shell.\n\nTwo chromes, one modal lifecycle (native `<dialog>` for top-layer rendering\nand focus trapping, with a fully transparent backdrop — no dimming). Both\nare the same dBoxProc double frame — 1px outer rule, 2px gap, 2px inner\nband, no shadow (vfModalFrame):\n\n- **Default:** the movable modal (movableDBoxProc) — the striped title bar\n set into the top of that frame, with a centered title over a white body.\n Drag the title bar to move it. `closable` adds the standard close box\n (left of the bar) — the HIG's own figures disagree on whether a movable\n modal carries one (Figure 5-1 says yes, Figure 6-1 and the Chapter 6 text\n say no), so the component enables either reading rather than enforcing\n one.\n- **`frame=\"plain\"`:** the modal dialog box — the bare frame, no title bar —\n and immovable, like the original. With no bar, `heading` only names the\n dialog and `closable` is ignored.\n\nOpen it with `show()` (or set the `open` attribute/property); close with\n`close()`. Escape closes it and fires `vf-close` with\n`{ reason: 'escape' }`; the close box and programmatic closing fire\n`{ reason: 'close' }`. With `light-dismiss`, a click outside the frame\ncloses it too, with `{ reason: 'outside' }` — for the About box; off by\ndefault, since the classic modal ignored an outside click.\n\nAnswers: a `<form method=\"dialog\">` inside it closes it with the\nsubmitting button's `value` as `returnValue`, and `close(value)` does the\nsame. `vf-close` carries it as `detail.returnValue`, null when the close\ncarried none (Escape, a click outside, the close box).\n\nKeyboard, the classic Dialog Manager's two rules (VfModalDialog):\non open, focus goes to the first text-entry control — a slotted control\nwith `autofocus` first — or, with none, to the default button\n(`vf-button variant=\"default\"`). Return or Enter activates the default\nbutton from anywhere in the dialog, a focused Cancel included; Space\npresses the focused control. A link keeps its own Enter, and in a\nmulti-line editor Return inserts the newline while the keypad's Enter\nactivates the button.\n\nSlotted children placed with `top`/`left` measure from the content region's\ncorner — the frame's inner edge, below the title bar — and flow content\nstarts at the same corner.\n---\n\n\n### **Events:**\n - **vf-close** - Dialog closed. Detail `{ reason: 'escape' | 'close' | 'outside', returnValue: string | null }` — `'outside'` only under `light-dismiss`.\n\n### **Slots:**\n - _default_ - Default slot: the dialog's content.\n\n### **CSS Properties:**\n - **--vf-dots-pattern** - the windoid bar's dot-grid dither — a 2×2 tile, one black pixel at the origin (`vfDots`; override the whole pattern like `--vf-desktop-pattern`) _(default: undefined)_\n- **--vf-titlebar-height** - window/dialog title bars _(default: undefined)_\n- **--vf-scrollbar-thumb** - scrollbar thumb/elevator (white) _(default: undefined)_\n- **--vf-scrollbar-track** - the scroll trough's base color under the dot-dither (white) _(default: undefined)_\n\n### **CSS Parts:**\n - **frame** - The outer frame (the double frame's 1px rule; the bar and the inner band sit inside it).\n- **title-bar** - The striped title bar (default chrome only).\n- **title** - The centered title patch (default chrome only).\n- **close-box** - The close widget (`closable`, default chrome only).\n- **body** - The white content region.\n- **content** - The scrolling region inside the body (the slotted content). Inert while the content fits; over-stuffed, it scrolls under a System 7 rail and becomes a keyboard stop.",
|
|
307
307
|
"attributes": [
|
|
308
308
|
{
|
|
309
309
|
"name": "heading",
|