vintage-frames 0.5.1 → 0.5.3

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.
@@ -2237,7 +2237,7 @@
2237
2237
  "declarations": [
2238
2238
  {
2239
2239
  "kind": "class",
2240
- "description": "`<vf-desktop>` — the full-bleed classic desktop container.\n\nRenders the desktop pattern — the classic 50% dither by default, or any of\nthe standard patterns by name (`pattern`, System 7's General Controls\nsetting) — and manages the stacking order and `active` state of slotted\n`vf-window` children: a `pointerdown` or\n`focusin` (keyboard focus) anywhere inside a window brings it to the front\nand makes it the single active window. The windows' light-DOM order is kept\nin step with the stacking order (bottom-most first, at pointer-gesture\nends), so tabbing walks the stack the way the eye does and Shift+Tab is\nits exact mirror.\n\nUtility windows (`vf-window[variant=\"utility\"]`) stack in a floating tier\nabove every document-tier window, restack only among themselves, and stand\noutside the single-active invariant entirely — clicking a palette neither\ndeactivates the active document window nor greys the palette, exactly as\nSystem 7's floating windoids behaved while their application was frontmost.\n\n**Deactivation.** On a real System 7 machine clicking the desktop clicked\nthe *Finder* — the frontmost application's windows lost their stripes.\nclearActive is that gesture's handler: it clears `active` from\nthe whole document tier, and **zero active windows is a legal state**,\nheld until a press or keyboard focus re-enters a document window (or a\nnew one is slotted, which activates it — opening a window brings its\napplication forward). The desktop never takes this decision itself: its\nfurniture is slotted light DOM (an icon layer, say), so only the page\nknows which of its children — or which presses on the bare dither — mean\n\"the Finder\", and it routes those through `clearActive()`. Left alone,\nthe classic always-one-active behavior is unchanged. activeWindow\nreads the current holder, and every change of holder — including to and\nfrom none — fires `vf-activate`.\n\nThe desktop is a raster with an explicit size, always: **`width` and\n`height`**, in system px, the way a WIND resource declared a window's —\nthe host box renders at the declared screen plus `2 × bezel` per axis, a\nwhole number of system pixels by construction (default 512×342, the\ncompact Mac's screen). Pure CSS sizing is not supported; the page sets\nthe numbers — directly, or via VfDesktop.fitWithin on\n`resize`/`onScaleChange` for a viewport-filling desktop — and positions\nthe sized box with its own stylesheet, keeping any sub-system-pixel\nslack on its side. `bezel` (system px) draws the black screen surround —\nthe CRT's unlit margin — around the screen, rounding its top corners\nwith the classic corner mask.\n\n**`pattern`** names the desktop pattern — `gray-50` (the dither) by\ndefault, any of the 38 standard patterns (docs/PATTERNS.md), or sixteen\nhex digits stating a custom 8×8 pattern, as on `vf-container`. It is\npainted as the screen's own background: black ink on an opaque white\npaper, one whole-surface raster at one image px per system px, 1-bit at\nevery density and zoom (src/pattern-fill.ts).\n\nCustom properties:\n- `--vf-desktop-pattern` — a consumer's own tile art in place of the\n pattern (the kit's default is a 1-bit 50% checker, opaque\n black-on-white on a 30-system-px tile). Set, it wins over `pattern` and\n renders as a placed tile grid at that same 30-px geometry\n (src/tile-grid.ts); a token swapped at runtime without touching the\n component wants a `requestUpdate()`.\n- `--vf-desktop` — base color painted *under* the pattern (default\n `#808080`). The pattern's paper is opaque, so this only becomes visible\n when `--vf-desktop-pattern` is overridden with a tile that has\n transparent cells (or with `none`).",
2240
+ "description": "`<vf-desktop>` — the full-bleed classic desktop container.\n\nRenders the desktop pattern — the classic 50% dither by default, or any of\nthe standard patterns by name (`pattern`, System 7's General Controls\nsetting) — and manages the stacking order and `active` state of slotted\n`vf-window` children: a `pointerdown` or\n`focusin` (keyboard focus) anywhere inside a window brings it to the front\nand makes it the single active window. The windows' light-DOM order is kept\nin step with the stacking order (bottom-most first, at pointer-gesture\nends), so tabbing walks the stack the way the eye does and Shift+Tab is\nits exact mirror.\n\nUtility windows (`vf-window[variant=\"utility\"]`) stack in a floating tier\nabove every document-tier window, restack only among themselves, and stand\noutside the single-active invariant entirely — clicking a palette neither\ndeactivates the active document window nor greys the palette, exactly as\nSystem 7's floating windoids behaved while their application was frontmost.\nA slotted `vf-menu-bar` (or a free-standing `vf-menu`) sits on a tier\nabove both, so its dropped menus cover palettes and document windows\nalike; only the screen-corner mask is in front of it.\n\n**Deactivation.** On a real System 7 machine clicking the desktop clicked\nthe *Finder* — the frontmost application's windows lost their stripes.\nclearActive is that gesture's handler: it clears `active` from\nthe whole document tier, and **zero active windows is a legal state**,\nheld until a press or keyboard focus re-enters a document window (or a\nnew one is slotted, which activates it — opening a window brings its\napplication forward). The desktop never takes this decision itself: its\nfurniture is slotted light DOM (an icon layer, say), so only the page\nknows which of its children — or which presses on the bare dither — mean\n\"the Finder\", and it routes those through `clearActive()`. Left alone,\nthe classic always-one-active behavior is unchanged. activeWindow\nreads the current holder, and every change of holder — including to and\nfrom none — fires `vf-activate`.\n\nThe desktop is a raster with an explicit size, always: **`width` and\n`height`**, in system px, the way a WIND resource declared a window's —\nthe host box renders at the declared screen plus `2 × bezel` per axis, a\nwhole number of system pixels by construction (default 512×342, the\ncompact Mac's screen). Pure CSS sizing is not supported; the page sets\nthe numbers — directly, or via VfDesktop.fitWithin on\n`resize`/`onScaleChange` for a viewport-filling desktop — and positions\nthe sized box with its own stylesheet, keeping any sub-system-pixel\nslack on its side. `bezel` (system px) draws the black screen surround —\nthe CRT's unlit margin — around the screen, rounding its top corners\nwith the classic corner mask.\n\n**`pattern`** names the desktop pattern — `gray-50` (the dither) by\ndefault, any of the 38 standard patterns (docs/PATTERNS.md), or sixteen\nhex digits stating a custom 8×8 pattern, as on `vf-container`. It is\npainted as the screen's own background: black ink on an opaque white\npaper, one whole-surface raster at one image px per system px, 1-bit at\nevery density and zoom (src/pattern-fill.ts).\n\nCustom properties:\n- `--vf-desktop-pattern` — a consumer's own tile art in place of the\n pattern (the kit's default is a 1-bit 50% checker, opaque\n black-on-white on a 30-system-px tile). Set, it wins over `pattern` and\n renders as a placed tile grid at that same 30-px geometry\n (src/tile-grid.ts); a token swapped at runtime without touching the\n component wants a `requestUpdate()`.\n- `--vf-desktop` — base color painted *under* the pattern (default\n `#808080`). The pattern's paper is opaque, so this only becomes visible\n when `--vf-desktop-pattern` is overridden with a tile that has\n transparent cells (or with `none`).",
2241
2241
  "name": "VfDesktop",
2242
2242
  "cssProperties": [
2243
2243
  {
@@ -2845,7 +2845,7 @@
2845
2845
  "declarations": [
2846
2846
  {
2847
2847
  "kind": "class",
2848
- "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. A `heading` renders as a centered\n display-face heading at the top of the body (the reference art's \"Dialog\n title\"); `closable` is ignored, there being no bar to carry the widget.\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' }`.",
2848
+ "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. A `heading` renders as a centered\n display-face heading at the top of the body (the reference art's \"Dialog\n title\"); `closable` is ignored, there being no bar to carry the widget.\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.",
2849
2849
  "name": "VfDialog",
2850
2850
  "cssProperties": [
2851
2851
  {
@@ -3129,6 +3129,21 @@
3129
3129
  "module": "src/modal-dialog.ts"
3130
3130
  }
3131
3131
  },
3132
+ {
3133
+ "kind": "field",
3134
+ "name": "lightDismiss",
3135
+ "type": {
3136
+ "text": "boolean"
3137
+ },
3138
+ "default": "false",
3139
+ "description": "Close on a click outside the frame — a press on the backdrop — and fire\n`vf-close` with `{ reason: 'outside' }`. Off by default: the classic modal\nignored an outside click (and beeped), and a dialog that asks a question\nshould keep ignoring it. Opt in for the About box and the splash, the\ndialogs the classic Mac did dismiss on a click.\n\nBoth halves of the click have to land outside — the two-step the\nplatform's own `closedby=\"any\"` light dismiss uses — so a press that\nstarts on a control and slides off the frame, or a title-bar drag\nreleased past it, leaves the dialog open. The press is consumed either\nway: a modal's backdrop lets nothing beneath it see the click, so\ndismissing the About box does not also select whatever was under the\npointer. Escape closes the dialog regardless, as it always has.",
3140
+ "attribute": "light-dismiss",
3141
+ "reflects": true,
3142
+ "inheritedFrom": {
3143
+ "name": "VfModalDialog",
3144
+ "module": "src/modal-dialog.ts"
3145
+ }
3146
+ },
3132
3147
  {
3133
3148
  "kind": "field",
3134
3149
  "name": "_dialog",
@@ -3198,6 +3213,61 @@
3198
3213
  "module": "src/modal-dialog.ts"
3199
3214
  }
3200
3215
  },
3216
+ {
3217
+ "kind": "field",
3218
+ "name": "#outsidePress",
3219
+ "privacy": "private",
3220
+ "type": {
3221
+ "text": "number | null"
3222
+ },
3223
+ "default": "null",
3224
+ "description": "The `pointerId` of a press that landed on the backdrop, held until its\nrelease decides whether the click was outside; null while nothing is\npending. See lightDismiss.",
3225
+ "inheritedFrom": {
3226
+ "name": "VfModalDialog",
3227
+ "module": "src/modal-dialog.ts"
3228
+ }
3229
+ },
3230
+ {
3231
+ "kind": "field",
3232
+ "name": "#dismissListeners",
3233
+ "privacy": "private",
3234
+ "readonly": true,
3235
+ "default": "new DocumentListenersController(this, () => [ [this._dialog, 'pointerdown', this.#onDialogPointerDown], [this._dialog, 'pointerup', this.#onDialogPointerUp], [this._dialog, 'pointercancel', this.#onDialogPointerCancel], ])",
3236
+ "description": "The light-dismiss listeners, on the `<dialog>` itself and only while it is\nopen. The `::backdrop` hit-tests as its dialog, and the frame the subclass\nrenders fills the dialog's box, so the dialog is an event's *target* only\nwhen the pointer was outside the frame — no rect arithmetic, no\ncomposedPath. Attached whether or not lightDismiss is set: the\nhandlers read the property at the release, so it can be toggled on an\nopen dialog.",
3237
+ "inheritedFrom": {
3238
+ "name": "VfModalDialog",
3239
+ "module": "src/modal-dialog.ts"
3240
+ }
3241
+ },
3242
+ {
3243
+ "kind": "field",
3244
+ "name": "#onDialogPointerDown",
3245
+ "privacy": "private",
3246
+ "description": "A press on the backdrop arms the dismissal. A press anywhere else — the\nframe, a control, the title bar — disarms it, so a stale arm (a press the\nplatform never released to us, a right-click's on macOS) can't survive to\nthe next release.",
3247
+ "inheritedFrom": {
3248
+ "name": "VfModalDialog",
3249
+ "module": "src/modal-dialog.ts"
3250
+ }
3251
+ },
3252
+ {
3253
+ "kind": "field",
3254
+ "name": "#onDialogPointerUp",
3255
+ "privacy": "private",
3256
+ "description": "The release completes it — the same press *and* release on the backdrop.\nDeliberately not the `click` event: UI Events dispatches a\npress-drag-release click at the common ancestor of the two targets, which\nfor a press on the frame released outside is the dialog itself, and that\nwould dismiss on exactly the gesture the two-step rule exists to ignore.",
3257
+ "inheritedFrom": {
3258
+ "name": "VfModalDialog",
3259
+ "module": "src/modal-dialog.ts"
3260
+ }
3261
+ },
3262
+ {
3263
+ "kind": "field",
3264
+ "name": "#onDialogPointerCancel",
3265
+ "privacy": "private",
3266
+ "inheritedFrom": {
3267
+ "name": "VfModalDialog",
3268
+ "module": "src/modal-dialog.ts"
3269
+ }
3270
+ },
3201
3271
  {
3202
3272
  "kind": "field",
3203
3273
  "name": "#invoker",
@@ -3477,7 +3547,7 @@
3477
3547
  ],
3478
3548
  "events": [
3479
3549
  {
3480
- "description": "Dialog closed. Detail `{ reason: 'escape' | 'close' }`.",
3550
+ "description": "Dialog closed. Detail `{ reason: 'escape' | 'close' | 'outside' }` — `'outside'` only under `light-dismiss`.",
3481
3551
  "name": "vf-close",
3482
3552
  "inheritedFrom": {
3483
3553
  "name": "VfModalDialog",
@@ -3581,6 +3651,19 @@
3581
3651
  "name": "VfModalDialog",
3582
3652
  "module": "src/modal-dialog.ts"
3583
3653
  }
3654
+ },
3655
+ {
3656
+ "name": "light-dismiss",
3657
+ "type": {
3658
+ "text": "boolean"
3659
+ },
3660
+ "default": "false",
3661
+ "description": "Close on a click outside the frame — a press on the backdrop — and fire\n`vf-close` with `{ reason: 'outside' }`. Off by default: the classic modal\nignored an outside click (and beeped), and a dialog that asks a question\nshould keep ignoring it. Opt in for the About box and the splash, the\ndialogs the classic Mac did dismiss on a click.\n\nBoth halves of the click have to land outside — the two-step the\nplatform's own `closedby=\"any\"` light dismiss uses — so a press that\nstarts on a control and slides off the frame, or a title-bar drag\nreleased past it, leaves the dialog open. The press is consumed either\nway: a modal's backdrop lets nothing beneath it see the click, so\ndismissing the About box does not also select whatever was under the\npointer. Escape closes the dialog regardless, as it always has.",
3662
+ "fieldName": "lightDismiss",
3663
+ "inheritedFrom": {
3664
+ "name": "VfModalDialog",
3665
+ "module": "src/modal-dialog.ts"
3666
+ }
3584
3667
  }
3585
3668
  ],
3586
3669
  "superclass": {
@@ -6943,7 +7026,7 @@
6943
7026
  "declarations": [
6944
7027
  {
6945
7028
  "kind": "class",
6946
- "description": "`<vf-menu>` — a pull-down menu: a bar label plus a dropped panel of\n`<vf-menu-item>` / `<vf-separator>` children.\n\nInside a `<vf-menu-bar>` the bar coordinates open state (only one menu open,\nhover-switching, outside-click/Escape dismissal) and owns the pointer\ngesture, which may travel between its menus. Used standalone, the menu\ntoggles itself on label click and manages its own dismissal, its own press\ngesture and item keyboard navigation (ArrowUp/ArrowDown, Home/End, and the\nshared first-letter type-ahead — src/type-ahead.ts) while open.\n\nPointer — the two styles `vf-select` supports, on the same terms (see\nsrc/menu-press.ts): the System 7 press-drag-release (press the title, slide\nonto a command, release over it) and a modern quick tap that leaves the menu\ndropped for a second click.\n\nTakes `top`/`left` like every other element (VfPositioned) — the\nnatural fit being a standalone menu, which is a free-standing menu button and\nplaces like any other control. The panel is anchored to the host's own box\n(`top: 100%`), so it follows a placed menu down without any further work.\nInside a `<vf-menu-bar>`, the bar lays its titles out in flow — placing one\nlifts it off the bar and the titles beside it close the gap.",
7029
+ "description": "`<vf-menu>` — a pull-down menu: a bar label plus a dropped panel of\n`<vf-menu-item>` / `<vf-separator>` children.\n\nInside a `<vf-menu-bar>` the bar coordinates open state (only one menu open,\nhover-switching, outside-click/Escape dismissal) and owns the pointer\ngesture, which may travel between its menus. Used standalone, the menu\ntoggles itself on label click and manages its own dismissal, its own press\ngesture and item keyboard navigation (ArrowUp/ArrowDown, Home/End, and the\nshared first-letter type-ahead — src/type-ahead.ts) while open.\n\nPointer — the two styles `vf-select` supports, on the same terms (see\nsrc/menu-press.ts): the System 7 press-drag-release (press the title, slide\nonto a command, release over it) and a modern quick tap that leaves the menu\ndropped for a second click.\n\nTakes `top`/`left` like every other element (VfPositioned) — the\nnatural fit being a standalone menu, which is a free-standing menu button and\nplaces like any other control. The panel is anchored to the host's own box\n(`top: 100%`), so it follows a placed menu down without any further work.\nInside a `<vf-menu-bar>`, the bar lays its titles out in flow — placing one\nlifts it off the bar and the titles beside it close the gap. Slotted straight\ninto a `<vf-desktop>`, a free-standing menu sits on the desktop's menu tier\nabove every window, palettes included, as a bar does.",
6947
7030
  "name": "VfMenu",
6948
7031
  "cssProperties": [
6949
7032
  {
@@ -19702,7 +19785,7 @@
19702
19785
  },
19703
19786
  {
19704
19787
  "kind": "class",
19705
- "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()`, backdrop — routes\nthrough the native `close` event, an Escape-close no longer leaves a stale\norigin behind, so the next open re-derives it.\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\nSubclasses supply only the frame chrome: a `render()` returning\n`<dialog",
19788
+ "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\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\nSubclasses supply only the frame chrome: a `render()` returning\n`<dialog",
19706
19789
  "name": "VfModalDialog",
19707
19790
  "members": [
19708
19791
  {
@@ -19760,6 +19843,17 @@
19760
19843
  "description": "Offset from the left of the viewport, in whole system px. See top.",
19761
19844
  "attribute": "left"
19762
19845
  },
19846
+ {
19847
+ "kind": "field",
19848
+ "name": "lightDismiss",
19849
+ "type": {
19850
+ "text": "boolean"
19851
+ },
19852
+ "default": "false",
19853
+ "description": "Close on a click outside the frame — a press on the backdrop — and fire\n`vf-close` with `{ reason: 'outside' }`. Off by default: the classic modal\nignored an outside click (and beeped), and a dialog that asks a question\nshould keep ignoring it. Opt in for the About box and the splash, the\ndialogs the classic Mac did dismiss on a click.\n\nBoth halves of the click have to land outside — the two-step the\nplatform's own `closedby=\"any\"` light dismiss uses — so a press that\nstarts on a control and slides off the frame, or a title-bar drag\nreleased past it, leaves the dialog open. The press is consumed either\nway: a modal's backdrop lets nothing beneath it see the click, so\ndismissing the About box does not also select whatever was under the\npointer. Escape closes the dialog regardless, as it always has.",
19854
+ "attribute": "light-dismiss",
19855
+ "reflects": true
19856
+ },
19763
19857
  {
19764
19858
  "kind": "field",
19765
19859
  "name": "_dialog",
@@ -19809,6 +19903,41 @@
19809
19903
  "default": "null",
19810
19904
  "description": "Close reason pending for the next native `close` event."
19811
19905
  },
19906
+ {
19907
+ "kind": "field",
19908
+ "name": "#outsidePress",
19909
+ "privacy": "private",
19910
+ "type": {
19911
+ "text": "number | null"
19912
+ },
19913
+ "default": "null",
19914
+ "description": "The `pointerId` of a press that landed on the backdrop, held until its\nrelease decides whether the click was outside; null while nothing is\npending. See lightDismiss."
19915
+ },
19916
+ {
19917
+ "kind": "field",
19918
+ "name": "#dismissListeners",
19919
+ "privacy": "private",
19920
+ "readonly": true,
19921
+ "default": "new DocumentListenersController(this, () => [ [this._dialog, 'pointerdown', this.#onDialogPointerDown], [this._dialog, 'pointerup', this.#onDialogPointerUp], [this._dialog, 'pointercancel', this.#onDialogPointerCancel], ])",
19922
+ "description": "The light-dismiss listeners, on the `<dialog>` itself and only while it is\nopen. The `::backdrop` hit-tests as its dialog, and the frame the subclass\nrenders fills the dialog's box, so the dialog is an event's *target* only\nwhen the pointer was outside the frame — no rect arithmetic, no\ncomposedPath. Attached whether or not lightDismiss is set: the\nhandlers read the property at the release, so it can be toggled on an\nopen dialog."
19923
+ },
19924
+ {
19925
+ "kind": "field",
19926
+ "name": "#onDialogPointerDown",
19927
+ "privacy": "private",
19928
+ "description": "A press on the backdrop arms the dismissal. A press anywhere else — the\nframe, a control, the title bar — disarms it, so a stale arm (a press the\nplatform never released to us, a right-click's on macOS) can't survive to\nthe next release."
19929
+ },
19930
+ {
19931
+ "kind": "field",
19932
+ "name": "#onDialogPointerUp",
19933
+ "privacy": "private",
19934
+ "description": "The release completes it — the same press *and* release on the backdrop.\nDeliberately not the `click` event: UI Events dispatches a\npress-drag-release click at the common ancestor of the two targets, which\nfor a press on the frame released outside is the dialog itself, and that\nwould dismiss on exactly the gesture the two-step rule exists to ignore."
19935
+ },
19936
+ {
19937
+ "kind": "field",
19938
+ "name": "#onDialogPointerCancel",
19939
+ "privacy": "private"
19940
+ },
19812
19941
  {
19813
19942
  "kind": "field",
19814
19943
  "name": "#invoker",
@@ -20020,7 +20149,7 @@
20020
20149
  ],
20021
20150
  "events": [
20022
20151
  {
20023
- "description": "The modal closed. `detail: { reason: 'escape' | 'close' }`.",
20152
+ "description": "The modal closed. `detail: { reason: 'escape' | 'close' | 'outside' }`.",
20024
20153
  "name": "vf-close"
20025
20154
  }
20026
20155
  ],
@@ -20065,6 +20194,15 @@
20065
20194
  },
20066
20195
  "description": "Offset from the left of the viewport, in whole system px. See top.",
20067
20196
  "fieldName": "left"
20197
+ },
20198
+ {
20199
+ "name": "light-dismiss",
20200
+ "type": {
20201
+ "text": "boolean"
20202
+ },
20203
+ "default": "false",
20204
+ "description": "Close on a click outside the frame — a press on the backdrop — and fire\n`vf-close` with `{ reason: 'outside' }`. Off by default: the classic modal\nignored an outside click (and beeped), and a dialog that asks a question\nshould keep ignoring it. Opt in for the About box and the splash, the\ndialogs the classic Mac did dismiss on a click.\n\nBoth halves of the click have to land outside — the two-step the\nplatform's own `closedby=\"any\"` light dismiss uses — so a press that\nstarts on a control and slides off the frame, or a title-bar drag\nreleased past it, leaves the dialog open. The press is consumed either\nway: a modal's backdrop lets nothing beneath it see the click, so\ndismissing the About box does not also select whatever was under the\npointer. Escape closes the dialog regardless, as it always has.",
20205
+ "fieldName": "lightDismiss"
20068
20206
  }
20069
20207
  ],
20070
20208
  "superclass": {
@@ -18,6 +18,9 @@ declare const VfDesktop_base: (new (...args: any[]) => import("../position.js").
18
18
  * outside the single-active invariant entirely — clicking a palette neither
19
19
  * deactivates the active document window nor greys the palette, exactly as
20
20
  * System 7's floating windoids behaved while their application was frontmost.
21
+ * A slotted `vf-menu-bar` (or a free-standing `vf-menu`) sits on a tier
22
+ * above both, so its dropped menus cover palettes and document windows
23
+ * alike; only the screen-corner mask is in front of it.
21
24
  *
22
25
  * **Deactivation.** On a real System 7 machine clicking the desktop clicked
23
26
  * the *Finder* — the frontmost application's windows lost their stripes.
@@ -14,14 +14,14 @@ import { SCREEN_CORNER as x, steppedCornerClip as S } from "../pixel-frame.js";
14
14
  import { LitElement as C, css as w, html as T, unsafeCSS as E } from "lit";
15
15
  import { property as D, query as O, queryAssignedElements as k } from "lit/decorators.js";
16
16
  //#region src/components/vf-desktop.ts
17
- var A = 1e6, j = 512, M = 342, N = "gray-50", P = p(d[N], "#000000", "#ffffff"), F = c(P.width, P.height, l(P.rects)), I = u(P.width), L = class extends o(C) {
17
+ var A = 1e6, j = 2 * A, M = 512, N = 342, P = "gray-50", F = p(d[P], "#000000", "#ffffff"), I = c(F.width, F.height, l(F.rects)), L = u(F.width), R = class extends o(C) {
18
18
  constructor(...e) {
19
- super(...e), this.width = j, this.height = M, this.bezel = 0, this.pattern = N, this.scale = new n(this), this.gridSnap = new y(this), this._token = "", this._desktopPattern = d[N], this.#e = !1, this.patternFill = new _(this, {
19
+ super(...e), this.width = M, this.height = N, this.bezel = 0, this.pattern = P, this.scale = new n(this), this.gridSnap = new y(this), this._token = "", this._desktopPattern = d[P], this.#e = !1, this.patternFill = new _(this, {
20
20
  getBox: () => this.screen,
21
21
  getPattern: () => this._token ? null : this._desktopPattern,
22
22
  getSize: () => ({
23
- width: this.width ?? j,
24
- height: this.height ?? M
23
+ width: this.width ?? M,
24
+ height: this.height ?? N
25
25
  })
26
26
  }), this._zCounter = 0, this._activeWindow = null, this._deactivated = !1, this._awaitingUpgrade = !1, this._pointerGesture = !1, this._restoringFocus = !1, this._gestureEnd = new b(this, () => [[
27
27
  document,
@@ -112,7 +112,7 @@ var A = 1e6, j = 512, M = 342, N = "gray-50", P = p(d[N], "#000000", "#ffffff"),
112
112
  position: absolute;
113
113
  inset: 0;
114
114
  z-index: -1;
115
- --_vf-tile-image: var(--vf-desktop-pattern, ${E(F)});
115
+ --_vf-tile-image: var(--vf-desktop-pattern, ${E(I)});
116
116
  /* Forced colors: hidden with the pattern, same posture. */
117
117
  @media (forced-colors: active) {
118
118
  display: none;
@@ -133,6 +133,19 @@ var A = 1e6, j = 512, M = 342, N = "gray-50", P = p(d[N], "#000000", "#ffffff"),
133
133
  ::slotted(vf-window) {
134
134
  position: relative;
135
135
  }
136
+ /* The menu tier: a slotted bar, or a free-standing menu placed on the
137
+ desktop, sits above both window tiers (MENU_BAR_Z), so a dropped
138
+ menu paints over a floating palette exactly as it does over a
139
+ document window. Stated here rather than left to the bar's own
140
+ :host z-index (1000, which the utility band outranks): an outer
141
+ tree's ::slotted declaration beats the inner tree's :host one, so the
142
+ desktop that hands out the window bands owns this band too. Under
143
+ the corner mask, like everything. */
144
+ ::slotted(vf-menu-bar),
145
+ ::slotted(vf-menu) {
146
+ position: relative;
147
+ z-index: ${j};
148
+ }
136
149
  /* With a bezel, the screen's top corners wear the SCREEN_CORNER mask,
137
150
  rounding into the surrounding black — the top pair only, because the
138
151
  classic framebuffer masked only those; the raster's bottom corners
@@ -252,21 +265,21 @@ var A = 1e6, j = 512, M = 342, N = "gray-50", P = p(d[N], "#000000", "#ffffff"),
252
265
  willUpdate(e) {
253
266
  if (super.willUpdate(e), this._token = m(this, "--vf-desktop-pattern"), e.has("pattern")) {
254
267
  let e = f(this.pattern);
255
- e === null && this.pattern?.trim() && !this.#e && (this.#e = !0, console.warn(`vf-desktop: unknown pattern "${this.pattern}" — a library name (docs/PATTERNS.md) or sixteen hex digits. Keeping ${N}.`)), this._desktopPattern = e ?? d[N];
268
+ e === null && this.pattern?.trim() && !this.#e && (this.#e = !0, console.warn(`vf-desktop: unknown pattern "${this.pattern}" — a library name (docs/PATTERNS.md) or sixteen hex digits. Keeping ${P}.`)), this._desktopPattern = e ?? d[P];
256
269
  }
257
270
  }
258
271
  updated(e) {
259
272
  if (e.has("bezel") && (this.bezel > 0 ? this.style.setProperty("--vf-desktop-bezel", i(this.bezel)) : this.style.removeProperty("--vf-desktop-bezel")), e.has("width") || e.has("height") || e.has("bezel")) {
260
- let e = this.width ?? j, t = this.height ?? M;
273
+ let e = this.width ?? M, t = this.height ?? N;
261
274
  this.style.width = i(e + 2 * this.bezel), this.style.height = i(t + 2 * this.bezel);
262
275
  }
263
276
  }
264
277
  render() {
265
- let e = this.width ?? j, t = this.height ?? M, n = this._token ? T`<div class="vf-tile-grid">
278
+ let e = this.width ?? M, t = this.height ?? N, n = this._token ? T`<div class="vf-tile-grid">
266
279
  ${h({
267
- cols: Math.ceil(e / I),
268
- rows: Math.ceil(t / I),
269
- tile: I
280
+ cols: Math.ceil(e / L),
281
+ rows: Math.ceil(t / L),
282
+ tile: L
270
283
  })}
271
284
  </div>` : null;
272
285
  return T`
@@ -284,6 +297,6 @@ var A = 1e6, j = 512, M = 342, N = "gray-50", P = p(d[N], "#000000", "#ffffff"),
284
297
  `;
285
298
  }
286
299
  };
287
- a([D({ type: Number })], L.prototype, "width", void 0), a([D({ type: Number })], L.prototype, "height", void 0), a([D({ type: Number })], L.prototype, "bezel", void 0), a([D()], L.prototype, "pattern", void 0), a([k({ selector: "vf-window" })], L.prototype, "_windows", void 0), a([O(".screen")], L.prototype, "screen", void 0), L = a([t("vf-desktop")], L);
300
+ a([D({ type: Number })], R.prototype, "width", void 0), a([D({ type: Number })], R.prototype, "height", void 0), a([D({ type: Number })], R.prototype, "bezel", void 0), a([D()], R.prototype, "pattern", void 0), a([k({ selector: "vf-window" })], R.prototype, "_windows", void 0), a([O(".screen")], R.prototype, "screen", void 0), R = a([t("vf-desktop")], R);
288
301
  //#endregion
289
- export { L as VfDesktop };
302
+ export { R as VfDesktop };
@@ -23,7 +23,9 @@ import './vf-button-group.js';
23
23
  * Open it with `show()` (or set the `open` attribute/property); close with
24
24
  * `close()`. Escape closes it and fires `vf-close` with
25
25
  * `{ reason: 'escape' }`; the close box and programmatic closing fire
26
- * `{ reason: 'close' }`.
26
+ * `{ reason: 'close' }`. With `light-dismiss`, a click outside the frame
27
+ * closes it too, with `{ reason: 'outside' }` — for the About box; off by
28
+ * default, since the classic modal ignored an outside click.
27
29
  *
28
30
  * @slot - Default slot: dialog body content.
29
31
  * @slot buttons - Optional action buttons. Rendered as a bottom-right
@@ -40,7 +42,8 @@ import './vf-button-group.js';
40
42
  * scrolls under a System 7 rail and becomes a keyboard stop.
41
43
  * @csspart footer - The action row wrapping the buttons.
42
44
  * @csspart buttons - The button group inside the footer.
43
- * @fires vf-close - Dialog closed. Detail `{ reason: 'escape' | 'close' }`.
45
+ * @fires vf-close - Dialog closed. Detail `{ reason: 'escape' | 'close' |
46
+ * 'outside' }` — `'outside'` only under `light-dismiss`.
44
47
  * @cssprop --vf-dots-pattern - the windoid bar's dot-grid dither — a 2×2 tile,
45
48
  * one black pixel at the origin (`vfDots`; override the whole pattern like
46
49
  * `--vf-desktop-pattern`)
@@ -112,6 +112,11 @@ var v = class extends r(f) {
112
112
  exactly (white rows y 69..87 of the sheet). */
113
113
  height: calc(var(--vf-scale, 1) * var(--vf-menubar-height, 20px));
114
114
  position: relative;
115
+ /* Above the page content a bar stands among: its dropped panels are
116
+ z-index 1000 inside this stacking context, so this is the level
117
+ they paint at. Inside a vf-desktop the desktop restates the level
118
+ through ::slotted(vf-menu-bar) — its utility-window band is
119
+ higher than this, and the desktop owns the tiers. */
115
120
  z-index: 1000;
116
121
  }
117
122
  .bar {
@@ -22,7 +22,9 @@ declare const VfMenu_base: (new (...args: any[]) => import("../position.js").VfP
22
22
  * places like any other control. The panel is anchored to the host's own box
23
23
  * (`top: 100%`), so it follows a placed menu down without any further work.
24
24
  * Inside a `<vf-menu-bar>`, the bar lays its titles out in flow — placing one
25
- * lifts it off the bar and the titles beside it close the gap.
25
+ * lifts it off the bar and the titles beside it close the gap. Slotted straight
26
+ * into a `<vf-desktop>`, a free-standing menu sits on the desktop's menu tier
27
+ * above every window, palettes included, as a bar does.
26
28
  *
27
29
  * @slot - Menu contents: `vf-menu-item` and `vf-separator` elements.
28
30
  * @slot label - Replaces the `label` text in the bar — e.g. a `vf-img` apple
@@ -1,8 +1,12 @@
1
1
  import { LitElement } from 'lit';
2
2
  import type { PropertyValues } from 'lit';
3
3
  import { ScaleController } from './scale.js';
4
- /** Reason a modal closed, carried by the `vf-close` event's detail. */
5
- export type VfCloseReason = 'escape' | 'close';
4
+ /**
5
+ * Reason a modal closed, carried by the `vf-close` event's detail: the Escape
6
+ * key, the close box or `close()`, or — with {@link VfModalDialog.lightDismiss}
7
+ * — a click outside the frame.
8
+ */
9
+ export type VfCloseReason = 'escape' | 'close' | 'outside';
6
10
  /**
7
11
  * The width an undeclared modal falls back to, in system px — a classic
8
12
  * dialog's own measure, wide enough for a line of chrome text and a button row.
@@ -23,9 +27,10 @@ export declare const modalDialogStyles: import("lit").CSSResult;
23
27
  * Owns the native `<dialog>` lifecycle every modal shares: `open` sync, `show()` /
24
28
  * `close()`, the {@link top}/{@link left} placement (stated or centered) and the
25
29
  * single `close` funnel that drops the written origin and fires `vf-close` with
26
- * the reason. Because every close path — Escape, `close()`, backdrop — routes
27
- * through the native `close` event, an Escape-close no longer leaves a stale
28
- * origin behind, so the next open re-derives it.
30
+ * the reason. Because every close path — Escape, `close()`, the opt-in
31
+ * {@link lightDismiss} click outside — routes through the native `close`
32
+ * event, an Escape-close no longer leaves a stale origin behind, so the next
33
+ * open re-derives it.
29
34
  *
30
35
  * Removing an open modal from the DOM is a close path too. HTML's dialog
31
36
  * *removing steps* take the element out of the top layer **without** running
@@ -42,7 +47,8 @@ export declare const modalDialogStyles: import("lit").CSSResult;
42
47
  * `<dialog @cancel=${this._onNativeCancel} @close=${this._onNativeClose}>` with
43
48
  * their role/ARIA and body, and {@link modalDialogStyles} in `static styles`.
44
49
  *
45
- * @fires vf-close - The modal closed. `detail: { reason: 'escape' | 'close' }`.
50
+ * @fires vf-close - The modal closed. `detail: { reason: 'escape' | 'close' |
51
+ * 'outside' }`.
46
52
  */
47
53
  export declare class VfModalDialog extends LitElement {
48
54
  #private;
@@ -94,6 +100,22 @@ export declare class VfModalDialog extends LitElement {
94
100
  top?: number | null;
95
101
  /** Offset from the left of the viewport, in whole system px. See {@link top}. */
96
102
  left?: number | null;
103
+ /**
104
+ * Close on a click outside the frame — a press on the backdrop — and fire
105
+ * `vf-close` with `{ reason: 'outside' }`. Off by default: the classic modal
106
+ * ignored an outside click (and beeped), and a dialog that asks a question
107
+ * should keep ignoring it. Opt in for the About box and the splash, the
108
+ * dialogs the classic Mac did dismiss on a click.
109
+ *
110
+ * Both halves of the click have to land outside — the two-step the
111
+ * platform's own `closedby="any"` light dismiss uses — so a press that
112
+ * starts on a control and slides off the frame, or a title-bar drag
113
+ * released past it, leaves the dialog open. The press is consumed either
114
+ * way: a modal's backdrop lets nothing beneath it see the click, so
115
+ * dismissing the About box does not also select whatever was under the
116
+ * pointer. Escape closes the dialog regardless, as it always has.
117
+ */
118
+ lightDismiss: boolean;
97
119
  protected _dialog: HTMLDialogElement;
98
120
  /**
99
121
  * The declared box as inline styles for the subclass's `<dialog>`:
@@ -1,9 +1,10 @@
1
1
  import { emit as e } from "./events.js";
2
2
  import { ScaleController as t, onScaleChange as n, snapSys as r, sysLength as i, toSysExact as a } from "./scale.js";
3
3
  import o from "./_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
4
- import { LitElement as s, css as c } from "lit";
5
- import { property as l, query as u } from "lit/decorators.js";
6
- var d = 24, f = c`
4
+ import { DocumentListenersController as s } from "./document-listeners.js";
5
+ import { LitElement as c, css as l } from "lit";
6
+ import { property as u, query as d } from "lit/decorators.js";
7
+ var f = 24, p = l`
7
8
  dialog {
8
9
  padding: 0;
9
10
  margin: auto;
@@ -42,9 +43,32 @@ var d = 24, f = c`
42
43
  the fallback is the arrow it always showed. */
43
44
  cursor: var(--vf-cursor, default);
44
45
  }
45
- `, p = class extends s {
46
+ `, m = class extends c {
46
47
  constructor(...e) {
47
- super(...e), this.scale = new t(this), this.open = !1, this.#e = !1, this.#n = null, this.#r = null, this.#l = () => this.settle();
48
+ super(...e), this.scale = new t(this), this.open = !1, this.lightDismiss = !1, this.#e = !1, this.#n = null, this.#r = null, this.#i = new s(this, () => [
49
+ [
50
+ this._dialog,
51
+ "pointerdown",
52
+ this.#a
53
+ ],
54
+ [
55
+ this._dialog,
56
+ "pointerup",
57
+ this.#o
58
+ ],
59
+ [
60
+ this._dialog,
61
+ "pointercancel",
62
+ this.#s
63
+ ]
64
+ ]), this.#a = (e) => {
65
+ this.#r = e.target === this._dialog ? e.pointerId : null;
66
+ }, this.#o = (e) => {
67
+ let t = this.#r === e.pointerId;
68
+ this.#r = null, !(!t || e.target !== this._dialog || !this.lightDismiss) && (this.#n = "outside", this.close());
69
+ }, this.#s = () => {
70
+ this.#r = null;
71
+ }, this.#c = null, this.#m = () => this.settle();
48
72
  }
49
73
  get dialogSize() {
50
74
  return {
@@ -63,38 +87,43 @@ var d = 24, f = c`
63
87
  }
64
88
  #n;
65
89
  #r;
90
+ #i;
91
+ #a;
92
+ #o;
93
+ #s;
94
+ #c;
66
95
  show() {
67
- this.open = !0, this.hasUpdated && this.#c();
96
+ this.open = !0, this.hasUpdated && this.#p();
68
97
  }
69
98
  close() {
70
- this.open = !1, this.hasUpdated && this.#c();
99
+ this.open = !1, this.hasUpdated && this.#p();
71
100
  }
72
101
  updated(e) {
73
- e.has("open") && this.#c(), (e.has("top") || e.has("left")) && this.settle(), this.#t();
102
+ e.has("open") && this.#p(), (e.has("top") || e.has("left")) && this.settle(), this.#t();
74
103
  }
75
- #i() {
104
+ #l() {
76
105
  let e = document.documentElement;
77
106
  return {
78
107
  width: a(e.clientWidth || window.innerWidth, this),
79
108
  height: a(e.clientHeight || window.innerHeight, this)
80
109
  };
81
110
  }
82
- #a(e, t) {
83
- let n = this.#i(), r = a(this._dialog?.offsetWidth ?? 0, this);
111
+ #u(e, t) {
112
+ let n = this.#l(), r = a(this._dialog?.offsetWidth ?? 0, this);
84
113
  return {
85
- x: Math.min(Math.max(e, d - r), n.width - d),
86
- y: Math.min(Math.max(t, 0), Math.max(0, n.height - d))
114
+ x: Math.min(Math.max(e, f - r), n.width - f),
115
+ y: Math.min(Math.max(t, 0), Math.max(0, n.height - f))
87
116
  };
88
117
  }
89
- #o() {
90
- let e = this.#i(), t = this._dialog?.getBoundingClientRect();
118
+ #d() {
119
+ let e = this.#l(), t = this._dialog?.getBoundingClientRect();
91
120
  return {
92
121
  x: (e.width - a(t?.width ?? 0, this)) / 2,
93
122
  y: (e.height - a(t?.height ?? 0, this)) / 2
94
123
  };
95
124
  }
96
125
  placeAt(e, t) {
97
- let n = this.#a(e, t);
126
+ let n = this.#u(e, t);
98
127
  this.left = r(n.x, this), this.top = r(n.y, this);
99
128
  }
100
129
  settle() {
@@ -103,10 +132,10 @@ var d = 24, f = c`
103
132
  let t = this.left != null || this.top != null ? {
104
133
  x: this.left ?? 0,
105
134
  y: this.top ?? 0
106
- } : this.#o(), n = this.#a(t.x, t.y);
135
+ } : this.#d(), n = this.#u(t.x, t.y);
107
136
  e.style.left = i(r(n.x, this)), e.style.top = i(r(n.y, this)), e.style.right = "auto", e.style.bottom = "auto", e.style.margin = "0";
108
137
  }
109
- #s() {
138
+ #f() {
110
139
  let e = this._dialog?.style;
111
140
  if (e) for (let t of [
112
141
  "left",
@@ -116,40 +145,44 @@ var d = 24, f = c`
116
145
  "margin"
117
146
  ]) e.removeProperty(t);
118
147
  }
119
- #c() {
148
+ #p() {
120
149
  let e = this._dialog;
121
- e && (this.open && !e.open ? (this.#r = document.activeElement, e.showModal(), this.settle(), this.#f(e)) : !this.open && e.open && e.close());
150
+ e && (this.open && !e.open ? (this.#c = document.activeElement, e.showModal(), this.settle(), this.#_(e), this.#i.attach()) : !this.open && e.open && e.close());
122
151
  }
123
- #l;
124
- #u;
125
- #d;
126
- #f(e) {
127
- typeof ResizeObserver < "u" && !this.#u && (this.#u = new ResizeObserver(this.#l), this.#u.observe(e)), window.addEventListener("resize", this.#l), this.#d ??= n(this.#l);
152
+ #m;
153
+ #h;
154
+ #g;
155
+ #_(e) {
156
+ typeof ResizeObserver < "u" && !this.#h && (this.#h = new ResizeObserver(this.#m), this.#h.observe(e)), window.addEventListener("resize", this.#m), this.#g ??= n(this.#m);
128
157
  }
129
- #p() {
130
- this.#u?.disconnect(), this.#u = void 0, window.removeEventListener("resize", this.#l), this.#d?.(), this.#d = void 0;
158
+ #v() {
159
+ this.#h?.disconnect(), this.#h = void 0, window.removeEventListener("resize", this.#m), this.#g?.(), this.#g = void 0;
131
160
  }
132
161
  disconnectedCallback() {
133
- super.disconnectedCallback(), this.#p();
162
+ super.disconnectedCallback(), this.#v();
134
163
  let e = this._dialog;
135
164
  if (e?.open) {
136
165
  e.close();
137
- let t = this.#r, n = document.activeElement;
166
+ let t = this.#c, n = document.activeElement;
138
167
  t instanceof HTMLElement && t.isConnected && (n === null || n === document.body) && t.focus();
139
168
  }
140
- this.#r = null;
169
+ this.#c = null;
141
170
  }
142
171
  _onNativeCancel() {
143
172
  this.#n = "escape";
144
173
  }
145
174
  _onNativeClose() {
146
175
  let t = this.#n ?? "close";
147
- this.#n = null, this.#r = null, this.#p(), this.open = !1, this.#s(), e(this, "vf-close", { reason: t });
176
+ this.#n = null, this.#c = null, this.#r = null, this.#i.detach(), this.#v(), this.open = !1, this.#f(), e(this, "vf-close", { reason: t });
148
177
  }
149
178
  };
150
- o([l({
179
+ o([u({
151
180
  type: Boolean,
152
181
  reflect: !0
153
- })], p.prototype, "open", void 0), o([l({ type: Number })], p.prototype, "width", void 0), o([l({ type: Number })], p.prototype, "height", void 0), o([l({ type: Number })], p.prototype, "top", void 0), o([l({ type: Number })], p.prototype, "left", void 0), o([u("dialog")], p.prototype, "_dialog", void 0);
182
+ })], m.prototype, "open", void 0), o([u({ type: Number })], m.prototype, "width", void 0), o([u({ type: Number })], m.prototype, "height", void 0), o([u({ type: Number })], m.prototype, "top", void 0), o([u({ type: Number })], m.prototype, "left", void 0), o([u({
183
+ type: Boolean,
184
+ reflect: !0,
185
+ attribute: "light-dismiss"
186
+ })], m.prototype, "lightDismiss", void 0), o([d("dialog")], m.prototype, "_dialog", void 0);
154
187
  //#endregion
155
- export { p as VfModalDialog, f as modalDialogStyles };
188
+ export { m as VfModalDialog, p as modalDialogStyles };
package/docs/SPEC.md CHANGED
@@ -201,6 +201,7 @@ Full-bleed classic desktop container.
201
201
  - **Behavior:** manages stacking of slotted `vf-window` children: `pointerdown` *or `focusin`* on a window brings it to front (incrementing z-index counter) and sets its `active` attribute, clearing `active` on the others — the `focusin` half is the keyboard route to activation: Tab landing anywhere in a background window (its undrawn-but-focusable widgets included) raises it. Listens via delegated pointerdown/focusin listeners + `slotchange`. Windows slotted before `vf-window` is defined are re-normalized once `customElements.whenDefined('vf-window')` settles, since the upgrade reflects each window's `active = true` default back out and upgrading a slotted node doesn't re-fire `slotchange`.
202
202
  - **DOM order follows z-order** (bottom-most first), so sequential focus order matches the visual stack and Shift+Tab mirrors Tab exactly. The sync runs at pointer-gesture end (a mid-gesture node move would clear the pointer capture a title-bar drag or grow-box resize holds) and on programmatic `bringToFront`, never from a focus-driven raise (moving the window focus just entered would re-order the tab sequence mid-traversal); focus surviving its own window's move is restored without re-raising that window. Non-window children (a menu bar, page content) keep their positions. `npm run verify:window-a11y`.
203
203
  - **Floating tier:** `vf-window[variant="utility"]` children stack in a z band `1_000_000` above the document tier (one shared monotonic counter, so a palette stays above every document window), restack only among themselves, and stand outside the single-active invariant both ways: clicking a palette doesn't deactivate the active document window, and activating a document window never clears a palette's `active` — System 7 windoid behavior while the app is frontmost. The tier test reads the `variant` *attribute*, so a not-yet-upgraded element still lands right.
204
+ - **Menu tier:** a slotted `vf-menu-bar` — or a free-standing `vf-menu` placed on the desktop — is restated at `z-index: 2_000_000` by the desktop's own `::slotted(vf-menu-bar), ::slotted(vf-menu)` rule (an outer-tree `::slotted` declaration beats the bar's inner `:host { z-index: 1000 }`), a band above the floating tier, so a dropped menu paints over palettes and document windows alike — the Menu Manager drew menus over every window. Only the screen-corner mask is in front. The full stack inside the screen's isolated context: consumer tile grid (−1) < document windows (counter) < utility windows (counter + 1_000_000) < menu tier (2_000_000) < corner mask (max). A popup *inside* a window (`vf-select`'s fixed panel) is a different case: it stacks within its window's context, so a palette can still cover it — KNOWN-BUGS.md. `npm run verify:archetypes` (MENU TIER).
204
205
  - **Deactivation:** on a real System 7 machine clicking the desktop clicked the *Finder* — the frontmost application's windows lost their stripes. `clearActive()` is that gesture's handler: it clears `active` from the whole document tier, and **zero active windows is a legal state**, held until a press or `focusin` re-enters a document window or a new one is slotted (opening a window brings its application forward — a newly slotted window ends a deliberate deactivation, where a mere survivor never does: removing a *background* window while deactivated promotes nothing, while removing the holder outside a deactivation promotes the topmost survivor). The desktop never takes the decision itself — a press on its own bare dither changes nothing; desktop furniture is slotted light DOM, so only the page knows which of its children mean "the Finder". Utility windows keep their `active` through a deactivation (their dots stay drawn). Every change of holder — window to window, window to none, none to window — fires `vf-activate`, once per change; re-asserting the current holder is silent. `npm run verify:desktop-activate`.
205
206
  - **Events:** `vf-activate` (detail `{ window: HTMLElement | null }` — the new holder, `null` on deactivation).
206
207
  - **Parts:** `desktop`.
@@ -226,15 +227,15 @@ The desktop-window shell: the classic document window (see DragThing screenshot)
226
227
 
227
228
  #### `vf-dialog` (`VfDialog`, vf-dialog.ts)
228
229
  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).
229
- - **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), `frame?: 'plain'`.
230
- - **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' }`. No backdrop dimming: `::backdrop { background: transparent; }`. **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.
230
+ - **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'`.
231
+ - **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`. **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.
231
232
  - **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`), `padding: 16px`. An optional `buttons` slot renders a bottom-right `vf-button-group` footer that only takes space when populated (equal-width, faces aligned). 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 is a flex column of a `.content` scroll region (heading + default slot; part `content`) over the pinned footer. While the content fits, nothing matches — rendering is pixel-identical to the old block flow. Once it overflows (`ScrollStateController`, the always-a-rail machinery), the region reserves the 16px channel as its own right padding, wears the drawn `vfScrollRail` rail (§4) boxed by a 1px `.scroll-frame` overlay, 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 wrapper 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.
232
233
  - **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.
233
234
  - **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. A `heading` renders as a centered display-face heading at the top of the body (`margin-bottom: 16px`) — the way those dialogs drew their title in content — and `closable` is ignored, there being no bar to carry the widget.
234
235
  - **A11y:** named by its own title patch — or, on the plain frame, its body-top heading; both carry `id="title"` — via `aria-labelledby` when `heading` is set. With no heading there is nothing to point at — `aria-labelledby` would resolve to an empty node and leave the dialog unnamed — so it falls back to `aria-label`, taking `label` if given and otherwise `'Dialog'`. An explicit `label` wins over `heading`. The close box is labeled `Close ${heading}` like `vf-window`'s.
235
236
  - **Slots:** default, `buttons`.
236
237
  - **Parts:** `frame`, `title-bar` (default chrome), `title`, `close-box` (when `closable`), `body`, `content`, `footer`, `buttons`.
237
- - **Events:** `vf-close`.
238
+ - **Events:** `vf-close` (detail `{ reason: 'escape' | 'close' | 'outside' }`).
238
239
 
239
240
  #### The alert box (composed — no component)
240
241
  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 the `buttons` slot. The reference page carries the live recipe, composed from `demo/icons/alert.png`.
@@ -191,7 +191,7 @@
191
191
  },
192
192
  {
193
193
  "name": "vf-desktop",
194
- "description": "`<vf-desktop>` — the full-bleed classic desktop container.\n\nRenders the desktop pattern — the classic 50% dither by default, or any of\nthe standard patterns by name (`pattern`, System 7's General Controls\nsetting) — and manages the stacking order and `active` state of slotted\n`vf-window` children: a `pointerdown` or\n`focusin` (keyboard focus) anywhere inside a window brings it to the front\nand makes it the single active window. The windows' light-DOM order is kept\nin step with the stacking order (bottom-most first, at pointer-gesture\nends), so tabbing walks the stack the way the eye does and Shift+Tab is\nits exact mirror.\n\nUtility windows (`vf-window[variant=\"utility\"]`) stack in a floating tier\nabove every document-tier window, restack only among themselves, and stand\noutside the single-active invariant entirely — clicking a palette neither\ndeactivates the active document window nor greys the palette, exactly as\nSystem 7's floating windoids behaved while their application was frontmost.\n\n**Deactivation.** On a real System 7 machine clicking the desktop clicked\nthe *Finder* — the frontmost application's windows lost their stripes.\nclearActive is that gesture's handler: it clears `active` from\nthe whole document tier, and **zero active windows is a legal state**,\nheld until a press or keyboard focus re-enters a document window (or a\nnew one is slotted, which activates it — opening a window brings its\napplication forward). The desktop never takes this decision itself: its\nfurniture is slotted light DOM (an icon layer, say), so only the page\nknows which of its children — or which presses on the bare dither — mean\n\"the Finder\", and it routes those through `clearActive()`. Left alone,\nthe classic always-one-active behavior is unchanged. activeWindow\nreads the current holder, and every change of holder — including to and\nfrom none — fires `vf-activate`.\n\nThe desktop is a raster with an explicit size, always: **`width` and\n`height`**, in system px, the way a WIND resource declared a window's —\nthe host box renders at the declared screen plus `2 × bezel` per axis, a\nwhole number of system pixels by construction (default 512×342, the\ncompact Mac's screen). Pure CSS sizing is not supported; the page sets\nthe numbers — directly, or via VfDesktop.fitWithin on\n`resize`/`onScaleChange` for a viewport-filling desktop — and positions\nthe sized box with its own stylesheet, keeping any sub-system-pixel\nslack on its side. `bezel` (system px) draws the black screen surround —\nthe CRT's unlit margin — around the screen, rounding its top corners\nwith the classic corner mask.\n\n**`pattern`** names the desktop pattern — `gray-50` (the dither) by\ndefault, any of the 38 standard patterns (docs/PATTERNS.md), or sixteen\nhex digits stating a custom 8×8 pattern, as on `vf-container`. It is\npainted as the screen's own background: black ink on an opaque white\npaper, one whole-surface raster at one image px per system px, 1-bit at\nevery density and zoom (src/pattern-fill.ts).\n\nCustom properties:\n- `--vf-desktop-pattern` — a consumer's own tile art in place of the\n pattern (the kit's default is a 1-bit 50% checker, opaque\n black-on-white on a 30-system-px tile). Set, it wins over `pattern` and\n renders as a placed tile grid at that same 30-px geometry\n (src/tile-grid.ts); a token swapped at runtime without touching the\n component wants a `requestUpdate()`.\n- `--vf-desktop` — base color painted *under* the pattern (default\n `#808080`). The pattern's paper is opaque, so this only becomes visible\n when `--vf-desktop-pattern` is overridden with a tile that has\n transparent cells (or with `none`).\n---\n\n\n### **Events:**\n - **vf-activate** - The active document-tier window changed. Detail `{ window: HTMLElement | null }` — the new holder, or `null` when the document tier deactivated (a {@link clearActive} call, or the active window leaving the DOM with none behind it). Fired once per change of holder, never for a re-assertion of the same one.\n\n### **Slots:**\n - _default_ - Default slot: menu bar, windows, anything.\n\n### **CSS Properties:**\n - **--vf-desktop** - base color under the desktop pattern — occluded by the pattern's opaque paper, so it only shows through a custom `--vf-desktop-pattern` _(default: #808080)_\n- **--vf-desktop-pattern** - a consumer's own desktop tile, in place of `pattern` — the kit's default is the 50% checker drawn as opaque black-on-white rects on a 30-system-px tile. Override the whole tile; consumer art renders as a placed tile grid at that same geometry (raster art magnifies nearest-neighbor, the `vf-img` idiom) _(default: undefined)_\n\n### **CSS Parts:**\n - **desktop** - The patterned screen surface — the whole-system-px raster (inset by `bezel` when one is set).",
194
+ "description": "`<vf-desktop>` — the full-bleed classic desktop container.\n\nRenders the desktop pattern — the classic 50% dither by default, or any of\nthe standard patterns by name (`pattern`, System 7's General Controls\nsetting) — and manages the stacking order and `active` state of slotted\n`vf-window` children: a `pointerdown` or\n`focusin` (keyboard focus) anywhere inside a window brings it to the front\nand makes it the single active window. The windows' light-DOM order is kept\nin step with the stacking order (bottom-most first, at pointer-gesture\nends), so tabbing walks the stack the way the eye does and Shift+Tab is\nits exact mirror.\n\nUtility windows (`vf-window[variant=\"utility\"]`) stack in a floating tier\nabove every document-tier window, restack only among themselves, and stand\noutside the single-active invariant entirely — clicking a palette neither\ndeactivates the active document window nor greys the palette, exactly as\nSystem 7's floating windoids behaved while their application was frontmost.\nA slotted `vf-menu-bar` (or a free-standing `vf-menu`) sits on a tier\nabove both, so its dropped menus cover palettes and document windows\nalike; only the screen-corner mask is in front of it.\n\n**Deactivation.** On a real System 7 machine clicking the desktop clicked\nthe *Finder* — the frontmost application's windows lost their stripes.\nclearActive is that gesture's handler: it clears `active` from\nthe whole document tier, and **zero active windows is a legal state**,\nheld until a press or keyboard focus re-enters a document window (or a\nnew one is slotted, which activates it — opening a window brings its\napplication forward). The desktop never takes this decision itself: its\nfurniture is slotted light DOM (an icon layer, say), so only the page\nknows which of its children — or which presses on the bare dither — mean\n\"the Finder\", and it routes those through `clearActive()`. Left alone,\nthe classic always-one-active behavior is unchanged. activeWindow\nreads the current holder, and every change of holder — including to and\nfrom none — fires `vf-activate`.\n\nThe desktop is a raster with an explicit size, always: **`width` and\n`height`**, in system px, the way a WIND resource declared a window's —\nthe host box renders at the declared screen plus `2 × bezel` per axis, a\nwhole number of system pixels by construction (default 512×342, the\ncompact Mac's screen). Pure CSS sizing is not supported; the page sets\nthe numbers — directly, or via VfDesktop.fitWithin on\n`resize`/`onScaleChange` for a viewport-filling desktop — and positions\nthe sized box with its own stylesheet, keeping any sub-system-pixel\nslack on its side. `bezel` (system px) draws the black screen surround —\nthe CRT's unlit margin — around the screen, rounding its top corners\nwith the classic corner mask.\n\n**`pattern`** names the desktop pattern — `gray-50` (the dither) by\ndefault, any of the 38 standard patterns (docs/PATTERNS.md), or sixteen\nhex digits stating a custom 8×8 pattern, as on `vf-container`. It is\npainted as the screen's own background: black ink on an opaque white\npaper, one whole-surface raster at one image px per system px, 1-bit at\nevery density and zoom (src/pattern-fill.ts).\n\nCustom properties:\n- `--vf-desktop-pattern` — a consumer's own tile art in place of the\n pattern (the kit's default is a 1-bit 50% checker, opaque\n black-on-white on a 30-system-px tile). Set, it wins over `pattern` and\n renders as a placed tile grid at that same 30-px geometry\n (src/tile-grid.ts); a token swapped at runtime without touching the\n component wants a `requestUpdate()`.\n- `--vf-desktop` — base color painted *under* the pattern (default\n `#808080`). The pattern's paper is opaque, so this only becomes visible\n when `--vf-desktop-pattern` is overridden with a tile that has\n transparent cells (or with `none`).\n---\n\n\n### **Events:**\n - **vf-activate** - The active document-tier window changed. Detail `{ window: HTMLElement | null }` — the new holder, or `null` when the document tier deactivated (a {@link clearActive} call, or the active window leaving the DOM with none behind it). Fired once per change of holder, never for a re-assertion of the same one.\n\n### **Slots:**\n - _default_ - Default slot: menu bar, windows, anything.\n\n### **CSS Properties:**\n - **--vf-desktop** - base color under the desktop pattern — occluded by the pattern's opaque paper, so it only shows through a custom `--vf-desktop-pattern` _(default: #808080)_\n- **--vf-desktop-pattern** - a consumer's own desktop tile, in place of `pattern` — the kit's default is the 50% checker drawn as opaque black-on-white rects on a 30-system-px tile. Override the whole tile; consumer art renders as a placed tile grid at that same geometry (raster art magnifies nearest-neighbor, the `vf-img` idiom) _(default: undefined)_\n\n### **CSS Parts:**\n - **desktop** - The patterned screen surface — the whole-system-px raster (inset by `bezel` when one is set).",
195
195
  "attributes": [
196
196
  {
197
197
  "name": "width",
@@ -228,7 +228,7 @@
228
228
  },
229
229
  {
230
230
  "name": "vf-dialog",
231
- "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. A `heading` renders as a centered\n display-face heading at the top of the body (the reference art's \"Dialog\n title\"); `closable` is ignored, there being no bar to carry the widget.\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' }`.\n---\n\n\n### **Events:**\n - **vf-close** - Dialog closed. Detail `{ reason: 'escape' | 'close' }`.\n\n### **Slots:**\n - _default_ - Default slot: dialog body content.\n- **buttons** - Optional action buttons. Rendered as a bottom-right `vf-button-group` (equal-width, faces aligned); the footer only takes space when the slot is populated.\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 (or the plain-frame heading).\n- **close-box** - The close widget (`closable`, default chrome only).\n- **body** - The white content area.\n- **content** - The scrolling region inside the body (heading + slotted content, not the footer). Inert while the content fits; over-stuffed, it scrolls under a System 7 rail and becomes a keyboard stop.\n- **footer** - The action row wrapping the buttons.\n- **buttons** - The button group inside the footer.",
231
+ "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. A `heading` renders as a centered\n display-face heading at the top of the body (the reference art's \"Dialog\n title\"); `closable` is ignored, there being no bar to carry the widget.\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---\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: dialog body content.\n- **buttons** - Optional action buttons. Rendered as a bottom-right `vf-button-group` (equal-width, faces aligned); the footer only takes space when the slot is populated.\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 (or the plain-frame heading).\n- **close-box** - The close widget (`closable`, default chrome only).\n- **body** - The white content area.\n- **content** - The scrolling region inside the body (heading + slotted content, not the footer). Inert while the content fits; over-stuffed, it scrolls under a System 7 rail and becomes a keyboard stop.\n- **footer** - The action row wrapping the buttons.\n- **buttons** - The button group inside the footer.",
232
232
  "attributes": [
233
233
  {
234
234
  "name": "heading",
@@ -274,6 +274,11 @@
274
274
  "name": "left",
275
275
  "description": "Offset from the left of the viewport, in whole system px. See top.",
276
276
  "values": []
277
+ },
278
+ {
279
+ "name": "light-dismiss",
280
+ "description": "Close on a click outside the frame — a press on the backdrop — and fire\n`vf-close` with `{ reason: 'outside' }`. Off by default: the classic modal\nignored an outside click (and beeped), and a dialog that asks a question\nshould keep ignoring it. Opt in for the About box and the splash, the\ndialogs the classic Mac did dismiss on a click.\n\nBoth halves of the click have to land outside — the two-step the\nplatform's own `closedby=\"any\"` light dismiss uses — so a press that\nstarts on a control and slides off the frame, or a title-bar drag\nreleased past it, leaves the dialog open. The press is consumed either\nway: a modal's backdrop lets nothing beneath it see the click, so\ndismissing the About box does not also select whatever was under the\npointer. Escape closes the dialog regardless, as it always has.",
281
+ "values": []
277
282
  }
278
283
  ],
279
284
  "references": []
@@ -638,7 +643,7 @@
638
643
  },
639
644
  {
640
645
  "name": "vf-menu",
641
- "description": "`<vf-menu>` — a pull-down menu: a bar label plus a dropped panel of\n`<vf-menu-item>` / `<vf-separator>` children.\n\nInside a `<vf-menu-bar>` the bar coordinates open state (only one menu open,\nhover-switching, outside-click/Escape dismissal) and owns the pointer\ngesture, which may travel between its menus. Used standalone, the menu\ntoggles itself on label click and manages its own dismissal, its own press\ngesture and item keyboard navigation (ArrowUp/ArrowDown, Home/End, and the\nshared first-letter type-ahead — src/type-ahead.ts) while open.\n\nPointer — the two styles `vf-select` supports, on the same terms (see\nsrc/menu-press.ts): the System 7 press-drag-release (press the title, slide\nonto a command, release over it) and a modern quick tap that leaves the menu\ndropped for a second click.\n\nTakes `top`/`left` like every other element (VfPositioned) — the\nnatural fit being a standalone menu, which is a free-standing menu button and\nplaces like any other control. The panel is anchored to the host's own box\n(`top: 100%`), so it follows a placed menu down without any further work.\nInside a `<vf-menu-bar>`, the bar lays its titles out in flow — placing one\nlifts it off the bar and the titles beside it close the gap.\n---\n\n\n### **Slots:**\n - _default_ - Menu contents: `vf-menu-item` and `vf-separator` elements.\n- **label** - Replaces the `label` text in the bar — e.g. a `vf-img` apple icon for the Apple menu. Keep the `label` attribute set too: it stays the menu's accessible name (the bar item's `aria-label` and the panel's) when the visible title is an image.\n\n### **CSS Properties:**\n - **--vf-menubar-height** - `vf-menu-bar` _(default: 20px)_\n- **--vf-menu-row-height** - `vf-menu-item` row pitch; the panel also spends one of these on every slotted `vf-separator`, the full row the MDEF gave a divider (rule 8px into it — see the panel CSS) _(default: 16px)_\n\n### **CSS Parts:**\n - **label** - The menu title in the bar (inverts while open; flashes when a closed menu's item is activated by its key equivalent).\n- **panel** - The dropped `.vf-panel` containing the items.",
646
+ "description": "`<vf-menu>` — a pull-down menu: a bar label plus a dropped panel of\n`<vf-menu-item>` / `<vf-separator>` children.\n\nInside a `<vf-menu-bar>` the bar coordinates open state (only one menu open,\nhover-switching, outside-click/Escape dismissal) and owns the pointer\ngesture, which may travel between its menus. Used standalone, the menu\ntoggles itself on label click and manages its own dismissal, its own press\ngesture and item keyboard navigation (ArrowUp/ArrowDown, Home/End, and the\nshared first-letter type-ahead — src/type-ahead.ts) while open.\n\nPointer — the two styles `vf-select` supports, on the same terms (see\nsrc/menu-press.ts): the System 7 press-drag-release (press the title, slide\nonto a command, release over it) and a modern quick tap that leaves the menu\ndropped for a second click.\n\nTakes `top`/`left` like every other element (VfPositioned) — the\nnatural fit being a standalone menu, which is a free-standing menu button and\nplaces like any other control. The panel is anchored to the host's own box\n(`top: 100%`), so it follows a placed menu down without any further work.\nInside a `<vf-menu-bar>`, the bar lays its titles out in flow — placing one\nlifts it off the bar and the titles beside it close the gap. Slotted straight\ninto a `<vf-desktop>`, a free-standing menu sits on the desktop's menu tier\nabove every window, palettes included, as a bar does.\n---\n\n\n### **Slots:**\n - _default_ - Menu contents: `vf-menu-item` and `vf-separator` elements.\n- **label** - Replaces the `label` text in the bar — e.g. a `vf-img` apple icon for the Apple menu. Keep the `label` attribute set too: it stays the menu's accessible name (the bar item's `aria-label` and the panel's) when the visible title is an image.\n\n### **CSS Properties:**\n - **--vf-menubar-height** - `vf-menu-bar` _(default: 20px)_\n- **--vf-menu-row-height** - `vf-menu-item` row pitch; the panel also spends one of these on every slotted `vf-separator`, the full row the MDEF gave a divider (rule 8px into it — see the panel CSS) _(default: 16px)_\n\n### **CSS Parts:**\n - **label** - The menu title in the bar (inverts while open; flashes when a closed menu's item is activated by its key equivalent).\n- **panel** - The dropped `.vf-panel` containing the items.",
642
647
  "attributes": [
643
648
  {
644
649
  "name": "label",
@@ -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.5.1",
4
+ "version": "0.5.3",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
@@ -457,7 +457,7 @@
457
457
  },
458
458
  {
459
459
  "name": "vf-desktop",
460
- "description": "`<vf-desktop>` — the full-bleed classic desktop container.\n\nRenders the desktop pattern — the classic 50% dither by default, or any of\nthe standard patterns by name (`pattern`, System 7's General Controls\nsetting) — and manages the stacking order and `active` state of slotted\n`vf-window` children: a `pointerdown` or\n`focusin` (keyboard focus) anywhere inside a window brings it to the front\nand makes it the single active window. The windows' light-DOM order is kept\nin step with the stacking order (bottom-most first, at pointer-gesture\nends), so tabbing walks the stack the way the eye does and Shift+Tab is\nits exact mirror.\n\nUtility windows (`vf-window[variant=\"utility\"]`) stack in a floating tier\nabove every document-tier window, restack only among themselves, and stand\noutside the single-active invariant entirely — clicking a palette neither\ndeactivates the active document window nor greys the palette, exactly as\nSystem 7's floating windoids behaved while their application was frontmost.\n\n**Deactivation.** On a real System 7 machine clicking the desktop clicked\nthe *Finder* — the frontmost application's windows lost their stripes.\nclearActive is that gesture's handler: it clears `active` from\nthe whole document tier, and **zero active windows is a legal state**,\nheld until a press or keyboard focus re-enters a document window (or a\nnew one is slotted, which activates it — opening a window brings its\napplication forward). The desktop never takes this decision itself: its\nfurniture is slotted light DOM (an icon layer, say), so only the page\nknows which of its children — or which presses on the bare dither — mean\n\"the Finder\", and it routes those through `clearActive()`. Left alone,\nthe classic always-one-active behavior is unchanged. activeWindow\nreads the current holder, and every change of holder — including to and\nfrom none — fires `vf-activate`.\n\nThe desktop is a raster with an explicit size, always: **`width` and\n`height`**, in system px, the way a WIND resource declared a window's —\nthe host box renders at the declared screen plus `2 × bezel` per axis, a\nwhole number of system pixels by construction (default 512×342, the\ncompact Mac's screen). Pure CSS sizing is not supported; the page sets\nthe numbers — directly, or via VfDesktop.fitWithin on\n`resize`/`onScaleChange` for a viewport-filling desktop — and positions\nthe sized box with its own stylesheet, keeping any sub-system-pixel\nslack on its side. `bezel` (system px) draws the black screen surround —\nthe CRT's unlit margin — around the screen, rounding its top corners\nwith the classic corner mask.\n\n**`pattern`** names the desktop pattern — `gray-50` (the dither) by\ndefault, any of the 38 standard patterns (docs/PATTERNS.md), or sixteen\nhex digits stating a custom 8×8 pattern, as on `vf-container`. It is\npainted as the screen's own background: black ink on an opaque white\npaper, one whole-surface raster at one image px per system px, 1-bit at\nevery density and zoom (src/pattern-fill.ts).\n\nCustom properties:\n- `--vf-desktop-pattern` — a consumer's own tile art in place of the\n pattern (the kit's default is a 1-bit 50% checker, opaque\n black-on-white on a 30-system-px tile). Set, it wins over `pattern` and\n renders as a placed tile grid at that same 30-px geometry\n (src/tile-grid.ts); a token swapped at runtime without touching the\n component wants a `requestUpdate()`.\n- `--vf-desktop` — base color painted *under* the pattern (default\n `#808080`). The pattern's paper is opaque, so this only becomes visible\n when `--vf-desktop-pattern` is overridden with a tile that has\n transparent cells (or with `none`).\n---\n\n\n### **Events:**\n - **vf-activate** - The active document-tier window changed. Detail `{ window: HTMLElement | null }` — the new holder, or `null` when the document tier deactivated (a {@link clearActive} call, or the active window leaving the DOM with none behind it). Fired once per change of holder, never for a re-assertion of the same one.\n\n### **Slots:**\n - _default_ - Default slot: menu bar, windows, anything.\n\n### **CSS Properties:**\n - **--vf-desktop** - base color under the desktop pattern — occluded by the pattern's opaque paper, so it only shows through a custom `--vf-desktop-pattern` _(default: #808080)_\n- **--vf-desktop-pattern** - a consumer's own desktop tile, in place of `pattern` — the kit's default is the 50% checker drawn as opaque black-on-white rects on a 30-system-px tile. Override the whole tile; consumer art renders as a placed tile grid at that same geometry (raster art magnifies nearest-neighbor, the `vf-img` idiom) _(default: undefined)_\n\n### **CSS Parts:**\n - **desktop** - The patterned screen surface — the whole-system-px raster (inset by `bezel` when one is set).",
460
+ "description": "`<vf-desktop>` — the full-bleed classic desktop container.\n\nRenders the desktop pattern — the classic 50% dither by default, or any of\nthe standard patterns by name (`pattern`, System 7's General Controls\nsetting) — and manages the stacking order and `active` state of slotted\n`vf-window` children: a `pointerdown` or\n`focusin` (keyboard focus) anywhere inside a window brings it to the front\nand makes it the single active window. The windows' light-DOM order is kept\nin step with the stacking order (bottom-most first, at pointer-gesture\nends), so tabbing walks the stack the way the eye does and Shift+Tab is\nits exact mirror.\n\nUtility windows (`vf-window[variant=\"utility\"]`) stack in a floating tier\nabove every document-tier window, restack only among themselves, and stand\noutside the single-active invariant entirely — clicking a palette neither\ndeactivates the active document window nor greys the palette, exactly as\nSystem 7's floating windoids behaved while their application was frontmost.\nA slotted `vf-menu-bar` (or a free-standing `vf-menu`) sits on a tier\nabove both, so its dropped menus cover palettes and document windows\nalike; only the screen-corner mask is in front of it.\n\n**Deactivation.** On a real System 7 machine clicking the desktop clicked\nthe *Finder* — the frontmost application's windows lost their stripes.\nclearActive is that gesture's handler: it clears `active` from\nthe whole document tier, and **zero active windows is a legal state**,\nheld until a press or keyboard focus re-enters a document window (or a\nnew one is slotted, which activates it — opening a window brings its\napplication forward). The desktop never takes this decision itself: its\nfurniture is slotted light DOM (an icon layer, say), so only the page\nknows which of its children — or which presses on the bare dither — mean\n\"the Finder\", and it routes those through `clearActive()`. Left alone,\nthe classic always-one-active behavior is unchanged. activeWindow\nreads the current holder, and every change of holder — including to and\nfrom none — fires `vf-activate`.\n\nThe desktop is a raster with an explicit size, always: **`width` and\n`height`**, in system px, the way a WIND resource declared a window's —\nthe host box renders at the declared screen plus `2 × bezel` per axis, a\nwhole number of system pixels by construction (default 512×342, the\ncompact Mac's screen). Pure CSS sizing is not supported; the page sets\nthe numbers — directly, or via VfDesktop.fitWithin on\n`resize`/`onScaleChange` for a viewport-filling desktop — and positions\nthe sized box with its own stylesheet, keeping any sub-system-pixel\nslack on its side. `bezel` (system px) draws the black screen surround —\nthe CRT's unlit margin — around the screen, rounding its top corners\nwith the classic corner mask.\n\n**`pattern`** names the desktop pattern — `gray-50` (the dither) by\ndefault, any of the 38 standard patterns (docs/PATTERNS.md), or sixteen\nhex digits stating a custom 8×8 pattern, as on `vf-container`. It is\npainted as the screen's own background: black ink on an opaque white\npaper, one whole-surface raster at one image px per system px, 1-bit at\nevery density and zoom (src/pattern-fill.ts).\n\nCustom properties:\n- `--vf-desktop-pattern` — a consumer's own tile art in place of the\n pattern (the kit's default is a 1-bit 50% checker, opaque\n black-on-white on a 30-system-px tile). Set, it wins over `pattern` and\n renders as a placed tile grid at that same 30-px geometry\n (src/tile-grid.ts); a token swapped at runtime without touching the\n component wants a `requestUpdate()`.\n- `--vf-desktop` — base color painted *under* the pattern (default\n `#808080`). The pattern's paper is opaque, so this only becomes visible\n when `--vf-desktop-pattern` is overridden with a tile that has\n transparent cells (or with `none`).\n---\n\n\n### **Events:**\n - **vf-activate** - The active document-tier window changed. Detail `{ window: HTMLElement | null }` — the new holder, or `null` when the document tier deactivated (a {@link clearActive} call, or the active window leaving the DOM with none behind it). Fired once per change of holder, never for a re-assertion of the same one.\n\n### **Slots:**\n - _default_ - Default slot: menu bar, windows, anything.\n\n### **CSS Properties:**\n - **--vf-desktop** - base color under the desktop pattern — occluded by the pattern's opaque paper, so it only shows through a custom `--vf-desktop-pattern` _(default: #808080)_\n- **--vf-desktop-pattern** - a consumer's own desktop tile, in place of `pattern` — the kit's default is the 50% checker drawn as opaque black-on-white rects on a 30-system-px tile. Override the whole tile; consumer art renders as a placed tile grid at that same geometry (raster art magnifies nearest-neighbor, the `vf-img` idiom) _(default: undefined)_\n\n### **CSS Parts:**\n - **desktop** - The patterned screen surface — the whole-system-px raster (inset by `bezel` when one is set).",
461
461
  "doc-url": "",
462
462
  "attributes": [
463
463
  {
@@ -554,7 +554,7 @@
554
554
  },
555
555
  {
556
556
  "name": "vf-dialog",
557
- "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. A `heading` renders as a centered\n display-face heading at the top of the body (the reference art's \"Dialog\n title\"); `closable` is ignored, there being no bar to carry the widget.\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' }`.\n---\n\n\n### **Events:**\n - **vf-close** - Dialog closed. Detail `{ reason: 'escape' | 'close' }`.\n\n### **Slots:**\n - _default_ - Default slot: dialog body content.\n- **buttons** - Optional action buttons. Rendered as a bottom-right `vf-button-group` (equal-width, faces aligned); the footer only takes space when the slot is populated.\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 (or the plain-frame heading).\n- **close-box** - The close widget (`closable`, default chrome only).\n- **body** - The white content area.\n- **content** - The scrolling region inside the body (heading + slotted content, not the footer). Inert while the content fits; over-stuffed, it scrolls under a System 7 rail and becomes a keyboard stop.\n- **footer** - The action row wrapping the buttons.\n- **buttons** - The button group inside the footer.",
557
+ "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. A `heading` renders as a centered\n display-face heading at the top of the body (the reference art's \"Dialog\n title\"); `closable` is ignored, there being no bar to carry the widget.\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---\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: dialog body content.\n- **buttons** - Optional action buttons. Rendered as a bottom-right `vf-button-group` (equal-width, faces aligned); the footer only takes space when the slot is populated.\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 (or the plain-frame heading).\n- **close-box** - The close widget (`closable`, default chrome only).\n- **body** - The white content area.\n- **content** - The scrolling region inside the body (heading + slotted content, not the footer). Inert while the content fits; over-stuffed, it scrolls under a System 7 rail and becomes a keyboard stop.\n- **footer** - The action row wrapping the buttons.\n- **buttons** - The button group inside the footer.",
558
558
  "doc-url": "",
559
559
  "attributes": [
560
560
  {
@@ -601,6 +601,11 @@
601
601
  "name": "left",
602
602
  "description": "Offset from the left of the viewport, in whole system px. See top.",
603
603
  "value": { "type": "number | null | undefined" }
604
+ },
605
+ {
606
+ "name": "light-dismiss",
607
+ "description": "Close on a click outside the frame — a press on the backdrop — and fire\n`vf-close` with `{ reason: 'outside' }`. Off by default: the classic modal\nignored an outside click (and beeped), and a dialog that asks a question\nshould keep ignoring it. Opt in for the About box and the splash, the\ndialogs the classic Mac did dismiss on a click.\n\nBoth halves of the click have to land outside — the two-step the\nplatform's own `closedby=\"any\"` light dismiss uses — so a press that\nstarts on a control and slides off the frame, or a title-bar drag\nreleased past it, leaves the dialog open. The press is consumed either\nway: a modal's backdrop lets nothing beneath it see the click, so\ndismissing the About box does not also select whatever was under the\npointer. Escape closes the dialog regardless, as it always has.",
608
+ "value": { "type": "boolean", "default": "false" }
604
609
  }
605
610
  ],
606
611
  "slots": [
@@ -613,7 +618,7 @@
613
618
  "events": [
614
619
  {
615
620
  "name": "vf-close",
616
- "description": "Dialog closed. Detail `{ reason: 'escape' | 'close' }`."
621
+ "description": "Dialog closed. Detail `{ reason: 'escape' | 'close' | 'outside' }` — `'outside'` only under `light-dismiss`."
617
622
  }
618
623
  ],
619
624
  "js": {
@@ -662,12 +667,17 @@
662
667
  "name": "left",
663
668
  "description": "Offset from the left of the viewport, in whole system px. See top.",
664
669
  "type": "number | null | undefined"
670
+ },
671
+ {
672
+ "name": "lightDismiss",
673
+ "description": "Close on a click outside the frame — a press on the backdrop — and fire\n`vf-close` with `{ reason: 'outside' }`. Off by default: the classic modal\nignored an outside click (and beeped), and a dialog that asks a question\nshould keep ignoring it. Opt in for the About box and the splash, the\ndialogs the classic Mac did dismiss on a click.\n\nBoth halves of the click have to land outside — the two-step the\nplatform's own `closedby=\"any\"` light dismiss uses — so a press that\nstarts on a control and slides off the frame, or a title-bar drag\nreleased past it, leaves the dialog open. The press is consumed either\nway: a modal's backdrop lets nothing beneath it see the click, so\ndismissing the About box does not also select whatever was under the\npointer. Escape closes the dialog regardless, as it always has.",
674
+ "type": "boolean"
665
675
  }
666
676
  ],
667
677
  "events": [
668
678
  {
669
679
  "name": "vf-close",
670
- "description": "Dialog closed. Detail `{ reason: 'escape' | 'close' }`."
680
+ "description": "Dialog closed. Detail `{ reason: 'escape' | 'close' | 'outside' }` — `'outside'` only under `light-dismiss`."
671
681
  }
672
682
  ]
673
683
  }
@@ -1495,7 +1505,7 @@
1495
1505
  },
1496
1506
  {
1497
1507
  "name": "vf-menu",
1498
- "description": "`<vf-menu>` — a pull-down menu: a bar label plus a dropped panel of\n`<vf-menu-item>` / `<vf-separator>` children.\n\nInside a `<vf-menu-bar>` the bar coordinates open state (only one menu open,\nhover-switching, outside-click/Escape dismissal) and owns the pointer\ngesture, which may travel between its menus. Used standalone, the menu\ntoggles itself on label click and manages its own dismissal, its own press\ngesture and item keyboard navigation (ArrowUp/ArrowDown, Home/End, and the\nshared first-letter type-ahead — src/type-ahead.ts) while open.\n\nPointer — the two styles `vf-select` supports, on the same terms (see\nsrc/menu-press.ts): the System 7 press-drag-release (press the title, slide\nonto a command, release over it) and a modern quick tap that leaves the menu\ndropped for a second click.\n\nTakes `top`/`left` like every other element (VfPositioned) — the\nnatural fit being a standalone menu, which is a free-standing menu button and\nplaces like any other control. The panel is anchored to the host's own box\n(`top: 100%`), so it follows a placed menu down without any further work.\nInside a `<vf-menu-bar>`, the bar lays its titles out in flow — placing one\nlifts it off the bar and the titles beside it close the gap.\n---\n\n\n### **Slots:**\n - _default_ - Menu contents: `vf-menu-item` and `vf-separator` elements.\n- **label** - Replaces the `label` text in the bar — e.g. a `vf-img` apple icon for the Apple menu. Keep the `label` attribute set too: it stays the menu's accessible name (the bar item's `aria-label` and the panel's) when the visible title is an image.\n\n### **CSS Properties:**\n - **--vf-menubar-height** - `vf-menu-bar` _(default: 20px)_\n- **--vf-menu-row-height** - `vf-menu-item` row pitch; the panel also spends one of these on every slotted `vf-separator`, the full row the MDEF gave a divider (rule 8px into it — see the panel CSS) _(default: 16px)_\n\n### **CSS Parts:**\n - **label** - The menu title in the bar (inverts while open; flashes when a closed menu's item is activated by its key equivalent).\n- **panel** - The dropped `.vf-panel` containing the items.",
1508
+ "description": "`<vf-menu>` — a pull-down menu: a bar label plus a dropped panel of\n`<vf-menu-item>` / `<vf-separator>` children.\n\nInside a `<vf-menu-bar>` the bar coordinates open state (only one menu open,\nhover-switching, outside-click/Escape dismissal) and owns the pointer\ngesture, which may travel between its menus. Used standalone, the menu\ntoggles itself on label click and manages its own dismissal, its own press\ngesture and item keyboard navigation (ArrowUp/ArrowDown, Home/End, and the\nshared first-letter type-ahead — src/type-ahead.ts) while open.\n\nPointer — the two styles `vf-select` supports, on the same terms (see\nsrc/menu-press.ts): the System 7 press-drag-release (press the title, slide\nonto a command, release over it) and a modern quick tap that leaves the menu\ndropped for a second click.\n\nTakes `top`/`left` like every other element (VfPositioned) — the\nnatural fit being a standalone menu, which is a free-standing menu button and\nplaces like any other control. The panel is anchored to the host's own box\n(`top: 100%`), so it follows a placed menu down without any further work.\nInside a `<vf-menu-bar>`, the bar lays its titles out in flow — placing one\nlifts it off the bar and the titles beside it close the gap. Slotted straight\ninto a `<vf-desktop>`, a free-standing menu sits on the desktop's menu tier\nabove every window, palettes included, as a bar does.\n---\n\n\n### **Slots:**\n - _default_ - Menu contents: `vf-menu-item` and `vf-separator` elements.\n- **label** - Replaces the `label` text in the bar — e.g. a `vf-img` apple icon for the Apple menu. Keep the `label` attribute set too: it stays the menu's accessible name (the bar item's `aria-label` and the panel's) when the visible title is an image.\n\n### **CSS Properties:**\n - **--vf-menubar-height** - `vf-menu-bar` _(default: 20px)_\n- **--vf-menu-row-height** - `vf-menu-item` row pitch; the panel also spends one of these on every slotted `vf-separator`, the full row the MDEF gave a divider (rule 8px into it — see the panel CSS) _(default: 16px)_\n\n### **CSS Parts:**\n - **label** - The menu title in the bar (inverts while open; flashes when a closed menu's item is activated by its key equivalent).\n- **panel** - The dropped `.vf-panel` containing the items.",
1499
1509
  "doc-url": "",
1500
1510
  "attributes": [
1501
1511
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vintage-frames",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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",
@@ -73,6 +73,7 @@
73
73
  "probe:menus": "node scripts/probe-menu-scale.mjs",
74
74
  "verify:desktop": "node scripts/verify-desktop-upgrade.mjs",
75
75
  "verify:desktop-activate": "node scripts/verify-desktop-activate.mjs",
76
+ "verify:dialog": "node scripts/verify-dialog.mjs",
76
77
  "verify:list-focus": "node scripts/verify-list-focus.mjs",
77
78
  "verify:control-heights": "node scripts/verify-control-heights.mjs",
78
79
  "verify:list-typeahead": "node scripts/verify-list-typeahead.mjs",