vintage-frames 0.19.0 → 0.19.1

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.
@@ -1,4 +1,5 @@
1
1
  import { LitElement } from 'lit';
2
+ import type { VfMenu } from './vf-menu.js';
2
3
  declare const VfMenuItem_base: (new (...args: any[]) => import("../position.js").VfPositionedInterface) & typeof LitElement;
3
4
  /**
4
5
  * `<vf-menu-item>` — a single command inside a `<vf-menu>` panel.
@@ -10,6 +11,11 @@ declare const VfMenuItem_base: (new (...args: any[]) => import("../position.js")
10
11
  * 3-blink inversion (~250ms), then dispatches `vf-menu-select` and asks its
11
12
  * ancestors to close the menu.
12
13
  *
14
+ * A `<vf-menu>` in the `submenu` slot makes the item open a submenu: it ends
15
+ * in ▶ where a shortcut would go, and choosing it opens the submenu beside
16
+ * it rather than firing `vf-menu-select`. The item stays highlighted while
17
+ * its submenu is open.
18
+ *
13
19
  * Takes `top`/`left` like every other element ({@link VfPositioned}), for the
14
20
  * consumer who wants a row somewhere other than a pulldown. Inside its parent
15
21
  * `<vf-menu>` the panel is as wide as its widest row and stacks them in flow,
@@ -18,10 +24,12 @@ declare const VfMenuItem_base: (new (...args: any[]) => import("../position.js")
18
24
  * it says — but not how a pulldown is laid out.
19
25
  *
20
26
  * @slot - The item label.
27
+ * @slot submenu - A `vf-menu`, the submenu the item opens.
21
28
  * @csspart item - The row container.
22
29
  * @csspart check - The ✓ checkmark glyph (rendered when `checked`).
23
30
  * @csspart label - The label wrapper around the default slot.
24
31
  * @csspart shortcut - The shortcut text, left-aligned in the shared column.
32
+ * @csspart arrow - The ▶ of an item that opens a submenu.
25
33
  * @fires vf-menu-select - After the blink completes. `detail: { value, item }`.
26
34
  * Named for the menu rather than plain `vf-select`, which would collide with
27
35
  * the `<vf-select>` popup on any delegated ancestor listener (that component
@@ -46,12 +54,19 @@ export declare class VfMenuItem extends VfMenuItem_base {
46
54
  /** Shows the classic ✓ checkmark in the left gutter. */
47
55
  checked: boolean;
48
56
  /**
49
- * Transient highlight — the full-row inversion the press-drag gesture paints
50
- * on the row under the pointer (`:hover` can't: under touch the pointer is
51
- * captured by the title the press started on). Managed by the menu, mirroring
52
- * `vf-option[active]`; not part of the authoring API.
57
+ * Transient highlight — the full-row inversion on the row under the pointer
58
+ * while its menu is open, button down or not (`:hover` can't: under touch
59
+ * the pointer is captured by the title the press started on), or on the
60
+ * item whose submenu is open while the pointer is elsewhere. Managed by the
61
+ * menu, mirroring `vf-option[active]`; not part of the authoring API.
53
62
  */
54
63
  active: boolean;
64
+ /**
65
+ * Whether this item's submenu is open. Reflected, and announced as
66
+ * `aria-expanded`. Managed by the menu ({@link VfMenu.expand}), like
67
+ * {@link active}; not part of the authoring API.
68
+ */
69
+ expanded: boolean;
55
70
  /**
56
71
  * Declares the item a *checkable* toggle up front, so it carries
57
72
  * `role="menuitemcheckbox"` and `aria-checked="false"` from the first render
@@ -78,11 +93,21 @@ export declare class VfMenuItem extends VfMenuItem_base {
78
93
  shortcut: string;
79
94
  /**
80
95
  * Value reported in the `vf-menu-select` event detail. Defaults to the item's
81
- * trimmed text content when unset.
96
+ * {@link labelText} when unset.
82
97
  */
83
98
  value?: string;
84
99
  /** `'on' | 'off'` while the selection blink runs, otherwise `null`. */
85
100
  private _blinkPhase;
101
+ /** Whether a `vf-menu` sits in the `submenu` slot. */
102
+ private _hasSubmenu;
103
+ /** The submenu this item opens: the `vf-menu` in its `submenu` slot, or null. */
104
+ get submenu(): VfMenu | null;
105
+ /**
106
+ * The item's label as text: what its default slot holds, trimmed, without
107
+ * the rows of a submenu. The default {@link value} and the menus'
108
+ * type-ahead read it, and it names an item that opens a submenu.
109
+ */
110
+ get labelText(): string;
86
111
  constructor();
87
112
  connectedCallback(): void;
88
113
  disconnectedCallback(): void;
@@ -95,6 +120,9 @@ export declare class VfMenuItem extends VfMenuItem_base {
95
120
  * over, which the row's own `click` never sees (a press that started on the
96
121
  * title dispatches its click above both of them). No-op while disabled or
97
122
  * already blinking.
123
+ *
124
+ * An item that opens a submenu is never chosen: it opens the submenu and
125
+ * moves focus to its first enabled item, as Enter, Space and → do.
98
126
  */
99
127
  activate(): void;
100
128
  }
@@ -7,47 +7,47 @@ import { vfDisplay as o } from "../styles/recipes/display-face.js";
7
7
  import { vfBase as s } from "../styles/recipes/host.js";
8
8
  import { releaseAfterGesture as c } from "../document-listeners.js";
9
9
  import { runSelectionBlink as l } from "../motion.js";
10
- import { CHECKMARK as u, glyphSvg as d } from "../glyphs.js";
11
- import { LitElement as f, css as p, html as m, nothing as h } from "lit";
12
- import { property as g, state as _ } from "lit/decorators.js";
13
- import { classMap as v } from "lit/directives/class-map.js";
10
+ import { CARET_RIGHT as u, CHECKMARK as d, glyphSvg as f } from "../glyphs.js";
11
+ import { LitElement as p, css as m, html as h, nothing as g } from "lit";
12
+ import { property as _, state as v } from "lit/decorators.js";
13
+ import { classMap as y } from "lit/directives/class-map.js";
14
14
  //#region src/components/vf-menu-item.ts
15
- var y = /* @__PURE__ */ new Map([
15
+ var b = /* @__PURE__ */ new Map([
16
16
  ["⌃", "Control"],
17
17
  ["⌥", "Alt"],
18
18
  ["⇧", "Shift"],
19
19
  ["⌘", "Meta"]
20
- ]), b = /* @__PURE__ */ new Map([
20
+ ]), x = /* @__PURE__ */ new Map([
21
21
  ["←", "ArrowLeft"],
22
22
  ["↑", "ArrowUp"],
23
23
  ["→", "ArrowRight"],
24
24
  ["↓", "ArrowDown"]
25
- ]), x = (e) => b.get(e) ?? e;
26
- function S(e) {
25
+ ]), S = (e) => x.get(e) ?? e;
26
+ function C(e) {
27
27
  let t = [...e.trim()], n = [];
28
- for (; t.length > 0 && y.has(t[0]);) n.push(y.get(t.shift()));
28
+ for (; t.length > 0 && b.has(t[0]);) n.push(b.get(t.shift()));
29
29
  let r = t.join("");
30
- return r ? [...n, x(r)].join("+") : "";
30
+ return r ? [...n, S(r)].join("+") : "";
31
31
  }
32
- function C(e, t) {
32
+ function w(e, t) {
33
33
  let n = [...t.trim()], r = {
34
34
  Meta: !1,
35
35
  Control: !1,
36
36
  Alt: !1,
37
37
  Shift: !1
38
38
  };
39
- for (; n.length > 0 && y.has(n[0]);) r[y.get(n.shift())] = !0;
39
+ for (; n.length > 0 && b.has(n[0]);) r[b.get(n.shift())] = !0;
40
40
  let i = n.join("");
41
41
  if (!i || !r.Meta && !r.Control && !r.Alt && [...i].length === 1) return !1;
42
42
  let a = r.Meta ? e.metaKey || e.ctrlKey : !e.metaKey, o = r.Control ? e.ctrlKey : r.Meta || !e.ctrlKey;
43
- return a && o && e.altKey === r.Alt && e.shiftKey === r.Shift && e.key.toLowerCase() === x(i).toLowerCase();
43
+ return a && o && e.altKey === r.Alt && e.shiftKey === r.Shift && e.key.toLowerCase() === S(i).toLowerCase();
44
44
  }
45
- var w = class extends a(f) {
45
+ var T = class extends a(p) {
46
46
  static {
47
47
  this.styles = [
48
48
  s,
49
49
  o,
50
- p`
50
+ m`
51
51
  :host {
52
52
  display: block;
53
53
  cursor: var(--vf-cursor, default);
@@ -137,15 +137,46 @@ var w = class extends a(f) {
137
137
  .item.has-shortcut {
138
138
  padding-right: calc(var(--vf-scale, 1) * 1px);
139
139
  }
140
+ /* An item that opens a submenu ends in ▶ where a shortcut would go: 8px
141
+ in from the row's end, as in Menus.png's submenu rows and a System 7.5
142
+ capture alike, and 2 rows down the 16px row (centered, the odd row
143
+ below). The 8px before it is the label↔▶ minimum, as the shortcut's
144
+ is, and only binds in the row that sets the panel's width. */
145
+ .item.has-submenu {
146
+ padding-right: calc(var(--vf-scale, 1) * 8px);
147
+ }
148
+ .arrow {
149
+ flex: none;
150
+ align-self: flex-start;
151
+ display: block;
152
+ margin: calc(var(--vf-scale, 1) * 2px) 0 0 calc(var(--vf-scale, 1) * 8px);
153
+ }
154
+ .arrow svg {
155
+ display: block;
156
+ width: calc(var(--vf-scale, 1) * 6px);
157
+ height: calc(var(--vf-scale, 1) * 11px);
158
+ }
159
+ /* What the submenu hangs from: a box as wide as the row at the row's
160
+ top, which the submenu's panel places itself against (vf-menu). Out
161
+ of flow, so the submenu never adds to its parent panel's width. */
162
+ .submenu {
163
+ position: absolute;
164
+ top: 0;
165
+ left: 0;
166
+ right: 0;
167
+ }
140
168
  :host([disabled]) .item {
141
169
  color: var(--vf-disabled, #c0c0c0);
142
170
  }
143
- /* Highlight = full-width inversion (hover, the press-drag's [active],
144
- keyboard focus, blink "on"). blink-on is scoped to the enabled host
145
- like its siblings: an item disabled mid-blink has its timer cancelled
146
- (see updated()), and this keeps it from flashing even for the frame
147
- before that lands. */
148
- :host(:not([disabled])) .item:hover,
171
+ /* Highlight = full-width inversion (the menu's [active], keyboard
172
+ focus, blink "on"; hover only for a row outside a menu). Inside a
173
+ menu the pointer's row is the menu's to say, since the item whose
174
+ submenu is open stays lit while the pointer is in the submenu, and
175
+ :hover would light a second row. blink-on is scoped to the enabled
176
+ host like its siblings: an item disabled mid-blink has its timer
177
+ cancelled (see updated()), and this keeps it from flashing even for
178
+ the frame before that lands. */
179
+ :host(:not([disabled])) .item:not(.in-menu):hover,
149
180
  :host(:not([disabled])[active]) .item,
150
181
  :host(:not([disabled]):focus) .item,
151
182
  :host(:not([disabled])) .item.blink-on {
@@ -173,84 +204,117 @@ var w = class extends a(f) {
173
204
  `
174
205
  ];
175
206
  }
207
+ get submenu() {
208
+ for (let e of this.children) if (e.localName === "vf-menu" && e.slot === "submenu") return e;
209
+ return null;
210
+ }
211
+ get labelText() {
212
+ let e = "";
213
+ for (let t of this.childNodes) t instanceof Element ? t.slot || (e += t.textContent ?? "") : t.nodeType === Node.TEXT_NODE && (e += t.textContent ?? "");
214
+ return e.trim();
215
+ }
176
216
  #e;
177
217
  #t;
178
218
  #n;
179
219
  #r;
180
220
  #i;
181
221
  constructor() {
182
- super(), this.scale = new r(this), this.disabled = !1, this.checked = !1, this.active = !1, this.checkable = !1, this.shortcut = "", this._blinkPhase = null, this.#t = !1, this.#n = !1, this.#r = this.attachInternals(), this.#i = !1, this.#d = (e) => {
183
- e.defaultPrevented || e.isComposing || this.disabled || !this.shortcut || this.closest("vf-menu[shortcuts], vf-menu-bar[shortcuts]") && C(e, this.shortcut) && (e.preventDefault(), !(e.repeat || this.#t) && (t(this, "vf-menu-flash-request", { item: this }, { composed: !1 }), this.activate()));
184
- }, this.addEventListener("keydown", this.#u), this.addEventListener("pointerdown", this.#c), this.addEventListener("click", this.#l);
222
+ super(), this.scale = new r(this), this.disabled = !1, this.checked = !1, this.active = !1, this.expanded = !1, this.checkable = !1, this.shortcut = "", this._blinkPhase = null, this._hasSubmenu = !1, this.#t = !1, this.#n = !1, this.#r = this.attachInternals(), this.#i = !1, this.#s = () => {
223
+ let e = this._hasSubmenu;
224
+ this.#r.ariaHasPopup = e ? "menu" : null, this.#r.ariaExpanded = e ? String(this.expanded) : null, this.#r.ariaLabel = e && this.labelText || null;
225
+ }, this.#c = () => {
226
+ this._hasSubmenu = this.submenu !== null;
227
+ }, this.#m = (e) => {
228
+ e.defaultPrevented || e.isComposing || this.disabled || !this.shortcut || this._hasSubmenu || this.closest("vf-menu[shortcuts], vf-menu-bar[shortcuts]") && w(e, this.shortcut) && (e.preventDefault(), !(e.repeat || this.#t) && (t(this, "vf-menu-flash-request", { item: this }, { composed: !1 }), this.activate()));
229
+ }, this.addEventListener("keydown", this.#p), this.addEventListener("pointerdown", this.#d), this.addEventListener("click", this.#f);
185
230
  }
186
231
  get #a() {
187
232
  return this.checkable || this.#n;
188
233
  }
189
234
  connectedCallback() {
190
- super.connectedCallback(), this.#s(), this.#o(), this.hasAttribute("tabindex") || (this.tabIndex = -1), document.addEventListener("keydown", this.#d);
235
+ super.connectedCallback(), this._hasSubmenu = this.submenu !== null, this.#u(), this.#o(), this.#s(), this.requestUpdate(), this.hasAttribute("tabindex") || (this.tabIndex = -1), document.addEventListener("keydown", this.#m);
191
236
  }
192
237
  disconnectedCallback() {
193
- super.disconnectedCallback(), document.removeEventListener("keydown", this.#d), this.#f();
238
+ super.disconnectedCallback(), document.removeEventListener("keydown", this.#m), this.#h();
194
239
  }
195
240
  updated(e) {
196
- e.has("disabled") && (this.#r.ariaDisabled = this.disabled ? "true" : null, this.disabled && this.#f()), e.has("checked") && this.checked && (this.#n = !0), (e.has("checked") || e.has("checkable")) && this.#s(), e.has("shortcut") && this.#o();
241
+ e.has("disabled") && (this.#r.ariaDisabled = this.disabled ? "true" : null, this.disabled && this.#h()), e.has("checked") && this.checked && (this.#n = !0), (e.has("checked") || e.has("checkable")) && this.#u(), (e.has("shortcut") || e.has("_hasSubmenu")) && this.#o(), (e.has("expanded") || e.has("_hasSubmenu")) && this.#s();
197
242
  }
198
243
  #o() {
199
- this.#r.ariaKeyShortcuts = S(this.shortcut) || null;
244
+ this.#r.ariaKeyShortcuts = !this._hasSubmenu && C(this.shortcut) || null;
245
+ }
246
+ #s;
247
+ #c;
248
+ #l(e) {
249
+ let t = this.submenu;
250
+ return t !== null && e.composedPath().includes(t);
200
251
  }
201
- #s() {
252
+ #u() {
202
253
  this.#a ? (this.#r.role = "menuitemcheckbox", this.#r.ariaChecked = this.checked ? "true" : "false") : (this.#r.role = "menuitem", this.#r.ariaChecked = null);
203
254
  }
204
255
  render() {
205
- let e = {
256
+ let e = this._hasSubmenu, t = {
206
257
  item: !0,
207
- "has-shortcut": this.shortcut !== "",
258
+ "in-menu": this.closest("vf-menu") !== null,
259
+ "has-shortcut": !e && this.shortcut !== "",
260
+ "has-submenu": e,
208
261
  "blink-on": this._blinkPhase === "on",
209
262
  "blink-off": this._blinkPhase === "off"
210
263
  };
211
- return m`
212
- <div class=${v(e)} part="item">
213
- ${this.checked ? m`<span class="check" part="check" aria-hidden="true"
214
- >${d(u, "checkmark")}</span
215
- >` : h}
216
- <span class="label" part="label"><slot></slot></span>
217
- ${this.shortcut ? m`<span class="shortcut" part="shortcut" aria-hidden="true"
218
- >${this.shortcut}</span
219
- >` : h}
264
+ return h`
265
+ <div class=${y(t)} part="item">
266
+ ${this.checked ? h`<span class="check" part="check" aria-hidden="true"
267
+ >${f(d, "checkmark")}</span
268
+ >` : g}
269
+ <span class="label" part="label"><slot @slotchange=${this.#s}></slot></span>
270
+ ${e ? h`<span class="arrow" part="arrow" aria-hidden="true"
271
+ >${f(u, "caret")}</span
272
+ >` : this.shortcut ? h`<span class="shortcut" part="shortcut" aria-hidden="true"
273
+ >${this.shortcut}</span
274
+ >` : g}
275
+ <div class="submenu">
276
+ <slot name="submenu" @slotchange=${this.#c}></slot>
277
+ </div>
220
278
  </div>
221
279
  `;
222
280
  }
223
- #c() {
224
- this.#i = this.closest("vf-menu") !== null, this.#i && c(() => {
281
+ #d(e) {
282
+ this.#l(e) || (this.#i = this.closest("vf-menu") !== null, this.#i && c(() => {
225
283
  this.#i = !1;
226
- });
284
+ }));
227
285
  }
228
- #l(t) {
229
- if (this.#i) {
230
- this.#i = !1;
231
- return;
286
+ #f(t) {
287
+ if (!this.#l(t)) {
288
+ if (this.#i) {
289
+ this.#i = !1;
290
+ return;
291
+ }
292
+ e(this, t, () => this.activate());
232
293
  }
233
- e(this, t, () => this.activate());
234
294
  }
235
- #u(e) {
236
- (e.key === "Enter" || e.key === " ") && (e.preventDefault(), e.stopPropagation(), this.activate());
295
+ #p(e) {
296
+ this.#l(e) || (e.key === "Enter" || e.key === " ") && (e.preventDefault(), e.stopPropagation(), this.activate());
237
297
  }
238
- #d;
298
+ #m;
239
299
  activate() {
240
300
  if (this.disabled || this.#t) return;
301
+ if (this._hasSubmenu) {
302
+ this.closest("vf-menu")?.expand(this, { focus: !0 });
303
+ return;
304
+ }
241
305
  this.#t = !0;
242
306
  let e = l((e) => {
243
307
  this._blinkPhase = e ? "on" : "off";
244
308
  }, () => {
245
- this._blinkPhase = null, this.#t = !1, this.#e = void 0, this.#p();
309
+ this._blinkPhase = null, this.#t = !1, this.#e = void 0, this.#g();
246
310
  });
247
311
  this.#t && (this.#e = e);
248
312
  }
249
- #f() {
313
+ #h() {
250
314
  this.#t && (this.#e?.cancel(), this.#e = void 0, this.#t = !1, this._blinkPhase = null);
251
315
  }
252
- #p() {
253
- let e = this.value ?? (this.textContent ?? "").trim();
316
+ #g() {
317
+ let e = this.value ?? this.labelText;
254
318
  t(this, "vf-menu-select", {
255
319
  value: e,
256
320
  item: this
@@ -259,18 +323,21 @@ var w = class extends a(f) {
259
323
  (!n || n.hasAttribute("open")) && t(this, "vf-menu-close-request", { item: this }, { composed: !1 });
260
324
  }
261
325
  };
262
- i([g({
326
+ i([_({
327
+ type: Boolean,
328
+ reflect: !0
329
+ })], T.prototype, "disabled", void 0), i([_({
263
330
  type: Boolean,
264
331
  reflect: !0
265
- })], w.prototype, "disabled", void 0), i([g({
332
+ })], T.prototype, "checked", void 0), i([_({
266
333
  type: Boolean,
267
334
  reflect: !0
268
- })], w.prototype, "checked", void 0), i([g({
335
+ })], T.prototype, "active", void 0), i([_({
269
336
  type: Boolean,
270
337
  reflect: !0
271
- })], w.prototype, "active", void 0), i([g({
338
+ })], T.prototype, "expanded", void 0), i([_({
272
339
  type: Boolean,
273
340
  reflect: !0
274
- })], w.prototype, "checkable", void 0), i([g()], w.prototype, "shortcut", void 0), i([g()], w.prototype, "value", void 0), i([_()], w.prototype, "_blinkPhase", void 0), w = i([n("vf-menu-item")], w);
341
+ })], T.prototype, "checkable", void 0), i([_()], T.prototype, "shortcut", void 0), i([_()], T.prototype, "value", void 0), i([v()], T.prototype, "_blinkPhase", void 0), i([v()], T.prototype, "_hasSubmenu", void 0), T = i([n("vf-menu-item")], T);
275
342
  //#endregion
276
- export { w as VfMenuItem };
343
+ export { T as VfMenuItem };
@@ -17,6 +17,16 @@ declare const VfMenu_base: (new (...args: any[]) => import("../position.js").VfP
17
17
  * onto a command, release over it) and a modern quick tap that leaves the menu
18
18
  * dropped for a second click.
19
19
  *
20
+ * A `vf-menu` in a `vf-menu-item`'s `submenu` slot is a **submenu**: no title,
21
+ * only its panel, opened beside its item by the menu holding the item
22
+ * ({@link expand}). The pointer resting on the item opens it in either style,
23
+ * and so do a click or tap on the item, →, Enter and Space. It overlaps its
24
+ * parent as System 7.5's did: its first row level with its item, its frame
25
+ * 5px inside the parent's right edge, covering the parent's frame and
26
+ * shadow. It opens on the side its parent opened toward, the right first,
27
+ * flips where that side has no room, and moves up to fit the screen. Menus
28
+ * nest to any depth.
29
+ *
20
30
  * Takes `top`/`left` like every other element ({@link VfPositioned}) — the
21
31
  * natural fit being a standalone menu, which is a free-standing menu button and
22
32
  * places like any other control. The panel is anchored to the host's own box
@@ -95,6 +105,31 @@ export declare class VfMenu extends VfMenu_base {
95
105
  * src/menu-press.ts), so it wants the box, not the element.
96
106
  */
97
107
  get labelRect(): DOMRect | null;
108
+ private _panelEl;
109
+ /**
110
+ * Viewport rect of the dropped panel, or `null` while closed. The press
111
+ * gesture hit-tests the open menus by their panels, deepest first.
112
+ */
113
+ get panelRect(): DOMRect | null;
114
+ /** The item in this menu whose submenu is open, or `null`. */
115
+ get expandedItem(): VfMenuItem | null;
116
+ /**
117
+ * Opens `item`'s submenu, closing the one open in this menu and everything
118
+ * under it; `null` closes it. With `focus`, keyboard focus goes on to the
119
+ * submenu's first enabled item. The press gesture, the keyboard and an
120
+ * item's `activate()` all open submenus through here. Does nothing while
121
+ * this menu is closed, or for a disabled item or one without a submenu.
122
+ */
123
+ expand(item: VfMenuItem | null, options?: {
124
+ focus?: boolean;
125
+ }): void;
126
+ /**
127
+ * Where the pointer is, from the press gesture: on `row` of this menu's
128
+ * panel, on none of its rows (`null`), or not in its panel (`undefined`).
129
+ * The row under the pointer is highlighted; with the pointer elsewhere, the
130
+ * item whose submenu is open is. Not part of the authoring API.
131
+ */
132
+ pointerOver(row: VfMenuItem | null | undefined): void;
98
133
  /** `true` while the key-equivalent flash paints the title inverted. */
99
134
  private _flashOn;
100
135
  connectedCallback(): void;