vintage-frames 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +40 -3
- package/dist/components/vf-container.d.ts +22 -2
- package/dist/components/vf-container.js +21 -14
- package/dist/components/vf-desktop.d.ts +3 -0
- package/dist/components/vf-desktop.js +26 -13
- package/dist/components/vf-menu-bar.js +35 -25
- package/dist/components/vf-menu.d.ts +3 -1
- package/dist/components/vf-window.js +44 -42
- package/dist/index.d.ts +8 -1
- package/dist/index.js +57 -56
- package/dist/styles/base.d.ts +2 -0
- package/dist/styles/recipes/rule.d.ts +41 -0
- package/dist/styles/recipes/rule.js +32 -0
- package/docs/SPEC.md +7 -5
- package/editor/vscode.html-custom-data.json +8 -3
- package/editor/web-types.json +14 -4
- package/package.json +3 -1
|
@@ -6,26 +6,27 @@ import { PlacementController as s, VfPositioned as c, warnMovableContract as l }
|
|
|
6
6
|
import { vfBase as u } from "../styles/recipes/host.js";
|
|
7
7
|
import { DOT_MOTIF as d, DOT_RECTS as f, DOT_SPAN as p, vfDots as m, vfStripes as h } from "../styles/recipes/pattern.js";
|
|
8
8
|
import { vfChromeFrame as g } from "../styles/recipes/surface.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
9
|
+
import { vfRule as _ } from "../styles/recipes/rule.js";
|
|
10
|
+
import { vfTitleBar as v, vfWindowWidgets as y } from "../styles/recipes/title-bar.js";
|
|
11
|
+
import { vfFocus as b } from "../styles/recipes/focus.js";
|
|
12
|
+
import { TileRasterCache as x, patternOverride as S, tileGrid as C, vfTileGrid as w } from "../tile-grid.js";
|
|
13
|
+
import { GridSnapController as T } from "../grid-snap.js";
|
|
14
|
+
import { VfSized as E } from "../size.js";
|
|
15
|
+
import { DragController as D } from "../drag.js";
|
|
16
|
+
import { TitleCenterController as O, chromeTitleBar as k, closeBox as A, widgetLabel as j, zoomBox as M } from "../chrome.js";
|
|
16
17
|
import "./vf-scroll-area.js";
|
|
17
|
-
import { LitElement as
|
|
18
|
-
import { property as
|
|
18
|
+
import { LitElement as N, css as P, html as F, nothing as I } from "lit";
|
|
19
|
+
import { property as L, state as R } from "lit/decorators.js";
|
|
19
20
|
//#region src/components/vf-window.ts
|
|
20
|
-
var
|
|
21
|
+
var z = 80, B = 54, V = 24, H = 8, U = class extends E(c(N)) {
|
|
21
22
|
constructor(...e) {
|
|
22
|
-
super(...e), this.heading = "", this.active = !0, this.closable = !0, this.zoomable = !1, this.movable = !1, this.resizable = !1, this.flush = !1, this._hasStatus = !1, this.scale = new n(this), this.gridSnap = new
|
|
23
|
+
super(...e), this.heading = "", this.active = !0, this.closable = !0, this.zoomable = !1, this.movable = !1, this.resizable = !1, this.flush = !1, this._hasStatus = !1, this.scale = new n(this), this.gridSnap = new T(this), this.titleCenter = new O(this), this._dotsPattern = "", this.#e = new x(), this.#t = (e, t, n) => {
|
|
23
24
|
let r = a(this.offsetWidth, this);
|
|
24
25
|
return {
|
|
25
|
-
x: Math.min(Math.max(e,
|
|
26
|
-
y: Math.min(Math.max(t, 0), Math.max(0, n.height -
|
|
26
|
+
x: Math.min(Math.max(e, V - r), n.width - V),
|
|
27
|
+
y: Math.min(Math.max(t, 0), Math.max(0, n.height - V))
|
|
27
28
|
};
|
|
28
|
-
}, this._placement = new s(this, (e, t, n) => this.#t(e, t, n)), this._drag = new
|
|
29
|
+
}, this._placement = new s(this, (e, t, n) => this.#t(e, t, n)), this._drag = new D(this, {
|
|
29
30
|
onDragStart: (e) => !this.movable || e.button !== 0 || e.composedPath().some((e) => e instanceof HTMLElement && e.classList.contains("box")) ? null : (this.#i(), this._placement.seed()),
|
|
30
31
|
onDrag: (e, t) => this._placement.moveTo(e, t)
|
|
31
32
|
}), this._resizeState = null, this.#n = !1, this.#r = !1;
|
|
@@ -35,12 +36,13 @@ var R = 80, z = 54, B = 24, V = 8, H = class extends T(c(M)) {
|
|
|
35
36
|
u,
|
|
36
37
|
h,
|
|
37
38
|
m,
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
w,
|
|
40
|
+
b,
|
|
40
41
|
g,
|
|
41
42
|
_,
|
|
42
43
|
v,
|
|
43
|
-
|
|
44
|
+
y,
|
|
45
|
+
P`
|
|
44
46
|
:host {
|
|
45
47
|
display: block;
|
|
46
48
|
position: relative;
|
|
@@ -221,7 +223,7 @@ var R = 80, z = 54, B = 24, V = 8, H = class extends T(c(M)) {
|
|
|
221
223
|
display: flex;
|
|
222
224
|
align-items: center;
|
|
223
225
|
height: calc(var(--vf-scale, 1) * var(--vf-status-bar-height, 15px));
|
|
224
|
-
|
|
226
|
+
/* The rule is vfRule's vf-rule-top on the element. */
|
|
225
227
|
background: var(--vf-white, #ffffff);
|
|
226
228
|
padding-inline: calc(var(--vf-scale, 1) * 6px);
|
|
227
229
|
line-height: calc(var(--vf-scale, 1) * var(--vf-line-height, 12px));
|
|
@@ -281,18 +283,18 @@ var R = 80, z = 54, B = 24, V = 8, H = class extends T(c(M)) {
|
|
|
281
283
|
}
|
|
282
284
|
#e;
|
|
283
285
|
willUpdate(e) {
|
|
284
|
-
super.willUpdate(e), this.variant === "utility" && (this._dotsPattern =
|
|
286
|
+
super.willUpdate(e), this.variant === "utility" && (this._dotsPattern = S(this, "--vf-dots-pattern"));
|
|
285
287
|
}
|
|
286
288
|
_dotsTexture() {
|
|
287
289
|
if (this.width == null) return;
|
|
288
290
|
let e = Math.max(1, this.width - 2);
|
|
289
|
-
if (this._dotsPattern) return
|
|
291
|
+
if (this._dotsPattern) return C({
|
|
290
292
|
cols: Math.ceil(e / p),
|
|
291
293
|
rows: 1,
|
|
292
294
|
tile: p
|
|
293
295
|
});
|
|
294
|
-
let t = Math.ceil((e + d) / p) * p, n =
|
|
295
|
-
return
|
|
296
|
+
let t = Math.ceil((e + d) / p) * p, n = H + d;
|
|
297
|
+
return F`<div
|
|
296
298
|
class="vf-tile-raster"
|
|
297
299
|
style="width:${i(t)};height:${i(n)};background-image:${this.#e.for(d, d, f, t, n)}"
|
|
298
300
|
></div>`;
|
|
@@ -346,7 +348,7 @@ var R = 80, z = 54, B = 24, V = 8, H = class extends T(c(M)) {
|
|
|
346
348
|
_onGrowPointerMove(e) {
|
|
347
349
|
let t = this._resizeState;
|
|
348
350
|
if (!t || e.pointerId !== t.pointerId) return;
|
|
349
|
-
let n = r(Math.max(
|
|
351
|
+
let n = r(Math.max(z, t.baseWidth + a(e.clientX - t.startX, this)), this), i = r(Math.max(B, t.baseHeight + a(e.clientY - t.startY, this)), this);
|
|
350
352
|
(n !== this.width || i !== this.height) && (t.emitPending = !0, t.resized = !0), this.width = n, this.height = i;
|
|
351
353
|
}
|
|
352
354
|
_onGrowPointerEnd(t) {
|
|
@@ -361,34 +363,34 @@ var R = 80, z = 54, B = 24, V = 8, H = class extends T(c(M)) {
|
|
|
361
363
|
});
|
|
362
364
|
}
|
|
363
365
|
render() {
|
|
364
|
-
return
|
|
366
|
+
return F`
|
|
365
367
|
<div
|
|
366
368
|
class="vf-frame vf-snap"
|
|
367
369
|
part="frame"
|
|
368
370
|
role="group"
|
|
369
|
-
aria-labelledby=${this.heading ? "title" :
|
|
371
|
+
aria-labelledby=${this.heading ? "title" : I}
|
|
370
372
|
>
|
|
371
|
-
${
|
|
372
|
-
${this.closable ?
|
|
373
|
+
${k(this._drag, F`
|
|
374
|
+
${this.closable ? A(j("Close", this.heading), this._onCloseClick) : I}
|
|
373
375
|
<span class="vf-title" part="title" id="title">${this.heading}</span>
|
|
374
|
-
${this.zoomable ? j(
|
|
376
|
+
${this.zoomable ? M(j("Zoom", this.heading), this._onZoomClick) : I}
|
|
375
377
|
`, this.variant === "utility" ? "vf-dots" : "vf-stripes", this.variant === "utility" ? this._dotsTexture() : void 0)}
|
|
376
378
|
<div class="body" part="body">
|
|
377
|
-
${this.scrollbars ?
|
|
379
|
+
${this.scrollbars ? F`
|
|
378
380
|
<vf-scroll-area
|
|
379
381
|
class="edge-scroll"
|
|
380
382
|
axis=${this.scrollbars}
|
|
381
|
-
label=${this.heading ||
|
|
383
|
+
label=${this.heading || I}
|
|
382
384
|
exportparts="viewport"
|
|
383
385
|
>
|
|
384
386
|
<slot></slot>
|
|
385
387
|
</vf-scroll-area>
|
|
386
|
-
` :
|
|
388
|
+
` : F`<slot></slot>`}
|
|
387
389
|
</div>
|
|
388
|
-
<div class="status ${this._hasStatus ? "" : "empty"}" part="status-bar">
|
|
390
|
+
<div class="status vf-rule-top ${this._hasStatus ? "" : "empty"}" part="status-bar">
|
|
389
391
|
<slot name="status" @slotchange=${this._onStatusSlotChange}></slot>
|
|
390
392
|
</div>
|
|
391
|
-
${this.resizable ?
|
|
393
|
+
${this.resizable ? F`
|
|
392
394
|
<div
|
|
393
395
|
class="grow"
|
|
394
396
|
part="grow-box"
|
|
@@ -398,29 +400,29 @@ var R = 80, z = 54, B = 24, V = 8, H = class extends T(c(M)) {
|
|
|
398
400
|
@pointercancel=${this._onGrowPointerEnd}
|
|
399
401
|
@lostpointercapture=${this._onGrowPointerEnd}
|
|
400
402
|
></div>
|
|
401
|
-
` :
|
|
403
|
+
` : I}
|
|
402
404
|
</div>
|
|
403
405
|
`;
|
|
404
406
|
}
|
|
405
407
|
};
|
|
406
|
-
o([
|
|
408
|
+
o([L({ reflect: !0 })], U.prototype, "variant", void 0), o([L()], U.prototype, "heading", void 0), o([L({
|
|
407
409
|
type: Boolean,
|
|
408
410
|
reflect: !0
|
|
409
|
-
})],
|
|
411
|
+
})], U.prototype, "active", void 0), o([L({
|
|
410
412
|
type: Boolean,
|
|
411
413
|
reflect: !0
|
|
412
|
-
})],
|
|
414
|
+
})], U.prototype, "closable", void 0), o([L({
|
|
413
415
|
type: Boolean,
|
|
414
416
|
reflect: !0
|
|
415
|
-
})],
|
|
417
|
+
})], U.prototype, "zoomable", void 0), o([L({
|
|
416
418
|
type: Boolean,
|
|
417
419
|
reflect: !0
|
|
418
|
-
})],
|
|
420
|
+
})], U.prototype, "movable", void 0), o([L({
|
|
419
421
|
type: Boolean,
|
|
420
422
|
reflect: !0
|
|
421
|
-
})],
|
|
423
|
+
})], U.prototype, "resizable", void 0), o([L({
|
|
422
424
|
type: Boolean,
|
|
423
425
|
reflect: !0
|
|
424
|
-
})],
|
|
426
|
+
})], U.prototype, "flush", void 0), o([L({ reflect: !0 })], U.prototype, "scrollbars", void 0), o([R()], U.prototype, "_hasStatus", void 0), U = o([t("vf-window")], U);
|
|
425
427
|
//#endregion
|
|
426
|
-
export {
|
|
428
|
+
export { U as VfWindow };
|
package/dist/index.d.ts
CHANGED
|
@@ -88,8 +88,15 @@ export { CURSOR_ARROW, CURSOR_I_BEAM, CURSOR_CROSSHAIR, CURSOR_WAIT, } from './c
|
|
|
88
88
|
* `static styles` to inherit the kit's type, panel frame, window chrome, focus
|
|
89
89
|
* ring, field well and System 7 scroll-rail skin.
|
|
90
90
|
*/
|
|
91
|
-
export { vfBase, vfBodyDecls, vfDisplay, vfDisplayDecls, vfStaticText, vfPanel, vfChromeFrame, vfModalFrame, vfTitleBar, vfWindowWidgets, vfHardShadowDecls, vfStripes, vfDots, vfFocus, vfFocusRing, vfFocusUnderline, vfToggle, vfField, vfScrollRail, vfTileSize, vfTileMaskSize, tileImage, tileRaster, tileRects, tileSpan, TILE_LATTICE, } from './styles/base.js';
|
|
91
|
+
export { vfBase, vfBodyDecls, vfDisplay, vfDisplayDecls, vfStaticText, vfPanel, vfChromeFrame, vfModalFrame, vfRule, vfTitleBar, vfWindowWidgets, vfHardShadowDecls, vfStripes, vfDots, vfFocus, vfFocusRing, vfFocusUnderline, vfToggle, vfField, vfScrollRail, vfTileSize, vfTileMaskSize, tileImage, tileRaster, tileRects, tileSpan, TILE_LATTICE, } from './styles/base.js';
|
|
92
92
|
export type { TileRect } from './styles/base.js';
|
|
93
|
+
/**
|
|
94
|
+
* The 1px rule's attribute grammar (`vf-container rule="top bottom"`): the
|
|
95
|
+
* edge names, the parser, and the class list a resolved rule puts on a box
|
|
96
|
+
* styled by `vfRule`.
|
|
97
|
+
*/
|
|
98
|
+
export { RULE_EDGES, parseRule, ruleClasses } from './styles/base.js';
|
|
99
|
+
export type { RuleEdge } from './styles/base.js';
|
|
93
100
|
/**
|
|
94
101
|
* The exact tiled fill (TILE-GRID-PLAN.md): a surface's own art as one
|
|
95
102
|
* whole-surface raster (`tileRaster` + the `vf-tile-raster` rules), and a
|
package/dist/index.js
CHANGED
|
@@ -15,59 +15,60 @@ import { PATTERNS as G, PATTERN_NAMES as K, PATTERN_SIZE as q, parsePattern as J
|
|
|
15
15
|
import { vfDots as Q, vfStripes as $ } from "./styles/recipes/pattern.js";
|
|
16
16
|
import { vfHardShadowDecls as ee } from "./styles/recipes/shadow.js";
|
|
17
17
|
import { vfChromeFrame as te, vfModalFrame as ne, vfPanel as re } from "./styles/recipes/surface.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
59
|
-
import {
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
64
|
-
import {
|
|
65
|
-
import {
|
|
66
|
-
import {
|
|
67
|
-
import {
|
|
68
|
-
import {
|
|
69
|
-
import {
|
|
70
|
-
import {
|
|
71
|
-
import {
|
|
72
|
-
import {
|
|
73
|
-
|
|
18
|
+
import { RULE_EDGES as ie, parseRule as ae, ruleClasses as oe, vfRule as se } from "./styles/recipes/rule.js";
|
|
19
|
+
import { vfTitleBar as ce, vfWindowWidgets as le } from "./styles/recipes/title-bar.js";
|
|
20
|
+
import { vfFocus as ue, vfFocusRing as de, vfFocusUnderline as fe } from "./styles/recipes/focus.js";
|
|
21
|
+
import { vfToggle as pe } from "./styles/recipes/toggle.js";
|
|
22
|
+
import { vfField as me } from "./styles/recipes/field.js";
|
|
23
|
+
import { vfScrollRail as he } from "./styles/recipes/scroll-rail.js";
|
|
24
|
+
import { TileRasterCache as ge, patternOverride as _e, tileGrid as ve, vfTileGrid as ye } from "./tile-grid.js";
|
|
25
|
+
import { TrackWidthController as be } from "./track-width.js";
|
|
26
|
+
import { PatternFillController as xe, vfPatternFill as Se } from "./pattern-fill.js";
|
|
27
|
+
import { GridSnapController as Ce, requestGridSnap as we } from "./grid-snap.js";
|
|
28
|
+
import { DocumentListenersController as Te } from "./document-listeners.js";
|
|
29
|
+
import { BUTTON_FACE as Ee, BUTTON_FRAME as De, RING_FRAME as Oe, RING_HOLE as ke, RING_INSET as Ae, SCREEN_CORNER as je, steppedCornerClip as Me, steppedRectClip as Ne, steppedRingClip as Pe } from "./pixel-frame.js";
|
|
30
|
+
import { VfDesktop as Fe } from "./components/vf-desktop.js";
|
|
31
|
+
import { VfSized as Ie } from "./size.js";
|
|
32
|
+
import { DragController as Le } from "./drag.js";
|
|
33
|
+
import { ScrollStateController as Re } from "./scroll-state.js";
|
|
34
|
+
import { BLINK_FLIPS as ze, BLINK_INTERVAL_MS as Be, PRESS_HOLD_MS as Ve, RENAME_DELAY_MS as He, prefersReducedMotion as Ue, runSelectionBlink as We } from "./motion.js";
|
|
35
|
+
import { CARET_DOWN as Ge, CHECKBOX_X as Ke, CHECKMARK as qe, RADIO_DOT as Je, RADIO_FACE as Ye, RADIO_RING as Xe, RADIO_RING_PRESSED as Ze, SLIDER_THUMB as Qe, SLIDER_THUMB_FACE as $e, STEPPER as et, STEPPER_DOWN_FILL as tt, STEPPER_UP_FILL as nt, glyphSvg as rt } from "./glyphs.js";
|
|
36
|
+
import { ScrollRailController as it, renderScrollRail as at } from "./scroll-rail.js";
|
|
37
|
+
import { VfScrollArea as ot } from "./components/vf-scroll-area.js";
|
|
38
|
+
import { VfWindow as st } from "./components/vf-window.js";
|
|
39
|
+
import { VfModalDialog as ct, modalDialogStyles as lt } from "./modal-dialog.js";
|
|
40
|
+
import { VfButtonGroup as ut } from "./components/vf-button-group.js";
|
|
41
|
+
import { VfDialog as dt } from "./components/vf-dialog.js";
|
|
42
|
+
import { VfSeparator as ft } from "./components/vf-separator.js";
|
|
43
|
+
import { VfFormControl as pt } from "./form-control.js";
|
|
44
|
+
import { VfButton as mt } from "./components/vf-button.js";
|
|
45
|
+
import { VfSwatch as ht } from "./components/vf-swatch.js";
|
|
46
|
+
import { VfToggleControl as gt } from "./toggle-control.js";
|
|
47
|
+
import { VfCheckbox as _t } from "./components/vf-checkbox.js";
|
|
48
|
+
import { VfRadio as vt } from "./components/vf-radio.js";
|
|
49
|
+
import { VfRadioGroup as yt } from "./components/vf-radio-group.js";
|
|
50
|
+
import { FocusRuleController as bt, focusModality as xt, trackFocusModality as St } from "./focus-modality.js";
|
|
51
|
+
import { VfTextControlBase as Ct } from "./text-control.js";
|
|
52
|
+
import { VfTextField as wt } from "./components/vf-text-field.js";
|
|
53
|
+
import { VfTextArea as Tt } from "./components/vf-text-area.js";
|
|
54
|
+
import { VfNumberField as Et } from "./components/vf-number-field.js";
|
|
55
|
+
import { VfOption as Dt } from "./components/vf-option.js";
|
|
56
|
+
import { VfSelect as Ot } from "./components/vf-select.js";
|
|
57
|
+
import { VfProgressBar as kt } from "./components/vf-progress-bar.js";
|
|
58
|
+
import { VfSlider as At } from "./components/vf-slider.js";
|
|
59
|
+
import { VfLabel as jt } from "./components/vf-label.js";
|
|
60
|
+
import { VfParagraph as Mt } from "./components/vf-paragraph.js";
|
|
61
|
+
import { VfImg as Nt } from "./components/vf-img.js";
|
|
62
|
+
import { VfIcon as Pt } from "./components/vf-icon.js";
|
|
63
|
+
import { VfMenuBar as Ft } from "./components/vf-menu-bar.js";
|
|
64
|
+
import { VfMenu as It } from "./components/vf-menu.js";
|
|
65
|
+
import { VfMenuItem as Lt } from "./components/vf-menu-item.js";
|
|
66
|
+
import { VfList as Rt } from "./components/vf-list.js";
|
|
67
|
+
import { VfListItem as zt } from "./components/vf-list-item.js";
|
|
68
|
+
import { VfFieldset as Bt } from "./components/vf-fieldset.js";
|
|
69
|
+
import { VfGrid as Vt } from "./components/vf-grid.js";
|
|
70
|
+
import { VfStack as Ht } from "./components/vf-stack.js";
|
|
71
|
+
import { VfContainer as Ut } from "./components/vf-container.js";
|
|
72
|
+
import { CURSOR_ARROW as Wt, CURSOR_CROSSHAIR as Gt, CURSOR_I_BEAM as Kt, CURSOR_WAIT as qt } from "./cursor-art.js";
|
|
73
|
+
import { applyCursor as Jt } from "./cursor.js";
|
|
74
|
+
export { ze as BLINK_FLIPS, Be as BLINK_INTERVAL_MS, Ee as BUTTON_FACE, De as BUTTON_FRAME, Ge as CARET_DOWN, Ke as CHECKBOX_X, qe as CHECKMARK, r as CLASSIC_DPI, i as CSS_REFERENCE_DPI, Wt as CURSOR_ARROW, Gt as CURSOR_CROSSHAIR, Kt as CURSOR_I_BEAM, qt as CURSOR_WAIT, Te as DocumentListenersController, Le as DragController, bt as FocusRuleController, Ce as GridSnapController, G as PATTERNS, K as PATTERN_NAMES, q as PATTERN_SIZE, D as PIXEL_GRID_METRICS, Ve as PRESS_HOLD_MS, xe as PatternFillController, T as PlacementController, Je as RADIO_DOT, Ye as RADIO_FACE, Xe as RADIO_RING, Ze as RADIO_RING_PRESSED, He as RENAME_DELAY_MS, Oe as RING_FRAME, ke as RING_HOLE, Ae as RING_INSET, ie as RULE_EDGES, je as SCREEN_CORNER, Qe as SLIDER_THUMB, $e as SLIDER_THUMB_FACE, et as STEPPER, tt as STEPPER_DOWN_FILL, nt as STEPPER_UP_FILL, a as SYSTEM_PX_IN_CSS_PX, d as ScaleController, it as ScrollRailController, Re as ScrollStateController, R as TILE_LATTICE, ge as TileRasterCache, be as TrackWidthController, k as VF_BODY_FAMILY, M as VF_DISPLAY_FAMILY, mt as VfButton, ut as VfButtonGroup, _t as VfCheckbox, Ut as VfContainer, Fe as VfDesktop, dt as VfDialog, Bt as VfFieldset, pt as VfFormControl, Vt as VfGrid, Pt as VfIcon, Nt as VfImg, jt as VfLabel, Rt as VfList, zt as VfListItem, It as VfMenu, Ft as VfMenuBar, Lt as VfMenuItem, ct as VfModalDialog, Et as VfNumberField, Dt as VfOption, Mt as VfParagraph, E as VfPositioned, kt as VfProgressBar, vt as VfRadio, yt as VfRadioGroup, ot as VfScrollArea, Ot as VfSelect, ft as VfSeparator, Ie as VfSized, At as VfSlider, Ht as VfStack, ht as VfSwatch, Tt as VfTextArea, Ct as VfTextControlBase, wt as VfTextField, gt as VfToggleControl, st as VfWindow, Jt as applyCursor, f as applyScale, t as defineElement, o as devicePxPerSystemPx, p as effectiveScale, e as emit, xt as focusModality, m as getScale, s as getZoom, rt as glyphSvg, lt as modalDialogStyles, h as onScaleChange, c as onZoomChange, J as parsePattern, ae as parseRule, Y as patternHex, X as patternMotif, _e as patternOverride, Z as patternRects, Ue as prefersReducedMotion, A as registerBodyFace, N as registerDisplayFace, O as registerEmbeddedFont, at as renderScrollRail, we as requestGridSnap, l as resetZoomBaseline, oe as ruleClasses, We as runSelectionBlink, g as snapSys, _ as snapToDevicePx, v as snapToSystemPx, Me as steppedCornerClip, Ne as steppedRectClip, Pe as steppedRingClip, y as sys, b as sysLength, x as sysLengths, S as systemPxQuantum, ve as tileGrid, z as tileImage, B as tileRaster, V as tileRects, H as tileSpan, C as toSys, w as toSysExact, St as trackFocusModality, u as truePixelRatio, I as vfBase, j as vfBodyDecls, te as vfChromeFrame, P as vfDisplay, F as vfDisplayDecls, Q as vfDots, n as vfElement, me as vfField, ue as vfFocus, de as vfFocusRing, fe as vfFocusUnderline, ee as vfHardShadowDecls, ne as vfModalFrame, re as vfPanel, Se as vfPatternFill, se as vfRule, he as vfScrollRail, L as vfStaticText, $ as vfStripes, ye as vfTileGrid, U as vfTileMaskSize, W as vfTileSize, ce as vfTitleBar, pe as vfToggle, le as vfWindowWidgets };
|
package/dist/styles/base.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export { vfStaticText } from './recipes/static-text.js';
|
|
|
29
29
|
export { vfDots, vfStripes } from './recipes/pattern.js';
|
|
30
30
|
export { vfHardShadowDecls } from './recipes/shadow.js';
|
|
31
31
|
export { vfChromeFrame, vfModalFrame, vfPanel } from './recipes/surface.js';
|
|
32
|
+
export { RULE_EDGES, parseRule, ruleClasses, vfRule } from './recipes/rule.js';
|
|
33
|
+
export type { RuleEdge } from './recipes/rule.js';
|
|
32
34
|
export { vfTitleBar, vfWindowWidgets } from './recipes/title-bar.js';
|
|
33
35
|
export { vfFocus, vfFocusRing, vfFocusUnderline } from './recipes/focus.js';
|
|
34
36
|
export { vfToggle } from './recipes/toggle.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four edges a rule can sit on, in CSS shorthand order.
|
|
3
|
+
*/
|
|
4
|
+
export declare const RULE_EDGES: readonly ["top", "right", "bottom", "left"];
|
|
5
|
+
export type RuleEdge = (typeof RULE_EDGES)[number];
|
|
6
|
+
/**
|
|
7
|
+
* The 1px rule — the single black line that is the 1-bit art's only edge —
|
|
8
|
+
* drawn on one side of a box. One class per edge, composable: the menu bar's
|
|
9
|
+
* floor is `.vf-rule-bottom`, a window's status strip's ceiling is
|
|
10
|
+
* `.vf-rule-top`, and `vf-container rule="…"` takes the edges by name.
|
|
11
|
+
*
|
|
12
|
+
* A border, not a pseudo-element or a painted line, so it lives inside the
|
|
13
|
+
* box's own `border-box` size (vfBase) and insets the padding box the way
|
|
14
|
+
* FrameRect insets a rectangle's interior: content, percentage fills and
|
|
15
|
+
* children placed against the box all begin inside the rule. Scales with
|
|
16
|
+
* `--vf-scale`, so it is one system px at every density, and paints in
|
|
17
|
+
* `--vf-black`, so it remaps under forced colors with the rest of the ink.
|
|
18
|
+
*
|
|
19
|
+
* One declaration per edge, so the kit cannot grow two rules — the
|
|
20
|
+
* {@link vfHardShadowDecls} principle, applied to the line.
|
|
21
|
+
*
|
|
22
|
+
* Being a border, it shares the kit's border-floor residual
|
|
23
|
+
* (docs/THREE-X-DISPLAYS.md): Chromium floors a fractional border-width to
|
|
24
|
+
* whole CSS px, so above 1× the line paints thinner than a system px (2
|
|
25
|
+
* device px of 3 at 2×) and the padding box begins that much inside — the
|
|
26
|
+
* same as every kit frame, and the same line the menu bar always drew.
|
|
27
|
+
* `npm run verify:rule` asserts a container's rule against the menu bar's.
|
|
28
|
+
*/
|
|
29
|
+
export declare const vfRule: import("lit").CSSResult;
|
|
30
|
+
/**
|
|
31
|
+
* The `rule` attribute grammar: edge names separated by whitespace, in any
|
|
32
|
+
* order (`"bottom"`, `"top bottom"`, all four for a framed box). Returns the
|
|
33
|
+
* edges in top/right/bottom/left order with repeats dropped; an empty array
|
|
34
|
+
* for an unset or blank value; and `null` for a value carrying any token that
|
|
35
|
+
* is not an edge name — the whole value is refused, not the one token, as
|
|
36
|
+
* `parsePattern` refuses an unrecognized pattern. Case-sensitive, like the
|
|
37
|
+
* pattern names.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseRule(value: string | null | undefined): RuleEdge[] | null;
|
|
40
|
+
/** The classes a resolved rule puts on its box: `vf-rule-top vf-rule-bottom`. */
|
|
41
|
+
export declare function ruleClasses(edges: readonly RuleEdge[]): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { css as e, unsafeCSS as t } from "lit";
|
|
2
|
+
//#region src/styles/recipes/rule.ts
|
|
3
|
+
var n = [
|
|
4
|
+
"top",
|
|
5
|
+
"right",
|
|
6
|
+
"bottom",
|
|
7
|
+
"left"
|
|
8
|
+
], r = t("calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000)"), i = e`
|
|
9
|
+
.vf-rule-top {
|
|
10
|
+
border-top: ${r};
|
|
11
|
+
}
|
|
12
|
+
.vf-rule-right {
|
|
13
|
+
border-right: ${r};
|
|
14
|
+
}
|
|
15
|
+
.vf-rule-bottom {
|
|
16
|
+
border-bottom: ${r};
|
|
17
|
+
}
|
|
18
|
+
.vf-rule-left {
|
|
19
|
+
border-left: ${r};
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
function a(e) {
|
|
23
|
+
if (e == null) return [];
|
|
24
|
+
let t = e.trim().split(/\s+/).filter(Boolean), r = new Set(t);
|
|
25
|
+
for (let e of r) if (!n.includes(e)) return null;
|
|
26
|
+
return n.filter((e) => r.has(e));
|
|
27
|
+
}
|
|
28
|
+
function o(e) {
|
|
29
|
+
return e.map((e) => `vf-rule-${e}`).join(" ");
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { n as RULE_EDGES, a as parseRule, o as ruleClasses, i as vfRule };
|
package/docs/SPEC.md
CHANGED
|
@@ -148,6 +148,7 @@ The span construction stays load-bearing for the CSS-repeated underlays beneath
|
|
|
148
148
|
- `vfHardShadowDecls` — the hard 1-bit drop shadow on its own, for composing into a surface that supplies its own border: `box-shadow: var(--vf-shadow-offset, 2px) var(--vf-shadow-offset, 2px) 0 0 var(--vf-black, #000)`. No blur, no spread — System 7's only depth cue. Every raised surface in the kit composes this one declaration (`vfPanel`, `vfChromeFrame`; `vf-swatch` composes it without either surface class).
|
|
149
149
|
- `vfPanel` — a `.vf-panel` class for menus/popups: white bg, `border: 1px solid var(--vf-black, #000)`, `vfHardShadowDecls`.
|
|
150
150
|
- `vfChromeFrame` — a `.vf-frame` class for the desktop window (`vf-window`): the same white face, 1px black border and hard offset shadow as `vfPanel`, no bevels. Skin only — the component supplies its own layout (a full-size flex column). `vf-dialog` does not use it: both of its chromes are `vfModalFrame`.
|
|
151
|
+
- `vfRule` — the 1px rule on one edge of a box, one class per edge: `.vf-rule-top`, `.vf-rule-right`, `.vf-rule-bottom`, `.vf-rule-left`, each `border-<edge>: 1px solid var(--vf-black, #000)` scaled by `--vf-scale`. A border, so it sits inside the box's `border-box` size and insets the padding box — content, percentage fills and children placed against the box begin inside the rule, as a rectangle's interior begins inside FrameRect's line. One declaration per edge, the `vfHardShadowDecls` principle applied to the line: `vf-menu-bar`'s floor and `vf-window`'s status-strip ceiling are these classes, and `vf-container rule="…"` takes the edges by name. The attribute grammar ships beside it (`RULE_EDGES`, `parseRule`, `ruleClasses`): edge names separated by whitespace in any order, repeats dropped, returned in shorthand order; a blank or unset value is no edges; any other token refuses the whole value, as `parsePattern` refuses an unrecognized pattern. Being a border it shares the kit's border-floor residual (docs/THREE-X-DISPLAYS.md — thin above 1× in Chromium, the padding box that much inside), exactly as every kit frame does. `npm run verify:rule` asserts a container's rule against the menu bar's own at four densities.
|
|
151
152
|
- `vfModalFrame` — the dBoxProc modal-dialog frame, both `vf-dialog` chromes: `.vf-modal-frame` (white face, 1px black border, **no shadow**) around `.vf-modal-frame-inner` (`margin: 2px; border: 2px solid`). **The movable modal is this frame with the striped bar set into it** (traced from a 2× System 7 capture): a `.vf-title-bar` as the frame's first child sits directly under the outer rule, `margin: 0 2px` so its stripes — flush to the bar's own edge here, `inset: 3px 0` — keep the same 2px of white from the outer rule the band keeps; the inner box that follows it drops its top margin and thins its top border to 1px, so the bar's own 1px rule plus that border make the 2px band the bar's floor, continuing down the sides and along the bottom. System 7's alert box drew the mirror trace — 2px outer, 2px gap, 1px inner rule, *with* the hard shadow (`Windows/modal dialog.png` vs the alert reference) — a different chrome the kit does not ship: an alert box is a consumer composition over this frame, not a component.
|
|
152
153
|
- `vfWindowWidgets` — the title-bar window widgets (`.box` base, `.close` left / `.zoom` right at 11×11 with the 1px white patch ring, the pressed go-away sunburst, the nested zoom square), shared by `vf-window` and a `closable` `vf-dialog` so the two components' widgets match by construction. The templates that pair with it (`closeBox`/`zoomBox`/`widgetLabel`) live in `src/chrome.ts` with `chromeTitleBar`. Geometry is the standard bar's; `vf-window`'s utility variant overrides sizes under its own selector.
|
|
153
154
|
- `vfTitleBar` — the striped title bar shared by `vf-window` and `vf-dialog`: a `.vf-title-bar` row (`height: var(--vf-titlebar-height, 18px)`, 1px bottom rule, `overflow: hidden`) and the `.vf-title` patch inside it (display face, white bg, `padding: 0 6px`, `z-index: 1` over the stripes, `nowrap` + ellipsis). Put a `.vf-stripes` layer in as the bar's first child.
|
|
@@ -200,6 +201,7 @@ Full-bleed classic desktop container.
|
|
|
200
201
|
- **Behavior:** manages stacking of slotted `vf-window` children: `pointerdown` *or `focusin`* on a window brings it to front (incrementing z-index counter) and sets its `active` attribute, clearing `active` on the others — the `focusin` half is the keyboard route to activation: Tab landing anywhere in a background window (its undrawn-but-focusable widgets included) raises it. Listens via delegated pointerdown/focusin listeners + `slotchange`. Windows slotted before `vf-window` is defined are re-normalized once `customElements.whenDefined('vf-window')` settles, since the upgrade reflects each window's `active = true` default back out and upgrading a slotted node doesn't re-fire `slotchange`.
|
|
201
202
|
- **DOM order follows z-order** (bottom-most first), so sequential focus order matches the visual stack and Shift+Tab mirrors Tab exactly. The sync runs at pointer-gesture end (a mid-gesture node move would clear the pointer capture a title-bar drag or grow-box resize holds) and on programmatic `bringToFront`, never from a focus-driven raise (moving the window focus just entered would re-order the tab sequence mid-traversal); focus surviving its own window's move is restored without re-raising that window. Non-window children (a menu bar, page content) keep their positions. `npm run verify:window-a11y`.
|
|
202
203
|
- **Floating tier:** `vf-window[variant="utility"]` children stack in a z band `1_000_000` above the document tier (one shared monotonic counter, so a palette stays above every document window), restack only among themselves, and stand outside the single-active invariant both ways: clicking a palette doesn't deactivate the active document window, and activating a document window never clears a palette's `active` — System 7 windoid behavior while the app is frontmost. The tier test reads the `variant` *attribute*, so a not-yet-upgraded element still lands right.
|
|
204
|
+
- **Menu tier:** a slotted `vf-menu-bar` — or a free-standing `vf-menu` placed on the desktop — is restated at `z-index: 2_000_000` by the desktop's own `::slotted(vf-menu-bar), ::slotted(vf-menu)` rule (an outer-tree `::slotted` declaration beats the bar's inner `:host { z-index: 1000 }`), a band above the floating tier, so a dropped menu paints over palettes and document windows alike — the Menu Manager drew menus over every window. Only the screen-corner mask is in front. The full stack inside the screen's isolated context: consumer tile grid (−1) < document windows (counter) < utility windows (counter + 1_000_000) < menu tier (2_000_000) < corner mask (max). A popup *inside* a window (`vf-select`'s fixed panel) is a different case: it stacks within its window's context, so a palette can still cover it — KNOWN-BUGS.md. `npm run verify:archetypes` (MENU TIER).
|
|
203
205
|
- **Deactivation:** on a real System 7 machine clicking the desktop clicked the *Finder* — the frontmost application's windows lost their stripes. `clearActive()` is that gesture's handler: it clears `active` from the whole document tier, and **zero active windows is a legal state**, held until a press or `focusin` re-enters a document window or a new one is slotted (opening a window brings its application forward — a newly slotted window ends a deliberate deactivation, where a mere survivor never does: removing a *background* window while deactivated promotes nothing, while removing the holder outside a deactivation promotes the topmost survivor). The desktop never takes the decision itself — a press on its own bare dither changes nothing; desktop furniture is slotted light DOM, so only the page knows which of its children mean "the Finder". Utility windows keep their `active` through a deactivation (their dots stay drawn). Every change of holder — window to window, window to none, none to window — fires `vf-activate`, once per change; re-asserting the current holder is silent. `npm run verify:desktop-activate`.
|
|
204
206
|
- **Events:** `vf-activate` (detail `{ window: HTMLElement | null }` — the new holder, `null` on deactivation).
|
|
205
207
|
- **Parts:** `desktop`.
|
|
@@ -214,7 +216,7 @@ The desktop-window shell: the classic document window (see DragThing screenshot)
|
|
|
214
216
|
- Zoom box: RIGHT side, same box, plus a small box nested in its top-left corner (sharing the widget's own top/left border; only the right and bottom edges are drawn). `:active` (pressed) → shows the identical sunburst as the close box; the nested box gives way to it.
|
|
215
217
|
- Body: `padding: 12px` (0 if `flush` or `scrollbars`), and `overflow: hidden` — the window being a fixed box, content taller than it is clipped at the frame the way the classic content region was, rather than painting out over the desktop; `scrollbars` is how the user reaches the rest. Two deliberate exemptions: `[scrollbars]` restores `overflow: visible`, because the edge-rail composition below pulls the scroll area one system px *outside* the body on every side and clipping would shave exactly that overhang off (the scroll area does its own clipping); and a control's drop-open panel is not clipped — `vf-select`'s list is `position: fixed` off the control's rect precisely to escape clipping ancestors (§5 vf-select), and it still escapes, because nothing between it and the viewport establishes a containing block for fixed descendants (the grid-snap correction is a `position: relative` left/top offset, never a transform — see §7). A `vf-menu` panel is anchored `position: absolute` and *would* clip, but a menu bar belongs to the desktop, not inside a window body.
|
|
216
218
|
- Grow box (if `resizable`): 15×15 at bottom-right corner, white bg, 1px black top/left borders, containing two overlapping small square outlines. Inactive: the cell and its borders stay, the nested squares go — System 7 drew a deactivated window's size box hollow, with its blanked scroll rails.
|
|
217
|
-
- Status bar (`status` slot): the classic bottom readout strip ("40px x 40px") — a 1px black rule over a white interior, `--vf-status-bar-height` (15px) in all, the grow box's own height, so a `resizable` window's grow box sits flush in the strip's right end (its top/left borders take over the rule there; the strip reserves 21px of right padding under `resizable` so text clears the cell). Slotted content rides the body face's native 12px line (`--vf-line-height`), whole-pixel centered in the 14px interior, `padding-inline: 6px`, nowrap + clipped. Takes **no space** until the slot is populated (the dialog-footer pattern), and collapses again when it empties. Composes with `scrollbars`: the edge rails' bottom overhang lands its frame line exactly on the strip's rule, so the two never double up — though the grow box then sits in the strip rather than the rail corner cell.
|
|
219
|
+
- Status bar (`status` slot): the classic bottom readout strip ("40px x 40px") — a 1px black rule (`vfRule`'s `.vf-rule-top`) over a white interior, `--vf-status-bar-height` (15px) in all, the grow box's own height, so a `resizable` window's grow box sits flush in the strip's right end (its top/left borders take over the rule there; the strip reserves 21px of right padding under `resizable` so text clears the cell). Slotted content rides the body face's native 12px line (`--vf-line-height`), whole-pixel centered in the 14px interior, `padding-inline: 6px`, nowrap + clipped. Takes **no space** until the slot is populated (the dialog-footer pattern), and collapses again when it empties. Composes with `scrollbars`: the edge rails' bottom overhang lands its frame line exactly on the strip's rule, so the two never double up — though the grow box then sits in the strip rather than the rail corner cell.
|
|
218
220
|
- Edge scroll rails (if `scrollbars`): the body slot renders inside a shadow `vf-scroll-area` (its `axis` = the attribute's value, `label` = the heading, `viewport` part re-exported) carrying the TeachText composition internally — `calc(100% + 2px·scale)` with `margin: -1px·scale`, one system px under the frame on every side, so the area's own frame border repaints the window's border lines and a `resizable` window's grow box (z-index 1) lands exactly over the rail-corner cell. Same caveats as the slotted composition (see vf-scroll-area §5), which remains supported for inset wells.
|
|
219
221
|
- Utility variant (`variant="utility"`): the slim windoid bar traced from `Windows/utility-window.png` — `--vf-titlebar-height-utility` (12px = 11px interior + 1px rule), the `vfDots` dither instead of stripes (flush to the side borders — see §4 vfDots), 7×7 widgets (`top: 2px`; close `left: 7px`, zoom `right: 8px` — the art is asymmetric by that pixel) with a 2px patch ring where the striped bar's is 1px (`--vf-widget-ring`, internal geometry: the windoid sheet clears two px of dither beside its widgets), and the nested zoom square shrunk so its edges land at sprite col/row 3. No title patch: the display face's 16px line box can't sit in an 11px interior, so `.vf-title` is `display: none` under the variant (a retheming consumer can re-show it via `::part(title)`) and the heading names the widgets. A pressed windoid widget inverts whole — black interior under a white (invisible) borderline — rather than flashing the big bar's 9×9 sunburst, which can't land on a 5×5 interior.
|
|
220
222
|
- **A11y:** the close/zoom `aria-label`s are qualified by the title when there is one (`Close ${heading}` / `Zoom ${heading}`, falling back to bare `Close` / `Zoom`) — several windows are open at once by design, so a bare repeated "Close" gives an AT user no way to tell which window a widget belongs to. The frame is `role="group"`, named by the title patch via `aria-labelledby` when there is a heading (the utility variant's hidden patch still names it — AccName resolves hidden labelledby targets) — `group` rather than `region` deliberately, so a desktop of windows doesn't pollute landmark navigation; the title bar is a `<div>`, never a `<header>`, which would map to an unnamed `banner` landmark even inside the shadow root. An inactive window's widgets stay in the tree and the tab order but paint no ink (transparent border/background/patch ring — the bare System 7 bar): a background window whose body holds nothing focusable is still reachable, activated by `vf-desktop`'s `focusin` raise the moment Tab lands on a widget, and never drops focus to `<body>` when it deactivates. `npm run verify:window-a11y`.
|
|
@@ -354,7 +356,7 @@ The classic popup menu control ("Macintosh HD ▼").
|
|
|
354
356
|
|
|
355
357
|
#### `vf-menu-bar` (`VfMenuBar`, vf-menu-bar.ts)
|
|
356
358
|
- **Attributes/props:** `label: string` — accessible name for the menubar, mirrored as host `aria-label` (guarded: a consumer's own `aria-label`/`aria-labelledby` is left alone). `rounded: boolean` (reflected) — draws the System 7 screen-corner mask over the bar's top-left/top-right corners. `shortcuts: boolean` (reflected) — makes every slotted menu's item `shortcut`s live key equivalents, one declaration for the whole bar the way MenuKey() answered for every menu at once (see vf-menu-item for the claim contract). Off by default: key equivalents are page-global, and only *the* menu bar should own them.
|
|
357
|
-
- **Visual:** `display: block/flex`, height `var(--vf-menubar-height, 20px)` — 19 white system px over the 1px black rule, the Menus.png bar strip exactly — white bg, `
|
|
359
|
+
- **Visual:** `display: block/flex`, height `var(--vf-menubar-height, 20px)` — 19 white system px over the 1px black rule, the Menus.png bar strip exactly — white bg, the rule `vfRule`'s `.vf-rule-bottom` on the bar (§4), children laid out horizontally from left. Adjacent titles are pulled 5px into each other (`margin-inline-start: -5px` on every slotted menu, absorbed by 14px of bar start padding — 9px of bar before the first plate, as System 7 placed the Apple title, plus the first title's 5px share): Menus.png spaces title ink 14px apart while each title's plate runs 10px left / 9px right of its own ink, so neighboring plates *overlap* by 5px, as the originals did. With `rounded`, two 5×5 system-px corner overlays paint the traced stair-step mask (per-row runs 5/3/2/1/1 — `SCREEN_CORNER` + `steppedCornerClip` in `pixel-frame.ts`) in `--vf-black` *over* the bar: on the classic screen the rounding was the ROM's black corner mask sitting on top of the menu bar, not a shape of the bar's own, so it is ink over any backdrop rather than a cutout.
|
|
358
360
|
- **Behavior:** container/controller for slotted `vf-menu` children. Pressing a menu label → opens it (label inverts while open). While any menu is open, hovering another label switches to it (classic behavior). Escape / outside click / item selection closes. `role="menubar"`, behind a first-connect ownership latch so a consumer's own role survives upgrade; the shadow `.bar` is `role="presentation"` and each slotted `vf-menu` host `role="none"`, so the `menubar → menuitem` ownership chain has no generics in it. While a menu is open: ArrowLeft/Right move between menus, ArrowDown/Up walk the open menu's items, Home/End jump to its first/last enabled item, and printable keys run the shared Finder first-letter type-ahead over the items (`src/type-ahead.ts`; Space stays the focused item's activation key, and the prefix resets on menu switch or close). The bar also **owns the press-drag-release gesture** across its menus (`MenuPressController`, `src/menu-press.ts` — see `vf-menu`), since one press may travel over several of them: it binds the opening `pointerdown` and hands the controller its own open/close rules, so the gesture changes *when* a menu opens, never *how*.
|
|
359
361
|
- **Slots:** default (vf-menu elements). **Parts:** `bar`.
|
|
360
362
|
|
|
@@ -428,10 +430,10 @@ The kit's layout primitive: a flexbox whose `gap`, `pad`, `width` and `height` a
|
|
|
428
430
|
- **Slots:** default (the children; `fill-width` / `fill-height` on any of them). **Parts:** none. **Events:** none.
|
|
429
431
|
|
|
430
432
|
#### `vf-container` (`VfContainer`, vf-container.ts)
|
|
431
|
-
A plain sized box: `width`/`height` in whole system px around a bare slot —
|
|
432
|
-
- **Attributes/props:** `width` / `height`: number (whole system px, via `VfSized`); the `top` / `left` pair via `VfPositioned` like nearly every component; `pattern?: string` — a library pattern by name or sixteen hex digits (docs/PATTERNS.md), painted as the box's background: unset paints nothing, an unrecognized value paints nothing and warns once. On a **child**: `fill-width` and `fill-height`, bare attributes as in `vf-stack`. That is the whole API.
|
|
433
|
+
A plain sized box: `width`/`height` in whole system px around a bare slot — nothing drawn but what `pattern` and `rule` name, no layout opinion. Explicit placement (src/position.ts) leaves one line of CSS the kit can't write for a consumer: children placed with `top`/`left` need a positioned ancestor, so a region of the consumer's *own* needs `position: relative` in a stylesheet. This component is that region as an element — a DITL's enclosing rectangle with nothing drawn in it.
|
|
434
|
+
- **Attributes/props:** `width` / `height`: number (whole system px, via `VfSized`); the `top` / `left` pair via `VfPositioned` like nearly every component; `pattern?: string` — a library pattern by name or sixteen hex digits (docs/PATTERNS.md), painted as the box's background: unset paints nothing, an unrecognized value paints nothing and warns once; `rule?: string` — edge names separated by spaces (`"bottom"`, `"top bottom"`, up to all four in any order), the 1px rule drawn on those edges as the box's own border inside the declared size: unset draws none, a value naming anything but an edge draws none and warns once. On a **child**: `fill-width` and `fill-height`, bare attributes as in `vf-stack`. That is the whole API.
|
|
433
435
|
- **Not a `vf-stack`.** The stack is a flexbox with opinions — an axis, a cross-axis default, fills compiled into flex. The container has none: in-flow children get normal flow, placed children get a coordinate system. Reach for it when the stack's opinions are the thing in the way — a field of placed icons, a fixed stage for placed art, a composition that brings its own layout with it.
|
|
434
|
-
- **Visual:** none unless `pattern` is set — no
|
|
436
|
+
- **Visual:** none unless `pattern` or `rule` is set — no role, keyboard behavior or selection; what it holds decides what it is. With `pattern`, the shadow box carries the fill as its own background (`vfPatternFill`: the `vf-pattern-fill` class always, `vf-patterned` while a pattern is resolved — *Patterns* under *Tiled fills*), riding the snap correction with the coordinate system; the raster is sized from the declared axes and measured on an undeclared one. With `rule`, the same box wears `vfRule`'s per-edge classes (§4): the rule is its border, inside the declared size (`box-sizing: border-box`), so a 24px `rule="bottom"` strip is 23 rows of box over one of ink — the menu bar's anatomy — and the padding box, which placed children anchor to and percentage fills resolve against, begins inside the rule. A pattern's paper reaches the rule and its phase anchors at the padding box, so it begins directly inside a top or left rule. `npm run verify:rule`. `:host` is a block with `width: fit-content`, so an undeclared axis shrink-wraps rather than claiming the parent's width (the stack's rule — a layout box must not hand out a size nobody declared; a declared dimension lands on the host's inline style and beats it). The slot sits in one shadow box coinciding with the host box (`.vf-snap`, `display: flow-root`, `height: 100%`): flow-root so a slotted margin cannot collapse through the top edge and push the coordinate origin off the host's corner, 100% so percentage fills resolve against a declared height. That box owns `position: relative` — the anchor for `top`/`left` children — the component's purpose. Content that outgrows the declared box overflows it rather than growing it.
|
|
435
437
|
- **Carries a `GridSnapController`.** A container's box is itself the consumer's coordinate system, including for non-`vf` content that cannot correct itself. The shadow box owns the positioning anchor and the `vf-snap` class *together*, so the correction moves the whole coordinate system and everything placed against it rides along, kit or not. A declared size and whole-px `top`/`left` are on the grid by construction; the controller covers the origin the page contributes. (This is the arrangement `vf-stack` has since adopted too — one shadow box owning anchor and correction together.)
|
|
436
438
|
- **Typographically transparent**, exactly as `vf-stack` and for the same reason: `font`, `-webkit-font-smoothing`, `color`, `user-select` and `text-align` return to `inherit`, so wrapping content in a sized box changes nothing about how that content reads.
|
|
437
439
|
- **`fill-width` / `fill-height`** are read about the host (`width`/`height: 100%`, for a parent that can give the box a size) and compiled for slotted children as the percentage form — normal flow has no flex axes to translate onto. `width: 100%` always binds against the box; `height: 100%` binds only against a declared `height` (percentage-against-auto computes to auto), so a fill with nothing to take is inert, not an error. A light-DOM declaration beats the `::slotted` rule, as everywhere.
|