vintage-frames 0.6.1 → 0.6.2

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.
@@ -2995,7 +2995,7 @@
2995
2995
  "declarations": [
2996
2996
  {
2997
2997
  "kind": "class",
2998
- "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.",
2998
+ "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\nKeyboard, the classic Dialog Manager's two rules (VfModalDialog):\non open, focus goes to the first text-entry control — a slotted control\nwith `autofocus` first — or, with none, to the default button\n(`vf-button variant=\"default\"`). Return or Enter activates the default\nbutton from anywhere in the dialog, a focused Cancel included; Space\npresses the focused control. A link keeps its own Enter, and in a\nmulti-line editor Return inserts the newline while the keypad's Enter\nactivates the button.",
2999
2999
  "name": "VfDialog",
3000
3000
  "cssProperties": [
3001
3001
  {
@@ -3379,11 +3379,64 @@
3379
3379
  },
3380
3380
  {
3381
3381
  "kind": "field",
3382
- "name": "#dismissListeners",
3382
+ "name": "#openListeners",
3383
3383
  "privacy": "private",
3384
3384
  "readonly": true,
3385
- "default": "new DocumentListenersController(this, () => [ [this._dialog, 'pointerdown', this.#onDialogPointerDown], [this._dialog, 'pointerup', this.#onDialogPointerUp], [this._dialog, 'pointercancel', this.#onDialogPointerCancel], ])",
3386
- "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.",
3385
+ "default": "new DocumentListenersController(this, () => [ [this._dialog, 'pointerdown', this.#onDialogPointerDown], [this._dialog, 'pointerup', this.#onDialogPointerUp], [this._dialog, 'pointercancel', this.#onDialogPointerCancel], [this._dialog, 'keydown', this.#onDialogKeydown], ])",
3386
+ "description": "The while-open listeners, on the `<dialog>` itself: the light-dismiss\npointer trio and the Return/Enter routing.\n\nLight dismiss: the `::backdrop` hit-tests as its dialog, and the frame the\nsubclass renders fills the dialog's box, so the dialog is an event's\n*target* only when the pointer was outside the frame — no rect arithmetic,\nno composedPath. Attached whether or not lightDismiss is set: the\nhandlers read the property at the release, so it can be toggled on an\nopen dialog.",
3387
+ "inheritedFrom": {
3388
+ "name": "VfModalDialog",
3389
+ "module": "src/modal-dialog.ts"
3390
+ }
3391
+ },
3392
+ {
3393
+ "kind": "field",
3394
+ "name": "defaultButton",
3395
+ "type": {
3396
+ "text": "HTMLElement | null"
3397
+ },
3398
+ "privacy": "protected",
3399
+ "description": "The dialog's default button — the one wearing the bold ring\n(`vf-button[variant=\"default\"]`) — or null with none, or none enabled.\nLooked for among the slotted content first, then in the shadow tree, so a\nconsumer's own modal that renders its buttons itself is covered too.\nOverride to name it some other way.",
3400
+ "readonly": true,
3401
+ "inheritedFrom": {
3402
+ "name": "VfModalDialog",
3403
+ "module": "src/modal-dialog.ts"
3404
+ }
3405
+ },
3406
+ {
3407
+ "kind": "field",
3408
+ "name": "initialFocusTarget",
3409
+ "type": {
3410
+ "text": "HTMLElement | null"
3411
+ },
3412
+ "privacy": "protected",
3413
+ "description": "Where focus lands on open. The classic Dialog Manager put the insertion\npoint in the first editable text item and gave nothing else focus at all;\nthe web needs a focused control for the keyboard to have anywhere to be,\nso with no text field the default button takes it (which is also what\nmakes Return and Space work there without a Tab). In order:\n\n1. a slotted control carrying `autofocus` — the author's say;\n2. the first enabled text-entry control (`vf-text-field`,\n `vf-number-field`, `vf-text-area`, or a native text input/textarea);\n3. the defaultButton;\n4. null — leave the browser's own choice alone.\n\nLeft to itself, `showModal()` focuses the first focusable thing in flat\ntree order, which for a Cancel/OK row is Cancel, and for a body with a\nlink in it is the link — so Return did the one thing a classic dialog's\nReturn never did. Override to choose differently.",
3414
+ "readonly": true,
3415
+ "inheritedFrom": {
3416
+ "name": "VfModalDialog",
3417
+ "module": "src/modal-dialog.ts"
3418
+ }
3419
+ },
3420
+ {
3421
+ "kind": "method",
3422
+ "name": "#focusInitial",
3423
+ "privacy": "private",
3424
+ "return": {
3425
+ "type": {
3426
+ "text": "void"
3427
+ }
3428
+ },
3429
+ "description": "Hand focus to initialFocusTarget, right after `showModal()`'s own\nfocusing steps have run (so this is the last word, not a race).",
3430
+ "inheritedFrom": {
3431
+ "name": "VfModalDialog",
3432
+ "module": "src/modal-dialog.ts"
3433
+ }
3434
+ },
3435
+ {
3436
+ "kind": "field",
3437
+ "name": "#onDialogKeydown",
3438
+ "privacy": "private",
3439
+ "description": "Return and Enter activate the default button from anywhere in the\ndialog, the way the classic Dialog Manager routed them — including from a\nfocused Cancel button, where Space is the key that presses the focused\ncontrol. Bubble phase, and only an uncancelled press: a control that\ntakes Enter for itself (`vf-select`'s list, `vf-list`, an icon's rename, a\ntext field whose form ran its implicit submission) cancels the keydown\nand is left alone. Two more keep their own Enter: a link, which follows\nit, and a multi-line editor, where Return inserts a newline and only the\nkeypad's Enter key fires the button — the same split a TextEdit item in a\nclassic dialog drew between the two keys.",
3387
3440
  "inheritedFrom": {
3388
3441
  "name": "VfModalDialog",
3389
3442
  "module": "src/modal-dialog.ts"
@@ -8470,10 +8523,10 @@
8470
8523
  "privacy": "protected",
8471
8524
  "return": {
8472
8525
  "type": {
8473
- "text": "void"
8526
+ "text": "boolean"
8474
8527
  }
8475
8528
  },
8476
- "description": "Emulates the form's implicit submission for a plain Enter press. HTML\ndefines it as *activating the form's default button* — the first submit\nbutton in tree order — so the submission carries that button's submitter\nidentity and name/value. A bare `requestSubmit()` carries neither\n(`event.submitter === null`), which made Enter and a click on the same\nform produce different payloads. `click()` is the activation: a native\nbutton becomes the real submitter, and a `vf-button` forwards it to its\ninner button, which runs the same transient-proxy path a pointer does. A\ndisabled default button means no submission at all, as in HTML; a form\nwith no submit button falls back to the bare call, also as in HTML.",
8529
+ "description": "Emulates the form's implicit submission for a plain Enter press. HTML\ndefines it as *activating the form's default button* — the first submit\nbutton in tree order — so the submission carries that button's submitter\nidentity and name/value. A bare `requestSubmit()` carries neither\n(`event.submitter === null`), which made Enter and a click on the same\nform produce different payloads. `click()` is the activation: a native\nbutton becomes the real submitter, and a `vf-button` forwards it to its\ninner button, which runs the same transient-proxy path a pointer does. A\ndisabled default button means no submission at all, as in HTML; a form\nwith no submit button falls back to the bare call, also as in HTML.\n\nReturns whether a form owner took the press — true even when a disabled\ndefault button meant no submission, since HTML gave the press to the form\neither way. The callers cancel the keydown on true, so a modal shell\n(`VfModalDialog`) sees the press as spoken for and does not also route it\nto the dialog's default button; with no form the press goes on to the\ndialog untouched.",
8477
8530
  "inheritedFrom": {
8478
8531
  "name": "VfTextControlBase",
8479
8532
  "module": "src/text-control.ts"
@@ -16004,10 +16057,10 @@
16004
16057
  "privacy": "protected",
16005
16058
  "return": {
16006
16059
  "type": {
16007
- "text": "void"
16060
+ "text": "boolean"
16008
16061
  }
16009
16062
  },
16010
- "description": "Emulates the form's implicit submission for a plain Enter press. HTML\ndefines it as *activating the form's default button* — the first submit\nbutton in tree order — so the submission carries that button's submitter\nidentity and name/value. A bare `requestSubmit()` carries neither\n(`event.submitter === null`), which made Enter and a click on the same\nform produce different payloads. `click()` is the activation: a native\nbutton becomes the real submitter, and a `vf-button` forwards it to its\ninner button, which runs the same transient-proxy path a pointer does. A\ndisabled default button means no submission at all, as in HTML; a form\nwith no submit button falls back to the bare call, also as in HTML.",
16063
+ "description": "Emulates the form's implicit submission for a plain Enter press. HTML\ndefines it as *activating the form's default button* — the first submit\nbutton in tree order — so the submission carries that button's submitter\nidentity and name/value. A bare `requestSubmit()` carries neither\n(`event.submitter === null`), which made Enter and a click on the same\nform produce different payloads. `click()` is the activation: a native\nbutton becomes the real submitter, and a `vf-button` forwards it to its\ninner button, which runs the same transient-proxy path a pointer does. A\ndisabled default button means no submission at all, as in HTML; a form\nwith no submit button falls back to the bare call, also as in HTML.\n\nReturns whether a form owner took the press — true even when a disabled\ndefault button meant no submission, since HTML gave the press to the form\neither way. The callers cancel the keydown on true, so a modal shell\n(`VfModalDialog`) sees the press as spoken for and does not also route it\nto the dialog's default button; with no form the press goes on to the\ndialog untouched.",
16011
16064
  "inheritedFrom": {
16012
16065
  "name": "VfTextControlBase",
16013
16066
  "module": "src/text-control.ts"
@@ -16871,7 +16924,7 @@
16871
16924
  }
16872
16925
  }
16873
16926
  ],
16874
- "description": "Enter in a single-line field triggers the associated form's implicit\nsubmission. The native `<input>` is shadow-encapsulated, so its form owner\nis null and the browser won't do this itself."
16927
+ "description": "Enter in a single-line field triggers the associated form's implicit\nsubmission. The native `<input>` is shadow-encapsulated, so its form owner\nis null and the browser won't do this itself. A press the form took is\ncancelled, so an enclosing modal does not route it again."
16875
16928
  },
16876
16929
  {
16877
16930
  "kind": "field",
@@ -17130,10 +17183,10 @@
17130
17183
  "privacy": "protected",
17131
17184
  "return": {
17132
17185
  "type": {
17133
- "text": "void"
17186
+ "text": "boolean"
17134
17187
  }
17135
17188
  },
17136
- "description": "Emulates the form's implicit submission for a plain Enter press. HTML\ndefines it as *activating the form's default button* — the first submit\nbutton in tree order — so the submission carries that button's submitter\nidentity and name/value. A bare `requestSubmit()` carries neither\n(`event.submitter === null`), which made Enter and a click on the same\nform produce different payloads. `click()` is the activation: a native\nbutton becomes the real submitter, and a `vf-button` forwards it to its\ninner button, which runs the same transient-proxy path a pointer does. A\ndisabled default button means no submission at all, as in HTML; a form\nwith no submit button falls back to the bare call, also as in HTML.",
17189
+ "description": "Emulates the form's implicit submission for a plain Enter press. HTML\ndefines it as *activating the form's default button* — the first submit\nbutton in tree order — so the submission carries that button's submitter\nidentity and name/value. A bare `requestSubmit()` carries neither\n(`event.submitter === null`), which made Enter and a click on the same\nform produce different payloads. `click()` is the activation: a native\nbutton becomes the real submitter, and a `vf-button` forwards it to its\ninner button, which runs the same transient-proxy path a pointer does. A\ndisabled default button means no submission at all, as in HTML; a form\nwith no submit button falls back to the bare call, also as in HTML.\n\nReturns whether a form owner took the press — true even when a disabled\ndefault button meant no submission, since HTML gave the press to the form\neither way. The callers cancel the keydown on true, so a modal shell\n(`VfModalDialog`) sees the press as spoken for and does not also route it\nto the dialog's default button; with no form the press goes on to the\ndialog untouched.",
17137
17190
  "inheritedFrom": {
17138
17191
  "name": "VfTextControlBase",
17139
17192
  "module": "src/text-control.ts"
@@ -19835,10 +19888,10 @@
19835
19888
  "privacy": "protected",
19836
19889
  "return": {
19837
19890
  "type": {
19838
- "text": "void"
19891
+ "text": "boolean"
19839
19892
  }
19840
19893
  },
19841
- "description": "Emulates the form's implicit submission for a plain Enter press. HTML\ndefines it as *activating the form's default button* — the first submit\nbutton in tree order — so the submission carries that button's submitter\nidentity and name/value. A bare `requestSubmit()` carries neither\n(`event.submitter === null`), which made Enter and a click on the same\nform produce different payloads. `click()` is the activation: a native\nbutton becomes the real submitter, and a `vf-button` forwards it to its\ninner button, which runs the same transient-proxy path a pointer does. A\ndisabled default button means no submission at all, as in HTML; a form\nwith no submit button falls back to the bare call, also as in HTML."
19894
+ "description": "Emulates the form's implicit submission for a plain Enter press. HTML\ndefines it as *activating the form's default button* — the first submit\nbutton in tree order — so the submission carries that button's submitter\nidentity and name/value. A bare `requestSubmit()` carries neither\n(`event.submitter === null`), which made Enter and a click on the same\nform produce different payloads. `click()` is the activation: a native\nbutton becomes the real submitter, and a `vf-button` forwards it to its\ninner button, which runs the same transient-proxy path a pointer does. A\ndisabled default button means no submission at all, as in HTML; a form\nwith no submit button falls back to the bare call, also as in HTML.\n\nReturns whether a form owner took the press — true even when a disabled\ndefault button meant no submission, since HTML gave the press to the form\neither way. The callers cancel the keydown on true, so a modal shell\n(`VfModalDialog`) sees the press as spoken for and does not also route it\nto the dialog's default button; with no form the press goes on to the\ndialog untouched."
19842
19895
  },
19843
19896
  {
19844
19897
  "kind": "method",
@@ -20778,7 +20831,7 @@
20778
20831
  },
20779
20832
  {
20780
20833
  "kind": "class",
20781
- "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",
20834
+ "description": "Base class for the modal shell `vf-dialog` — and for a consumer's own\nmodal (an alert box, say) authored against the kit.\n\nOwns the native `<dialog>` lifecycle every modal shares: `open` sync, `show()` /\n`close()`, the top/left placement (stated or centered) and the\nsingle `close` funnel that drops the written origin and fires `vf-close` with\nthe reason. Because every close path — Escape, `close()`, the opt-in\nlightDismiss click outside — routes through the native `close`\nevent, an Escape-close no longer leaves a stale origin behind, so the next\nopen re-derives it.\n\nIt also owns the two keyboard rules every classic dialog followed (see\ndefaultButton and initialFocusTarget): Return or Enter\nactivates the default button from anywhere in the dialog, and the box opens\nwith the insertion point in its first text field — or, with none, focus on\nthe default button.\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",
20782
20835
  "name": "VfModalDialog",
20783
20836
  "members": [
20784
20837
  {
@@ -20908,11 +20961,48 @@
20908
20961
  },
20909
20962
  {
20910
20963
  "kind": "field",
20911
- "name": "#dismissListeners",
20964
+ "name": "#openListeners",
20912
20965
  "privacy": "private",
20913
20966
  "readonly": true,
20914
- "default": "new DocumentListenersController(this, () => [ [this._dialog, 'pointerdown', this.#onDialogPointerDown], [this._dialog, 'pointerup', this.#onDialogPointerUp], [this._dialog, 'pointercancel', this.#onDialogPointerCancel], ])",
20915
- "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."
20967
+ "default": "new DocumentListenersController(this, () => [ [this._dialog, 'pointerdown', this.#onDialogPointerDown], [this._dialog, 'pointerup', this.#onDialogPointerUp], [this._dialog, 'pointercancel', this.#onDialogPointerCancel], [this._dialog, 'keydown', this.#onDialogKeydown], ])",
20968
+ "description": "The while-open listeners, on the `<dialog>` itself: the light-dismiss\npointer trio and the Return/Enter routing.\n\nLight dismiss: the `::backdrop` hit-tests as its dialog, and the frame the\nsubclass renders fills the dialog's box, so the dialog is an event's\n*target* only when the pointer was outside the frame — no rect arithmetic,\nno composedPath. Attached whether or not lightDismiss is set: the\nhandlers read the property at the release, so it can be toggled on an\nopen dialog."
20969
+ },
20970
+ {
20971
+ "kind": "field",
20972
+ "name": "defaultButton",
20973
+ "type": {
20974
+ "text": "HTMLElement | null"
20975
+ },
20976
+ "privacy": "protected",
20977
+ "description": "The dialog's default button — the one wearing the bold ring\n(`vf-button[variant=\"default\"]`) — or null with none, or none enabled.\nLooked for among the slotted content first, then in the shadow tree, so a\nconsumer's own modal that renders its buttons itself is covered too.\nOverride to name it some other way.",
20978
+ "readonly": true
20979
+ },
20980
+ {
20981
+ "kind": "field",
20982
+ "name": "initialFocusTarget",
20983
+ "type": {
20984
+ "text": "HTMLElement | null"
20985
+ },
20986
+ "privacy": "protected",
20987
+ "description": "Where focus lands on open. The classic Dialog Manager put the insertion\npoint in the first editable text item and gave nothing else focus at all;\nthe web needs a focused control for the keyboard to have anywhere to be,\nso with no text field the default button takes it (which is also what\nmakes Return and Space work there without a Tab). In order:\n\n1. a slotted control carrying `autofocus` — the author's say;\n2. the first enabled text-entry control (`vf-text-field`,\n `vf-number-field`, `vf-text-area`, or a native text input/textarea);\n3. the defaultButton;\n4. null — leave the browser's own choice alone.\n\nLeft to itself, `showModal()` focuses the first focusable thing in flat\ntree order, which for a Cancel/OK row is Cancel, and for a body with a\nlink in it is the link — so Return did the one thing a classic dialog's\nReturn never did. Override to choose differently.",
20988
+ "readonly": true
20989
+ },
20990
+ {
20991
+ "kind": "method",
20992
+ "name": "#focusInitial",
20993
+ "privacy": "private",
20994
+ "return": {
20995
+ "type": {
20996
+ "text": "void"
20997
+ }
20998
+ },
20999
+ "description": "Hand focus to initialFocusTarget, right after `showModal()`'s own\nfocusing steps have run (so this is the last word, not a race)."
21000
+ },
21001
+ {
21002
+ "kind": "field",
21003
+ "name": "#onDialogKeydown",
21004
+ "privacy": "private",
21005
+ "description": "Return and Enter activate the default button from anywhere in the\ndialog, the way the classic Dialog Manager routed them — including from a\nfocused Cancel button, where Space is the key that presses the focused\ncontrol. Bubble phase, and only an uncancelled press: a control that\ntakes Enter for itself (`vf-select`'s list, `vf-list`, an icon's rename, a\ntext field whose form ran its implicit submission) cancels the keydown\nand is left alone. Two more keep their own Enter: a link, which follows\nit, and a multi-line editor, where Return inserts a newline and only the\nkeypad's Enter key fires the button — the same split a TextEdit item in a\nclassic dialog drew between the two keys."
20916
21006
  },
20917
21007
  {
20918
21008
  "kind": "field",
@@ -27,6 +27,15 @@ import './vf-button-group.js';
27
27
  * closes it too, with `{ reason: 'outside' }` — for the About box; off by
28
28
  * default, since the classic modal ignored an outside click.
29
29
  *
30
+ * Keyboard, the classic Dialog Manager's two rules ({@link VfModalDialog}):
31
+ * on open, focus goes to the first text-entry control — a slotted control
32
+ * with `autofocus` first — or, with none, to the default button
33
+ * (`vf-button variant="default"`). Return or Enter activates the default
34
+ * button from anywhere in the dialog, a focused Cancel included; Space
35
+ * presses the focused control. A link keeps its own Enter, and in a
36
+ * multi-line editor Return inserts the newline while the keypad's Enter
37
+ * activates the button.
38
+ *
30
39
  * @slot - Default slot: dialog body content.
31
40
  * @slot buttons - Optional action buttons. Rendered as a bottom-right
32
41
  * `vf-button-group` (equal-width, faces aligned); the footer only takes
@@ -26,7 +26,7 @@ var y = class extends r(l) {
26
26
  this.#c(), this.pressed = null;
27
27
  }, this.#d = (e) => {
28
28
  if (this.isSubmitEnter(e)) {
29
- this.requestImplicitSubmit();
29
+ this.requestImplicitSubmit() && e.preventDefault();
30
30
  return;
31
31
  }
32
32
  if (!(this.isDisabled || this.readonly) && !(e.isComposing || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) switch (e.key) {
@@ -40,7 +40,8 @@ export declare class VfTextField extends VfTextField_base {
40
40
  /**
41
41
  * Enter in a single-line field triggers the associated form's implicit
42
42
  * submission. The native `<input>` is shadow-encapsulated, so its form owner
43
- * is null and the browser won't do this itself.
43
+ * is null and the browser won't do this itself. A press the form took is
44
+ * cancelled, so an enclosing modal does not route it again.
44
45
  */
45
46
  private handleKeydown;
46
47
  protected render(): import("lit-html").TemplateResult<1>;
@@ -41,7 +41,7 @@ var d = class extends n(a) {
41
41
  this.forwardedAttributes = [...a.forwardedAttributes, "pattern"];
42
42
  }
43
43
  handleKeydown(e) {
44
- this.isSubmitEnter(e) && this.requestImplicitSubmit();
44
+ this.isSubmitEnter(e) && this.requestImplicitSubmit() && e.preventDefault();
45
45
  }
46
46
  render() {
47
47
  return s`
@@ -32,6 +32,12 @@ export declare const modalDialogStyles: import("lit").CSSResult;
32
32
  * event, an Escape-close no longer leaves a stale origin behind, so the next
33
33
  * open re-derives it.
34
34
  *
35
+ * It also owns the two keyboard rules every classic dialog followed (see
36
+ * {@link defaultButton} and {@link initialFocusTarget}): Return or Enter
37
+ * activates the default button from anywhere in the dialog, and the box opens
38
+ * with the insertion point in its first text field — or, with none, focus on
39
+ * the default button.
40
+ *
35
41
  * Removing an open modal from the DOM is a close path too. HTML's dialog
36
42
  * *removing steps* take the element out of the top layer **without** running
37
43
  * the close algorithm — no `close` event, no focus restoration — which is
@@ -124,6 +130,33 @@ export declare class VfModalDialog extends LitElement {
124
130
  * drag from squeezing the box.
125
131
  */
126
132
  protected get dialogSize(): Record<string, string | null>;
133
+ /**
134
+ * The dialog's default button — the one wearing the bold ring
135
+ * (`vf-button[variant="default"]`) — or null with none, or none enabled.
136
+ * Looked for among the slotted content first, then in the shadow tree, so a
137
+ * consumer's own modal that renders its buttons itself is covered too.
138
+ * Override to name it some other way.
139
+ */
140
+ protected get defaultButton(): HTMLElement | null;
141
+ /**
142
+ * Where focus lands on open. The classic Dialog Manager put the insertion
143
+ * point in the first editable text item and gave nothing else focus at all;
144
+ * the web needs a focused control for the keyboard to have anywhere to be,
145
+ * so with no text field the default button takes it (which is also what
146
+ * makes Return and Space work there without a Tab). In order:
147
+ *
148
+ * 1. a slotted control carrying `autofocus` — the author's say;
149
+ * 2. the first enabled text-entry control (`vf-text-field`,
150
+ * `vf-number-field`, `vf-text-area`, or a native text input/textarea);
151
+ * 3. the {@link defaultButton};
152
+ * 4. null — leave the browser's own choice alone.
153
+ *
154
+ * Left to itself, `showModal()` focuses the first focusable thing in flat
155
+ * tree order, which for a Cancel/OK row is Cancel, and for a body with a
156
+ * link in it is the link — so Return did the one thing a classic dialog's
157
+ * Return never did. Override to choose differently.
158
+ */
159
+ protected get initialFocusTarget(): HTMLElement | null;
127
160
  /** Open the modal (native `showModal()`), pinned onto the device grid. */
128
161
  show(): void;
129
162
  /** Close the modal. Fires `vf-close` with `{ reason: 'close' }`. */
@@ -4,7 +4,15 @@ import o from "./_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js"
4
4
  import { DocumentListenersController as s } from "./document-listeners.js";
5
5
  import { LitElement as c, css as l } from "lit";
6
6
  import { property as u, query as d } from "lit/decorators.js";
7
- var f = 24, p = l`
7
+ var f = 24, p = /* @__PURE__ */ new Set([
8
+ "text",
9
+ "search",
10
+ "url",
11
+ "tel",
12
+ "email",
13
+ "password",
14
+ "number"
15
+ ]), m = (e) => e instanceof HTMLInputElement ? p.has(e.type) : !0, h = l`
8
16
  dialog {
9
17
  padding: 0;
10
18
  margin: auto;
@@ -43,32 +51,43 @@ var f = 24, p = l`
43
51
  the fallback is the arrow it always showed. */
44
52
  cursor: var(--vf-cursor, default);
45
53
  }
46
- `, m = class extends c {
54
+ `, g = class extends c {
47
55
  constructor(...e) {
48
56
  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
57
  [
50
58
  this._dialog,
51
59
  "pointerdown",
52
- this.#a
60
+ this.#s
53
61
  ],
54
62
  [
55
63
  this._dialog,
56
64
  "pointerup",
57
- this.#o
65
+ this.#c
58
66
  ],
59
67
  [
60
68
  this._dialog,
61
69
  "pointercancel",
62
- this.#s
70
+ this.#l
71
+ ],
72
+ [
73
+ this._dialog,
74
+ "keydown",
75
+ this.#o
63
76
  ]
64
- ]), this.#a = (e) => {
77
+ ]), this.#o = (e) => {
78
+ if (e.key !== "Enter" || e.defaultPrevented || e.isComposing || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
79
+ let t = e.composedPath();
80
+ if (t.some((e) => e instanceof HTMLAnchorElement && e.hasAttribute("href")) || t.some((e) => e instanceof HTMLTextAreaElement || e instanceof HTMLElement && e.isContentEditable) && e.code !== "NumpadEnter") return;
81
+ let n = this.defaultButton;
82
+ n && (e.preventDefault(), n.click());
83
+ }, this.#s = (e) => {
65
84
  this.#r = e.target === this._dialog ? e.pointerId : null;
66
- }, this.#o = (e) => {
85
+ }, this.#c = (e) => {
67
86
  let t = this.#r === e.pointerId;
68
87
  this.#r = null, !(!t || e.target !== this._dialog || !this.lightDismiss) && (this.#n = "outside", this.close());
69
- }, this.#s = () => {
88
+ }, this.#l = () => {
70
89
  this.#r = null;
71
- }, this.#c = null, this.#m = () => this.settle();
90
+ }, this.#u = null, this.#g = () => this.settle();
72
91
  }
73
92
  get dialogSize() {
74
93
  return {
@@ -88,42 +107,53 @@ var f = 24, p = l`
88
107
  #n;
89
108
  #r;
90
109
  #i;
91
- #a;
110
+ get defaultButton() {
111
+ let e = "vf-button[variant=\"default\"]";
112
+ return [...this.querySelectorAll(e), ...this.renderRoot.querySelectorAll(e)].find((e) => !e.matches(":disabled")) ?? null;
113
+ }
114
+ get initialFocusTarget() {
115
+ return this.querySelector("[autofocus]") || ([...this.querySelectorAll("vf-text-field, vf-number-field, vf-text-area, textarea, input")].find((e) => m(e) && !e.matches(":disabled")) ?? this.defaultButton);
116
+ }
117
+ #a() {
118
+ this.initialFocusTarget?.focus();
119
+ }
92
120
  #o;
93
121
  #s;
94
122
  #c;
123
+ #l;
124
+ #u;
95
125
  show() {
96
- this.open = !0, this.hasUpdated && this.#p();
126
+ this.open = !0, this.hasUpdated && this.#h();
97
127
  }
98
128
  close() {
99
- this.open = !1, this.hasUpdated && this.#p();
129
+ this.open = !1, this.hasUpdated && this.#h();
100
130
  }
101
131
  updated(e) {
102
- e.has("open") && this.#p(), (e.has("top") || e.has("left")) && this.settle(), this.#t();
132
+ e.has("open") && this.#h(), (e.has("top") || e.has("left")) && this.settle(), this.#t();
103
133
  }
104
- #l() {
134
+ #d() {
105
135
  let e = document.documentElement;
106
136
  return {
107
137
  width: a(e.clientWidth || window.innerWidth, this),
108
138
  height: a(e.clientHeight || window.innerHeight, this)
109
139
  };
110
140
  }
111
- #u(e, t) {
112
- let n = this.#l(), r = a(this._dialog?.offsetWidth ?? 0, this);
141
+ #f(e, t) {
142
+ let n = this.#d(), r = a(this._dialog?.offsetWidth ?? 0, this);
113
143
  return {
114
144
  x: Math.min(Math.max(e, f - r), n.width - f),
115
145
  y: Math.min(Math.max(t, 0), Math.max(0, n.height - f))
116
146
  };
117
147
  }
118
- #d() {
119
- let e = this.#l(), t = this._dialog?.getBoundingClientRect();
148
+ #p() {
149
+ let e = this.#d(), t = this._dialog?.getBoundingClientRect();
120
150
  return {
121
151
  x: (e.width - a(t?.width ?? 0, this)) / 2,
122
152
  y: (e.height - a(t?.height ?? 0, this)) / 2
123
153
  };
124
154
  }
125
155
  placeAt(e, t) {
126
- let n = this.#u(e, t);
156
+ let n = this.#f(e, t);
127
157
  this.left = r(n.x, this), this.top = r(n.y, this);
128
158
  }
129
159
  settle() {
@@ -132,10 +162,10 @@ var f = 24, p = l`
132
162
  let t = this.left != null || this.top != null ? {
133
163
  x: this.left ?? 0,
134
164
  y: this.top ?? 0
135
- } : this.#d(), n = this.#u(t.x, t.y);
165
+ } : this.#p(), n = this.#f(t.x, t.y);
136
166
  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";
137
167
  }
138
- #f() {
168
+ #m() {
139
169
  let e = this._dialog?.style;
140
170
  if (e) for (let t of [
141
171
  "left",
@@ -145,44 +175,44 @@ var f = 24, p = l`
145
175
  "margin"
146
176
  ]) e.removeProperty(t);
147
177
  }
148
- #p() {
178
+ #h() {
149
179
  let e = this._dialog;
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());
180
+ e && (this.open && !e.open ? (this.#u = document.activeElement, e.showModal(), this.#a(), this.settle(), this.#y(e), this.#i.attach()) : !this.open && e.open && e.close());
151
181
  }
152
- #m;
153
- #h;
154
182
  #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);
183
+ #_;
184
+ #v;
185
+ #y(e) {
186
+ typeof ResizeObserver < "u" && !this.#_ && (this.#_ = new ResizeObserver(this.#g), this.#_.observe(e)), window.addEventListener("resize", this.#g), this.#v ??= n(this.#g);
157
187
  }
158
- #v() {
159
- this.#h?.disconnect(), this.#h = void 0, window.removeEventListener("resize", this.#m), this.#g?.(), this.#g = void 0;
188
+ #b() {
189
+ this.#_?.disconnect(), this.#_ = void 0, window.removeEventListener("resize", this.#g), this.#v?.(), this.#v = void 0;
160
190
  }
161
191
  disconnectedCallback() {
162
- super.disconnectedCallback(), this.#v();
192
+ super.disconnectedCallback(), this.#b();
163
193
  let e = this._dialog;
164
194
  if (e?.open) {
165
195
  e.close();
166
- let t = this.#c, n = document.activeElement;
196
+ let t = this.#u, n = document.activeElement;
167
197
  t instanceof HTMLElement && t.isConnected && (n === null || n === document.body) && t.focus();
168
198
  }
169
- this.#c = null;
199
+ this.#u = null;
170
200
  }
171
201
  _onNativeCancel() {
172
202
  this.#n = "escape";
173
203
  }
174
204
  _onNativeClose() {
175
205
  let t = this.#n ?? "close";
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 });
206
+ this.#n = null, this.#u = null, this.#r = null, this.#i.detach(), this.#b(), this.open = !1, this.#m(), e(this, "vf-close", { reason: t });
177
207
  }
178
208
  };
179
209
  o([u({
180
210
  type: Boolean,
181
211
  reflect: !0
182
- })], m.prototype, "open", void 0), o([u({ type: Number })], m.prototype, "width", void 0), o([u({ type: Number })], m.prototype, "height", void 0), o([u({ type: Number })], m.prototype, "top", void 0), o([u({ type: Number })], m.prototype, "left", void 0), o([u({
212
+ })], g.prototype, "open", void 0), o([u({ type: Number })], g.prototype, "width", void 0), o([u({ type: Number })], g.prototype, "height", void 0), o([u({ type: Number })], g.prototype, "top", void 0), o([u({ type: Number })], g.prototype, "left", void 0), o([u({
183
213
  type: Boolean,
184
214
  reflect: !0,
185
215
  attribute: "light-dismiss"
186
- })], m.prototype, "lightDismiss", void 0), o([d("dialog")], m.prototype, "_dialog", void 0);
216
+ })], g.prototype, "lightDismiss", void 0), o([d("dialog")], g.prototype, "_dialog", void 0);
187
217
  //#endregion
188
- export { m as VfModalDialog, p as modalDialogStyles };
218
+ export { g as VfModalDialog, h as modalDialogStyles };
@@ -116,8 +116,15 @@ export declare class VfTextControlBase extends VfShadowRoleControl {
116
116
  * inner button, which runs the same transient-proxy path a pointer does. A
117
117
  * disabled default button means no submission at all, as in HTML; a form
118
118
  * with no submit button falls back to the bare call, also as in HTML.
119
+ *
120
+ * Returns whether a form owner took the press — true even when a disabled
121
+ * default button meant no submission, since HTML gave the press to the form
122
+ * either way. The callers cancel the keydown on true, so a modal shell
123
+ * (`VfModalDialog`) sees the press as spoken for and does not also route it
124
+ * to the dialog's default button; with no form the press goes on to the
125
+ * dialog untouched.
119
126
  */
120
- protected requestImplicitSubmit(): void;
127
+ protected requestImplicitSubmit(): boolean;
121
128
  /**
122
129
  * Dispatch a bubbling, composed value event (SPEC §4). Text fields use the
123
130
  * default `{ value }` detail; `vf-number-field` passes an enriched detail that
@@ -56,14 +56,9 @@ var l = class extends a {
56
56
  }
57
57
  requestImplicitSubmit() {
58
58
  let e = this.internals.form;
59
- if (!e) return;
59
+ if (!e) return !1;
60
60
  let t = [...e.elements].find((e) => e instanceof HTMLButtonElement && e.type === "submit" || e instanceof HTMLInputElement && (e.type === "submit" || e.type === "image") || e.localName === "vf-button" && e.type?.toLowerCase() === "submit");
61
- if (t) {
62
- if (t.matches(":disabled")) return;
63
- t.click();
64
- return;
65
- }
66
- e.requestSubmit();
61
+ return t ? (t.matches(":disabled") || t.click(), !0) : (e.requestSubmit(), !0);
67
62
  }
68
63
  emitValue(t, n = { value: this.value }) {
69
64
  e(this, t, n);
package/docs/SPEC.md CHANGED
@@ -229,7 +229,7 @@ The desktop-window shell: the classic document window (see DragThing screenshot)
229
229
  #### `vf-dialog` (`VfDialog`, vf-dialog.ts)
230
230
  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).
231
231
  - **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'`.
232
- - **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
+ - **Implementation:** wraps a native `<dialog>` (for top-layer + focus trap). `show()` → `showModal()`; `close()` closes. Keep `open` attr in sync both directions. Drag the title bar to move it (shared `DragController` with `vf-window`), stating `top`/`left` in system px; drags starting on the close widget are ignored (same composedPath guard as `vf-window`). Escape → close + `vf-close` detail `{ reason: 'escape' }`; close box/programmatic/close() → `{ reason: 'close' }`; a click outside under `light-dismiss` → `{ reason: 'outside' }`. No backdrop dimming: `::backdrop { background: transparent; }`. **Light dismiss is target identity, not geometry** (`VfModalDialog`): the `::backdrop` hit-tests as its `<dialog>`, and the frame fills the dialog's box, so the dialog is a pointer event's *target* exactly when the pointer was outside the frame — the listeners sit on the `<dialog>` itself, attached while open. It takes both halves of the click: a `pointerdown` on the backdrop arms (any other press disarms, so a right-click macOS never releases can't leave a stale arm), and a `pointerup` on the backdrop with the same `pointerId` closes — the platform's own `closedby="any"` two-step, so a press that starts on a control and slides off, or a title-bar drag released past the frame, leaves the dialog open. Deliberately not the `click` event: UI Events dispatches a press-drag-release click at the common ancestor of the two targets, which for a press on the frame released outside is the dialog itself. The platform's `closedby` attribute is not used because its dismissal arrives as `cancel`, which the base already reads as Escape. The press is consumed either way — a modal's backdrop lets nothing beneath it see the click — which is what lets a page dismiss its About box without also selecting the icon under the pointer. `npm run verify:dialog`. **Keyboard: the Dialog Manager's two rules** (`VfModalDialog`). *Initial focus:* right after `showModal()`'s own focusing steps, focus is handed to `initialFocusTarget` — a slotted control carrying `autofocus`, else the first enabled text-entry control (`vf-text-field`, `vf-number-field`, `vf-text-area`, or a native text input/textarea), else the default button (`defaultButton`: the first enabled `vf-button[variant="default"]`, looked for among the slotted content and then the shadow tree), else nothing, leaving the browser's pick. The classic dialog put the insertion point in its first text item and gave nothing else focus; the web needs a focused control, and the default button is the one that makes Return and Space work without a Tab. Left alone, `showModal()` focuses the first focusable thing in flat-tree order — Cancel in a Cancel/OK row, or a link in the body — so Return pressed the wrong thing. Both getters are `protected`, for a consumer's own modal to override. *Return/Enter:* a `keydown` listener on the `<dialog>`, bubble phase, attached while open with the light-dismiss trio. An uncancelled, unmodified Enter activates `defaultButton` (its `click()`, so a `type="submit"` default runs its proxy path) and cancels the keydown, which is what keeps a focused Cancel from firing itself — Space presses the focused control, as on the classic Mac. Anything that took Enter for itself cancelled the keydown first and is left alone: `vf-select`'s list, `vf-list`, an icon's rename, and a text field whose form ran its implicit submission (`requestImplicitSubmit` now reports whether a form owner took the press, and the fields cancel on true — so the submission runs once, not once per layer). A link keeps its own Enter. A multi-line editor (`<textarea>`, contenteditable) keeps Return for the newline; only `code === 'NumpadEnter'` fires the button there — the split a TextEdit item drew between the two keys. With no enabled default button the press is left native. Escape is unchanged (the `cancel` path above). **Removal while open is a close path** (`VfModalDialog.disconnectedCallback`): HTML's dialog removing steps skip the close algorithm entirely, which is exactly what a framework unmount does — the teardown routes through the same native-`close` funnel (`vf-close` fires on the removed element; nothing bubbles, it left the tree), `open` and the written origin reconcile so a re-append mounts closed, and focus returns to the element focused at open time. **The placement re-settles while open** whenever the dialog's own box resizes — slotted content upgrading after `showModal()`, `--vf-scale` moving under zoom — the viewport resizes, or the scale changes. What survives depends on how the modal got where it is: an unplaced one re-centers (it never claimed a spot, and the stranded case is exactly this one), while a dragged or authored origin is kept and only re-clamped on screen.
233
233
  - **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.
234
234
  - **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.
235
235
  - **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.
@@ -308,7 +308,7 @@ The color-swatch button: a well of solid color — a palette cell.
308
308
  - **Attributes/props:** `value`, `placeholder`, `disabled`, `readonly`, `type: string` (default `'text'`; pass through to input), `name`. The input-behavior attributes — `autocomplete`, `inputmode`, `enterkeyhint`, `maxlength`, `pattern`, `spellcheck`, `autocapitalize` — are **forwarded verbatim** from the host onto the inner input (observed attributes, not reactive properties: four of them are globals with IDL accessors already on `HTMLElement`, and a Lit `@property` would shadow the platform member — the `align`/`draggable` trap). The platform only honors them on the element that actually takes the input, which is in the shadow root.
309
309
  - **Visual:** inner `<input>`: white bg, `1px solid black`, NO radius, height `var(--vf-control-height, 22px)`, `padding: 0 6px`, font tokens but `font-weight: var(--vf-font-weight, 700)`. `user-select: text`. Focus: for a **keyboard** focus, the kit's 1px dashed rule (`vfFocusUnderline`, §4) one blank system px row under the well — no dotted outline and no thickened border. A click leaves it unmarked: the insertion point is already the answer to where focus went (see §4 on why this can't be `:focus-visible`). Disabled: the text dims to gray; the black border stays. Selected text inverts to solid black-on-white (`.vf-field::selection`, using `--vf-highlight`/`--vf-highlight-text`) — the 1-bit System 7 selection, shared by all three editable fields via the `vfField` skin.
310
310
  - **Structure:** the `<input>` sits in a `.vf-field-well` wrapper, which is what the focus rule hangs from and what carries `vf-snap`. A replaced element draws no pseudo-element of its own, and the host is never the thing grid snapping moves, so the wrapper is the only box that is both the well's exact shape and on the corrected grid. Same wrapper in all three fields, assembled by `VfTextControlBase.wellClass` so the focus gate can't drift between them. Two consequences when embedding: the rule paints 2 system px **below the host's own box** (`pointer-events: none`, so it never takes a click meant for what sits under it, but a tight `overflow: hidden` ancestor clips it), and a `width` set on `::part(input)` sizes the control without moving the wrapper the rule spans — a field's width belongs on the host or on `--vf-field-width`.
311
- - **Behavior:** form-associated; syncs `value` on input; `formResetCallback` restores default. A plain Enter runs the form's **implicit submission** the way HTML defines it — activating the form's default button (first submit button in tree order, `vf-button` included), so the submission carries a real `submitter` and that button's `name`/`value`, a disabled default button submits nothing, and only a form with no submit button falls back to a bare `requestSubmit()` (`requestImplicitSubmit`, text-control.ts).
311
+ - **Behavior:** form-associated; syncs `value` on input; `formResetCallback` restores default. A plain Enter runs the form's **implicit submission** the way HTML defines it — activating the form's default button (first submit button in tree order, `vf-button` included), so the submission carries a real `submitter` and that button's `name`/`value`, a disabled default button submits nothing, and only a form with no submit button falls back to a bare `requestSubmit()` (`requestImplicitSubmit`, text-control.ts). When a form owner took the press the keydown is cancelled, so an enclosing `vf-dialog` does not route the same Enter to its default button a second time; with no form the press goes on to the dialog, whose default button it activates.
312
312
  - **Parts:** `input`.
313
313
  - **Events:** `vf-input` detail `{ value }` on every keystroke; `vf-change` detail `{ value }` on commit (native change). Plus the native pair per §2: the inner input's own composed `input` crosses the boundary itself; `change` is re-dispatched from the host.
314
314
 
@@ -253,7 +253,7 @@
253
253
  },
254
254
  {
255
255
  "name": "vf-dialog",
256
- "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.",
256
+ "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\nKeyboard, the classic Dialog Manager's two rules (VfModalDialog):\non open, focus goes to the first text-entry control — a slotted control\nwith `autofocus` first — or, with none, to the default button\n(`vf-button variant=\"default\"`). Return or Enter activates the default\nbutton from anywhere in the dialog, a focused Cancel included; Space\npresses the focused control. A link keeps its own Enter, and in a\nmulti-line editor Return inserts the newline while the keypad's Enter\nactivates the button.\n---\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.",
257
257
  "attributes": [
258
258
  {
259
259
  "name": "heading",
@@ -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.6.1",
4
+ "version": "0.6.2",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
@@ -604,7 +604,7 @@
604
604
  },
605
605
  {
606
606
  "name": "vf-dialog",
607
- "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.",
607
+ "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\nKeyboard, the classic Dialog Manager's two rules (VfModalDialog):\non open, focus goes to the first text-entry control — a slotted control\nwith `autofocus` first — or, with none, to the default button\n(`vf-button variant=\"default\"`). Return or Enter activates the default\nbutton from anywhere in the dialog, a focused Cancel included; Space\npresses the focused control. A link keeps its own Enter, and in a\nmulti-line editor Return inserts the newline while the keypad's Enter\nactivates the button.\n---\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.",
608
608
  "doc-url": "",
609
609
  "attributes": [
610
610
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vintage-frames",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
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",