vintage-frames 0.1.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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/custom-elements.json +19807 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
  5. package/dist/chrome.d.ts +73 -0
  6. package/dist/chrome.js +94 -0
  7. package/dist/components/vf-button-group.d.ts +50 -0
  8. package/dist/components/vf-button-group.js +68 -0
  9. package/dist/components/vf-button.d.ts +169 -0
  10. package/dist/components/vf-button.js +234 -0
  11. package/dist/components/vf-checkbox.d.ts +60 -0
  12. package/dist/components/vf-checkbox.js +135 -0
  13. package/dist/components/vf-container.d.ts +95 -0
  14. package/dist/components/vf-container.js +86 -0
  15. package/dist/components/vf-desktop.d.ts +243 -0
  16. package/dist/components/vf-desktop.js +310 -0
  17. package/dist/components/vf-dialog.d.ts +105 -0
  18. package/dist/components/vf-dialog.js +280 -0
  19. package/dist/components/vf-fieldset.d.ts +34 -0
  20. package/dist/components/vf-fieldset.js +76 -0
  21. package/dist/components/vf-grid.d.ts +124 -0
  22. package/dist/components/vf-grid.js +133 -0
  23. package/dist/components/vf-icon.d.ts +356 -0
  24. package/dist/components/vf-icon.js +507 -0
  25. package/dist/components/vf-img.d.ts +63 -0
  26. package/dist/components/vf-img.js +65 -0
  27. package/dist/components/vf-label.d.ts +117 -0
  28. package/dist/components/vf-label.js +151 -0
  29. package/dist/components/vf-list-item.d.ts +48 -0
  30. package/dist/components/vf-list-item.js +98 -0
  31. package/dist/components/vf-list.d.ts +77 -0
  32. package/dist/components/vf-list.js +256 -0
  33. package/dist/components/vf-menu-bar.d.ts +64 -0
  34. package/dist/components/vf-menu-bar.js +236 -0
  35. package/dist/components/vf-menu-item.d.ts +91 -0
  36. package/dist/components/vf-menu-item.js +252 -0
  37. package/dist/components/vf-menu.d.ts +95 -0
  38. package/dist/components/vf-menu.js +346 -0
  39. package/dist/components/vf-number-field.d.ts +60 -0
  40. package/dist/components/vf-number-field.js +248 -0
  41. package/dist/components/vf-option.d.ts +49 -0
  42. package/dist/components/vf-option.js +119 -0
  43. package/dist/components/vf-paragraph.d.ts +73 -0
  44. package/dist/components/vf-paragraph.js +66 -0
  45. package/dist/components/vf-progress-bar.d.ts +69 -0
  46. package/dist/components/vf-progress-bar.js +272 -0
  47. package/dist/components/vf-radio-group.d.ts +109 -0
  48. package/dist/components/vf-radio-group.js +105 -0
  49. package/dist/components/vf-radio.d.ts +59 -0
  50. package/dist/components/vf-radio.js +135 -0
  51. package/dist/components/vf-scroll-area.d.ts +75 -0
  52. package/dist/components/vf-scroll-area.js +124 -0
  53. package/dist/components/vf-select.d.ts +334 -0
  54. package/dist/components/vf-select.js +658 -0
  55. package/dist/components/vf-separator.d.ts +38 -0
  56. package/dist/components/vf-separator.js +55 -0
  57. package/dist/components/vf-slider.d.ts +80 -0
  58. package/dist/components/vf-slider.js +255 -0
  59. package/dist/components/vf-stack.d.ts +188 -0
  60. package/dist/components/vf-stack.js +168 -0
  61. package/dist/components/vf-swatch.d.ts +98 -0
  62. package/dist/components/vf-swatch.js +201 -0
  63. package/dist/components/vf-text-area.d.ts +77 -0
  64. package/dist/components/vf-text-area.js +121 -0
  65. package/dist/components/vf-text-field.d.ts +53 -0
  66. package/dist/components/vf-text-field.js +79 -0
  67. package/dist/components/vf-window.d.ts +157 -0
  68. package/dist/components/vf-window.js +377 -0
  69. package/dist/cursor-art.d.ts +37 -0
  70. package/dist/cursor-art.js +41 -0
  71. package/dist/cursor.d.ts +49 -0
  72. package/dist/cursor.js +171 -0
  73. package/dist/define.d.ts +45 -0
  74. package/dist/define.js +15 -0
  75. package/dist/document-listeners.d.ts +62 -0
  76. package/dist/document-listeners.js +29 -0
  77. package/dist/drag.d.ts +49 -0
  78. package/dist/drag.js +30 -0
  79. package/dist/events.d.ts +79 -0
  80. package/dist/events.js +25 -0
  81. package/dist/focus-modality.d.ts +94 -0
  82. package/dist/focus-modality.js +56 -0
  83. package/dist/form-control.d.ts +247 -0
  84. package/dist/form-control.js +127 -0
  85. package/dist/glyphs.d.ts +117 -0
  86. package/dist/glyphs.js +100 -0
  87. package/dist/grid-snap.d.ts +73 -0
  88. package/dist/grid-snap.js +135 -0
  89. package/dist/index.d.ts +145 -0
  90. package/dist/index.js +71 -0
  91. package/dist/menu-press.d.ts +60 -0
  92. package/dist/menu-press.js +97 -0
  93. package/dist/modal-dialog.d.ts +148 -0
  94. package/dist/modal-dialog.js +155 -0
  95. package/dist/motion.d.ts +72 -0
  96. package/dist/motion.js +22 -0
  97. package/dist/number.d.ts +24 -0
  98. package/dist/number.js +12 -0
  99. package/dist/open-art.d.ts +40 -0
  100. package/dist/open-art.js +35 -0
  101. package/dist/pixel-frame.d.ts +102 -0
  102. package/dist/pixel-frame.js +77 -0
  103. package/dist/popup-overflow.d.ts +157 -0
  104. package/dist/popup-overflow.js +45 -0
  105. package/dist/position.d.ts +187 -0
  106. package/dist/position.js +74 -0
  107. package/dist/scale.d.ts +259 -0
  108. package/dist/scale.js +133 -0
  109. package/dist/scroll-rail.d.ts +112 -0
  110. package/dist/scroll-rail.js +194 -0
  111. package/dist/scroll-state.d.ts +113 -0
  112. package/dist/scroll-state.js +55 -0
  113. package/dist/size.d.ts +47 -0
  114. package/dist/size.js +30 -0
  115. package/dist/styles/base.d.ts +38 -0
  116. package/dist/styles/body-font.d.ts +7 -0
  117. package/dist/styles/body-font.js +9 -0
  118. package/dist/styles/display-font.d.ts +7 -0
  119. package/dist/styles/display-font.js +9 -0
  120. package/dist/styles/recipes/body-face.d.ts +14 -0
  121. package/dist/styles/recipes/body-face.js +6 -0
  122. package/dist/styles/recipes/display-face.d.ts +23 -0
  123. package/dist/styles/recipes/display-face.js +10 -0
  124. package/dist/styles/recipes/field.d.ts +20 -0
  125. package/dist/styles/recipes/field.js +68 -0
  126. package/dist/styles/recipes/focus.d.ts +76 -0
  127. package/dist/styles/recipes/focus.js +39 -0
  128. package/dist/styles/recipes/host.d.ts +5 -0
  129. package/dist/styles/recipes/host.js +62 -0
  130. package/dist/styles/recipes/pattern.d.ts +80 -0
  131. package/dist/styles/recipes/pattern.js +82 -0
  132. package/dist/styles/recipes/scroll-rail.d.ts +69 -0
  133. package/dist/styles/recipes/scroll-rail.js +222 -0
  134. package/dist/styles/recipes/shadow.d.ts +13 -0
  135. package/dist/styles/recipes/shadow.js +5 -0
  136. package/dist/styles/recipes/static-text.d.ts +24 -0
  137. package/dist/styles/recipes/static-text.js +17 -0
  138. package/dist/styles/recipes/surface.d.ts +28 -0
  139. package/dist/styles/recipes/surface.js +27 -0
  140. package/dist/styles/recipes/tile.d.ts +122 -0
  141. package/dist/styles/recipes/tile.js +40 -0
  142. package/dist/styles/recipes/title-bar.d.ts +49 -0
  143. package/dist/styles/recipes/title-bar.js +134 -0
  144. package/dist/styles/recipes/toggle.d.ts +10 -0
  145. package/dist/styles/recipes/toggle.js +20 -0
  146. package/dist/styles/register-embedded-font.d.ts +64 -0
  147. package/dist/styles/register-embedded-font.js +30 -0
  148. package/dist/text-control.d.ts +131 -0
  149. package/dist/text-control.js +83 -0
  150. package/dist/tile-grid.d.ts +111 -0
  151. package/dist/tile-grid.js +53 -0
  152. package/dist/toggle-control.d.ts +60 -0
  153. package/dist/toggle-control.js +41 -0
  154. package/dist/track-width.d.ts +42 -0
  155. package/dist/track-width.js +30 -0
  156. package/dist/type-ahead.d.ts +35 -0
  157. package/dist/type-ahead.js +20 -0
  158. package/dist/zoom.d.ts +204 -0
  159. package/dist/zoom.js +105 -0
  160. package/docs/SPEC.md +2471 -0
  161. package/editor/vscode.html-custom-data.json +1320 -0
  162. package/editor/web-types.json +3237 -0
  163. package/package.json +118 -0
@@ -0,0 +1,252 @@
1
+ import { vfElement as e } from "../define.js";
2
+ import { ScaleController as t } from "../scale.js";
3
+ import n from "../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
4
+ import { vfDisplay as r } from "../styles/recipes/display-face.js";
5
+ import { vfBase as i } from "../styles/recipes/host.js";
6
+ import { releaseAfterGesture as a } from "../document-listeners.js";
7
+ import { deferActivation as o, emit as s } from "../events.js";
8
+ import { runSelectionBlink as c } from "../motion.js";
9
+ import { CHECKMARK as l, glyphSvg as u } from "../glyphs.js";
10
+ import { LitElement as d, css as f, html as p, nothing as m } from "lit";
11
+ import { property as h, state as g } from "lit/decorators.js";
12
+ import { classMap as _ } from "lit/directives/class-map.js";
13
+ //#region src/components/vf-menu-item.ts
14
+ var v = /* @__PURE__ */ new Map([
15
+ ["⌃", "Control"],
16
+ ["⌥", "Alt"],
17
+ ["⇧", "Shift"],
18
+ ["⌘", "Meta"]
19
+ ]);
20
+ function y(e) {
21
+ let t = [...e.trim()], n = [];
22
+ for (; t.length > 0 && v.has(t[0]);) n.push(v.get(t.shift()));
23
+ let r = t.join("");
24
+ return r ? [...n, r].join("+") : "";
25
+ }
26
+ var b = class extends d {
27
+ static {
28
+ this.styles = [
29
+ i,
30
+ r,
31
+ f`
32
+ :host {
33
+ display: block;
34
+ cursor: var(--vf-cursor, default);
35
+ outline: none;
36
+ }
37
+ .item {
38
+ display: flex;
39
+ align-items: center;
40
+ /* Menus.png puts every menu row on a 16px pitch: the New Folder / Open
41
+ / Print pulldown's nine inter-band gaps are all exact multiples of 16
42
+ (48, 32, 16, 16, 16, 48, 16, 32, 16 — the 32s and 48s are separator
43
+ groups). A row is 3px above + the 9px glyph + 4px below.
44
+
45
+ Its own token rather than a share of --vf-popup-height: the art has
46
+ pulldown and popup rows identical, but tying them would make
47
+ re-theming the popup *pill* move every pulldown row, which is
48
+ semantically wrong. --vf-control-height is scoped to fields (22). */
49
+ height: calc(var(--vf-scale, 1) * var(--vf-menu-row-height, 16px));
50
+ /* Lock the line box to the row box, as vf-option does. vfDisplay's 1.25
51
+ line-height resolves taller than a 16px row, and with align-items:
52
+ center the excess spills equally above and below — invisible per row,
53
+ but it overflowed the panel and summoned a scrollbar the last time a
54
+ row shrank under an inherited line-height. Derived from the same
55
+ expression as the height so a re-theme can't reintroduce the gap. */
56
+ line-height: calc(var(--vf-scale, 1) * var(--vf-menu-row-height, 16px));
57
+ /* Shares --vf-select-gutter with vf-select/vf-option: Menus.png puts a
58
+ pulldown's label ink at the same inset as a popup's, and both hold
59
+ the same 9px ✓. (The token name says "select" for historical
60
+ reasons; it is the checkmark column for every menu surface.) */
61
+ padding: 0 calc(var(--vf-scale, 1) * 12px) 0
62
+ calc(var(--vf-scale, 1) * var(--vf-select-gutter, 16px));
63
+ position: relative;
64
+ white-space: nowrap;
65
+ }
66
+ /* The inherited color deliberately lets a disabled row dim its ✓ along
67
+ with the label. Brief §1 says dimming greys the label only while chrome
68
+ glyphs stay black, but authentic System 7 greyed the whole disabled
69
+ row, check included — and reference behavior wins over the rule of
70
+ thumb. Documented deviation, not an oversight. */
71
+ /* 3px in from the row's left edge on both axes, matching Menus.png (✓ ink
72
+ at +4 from the panel border, i.e. 3 inside it) and vf-option's
73
+ identical rule. The 16px row less the 9px glyph leaves 7 — an odd
74
+ remainder, so it is biased 3 above / 4 below exactly as the art has it
75
+ rather than centred on 3.5 authored px (10.5 device px at the default
76
+ 3×), which would fringe at every scale. */
77
+ .check {
78
+ position: absolute;
79
+ left: calc(var(--vf-scale, 1) * 3px);
80
+ top: calc(var(--vf-scale, 1) * 3px);
81
+ display: block;
82
+ color: inherit;
83
+ }
84
+ /* Native 9×9 (1:1, crisp). */
85
+ .check svg {
86
+ display: block;
87
+ width: calc(var(--vf-scale, 1) * 9px);
88
+ height: calc(var(--vf-scale, 1) * 9px);
89
+ }
90
+ .label {
91
+ flex: 1;
92
+ }
93
+ /* The shortcut is LEFT-aligned ink in a right-anchored slot, the way the
94
+ MDEF drew it: in Menus.png's File pulldown every ⌘ starts at the same
95
+ x — 23px left of the right border — whatever the letter, with ⌘W
96
+ running to within 3px of the border. Flexing the label and letting the
97
+ span hug the right edge instead right-aligned the pairs, so the ⌘
98
+ column jagged with each letter's width. The slot is the classic
99
+ reserve: ⌘'s 11px advance + the face's widest letter (M/W, 12px) —
100
+ sized so ⌘W exactly fills it and no pair moves the anchor.
101
+ min-width, not width, so a longer shortcut ("⌘⇧S") widens its own
102
+ slot rather than painting past the row. The 8px margin is the
103
+ label↔shortcut minimum ("Close Window"'s label box ends at +108, the
104
+ slot starts at +116); it only binds in the row that sets the panel's
105
+ intrinsic width. */
106
+ .shortcut {
107
+ flex: none;
108
+ margin-left: calc(var(--vf-scale, 1) * 8px);
109
+ min-width: calc(
110
+ var(--vf-scale, 1) * var(--vf-menu-shortcut-column, 23px)
111
+ );
112
+ text-align: left;
113
+ }
114
+ /* A shortcut row trades the label's 12px right clearance for the art's
115
+ 1px — the slot itself is the clearance there (1 + 23 puts the slot
116
+ edge at −24, and ⌘'s 1px bearing lands its ink at −23, as traced).
117
+ Label-only rows keep the 12. */
118
+ .item.has-shortcut {
119
+ padding-right: calc(var(--vf-scale, 1) * 1px);
120
+ }
121
+ :host([disabled]) .item {
122
+ color: var(--vf-disabled, #c0c0c0);
123
+ }
124
+ /* Highlight = full-width inversion (hover, the press-drag's [active],
125
+ keyboard focus, blink "on"). blink-on is scoped to the enabled host
126
+ like its siblings: an item disabled mid-blink has its timer cancelled
127
+ (see updated()), and this keeps it from flashing even for the frame
128
+ before that lands. */
129
+ :host(:not([disabled])) .item:hover,
130
+ :host(:not([disabled])[active]) .item,
131
+ :host(:not([disabled]):focus) .item,
132
+ :host(:not([disabled])) .item.blink-on {
133
+ /* Forced colors: exempt the inverted row from the mode's text
134
+ backplate, which would land a Canvas slab on the highlight bar —
135
+ see vf-list-item's forced-colors note. The pair is already the
136
+ user's own via the vfBase token remap. */
137
+ @media (forced-colors: active) {
138
+ forced-color-adjust: none;
139
+ }
140
+ background: var(--vf-highlight, #000);
141
+ color: var(--vf-highlight-text, #fff);
142
+ }
143
+ /* While blinking, the timer — not the pointer — owns the inversion. Each
144
+ highlight source needs its own override at matching specificity; the
145
+ [active] one is what lets a drag-picked row keep its flag through the
146
+ blink, so the release reads as the highlight flashing off. */
147
+ :host(:not([disabled])) .item.blink-off:hover,
148
+ :host(:not([disabled])[active]) .item.blink-off,
149
+ :host(:not([disabled]):focus) .item.blink-off,
150
+ .item.blink-off {
151
+ background: transparent;
152
+ color: inherit;
153
+ }
154
+ `
155
+ ];
156
+ }
157
+ #e;
158
+ #t;
159
+ #n;
160
+ #r;
161
+ #i;
162
+ constructor() {
163
+ super(), this.scale = new t(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.addEventListener("keydown", this.#u), this.addEventListener("pointerdown", this.#c), this.addEventListener("click", this.#l);
164
+ }
165
+ get #a() {
166
+ return this.checkable || this.#n;
167
+ }
168
+ connectedCallback() {
169
+ super.connectedCallback(), this.#s(), this.#o(), this.hasAttribute("tabindex") || (this.tabIndex = -1);
170
+ }
171
+ disconnectedCallback() {
172
+ super.disconnectedCallback(), this.#d();
173
+ }
174
+ updated(e) {
175
+ e.has("disabled") && (this.#r.ariaDisabled = this.disabled ? "true" : null, this.disabled && this.#d()), e.has("checked") && this.checked && (this.#n = !0), (e.has("checked") || e.has("checkable")) && this.#s(), e.has("shortcut") && this.#o();
176
+ }
177
+ #o() {
178
+ this.#r.ariaKeyShortcuts = y(this.shortcut) || null;
179
+ }
180
+ #s() {
181
+ this.#a ? (this.#r.role = "menuitemcheckbox", this.#r.ariaChecked = this.checked ? "true" : "false") : (this.#r.role = "menuitem", this.#r.ariaChecked = null);
182
+ }
183
+ render() {
184
+ let e = {
185
+ item: !0,
186
+ "has-shortcut": this.shortcut !== "",
187
+ "blink-on": this._blinkPhase === "on",
188
+ "blink-off": this._blinkPhase === "off"
189
+ };
190
+ return p`
191
+ <div class=${_(e)} part="item">
192
+ ${this.checked ? p`<span class="check" part="check" aria-hidden="true"
193
+ >${u(l, "checkmark")}</span
194
+ >` : m}
195
+ <span class="label" part="label"><slot></slot></span>
196
+ ${this.shortcut ? p`<span class="shortcut" part="shortcut" aria-hidden="true"
197
+ >${this.shortcut}</span
198
+ >` : m}
199
+ </div>
200
+ `;
201
+ }
202
+ #c() {
203
+ this.#i = this.closest("vf-menu") !== null, this.#i && a(() => {
204
+ this.#i = !1;
205
+ });
206
+ }
207
+ #l(e) {
208
+ if (this.#i) {
209
+ this.#i = !1;
210
+ return;
211
+ }
212
+ o(this, e, () => this.activate());
213
+ }
214
+ #u(e) {
215
+ (e.key === "Enter" || e.key === " ") && (e.preventDefault(), e.stopPropagation(), this.activate());
216
+ }
217
+ activate() {
218
+ if (this.disabled || this.#t) return;
219
+ this.#t = !0;
220
+ let e = c((e) => {
221
+ this._blinkPhase = e ? "on" : "off";
222
+ }, () => {
223
+ this._blinkPhase = null, this.#t = !1, this.#e = void 0, this.#f();
224
+ });
225
+ this.#t && (this.#e = e);
226
+ }
227
+ #d() {
228
+ this.#t && (this.#e?.cancel(), this.#e = void 0, this.#t = !1, this._blinkPhase = null);
229
+ }
230
+ #f() {
231
+ let e = this.value ?? (this.textContent ?? "").trim();
232
+ s(this, "vf-menu-select", {
233
+ value: e,
234
+ item: this
235
+ }), s(this, "vf-menu-close-request", { item: this }, { composed: !1 });
236
+ }
237
+ };
238
+ n([h({
239
+ type: Boolean,
240
+ reflect: !0
241
+ })], b.prototype, "disabled", void 0), n([h({
242
+ type: Boolean,
243
+ reflect: !0
244
+ })], b.prototype, "checked", void 0), n([h({
245
+ type: Boolean,
246
+ reflect: !0
247
+ })], b.prototype, "active", void 0), n([h({
248
+ type: Boolean,
249
+ reflect: !0
250
+ })], b.prototype, "checkable", void 0), n([h()], b.prototype, "shortcut", void 0), n([h()], b.prototype, "value", void 0), n([g()], b.prototype, "_blinkPhase", void 0), b = n([e("vf-menu-item")], b);
251
+ //#endregion
252
+ export { b as VfMenuItem };
@@ -0,0 +1,95 @@
1
+ import { LitElement } from 'lit';
2
+ import type { VfMenuItem } from './vf-menu-item.js';
3
+ /**
4
+ * `<vf-menu>` — a pull-down menu: a bar label plus a dropped panel of
5
+ * `<vf-menu-item>` / `<vf-separator>` children.
6
+ *
7
+ * Inside a `<vf-menu-bar>` the bar coordinates open state (only one menu open,
8
+ * hover-switching, outside-click/Escape dismissal) and owns the pointer
9
+ * gesture, which may travel between its menus. Used standalone, the menu
10
+ * toggles itself on label click and manages its own dismissal, its own press
11
+ * gesture and item keyboard navigation (ArrowUp/ArrowDown, Home/End, and the
12
+ * shared first-letter type-ahead — src/type-ahead.ts) while open.
13
+ *
14
+ * Pointer — the two styles `vf-select` supports, on the same terms (see
15
+ * src/menu-press.ts): the System 7 press-drag-release (press the title, slide
16
+ * onto a command, release over it) and a modern quick tap that leaves the menu
17
+ * dropped for a second click.
18
+ *
19
+ * @slot - Menu contents: `vf-menu-item` and `vf-separator` elements.
20
+ * @slot label - Replaces the `label` text in the bar — e.g. a `vf-img` apple
21
+ * icon for the Apple menu. Keep the `label` attribute set too: it stays the
22
+ * menu's accessible name (the bar item's `aria-label` and the panel's) when
23
+ * the visible title is an image.
24
+ * @csspart label - The menu title in the bar (inverts while open).
25
+ * @csspart panel - The dropped `.vf-panel` containing the items.
26
+ * @cssprop [--vf-menubar-height=20px] - `vf-menu-bar`
27
+ * @cssprop [--vf-menu-row-height=16px] - `vf-menu-item` row pitch; the panel
28
+ * also spends one of these on every slotted `vf-separator`, the full row the
29
+ * MDEF gave a divider (rule 8px into it — see the panel CSS)
30
+ */
31
+ export declare class VfMenu extends LitElement {
32
+ #private;
33
+ static styles: import("lit").CSSResult[];
34
+ private readonly scale;
35
+ /** Device-pixel grid snapping (opt in with applyGridSnap()); see src/grid-snap.ts. */
36
+ private readonly gridSnap;
37
+ /**
38
+ * The menu title shown in the bar, and the menu's accessible name. Slotted
39
+ * `label` content replaces it visually (see the `label` slot) but this text
40
+ * keeps naming the menu for AT.
41
+ */
42
+ label: string;
43
+ /**
44
+ * Whether the panel is dropped. Reflected. Managed by the parent
45
+ * `vf-menu-bar` when present, otherwise by the menu itself.
46
+ */
47
+ open: boolean;
48
+ /**
49
+ * The bar label's tabindex. A parent `vf-menu-bar` owns a roving tabindex
50
+ * across its menus and sets this to 0 on the active menu, -1 on the rest,
51
+ * so the whole bar is a single Tab stop. Defaults to 0 so a standalone
52
+ * menu is Tab-focusable on its own.
53
+ */
54
+ barTabIndex: number;
55
+ private _labelEl;
56
+ private _assignedItems;
57
+ /** The enabled `vf-menu-item` children, in document order. */
58
+ get items(): VfMenuItem[];
59
+ /**
60
+ * Every slotted `vf-menu-item`, in document order — **disabled rows
61
+ * included**, unlike {@link items}. The press gesture hit-tests against
62
+ * these: releasing over a disabled row has to cancel, not fall through to
63
+ * whatever the panel covers.
64
+ */
65
+ get allItems(): VfMenuItem[];
66
+ /**
67
+ * Viewport rect of the bar title, or `null` before the first render. The
68
+ * press gesture hit-tests by coordinates rather than by event target (see
69
+ * src/menu-press.ts), so it wants the box, not the element.
70
+ */
71
+ get labelRect(): DOMRect | null;
72
+ connectedCallback(): void;
73
+ disconnectedCallback(): void;
74
+ protected updated(changed: Map<PropertyKey, unknown>): void;
75
+ /**
76
+ * Moves keyboard focus to the menu's bar label — the host's one focusable
77
+ * point. Overridden because the platform's `focus()` is a silent no-op here:
78
+ * no `delegatesFocus`, no host tabindex, so the standard method did nothing
79
+ * and `vf-label for` (which calls `target.focus()`) couldn't reach the menu.
80
+ */
81
+ focus(options?: FocusOptions): void;
82
+ /**
83
+ * @deprecated Use `focus()` — the standard method now does this. (Spelled
84
+ * without a link: a `{@link}` inside a deprecated tag makes the cem
85
+ * analyzer attach the parsed AST node to the manifest, which then fails
86
+ * to serialize — "Converting circular structure to JSON".)
87
+ */
88
+ focusLabel(): void;
89
+ protected render(): import("lit-html").TemplateResult<1>;
90
+ }
91
+ declare global {
92
+ interface HTMLElementTagNameMap {
93
+ 'vf-menu': VfMenu;
94
+ }
95
+ }
@@ -0,0 +1,346 @@
1
+ import { vfElement as e } from "../define.js";
2
+ import { ScaleController as t } from "../scale.js";
3
+ import n from "../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
4
+ import { vfDisplay as r } from "../styles/recipes/display-face.js";
5
+ import { vfBase as i } from "../styles/recipes/host.js";
6
+ import { vfPanel as a } from "../styles/recipes/surface.js";
7
+ import { vfFocusUnderline as o } from "../styles/recipes/focus.js";
8
+ import { GridSnapController as s } from "../grid-snap.js";
9
+ import { DocumentListenersController as c, releaseAfterGesture as l } from "../document-listeners.js";
10
+ import { deferActivation as u, emit as d } from "../events.js";
11
+ import { FocusRuleController as f } from "../focus-modality.js";
12
+ import { TypeAheadBuffer as p } from "../type-ahead.js";
13
+ import { MenuPressController as m } from "../menu-press.js";
14
+ import { LitElement as h, css as g, html as _, nothing as v } from "lit";
15
+ import { property as y, query as b, queryAssignedElements as x } from "lit/decorators.js";
16
+ //#region src/components/vf-menu.ts
17
+ var S = class extends h {
18
+ constructor(...e) {
19
+ super(...e), this.scale = new t(this), this.gridSnap = new s(this), this.#e = new f(this), this.label = "", this.open = !1, this.barTabIndex = 0, this.#t = new m(this, {
20
+ menus: () => [this],
21
+ open: (e) => {
22
+ e.open = !0;
23
+ },
24
+ close: () => {
25
+ this.open = !1;
26
+ }
27
+ }), this.#r = this.attachInternals(), this.#i = new p(), this.#a = !1, this.#o = () => {
28
+ this.#n || this.focus(), this.open = !1;
29
+ }, this.#s = (e) => {
30
+ e.composedPath().includes(this) || (this.open = !1);
31
+ }, this.#c = (e) => {
32
+ if (!e.defaultPrevented) switch (e.key) {
33
+ case "Escape":
34
+ e.preventDefault(), this.open = !1, this.focus();
35
+ break;
36
+ case "Tab":
37
+ this.open = !1;
38
+ break;
39
+ case "ArrowDown":
40
+ case "ArrowUp":
41
+ e.preventDefault(), this.#l(e.key === "ArrowDown" ? 1 : -1);
42
+ break;
43
+ case "Home":
44
+ case "End": {
45
+ e.preventDefault();
46
+ let t = this.items;
47
+ t[e.key === "Home" ? 0 : t.length - 1]?.focus();
48
+ break;
49
+ }
50
+ default: {
51
+ if (e.key.length !== 1 || e.key === " " || e.metaKey || e.ctrlKey || e.altKey) break;
52
+ e.preventDefault();
53
+ let t = this.items, n = t.indexOf(document.activeElement);
54
+ t[this.#i.feed(e.key, n, t.map((e) => ({
55
+ text: e.textContent ?? "",
56
+ disabled: !1
57
+ })))]?.focus();
58
+ break;
59
+ }
60
+ }
61
+ }, this.#u = new c(this, () => [[
62
+ document,
63
+ "pointerdown",
64
+ this.#s,
65
+ !0
66
+ ], [
67
+ document,
68
+ "keydown",
69
+ this.#c,
70
+ !0
71
+ ]]), this.#f = (e) => {
72
+ if (!this.open || this.#n) return;
73
+ let t = e.relatedTarget;
74
+ t instanceof Node && (this.contains(t) || this.renderRoot.contains(t)) || (this.open = !1);
75
+ }, this.#p = (e) => {
76
+ this.#e.suppress(), !this.#n && this.#t.onPointerDown(e);
77
+ };
78
+ }
79
+ static {
80
+ this.styles = [
81
+ i,
82
+ r,
83
+ a,
84
+ g`
85
+ :host {
86
+ display: inline-block;
87
+ position: relative;
88
+ }
89
+ .label {
90
+ display: flex;
91
+ align-items: center;
92
+ height: calc(var(--vf-scale, 1) * var(--vf-menubar-height, 20px));
93
+ /* The label box IS the title's black plate (and its hit rect), and
94
+ Menus.png puts the plate 10px left / 9px right of the title ink.
95
+ Chicago carries a 1px bearing on each side inside the text box, so
96
+ 9/8 in layout lands the plate at ink −10/+9. In a bar, vf-menu-bar
97
+ overlaps adjacent boxes 5px on top of this — the plates share a
98
+ band, as the originals did. */
99
+ padding-inline: calc(var(--vf-scale, 1) * 9px) calc(var(--vf-scale, 1) * 8px);
100
+ white-space: nowrap;
101
+ /* The press-drag gesture owns pointer moves while the title is held;
102
+ suppress the browser's own touch panning/scrolling so a touch drag
103
+ walks the menu instead of scrolling the page. */
104
+ touch-action: none;
105
+ cursor: var(--vf-cursor, default);
106
+ }
107
+ :host([open]) .label {
108
+ /* Forced colors: exempt the inverted title from the mode's text
109
+ backplate, which would land a Canvas slab on the highlight bar —
110
+ see vf-list-item's forced-colors note. The pair is already the
111
+ user's own via the vfBase token remap. */
112
+ @media (forced-colors: active) {
113
+ forced-color-adjust: none;
114
+ }
115
+ background: var(--vf-highlight, #000);
116
+ color: var(--vf-highlight-text, #fff);
117
+ /* The hilite leaves the bar's top row white: System 7's plate is rows
118
+ 1..18 of the 20px bar (the Label specimen in Menus.png is 18px of
119
+ plate, and InfiniteMac shows the white row above it). Transparent
120
+ borders + padding-box clip inset the paint without moving the box:
121
+ the top row shows the bar's white through, the bottom row the bar's
122
+ own black rule, and the hit rect stays the full bar height so a
123
+ click slammed against the screen top still opens the menu. The
124
+ 18px content box centers the 16px em on the same rows as the
125
+ closed state, so the title never shifts. Declared only while open
126
+ because forced-colors mode repaints transparent borders CanvasText
127
+ on closed labels; here forced-color-adjust above already exempts
128
+ them. */
129
+ border-block: calc(var(--vf-scale, 1) * 1px) solid transparent;
130
+ background-clip: padding-box;
131
+ }
132
+ /* The title rides in its own box so the focus rule spans the title and
133
+ not the bar cell's 10px padding — the same reason vf-button wraps its
134
+ label. A line-height of 1 shrinks that box to the face's own em, whose
135
+ bottom edge IS the descender line (the Chicago 12/4 metrics — see
136
+ register-embedded-font.ts), so the rule below it clears every glyph
137
+ instead of being crossed by a 'g' the way the button's is. A slotted
138
+ icon — the Apple menu's vf-img — sizes the box instead, and the same
139
+ rule lands one blank row under the artwork. The title doesn't move
140
+ either way: symmetric half-leading and align-items:center put the
141
+ baseline in the same place at any line height. */
142
+ .title {
143
+ position: relative;
144
+ display: flex;
145
+ align-items: center;
146
+ line-height: 1;
147
+ }
148
+ /* Keyboard focus is the kit's dashed rule under the title, not a ring
149
+ around the bar cell (see vfFocusUnderline) — one blank row under the
150
+ box, so −(1 + 1).
151
+
152
+ Gated on a class, not :focus-visible, exactly as vf-select is: the
153
+ press-drag gesture suppresses the browser's own mouse focus and
154
+ MenuPressController calls focus() instead, and Blink reads a
155
+ scripted focus as a visible one. So :focus-visible is true after a
156
+ plain mouse press on a title. FocusRuleController consults the page's
157
+ last input modality instead (see src/focus-modality.ts).
158
+
159
+ Only while CLOSED: a dropped menu already says where focus is, in the
160
+ louder of the two languages — the whole cell inverts. Drawing the rule
161
+ under an inverted title as well just adds a second, quieter mark
162
+ saying the same thing (in white, since it is currentColor). The mark
163
+ is for the state the inversion can't express: focused, not yet open.
164
+ The class stays on through the open state, so the rule comes back by
165
+ itself when the menu closes and hands focus back to the title. */
166
+ .label:focus {
167
+ outline: none;
168
+ }
169
+ :host(:not([open])) .label.vf-focus-rule .title::after {
170
+ --vf-focus-underline-offset: -2px;
171
+ ${o}
172
+ }
173
+ .panel {
174
+ position: absolute;
175
+ /* The panel anchors to the host, not the corrected label, so it
176
+ composes the snap offset (see grid-snap.ts) to ride along. */
177
+ top: calc(100% + var(--vf-snap-dy, 0px));
178
+ left: var(--vf-snap-dx, 0px);
179
+ /* A menu is as wide as its widest row, the way the MDEF sized it —
180
+ Menus.png's File pulldown is 141px, its cm/inches popup 94. The old
181
+ 180px floor held every smaller panel apart from its own rows, which
182
+ the right-anchored shortcut column would turn into a visible gulf
183
+ between label and ⌘. The floor that remains is the host's own
184
+ width, so a panel is never narrower than the bar title it hangs
185
+ from. (The nowrap rows make min-content the widest row, so the
186
+ shrink-to-fit abspos width IS the MDEF width.) */
187
+ min-width: 100%;
188
+ /* No vertical inset: every panel in Menus.png — both pulldowns, the
189
+ open popup and the closed pill — puts its first row's ink at +4 from
190
+ the border box, which is the 1px border plus the row's own 3px ✓
191
+ bias. The bottom border likewise sits immediately after the last row.
192
+ A 2px inset here displaced every row by 2px against the art; it was
193
+ invisible while the rows themselves were 6px too tall. */
194
+ padding: 0;
195
+ z-index: 1000;
196
+ /* As on the title: the held press, not the browser, owns touch moves
197
+ over the dropped panel. */
198
+ touch-action: none;
199
+ /* Slotted vf-separators render as the classic dimmed dotted menu rule. */
200
+ --vf-separator-color: var(--vf-disabled, #c0c0c0);
201
+ --vf-separator-style: dotted;
202
+ }
203
+ :host(:not([open])) .panel {
204
+ display: none;
205
+ }
206
+ /* A divider is an ITEM: the MDEF gave "-" a full row, so a separator
207
+ spends one whole --vf-menu-row-height, not a thin rule with token
208
+ margins — Menus.png's File pulldown runs a 32px ink pitch across
209
+ every divider group (vs 16 within one), and InfiniteMac's System 7.5
210
+ Edit menu puts the rule 12px under the ⌘ band above and 10px over
211
+ the one below: the neighbor rows' own 4/3px insets plus 8 over /
212
+ 7 under the 1px rule. Derived from the row token (8 = H/2,
213
+ 7 = H/2 − 1) so a re-themed pitch keeps dividers on it. */
214
+ .panel ::slotted(vf-separator) {
215
+ margin: calc(var(--vf-scale, 1) * var(--vf-menu-row-height, 16px) / 2)
216
+ 0
217
+ calc(var(--vf-scale, 1) * (var(--vf-menu-row-height, 16px) / 2 - 1px));
218
+ }
219
+ `
220
+ ];
221
+ }
222
+ #e;
223
+ #t;
224
+ get items() {
225
+ return this.allItems.filter((e) => !e.disabled);
226
+ }
227
+ get allItems() {
228
+ return this._assignedItems;
229
+ }
230
+ get labelRect() {
231
+ return this._labelEl?.getBoundingClientRect() ?? null;
232
+ }
233
+ get #n() {
234
+ return this.closest("vf-menu-bar") !== null;
235
+ }
236
+ #r;
237
+ #i;
238
+ #a;
239
+ #o;
240
+ #s;
241
+ #c;
242
+ #l(e) {
243
+ let t = this.items;
244
+ if (t.length === 0) return;
245
+ let n = t.indexOf(document.activeElement), r;
246
+ r = n < 0 ? e === 1 ? 0 : t.length - 1 : (n + e + t.length) % t.length, t[r]?.focus();
247
+ }
248
+ #u;
249
+ connectedCallback() {
250
+ super.connectedCallback(), this.#d(), this.requestUpdate(), this.addEventListener("vf-menu-close-request", this.#o), this.addEventListener("pointerdown", this.#p), this.addEventListener("focusout", this.#f);
251
+ }
252
+ #d() {
253
+ this.#r.role = this.#n ? "none" : null;
254
+ }
255
+ disconnectedCallback() {
256
+ super.disconnectedCallback(), this.removeEventListener("vf-menu-close-request", this.#o), this.removeEventListener("pointerdown", this.#p), this.removeEventListener("focusout", this.#f);
257
+ }
258
+ #f;
259
+ updated(e) {
260
+ if (e.has("open")) {
261
+ if (this.open && !this.#n ? this.#u.attach() : this.#u.detach(), !this.open) for (let e of this.allItems) e.active = !1;
262
+ this.open || this.#i.reset();
263
+ }
264
+ }
265
+ #p;
266
+ focus(e) {
267
+ this._labelEl?.focus(e);
268
+ }
269
+ focusLabel() {
270
+ this.focus();
271
+ }
272
+ render() {
273
+ return _`
274
+ <div
275
+ class="label vf-snap ${this.#e.marked ? "vf-focus-rule" : ""}"
276
+ part="label"
277
+ role=${this.#n ? "menuitem" : "button"}
278
+ tabindex=${this.barTabIndex}
279
+ aria-haspopup="menu"
280
+ aria-expanded=${this.open ? "true" : "false"}
281
+ aria-label=${this.label || v}
282
+ @pointerdown=${this.#h}
283
+ @click=${this.#g}
284
+ @pointerenter=${this.#_}
285
+ @keydown=${this.#v}
286
+ >
287
+ <span class="title"><slot name="label">${this.label}</slot></span>
288
+ </div>
289
+ <div
290
+ class="panel vf-panel"
291
+ part="panel"
292
+ role="menu"
293
+ aria-label=${this.label || v}
294
+ >
295
+ <slot></slot>
296
+ </div>
297
+ `;
298
+ }
299
+ #m() {
300
+ if (!this.#n) {
301
+ this.open = !this.open;
302
+ return;
303
+ }
304
+ d(this, "vf-menu-toggle-request", { menu: this }, {
305
+ cancelable: !0,
306
+ composed: !1
307
+ }) && (this.open = !this.open);
308
+ }
309
+ #h() {
310
+ this.#a = !0, l(() => {
311
+ this.#a = !1;
312
+ });
313
+ }
314
+ #g(e) {
315
+ if (this.#a) {
316
+ this.#a = !1;
317
+ return;
318
+ }
319
+ u(this, e, () => this.#m());
320
+ }
321
+ #_() {
322
+ d(this, "vf-menu-hover", { menu: this }, { composed: !1 });
323
+ }
324
+ #v(e) {
325
+ if (e.key === "Enter" || e.key === " ") {
326
+ e.preventDefault();
327
+ let t = this.open;
328
+ this.#m(), !t && this.open && this.#y();
329
+ } else e.key === "ArrowDown" && !this.open && (e.preventDefault(), this.#m(), this.open && this.#y());
330
+ }
331
+ async #y() {
332
+ await this.updateComplete, this.items[0]?.focus();
333
+ }
334
+ };
335
+ n([y()], S.prototype, "label", void 0), n([y({
336
+ type: Boolean,
337
+ reflect: !0
338
+ })], S.prototype, "open", void 0), n([y({
339
+ type: Number,
340
+ attribute: !1
341
+ })], S.prototype, "barTabIndex", void 0), n([b(".label")], S.prototype, "_labelEl", void 0), n([x({
342
+ selector: "vf-menu-item",
343
+ flatten: !0
344
+ })], S.prototype, "_assignedItems", void 0), S = n([e("vf-menu")], S);
345
+ //#endregion
346
+ export { S as VfMenu };