vintage-frames 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +158 -10
- package/dist/components/vf-desktop.d.ts +38 -17
- package/dist/components/vf-desktop.js +6 -4
- package/dist/components/vf-dialog.d.ts +5 -2
- package/dist/modal-dialog.d.ts +28 -6
- package/dist/modal-dialog.js +67 -34
- package/docs/SPEC.md +4 -4
- package/editor/vscode.html-custom-data.json +7 -2
- package/editor/web-types.json +15 -5
- package/package.json +2 -1
package/custom-elements.json
CHANGED
|
@@ -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,
|
|
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, synced in a task once\nany pointer gesture has ended), so tabbing walks the stack the way the eye\ndoes and Shift+Tab is its 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
|
{
|
|
@@ -2460,6 +2460,16 @@
|
|
|
2460
2460
|
"default": "false",
|
|
2461
2461
|
"description": "Whether _syncDomOrder is putting focus back after a node move."
|
|
2462
2462
|
},
|
|
2463
|
+
{
|
|
2464
|
+
"kind": "field",
|
|
2465
|
+
"name": "_domSyncTimer",
|
|
2466
|
+
"type": {
|
|
2467
|
+
"text": "number"
|
|
2468
|
+
},
|
|
2469
|
+
"privacy": "private",
|
|
2470
|
+
"default": "0",
|
|
2471
|
+
"description": "The pending DOM-order sync task, or 0 (see _requestDomSync)."
|
|
2472
|
+
},
|
|
2463
2473
|
{
|
|
2464
2474
|
"kind": "field",
|
|
2465
2475
|
"name": "_gestureEnd",
|
|
@@ -2503,7 +2513,7 @@
|
|
|
2503
2513
|
}
|
|
2504
2514
|
}
|
|
2505
2515
|
],
|
|
2506
|
-
"description": "Bring a slotted window to the front of its tier. A document-tier window\nalso becomes the single active window (clearing `active` on the other\ndocument windows); a utility window restacks within the floating tier\nand leaves every `active` state alone.\n\nThe light-DOM order of the slotted windows follows (_syncDomOrder):\nvisual stacking and sequential focus order come from independent channels\n(z-index vs DOM position), and letting them drift apart is how a desktop\ntabs front-to-back one way and back-to-front the other, with widgets\nreachable only travelling backwards.
|
|
2516
|
+
"description": "Bring a slotted window to the front of its tier. A document-tier window\nalso becomes the single active window (clearing `active` on the other\ndocument windows); a utility window restacks within the floating tier\nand leaves every `active` state alone.\n\nThe light-DOM order of the slotted windows follows (_syncDomOrder):\nvisual stacking and sequential focus order come from independent channels\n(z-index vs DOM position), and letting them drift apart is how a desktop\ntabs front-to-back one way and back-to-front the other, with widgets\nreachable only travelling backwards. The sync runs in a task, and not\nbefore any in-flight pointer gesture has ended — moving a node clears the\npointer capture a title-bar drag or grow-box resize holds on it (a\nbackground window must stay draggable in the same gesture that raises\nit), and a node moved between a press's release and its `click` costs a\ncontrol in the raised window that click (see _requestDomSync)."
|
|
2507
2517
|
},
|
|
2508
2518
|
{
|
|
2509
2519
|
"kind": "field",
|
|
@@ -2552,7 +2562,7 @@
|
|
|
2552
2562
|
"text": "void"
|
|
2553
2563
|
}
|
|
2554
2564
|
},
|
|
2555
|
-
"description": "
|
|
2565
|
+
"description": "Schedule the DOM-order sync: one task from now, coalesced, and never\nwhile a pointer is down — the gesture's end schedules it instead.\n\nA task rather than the synchronous move this used to be, because the\nbrowser dispatches a release's pointerup, mouseup and click in ONE task,\nand Chromium drops a click whose mousedown node left the tree before the\nclick was dispatched — and re-inserting a window is a removal. Synced at\npointerup, a raise moved the window between the press and its click, so\na control in a background window (a checkbox in a palette under another\npalette) got its press and release but never its click: it looked\npressed and never acted. Not a microtask either — that runs between the\nlisteners of the same dispatch, still ahead of the click. A task runs\nafter the whole chain, the shape `deferActivation` (src/events.ts) falls\nback on for the same reason."
|
|
2556
2566
|
},
|
|
2557
2567
|
{
|
|
2558
2568
|
"kind": "method",
|
|
@@ -2571,7 +2581,7 @@
|
|
|
2571
2581
|
}
|
|
2572
2582
|
}
|
|
2573
2583
|
],
|
|
2574
|
-
"description": "Raise the window an event originated in, skipping the restack/activation\nchurn when it is already on top of its own tier (and, for a document\nwindow, already active): otherwise every click inside the front window\nwould bump _zCounter and re-run the whole-fleet activation loop for\nnothing.\n\nRestacks z/active only — deliberately no DOM sync. A focus-driven raise\nMUST NOT move nodes: moving the window focus just entered re-orders the\nsequence mid-traversal, and a Shift+Tab that raises each window it\nenters (pushing it forward in the DOM, back the way the traversal came)\nwould revisit it forever. The pointer path
|
|
2584
|
+
"description": "Raise the window an event originated in, skipping the restack/activation\nchurn when it is already on top of its own tier (and, for a document\nwindow, already active): otherwise every click inside the front window\nwould bump _zCounter and re-run the whole-fleet activation loop for\nnothing.\n\nRestacks z/active only — deliberately no DOM sync. A focus-driven raise\nMUST NOT move nodes: moving the window focus just entered re-orders the\nsequence mid-traversal, and a Shift+Tab that raises each window it\nenters (pushing it forward in the DOM, back the way the traversal came)\nwould revisit it forever. The pointer path schedules the sync at gesture\nend instead, which is also the next safe point after any keyboard-session\nstaleness."
|
|
2575
2585
|
},
|
|
2576
2586
|
{
|
|
2577
2587
|
"kind": "field",
|
|
@@ -2583,7 +2593,7 @@
|
|
|
2583
2593
|
"kind": "field",
|
|
2584
2594
|
"name": "_onGestureEnd",
|
|
2585
2595
|
"privacy": "private",
|
|
2586
|
-
"description": "The press ended (or was cancelled) —
|
|
2596
|
+
"description": "The press ended (or was cancelled) — schedule the DOM-order sync.\nUnconditional rather than only-if-raised: the sync no-ops when order\nalready agrees, and running it after every gesture is what heals the\nstaleness a keyboard-only stretch leaves behind (focus-driven raises\nchange z but never move nodes — see _raise)."
|
|
2587
2597
|
},
|
|
2588
2598
|
{
|
|
2589
2599
|
"kind": "field",
|
|
@@ -2715,7 +2725,7 @@
|
|
|
2715
2725
|
"text": "void"
|
|
2716
2726
|
}
|
|
2717
2727
|
},
|
|
2718
|
-
"description": "Re-order the slotted windows in the light DOM to match their z-order\n(bottom-most first), so sequential focus navigation walks the stack the\nway the eye does — and Shift+Tab is its exact mirror. The utility band\nsorts the floating tier after every document window by construction.\n\nMinimal-move: windows already in relative order are never touched (the\ncommon case — after one raise, one window moves). Non-window siblings\n(a menu bar, page content) keep their positions; only a window that must\ncross the stack moves past them. Moving a node containing the focused\nelement drops focus to `<body>`, so it is restored afterwards — behind\n`_restoringFocus`, because the restore re-fires focusin (see\n_onFocusIn) on an element that may sit in a background window.\
|
|
2728
|
+
"description": "Re-order the slotted windows in the light DOM to match their z-order\n(bottom-most first), so sequential focus navigation walks the stack the\nway the eye does — and Shift+Tab is its exact mirror. The utility band\nsorts the floating tier after every document window by construction.\n\nMinimal-move: windows already in relative order are never touched (the\ncommon case — after one raise, one window moves). Non-window siblings\n(a menu bar, page content) keep their positions; only a window that must\ncross the stack moves past them. Moving a node containing the focused\nelement drops focus to `<body>`, so it is restored afterwards — behind\n`_restoringFocus`, because the restore re-fires focusin (see\n_onFocusIn) on an element that may sit in a background window.\nReached only through _requestDomSync's task — after a pointer\ngesture or a programmatic raise, never from a focus-driven one (see\n_raise for why)."
|
|
2719
2729
|
},
|
|
2720
2730
|
{
|
|
2721
2731
|
"kind": "field",
|
|
@@ -2845,7 +2855,7 @@
|
|
|
2845
2855
|
"declarations": [
|
|
2846
2856
|
{
|
|
2847
2857
|
"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' }`.",
|
|
2858
|
+
"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
2859
|
"name": "VfDialog",
|
|
2850
2860
|
"cssProperties": [
|
|
2851
2861
|
{
|
|
@@ -3129,6 +3139,21 @@
|
|
|
3129
3139
|
"module": "src/modal-dialog.ts"
|
|
3130
3140
|
}
|
|
3131
3141
|
},
|
|
3142
|
+
{
|
|
3143
|
+
"kind": "field",
|
|
3144
|
+
"name": "lightDismiss",
|
|
3145
|
+
"type": {
|
|
3146
|
+
"text": "boolean"
|
|
3147
|
+
},
|
|
3148
|
+
"default": "false",
|
|
3149
|
+
"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.",
|
|
3150
|
+
"attribute": "light-dismiss",
|
|
3151
|
+
"reflects": true,
|
|
3152
|
+
"inheritedFrom": {
|
|
3153
|
+
"name": "VfModalDialog",
|
|
3154
|
+
"module": "src/modal-dialog.ts"
|
|
3155
|
+
}
|
|
3156
|
+
},
|
|
3132
3157
|
{
|
|
3133
3158
|
"kind": "field",
|
|
3134
3159
|
"name": "_dialog",
|
|
@@ -3198,6 +3223,61 @@
|
|
|
3198
3223
|
"module": "src/modal-dialog.ts"
|
|
3199
3224
|
}
|
|
3200
3225
|
},
|
|
3226
|
+
{
|
|
3227
|
+
"kind": "field",
|
|
3228
|
+
"name": "#outsidePress",
|
|
3229
|
+
"privacy": "private",
|
|
3230
|
+
"type": {
|
|
3231
|
+
"text": "number | null"
|
|
3232
|
+
},
|
|
3233
|
+
"default": "null",
|
|
3234
|
+
"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.",
|
|
3235
|
+
"inheritedFrom": {
|
|
3236
|
+
"name": "VfModalDialog",
|
|
3237
|
+
"module": "src/modal-dialog.ts"
|
|
3238
|
+
}
|
|
3239
|
+
},
|
|
3240
|
+
{
|
|
3241
|
+
"kind": "field",
|
|
3242
|
+
"name": "#dismissListeners",
|
|
3243
|
+
"privacy": "private",
|
|
3244
|
+
"readonly": true,
|
|
3245
|
+
"default": "new DocumentListenersController(this, () => [ [this._dialog, 'pointerdown', this.#onDialogPointerDown], [this._dialog, 'pointerup', this.#onDialogPointerUp], [this._dialog, 'pointercancel', this.#onDialogPointerCancel], ])",
|
|
3246
|
+
"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.",
|
|
3247
|
+
"inheritedFrom": {
|
|
3248
|
+
"name": "VfModalDialog",
|
|
3249
|
+
"module": "src/modal-dialog.ts"
|
|
3250
|
+
}
|
|
3251
|
+
},
|
|
3252
|
+
{
|
|
3253
|
+
"kind": "field",
|
|
3254
|
+
"name": "#onDialogPointerDown",
|
|
3255
|
+
"privacy": "private",
|
|
3256
|
+
"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.",
|
|
3257
|
+
"inheritedFrom": {
|
|
3258
|
+
"name": "VfModalDialog",
|
|
3259
|
+
"module": "src/modal-dialog.ts"
|
|
3260
|
+
}
|
|
3261
|
+
},
|
|
3262
|
+
{
|
|
3263
|
+
"kind": "field",
|
|
3264
|
+
"name": "#onDialogPointerUp",
|
|
3265
|
+
"privacy": "private",
|
|
3266
|
+
"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.",
|
|
3267
|
+
"inheritedFrom": {
|
|
3268
|
+
"name": "VfModalDialog",
|
|
3269
|
+
"module": "src/modal-dialog.ts"
|
|
3270
|
+
}
|
|
3271
|
+
},
|
|
3272
|
+
{
|
|
3273
|
+
"kind": "field",
|
|
3274
|
+
"name": "#onDialogPointerCancel",
|
|
3275
|
+
"privacy": "private",
|
|
3276
|
+
"inheritedFrom": {
|
|
3277
|
+
"name": "VfModalDialog",
|
|
3278
|
+
"module": "src/modal-dialog.ts"
|
|
3279
|
+
}
|
|
3280
|
+
},
|
|
3201
3281
|
{
|
|
3202
3282
|
"kind": "field",
|
|
3203
3283
|
"name": "#invoker",
|
|
@@ -3477,7 +3557,7 @@
|
|
|
3477
3557
|
],
|
|
3478
3558
|
"events": [
|
|
3479
3559
|
{
|
|
3480
|
-
"description": "Dialog closed. Detail `{ reason: 'escape' | 'close' }`.",
|
|
3560
|
+
"description": "Dialog closed. Detail `{ reason: 'escape' | 'close' | 'outside' }` — `'outside'` only under `light-dismiss`.",
|
|
3481
3561
|
"name": "vf-close",
|
|
3482
3562
|
"inheritedFrom": {
|
|
3483
3563
|
"name": "VfModalDialog",
|
|
@@ -3581,6 +3661,19 @@
|
|
|
3581
3661
|
"name": "VfModalDialog",
|
|
3582
3662
|
"module": "src/modal-dialog.ts"
|
|
3583
3663
|
}
|
|
3664
|
+
},
|
|
3665
|
+
{
|
|
3666
|
+
"name": "light-dismiss",
|
|
3667
|
+
"type": {
|
|
3668
|
+
"text": "boolean"
|
|
3669
|
+
},
|
|
3670
|
+
"default": "false",
|
|
3671
|
+
"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.",
|
|
3672
|
+
"fieldName": "lightDismiss",
|
|
3673
|
+
"inheritedFrom": {
|
|
3674
|
+
"name": "VfModalDialog",
|
|
3675
|
+
"module": "src/modal-dialog.ts"
|
|
3676
|
+
}
|
|
3584
3677
|
}
|
|
3585
3678
|
],
|
|
3586
3679
|
"superclass": {
|
|
@@ -19702,7 +19795,7 @@
|
|
|
19702
19795
|
},
|
|
19703
19796
|
{
|
|
19704
19797
|
"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()`,
|
|
19798
|
+
"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
19799
|
"name": "VfModalDialog",
|
|
19707
19800
|
"members": [
|
|
19708
19801
|
{
|
|
@@ -19760,6 +19853,17 @@
|
|
|
19760
19853
|
"description": "Offset from the left of the viewport, in whole system px. See top.",
|
|
19761
19854
|
"attribute": "left"
|
|
19762
19855
|
},
|
|
19856
|
+
{
|
|
19857
|
+
"kind": "field",
|
|
19858
|
+
"name": "lightDismiss",
|
|
19859
|
+
"type": {
|
|
19860
|
+
"text": "boolean"
|
|
19861
|
+
},
|
|
19862
|
+
"default": "false",
|
|
19863
|
+
"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.",
|
|
19864
|
+
"attribute": "light-dismiss",
|
|
19865
|
+
"reflects": true
|
|
19866
|
+
},
|
|
19763
19867
|
{
|
|
19764
19868
|
"kind": "field",
|
|
19765
19869
|
"name": "_dialog",
|
|
@@ -19809,6 +19913,41 @@
|
|
|
19809
19913
|
"default": "null",
|
|
19810
19914
|
"description": "Close reason pending for the next native `close` event."
|
|
19811
19915
|
},
|
|
19916
|
+
{
|
|
19917
|
+
"kind": "field",
|
|
19918
|
+
"name": "#outsidePress",
|
|
19919
|
+
"privacy": "private",
|
|
19920
|
+
"type": {
|
|
19921
|
+
"text": "number | null"
|
|
19922
|
+
},
|
|
19923
|
+
"default": "null",
|
|
19924
|
+
"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."
|
|
19925
|
+
},
|
|
19926
|
+
{
|
|
19927
|
+
"kind": "field",
|
|
19928
|
+
"name": "#dismissListeners",
|
|
19929
|
+
"privacy": "private",
|
|
19930
|
+
"readonly": true,
|
|
19931
|
+
"default": "new DocumentListenersController(this, () => [ [this._dialog, 'pointerdown', this.#onDialogPointerDown], [this._dialog, 'pointerup', this.#onDialogPointerUp], [this._dialog, 'pointercancel', this.#onDialogPointerCancel], ])",
|
|
19932
|
+
"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."
|
|
19933
|
+
},
|
|
19934
|
+
{
|
|
19935
|
+
"kind": "field",
|
|
19936
|
+
"name": "#onDialogPointerDown",
|
|
19937
|
+
"privacy": "private",
|
|
19938
|
+
"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."
|
|
19939
|
+
},
|
|
19940
|
+
{
|
|
19941
|
+
"kind": "field",
|
|
19942
|
+
"name": "#onDialogPointerUp",
|
|
19943
|
+
"privacy": "private",
|
|
19944
|
+
"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."
|
|
19945
|
+
},
|
|
19946
|
+
{
|
|
19947
|
+
"kind": "field",
|
|
19948
|
+
"name": "#onDialogPointerCancel",
|
|
19949
|
+
"privacy": "private"
|
|
19950
|
+
},
|
|
19812
19951
|
{
|
|
19813
19952
|
"kind": "field",
|
|
19814
19953
|
"name": "#invoker",
|
|
@@ -20020,7 +20159,7 @@
|
|
|
20020
20159
|
],
|
|
20021
20160
|
"events": [
|
|
20022
20161
|
{
|
|
20023
|
-
"description": "The modal closed. `detail: { reason: 'escape' | 'close' }`.",
|
|
20162
|
+
"description": "The modal closed. `detail: { reason: 'escape' | 'close' | 'outside' }`.",
|
|
20024
20163
|
"name": "vf-close"
|
|
20025
20164
|
}
|
|
20026
20165
|
],
|
|
@@ -20065,6 +20204,15 @@
|
|
|
20065
20204
|
},
|
|
20066
20205
|
"description": "Offset from the left of the viewport, in whole system px. See top.",
|
|
20067
20206
|
"fieldName": "left"
|
|
20207
|
+
},
|
|
20208
|
+
{
|
|
20209
|
+
"name": "light-dismiss",
|
|
20210
|
+
"type": {
|
|
20211
|
+
"text": "boolean"
|
|
20212
|
+
},
|
|
20213
|
+
"default": "false",
|
|
20214
|
+
"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.",
|
|
20215
|
+
"fieldName": "lightDismiss"
|
|
20068
20216
|
}
|
|
20069
20217
|
],
|
|
20070
20218
|
"superclass": {
|
|
@@ -9,9 +9,9 @@ declare const VfDesktop_base: (new (...args: any[]) => import("../position.js").
|
|
|
9
9
|
* `vf-window` children: a `pointerdown` or
|
|
10
10
|
* `focusin` (keyboard focus) anywhere inside a window brings it to the front
|
|
11
11
|
* and makes it the single active window. The windows' light-DOM order is kept
|
|
12
|
-
* in step with the stacking order (bottom-most first,
|
|
13
|
-
*
|
|
14
|
-
* its exact mirror.
|
|
12
|
+
* in step with the stacking order (bottom-most first, synced in a task once
|
|
13
|
+
* any pointer gesture has ended), so tabbing walks the stack the way the eye
|
|
14
|
+
* does and Shift+Tab is its exact mirror.
|
|
15
15
|
*
|
|
16
16
|
* Utility windows (`vf-window[variant="utility"]`) stack in a floating tier
|
|
17
17
|
* above every document-tier window, restack only among themselves, and stand
|
|
@@ -183,6 +183,8 @@ export declare class VfDesktop extends VfDesktop_base {
|
|
|
183
183
|
private _pointerGesture;
|
|
184
184
|
/** Whether {@link _syncDomOrder} is putting focus back after a node move. */
|
|
185
185
|
private _restoringFocus;
|
|
186
|
+
/** The pending DOM-order sync task, or 0 (see {@link _requestDomSync}). */
|
|
187
|
+
private _domSyncTimer;
|
|
186
188
|
/**
|
|
187
189
|
* Ends the pointer-gesture window that defers DOM reordering. Capture-phase
|
|
188
190
|
* on the document so a component's `stopPropagation` can't strand the flag,
|
|
@@ -206,10 +208,12 @@ export declare class VfDesktop extends VfDesktop_base {
|
|
|
206
208
|
* visual stacking and sequential focus order come from independent channels
|
|
207
209
|
* (z-index vs DOM position), and letting them drift apart is how a desktop
|
|
208
210
|
* tabs front-to-back one way and back-to-front the other, with widgets
|
|
209
|
-
* reachable only travelling backwards.
|
|
210
|
-
* pointer gesture — moving a node clears the
|
|
211
|
-
* drag or grow-box resize holds on it
|
|
212
|
-
* draggable in the same gesture that raises
|
|
211
|
+
* reachable only travelling backwards. The sync runs in a task, and not
|
|
212
|
+
* before any in-flight pointer gesture has ended — moving a node clears the
|
|
213
|
+
* pointer capture a title-bar drag or grow-box resize holds on it (a
|
|
214
|
+
* background window must stay draggable in the same gesture that raises
|
|
215
|
+
* it), and a node moved between a press's release and its `click` costs a
|
|
216
|
+
* control in the raised window that click (see {@link _requestDomSync}).
|
|
213
217
|
*/
|
|
214
218
|
bringToFront(win: HTMLElement): void;
|
|
215
219
|
/** The active document-tier window, or null while the tier is deactivated
|
|
@@ -226,7 +230,22 @@ export declare class VfDesktop extends VfDesktop_base {
|
|
|
226
230
|
clearActive(): void;
|
|
227
231
|
/** The z/active half of a raise, shared by every path. */
|
|
228
232
|
private _restack;
|
|
229
|
-
/**
|
|
233
|
+
/**
|
|
234
|
+
* Schedule the DOM-order sync: one task from now, coalesced, and never
|
|
235
|
+
* while a pointer is down — the gesture's end schedules it instead.
|
|
236
|
+
*
|
|
237
|
+
* A task rather than the synchronous move this used to be, because the
|
|
238
|
+
* browser dispatches a release's pointerup, mouseup and click in ONE task,
|
|
239
|
+
* and Chromium drops a click whose mousedown node left the tree before the
|
|
240
|
+
* click was dispatched — and re-inserting a window is a removal. Synced at
|
|
241
|
+
* pointerup, a raise moved the window between the press and its click, so
|
|
242
|
+
* a control in a background window (a checkbox in a palette under another
|
|
243
|
+
* palette) got its press and release but never its click: it looked
|
|
244
|
+
* pressed and never acted. Not a microtask either — that runs between the
|
|
245
|
+
* listeners of the same dispatch, still ahead of the click. A task runs
|
|
246
|
+
* after the whole chain, the shape `deferActivation` (src/events.ts) falls
|
|
247
|
+
* back on for the same reason.
|
|
248
|
+
*/
|
|
230
249
|
private _requestDomSync;
|
|
231
250
|
/**
|
|
232
251
|
* Raise the window an event originated in, skipping the restack/activation
|
|
@@ -239,8 +258,9 @@ export declare class VfDesktop extends VfDesktop_base {
|
|
|
239
258
|
* MUST NOT move nodes: moving the window focus just entered re-orders the
|
|
240
259
|
* sequence mid-traversal, and a Shift+Tab that raises each window it
|
|
241
260
|
* enters (pushing it forward in the DOM, back the way the traversal came)
|
|
242
|
-
* would revisit it forever. The pointer path
|
|
243
|
-
* which is also the next safe point after any keyboard-session
|
|
261
|
+
* would revisit it forever. The pointer path schedules the sync at gesture
|
|
262
|
+
* end instead, which is also the next safe point after any keyboard-session
|
|
263
|
+
* staleness.
|
|
244
264
|
*/
|
|
245
265
|
private _raise;
|
|
246
266
|
/**
|
|
@@ -251,11 +271,11 @@ export declare class VfDesktop extends VfDesktop_base {
|
|
|
251
271
|
*/
|
|
252
272
|
private _onPointerDown;
|
|
253
273
|
/**
|
|
254
|
-
* The press ended (or was cancelled) —
|
|
255
|
-
* rather than only-if-raised: the sync no-ops when order
|
|
256
|
-
* and running it
|
|
257
|
-
* keyboard-only stretch leaves behind (focus-driven raises
|
|
258
|
-
* never move nodes — see {@link _raise}).
|
|
274
|
+
* The press ended (or was cancelled) — schedule the DOM-order sync.
|
|
275
|
+
* Unconditional rather than only-if-raised: the sync no-ops when order
|
|
276
|
+
* already agrees, and running it after every gesture is what heals the
|
|
277
|
+
* staleness a keyboard-only stretch leaves behind (focus-driven raises
|
|
278
|
+
* change z but never move nodes — see {@link _raise}).
|
|
259
279
|
*/
|
|
260
280
|
private _onGestureEnd;
|
|
261
281
|
/**
|
|
@@ -311,8 +331,9 @@ export declare class VfDesktop extends VfDesktop_base {
|
|
|
311
331
|
* element drops focus to `<body>`, so it is restored afterwards — behind
|
|
312
332
|
* `_restoringFocus`, because the restore re-fires focusin (see
|
|
313
333
|
* {@link _onFocusIn}) on an element that may sit in a background window.
|
|
314
|
-
*
|
|
315
|
-
* focus-driven
|
|
334
|
+
* Reached only through {@link _requestDomSync}'s task — after a pointer
|
|
335
|
+
* gesture or a programmatic raise, never from a focus-driven one (see
|
|
336
|
+
* {@link _raise} for why).
|
|
316
337
|
*/
|
|
317
338
|
private _syncDomOrder;
|
|
318
339
|
protected willUpdate(changed: PropertyValues<this>): void;
|
|
@@ -23,7 +23,7 @@ var A = 1e6, j = 2 * A, M = 512, N = 342, P = "gray-50", F = p(d[P], "#000000",
|
|
|
23
23
|
width: this.width ?? M,
|
|
24
24
|
height: this.height ?? N
|
|
25
25
|
})
|
|
26
|
-
}), this._zCounter = 0, this._activeWindow = null, this._deactivated = !1, this._awaitingUpgrade = !1, this._pointerGesture = !1, this._restoringFocus = !1, this._gestureEnd = new b(this, () => [[
|
|
26
|
+
}), this._zCounter = 0, this._activeWindow = null, this._deactivated = !1, this._awaitingUpgrade = !1, this._pointerGesture = !1, this._restoringFocus = !1, this._domSyncTimer = 0, this._gestureEnd = new b(this, () => [[
|
|
27
27
|
document,
|
|
28
28
|
"pointerup",
|
|
29
29
|
this._onGestureEnd,
|
|
@@ -38,7 +38,7 @@ var A = 1e6, j = 2 * A, M = 512, N = 342, P = "gray-50", F = p(d[P], "#000000",
|
|
|
38
38
|
let t = this._windowFromEvent(e);
|
|
39
39
|
t && this._raise(t);
|
|
40
40
|
}, this._onGestureEnd = () => {
|
|
41
|
-
this._gestureEnd.detach(), this._pointerGesture = !1, this.
|
|
41
|
+
this._gestureEnd.detach(), this._pointerGesture = !1, this._requestDomSync();
|
|
42
42
|
}, this._onFocusIn = (e) => {
|
|
43
43
|
if (this._restoringFocus) return;
|
|
44
44
|
let t = this._windowFromEvent(e);
|
|
@@ -185,7 +185,7 @@ var A = 1e6, j = 2 * A, M = 512, N = 342, P = "gray-50", F = p(d[P], "#000000",
|
|
|
185
185
|
super.connectedCallback(), this.addEventListener("pointerdown", this._onPointerDown), this.addEventListener("focusin", this._onFocusIn);
|
|
186
186
|
}
|
|
187
187
|
disconnectedCallback() {
|
|
188
|
-
this.removeEventListener("pointerdown", this._onPointerDown), this.removeEventListener("focusin", this._onFocusIn), this._pointerGesture = !1, super.disconnectedCallback();
|
|
188
|
+
this.removeEventListener("pointerdown", this._onPointerDown), this.removeEventListener("focusin", this._onFocusIn), this._pointerGesture = !1, this._domSyncTimer &&= (clearTimeout(this._domSyncTimer), 0), super.disconnectedCallback();
|
|
189
189
|
}
|
|
190
190
|
_isUtility(e) {
|
|
191
191
|
return e.getAttribute("variant") === "utility";
|
|
@@ -204,7 +204,9 @@ var A = 1e6, j = 2 * A, M = 512, N = 342, P = "gray-50", F = p(d[P], "#000000",
|
|
|
204
204
|
e.style.zIndex = String(++this._zCounter + (t ? A : 0)), t || this._setActive(e);
|
|
205
205
|
}
|
|
206
206
|
_requestDomSync() {
|
|
207
|
-
this._pointerGesture || this.
|
|
207
|
+
this._pointerGesture || this._domSyncTimer || (this._domSyncTimer = window.setTimeout(() => {
|
|
208
|
+
this._domSyncTimer = 0, this._pointerGesture || this._syncDomOrder();
|
|
209
|
+
}, 0));
|
|
208
210
|
}
|
|
209
211
|
_raise(e) {
|
|
210
212
|
let t = this._isUtility(e), n = this._windows.filter((e) => this._isUtility(e) === t);
|
|
@@ -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`)
|
package/dist/modal-dialog.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
5
|
-
|
|
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()`,
|
|
27
|
-
* through the native `close`
|
|
28
|
-
* origin behind, so the next
|
|
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>`:
|
package/dist/modal-dialog.js
CHANGED
|
@@ -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 {
|
|
5
|
-
import {
|
|
6
|
-
|
|
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
|
-
`,
|
|
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.#
|
|
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.#
|
|
96
|
+
this.open = !0, this.hasUpdated && this.#p();
|
|
68
97
|
}
|
|
69
98
|
close() {
|
|
70
|
-
this.open = !1, this.hasUpdated && this.#
|
|
99
|
+
this.open = !1, this.hasUpdated && this.#p();
|
|
71
100
|
}
|
|
72
101
|
updated(e) {
|
|
73
|
-
e.has("open") && this.#
|
|
102
|
+
e.has("open") && this.#p(), (e.has("top") || e.has("left")) && this.settle(), this.#t();
|
|
74
103
|
}
|
|
75
|
-
#
|
|
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
|
-
#
|
|
83
|
-
let n = 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,
|
|
86
|
-
y: Math.min(Math.max(t, 0), Math.max(0, n.height -
|
|
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
|
-
#
|
|
90
|
-
let e = this.#
|
|
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.#
|
|
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.#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
148
|
+
#p() {
|
|
120
149
|
let e = this._dialog;
|
|
121
|
-
e && (this.open && !e.open ? (this.#
|
|
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
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
typeof ResizeObserver < "u" && !this.#
|
|
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
|
-
#
|
|
130
|
-
this.#
|
|
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.#
|
|
162
|
+
super.disconnectedCallback(), this.#v();
|
|
134
163
|
let e = this._dialog;
|
|
135
164
|
if (e?.open) {
|
|
136
165
|
e.close();
|
|
137
|
-
let t = this.#
|
|
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.#
|
|
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.#
|
|
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([
|
|
179
|
+
o([u({
|
|
151
180
|
type: Boolean,
|
|
152
181
|
reflect: !0
|
|
153
|
-
})],
|
|
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 {
|
|
188
|
+
export { m as VfModalDialog, p as modalDialogStyles };
|
package/docs/SPEC.md
CHANGED
|
@@ -199,7 +199,7 @@ Full-bleed classic desktop container.
|
|
|
199
199
|
- **Visual:** `display: block; position: relative;` — the paint lives on an inner screen surface (part `desktop`, `overflow: hidden` — the whole-system-px raster, inset by `bezel` when one is set). Screen surface = the `pattern` (the classic 50% dither, `gray-50`, by default), painted as the screen's own background by the pattern fill (*Patterns* under *Tiled fills*): black ink on the fill's opaque `--vf-white` paper, one whole-surface raster — the authentic black-on-white dither. A consumer `--vf-desktop-pattern` renders instead as a placed tile grid on the token's 30-system-px tile with no paper, so `var(--vf-desktop, #808080)` beneath shows only under a custom tile with transparent cells (or `none`). Forced colors: flat Canvas.
|
|
200
200
|
- **Slots:** default (menu bar, windows, anything).
|
|
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
|
-
- **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
|
|
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 in a task after a pointer gesture ends, or after a programmatic `bringToFront` — never mid-gesture (a node move would clear the pointer capture a title-bar drag or grow-box resize holds), never between a press's release and its `click` (Chromium drops a click whose mousedown node was re-inserted first, which used to cost a control in a background window its click), and 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
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).
|
|
205
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`.
|
|
@@ -227,15 +227,15 @@ The desktop-window shell: the classic document window (see DragThing screenshot)
|
|
|
227
227
|
|
|
228
228
|
#### `vf-dialog` (`VfDialog`, vf-dialog.ts)
|
|
229
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).
|
|
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), `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' }`. 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.
|
|
232
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.
|
|
233
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.
|
|
234
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.
|
|
235
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.
|
|
236
236
|
- **Slots:** default, `buttons`.
|
|
237
237
|
- **Parts:** `frame`, `title-bar` (default chrome), `title`, `close-box` (when `closable`), `body`, `content`, `footer`, `buttons`.
|
|
238
|
-
- **Events:** `vf-close
|
|
238
|
+
- **Events:** `vf-close` (detail `{ reason: 'escape' | 'close' | 'outside' }`).
|
|
239
239
|
|
|
240
240
|
#### The alert box (composed — no component)
|
|
241
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,
|
|
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, synced in a task once\nany pointer gesture has ended), so tabbing walks the stack the way the eye\ndoes and Shift+Tab is its 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' }
|
|
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": []
|
package/editor/web-types.json
CHANGED
|
@@ -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.
|
|
4
|
+
"version": "0.5.4",
|
|
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,
|
|
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, synced in a task once\nany pointer gesture has ended), so tabbing walks the stack the way the eye\ndoes and Shift+Tab is its 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' }
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vintage-frames",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
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",
|