vintage-frames 0.12.3 → 0.12.5

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A web component kit that rebuilds the classic Apple System 7 interface elements.
4
4
 
5
- View the [Examples Page](https://aportilla.github.io/vintage-frames/) to see every element live, with its full API.
5
+ View the [Examples Page](https://vintage-frames.portill.io/) to see every element live, with its full API.
6
6
 
7
7
  ```sh
8
8
  npm install vintage-frames
@@ -233,7 +233,7 @@
233
233
  "declarations": [
234
234
  {
235
235
  "kind": "class",
236
- "description": "The classic System 7 push button (\"OK\", \"Cancel\", \"Install\", …).\n\nA rounded-rectangle control with a 1px black border and white face that\ninverts to white-on-black while pressed. `variant=\"default\"` draws the\nclassic bold double ring around the button, marking it as the default\naction of a dialog.\n\nThe rounded corners are not `border-radius` arcs: frame, face, and ring are\nstepped `clip-path` silhouettes traced pixel-for-pixel from the kit's button\nreference sheet (see `src/pixel-frame.ts`), so every corner renders as the\nexact 1-bit staircase with no antialiasing.\n\nKeyboard focus — an affordance System 7 didn't have, drawn in its idiom\nanyway — is a 1px dashed rule under the label text (`vfFocusUnderline`)\nrather than a ring around the control.\n\nForm-associated: place it inside a `<form>` and `type=\"submit\"` submits the\nform (contributing its `name`/`value` and any `form*` override to the\nsubmission), `type=\"reset\"` resets it. Enter and Space activate it via the\ninner native button, and the submission runs at the end of the click's\npropagation, so `preventDefault()` on the button cancels it the way it\ncancels a native one.\n\nOne thing the platform will not allow: `event.submitter` cannot BE a\nform-associated custom element. It is the transient native proxy this\nbutton submits through — so read the submitting button as\n`event.submitter.closest('vf-button')`, and its identity from\n`submitter.name`/`.value` rather than by comparing element references.\n\nA host-level `aria-label` / `aria-labelledby` names the inner button, and\n`description` (or a host-level `aria-describedby`) describes it — the same\nbridge the fields use, since the role lives on a shadow-internal node the\nplatform can't deliver those to. A `<label for>` deliberately does not\nname it: a `<button>` is not a labelable element.",
236
+ "description": "The classic System 7 push button (\"OK\", \"Cancel\", \"Install\", …).\n\nA rounded-rectangle control with a 1px black border and white face that\ninverts to white-on-black while pressed. `variant=\"default\"` draws the\nclassic bold double ring around the button, marking it as the default\naction of a dialog.\n\nThe rounded corners are not `border-radius` arcs: frame, face, and ring are\nstepped `clip-path` silhouettes traced pixel-for-pixel from the kit's button\nreference sheet (see `src/pixel-frame.ts`), so every corner renders as the\nexact 1-bit staircase with no antialiasing.\n\nKeyboard focus — an affordance System 7 didn't have, drawn in its idiom\nanyway — is a 1px dashed rule under the label text (`vfFocusUnderline`)\nrather than a ring around the control.\n\nForm-associated: place it inside a `<form>` and `type=\"submit\"` submits the\nform (contributing its `name`/`value` and any `form*` override to the\nsubmission), `type=\"reset\"` resets it. Enter and Space activate it via the\ninner native button, and the submission runs at the end of the click's\npropagation, so `preventDefault()` on the button cancels it the way it\ncancels a native one.\n\nOne thing the platform will not allow: `event.submitter` cannot BE a\nform-associated custom element. It is the transient native proxy this\nbutton submits through — so read the submitting button as\n`event.submitter.closest('vf-button')`, and its identity from\n`submitter.name`/`.value` rather than by comparing element references.\n\nWith an `href` it is a link instead: the same art over an `<a href>` in\nplace of the `<button>`, so it is followed the way any link is — a click,\nEnter, a modified or middle click into a new tab, the context menu's link\nitems — and `target`, `rel` and `download` mean what they mean on `<a>`.\nA link is not a form button: `type`, `name`, `value` and the `form*`\noverrides do nothing while `href` is set.\n\nA host-level `aria-label` / `aria-labelledby` names the inner button, and\n`description` (or a host-level `aria-describedby`) describes it — the same\nbridge the fields use, since the role lives on a shadow-internal node the\nplatform can't deliver those to. A `<label for>` deliberately does not\nname it: a `<button>` is not a labelable element.",
237
237
  "name": "VfButton",
238
238
  "cssProperties": [
239
239
  {
@@ -244,7 +244,7 @@
244
244
  ],
245
245
  "cssParts": [
246
246
  {
247
- "description": "The inner native `<button>` element.",
247
+ "description": "The inner native `<button>` element — an `<a>` with `href`.",
248
248
  "name": "button"
249
249
  }
250
250
  ],
@@ -362,6 +362,46 @@
362
362
  "attribute": "formtarget",
363
363
  "reflects": true
364
364
  },
365
+ {
366
+ "kind": "field",
367
+ "name": "href",
368
+ "type": {
369
+ "text": "string | undefined"
370
+ },
371
+ "description": "A URL, and the button becomes a link to it: an `<a href>` renders in\nplace of the inner `<button>` under the same art, and the platform\nfollows it — a click, Enter, a modified or middle click, the context\nmenu's link items. An empty value is no link.\n\nA link is not a form button. While `href` is set, type,\nname, value and the `form*` overrides do nothing, and the\nbutton is never a form's default button for a field's Enter. Inside a\n`vf-dialog`, `variant=\"default\"` still makes it the dialog's default\nbutton, so Return follows the link.\n\nDisabled — by attribute or an ancestor `<fieldset disabled>` — the `<a>`\nkeeps rendering but loses its `href`, taking `role=\"link\"` and\n`aria-disabled=\"true\"` instead: out of the tab order, and inert to the\npointer and to click, as a disabled `<button>` is.",
372
+ "attribute": "href",
373
+ "reflects": true
374
+ },
375
+ {
376
+ "kind": "field",
377
+ "name": "target",
378
+ "type": {
379
+ "text": "string | undefined"
380
+ },
381
+ "description": "With href: where the link opens, as `<a target>`.",
382
+ "attribute": "target",
383
+ "reflects": true
384
+ },
385
+ {
386
+ "kind": "field",
387
+ "name": "rel",
388
+ "type": {
389
+ "text": "string | undefined"
390
+ },
391
+ "description": "With href: the link's relationship to this page, as `<a rel>`.",
392
+ "attribute": "rel",
393
+ "reflects": true
394
+ },
395
+ {
396
+ "kind": "field",
397
+ "name": "download",
398
+ "type": {
399
+ "text": "string | undefined"
400
+ },
401
+ "description": "With href: download the resource instead of navigating to it, as\n`<a download>` — the value, when there is one, names the saved file.",
402
+ "attribute": "download",
403
+ "reflects": true
404
+ },
365
405
  {
366
406
  "kind": "field",
367
407
  "name": "scale",
@@ -380,11 +420,22 @@
380
420
  },
381
421
  {
382
422
  "kind": "field",
383
- "name": "buttonEl",
423
+ "name": "control",
384
424
  "type": {
385
- "text": "HTMLButtonElement | null"
425
+ "text": "HTMLElement | null"
386
426
  },
387
- "privacy": "private"
427
+ "privacy": "private",
428
+ "description": "The inner `<button>` — or `<a>`, under href."
429
+ },
430
+ {
431
+ "kind": "field",
432
+ "name": "isLink",
433
+ "type": {
434
+ "text": "boolean"
435
+ },
436
+ "privacy": "private",
437
+ "description": "Whether href makes this a link.",
438
+ "readonly": true
388
439
  },
389
440
  {
390
441
  "kind": "field",
@@ -414,7 +465,34 @@
414
465
  "text": "void"
415
466
  }
416
467
  },
417
- "description": "Forwards a synthetic activation to the real `<button>` in the shadow\nroot. A `click()` on the host dispatches at the host and propagates *up*,\nnever down to the inner button's listener — so the native control, the\nactual activation surface, is handed the call instead. Its click then\nbubbles back out composed, exactly as a pointer's does. This is what the\nfields' implicit submission (Enter) activates as the form's default\nbutton; a disabled button swallows it natively, as it should.\n\nBefore the first render there is no inner button to forward to. A native\n`click()` always fires, so rather than doing nothing this falls back to\ndispatching at the host — no activation behavior to run yet, but the\nevent a caller asked for."
468
+ "description": "Forwards a synthetic activation to the real `<button>` in the shadow\nroot. A `click()` on the host dispatches at the host and propagates *up*,\nnever down to the inner button's listener — so the native control, the\nactual activation surface, is handed the call instead. Its click then\nbubbles back out composed, exactly as a pointer's does. This is what the\nfields' implicit submission (Enter) activates as the form's default\nbutton; a disabled button swallows it natively, as it should. Under\nhref the `<a>` is handed the call and follows itself — which is\nhow a dialog's Return reaches a default button that is a link. A\ndisabled link has no native state to swallow it, so it is withheld here.\n\nBefore the first render there is no inner button to forward to. A native\n`click()` always fires, so rather than doing nothing this falls back to\ndispatching at the host — no activation behavior to run yet, but the\nevent a caller asked for."
469
+ },
470
+ {
471
+ "kind": "method",
472
+ "name": "renderButton",
473
+ "privacy": "private",
474
+ "parameters": [
475
+ {
476
+ "name": "label",
477
+ "type": {
478
+ "text": "TemplateResult"
479
+ }
480
+ }
481
+ ]
482
+ },
483
+ {
484
+ "kind": "method",
485
+ "name": "renderLink",
486
+ "privacy": "private",
487
+ "parameters": [
488
+ {
489
+ "name": "label",
490
+ "type": {
491
+ "text": "TemplateResult"
492
+ }
493
+ }
494
+ ],
495
+ "description": "The link. No click listener: following it is the platform's activation\nbehavior. A disabled one is an `<a>` without an `href`, which has no\nlink role unless it is given one."
418
496
  },
419
497
  {
420
498
  "kind": "field",
@@ -1110,6 +1188,38 @@
1110
1188
  "description": "See formAction. Overrides the form's `target`.",
1111
1189
  "fieldName": "formTarget"
1112
1190
  },
1191
+ {
1192
+ "name": "href",
1193
+ "type": {
1194
+ "text": "string | undefined"
1195
+ },
1196
+ "description": "A URL, and the button becomes a link to it: an `<a href>` renders in\nplace of the inner `<button>` under the same art, and the platform\nfollows it — a click, Enter, a modified or middle click, the context\nmenu's link items. An empty value is no link.\n\nA link is not a form button. While `href` is set, type,\nname, value and the `form*` overrides do nothing, and the\nbutton is never a form's default button for a field's Enter. Inside a\n`vf-dialog`, `variant=\"default\"` still makes it the dialog's default\nbutton, so Return follows the link.\n\nDisabled — by attribute or an ancestor `<fieldset disabled>` — the `<a>`\nkeeps rendering but loses its `href`, taking `role=\"link\"` and\n`aria-disabled=\"true\"` instead: out of the tab order, and inert to the\npointer and to click, as a disabled `<button>` is.",
1197
+ "fieldName": "href"
1198
+ },
1199
+ {
1200
+ "name": "target",
1201
+ "type": {
1202
+ "text": "string | undefined"
1203
+ },
1204
+ "description": "With href: where the link opens, as `<a target>`.",
1205
+ "fieldName": "target"
1206
+ },
1207
+ {
1208
+ "name": "rel",
1209
+ "type": {
1210
+ "text": "string | undefined"
1211
+ },
1212
+ "description": "With href: the link's relationship to this page, as `<a rel>`.",
1213
+ "fieldName": "rel"
1214
+ },
1215
+ {
1216
+ "name": "download",
1217
+ "type": {
1218
+ "text": "string | undefined"
1219
+ },
1220
+ "description": "With href: download the resource instead of navigating to it, as\n`<a download>` — the value, when there is one, names the saved file.",
1221
+ "fieldName": "download"
1222
+ },
1113
1223
  {
1114
1224
  "name": "top",
1115
1225
  "type": {
@@ -1,3 +1,4 @@
1
+ import { type TemplateResult } from 'lit';
1
2
  import { VfShadowRoleControl } from '../form-control.js';
2
3
  declare const VfButton_base: (new (...args: any[]) => import("../position.js").VfPositionedInterface) & typeof VfShadowRoleControl;
3
4
  /**
@@ -30,6 +31,13 @@ declare const VfButton_base: (new (...args: any[]) => import("../position.js").V
30
31
  * `event.submitter.closest('vf-button')`, and its identity from
31
32
  * `submitter.name`/`.value` rather than by comparing element references.
32
33
  *
34
+ * With an `href` it is a link instead: the same art over an `<a href>` in
35
+ * place of the `<button>`, so it is followed the way any link is — a click,
36
+ * Enter, a modified or middle click into a new tab, the context menu's link
37
+ * items — and `target`, `rel` and `download` mean what they mean on `<a>`.
38
+ * A link is not a form button: `type`, `name`, `value` and the `form*`
39
+ * overrides do nothing while `href` is set.
40
+ *
33
41
  * A host-level `aria-label` / `aria-labelledby` names the inner button, and
34
42
  * `description` (or a host-level `aria-describedby`) describes it — the same
35
43
  * bridge the fields use, since the role lives on a shadow-internal node the
@@ -37,7 +45,8 @@ declare const VfButton_base: (new (...args: any[]) => import("../position.js").V
37
45
  * name it: a `<button>` is not a labelable element.
38
46
  *
39
47
  * @slot - The button label.
40
- * @csspart button - The inner native `<button>` element.
48
+ * @csspart button - The inner native `<button>` element — an `<a>` with
49
+ * `href`.
41
50
  * @cssprop [--vf-button-height=20px] - `vf-button` face (the default ring's
42
51
  * inner box is 80×20)
43
52
  */
@@ -91,11 +100,41 @@ export declare class VfButton extends VfButton_base {
91
100
  formNoValidate: boolean;
92
101
  /** See {@link formAction}. Overrides the form's `target`. */
93
102
  formTarget: string;
103
+ /**
104
+ * A URL, and the button becomes a link to it: an `<a href>` renders in
105
+ * place of the inner `<button>` under the same art, and the platform
106
+ * follows it — a click, Enter, a modified or middle click, the context
107
+ * menu's link items. An empty value is no link.
108
+ *
109
+ * A link is not a form button. While `href` is set, {@link type},
110
+ * {@link name}, {@link value} and the `form*` overrides do nothing, and the
111
+ * button is never a form's default button for a field's Enter. Inside a
112
+ * `vf-dialog`, `variant="default"` still makes it the dialog's default
113
+ * button, so Return follows the link.
114
+ *
115
+ * Disabled — by attribute or an ancestor `<fieldset disabled>` — the `<a>`
116
+ * keeps rendering but loses its `href`, taking `role="link"` and
117
+ * `aria-disabled="true"` instead: out of the tab order, and inert to the
118
+ * pointer and to {@link click}, as a disabled `<button>` is.
119
+ */
120
+ href?: string;
121
+ /** With {@link href}: where the link opens, as `<a target>`. */
122
+ target?: string;
123
+ /** With {@link href}: the link's relationship to this page, as `<a rel>`. */
124
+ rel?: string;
125
+ /**
126
+ * With {@link href}: download the resource instead of navigating to it, as
127
+ * `<a download>` — the value, when there is one, names the saved file.
128
+ */
129
+ download?: string;
94
130
  /** Default-on display scaling (true 72dpi size); see src/scale.ts. */
95
131
  private readonly scale;
96
132
  /** Device-pixel grid snapping; see src/grid-snap.ts. */
97
133
  private readonly gridSnap;
98
- private buttonEl;
134
+ /** The inner `<button>` — or `<a>`, under {@link href}. */
135
+ private control;
136
+ /** Whether {@link href} makes this a link. */
137
+ private get isLink();
99
138
  /**
100
139
  * {@link type}, resolved the way HTML resolves an enumerated attribute:
101
140
  * ASCII case-insensitively, with anything unrecognized falling to the
@@ -115,7 +154,10 @@ export declare class VfButton extends VfButton_base {
115
154
  * actual activation surface, is handed the call instead. Its click then
116
155
  * bubbles back out composed, exactly as a pointer's does. This is what the
117
156
  * fields' implicit submission (Enter) activates as the form's default
118
- * button; a disabled button swallows it natively, as it should.
157
+ * button; a disabled button swallows it natively, as it should. Under
158
+ * {@link href} the `<a>` is handed the call and follows itself — which is
159
+ * how a dialog's Return reaches a default button that is a link. A
160
+ * disabled link has no native state to swallow it, so it is withheld here.
119
161
  *
120
162
  * Before the first render there is no inner button to forward to. A native
121
163
  * `click()` always fires, so rather than doing nothing this falls back to
@@ -123,7 +165,14 @@ export declare class VfButton extends VfButton_base {
123
165
  * event a caller asked for.
124
166
  */
125
167
  click(): void;
126
- render(): import("lit-html").TemplateResult<1>;
168
+ render(): TemplateResult<1>;
169
+ private renderButton;
170
+ /**
171
+ * The link. No click listener: following it is the platform's activation
172
+ * behavior. A disabled one is an `<a>` without an `href`, which has no
173
+ * link role unless it is given one.
174
+ */
175
+ private renderLink;
127
176
  /**
128
177
  * The click that may become a submission or a reset.
129
178
  *
@@ -90,8 +90,9 @@ var S = "vf-submitter", C = class extends i(h) {
90
90
  the stepped pseudo-element silhouettes below. Keeping the clip-paths
91
91
  off the button itself leaves the hit area a plain rectangle, and
92
92
  anything painted outside the silhouette (a ring a consumer restores
93
- on ::part(button)) unclipped. */
94
- button {
93
+ on ::part(button)) unclipped. .control is that button, or the <a>
94
+ that stands in for it under href: the art is the same either way. */
95
+ .control {
95
96
  position: relative;
96
97
  /* Own stacking context so the negative-z silhouettes stay inside the
97
98
  button: above everything behind it, below the label. */
@@ -118,31 +119,40 @@ var S = "vf-submitter", C = class extends i(h) {
118
119
  line-height: inherit;
119
120
  white-space: nowrap;
120
121
  cursor: inherit;
122
+ /* What the UA sheet gives a <button> and an <a> does not: no
123
+ underline, and none of the page's text styling inherited through
124
+ the host — so the link reads exactly as the button does. */
125
+ text-decoration: none;
126
+ text-transform: none;
127
+ text-indent: 0;
128
+ text-shadow: none;
129
+ letter-spacing: normal;
130
+ word-spacing: normal;
121
131
  }
122
132
  /* Frame: the outer silhouette in solid black. The face below covers all
123
133
  but the outline, leaving the reference's exact border pixels — the
124
134
  QuickDraw difference-of-silhouettes, not a stroked border. */
125
- button::before,
126
- button::after {
135
+ .control::before,
136
+ .control::after {
127
137
  content: '';
128
138
  position: absolute;
129
139
  inset: 0;
130
140
  z-index: -1;
131
141
  }
132
- button::before {
142
+ .control::before {
133
143
  background: var(--vf-black, #000);
134
144
  clip-path: ${y(p(u))};
135
145
  }
136
146
  /* Face: white fill inset one pixel, with its own traced corner steps. */
137
- button::after {
147
+ .control::after {
138
148
  background: var(--vf-white, #fff);
139
149
  clip-path: ${y(p(l))};
140
150
  }
141
151
  /* Pressed: instant white-on-black inversion. */
142
- button:active:not(:disabled) {
152
+ .control:active:not(:disabled) {
143
153
  color: var(--vf-white, #fff);
144
154
  }
145
- button:active:not(:disabled)::after {
155
+ .control:active:not(:disabled)::after {
146
156
  background: var(--vf-black, #000);
147
157
  }
148
158
  /* The label rides in its own box so the focus underline can span the
@@ -154,7 +164,7 @@ var S = "vf-submitter", C = class extends i(h) {
154
164
  }
155
165
  /* Keyboard focus is the dashed rule under the label, not a ring around
156
166
  the control — so the UA's own outline goes. */
157
- button:focus-visible {
167
+ .control:focus-visible {
158
168
  outline: none;
159
169
  }
160
170
  /* …and off the host too. Blink doesn't currently propagate
@@ -166,39 +176,77 @@ var S = "vf-submitter", C = class extends i(h) {
166
176
  :host(:focus-visible) {
167
177
  outline: none;
168
178
  }
169
- button:focus-visible .label::after {
179
+ .control:focus-visible .label::after {
170
180
  ${s}
171
181
  }
172
182
  /* Disabled: only the label dims to gray; the solid black border stays. */
173
- button:disabled {
183
+ .control:disabled {
174
184
  color: var(--vf-disabled, #c0c0c0);
175
185
  }
186
+ /* A disabled link has no disabled state of its own, so it is spelled
187
+ out: the same dimmed label, and no press. Letting the pointer through
188
+ to the host keeps the face from inverting (:active never reaches the
189
+ link) and makes the press's target the host itself — a disabled form
190
+ control, which the platform dispatches no click to, as it dispatches
191
+ none to a disabled <button>. */
192
+ a.control[aria-disabled='true'] {
193
+ color: var(--vf-disabled, #c0c0c0);
194
+ pointer-events: none;
195
+ }
176
196
  `
177
197
  ];
178
198
  }
199
+ get isLink() {
200
+ return !!this.href;
201
+ }
179
202
  #e;
180
203
  get resolvedType() {
181
204
  let e = String(this.type ?? "").toLowerCase();
182
205
  return e === "submit" ? "submit" : e === "reset" ? "reset" : "button";
183
206
  }
184
207
  click() {
185
- let e = this.buttonEl;
186
- e ? e.click() : super.click();
208
+ let e = this.control;
209
+ e ? this.isLink && this.isDisabled || e.click() : super.click();
187
210
  }
188
211
  render() {
212
+ let e = _`<span class="label"><slot></slot></span>`;
213
+ return _`
214
+ ${this.isLink ? this.renderLink(e) : this.renderButton(e)}
215
+ ${this.renderDescription()}
216
+ `;
217
+ }
218
+ renderButton(e) {
189
219
  return _`
190
220
  <button
191
221
  part="button"
192
- class="vf-snap"
222
+ class="control vf-snap"
193
223
  type="button"
194
224
  aria-label=${this.hostAriaLabel || v}
195
225
  aria-describedby=${this.describedBy}
196
226
  ?disabled=${this.isDisabled}
197
227
  @click=${this.handleClick}
198
228
  >
199
- <span class="label"><slot></slot></span>
229
+ ${e}
200
230
  </button>
201
- ${this.renderDescription()}
231
+ `;
232
+ }
233
+ renderLink(e) {
234
+ let t = this.isDisabled;
235
+ return _`
236
+ <a
237
+ part="button"
238
+ class="control vf-snap"
239
+ href=${t ? v : this.href}
240
+ target=${this.target || v}
241
+ rel=${this.rel || v}
242
+ download=${this.download ?? v}
243
+ role=${t ? "link" : v}
244
+ aria-disabled=${t ? "true" : v}
245
+ aria-label=${this.hostAriaLabel || v}
246
+ aria-describedby=${this.describedBy}
247
+ >
248
+ ${e}
249
+ </a>
202
250
  `;
203
251
  }
204
252
  activate() {
@@ -229,6 +277,6 @@ r([b({ reflect: !0 })], C.prototype, "variant", void 0), r([b({ reflect: !0 })],
229
277
  })], C.prototype, "formNoValidate", void 0), r([b({
230
278
  attribute: "formtarget",
231
279
  reflect: !0
232
- })], C.prototype, "formTarget", void 0), r([x("button")], C.prototype, "buttonEl", void 0), C = r([t("vf-button")], C);
280
+ })], C.prototype, "formTarget", void 0), r([b({ reflect: !0 })], C.prototype, "href", void 0), r([b({ reflect: !0 })], C.prototype, "target", void 0), r([b({ reflect: !0 })], C.prototype, "rel", void 0), r([b({ reflect: !0 })], C.prototype, "download", void 0), r([x(".control")], C.prototype, "control", void 0), C = r([t("vf-button")], C);
233
281
  //#endregion
234
282
  export { C as VfButton };
@@ -57,7 +57,7 @@ var l = class extends a {
57
57
  requestImplicitSubmit() {
58
58
  let e = this.internals.form;
59
59
  if (!e) return !1;
60
- let t = [...e.elements].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");
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.href && e.type?.toLowerCase() === "submit");
61
61
  return t ? (t.matches(":disabled") || t.click(), !0) : (e.requestSubmit(), !0);
62
62
  }
63
63
  emitValue(t, n = { value: this.value }) {
package/docs/SPEC.md CHANGED
@@ -250,8 +250,8 @@ System 7's fixed modal alert (double frame: 2px outer, 2px gap, 1px inner rule,
250
250
  ### Group B — buttons & toggles
251
251
 
252
252
  #### `vf-button` (`VfButton`, vf-button.ts)
253
- - **Attributes/props:** `variant?: 'default'` (the double-ring default button, e.g. "Install"), `disabled`, `type: 'button' | 'submit' | 'reset'` (reflected; default `'button'`), plus the submission overrides `formaction`, `formenctype`, `formmethod`, `formnovalidate` and `formtarget`, each honored on `type="submit"` only, as HTML honors them — they are handed to the native proxy below, so the behavior is the platform's rather than an emulation. (`formAction`'s *getter* returns the string as set, where the native IDL returns it resolved against the base URL; the submission resolves normally.) `type` is read the way HTML reads an enumerated attribute — ASCII case-insensitively, unrecognized values falling to the default — so `type="SUBMIT"` is a submit button. Two deliberate departures: HTML's missing-value default is `submit` and this one is `button`, because an element that silently submits the form it sits in is surprising; and the invalid-value default follows the missing one rather than HTML's `submit`, so a misspelling does nothing instead of submitting.
254
- - **Visual:** inner `<button>`: height `var(--vf-button-height, 20px)`, `min-width: 64px`, `padding: 0 14px`, bold black text, font per tokens. The button is 20px, not the fields' 22px: both 1x sheets measure the face at 80×20, and the default ring's *inner* box is exactly that, so the ring traces assume a 20px face. The label rides in its own `.label` span (the flex item, shrink-wrapped to the text) so the focus rule below can span the text rather than the padded face. The rounded rect is NOT `border-radius` (which antialiases): the button paints no box of its own; two pseudo-element layers carry stepped `clip-path` silhouettes traced from the reference sheet (`src/pixel-frame.ts`, traced from the 1x button reference sheet; `npm run verify:buttons` checks the compiled polygons render those profiles faithfully): `::before` fills `var(--vf-black)` clipped to the outer silhouette (corner insets `[3,1,1]`, then straight), `::after` fills `var(--vf-white)` clipped to the face (row 1 corner insets `[3,2]`, then 1px inside) — the 1px frame, corner steps included, is the QuickDraw-style difference of the two. Clip-paths stay off the `<button>` so the hit area stays a plain rectangle and anything painted outside the silhouette isn't swallowed. All coordinates are `calc(var(--vf-scale,1) * Npx)` system pixels, so edges land on whole device pixels and never antialias.
253
+ - **Attributes/props:** `variant?: 'default'` (the double-ring default button, e.g. "Install"), `disabled`, `type: 'button' | 'submit' | 'reset'` (reflected; default `'button'`), plus the submission overrides `formaction`, `formenctype`, `formmethod`, `formnovalidate` and `formtarget`, each honored on `type="submit"` only, as HTML honors them — they are handed to the native proxy below, so the behavior is the platform's rather than an emulation. (`formAction`'s *getter* returns the string as set, where the native IDL returns it resolved against the base URL; the submission resolves normally.) `type` is read the way HTML reads an enumerated attribute — ASCII case-insensitively, unrecognized values falling to the default — so `type="SUBMIT"` is a submit button. Two deliberate departures: HTML's missing-value default is `submit` and this one is `button`, because an element that silently submits the form it sits in is surprising; and the invalid-value default follows the missing one rather than HTML's `submit`, so a misspelling does nothing instead of submitting. `href` (optional, reflected; unset by default so no `href=""` lands on every host) with `target`, `rel` and `download` makes it a link — see **Link** below.
254
+ - **Visual:** inner `<button>` (an `<a>` under `href`, carrying the same `.control` rules): height `var(--vf-button-height, 20px)`, `min-width: 64px`, `padding: 0 14px`, bold black text, font per tokens. The button is 20px, not the fields' 22px: both 1x sheets measure the face at 80×20, and the default ring's *inner* box is exactly that, so the ring traces assume a 20px face. The label rides in its own `.label` span (the flex item, shrink-wrapped to the text) so the focus rule below can span the text rather than the padded face. The rounded rect is NOT `border-radius` (which antialiases): the button paints no box of its own; two pseudo-element layers carry stepped `clip-path` silhouettes traced from the reference sheet (`src/pixel-frame.ts`, traced from the 1x button reference sheet; `npm run verify:buttons` checks the compiled polygons render those profiles faithfully): `::before` fills `var(--vf-black)` clipped to the outer silhouette (corner insets `[3,1,1]`, then straight), `::after` fills `var(--vf-white)` clipped to the face (row 1 corner insets `[3,2]`, then 1px inside) — the 1px frame, corner steps included, is the QuickDraw-style difference of the two. Clip-paths stay off the `<button>` so the hit area stays a plain rectangle and anything painted outside the silhouette isn't swallowed. All coordinates are `calc(var(--vf-scale,1) * Npx)` system pixels, so edges land on whole device pixels and never antialias.
255
255
  - `:active` (pressed, not disabled): invert — the `::after` face flips to black, white text.
256
256
  - `:focus-visible`: **no ring** — the UA outline is off on both the inner button and the (`delegatesFocus`) host, replaced by `vfFocusUnderline` (§4) on the label: a 1px dashed rule 1px below the baseline, i.e. row 15 of the 20px face, one blank row under the glyph ink. It inverts with the label when pressed; the controls that cannot carry the mark on their own face keep the dotted ring. `npm run verify:focus` and the focused button in `npm run shot:verify` assert the rendered pixels.
257
257
  - `disabled`: only the label dims to `var(--vf-disabled, #c0c0c0)`; the 1px black border stays black. (For `variant="default"`, the fat outer ring dims to `var(--vf-disabled)` while the inner black border stays — the ring and the title dim together, the button's own frame does not. System 7 dimmed both with a 50% stipple; flat gray standing in for it is the kit's liberty, per §1.) The ring rule is keyed on **both** disabled routes — `[disabled]` and `:state(form-disabled)` — as separate rules: an ancestor `<fieldset disabled>` arrives through `formDisabledCallback` and sets no attribute, and keyed on the attribute alone it left a solid black ring around a greyed label.
@@ -260,10 +260,11 @@ System 7's fixed modal alert (double frame: 2px outer, 2px gap, 1px inner rule,
260
260
  - **Behavior:** form-associated. `type="submit"` submits by inserting a transient native proxy button carrying this button's `name`/`value` and any `form*` override, then clicking it — a form-associated custom element can't be a native submitter itself (`requestSubmit(vfButton)` throws a `TypeError`, "not a submit button"), and a bare `requestSubmit()` would submit with `event.submitter === null` and no name/value; `reset` → the same proxy path. Enter/Space work natively via the inner button, and the host overrides `click()` to forward to it (a click dispatched at the host propagates up, never down into the shadow tree; before the first render there is no inner button, so it falls back to dispatching at the host, since a native `click()` always fires) — which is also how the fields' Enter reaches this button as the form's default button (see `requestImplicitSubmit`, text-control.ts; its `vf-button` leg lowercases `type` for the enumerated rule above).
261
261
  - **When the action runs.** At the *end* of the click's propagation, not where the listener sits. HTML runs a button's activation behavior once the click has finished propagating, which is what lets `preventDefault()` on the button cancel the submission; this component's listener is on the inner `<button>`, first on that path rather than last, so acting there beat every listener a consumer could write and only a capture-phase cancel ever landed. It now defers by adding a listener to the window *during* dispatch (each node's listener list is read as that node is reached), so both phases cancel. `stopPropagation()` cancels nothing, per HTML — the event then never reaches the window and a task picks the action up instead; a task and never a microtask, which would interleave between the listeners of a trusted dispatch.
262
262
  - **Submitter identity.** `event.submitter` can never *be* the `vf-button` — the platform forbids it. It is the proxy, which is parented to the host and carries a `slot` name no shadow root offers: unslotted, so it is never rendered, measured or in the a11y tree, and off the flattened tree so its own click can't travel back up through the shadow `<button>` the click handler is bound to. Being a light-DOM descendant is what makes `event.submitter.closest('vf-button')` resolve to the component — the closest to identity the platform allows. Its own click is stopped at the proxy, so one press is one click at the form (`stopPropagation` leaves the activation behavior alone).
263
+ - **Link:** a non-empty `href` renders an `<a part="button" href target rel download>` in place of the `<button>`, with no click listener of its own: following it — a click, Enter, a modified or middle click into a new tab, the context menu's link items — is the platform's. The art is identical at rest, focused and pressed; the `.control` rule adds the resets the UA sheet gives a `<button>` and not an `<a>` (`text-decoration`, `text-transform`, `text-indent`, `text-shadow`, `letter-spacing`, `word-spacing`), so the host's inherited text styling can't reach the label. A link is not a form button: `type`, `name`, `value` and the `form*` overrides do nothing, and `requestImplicitSubmit` skips a `vf-button` with an `href` whatever its `type`. The host stays form-associated (it cannot opt out per instance), so `disabled` and an ancestor `<fieldset disabled>` still disable it. Disabled, the `<a>` drops its `href` and takes `role="link"` + `aria-disabled="true"` — out of the tab order, label dimmed, ring dimmed — with `pointer-events: none`, which keeps the face from inverting and makes a press land on the disabled host, which gets no click; `click()` is withheld outright, since a disabled `<a>` has no native state to swallow it. `click()` on an enabled link forwards to the `<a>`, so a `vf-dialog`'s Return follows a `variant="default"` link, and the dialog's keydown leaves Enter on a focused link to the link (its composed path carries the `<a href>`).
263
264
  - **Name:** the slotted label, with a host-level `aria-label`/`aria-labelledby` bridged to the inner button when present (see §4's name bridge); a `<label for>` deliberately doesn't name it. `description` and a host-level `aria-describedby` reach it through the same shadow-internal span the fields use.
264
- - **Slots:** default (label). **Parts:** `button`.
265
+ - **Slots:** default (label). **Parts:** `button` (the `<button>`, or the `<a>` under `href`).
265
266
  - **Events:** none custom (native `click` suffices).
266
- - **Verified by:** `npm run verify:button` (the form contract, the name bridge, the enumerated `type`, both disabled routes and the pinned metrics); `verify:buttons` (the clip-path traces), `verify:focus` (the dashed rule).
267
+ - **Verified by:** `npm run verify:button` (the form contract, the name bridge, the enumerated `type`, both disabled routes, the pinned metrics, and the link: role, following by click/Enter/`click()`/modified click, the disabled link, form inertness, a dialog's default link and pixel parity with the button); `verify:buttons` (the clip-path traces), `verify:focus` (the dashed rule).
267
268
 
268
269
  #### `vf-button-group` (`VfButtonGroup`, vf-button-group.ts)
269
270
  - **Attributes/props:** `vertical` (stack in a column instead of a row), `natural` (let each button keep its own content width; off by default, so grouped buttons are uniform width — the classic System 7 dialog behavior).
@@ -41,7 +41,7 @@
41
41
  },
42
42
  {
43
43
  "name": "vf-button",
44
- "description": "The classic System 7 push button (\"OK\", \"Cancel\", \"Install\", …).\n\nA rounded-rectangle control with a 1px black border and white face that\ninverts to white-on-black while pressed. `variant=\"default\"` draws the\nclassic bold double ring around the button, marking it as the default\naction of a dialog.\n\nThe rounded corners are not `border-radius` arcs: frame, face, and ring are\nstepped `clip-path` silhouettes traced pixel-for-pixel from the kit's button\nreference sheet (see `src/pixel-frame.ts`), so every corner renders as the\nexact 1-bit staircase with no antialiasing.\n\nKeyboard focus — an affordance System 7 didn't have, drawn in its idiom\nanyway — is a 1px dashed rule under the label text (`vfFocusUnderline`)\nrather than a ring around the control.\n\nForm-associated: place it inside a `<form>` and `type=\"submit\"` submits the\nform (contributing its `name`/`value` and any `form*` override to the\nsubmission), `type=\"reset\"` resets it. Enter and Space activate it via the\ninner native button, and the submission runs at the end of the click's\npropagation, so `preventDefault()` on the button cancels it the way it\ncancels a native one.\n\nOne thing the platform will not allow: `event.submitter` cannot BE a\nform-associated custom element. It is the transient native proxy this\nbutton submits through — so read the submitting button as\n`event.submitter.closest('vf-button')`, and its identity from\n`submitter.name`/`.value` rather than by comparing element references.\n\nA host-level `aria-label` / `aria-labelledby` names the inner button, and\n`description` (or a host-level `aria-describedby`) describes it — the same\nbridge the fields use, since the role lives on a shadow-internal node the\nplatform can't deliver those to. A `<label for>` deliberately does not\nname it: a `<button>` is not a labelable element.\n---\n\n\n### **Slots:**\n - _default_ - The button label.\n\n### **CSS Properties:**\n - **--vf-button-height** - `vf-button` face (the default ring's inner box is 80×20) _(default: 20px)_\n\n### **CSS Parts:**\n - **button** - The inner native `<button>` element.",
44
+ "description": "The classic System 7 push button (\"OK\", \"Cancel\", \"Install\", …).\n\nA rounded-rectangle control with a 1px black border and white face that\ninverts to white-on-black while pressed. `variant=\"default\"` draws the\nclassic bold double ring around the button, marking it as the default\naction of a dialog.\n\nThe rounded corners are not `border-radius` arcs: frame, face, and ring are\nstepped `clip-path` silhouettes traced pixel-for-pixel from the kit's button\nreference sheet (see `src/pixel-frame.ts`), so every corner renders as the\nexact 1-bit staircase with no antialiasing.\n\nKeyboard focus — an affordance System 7 didn't have, drawn in its idiom\nanyway — is a 1px dashed rule under the label text (`vfFocusUnderline`)\nrather than a ring around the control.\n\nForm-associated: place it inside a `<form>` and `type=\"submit\"` submits the\nform (contributing its `name`/`value` and any `form*` override to the\nsubmission), `type=\"reset\"` resets it. Enter and Space activate it via the\ninner native button, and the submission runs at the end of the click's\npropagation, so `preventDefault()` on the button cancels it the way it\ncancels a native one.\n\nOne thing the platform will not allow: `event.submitter` cannot BE a\nform-associated custom element. It is the transient native proxy this\nbutton submits through — so read the submitting button as\n`event.submitter.closest('vf-button')`, and its identity from\n`submitter.name`/`.value` rather than by comparing element references.\n\nWith an `href` it is a link instead: the same art over an `<a href>` in\nplace of the `<button>`, so it is followed the way any link is — a click,\nEnter, a modified or middle click into a new tab, the context menu's link\nitems — and `target`, `rel` and `download` mean what they mean on `<a>`.\nA link is not a form button: `type`, `name`, `value` and the `form*`\noverrides do nothing while `href` is set.\n\nA host-level `aria-label` / `aria-labelledby` names the inner button, and\n`description` (or a host-level `aria-describedby`) describes it — the same\nbridge the fields use, since the role lives on a shadow-internal node the\nplatform can't deliver those to. A `<label for>` deliberately does not\nname it: a `<button>` is not a labelable element.\n---\n\n\n### **Slots:**\n - _default_ - The button label.\n\n### **CSS Properties:**\n - **--vf-button-height** - `vf-button` face (the default ring's inner box is 80×20) _(default: 20px)_\n\n### **CSS Parts:**\n - **button** - The inner native `<button>` element — an `<a>` with `href`.",
45
45
  "attributes": [
46
46
  {
47
47
  "name": "variant",
@@ -92,6 +92,26 @@
92
92
  "description": "See formAction. Overrides the form's `target`.",
93
93
  "values": []
94
94
  },
95
+ {
96
+ "name": "href",
97
+ "description": "A URL, and the button becomes a link to it: an `<a href>` renders in\nplace of the inner `<button>` under the same art, and the platform\nfollows it — a click, Enter, a modified or middle click, the context\nmenu's link items. An empty value is no link.\n\nA link is not a form button. While `href` is set, type,\nname, value and the `form*` overrides do nothing, and the\nbutton is never a form's default button for a field's Enter. Inside a\n`vf-dialog`, `variant=\"default\"` still makes it the dialog's default\nbutton, so Return follows the link.\n\nDisabled — by attribute or an ancestor `<fieldset disabled>` — the `<a>`\nkeeps rendering but loses its `href`, taking `role=\"link\"` and\n`aria-disabled=\"true\"` instead: out of the tab order, and inert to the\npointer and to click, as a disabled `<button>` is.",
98
+ "values": []
99
+ },
100
+ {
101
+ "name": "target",
102
+ "description": "With href: where the link opens, as `<a target>`.",
103
+ "values": []
104
+ },
105
+ {
106
+ "name": "rel",
107
+ "description": "With href: the link's relationship to this page, as `<a rel>`.",
108
+ "values": []
109
+ },
110
+ {
111
+ "name": "download",
112
+ "description": "With href: download the resource instead of navigating to it, as\n`<a download>` — the value, when there is one, names the saved file.",
113
+ "values": []
114
+ },
95
115
  {
96
116
  "name": "top",
97
117
  "description": "Offset from the top of the positioning parent, in whole system px.\nSetting this (or `left`) absolutely positions the element within its\nparent; the coordinate left unset is 0. Remove both to return the\nelement to normal flow.",
@@ -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.12.3",
4
+ "version": "0.12.5",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
@@ -85,7 +85,7 @@
85
85
  },
86
86
  {
87
87
  "name": "vf-button",
88
- "description": "The classic System 7 push button (\"OK\", \"Cancel\", \"Install\", …).\n\nA rounded-rectangle control with a 1px black border and white face that\ninverts to white-on-black while pressed. `variant=\"default\"` draws the\nclassic bold double ring around the button, marking it as the default\naction of a dialog.\n\nThe rounded corners are not `border-radius` arcs: frame, face, and ring are\nstepped `clip-path` silhouettes traced pixel-for-pixel from the kit's button\nreference sheet (see `src/pixel-frame.ts`), so every corner renders as the\nexact 1-bit staircase with no antialiasing.\n\nKeyboard focus — an affordance System 7 didn't have, drawn in its idiom\nanyway — is a 1px dashed rule under the label text (`vfFocusUnderline`)\nrather than a ring around the control.\n\nForm-associated: place it inside a `<form>` and `type=\"submit\"` submits the\nform (contributing its `name`/`value` and any `form*` override to the\nsubmission), `type=\"reset\"` resets it. Enter and Space activate it via the\ninner native button, and the submission runs at the end of the click's\npropagation, so `preventDefault()` on the button cancels it the way it\ncancels a native one.\n\nOne thing the platform will not allow: `event.submitter` cannot BE a\nform-associated custom element. It is the transient native proxy this\nbutton submits through — so read the submitting button as\n`event.submitter.closest('vf-button')`, and its identity from\n`submitter.name`/`.value` rather than by comparing element references.\n\nA host-level `aria-label` / `aria-labelledby` names the inner button, and\n`description` (or a host-level `aria-describedby`) describes it — the same\nbridge the fields use, since the role lives on a shadow-internal node the\nplatform can't deliver those to. A `<label for>` deliberately does not\nname it: a `<button>` is not a labelable element.\n---\n\n\n### **Slots:**\n - _default_ - The button label.\n\n### **CSS Properties:**\n - **--vf-button-height** - `vf-button` face (the default ring's inner box is 80×20) _(default: 20px)_\n\n### **CSS Parts:**\n - **button** - The inner native `<button>` element.",
88
+ "description": "The classic System 7 push button (\"OK\", \"Cancel\", \"Install\", …).\n\nA rounded-rectangle control with a 1px black border and white face that\ninverts to white-on-black while pressed. `variant=\"default\"` draws the\nclassic bold double ring around the button, marking it as the default\naction of a dialog.\n\nThe rounded corners are not `border-radius` arcs: frame, face, and ring are\nstepped `clip-path` silhouettes traced pixel-for-pixel from the kit's button\nreference sheet (see `src/pixel-frame.ts`), so every corner renders as the\nexact 1-bit staircase with no antialiasing.\n\nKeyboard focus — an affordance System 7 didn't have, drawn in its idiom\nanyway — is a 1px dashed rule under the label text (`vfFocusUnderline`)\nrather than a ring around the control.\n\nForm-associated: place it inside a `<form>` and `type=\"submit\"` submits the\nform (contributing its `name`/`value` and any `form*` override to the\nsubmission), `type=\"reset\"` resets it. Enter and Space activate it via the\ninner native button, and the submission runs at the end of the click's\npropagation, so `preventDefault()` on the button cancels it the way it\ncancels a native one.\n\nOne thing the platform will not allow: `event.submitter` cannot BE a\nform-associated custom element. It is the transient native proxy this\nbutton submits through — so read the submitting button as\n`event.submitter.closest('vf-button')`, and its identity from\n`submitter.name`/`.value` rather than by comparing element references.\n\nWith an `href` it is a link instead: the same art over an `<a href>` in\nplace of the `<button>`, so it is followed the way any link is — a click,\nEnter, a modified or middle click into a new tab, the context menu's link\nitems — and `target`, `rel` and `download` mean what they mean on `<a>`.\nA link is not a form button: `type`, `name`, `value` and the `form*`\noverrides do nothing while `href` is set.\n\nA host-level `aria-label` / `aria-labelledby` names the inner button, and\n`description` (or a host-level `aria-describedby`) describes it — the same\nbridge the fields use, since the role lives on a shadow-internal node the\nplatform can't deliver those to. A `<label for>` deliberately does not\nname it: a `<button>` is not a labelable element.\n---\n\n\n### **Slots:**\n - _default_ - The button label.\n\n### **CSS Properties:**\n - **--vf-button-height** - `vf-button` face (the default ring's inner box is 80×20) _(default: 20px)_\n\n### **CSS Parts:**\n - **button** - The inner native `<button>` element — an `<a>` with `href`.",
89
89
  "doc-url": "",
90
90
  "attributes": [
91
91
  {
@@ -136,6 +136,26 @@
136
136
  "description": "See formAction. Overrides the form's `target`.",
137
137
  "value": { "type": "string", "default": "''" }
138
138
  },
139
+ {
140
+ "name": "href",
141
+ "description": "A URL, and the button becomes a link to it: an `<a href>` renders in\nplace of the inner `<button>` under the same art, and the platform\nfollows it — a click, Enter, a modified or middle click, the context\nmenu's link items. An empty value is no link.\n\nA link is not a form button. While `href` is set, type,\nname, value and the `form*` overrides do nothing, and the\nbutton is never a form's default button for a field's Enter. Inside a\n`vf-dialog`, `variant=\"default\"` still makes it the dialog's default\nbutton, so Return follows the link.\n\nDisabled — by attribute or an ancestor `<fieldset disabled>` — the `<a>`\nkeeps rendering but loses its `href`, taking `role=\"link\"` and\n`aria-disabled=\"true\"` instead: out of the tab order, and inert to the\npointer and to click, as a disabled `<button>` is.",
142
+ "value": { "type": "string | undefined" }
143
+ },
144
+ {
145
+ "name": "target",
146
+ "description": "With href: where the link opens, as `<a target>`.",
147
+ "value": { "type": "string | undefined" }
148
+ },
149
+ {
150
+ "name": "rel",
151
+ "description": "With href: the link's relationship to this page, as `<a rel>`.",
152
+ "value": { "type": "string | undefined" }
153
+ },
154
+ {
155
+ "name": "download",
156
+ "description": "With href: download the resource instead of navigating to it, as\n`<a download>` — the value, when there is one, names the saved file.",
157
+ "value": { "type": "string | undefined" }
158
+ },
139
159
  {
140
160
  "name": "top",
141
161
  "description": "Offset from the top of the positioning parent, in whole system px.\nSetting this (or `left`) absolutely positions the element within its\nparent; the coordinate left unset is 0. Remove both to return the\nelement to normal flow.",
@@ -221,6 +241,26 @@
221
241
  "description": "See formAction. Overrides the form's `target`.",
222
242
  "type": "string"
223
243
  },
244
+ {
245
+ "name": "href",
246
+ "description": "A URL, and the button becomes a link to it: an `<a href>` renders in\nplace of the inner `<button>` under the same art, and the platform\nfollows it — a click, Enter, a modified or middle click, the context\nmenu's link items. An empty value is no link.\n\nA link is not a form button. While `href` is set, type,\nname, value and the `form*` overrides do nothing, and the\nbutton is never a form's default button for a field's Enter. Inside a\n`vf-dialog`, `variant=\"default\"` still makes it the dialog's default\nbutton, so Return follows the link.\n\nDisabled — by attribute or an ancestor `<fieldset disabled>` — the `<a>`\nkeeps rendering but loses its `href`, taking `role=\"link\"` and\n`aria-disabled=\"true\"` instead: out of the tab order, and inert to the\npointer and to click, as a disabled `<button>` is.",
247
+ "type": "string | undefined"
248
+ },
249
+ {
250
+ "name": "target",
251
+ "description": "With href: where the link opens, as `<a target>`.",
252
+ "type": "string | undefined"
253
+ },
254
+ {
255
+ "name": "rel",
256
+ "description": "With href: the link's relationship to this page, as `<a rel>`.",
257
+ "type": "string | undefined"
258
+ },
259
+ {
260
+ "name": "download",
261
+ "description": "With href: download the resource instead of navigating to it, as\n`<a download>` — the value, when there is one, names the saved file.",
262
+ "type": "string | undefined"
263
+ },
224
264
  {
225
265
  "name": "top",
226
266
  "description": "Offset from the top of the positioning parent, in whole system px.\nSetting this (or `left`) absolutely positions the element within its\nparent; the coordinate left unset is 0. Remove both to return the\nelement to normal flow.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vintage-frames",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
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",