vintage-frames 0.17.0 → 0.18.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.
@@ -3908,6 +3908,20 @@
3908
3908
  "module": "src/modal-dialog.ts"
3909
3909
  }
3910
3910
  },
3911
+ {
3912
+ "kind": "field",
3913
+ "name": "#opened",
3914
+ "privacy": "private",
3915
+ "type": {
3916
+ "text": "boolean"
3917
+ },
3918
+ "default": "false",
3919
+ "description": "Whether the modal has opened since its last close was funneled: true from\nan open until `#funnelClose` runs for the close that ends it, which can be\nafter the native `<dialog>` has closed, while its `close` event is queued.",
3920
+ "inheritedFrom": {
3921
+ "name": "VfModalDialog",
3922
+ "module": "src/modal-dialog.ts"
3923
+ }
3924
+ },
3911
3925
  {
3912
3926
  "kind": "method",
3913
3927
  "name": "show",
@@ -4095,7 +4109,7 @@
4095
4109
  "text": "void"
4096
4110
  }
4097
4111
  },
4098
- "description": "Reconcile the native `<dialog>` with `open`. Opening states the origin —\nthe author's, or the centered one — onto the top-layer box; the UA's own\n`margin: auto` centering lands on a half pixel whenever viewport minus\ndialog is odd, which fringes all the 1-bit chrome inside. Closing just\ncalls `dialog.close()`, routing teardown through the native `close` event\nso _onNativeClose is the one place placement is cleared and\n`vf-close` is fired.",
4112
+ "description": "Reconcile the native `<dialog>` with `open`. Opening states the origin —\nthe author's, or the centered one — onto the top-layer box; the UA's own\n`margin: auto` centering lands on a half pixel whenever viewport minus\ndialog is odd, which fringes all the 1-bit chrome inside. Closing just\ncalls `dialog.close()`, and the native `close` event runs the close funnel,\n`#funnelClose`, the one place placement is cleared and `vf-close` is\nfired.",
4099
4113
  "inheritedFrom": {
4100
4114
  "name": "VfModalDialog",
4101
4115
  "module": "src/modal-dialog.ts"
@@ -4152,7 +4166,7 @@
4152
4166
  }
4153
4167
  }
4154
4168
  ],
4155
- "description": "While open, watch everything that invalidates the placement: the dialog's\nown box changing (content upgrade/growth, and `--vf-scale` moving under a\nzoom or density change, since every metric resizes with it), the viewport\nresizing, and the scale itself — which the box alone would miss on a page\nthat pins `--vf-scale`, where a zoom moves the lattice without resizing\nanything. Undone in _onNativeClose / `disconnectedCallback`.",
4169
+ "description": "While open, watch everything that invalidates the placement: the dialog's\nown box changing (content upgrade/growth, and `--vf-scale` moving under a\nzoom or density change, since every metric resizes with it), the viewport\nresizing, and the scale itself — which the box alone would miss on a page\nthat pins `--vf-scale`, where a zoom moves the lattice without resizing\nanything. Undone by the close funnel and `disconnectedCallback`.",
4156
4170
  "inheritedFrom": {
4157
4171
  "name": "VfModalDialog",
4158
4172
  "module": "src/modal-dialog.ts"
@@ -4196,7 +4210,22 @@
4196
4210
  "text": "void"
4197
4211
  }
4198
4212
  },
4199
- "description": "Native `close` — the single teardown funnel for every close path. Drops\nthe written origin so the next open re-derives it from the box it will\nactually have (a stated `top`/`left` is the author's or the user's and\nsurvives, and is re-applied by that open), syncs `open`, and fires\n`vf-close` with the reason and the value the close carried.",
4213
+ "description": "Native `close`, which every close path ends in: syncs `open` and runs the\nclose funnel. An open that came before the event has run the funnel for\nthis close already, and the event passes.",
4214
+ "inheritedFrom": {
4215
+ "name": "VfModalDialog",
4216
+ "module": "src/modal-dialog.ts"
4217
+ }
4218
+ },
4219
+ {
4220
+ "kind": "method",
4221
+ "name": "#funnelClose",
4222
+ "privacy": "private",
4223
+ "return": {
4224
+ "type": {
4225
+ "text": "void"
4226
+ }
4227
+ },
4228
+ "description": "The close funnel, the single teardown for every close path. Drops the\nwritten origin so the next open re-derives it from the box it will\nactually have (a stated `top`/`left` is the author's or the user's and\nsurvives, and is re-applied by that open), and fires `vf-close` with the\nreason and the value the close carried.",
4200
4229
  "inheritedFrom": {
4201
4230
  "name": "VfModalDialog",
4202
4231
  "module": "src/modal-dialog.ts"
@@ -24316,7 +24345,7 @@
24316
24345
  },
24317
24346
  {
24318
24347
  "kind": "class",
24319
- "description": "Base class for the modal shell `vf-dialog` — and for a consumer's own\nmodal (an alert box, say) authored against the kit.\n\nOwns the native `<dialog>` lifecycle every modal shares: `open` sync, `show()` /\n`close()`, the top/left placement (stated or centered) and the\nsingle `close` funnel that drops the written origin and fires `vf-close` with\nthe reason. Because every close path — Escape, `close()`, the opt-in\nlightDismiss click outside — routes through the native `close`\nevent, an Escape-close no longer leaves a stale origin behind, so the next\nopen re-derives it.\n\nIt also owns the two keyboard rules every classic dialog followed (see\ndefaultButton and initialFocusTarget): Return or Enter\nactivates the default button from anywhere in the dialog, and the box opens\nwith the insertion point in its first text field — or, with none, focus on\nthe default button. A field marked `autoselect` takes the focus with its\ntext selected, on every open.\n\nRemoving an open modal from the DOM is a close path too. HTML's dialog\n*removing steps* take the element out of the top layer **without** running\nthe close algorithm — no `close` event, no focus restoration — which is\nexactly what the standard framework pattern of unmounting a dialog instead\nof calling `close()` does. `disconnectedCallback` routes that path through\nthe same funnel: the removed element still fires `vf-close` (heard by\nlisteners on the element itself — it has left the tree, so nothing\nbubbles), `open` and the pinned margins reconcile so a re-append mounts it\nclosed and re-centered, and focus returns to the element that was focused\nwhen the modal opened.\n\nA `<form method=\"dialog\">` inside the modal closes it with the submitting\nbutton's `value` as returnValue, as a form inside a native\n`<dialog>` does. The platform's own rule looks for a `<dialog>` around the\nform, and this one is in the shadow root, so the modal listens for the\nsubmit itself.\n\nSubclasses supply only the frame chrome: a `render()` returning\n`<dialog",
24348
+ "description": "Base class for the modal shell `vf-dialog` — and for a consumer's own\nmodal (an alert box, say) authored against the kit.\n\nOwns the native `<dialog>` lifecycle every modal shares: `open` sync, `show()` /\n`close()`, the top/left placement (stated or centered) and the\nsingle `close` funnel that drops the written origin and fires `vf-close` with\nthe reason. Because every close path — Escape, `close()`, the opt-in\nlightDismiss click outside — routes through the native `close`\nevent, an Escape-close no longer leaves a stale origin behind, so the next\nopen re-derives it. That event is queued: a modal opened again before it\ncomes (`close()` then `show()` in one turn) runs the funnel at the open,\ninside `show()`, so the close's `vf-close` still comes before the open's\n`vf-show`, and the late event changes nothing.\n\nIt also owns the two keyboard rules every classic dialog followed (see\ndefaultButton and initialFocusTarget): Return or Enter\nactivates the default button from anywhere in the dialog, and the box opens\nwith the insertion point in its first text field — or, with none, focus on\nthe default button. A field marked `autoselect` takes the focus with its\ntext selected, on every open.\n\nRemoving an open modal from the DOM is a close path too. HTML's dialog\n*removing steps* take the element out of the top layer **without** running\nthe close algorithm — no `close` event, no focus restoration — which is\nexactly what the standard framework pattern of unmounting a dialog instead\nof calling `close()` does. `disconnectedCallback` routes that path through\nthe same funnel: the removed element still fires `vf-close` (heard by\nlisteners on the element itself — it has left the tree, so nothing\nbubbles), `open` and the pinned margins reconcile so a re-append mounts it\nclosed and re-centered, and focus returns to the element that was focused\nwhen the modal opened.\n\nA `<form method=\"dialog\">` inside the modal closes it with the submitting\nbutton's `value` as returnValue, as a form inside a native\n`<dialog>` does. The platform's own rule looks for a `<dialog>` around the\nform, and this one is in the shadow root, so the modal listens for the\nsubmit itself.\n\nSubclasses supply only the frame chrome: a `render()` returning\n`<dialog",
24320
24349
  "name": "VfModalDialog",
24321
24350
  "members": [
24322
24351
  {
@@ -24551,6 +24580,16 @@
24551
24580
  "default": "null",
24552
24581
  "description": "The element that was focused when the modal opened. The normal close path\nrestores focus natively, but the removal path (see the class doc) skips\nthe close algorithm, and by the time `disconnectedCallback` runs, focus\nhas already fallen to `<body>` — so we keep our own record."
24553
24582
  },
24583
+ {
24584
+ "kind": "field",
24585
+ "name": "#opened",
24586
+ "privacy": "private",
24587
+ "type": {
24588
+ "text": "boolean"
24589
+ },
24590
+ "default": "false",
24591
+ "description": "Whether the modal has opened since its last close was funneled: true from\nan open until `#funnelClose` runs for the close that ends it, which can be\nafter the native `<dialog>` has closed, while its `close` event is queued."
24592
+ },
24554
24593
  {
24555
24594
  "kind": "method",
24556
24595
  "name": "show",
@@ -24702,7 +24741,7 @@
24702
24741
  "text": "void"
24703
24742
  }
24704
24743
  },
24705
- "description": "Reconcile the native `<dialog>` with `open`. Opening states the origin —\nthe author's, or the centered one — onto the top-layer box; the UA's own\n`margin: auto` centering lands on a half pixel whenever viewport minus\ndialog is odd, which fringes all the 1-bit chrome inside. Closing just\ncalls `dialog.close()`, routing teardown through the native `close` event\nso _onNativeClose is the one place placement is cleared and\n`vf-close` is fired."
24744
+ "description": "Reconcile the native `<dialog>` with `open`. Opening states the origin —\nthe author's, or the centered one — onto the top-layer box; the UA's own\n`margin: auto` centering lands on a half pixel whenever viewport minus\ndialog is odd, which fringes all the 1-bit chrome inside. Closing just\ncalls `dialog.close()`, and the native `close` event runs the close funnel,\n`#funnelClose`, the one place placement is cleared and `vf-close` is\nfired."
24706
24745
  },
24707
24746
  {
24708
24747
  "kind": "field",
@@ -24743,7 +24782,7 @@
24743
24782
  }
24744
24783
  }
24745
24784
  ],
24746
- "description": "While open, watch everything that invalidates the placement: the dialog's\nown box changing (content upgrade/growth, and `--vf-scale` moving under a\nzoom or density change, since every metric resizes with it), the viewport\nresizing, and the scale itself — which the box alone would miss on a page\nthat pins `--vf-scale`, where a zoom moves the lattice without resizing\nanything. Undone in _onNativeClose / `disconnectedCallback`."
24785
+ "description": "While open, watch everything that invalidates the placement: the dialog's\nown box changing (content upgrade/growth, and `--vf-scale` moving under a\nzoom or density change, since every metric resizes with it), the viewport\nresizing, and the scale itself — which the box alone would miss on a page\nthat pins `--vf-scale`, where a zoom moves the lattice without resizing\nanything. Undone by the close funnel and `disconnectedCallback`."
24747
24786
  },
24748
24787
  {
24749
24788
  "kind": "method",
@@ -24775,7 +24814,18 @@
24775
24814
  "text": "void"
24776
24815
  }
24777
24816
  },
24778
- "description": "Native `close` — the single teardown funnel for every close path. Drops\nthe written origin so the next open re-derives it from the box it will\nactually have (a stated `top`/`left` is the author's or the user's and\nsurvives, and is re-applied by that open), syncs `open`, and fires\n`vf-close` with the reason and the value the close carried."
24817
+ "description": "Native `close`, which every close path ends in: syncs `open` and runs the\nclose funnel. An open that came before the event has run the funnel for\nthis close already, and the event passes."
24818
+ },
24819
+ {
24820
+ "kind": "method",
24821
+ "name": "#funnelClose",
24822
+ "privacy": "private",
24823
+ "return": {
24824
+ "type": {
24825
+ "text": "void"
24826
+ }
24827
+ },
24828
+ "description": "The close funnel, the single teardown for every close path. Drops the\nwritten origin so the next open re-derives it from the box it will\nactually have (a stated `top`/`left` is the author's or the user's and\nsurvives, and is re-applied by that open), and fires `vf-close` with the\nreason and the value the close carried."
24779
24829
  }
24780
24830
  ],
24781
24831
  "events": [
@@ -19,6 +19,8 @@ export interface Slot {
19
19
  export interface BoxFrame {
20
20
  /** The picture, a PNG. */
21
21
  picture: Uint8Array;
22
+ /** One system px, in the picture's px. Each of the artwork's px is a whole number of them. */
23
+ scale: number;
22
24
  /** Where the artwork goes. Without artwork the icon goes there, `stamp` times its size and centered. */
23
25
  artwork: Slot & {
24
26
  stamp: number;
@@ -46,7 +48,7 @@ export interface BoxInput {
46
48
  manifest: AppManifest;
47
49
  /** The icon's PNG, 32 × 32. */
48
50
  icon: Uint8Array;
49
- /** The artwork's PNG: the artwork slot's size, or that divided by a whole number. */
51
+ /** The artwork's PNG: the artwork slot's size in system px, or that divided by a whole number. */
50
52
  artwork?: Uint8Array;
51
53
  }
52
54
  /** The box in the kit's frame, as a PNG. */
package/dist/build/box.js CHANGED
@@ -16,9 +16,12 @@ async function a(e, { manifest: n, icon: r, artwork: i }) {
16
16
  if (c.width !== 32 || c.height !== 32) throw Error(`the icon is ${c.width} × ${c.height}, and an icon is 32 × 32`);
17
17
  let u = e.artwork;
18
18
  if (i) {
19
- let e = await t(i, "the artwork"), n = u.width / e.width;
20
- if (!Number.isInteger(n) || e.height * n !== u.height) throw Error(`the artwork is ${e.width} × ${e.height}: draw it at ${u.width} × ${u.height}, or that divided by a whole number (${o(u)})`);
21
- s(a, e, u.left, u.top, n, u);
19
+ let n = await t(i, "the artwork"), r = {
20
+ width: u.width / e.scale,
21
+ height: u.height / e.scale
22
+ }, c = r.width / n.width;
23
+ if (!Number.isInteger(c) || n.height * c !== r.height) throw Error(`the artwork is ${n.width} × ${n.height}: draw it at ${r.width} × ${r.height}, or that divided by a whole number (${o(r)})`);
24
+ s(a, n, u.left, u.top, c * e.scale, u);
22
25
  } else {
23
26
  let e = 32 * u.stamp;
24
27
  s(a, c, u.left + Math.floor((u.width - e) / 2), u.top + Math.floor((u.height - e) / 2), u.stamp, u);
@@ -1,42 +1,44 @@
1
+ import "../shell/app-file.js";
1
2
  import { VF_BODY_STRIKE as e } from "../styles/body-strike.js";
2
3
  import { VF_DISPLAY_STRIKE as t } from "../styles/display-strike.js";
3
4
  import { FRAME_PNG as n } from "./frame-picture.js";
4
5
  //#region src/build/frame.ts
5
- var r = {
6
- left: 48,
7
- width: 384,
8
- scale: 3,
6
+ var r = 3, i = {
7
+ left: 16 * r,
8
+ width: 128 * r,
9
+ scale: r,
9
10
  ink: "#000000",
10
11
  align: "center"
11
- }, i = {
12
+ }, a = {
12
13
  picture: n,
14
+ scale: r,
13
15
  artwork: {
14
- left: 48,
15
- top: 48,
16
- width: 384,
17
- height: 384,
18
- stamp: 8
16
+ left: 16 * r,
17
+ top: 16 * r,
18
+ width: 128 * r,
19
+ height: 128 * r,
20
+ stamp: 3 * r
19
21
  },
20
22
  text: [
21
23
  {
22
- ...r,
23
- top: 456,
24
+ ...i,
25
+ top: 152 * r,
24
26
  strike: t,
25
27
  text: "{name}"
26
28
  },
27
29
  {
28
- ...r,
29
- top: 510,
30
+ ...i,
31
+ top: 170 * r,
30
32
  strike: e,
31
33
  text: "Version {version}"
32
34
  },
33
35
  {
34
- ...r,
35
- top: 546,
36
+ ...i,
37
+ top: 182 * r,
36
38
  strike: e,
37
39
  text: "by {author}"
38
40
  }
39
41
  ]
40
42
  };
41
43
  //#endregion
42
- export { i as FRAME };
44
+ export { a as FRAME };
@@ -4,18 +4,19 @@
4
4
  */
5
5
  import type { Plugin } from 'vite';
6
6
  export interface AppFileOptions {
7
- /** The application's id, name, version and author: its `app.ts`. */
7
+ /** The application's id, name, version and author, and its description if it has one: its `app.ts`. */
8
8
  app: {
9
9
  id: string;
10
10
  name: string;
11
11
  version: string;
12
12
  author: string;
13
+ description?: string;
13
14
  };
14
15
  /** The module whose default export is the application's factory. */
15
16
  entry: string;
16
17
  /** Its 32 × 32 icon, a PNG. */
17
18
  icon: string;
18
- /** Its artwork for the box: a PNG at the artwork slot's size, or that divided by a whole number. */
19
+ /** Its artwork for the box: a 128 × 128 PNG, or that divided by a whole number. */
19
20
  artwork?: string;
20
21
  /** A directory to write the app file to as well, such as a site's `apps/`. */
21
22
  copyTo?: string;
@@ -1,40 +1,41 @@
1
- import { inspectAppFile as e, satisfies as t } from "../shell/app-file.js";
2
- import { VERSION as n } from "../shell/version.js";
3
- import { packApp as r } from "./app-file.js";
4
- import { mkdir as i, readFile as a, writeFile as o } from "node:fs/promises";
5
- import { join as s, relative as c, resolve as l } from "node:path";
1
+ import { inspectAppFile as e, isVersion as t, satisfies as n } from "../shell/app-file.js";
2
+ import { VERSION as r } from "../shell/version.js";
3
+ import { packApp as i } from "./app-file.js";
4
+ import { mkdir as a, readFile as o, writeFile as s } from "node:fs/promises";
5
+ import { join as c, relative as l, resolve as u } from "node:path";
6
6
  //#region src/build/plugins.ts
7
- var u = [
7
+ var d = [
8
8
  "vintage-frames",
9
9
  "vintage-frames/shell",
10
10
  "vintage-frames/shell/pure"
11
- ], d = "app-code";
12
- function f(e) {
11
+ ], f = "app-code";
12
+ function p(e) {
13
13
  let t = e.match(/(?<=[\\/]node_modules[\\/])(?:@[^\\/]+[\\/])?[^\\/]+/g)?.at(-1)?.replace("\\", "/");
14
14
  return t === "vintage-frames" ? "a copy of vintage-frames" : t && /^(?:lit|lit-html|lit-element|@lit\/.+|@lit-labs\/.+)$/.test(t) ? `Lit (${t})` : null;
15
15
  }
16
- async function p(e) {
17
- let t = JSON.parse(await a(s(e, "package.json"), "utf8")), r = t.devDependencies?.["vintage-frames"] ?? t.dependencies?.["vintage-frames"];
18
- if (typeof r != "string") throw Error("package.json has no vintage-frames dev dependency");
19
- if (!/^\^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(r)) throw Error(`package.json's vintage-frames range is "${r}", and an app file requires a caret range: ^${n}`);
20
- return r;
16
+ async function m(e) {
17
+ let n = JSON.parse(await o(c(e, "package.json"), "utf8")), i = n.devDependencies?.["vintage-frames"] ?? n.dependencies?.["vintage-frames"];
18
+ if (typeof i != "string") throw Error("package.json has no vintage-frames dev dependency");
19
+ if (!i.startsWith("^") || !t(i.slice(1))) throw Error(`package.json's vintage-frames range is "${i}", and an app file requires a caret range: ^${r}`);
20
+ return i;
21
21
  }
22
- function m(e) {
23
- let { app: c, entry: d, icon: m, artwork: h, copyTo: g } = e;
22
+ function h(e) {
23
+ let { app: l, entry: f, icon: h, artwork: g, copyTo: _ } = e;
24
24
  for (let e of [
25
25
  "id",
26
26
  "name",
27
27
  "version",
28
28
  "author"
29
- ]) if (typeof c?.[e] != "string" || !c[e]) throw Error(`vintage-frames appFile(): app.${e} is missing`);
30
- if (!d || !m) throw Error(`vintage-frames appFile(): ${d ? "icon" : "entry"} is missing`);
31
- let _ = `${c.name.replace(/[\\/:*?"<>|]/g, "-")}.png`, v = "", y = null;
29
+ ]) if (typeof l?.[e] != "string" || !l[e]) throw Error(`vintage-frames appFile(): app.${e} is missing`);
30
+ if (!t(l.version)) throw Error(`vintage-frames appFile(): app.version, ${l.version}, is not a version`);
31
+ if (!f || !h) throw Error(`vintage-frames appFile(): ${f ? "icon" : "entry"} is missing`);
32
+ let v = `${l.name.replace(/[\\/:*?"<>|]/g, "-")}.png`, y = "", b = null;
32
33
  return {
33
34
  name: "vintage-frames:app-file",
34
35
  apply: "build",
35
36
  config: () => ({ build: {
36
37
  lib: {
37
- entry: d,
38
+ entry: f,
38
39
  formats: ["es"],
39
40
  fileName: "app"
40
41
  },
@@ -45,79 +46,80 @@ function m(e) {
45
46
  }
46
47
  } }),
47
48
  configResolved(e) {
48
- v = e.root;
49
+ y = e.root;
49
50
  },
50
51
  buildStart() {
51
52
  for (let e of [
52
- m,
53
53
  h,
54
+ g,
54
55
  "package.json"
55
- ]) e && this.addWatchFile(l(v, e));
56
+ ]) e && this.addWatchFile(u(y, e));
56
57
  },
57
58
  generateBundle: {
58
59
  order: "post",
59
- async handler(e, i) {
60
- let o = Object.values(i), s = o.filter((e) => e.type === "chunk"), d = o.filter((e) => e.type === "asset").map((e) => e.fileName);
61
- d.length && this.error(`${c.name} emits ${d.join(", ")} beside its code, and an app file holds one module: no CSS import, no source map file, its art inlined.`);
62
- let [g] = s;
63
- (!g || s.length > 1) && this.error(`${c.name} builds to ${s.length} modules, and an app file holds one.`);
64
- for (let e of Object.keys(g.modules)) {
65
- let t = f(e);
66
- t && this.error(`${c.name} bundles ${t}. It composes the page's kit and brings none of its own.`);
60
+ async handler(e, t) {
61
+ let a = Object.values(t), s = a.filter((e) => e.type === "chunk"), c = a.filter((e) => e.type === "asset").map((e) => e.fileName);
62
+ c.length && this.error(`${l.name} emits ${c.join(", ")} beside its code, and an app file holds one module: no CSS import, no source map file, its art inlined.`);
63
+ let [f] = s;
64
+ (!f || s.length > 1) && this.error(`${l.name} builds to ${s.length} modules, and an app file holds one.`);
65
+ for (let e of Object.keys(f.modules)) {
66
+ let t = p(e);
67
+ t && this.error(`${l.name} bundles ${t}. It composes the page's kit and brings none of its own.`);
67
68
  }
68
- let b = [...g.imports, ...g.dynamicImports].filter((e) => !u.includes(e));
69
- b.length && this.error(`${c.name} imports ${b.join(", ")}. An application imports ${u.join(", ")} and its own files.`);
69
+ let _ = [...f.imports, ...f.dynamicImports].filter((e) => !d.includes(e));
70
+ _.length && this.error(`${l.name} imports ${_.join(", ")}. An application imports ${d.join(", ")} and its own files.`);
70
71
  try {
71
- let e = await p(v);
72
- if (!t("0.17.0", e)) throw Error(`package.json requires vintage-frames ${e}, and the build is on ${n}: move the range`);
73
- y = await r({
72
+ let e = await m(y);
73
+ if (!n("0.18.0", e)) throw Error(`package.json requires vintage-frames ${e}, and the build is on ${r}: move the range`);
74
+ b = await i({
74
75
  manifest: {
75
- id: c.id,
76
- name: c.name,
77
- version: c.version,
76
+ id: l.id,
77
+ name: l.name,
78
+ version: l.version,
78
79
  requires: e,
79
- author: c.author
80
+ author: l.author,
81
+ description: l.description
80
82
  },
81
- icon: new Uint8Array(await a(l(v, m))),
82
- code: g.code,
83
- artwork: h ? new Uint8Array(await a(l(v, h))) : void 0
83
+ icon: new Uint8Array(await o(u(y, h))),
84
+ code: f.code,
85
+ artwork: g ? new Uint8Array(await o(u(y, g))) : void 0
84
86
  });
85
87
  } catch (e) {
86
- this.error(`${c.name}: ${e.message}`);
88
+ this.error(`${l.name}: ${e.message}`);
87
89
  }
88
- delete i[g.fileName], this.emitFile({
90
+ delete t[f.fileName], this.emitFile({
89
91
  type: "asset",
90
- fileName: _,
91
- source: y
92
+ fileName: v,
93
+ source: b
92
94
  });
93
95
  }
94
96
  },
95
97
  async writeBundle() {
96
- if (!g || !y) return;
97
- let e = l(v, g);
98
- await i(e, { recursive: !0 }), await o(s(e, _), y);
98
+ if (!_ || !b) return;
99
+ let e = u(y, _);
100
+ await a(e, { recursive: !0 }), await s(c(e, v), b);
99
101
  }
100
102
  };
101
103
  }
102
- function h() {
103
- let r = "";
104
+ function g() {
105
+ let t = "";
104
106
  return {
105
107
  name: "vintage-frames:app-files",
106
108
  enforce: "pre",
107
109
  configResolved(e) {
108
- r = e.root;
110
+ t = e.root;
109
111
  },
110
112
  async load(i) {
111
- let o = i.indexOf("?");
112
- if (o < 0) return null;
113
- let s = i.slice(0, o), l = new URLSearchParams(i.slice(o + 1));
114
- if (!s.endsWith(".png") || !(l.has("app") || l.has(d))) return null;
113
+ let a = i.indexOf("?");
114
+ if (a < 0) return null;
115
+ let s = i.slice(0, a), c = new URLSearchParams(i.slice(a + 1));
116
+ if (!s.endsWith(".png") || !(c.has("app") || c.has(f))) return null;
115
117
  this.addWatchFile(s);
116
- let u = c(r, s), f = await e(new Uint8Array(await a(s)));
117
- typeof f == "string" && this.error(`${u} is not an app file: ${f}.`);
118
- let { manifest: p, code: m } = f;
119
- return l.has(d) ? m : (t("0.17.0", p.requires) || this.error(`${u}: ${p.name} ${p.version} requires vintage-frames ${p.requires}, and this site has ${n}. Build ${p.name} on ${n} and copy its app file in.`), [
120
- `import factory from ${JSON.stringify(`${s}?${d}`)}`,
118
+ let u = l(t, s), d = await e(new Uint8Array(await o(s)));
119
+ typeof d == "string" && this.error(`${u} is not an app file: ${d}.`);
120
+ let { manifest: p, code: m } = d;
121
+ return c.has(f) ? m : (n("0.18.0", p.requires) || this.error(`${u}: ${p.name} ${p.version} requires vintage-frames ${p.requires}, and this site has ${r}. Build ${p.name} on ${r} and copy its app file in.`), [
122
+ `import factory from ${JSON.stringify(`${s}?${f}`)}`,
121
123
  `export const manifest = ${JSON.stringify(p)}`,
122
124
  "export default (...args) => ({ ...factory(...args), icon: manifest.icon })",
123
125
  ""
@@ -126,4 +128,4 @@ function h() {
126
128
  };
127
129
  }
128
130
  //#endregion
129
- export { m as appFile, h as appFiles };
131
+ export { h as appFile, g as appFiles };
@@ -51,7 +51,10 @@ export declare const modalDialogStyles: import("lit").CSSResult;
51
51
  * the reason. Because every close path — Escape, `close()`, the opt-in
52
52
  * {@link lightDismiss} click outside — routes through the native `close`
53
53
  * event, an Escape-close no longer leaves a stale origin behind, so the next
54
- * open re-derives it.
54
+ * open re-derives it. That event is queued: a modal opened again before it
55
+ * comes (`close()` then `show()` in one turn) runs the funnel at the open,
56
+ * inside `show()`, so the close's `vf-close` still comes before the open's
57
+ * `vf-show`, and the late event changes nothing.
55
58
  *
56
59
  * It also owns the two keyboard rules every classic dialog followed (see
57
60
  * {@link defaultButton} and {@link initialFocusTarget}): Return or Enter
@@ -242,11 +245,9 @@ export declare class VfModalDialog extends LitElement {
242
245
  /** Native `cancel` (Escape): remember the reason; `close` follows. */
243
246
  protected _onNativeCancel(): void;
244
247
  /**
245
- * Native `close` — the single teardown funnel for every close path. Drops
246
- * the written origin so the next open re-derives it from the box it will
247
- * actually have (a stated `top`/`left` is the author's or the user's and
248
- * survives, and is re-applied by that open), syncs `open`, and fires
249
- * `vf-close` with the reason and the value the close carried.
248
+ * Native `close`, which every close path ends in: syncs `open` and runs the
249
+ * close funnel. An open that came before the event has run the funnel for
250
+ * this close already, and the event passes.
250
251
  */
251
252
  protected _onNativeClose(): void;
252
253
  }
@@ -112,7 +112,7 @@ var p = 24, m = /* @__PURE__ */ new Set([
112
112
  this.#o = null, !(!t || e.target !== this._dialog || !this.lightDismiss) && (this.#n = "outside", this.close());
113
113
  }, this.#f = () => {
114
114
  this.#o = null;
115
- }, this.#p = null, this.#y = () => this.settle(), this.addEventListener("submit", this.#a);
115
+ }, this.#p = null, this.#m = !1, this.#b = () => this.settle(), this.addEventListener("submit", this.#a);
116
116
  }
117
117
  #a;
118
118
  #o;
@@ -133,41 +133,42 @@ var p = 24, m = /* @__PURE__ */ new Set([
133
133
  #d;
134
134
  #f;
135
135
  #p;
136
+ #m;
136
137
  show() {
137
- this.open = !0, this.hasUpdated && this.#v();
138
+ this.open = !0, this.hasUpdated && this.#y();
138
139
  }
139
140
  close(e) {
140
- this.open && e !== void 0 && (this.#r = e), this.open = !1, this.hasUpdated && this.#v();
141
+ this.open && e !== void 0 && (this.#r = e), this.open = !1, this.hasUpdated && this.#y();
141
142
  }
142
143
  closeFromBox(e) {
143
144
  this.open && (this.#i = e), this.close();
144
145
  }
145
146
  updated(e) {
146
- e.has("open") && this.#v(), (e.has("top") || e.has("left")) && this.settle(), this.#t();
147
+ e.has("open") && this.#y(), (e.has("top") || e.has("left")) && this.settle(), this.#t();
147
148
  }
148
- #m() {
149
+ #h() {
149
150
  let e = document.documentElement;
150
151
  return {
151
152
  width: o(e.clientWidth || window.innerWidth, this),
152
153
  height: o(e.clientHeight || window.innerHeight, this)
153
154
  };
154
155
  }
155
- #h(e, t) {
156
- let n = this.#m(), r = o(this._dialog?.offsetWidth ?? 0, this);
156
+ #g(e, t) {
157
+ let n = this.#h(), r = o(this._dialog?.offsetWidth ?? 0, this);
157
158
  return {
158
159
  x: Math.min(Math.max(e, p - r), n.width - p),
159
160
  y: Math.min(Math.max(t, 0), Math.max(0, n.height - p))
160
161
  };
161
162
  }
162
- #g() {
163
- let e = this.#m(), t = this._dialog?.getBoundingClientRect();
163
+ #_() {
164
+ let e = this.#h(), t = this._dialog?.getBoundingClientRect();
164
165
  return {
165
166
  x: (e.width - o(t?.width ?? 0, this)) / 2,
166
167
  y: (e.height - o(t?.height ?? 0, this)) / 2
167
168
  };
168
169
  }
169
170
  placeAt(e, t) {
170
- let n = this.#h(e, t);
171
+ let n = this.#g(e, t);
171
172
  this.left = i(n.x, this), this.top = i(n.y, this);
172
173
  }
173
174
  settle() {
@@ -176,10 +177,10 @@ var p = 24, m = /* @__PURE__ */ new Set([
176
177
  let t = this.left != null || this.top != null ? {
177
178
  x: this.left ?? 0,
178
179
  y: this.top ?? 0
179
- } : this.#g(), n = this.#h(t.x, t.y);
180
+ } : this.#_(), n = this.#g(t.x, t.y);
180
181
  e.style.left = a(i(n.x, this)), e.style.top = a(i(n.y, this)), e.style.right = "auto", e.style.bottom = "auto", e.style.margin = "0";
181
182
  }
182
- #_() {
183
+ #v() {
183
184
  let e = this._dialog?.style;
184
185
  if (e) for (let t of [
185
186
  "left",
@@ -189,27 +190,31 @@ var p = 24, m = /* @__PURE__ */ new Set([
189
190
  "margin"
190
191
  ]) e.removeProperty(t);
191
192
  }
192
- #v() {
193
+ #y() {
193
194
  let e = this._dialog;
194
195
  if (e) {
195
196
  if (this.open && !e.open) {
196
- this.#p = document.activeElement, this.returnValue = "", e.showModal();
197
+ if (this.#m) {
198
+ this.#T(), this.#y();
199
+ return;
200
+ }
201
+ this.#p = document.activeElement, this.returnValue = "", this.#n = null, this.#r = null, this.#i = null, e.showModal(), this.#m = !0;
197
202
  let n = this.#c();
198
- this.settle(), this.#S(e), this.#s.attach(), t(this, "vf-show", { focus: n }), n && _(n) && n.select?.();
203
+ this.settle(), this.#C(e), this.#s.attach(), t(this, "vf-show", { focus: n }), n && _(n) && n.select?.();
199
204
  } else !this.open && e.open && e.close();
200
205
  }
201
206
  }
202
- #y;
203
207
  #b;
204
208
  #x;
205
- #S(e) {
206
- typeof ResizeObserver < "u" && !this.#b && (this.#b = new ResizeObserver(this.#y), this.#b.observe(e)), window.addEventListener("resize", this.#y), this.#x ??= r(this.#y);
209
+ #S;
210
+ #C(e) {
211
+ typeof ResizeObserver < "u" && !this.#x && (this.#x = new ResizeObserver(this.#b), this.#x.observe(e)), window.addEventListener("resize", this.#b), this.#S ??= r(this.#b);
207
212
  }
208
- #C() {
209
- this.#b?.disconnect(), this.#b = void 0, window.removeEventListener("resize", this.#y), this.#x?.(), this.#x = void 0;
213
+ #w() {
214
+ this.#x?.disconnect(), this.#x = void 0, window.removeEventListener("resize", this.#b), this.#S?.(), this.#S = void 0;
210
215
  }
211
216
  disconnectedCallback() {
212
- super.disconnectedCallback(), this.#C();
217
+ super.disconnectedCallback(), this.#w();
213
218
  let e = this._dialog;
214
219
  if (e?.open) {
215
220
  e.close();
@@ -222,8 +227,11 @@ var p = 24, m = /* @__PURE__ */ new Set([
222
227
  this.#n = "escape";
223
228
  }
224
229
  _onNativeClose() {
230
+ !this.#m || this._dialog?.open || (this.open = !1, this.#T());
231
+ }
232
+ #T() {
225
233
  let e = this.#n ?? "close", n = this.#r, r = this.#i;
226
- this.#n = null, this.#r = null, this.#i = null, n !== null && (this.returnValue = n), this.#p = null, this.#o = null, this.#s.detach(), this.#C(), this.open = !1, this.#_(), t(this, "vf-close", {
234
+ this.#n = null, this.#r = null, this.#i = null, this.#m = !1, n !== null && (this.returnValue = n), this.#p = null, this.#o = null, this.#s.detach(), this.#w(), this.#v(), t(this, "vf-close", {
227
235
  reason: e,
228
236
  returnValue: n,
229
237
  ...r
@@ -4,13 +4,19 @@
4
4
  * JSON, and its code, one ES module compressed with zlib. Any PNG tool reads
5
5
  * the chunks and any decoder skips them.
6
6
  *
7
- * This is the reading half, and checking a version against a range. It is
7
+ * This is the reading half: the reader, checking a version against a range,
8
+ * putting versions in order, and the scale every box is drawn at. It is
8
9
  * pure and runs the same under Node and in a page, through the platform's
9
10
  * compression streams. Writing an app file, and the box, are
10
11
  * `vintage-frames/build`'s.
11
12
  */
12
13
  /** The app file format this kit reads and writes. */
13
14
  export declare const APP_FILE_FORMAT = 1;
15
+ /**
16
+ * One system px of an app file's box, in its picture's px: every box of
17
+ * this format is drawn at 3×, each system px a 3 × 3 block.
18
+ */
19
+ export declare const BOX_SCALE = 3;
14
20
  /** The iTXt keywords of the manifest's chunk and the code's. */
15
21
  export declare const MANIFEST_KEYWORD = "vintage-frames.app";
16
22
  export declare const CODE_KEYWORD = "vintage-frames.code";
@@ -24,6 +30,8 @@ export interface AppManifest {
24
30
  /** The kit range the code was built and checked against: `^0.17.0`. */
25
31
  requires: string;
26
32
  author: string;
33
+ /** What the application is, in a sentence or two of plain text. */
34
+ description?: string;
27
35
  /** Its 32 × 32 icon: its PNG as a `data:` URL. */
28
36
  icon: string;
29
37
  }
@@ -62,11 +70,25 @@ export declare function manifestOf(json: unknown): AppManifest | string;
62
70
  /**
63
71
  * The manifest and code an app file carries, or why it isn't one: not a
64
72
  * whole PNG, a chunk missing or doubled, a format this kit doesn't read, a
65
- * field missing, or code that doesn't inflate.
73
+ * field missing or unreadable, or code that doesn't inflate. The reason ends
74
+ * a sentence such as "Meteors.png is not an app file: …", and is for people:
75
+ * its wording can change in any release, so match on nothing in it.
66
76
  */
67
77
  export declare function inspectAppFile(bytes: Uint8Array): Promise<AppFile | string>;
68
78
  /** The manifest and code an app file carries, or null for any other PNG. */
69
79
  export declare function readAppFile(bytes: Uint8Array): Promise<AppFile | null>;
80
+ /**
81
+ * Whether `text` is a version exactly as semver writes one: `0.1.0`,
82
+ * `1.0.0-rc.1`, but not `v0.1.0`, ` 0.1.0` or `01.2.3`.
83
+ */
84
+ export declare function isVersion(text: string): boolean;
85
+ /**
86
+ * Below zero, zero or above as version `a` comes before, with or after `b`,
87
+ * in semver's order: a release after its prereleases, build metadata
88
+ * ignored. Anything that isn't a version, a string or not, comes before
89
+ * every version, so a sort never throws.
90
+ */
91
+ export declare function compareVersions(a: string, b: string): number;
70
92
  /**
71
93
  * Whether `version` meets `range`: an exact version, or a caret range as
72
94
  * npm reads it. `^1.2.3` takes 1.x from 1.2.3 on, `^0.2.3` takes 0.2.x from
@@ -1,27 +1,28 @@
1
1
  //#region src/shell/app-file.ts
2
- var e = 1, t = "vintage-frames.app", n = "vintage-frames.code", r = [
2
+ var e = 1, t = 3, n = "vintage-frames.app", r = "vintage-frames.code", i = [
3
3
  "id",
4
4
  "name",
5
5
  "version",
6
6
  "requires",
7
7
  "author",
8
+ "description",
8
9
  "icon"
9
- ], i = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10), a = Uint32Array.from({ length: 256 }, (e, t) => {
10
+ ], a = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10), o = Uint32Array.from({ length: 256 }, (e, t) => {
10
11
  let n = t;
11
12
  for (let e = 0; e < 8; e++) n = n & 1 ? 3988292384 ^ n >>> 1 : n >>> 1;
12
13
  return n >>> 0;
13
14
  });
14
- function o(e) {
15
+ function s(e) {
15
16
  let t = 4294967295;
16
- for (let n of e) t = a[(t ^ n) & 255] ^ t >>> 8;
17
+ for (let n of e) t = o[(t ^ n) & 255] ^ t >>> 8;
17
18
  return (t ^ 4294967295) >>> 0;
18
19
  }
19
- function s(e) {
20
- if (e.length < 8 || i.some((t, n) => e[n] !== t)) return null;
20
+ function c(e) {
21
+ if (e.length < 8 || a.some((t, n) => e[n] !== t)) return null;
21
22
  let t = new DataView(e.buffer, e.byteOffset, e.byteLength), n = [], r = 8;
22
23
  for (; r + 12 <= e.length;) {
23
24
  let i = r + 12 + t.getUint32(r);
24
- if (i > e.length || o(e.subarray(r + 4, i - 4)) !== t.getUint32(i - 4)) return null;
25
+ if (i > e.length || s(e.subarray(r + 4, i - 4)) !== t.getUint32(i - 4)) return null;
25
26
  let a = String.fromCharCode(...e.subarray(r + 4, r + 8));
26
27
  if (n.push({
27
28
  type: a,
@@ -33,85 +34,85 @@ function s(e) {
33
34
  }
34
35
  return null;
35
36
  }
36
- function c(e) {
37
+ function l(e) {
37
38
  let t = e.indexOf(0);
38
39
  return t > 0 ? String.fromCharCode(...e.subarray(0, t)) : "";
39
40
  }
40
- var l = new TextDecoder("utf-8", { fatal: !0 });
41
- async function u(e) {
41
+ var u = new TextDecoder("utf-8", { fatal: !0 });
42
+ async function d(e) {
42
43
  let t = e.indexOf(0), n = e[t + 1], r = e[t + 2], i = e.indexOf(0, t + 3), a = i < 0 ? -1 : e.indexOf(0, i + 1);
43
44
  if (t < 1 || a < 0 || n > 1 || r !== 0) throw Error("a broken iTXt chunk");
44
45
  let o = e.subarray(a + 1);
45
- return l.decode(n ? await d(o) : o);
46
+ return u.decode(n ? await f(o) : o);
46
47
  }
47
- async function d(e) {
48
+ async function f(e) {
48
49
  let t = new Blob([e.slice()]).stream().pipeThrough(new DecompressionStream("deflate"));
49
50
  return new Uint8Array(await new Response(t).arrayBuffer());
50
51
  }
51
- async function f(e) {
52
+ async function p(e) {
52
53
  let t = new Blob([e.slice()]).stream().pipeThrough(new CompressionStream("deflate"));
53
54
  return new Uint8Array(await new Response(t).arrayBuffer());
54
55
  }
55
- function p(e) {
56
+ function m(e) {
56
57
  if (!e || typeof e != "object") return "its manifest is not an object";
57
58
  let t = e;
58
59
  if (t.format !== 1) return typeof t.format == "number" ? `it is format ${t.format}, and this kit reads format 1` : "its manifest has no format";
59
- let n = {
60
- format: 1,
61
- id: "",
62
- name: "",
63
- version: "",
64
- requires: "",
65
- author: "",
66
- icon: ""
67
- };
68
- for (let e of r) {
60
+ let n = { format: 1 };
61
+ for (let e of i) {
69
62
  let r = t[e];
63
+ if (e === "description") {
64
+ if (r !== void 0 && typeof r != "string") return "its description is not text";
65
+ typeof r == "string" && r.trim() && (n.description = r);
66
+ continue;
67
+ }
70
68
  if (typeof r != "string" || !r) return `its manifest has no ${e}`;
69
+ if (e === "version" && !y(r)) return `its version, ${r}, is not a version`;
70
+ if (e === "requires" && !b(r)) return `its requires, ${r}, is not a range`;
71
71
  n[e] = r;
72
72
  }
73
73
  return n;
74
74
  }
75
- async function m(e) {
76
- let r = s(e);
77
- if (!r) return "it is not a whole PNG";
75
+ async function h(e) {
76
+ let t = c(e);
77
+ if (!t) return "it is not a whole PNG";
78
78
  let i = /* @__PURE__ */ new Map();
79
- for (let { type: e, data: t } of r) {
79
+ for (let { type: e, data: n } of t) {
80
80
  if (e !== "iTXt") continue;
81
- let n = c(t);
82
- if (n === "vintage-frames.app" || n === "vintage-frames.code") {
83
- if (i.has(n)) return `it has two ${n} chunks`;
84
- i.set(n, t);
81
+ let t = l(n);
82
+ if (t === "vintage-frames.app" || t === "vintage-frames.code") {
83
+ if (i.has(t)) return `it has two ${t} chunks`;
84
+ i.set(t, n);
85
85
  }
86
86
  }
87
- let a = i.get(t), o = i.get(n);
87
+ let a = i.get(n), o = i.get(r);
88
88
  if (!a && !o) return "it carries no application";
89
89
  if (!a) return "its manifest is missing";
90
90
  if (!o) return "its code is missing";
91
- let l;
91
+ let s;
92
92
  try {
93
- l = JSON.parse(await u(a));
93
+ s = JSON.parse(await d(a));
94
94
  } catch {
95
95
  return "its manifest does not read";
96
96
  }
97
- let d = p(l);
98
- if (typeof d == "string") return d;
97
+ let u = m(s);
98
+ if (typeof u == "string") return u;
99
99
  try {
100
100
  return {
101
- manifest: d,
102
- code: await u(o)
101
+ manifest: u,
102
+ code: await d(o)
103
103
  };
104
104
  } catch {
105
105
  return "its code does not inflate";
106
106
  }
107
107
  }
108
- async function h(e) {
109
- let t = await m(e);
108
+ async function g(e) {
109
+ let t = await h(e);
110
110
  return typeof t == "string" ? null : t;
111
111
  }
112
- var g = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z.-]+)?$/;
113
- function _(e) {
114
- let t = g.exec(e.trim());
112
+ var _ = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
113
+ function v(e) {
114
+ if (typeof e != "string") return null;
115
+ let t = _.exec(e.trim().replace(/^v/, ""));
115
116
  return t ? {
116
117
  core: [
117
118
  Number(t[1]),
@@ -121,7 +122,18 @@ function _(e) {
121
122
  pre: t[4] ? t[4].split(".") : []
122
123
  } : null;
123
124
  }
124
- function v(e, t) {
125
+ function y(e) {
126
+ return _.test(e);
127
+ }
128
+ function b(e) {
129
+ return y(e.startsWith("^") ? e.slice(1) : e);
130
+ }
131
+ function x(e) {
132
+ if (typeof e != "string") return null;
133
+ let t = e.trim();
134
+ return v(t.startsWith("^") ? t.slice(1) : t);
135
+ }
136
+ function S(e, t) {
125
137
  for (let n = 0; n < 3; n++) if (e.core[n] !== t.core[n]) return e.core[n] - t.core[n];
126
138
  if (!e.pre.length || !t.pre.length) return t.pre.length - e.pre.length;
127
139
  for (let n = 0; n < Math.max(e.pre.length, t.pre.length); n++) {
@@ -134,28 +146,32 @@ function v(e, t) {
134
146
  }
135
147
  return 0;
136
148
  }
137
- function y(e, t) {
138
- let n = t.trim().startsWith("^"), r = _(e), i = _(n ? t.trim().slice(1) : t);
139
- if (!r || !i) return !1;
140
- if (!n) return v(r, i) === 0;
141
- if (r.pre.length && !(i.pre.length && r.core.every((e, t) => e === i.core[t]))) return !1;
142
- let [a, o, s] = i.core, c = {
143
- core: a ? [
144
- a + 1,
149
+ function C(e, t) {
150
+ let n = v(e), r = v(t);
151
+ return n && r ? S(n, r) : !!n - +!!r;
152
+ }
153
+ function w(e, t) {
154
+ let n = v(e), r = x(t);
155
+ if (!n || !r) return !1;
156
+ if (!t.trim().startsWith("^")) return S(n, r) === 0;
157
+ if (n.pre.length && !(r.pre.length && n.core.every((e, t) => e === r.core[t]))) return !1;
158
+ let [i, a, o] = r.core, s = {
159
+ core: i ? [
160
+ i + 1,
145
161
  0,
146
162
  0
147
- ] : o ? [
163
+ ] : a ? [
148
164
  0,
149
- o + 1,
165
+ a + 1,
150
166
  0
151
167
  ] : [
152
168
  0,
153
169
  0,
154
- s + 1
170
+ o + 1
155
171
  ],
156
172
  pre: []
157
173
  };
158
- return v(r, i) >= 0 && v(r, c) < 0;
174
+ return S(n, r) >= 0 && S(n, s) < 0;
159
175
  }
160
176
  //#endregion
161
- export { e as APP_FILE_FORMAT, n as CODE_KEYWORD, t as MANIFEST_KEYWORD, i as PNG_SIGNATURE, o as crc32, f as deflate, d as inflate, m as inspectAppFile, c as itxtKeyword, p as manifestOf, s as pngChunks, h as readAppFile, y as satisfies };
177
+ export { e as APP_FILE_FORMAT, t as BOX_SCALE, r as CODE_KEYWORD, n as MANIFEST_KEYWORD, a as PNG_SIGNATURE, C as compareVersions, s as crc32, p as deflate, f as inflate, h as inspectAppFile, y as isVersion, l as itxtKeyword, m as manifestOf, c as pngChunks, g as readAppFile, w as satisfies };
@@ -25,6 +25,6 @@ export type { LocalStorageStateOptions, SavedSession, SavedWindow, SessionSnapsh
25
25
  export { BAND, CASCADE_SLOTS, CASCADE_STEP, ICON_CELL, ICON_COLUMN, ICON_INSET, ICON_ROW, NEAR, WINDOW_ORIGIN, cascadedBox, cascadeFrom, cascadeSlot, centeredBox, cleanUp, collisions, desktopLattice, fieldExtent, fillOrder, folderLattice, frameOf, isPin, latticeCell, latticeSlot, nearBox, nextFreeCell, pinOf, pinTo, trashCell, windowOrigin, } from './geometry.js';
26
26
  export type { Bands, Box, CascadeOptions, EdgePin, Frame, Lattice, LatticeOptions, Pin, Point, Policy, Size, } from './geometry.js';
27
27
  export { startClock, formatDate, formatTime, DATE_HOLD_MS } from './clock.js';
28
- export { APP_FILE_FORMAT, readAppFile, satisfies } from './app-file.js';
28
+ export { APP_FILE_FORMAT, BOX_SCALE, compareVersions, inspectAppFile, readAppFile, satisfies } from './app-file.js';
29
29
  export type { AppFile, AppManifest } from './app-file.js';
30
30
  export { VERSION } from './version.js';
@@ -7,6 +7,6 @@ import { createShell as ae, defineApp as oe } from "./shell.js";
7
7
  import { fileByDrag as se } from "./filing.js";
8
8
  import { FINDER as ce, finder as le } from "./finder.js";
9
9
  import { indexedDbStorage as ue } from "./storage.js";
10
- import { APP_FILE_FORMAT as de, readAppFile as fe, satisfies as pe } from "./app-file.js";
11
- import { VERSION as me } from "./version.js";
12
- export { de as APP_FILE_FORMAT, e as APP_KIND, b as BAND, x as CASCADE_SLOTS, S as CASCADE_STEP, te as DATE_HOLD_MS, t as DISK, ce as FINDER, n as FOLDER, C as ICON_CELL, w as ICON_COLUMN, T as ICON_INSET, E as ICON_ROW, D as NEAR, r as TRASH, i as UNTITLED_FOLDER, me as VERSION, O as WINDOW_ORIGIN, a as appIdOf, k as cascadeFrom, A as cascadeSlot, j as cascadedBox, M as centeredBox, o as childrenOf, N as cleanUp, P as collisions, s as containerOf, c as copyName, l as createCatalog, ae as createShell, Y as createWindowManager, oe as defineApp, u as descendantsOf, F as desktopLattice, d as enclosingFolders, I as fieldExtent, se as fileByDrag, L as fillOrder, le as finder, X as focusInto, R as folderLattice, ne as formatDate, re as formatTime, z as frameOf, ue as indexedDbStorage, f as isContainerKind, p as isInside, B as isPin, m as isTrashed, h as isVolume, g as itemCount, _ as itemOf, V as latticeCell, H as latticeSlot, Z as localStorageState, v as memoryStorage, Q as mergeSession, U as nearBox, y as nextFolderName, W as nextFreeCell, $ as openWindowsOf, G as pinOf, K as pinTo, fe as readAppFile, ee as readSession, pe as satisfies, ie as startClock, q as trashCell, J as windowOrigin };
10
+ import { APP_FILE_FORMAT as de, BOX_SCALE as fe, compareVersions as pe, inspectAppFile as me, readAppFile as he, satisfies as ge } from "./app-file.js";
11
+ import { VERSION as _e } from "./version.js";
12
+ export { de as APP_FILE_FORMAT, e as APP_KIND, b as BAND, fe as BOX_SCALE, x as CASCADE_SLOTS, S as CASCADE_STEP, te as DATE_HOLD_MS, t as DISK, ce as FINDER, n as FOLDER, C as ICON_CELL, w as ICON_COLUMN, T as ICON_INSET, E as ICON_ROW, D as NEAR, r as TRASH, i as UNTITLED_FOLDER, _e as VERSION, O as WINDOW_ORIGIN, a as appIdOf, k as cascadeFrom, A as cascadeSlot, j as cascadedBox, M as centeredBox, o as childrenOf, N as cleanUp, P as collisions, pe as compareVersions, s as containerOf, c as copyName, l as createCatalog, ae as createShell, Y as createWindowManager, oe as defineApp, u as descendantsOf, F as desktopLattice, d as enclosingFolders, I as fieldExtent, se as fileByDrag, L as fillOrder, le as finder, X as focusInto, R as folderLattice, ne as formatDate, re as formatTime, z as frameOf, ue as indexedDbStorage, me as inspectAppFile, f as isContainerKind, p as isInside, B as isPin, m as isTrashed, h as isVolume, g as itemCount, _ as itemOf, V as latticeCell, H as latticeSlot, Z as localStorageState, v as memoryStorage, Q as mergeSession, U as nearBox, y as nextFolderName, W as nextFreeCell, $ as openWindowsOf, G as pinOf, K as pinTo, he as readAppFile, ee as readSession, ge as satisfies, ie as startClock, q as trashCell, J as windowOrigin };
@@ -8,6 +8,6 @@
8
8
  export * from './catalog.js';
9
9
  export * from './geometry.js';
10
10
  export * from './state.js';
11
- export { APP_FILE_FORMAT, readAppFile, satisfies } from './app-file.js';
11
+ export { APP_FILE_FORMAT, BOX_SCALE, compareVersions, inspectAppFile, readAppFile, satisfies } from './app-file.js';
12
12
  export type { AppFile, AppManifest } from './app-file.js';
13
13
  export { VERSION } from './version.js';
@@ -1,6 +1,6 @@
1
1
  import { APP_KIND as e, DISK as t, FOLDER as n, TRASH as r, UNTITLED_FOLDER as i, appIdOf as a, childrenOf as o, containerOf as s, copyName as c, createCatalog as l, descendantsOf as u, enclosingFolders as d, isContainerKind as f, isInside as p, isTrashed as m, isVolume as h, itemCount as g, itemOf as _, memoryStorage as v, nextFolderName as y } from "./catalog.js";
2
2
  import { BAND as b, CASCADE_SLOTS as x, CASCADE_STEP as S, ICON_CELL as C, ICON_COLUMN as w, ICON_INSET as T, ICON_ROW as E, NEAR as D, WINDOW_ORIGIN as O, cascadeFrom as k, cascadeSlot as A, cascadedBox as j, centeredBox as M, cleanUp as N, collisions as P, desktopLattice as F, fieldExtent as I, fillOrder as L, folderLattice as R, frameOf as z, isPin as B, latticeCell as V, latticeSlot as H, nearBox as U, nextFreeCell as W, pinOf as G, pinTo as K, trashCell as q, windowOrigin as J } from "./geometry.js";
3
3
  import { localStorageState as Y, mergeSession as X, openWindowsOf as Z, readSession as Q } from "./state.js";
4
- import { APP_FILE_FORMAT as $, readAppFile as ee, satisfies as te } from "./app-file.js";
5
- import { VERSION as ne } from "./version.js";
6
- export { $ as APP_FILE_FORMAT, e as APP_KIND, b as BAND, x as CASCADE_SLOTS, S as CASCADE_STEP, t as DISK, n as FOLDER, C as ICON_CELL, w as ICON_COLUMN, T as ICON_INSET, E as ICON_ROW, D as NEAR, r as TRASH, i as UNTITLED_FOLDER, ne as VERSION, O as WINDOW_ORIGIN, a as appIdOf, k as cascadeFrom, A as cascadeSlot, j as cascadedBox, M as centeredBox, o as childrenOf, N as cleanUp, P as collisions, s as containerOf, c as copyName, l as createCatalog, u as descendantsOf, F as desktopLattice, d as enclosingFolders, I as fieldExtent, L as fillOrder, R as folderLattice, z as frameOf, f as isContainerKind, p as isInside, B as isPin, m as isTrashed, h as isVolume, g as itemCount, _ as itemOf, V as latticeCell, H as latticeSlot, Y as localStorageState, v as memoryStorage, X as mergeSession, U as nearBox, y as nextFolderName, W as nextFreeCell, Z as openWindowsOf, G as pinOf, K as pinTo, ee as readAppFile, Q as readSession, te as satisfies, q as trashCell, J as windowOrigin };
4
+ import { APP_FILE_FORMAT as $, BOX_SCALE as ee, compareVersions as te, inspectAppFile as ne, readAppFile as re, satisfies as ie } from "./app-file.js";
5
+ import { VERSION as ae } from "./version.js";
6
+ export { $ as APP_FILE_FORMAT, e as APP_KIND, b as BAND, ee as BOX_SCALE, x as CASCADE_SLOTS, S as CASCADE_STEP, t as DISK, n as FOLDER, C as ICON_CELL, w as ICON_COLUMN, T as ICON_INSET, E as ICON_ROW, D as NEAR, r as TRASH, i as UNTITLED_FOLDER, ae as VERSION, O as WINDOW_ORIGIN, a as appIdOf, k as cascadeFrom, A as cascadeSlot, j as cascadedBox, M as centeredBox, o as childrenOf, N as cleanUp, P as collisions, te as compareVersions, s as containerOf, c as copyName, l as createCatalog, u as descendantsOf, F as desktopLattice, d as enclosingFolders, I as fieldExtent, L as fillOrder, R as folderLattice, z as frameOf, ne as inspectAppFile, f as isContainerKind, p as isInside, B as isPin, m as isTrashed, h as isVolume, g as itemCount, _ as itemOf, V as latticeCell, H as latticeSlot, Y as localStorageState, v as memoryStorage, X as mergeSession, U as nearBox, y as nextFolderName, W as nextFreeCell, Z as openWindowsOf, G as pinOf, K as pinTo, re as readAppFile, Q as readSession, ie as satisfies, q as trashCell, J as windowOrigin };
@@ -144,7 +144,7 @@ function d(l, d) {
144
144
  let n = (r) => {
145
145
  r.target === t && (t.removeEventListener("vf-close", n), e(r.detail.returnValue));
146
146
  };
147
- t.addEventListener("vf-close", n), t.show();
147
+ t.show(), t.addEventListener("vf-close", n);
148
148
  });
149
149
  },
150
150
  gate(e, t) {
@@ -1,2 +1,2 @@
1
1
  /** The kit's version, as published: package.json's `version`. `npm version` rewrites it (the `version` script). */
2
- export declare const VERSION = "0.17.0";
2
+ export declare const VERSION = "0.18.0";
@@ -1,4 +1,4 @@
1
1
  //#region src/shell/version.ts
2
- var e = "0.17.0";
2
+ var e = "0.18.0";
3
3
  //#endregion
4
4
  export { e as VERSION };
package/docs/SPEC.md CHANGED
@@ -234,7 +234,7 @@ The desktop-window shell: the classic document window (see DragThing screenshot)
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
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` — the first slotted control carrying `autofocus` or `autoselect`, 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. A field marked `autoselect` has its whole text selected on every open, right after `vf-show` and once the field has rendered, so a value set before `show()` or in a `vf-show` handler is the text selected. Otherwise the modal selects no text: whether a field opens selected is the page's call. 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.
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` — the first slotted control carrying `autofocus` or `autoselect`, 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. A field marked `autoselect` has its whole text selected on every open, right after `vf-show` and once the field has rendered, so a value set before `show()` or in a `vf-show` handler is the text selected. Otherwise the modal selects no text: whether a field opens selected is the page's call. 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. **Opened again before a close's `close` event comes** — the native event is queued, so a `close()` and a `show()` in one turn do it — the open runs the funnel for that close first: its `vf-close` comes before the open's `vf-show`, and the late event finds the modal open and changes nothing, where it used to close the dialog that had just opened. That `vf-close` fires inside `show()`, so a one-time listener for the open's own answer goes on after `show()` (the shell's `ask()` does). The open drops any close value, reason or keys still pending, so a close asked for in that `vf-close` leaves nothing for a later one. `npm run verify:dialog`, REOPEN. **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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
3
3
  "name": "vintage-frames",
4
- "version": "0.17.0",
4
+ "version": "0.18.0",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vintage-frames",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Lit web components that emulate the look and feel of classic Mac OS System 7",
5
5
  "type": "module",
6
6
  "license": "MIT",