vintage-frames 0.19.0 → 0.20.0

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.
Files changed (68) hide show
  1. package/README.md +1 -18
  2. package/custom-elements.json +247 -59
  3. package/dist/components/vf-menu-bar.d.ts +6 -4
  4. package/dist/components/vf-menu-bar.js +26 -56
  5. package/dist/components/vf-menu-item.d.ts +33 -5
  6. package/dist/components/vf-menu-item.js +128 -61
  7. package/dist/components/vf-menu.d.ts +35 -0
  8. package/dist/components/vf-menu.js +171 -111
  9. package/dist/glyphs.d.ts +6 -0
  10. package/dist/glyphs.js +18 -14
  11. package/dist/menu-navigation.d.ts +34 -0
  12. package/dist/menu-navigation.js +53 -0
  13. package/dist/menu-press.d.ts +25 -7
  14. package/dist/menu-press.js +78 -44
  15. package/dist/motion.d.ts +10 -0
  16. package/dist/text-control.d.ts +3 -3
  17. package/docs/SPEC.md +13 -9
  18. package/editor/vscode.html-custom-data.json +13 -8
  19. package/editor/web-types.json +51 -15
  20. package/package.json +4 -34
  21. package/dist/build/app-file.d.ts +0 -21
  22. package/dist/build/app-file.js +0 -42
  23. package/dist/build/box.d.ts +0 -57
  24. package/dist/build/box.js +0 -93
  25. package/dist/build/client.d.ts +0 -12
  26. package/dist/build/frame-picture.d.ts +0 -6
  27. package/dist/build/frame-picture.js +0 -4
  28. package/dist/build/frame.d.ts +0 -11
  29. package/dist/build/frame.js +0 -44
  30. package/dist/build/index.d.ts +0 -14
  31. package/dist/build/index.js +0 -4
  32. package/dist/build/plugins.d.ts +0 -41
  33. package/dist/build/plugins.js +0 -134
  34. package/dist/build/png.d.ts +0 -24
  35. package/dist/build/png.js +0 -116
  36. package/dist/shell/alert.d.ts +0 -40
  37. package/dist/shell/alert.js +0 -41
  38. package/dist/shell/app-file.d.ts +0 -115
  39. package/dist/shell/app-file.js +0 -189
  40. package/dist/shell/catalog.d.ts +0 -212
  41. package/dist/shell/catalog.js +0 -317
  42. package/dist/shell/clock.d.ts +0 -16
  43. package/dist/shell/clock.js +0 -36
  44. package/dist/shell/filing.d.ts +0 -39
  45. package/dist/shell/filing.js +0 -68
  46. package/dist/shell/finder.d.ts +0 -179
  47. package/dist/shell/finder.js +0 -662
  48. package/dist/shell/geometry.d.ts +0 -203
  49. package/dist/shell/geometry.js +0 -257
  50. package/dist/shell/index.d.ts +0 -30
  51. package/dist/shell/index.js +0 -12
  52. package/dist/shell/pure.d.ts +0 -13
  53. package/dist/shell/pure.js +0 -6
  54. package/dist/shell/shell.d.ts +0 -177
  55. package/dist/shell/shell.js +0 -272
  56. package/dist/shell/state.d.ts +0 -88
  57. package/dist/shell/state.js +0 -103
  58. package/dist/shell/storage.d.ts +0 -14
  59. package/dist/shell/storage.js +0 -41
  60. package/dist/shell/version.d.ts +0 -2
  61. package/dist/shell/version.js +0 -4
  62. package/dist/shell/windows.d.ts +0 -278
  63. package/dist/shell/windows.js +0 -447
  64. package/dist/styles/body-strike.d.ts +0 -12
  65. package/dist/styles/body-strike.js +0 -3961
  66. package/dist/styles/display-strike.d.ts +0 -12
  67. package/dist/styles/display-strike.js +0 -4343
  68. package/dist/styles/strike.d.ts +0 -30
@@ -8,11 +8,12 @@ import { GridSnapController as o } from "../grid-snap.js";
8
8
  import { DocumentListenersController as s } from "../document-listeners.js";
9
9
  import { SCREEN_CORNER as c, steppedCornerClip as l } from "../pixel-frame.js";
10
10
  import { TypeAheadBuffer as u } from "../type-ahead.js";
11
- import { MenuPressController as d } from "../menu-press.js";
12
- import { LitElement as f, css as p, html as m, unsafeCSS as h } from "lit";
13
- import { property as g, queryAssignedElements as _, state as v } from "lit/decorators.js";
11
+ import { navigateMenus as d } from "../menu-navigation.js";
12
+ import { MenuPressController as f } from "../menu-press.js";
13
+ import { LitElement as p, css as m, html as h, unsafeCSS as g } from "lit";
14
+ import { property as _, queryAssignedElements as v, state as y } from "lit/decorators.js";
14
15
  //#region src/components/vf-menu-bar.ts
15
- var y = class extends r(f) {
16
+ var b = class extends r(p) {
16
17
  constructor(...e) {
17
18
  super(...e), this.scale = new t(this), this.gridSnap = new o(this), this.label = "", this.rounded = !1, this.shortcuts = !1, this._hasEnd = !1, this.#e = null, this.#t = this.attachInternals(), this.#n = new u(), this.#r = null, this.#i = new s(this, () => [[
18
19
  document,
@@ -24,7 +25,7 @@ var y = class extends r(f) {
24
25
  "keydown",
25
26
  this.#_,
26
27
  !0
27
- ]]), this.#a = new d(this, {
28
+ ]]), this.#a = new f(this, {
28
29
  menus: () => this._menus,
29
30
  open: (e) => this.#m(e),
30
31
  close: () => this.#h()
@@ -43,39 +44,15 @@ var y = class extends r(f) {
43
44
  }, this.#g = (e) => {
44
45
  e.composedPath().includes(this) || this.#h();
45
46
  }, this.#_ = (e) => {
46
- if (!(e.defaultPrevented || !this.#e)) switch (e.key) {
47
- case "Escape":
48
- e.preventDefault(), this.#e.focus(), this.#h();
49
- break;
50
- case "Tab":
51
- this.#h();
52
- break;
53
- case "ArrowLeft":
54
- case "ArrowRight":
55
- e.preventDefault(), this.#y(e.key === "ArrowRight" ? 1 : -1);
56
- break;
57
- case "ArrowDown":
58
- case "ArrowUp":
59
- e.preventDefault(), this.#b(e.key === "ArrowDown" ? 1 : -1);
60
- break;
61
- case "Home":
62
- case "End": {
63
- e.preventDefault();
64
- let t = this.#e.items;
65
- t[e.key === "Home" ? 0 : t.length - 1]?.focus();
66
- break;
67
- }
68
- default: {
69
- if (e.key.length !== 1 || e.key === " " || e.metaKey || e.ctrlKey || e.altKey) break;
70
- e.preventDefault();
71
- let t = this.#e.items, n = t.indexOf(document.activeElement);
72
- t[this.#n.feed(e.key, n, t.map((e) => ({
73
- text: e.textContent ?? "",
74
- disabled: !1
75
- })))]?.focus();
76
- break;
77
- }
78
- }
47
+ let t = this.#e;
48
+ t && d(e, {
49
+ menu: t,
50
+ typeAhead: this.#n,
51
+ close: (e) => {
52
+ e && t.focus(), this.#h();
53
+ },
54
+ switchMenu: (e) => this.#y(e)
55
+ });
79
56
  }, this.#v = (e) => {
80
57
  if (this.#e) return;
81
58
  let t = this._menus;
@@ -105,7 +82,7 @@ var y = class extends r(f) {
105
82
  this.styles = [
106
83
  i,
107
84
  a,
108
- p`
85
+ m`
109
86
  :host {
110
87
  display: block;
111
88
  /* 19 white system px over the 1px black rule — Menus.png's bar strip
@@ -177,11 +154,11 @@ var y = class extends r(f) {
177
154
  }
178
155
  :host([rounded]) .bar::before {
179
156
  left: 0;
180
- clip-path: ${h(l(c, "left"))};
157
+ clip-path: ${g(l(c, "left"))};
181
158
  }
182
159
  :host([rounded]) .bar::after {
183
160
  right: 0;
184
- clip-path: ${h(l(c, "right"))};
161
+ clip-path: ${g(l(c, "right"))};
185
162
  }
186
163
  `
187
164
  ];
@@ -206,7 +183,7 @@ var y = class extends r(f) {
206
183
  e.has("label") && (this.#t.ariaLabel = this.label || null);
207
184
  }
208
185
  render() {
209
- return m`
186
+ return h`
210
187
  <div class="bar vf-snap vf-rule-bottom" part="bar" role="presentation">
211
188
  <slot @slotchange=${this.#c}></slot>
212
189
  <slot
@@ -235,11 +212,11 @@ var y = class extends r(f) {
235
212
  #p;
236
213
  #m(e) {
237
214
  for (let t of this._menus) t !== e && (t.open = !1);
238
- e.open = !0, this.#e = e, this.#r = e, this.#n.reset(), this.#l(), this.#i.attach();
215
+ e.open = !0, this.#e = e, this.#r = e, this.#n.reset(), this.#l(), this.#i.attach(), this.#a.track(!0);
239
216
  }
240
217
  #h() {
241
218
  for (let e of this._menus) e.open = !1;
242
- this.#e = null, this.#n.reset(), this.#i.detach(), this.#l();
219
+ this.#e = null, this.#n.reset(), this.#i.detach(), this.#a.track(!1), this.#l();
243
220
  }
244
221
  #g;
245
222
  #_;
@@ -250,23 +227,16 @@ var y = class extends r(f) {
250
227
  let n = t[(t.indexOf(this.#e) + e + t.length) % t.length];
251
228
  n && n !== this.#e && (this.#m(n), n.focus());
252
229
  }
253
- #b(e) {
254
- if (!this.#e) return;
255
- let t = this.#e.items;
256
- if (t.length === 0) return;
257
- let n = t.indexOf(document.activeElement), r;
258
- r = n < 0 ? e === 1 ? 0 : t.length - 1 : (n + e + t.length) % t.length, t[r]?.focus();
259
- }
260
230
  };
261
- n([g()], y.prototype, "label", void 0), n([g({
231
+ n([_()], b.prototype, "label", void 0), n([_({
262
232
  type: Boolean,
263
233
  reflect: !0
264
- })], y.prototype, "rounded", void 0), n([g({
234
+ })], b.prototype, "rounded", void 0), n([_({
265
235
  type: Boolean,
266
236
  reflect: !0
267
- })], y.prototype, "shortcuts", void 0), n([_({
237
+ })], b.prototype, "shortcuts", void 0), n([v({
268
238
  selector: "vf-menu",
269
239
  flatten: !0
270
- })], y.prototype, "_menus", void 0), n([v()], y.prototype, "_hasEnd", void 0), y = n([e("vf-menu-bar")], y);
240
+ })], b.prototype, "_menus", void 0), n([y()], b.prototype, "_hasEnd", void 0), b = n([e("vf-menu-bar")], b);
271
241
  //#endregion
272
- export { y as VfMenuBar };
242
+ export { b as VfMenuBar };
@@ -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;