vintage-frames 0.3.0 → 0.5.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.
- package/custom-elements.json +181 -32
- package/dist/components/vf-button.js +25 -25
- package/dist/components/vf-checkbox.js +15 -15
- package/dist/components/vf-container.d.ts +37 -3
- package/dist/components/vf-container.js +32 -10
- package/dist/components/vf-desktop.d.ts +103 -22
- package/dist/components/vf-desktop.js +100 -121
- package/dist/components/vf-dialog.d.ts +19 -15
- package/dist/components/vf-dialog.js +45 -53
- package/dist/components/vf-icon.js +56 -56
- package/dist/components/vf-list-item.js +16 -16
- package/dist/components/vf-list.js +26 -26
- package/dist/components/vf-menu-item.js +24 -24
- package/dist/components/vf-menu.js +27 -27
- package/dist/components/vf-number-field.js +17 -17
- package/dist/components/vf-progress-bar.js +3 -3
- package/dist/components/vf-radio-group.js +13 -13
- package/dist/components/vf-radio.js +16 -16
- package/dist/components/vf-select.js +27 -27
- package/dist/components/vf-slider.js +32 -32
- package/dist/components/vf-window.js +69 -69
- package/dist/index.d.ts +14 -0
- package/dist/index.js +73 -71
- package/dist/modal-dialog.js +21 -21
- package/dist/pattern-fill.d.ts +91 -0
- package/dist/pattern-fill.js +74 -0
- package/dist/patterns.d.ts +80 -0
- package/dist/patterns.js +477 -0
- package/dist/scroll-rail.js +37 -37
- package/dist/styles/recipes/pattern.d.ts +2 -12
- package/dist/styles/recipes/pattern.js +11 -15
- package/dist/styles/recipes/scroll-rail.d.ts +3 -12
- package/dist/styles/recipes/scroll-rail.js +7 -15
- package/dist/styles/recipes/surface.d.ts +18 -7
- package/dist/styles/recipes/surface.js +15 -0
- package/dist/text-control.js +11 -11
- package/dist/tile-grid.d.ts +4 -2
- package/dist/tile-grid.js +3 -3
- package/dist/toggle-control.js +5 -5
- package/dist/track-width.d.ts +7 -0
- package/dist/track-width.js +6 -3
- package/docs/SPEC.md +14 -10
- package/editor/vscode.html-custom-data.json +14 -4
- package/editor/web-types.json +46 -8
- package/package.json +3 -1
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import { tileImage as e, tileRects as t, vfTileMaskSize as n } from "./tile.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
1,
|
|
7
|
-
1
|
|
8
|
-
], [
|
|
9
|
-
2,
|
|
10
|
-
1,
|
|
11
|
-
1,
|
|
12
|
-
1
|
|
13
|
-
]], o = e(4, 2, t(a)), s = r`
|
|
2
|
+
import { PATTERNS as r, patternMotif as i } from "../../patterns.js";
|
|
3
|
+
import { css as a, unsafeCSS as o } from "lit";
|
|
4
|
+
//#region src/styles/recipes/scroll-rail.ts
|
|
5
|
+
var s = i(r["gray-25"]), c = s.width, l = s.height, u = s.rects, d = e(c, l, t(u)), f = a`
|
|
14
6
|
/* Hide the native bar, keep the native scrolling. Both lines ship:
|
|
15
7
|
scrollbar-width reaches Chromium 121+/Firefox/Safari 18.2+, the pseudo
|
|
16
8
|
covers older WebKit — the last scrollbar pseudo left in the kit. */
|
|
@@ -210,8 +202,8 @@ var a = [[
|
|
|
210
202
|
@media (forced-colors: active) {
|
|
211
203
|
.vf-rail-trough {
|
|
212
204
|
background-color: var(--vf-black, #000);
|
|
213
|
-
mask-image: ${
|
|
214
|
-
${n(
|
|
205
|
+
mask-image: ${o(d)};
|
|
206
|
+
${n(c, l)}
|
|
215
207
|
}
|
|
216
208
|
.vf-rail-trough-art {
|
|
217
209
|
display: none;
|
|
@@ -219,4 +211,4 @@ var a = [[
|
|
|
219
211
|
}
|
|
220
212
|
`;
|
|
221
213
|
//#endregion
|
|
222
|
-
export {
|
|
214
|
+
export { c as TROUGH_MOTIF_X, l as TROUGH_MOTIF_Y, u as TROUGH_RECTS, f as vfScrollRail };
|
|
@@ -4,14 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare const vfPanel: import("lit").CSSResult;
|
|
6
6
|
/**
|
|
7
|
-
* Chrome-frame recipe for the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Chrome-frame recipe for the desktop window (vf-window): the same white
|
|
8
|
+
* face, 1px black border and hard offset shadow as {@link vfPanel}. Apply
|
|
9
|
+
* `.vf-frame` to the outer frame element.
|
|
10
10
|
*
|
|
11
|
-
* Skin only —
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* the native `<dialog>` shrink-wraps.
|
|
11
|
+
* Skin only — the component adds its own layout (a full-size flex column: the
|
|
12
|
+
* body flexes, the title bar and grow box don't). vf-dialog does not use it:
|
|
13
|
+
* both of its chromes are {@link vfModalFrame}, which casts no shadow.
|
|
15
14
|
*/
|
|
16
15
|
export declare const vfChromeFrame: import("lit").CSSResult;
|
|
17
16
|
/**
|
|
@@ -24,5 +23,17 @@ export declare const vfChromeFrame: import("lit").CSSResult;
|
|
|
24
23
|
*
|
|
25
24
|
* Apply `.vf-modal-frame` to the outer element and `.vf-modal-frame-inner` to
|
|
26
25
|
* a wrapper inside it; content goes in the wrapper.
|
|
26
|
+
*
|
|
27
|
+
* **The movable modal (movableDBoxProc) is this same frame with the striped
|
|
28
|
+
* title bar set into it** — traced from a 2× capture of a System 7 movable
|
|
29
|
+
* modal: the standard 18px bar (`vfTitleBar`, 17-row interior, 3 white rows /
|
|
30
|
+
* six stripes / 3 white rows, the title patch's 7px margins) sits directly
|
|
31
|
+
* under the outer rule, inset 2px at either end so its stripes keep the same
|
|
32
|
+
* 2px of white from the outer rule that the inner band keeps; the bar's own
|
|
33
|
+
* 1px rule plus the inner box's 1px top border make the 2px band its floor,
|
|
34
|
+
* which then continues down both sides and along the bottom as the band
|
|
35
|
+
* around the body. No shadow anywhere. Put the `.vf-title-bar` between the
|
|
36
|
+
* two elements, as the first child of `.vf-modal-frame`, and the three rules
|
|
37
|
+
* below do the rest.
|
|
27
38
|
*/
|
|
28
39
|
export declare const vfModalFrame: import("lit").CSSResult;
|
|
@@ -22,6 +22,21 @@ var r = n(`
|
|
|
22
22
|
margin: calc(var(--vf-scale, 1) * 2px);
|
|
23
23
|
border: calc(var(--vf-scale, 1) * 2px) solid var(--vf-black, #000);
|
|
24
24
|
}
|
|
25
|
+
/* The bar set into the frame: 2px in from the outer rule at either end, its
|
|
26
|
+
stripes flush to the bar's own edge (vfStripes' 1px side inset is the
|
|
27
|
+
window frame's buffer — here the frame's gap is that buffer). */
|
|
28
|
+
.vf-modal-frame > .vf-title-bar {
|
|
29
|
+
margin: 0 calc(var(--vf-scale, 1) * 2px);
|
|
30
|
+
}
|
|
31
|
+
.vf-modal-frame > .vf-title-bar .vf-stripes {
|
|
32
|
+
inset: calc(var(--vf-scale, 1) * 3px) 0;
|
|
33
|
+
}
|
|
34
|
+
/* Under a bar, the inner box's top border is the second row of the band —
|
|
35
|
+
the bar's 1px rule is the first — and there is no gap row between. */
|
|
36
|
+
.vf-modal-frame > .vf-title-bar + .vf-modal-frame-inner {
|
|
37
|
+
margin-top: 0;
|
|
38
|
+
border-top-width: calc(var(--vf-scale, 1) * 1px);
|
|
39
|
+
}
|
|
25
40
|
`;
|
|
26
41
|
//#endregion
|
|
27
42
|
export { a as vfChromeFrame, o as vfModalFrame, i as vfPanel };
|
package/dist/text-control.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
1
|
+
import { emit as e, emitNative as t } from "./events.js";
|
|
2
|
+
import { ScaleController as n } from "./scale.js";
|
|
3
|
+
import r from "./_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
|
|
4
|
+
import { GridSnapController as i } from "./grid-snap.js";
|
|
5
5
|
import { VfShadowRoleControl as a } from "./form-control.js";
|
|
6
6
|
import { FocusRuleController as o } from "./focus-modality.js";
|
|
7
7
|
import { nothing as s } from "lit";
|
|
8
8
|
import { property as c } from "lit/decorators.js";
|
|
9
9
|
//#region src/text-control.ts
|
|
10
10
|
var l = class extends a {
|
|
11
|
-
constructor(...
|
|
12
|
-
super(...
|
|
11
|
+
constructor(...e) {
|
|
12
|
+
super(...e), this.value = "", this.placeholder = "", this.readonly = !1, this.name = "", this.label = "", this.scale = new n(this), this.gridSnap = new i(this), this.focusRule = new o(this);
|
|
13
13
|
}
|
|
14
14
|
static {
|
|
15
15
|
this.shadowRootOptions = {
|
|
@@ -65,19 +65,19 @@ var l = class extends a {
|
|
|
65
65
|
}
|
|
66
66
|
e.requestSubmit();
|
|
67
67
|
}
|
|
68
|
-
emitValue(
|
|
69
|
-
|
|
68
|
+
emitValue(t, n = { value: this.value }) {
|
|
69
|
+
e(this, t, n);
|
|
70
70
|
}
|
|
71
71
|
handleInput(e) {
|
|
72
72
|
this.value = e.target.value, this.emitValue("vf-input");
|
|
73
73
|
}
|
|
74
74
|
handleChange(e) {
|
|
75
|
-
this.value = e.target.value, this.emitValue("vf-change"),
|
|
75
|
+
this.value = e.target.value, this.emitValue("vf-change"), t(this, "change");
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
|
-
|
|
78
|
+
r([c()], l.prototype, "value", void 0), r([c()], l.prototype, "placeholder", void 0), r([c({
|
|
79
79
|
type: Boolean,
|
|
80
80
|
reflect: !0
|
|
81
|
-
})], l.prototype, "readonly", void 0),
|
|
81
|
+
})], l.prototype, "readonly", void 0), r([c({ reflect: !0 })], l.prototype, "name", void 0), r([c()], l.prototype, "label", void 0);
|
|
82
82
|
//#endregion
|
|
83
83
|
export { l as VfTextControlBase };
|
package/dist/tile-grid.d.ts
CHANGED
|
@@ -103,9 +103,11 @@ export declare function patternOverride(el: Element, token: string): string;
|
|
|
103
103
|
* A one-entry cache for a surface's whole-surface raster, keyed by its
|
|
104
104
|
* system-px size — so every render with an unchanged size (all of them except
|
|
105
105
|
* a declared-size change) reuses the encoded image instead of re-running
|
|
106
|
-
* `tileRaster`'s canvas encode.
|
|
106
|
+
* `tileRaster`'s canvas encode. A surface whose *art* can change under the
|
|
107
|
+
* same size (a `pattern` attribute, src/pattern-fill.ts) passes an `id` for
|
|
108
|
+
* it — the fixed-motif surfaces leave it empty.
|
|
107
109
|
*/
|
|
108
110
|
export declare class TileRasterCache {
|
|
109
111
|
#private;
|
|
110
|
-
for(motifWidth: number, motifHeight: number, rects: readonly TileRect[], width: number, height: number): string;
|
|
112
|
+
for(motifWidth: number, motifHeight: number, rects: readonly TileRect[], width: number, height: number, id?: string): string;
|
|
111
113
|
}
|
package/dist/tile-grid.js
CHANGED
|
@@ -44,9 +44,9 @@ function o(e, t) {
|
|
|
44
44
|
var s = class {
|
|
45
45
|
#e = "";
|
|
46
46
|
#t = "";
|
|
47
|
-
for(e, n, r, i, a) {
|
|
48
|
-
let
|
|
49
|
-
return this.#e !==
|
|
47
|
+
for(e, n, r, i, a, o = "") {
|
|
48
|
+
let s = `${o}|${i}x${a}`;
|
|
49
|
+
return this.#e !== s && (this.#e = s, this.#t = t(e, n, r, i, a)), this.#t;
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
52
|
//#endregion
|
package/dist/toggle-control.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { deferActivation as e } from "./events.js";
|
|
2
|
+
import { ScaleController as t } from "./scale.js";
|
|
3
|
+
import { GridSnapController as n } from "./grid-snap.js";
|
|
4
4
|
//#region src/toggle-control.ts
|
|
5
5
|
var r = (r) => {
|
|
6
6
|
class i extends r {
|
|
@@ -9,8 +9,8 @@ var r = (r) => {
|
|
|
9
9
|
}
|
|
10
10
|
#e;
|
|
11
11
|
constructor(...r) {
|
|
12
|
-
super(...r), this.scale = new
|
|
13
|
-
|
|
12
|
+
super(...r), this.scale = new t(this), this.gridSnap = new n(this), this.#e = null, this.selfManagedTabIndex = !1, this.#n = (t) => {
|
|
13
|
+
e(this, t, () => this.#t());
|
|
14
14
|
}, this.#r = (e) => {
|
|
15
15
|
e.key === " " && (e.preventDefault(), !e.repeat && this.dispatchEvent(new MouseEvent("click", {
|
|
16
16
|
bubbles: !0,
|
package/dist/track-width.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
|
20
20
|
* why re-observing is skipped when the element hasn't changed: `observe()` on
|
|
21
21
|
* an already-observed element re-fires the initial callback, and `hostUpdated`
|
|
22
22
|
* runs on every update.
|
|
23
|
+
*
|
|
24
|
+
* The height comes along from the same observation, for a box measured on
|
|
25
|
+
* both axes: `PatternFillController` (src/pattern-fill.ts) sizes a pattern
|
|
26
|
+
* raster to a box whose extent nobody declared.
|
|
23
27
|
*/
|
|
24
28
|
export declare class TrackWidthController implements ReactiveController {
|
|
25
29
|
private readonly host;
|
|
@@ -27,6 +31,7 @@ export declare class TrackWidthController implements ReactiveController {
|
|
|
27
31
|
private resizeObserver?;
|
|
28
32
|
private observed;
|
|
29
33
|
private measured;
|
|
34
|
+
private measuredHeight;
|
|
30
35
|
constructor(host: ReactiveControllerHost, getTrack: () => HTMLElement | null | undefined);
|
|
31
36
|
/**
|
|
32
37
|
* Content width of the track in CSS px (already scaled — convert with
|
|
@@ -34,6 +39,8 @@ export declare class TrackWidthController implements ReactiveController {
|
|
|
34
39
|
* each component treats as "not measured yet" and falls back from.
|
|
35
40
|
*/
|
|
36
41
|
get width(): number;
|
|
42
|
+
/** Content height of the track in CSS px, on the same terms as {@link width}. */
|
|
43
|
+
get height(): number;
|
|
37
44
|
hostConnected(): void;
|
|
38
45
|
hostUpdated(): void;
|
|
39
46
|
hostDisconnected(): void;
|
package/dist/track-width.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
//#region src/track-width.ts
|
|
2
2
|
var e = class {
|
|
3
3
|
constructor(e, t) {
|
|
4
|
-
this.host = e, this.getTrack = t, this.observed = null, this.measured = 0, e.addController(this);
|
|
4
|
+
this.host = e, this.getTrack = t, this.observed = null, this.measured = 0, this.measuredHeight = 0, e.addController(this);
|
|
5
5
|
}
|
|
6
6
|
get width() {
|
|
7
7
|
return this.measured;
|
|
8
8
|
}
|
|
9
|
+
get height() {
|
|
10
|
+
return this.measuredHeight;
|
|
11
|
+
}
|
|
9
12
|
hostConnected() {
|
|
10
13
|
this.wire();
|
|
11
14
|
}
|
|
@@ -21,8 +24,8 @@ var e = class {
|
|
|
21
24
|
!e || e === this.observed || (this.resizeObserver ??= new ResizeObserver((e) => {
|
|
22
25
|
let t = e[0];
|
|
23
26
|
if (!t) return;
|
|
24
|
-
let n = Math.floor(t.contentRect.width);
|
|
25
|
-
n !== this.measured && (this.measured = n, this.host.requestUpdate());
|
|
27
|
+
let n = Math.floor(t.contentRect.width), r = Math.floor(t.contentRect.height);
|
|
28
|
+
(n !== this.measured || r !== this.measuredHeight) && (this.measured = n, this.measuredHeight = r, this.host.requestUpdate());
|
|
26
29
|
}), this.resizeObserver.observe(e), this.observed = e);
|
|
27
30
|
}
|
|
28
31
|
};
|
package/docs/SPEC.md
CHANGED
|
@@ -133,6 +133,8 @@ The span construction (`vfTileSize`, `tileImage`, `tileSpan`) makes the stored l
|
|
|
133
133
|
|
|
134
134
|
The span construction stays load-bearing for the CSS-repeated underlays beneath the opaque kit fills and for the forced-colors mask branches (no mask pipeline rasterizes exactly at a zoom-minted scale, so forced-colors-plus-zoom remains the one accepted residual). The scroll trough — once the one surface that could not convert, a `::-webkit-scrollbar` pseudo hosting no children — is ordinary DOM now (§4 `vfScrollRail`) and renders through `tileRaster` like the desktop dither: 1-bit at every scale, zoom-minted ones included. `npm run verify:tile` asserts zero gray on the four converted surfaces at eight densities — the ladder plus 1.7 and 2.3, the emulated stand-ins for Safari's broken rungs; `npm run verify:scrollbars` asserts the same on the trough.
|
|
135
135
|
|
|
136
|
+
**Patterns.** The 38 standard patterns (`src/patterns.ts`; the table is docs/PATTERNS.md) are 8-byte QuickDraw `Pattern`s in palette order and QuickDraw phase. `gray-50`, `gray-25` and `dots` are the desktop dither, the scroll trough and the windoid bar's motifs, and those surfaces draw them by reference — `patternMotif` on the minimal cell, so the 2×2 / 4×2 rect data and the 30/60-px token spans are the ones they always had. `vf-container pattern` and `vf-desktop pattern` take a name or sixteen hex digits and paint the pattern as the box's **own background** (`PatternFillController` + `vfPatternFill`, src/pattern-fill.ts): the same whole-surface raster, its `background-size` stated in system px — one length quantized once over the whole image — under `image-rendering: pixelated`, literal black ink on the `--vf-white` paper the recipe supplies. A background rather than a child element, so a patterned box is no stacking context (a slotted control's fixed panel is never trapped under a later sibling) and keeps `overflow: visible`; `image-rendering` inherits, so the recipe hands `auto` back to the slot. A declared axis sizes the raster exactly (plus one cell of overdraw the painting area crops); an undeclared one is measured through `TrackWidthController`. Phase is anchored at the box's own origin. Forced colors: flat Canvas, the desktop's posture. `npm run verify:pattern` asserts the library, the three motif equivalences and zero gray at the same eight densities.
|
|
137
|
+
|
|
136
138
|
**`--vf-cursor` (hiding the pointer for a page-drawn cursor).** A page that draws its own cursor — a JS-positioned image on the system-pixel grid, which is what `applyCursor()` (`src/cursor.ts`) sets up with the kit's embedded System 7 pointer set — needs the native pointer gone from every surface, and `html { cursor: none }` alone cannot get it there: inheritance stops wherever a shadow stylesheet states a cursor of its own, and the kit states one deliberately on its chrome — control hosts, the title-bar widgets, the number stepper, the scrollbar rails, the grow box, the editable wells (whose I-beam Firefox's UA sheet would otherwise pin), and a modal's top-layer backdrop, which no page rule reaches at all. Every one of those declarations reads this token first, so `--vf-cursor: none` on `:root` (custom properties inherit through every shadow root) covers all of them. The page's own side must be a blanket, not a root rule: the UA sheets put `cursor: default` on `<button>` and `cursor: text` on `<input>`, and an element's own declaration beats anything it would inherit — so a bare `html { cursor: none }` leaves the OS arrow alive over every native control in the light DOM. The pair that empties everything is `* { cursor: none !important }` plus the token — `applyCursor()` applies both itself, once its art has decoded; state the pair by hand only under a hand-rolled overlay. Unset, each spot keeps its classic pointer — the arrow on chrome, the I-beam in an enabled well.
|
|
137
139
|
|
|
138
140
|
## 4. Shared recipes (in `src/styles/base.ts`)
|
|
@@ -145,8 +147,8 @@ The span construction stays load-bearing for the CSS-repeated underlays beneath
|
|
|
145
147
|
- `vfTileSize` / `tileImage` — the span a CSS-REPEATING fill takes, and its art. Every metric in the kit is `calc(var(--vf-scale, 1) * Npx)`, and paint snaps each box to the device grid on its own — but a tiled fill is one snapped box holding N *unsnapped* repeats, each placed at `k × tileSize`, so a tile size the layout grid cannot hold drifts a fraction of a device pixel further with every repeat. Load-bearing for the underlays and the forced-colors masks; the converted surfaces (the scroll trough now among them) render through `vfTileGrid` / `tileGrid` / `tileRaster` instead. See *Tiled fills* below.
|
|
146
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).
|
|
147
149
|
- `vfPanel` — a `.vf-panel` class for menus/popups: white bg, `border: 1px solid var(--vf-black, #000)`, `vfHardShadowDecls`.
|
|
148
|
-
- `vfChromeFrame` — a `.vf-frame` class for the
|
|
149
|
-
- `vfModalFrame` — the dBoxProc modal-dialog frame
|
|
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
|
+
- `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.
|
|
150
152
|
- `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.
|
|
151
153
|
- `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.
|
|
152
154
|
- Title geometry is whole system px, traced from the InfiniteMac reference: the face's 16px line box on interior rows 1..16 (`align-items: flex-start` + 1px top margin, `line-height: var(--vf-line-height-display, 16px)`) lands the 9px cap band on rows 4..12 — 4px of white above and below — and 6px padding plus the letters' 1px side bearing leaves 7px of white between ink and stripes. Neither axis is flex-centered onto a fraction: vertically the row is stated, horizontally the flex-centered patch is held on the placement lattice by `TitleCenterController` (src/chrome.ts), which cancels the parity half-pixel through the controller-owned `--vf-title-dx`.
|
|
@@ -191,13 +193,15 @@ Every recipe also declares its size — `width`, and `height` where the window i
|
|
|
191
193
|
|
|
192
194
|
#### `vf-desktop` (`VfDesktop`, vf-desktop.ts)
|
|
193
195
|
Full-bleed classic desktop container.
|
|
194
|
-
- **Attributes/props:** `width`, `height: number` (system px, defaults 512×342 — the compact Mac's raster) — the screen's own size, the way a WIND resource declared a window's. The host box renders at exactly `width + 2·bezel` per axis (a live `sysLength` written as host inline size on every update), always a whole number of system px. **Pure CSS sizing is not supported**: the inline size wins over any stylesheet, so the page sets the numbers — directly or via `fitWithin` — and positions the explicitly sized desktop with its own CSS, keeping any sub-system-pixel slack in the page's own layout. `bezel: number` (system px, default 0) — the black screen surround, the CRT's unlit margin between raster and case, added onto the declared screen on every side. The screen owns flow, an absolute window's containing block and the drag clip, so a dragged window crops at the raster's edge. Its two *top* corners wear the `SCREEN_CORNER` mask (the classic framebuffer rounded only the top pair; the raster's bottom corners ran square), painted above everything like the hardware mask — the masks land over a slotted menu bar's corners, so the bar needs no `rounded` of its own inside a bezeled desktop. The bezel width is written onto the host as `--vf-desktop-bezel` (self-set geometry, like `--vf-scale` — component-owned, not a theming token).
|
|
195
|
-
- **Methods:** `fitWithin(maxWidth, maxHeight)` (CSS px) → sets `width`/ `height` to the largest whole-system-px raster whose host box — bezel included — fits the bound, per the current effective scale, and returns `{ width, height }`. The page's half of the sizing contract: call it on `resize` and `onScaleChange` (zoom and density moves change what a system px costs in CSS px), as a full-screen faux desktop does.
|
|
196
|
-
- **Visual:** `display: block; position: relative;` — the paint lives on an inner screen surface (part `desktop`, `overflow: hidden` — the whole-system-px raster, inset by `bezel` when one is set). Screen surface = classic 50% dither
|
|
196
|
+
- **Attributes/props:** `width`, `height: number` (system px, defaults 512×342 — the compact Mac's raster) — the screen's own size, the way a WIND resource declared a window's. The host box renders at exactly `width + 2·bezel` per axis (a live `sysLength` written as host inline size on every update), always a whole number of system px. **Pure CSS sizing is not supported**: the inline size wins over any stylesheet, so the page sets the numbers — directly or via `fitWithin` — and positions the explicitly sized desktop with its own CSS, keeping any sub-system-pixel slack in the page's own layout. `bezel: number` (system px, default 0) — the black screen surround, the CRT's unlit margin between raster and case, added onto the declared screen on every side. The screen owns flow, an absolute window's containing block and the drag clip, so a dragged window crops at the raster's edge. Its two *top* corners wear the `SCREEN_CORNER` mask (the classic framebuffer rounded only the top pair; the raster's bottom corners ran square), painted above everything like the hardware mask — the masks land over a slotted menu bar's corners, so the bar needs no `rounded` of its own inside a bezeled desktop. The bezel width is written onto the host as `--vf-desktop-bezel` (self-set geometry, like `--vf-scale` — component-owned, not a theming token). `pattern: string` (default `'gray-50'`) — the desktop pattern, System 7's General Controls setting: a library name (docs/PATTERNS.md) or sixteen hex digits stating a custom 8×8; an unrecognized value warns once and keeps the dither. A `--vf-desktop-pattern` token override wins over it.
|
|
197
|
+
- **Methods:** `fitWithin(maxWidth, maxHeight)` (CSS px) → sets `width`/ `height` to the largest whole-system-px raster whose host box — bezel included — fits the bound, per the current effective scale, and returns `{ width, height }`. The page's half of the sizing contract: call it on `resize` and `onScaleChange` (zoom and density moves change what a system px costs in CSS px), as a full-screen faux desktop does. `clearActive()` → deactivate the whole document tier (see *Deactivation* under Behavior). `activeWindow: HTMLElement | null` (read-only) → the active document-tier window, or `null` while the tier is deactivated or empty; utility windows are never the holder.
|
|
198
|
+
- **Visual:** `display: block; position: relative;` — the paint lives on an inner screen surface (part `desktop`, `overflow: hidden` — the whole-system-px raster, inset by `bezel` when one is set). Screen surface = the `pattern` (the classic 50% dither, `gray-50`, by default), painted as the screen's own background by the pattern fill (*Patterns* under *Tiled fills*): black ink on the fill's opaque `--vf-white` paper, one whole-surface raster — the authentic black-on-white dither. A consumer `--vf-desktop-pattern` renders instead as a placed tile grid on the token's 30-system-px tile with no paper, so `var(--vf-desktop, #808080)` beneath shows only under a custom tile with transparent cells (or `none`). Forced colors: flat Canvas.
|
|
197
199
|
- **Slots:** default (menu bar, windows, anything).
|
|
198
200
|
- **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`.
|
|
199
201
|
- **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`.
|
|
200
202
|
- **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.
|
|
203
|
+
- **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
|
+
- **Events:** `vf-activate` (detail `{ window: HTMLElement | null }` — the new holder, `null` on deactivation).
|
|
201
205
|
- **Parts:** `desktop`.
|
|
202
206
|
|
|
203
207
|
#### `vf-window` (`VfWindow`, vf-window.ts)
|
|
@@ -220,12 +224,12 @@ The desktop-window shell: the classic document window (see DragThing screenshot)
|
|
|
220
224
|
- **Events:** `vf-close`, `vf-zoom` (detail `{}`), `vf-resize` (detail `{ width, height, commit }`).
|
|
221
225
|
|
|
222
226
|
#### `vf-dialog` (`VfDialog`, vf-dialog.ts)
|
|
223
|
-
The modal-dialog shell: movable modal by default (
|
|
227
|
+
The modal-dialog shell: movable modal by default (the dBoxProc double frame with the striped title bar set into it — traced from a 2× System 7 capture of a movable modal), the modal dialog box with `frame="plain"` (the bare double frame; see the Group A recipe table).
|
|
224
228
|
- **Attributes/props:** `open: boolean` (reflect), `heading: string`, `top: number` / `left: number` (whole system px, in **viewport** coordinates — see §1 Explicit placement; unset means centered), `width: number` / `height: number` (**declare them both** — whole system px, the same fixed box `vf-window` is. A native `<dialog>` is `width: fit-content` measured against the space left beside its own offsets, and stating an offset is how the movable modal is positioned, so an undeclared dialog squeezes itself and reflows as it is dragged toward an edge. The two fall back differently — width to 260 system px, height to the content — and it names whichever are missing, once, on the open that first shows it), `label: string` (accessible name for a dialog with no `heading`), `closable: boolean` (default **false** — the bare movable-modal bar; the close box is opt-in because the HIG's Chapter 6 text denies a movable modal one while its Figure 5-1 grants it — the parameter enables either reading), `frame?: 'plain'`.
|
|
225
229
|
- **Implementation:** wraps a native `<dialog>` (for top-layer + focus trap). `show()` → `showModal()`; `close()` closes. Keep `open` attr in sync both directions. Drag the title bar to move it (shared `DragController` with `vf-window`), stating `top`/`left` in system px; drags starting on the close widget are ignored (same composedPath guard as `vf-window`). Escape → close + `vf-close` detail `{ reason: 'escape' }`; close box/programmatic/close() → `{ reason: 'close' }`. No backdrop dimming: `::backdrop { background: transparent; }`. **Removal while open is a close path** (`VfModalDialog.disconnectedCallback`): HTML's dialog removing steps skip the close algorithm entirely, which is exactly what a framework unmount does — the teardown routes through the same native-`close` funnel (`vf-close` fires on the removed element; nothing bubbles, it left the tree), `open` and the written origin reconcile so a re-append mounts closed, and focus returns to the element focused at open time. **The placement re-settles while open** whenever the dialog's own box resizes — slotted content upgrading after `showModal()`, `--vf-scale` moving under zoom — the viewport resizes, or the scale changes. What survives depends on how the modal got where it is: an unplaced one re-centers (it never claimed a spot, and the stranded case is exactly this one), while a dragged or authored origin is kept and only re-clamped on screen.
|
|
226
|
-
- **Visual (default chrome):** `
|
|
230
|
+
- **Visual (default chrome):** `vfModalFrame` with `vfTitleBar` set into it (§4) — the bar is literally the recipe `vf-window` uses, so it is identical by construction (stripes + centered title) rather than by matching copies; the frame is the modal double frame, **not** the window's shadowed `vfChromeFrame`. Outside → in: 1px outer rule; the 18px bar directly under it, 2px in at either end with its stripes starting 2px from the rule; the 2px band as the bar's floor (the bar's rule + the inner box's 1px top border); 2px gap + 2px band down the sides and along the bottom; no shadow. The body begins 20px below the dialog's top and 5px inside its other three edges. The bar takes the default `--vf-title-inset` (16px) — 60px when `closable`, the same clearance as `vf-window`, since the centered title needs symmetric room for the widget — and sets `touch-action: none` unconditionally, having no immovable state. `closable` renders the shared close box (`vfWindowWidgets` + `closeBox()` — byte-identical to `vf-window`'s, per the `moveable modal dialog.png` reference). Body is WHITE (`--vf-surface: #fff`), `padding: 16px`. An optional `buttons` slot renders a bottom-right `vf-button-group` footer that only takes space when populated (equal-width, faces aligned). Both chromes are full-height flex columns and the body takes the slack, for the same reason `vf-window`'s frame is: the declared `height` lands on the `<dialog>` (see `dialogSize`), and the recipes are skin only. The frame is the flex child of the `<dialog>` itself (`dialog[open]` is a flex column in `modalDialogStyles` — `[open]`-scoped, or it would out-cascade the UA's `dialog:not([open]) { display: none }`), not a `height: 100%` block: a percentage can't resolve against the undeclared-height dialog that only the UA's `dialog:modal` max-height caps, and that spill was how a viewport-tall modal stranded its buttons off-screen. **The box never grows, but over-stuffed content scrolls instead of clipping**: the body is a flex column of a `.content` scroll region (heading + default slot; part `content`) over the pinned footer. While the content fits, nothing matches — rendering is pixel-identical to the old block flow. Once it overflows (`ScrollStateController`, the always-a-rail machinery), the region reserves the 16px channel as its own right padding, wears the drawn `vfScrollRail` rail (§4) boxed by a 1px `.scroll-frame` overlay, and becomes a keyboard stop (`tabindex="0"`, `role="group"`, the kit's dotted ring) so the copy is scrollable without a pointer. The rail rides the wrapper as an overlay pinned to its right edge, deliberately out of the layout flow: a rail *column*'s two fixed 15px arrow cells would hand the region a 32px minimum height, and a short dialog would then measure as fitting with the rail shown and overflowing without it — flip-flopping forever. The drop-open exemption is unchanged.
|
|
227
231
|
- **The CSS and the controller state the same thing.** `.content` is `overflow-y: hidden` until the controller flags overflow, then `overflow-y: scroll`. It is deliberately never `auto`: the controller ignores the body face's negative half-leading (`LEADING_SPILL_SYS`, src/scroll-state.ts — `vf-paragraph` sets a 12-system-px line box under a 16-system-px em, so the inline box spills 2 inkless system px past the block box and `scrollHeight` counts it), and `auto` has no way to know that. With `auto` the two disagreed, and a fixed info dialog whose copy ends in a `vf-paragraph` rubber-banded under the wheel with no rail shown — 6 CSS px at scale 3. `hidden` still scrolls programmatically, so `scrollIntoView` on a focused control is unaffected. `npm run verify:contract`, OVERFLOW group.
|
|
228
|
-
- **Visual (`frame="plain"`):** `vfModalFrame` (§4 — 1px outer, 2px gap, 2px inner band, no shadow, per `Windows/modal dialog.png`)
|
|
232
|
+
- **Visual (`frame="plain"`):** the same `vfModalFrame` (§4 — 1px outer, 2px gap, 2px inner band, no shadow, per `Windows/modal dialog.png`) with no title bar, and immovable like the original dBoxProc dialog (nothing renders a drag handle). The body begins 5px inside every edge. A `heading` renders as a centered display-face heading at the top of the body (`margin-bottom: 16px`) — the way those dialogs drew their title in content — and `closable` is ignored, there being no bar to carry the widget.
|
|
229
233
|
- **A11y:** named by its own title patch — or, on the plain frame, its body-top heading; both carry `id="title"` — via `aria-labelledby` when `heading` is set. With no heading there is nothing to point at — `aria-labelledby` would resolve to an empty node and leave the dialog unnamed — so it falls back to `aria-label`, taking `label` if given and otherwise `'Dialog'`. An explicit `label` wins over `heading`. The close box is labeled `Close ${heading}` like `vf-window`'s.
|
|
230
234
|
- **Slots:** default, `buttons`.
|
|
231
235
|
- **Parts:** `frame`, `title-bar` (default chrome), `title`, `close-box` (when `closable`), `body`, `content`, `footer`, `buttons`.
|
|
@@ -425,9 +429,9 @@ The kit's layout primitive: a flexbox whose `gap`, `pad`, `width` and `height` a
|
|
|
425
429
|
|
|
426
430
|
#### `vf-container` (`VfContainer`, vf-container.ts)
|
|
427
431
|
A plain sized box: `width`/`height` in whole system px around a bare slot — no paint, 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.
|
|
428
|
-
- **Attributes/props:** `width` / `height`: number (whole system px, via `VfSized`); the `top` / `left` pair via `VfPositioned` like nearly every component. On a **child**: `fill-width` and `fill-height`, bare attributes as in `vf-stack`. That is the whole API.
|
|
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.
|
|
429
433
|
- **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.
|
|
430
|
-
- **Visual:** none — no border,
|
|
434
|
+
- **Visual:** none unless `pattern` is set — no border, 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. `: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.
|
|
431
435
|
- **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.)
|
|
432
436
|
- **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.
|
|
433
437
|
- **`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.
|
|
@@ -154,8 +154,13 @@
|
|
|
154
154
|
},
|
|
155
155
|
{
|
|
156
156
|
"name": "vf-container",
|
|
157
|
-
"description": "`<vf-container>` — a box that is nothing but its declared size.\n\nThe kit's positioned-placement story (src/position.ts) ends with one line of\nCSS it can't write for you: children placed with `top`/`left` need a\npositioned ancestor, and while every kit container is one — a window body, a\nstack, a fieldset — a region of *your own* needs `position: relative` in a\nstylesheet. This component is that region as an element: declare `width` and\n`height` in whole system px, slot anything into it, place children against\nits origin. A DITL's enclosing rectangle, with nothing drawn in it.\n\n```html\n<vf-container width=\"200\" height=\"120\">\n <vf-icon label=\"System\" width=\"64\" top=\"8\" left=\"12\">…</vf-icon>\n <vf-icon label=\"Finder\" width=\"64\" top=\"8\" left=\"104\">…</vf-icon>\n</vf-container>\n```\n\nThe rectangle is the whole API — `width`/`height` here, plus the `top`/`left`\npair nearly every component takes (VfPositioned), so a container is\nitself placeable: inside a window, a desktop, or another container, at whole\nsystem px that keep its box on the device-pixel grid by construction.\n\n**It is not a `vf-stack`.** The stack is a flexbox with opinions — it\ndistributes children along an axis, compiles `fill-width`/`fill-height` into\nflex, defaults a cross-axis alignment. This box has no layout opinion at\nall: in-flow children get normal flow, placed children get a coordinate\nsystem, and that is the whole API. Reach for it when the stack's opinions\nare the thing in the way — a field of placed icons, a fixed stage for\nabsolutely-positioned art, a consumer's own composition that brings its\nlayout with it.\n\n**The declared size is the layout.** `width`/`height` land on the host as a\nlive `calc(var(--vf-scale, 1) * Npx)` (VfSized), so the box scales\nwith the display and sits on the device-pixel grid by construction. Content\nthat outgrows the box overflows it rather than growing it — the number is\nthe layout, and content that doesn't fit is a number to raise. Leave a\ndimension off and that axis shrink-wraps: `fit-content`, not the parent's\nwidth, because a layout box that silently claimed a size nobody declared\nwould be inventing one (the `vf-stack` rule, held here too).\n\n**It paints nothing and means nothing
|
|
157
|
+
"description": "`<vf-container>` — a box that is nothing but its declared size.\n\nThe kit's positioned-placement story (src/position.ts) ends with one line of\nCSS it can't write for you: children placed with `top`/`left` need a\npositioned ancestor, and while every kit container is one — a window body, a\nstack, a fieldset — a region of *your own* needs `position: relative` in a\nstylesheet. This component is that region as an element: declare `width` and\n`height` in whole system px, slot anything into it, place children against\nits origin. A DITL's enclosing rectangle, with nothing drawn in it.\n\n```html\n<vf-container width=\"200\" height=\"120\">\n <vf-icon label=\"System\" width=\"64\" top=\"8\" left=\"12\">…</vf-icon>\n <vf-icon label=\"Finder\" width=\"64\" top=\"8\" left=\"104\">…</vf-icon>\n</vf-container>\n```\n\nThe rectangle is the whole API — `width`/`height` here, plus the `top`/`left`\npair nearly every component takes (VfPositioned), so a container is\nitself placeable: inside a window, a desktop, or another container, at whole\nsystem px that keep its box on the device-pixel grid by construction.\n\n**It is not a `vf-stack`.** The stack is a flexbox with opinions — it\ndistributes children along an axis, compiles `fill-width`/`fill-height` into\nflex, defaults a cross-axis alignment. This box has no layout opinion at\nall: in-flow children get normal flow, placed children get a coordinate\nsystem, and that is the whole API. Reach for it when the stack's opinions\nare the thing in the way — a field of placed icons, a fixed stage for\nabsolutely-positioned art, a consumer's own composition that brings its\nlayout with it.\n\n**The declared size is the layout.** `width`/`height` land on the host as a\nlive `calc(var(--vf-scale, 1) * Npx)` (VfSized), so the box scales\nwith the display and sits on the device-pixel grid by construction. Content\nthat outgrows the box overflows it rather than growing it — the number is\nthe layout, and content that doesn't fit is a number to raise. Leave a\ndimension off and that axis shrink-wraps: `fit-content`, not the parent's\nwidth, because a layout box that silently claimed a size nobody declared\nwould be inventing one (the `vf-stack` rule, held here too).\n\n**It paints nothing and means nothing** — unless `pattern` says what to\npaint. No border, role, keyboard behavior or selection; what it holds\ndecides what it is.\n\n**`pattern` fills the box with a 1-bit pattern**: one of the 38 standard\nMacPaint patterns by name (`pattern=\"bricks\"`, `pattern=\"gray-50\"` —\ndocs/PATTERNS.md has the table), or sixteen hex digits stating a custom\n8×8 pattern row by row, the way a PAT resource did. It is painted as the\nbox's own background — black ink on a `--vf-white` ground, anchored at\nthe box's top-left corner, under the content — by the same whole-surface\nraster mechanism as the desktop dither, so it is 1-bit at every density\nand zoom (src/pattern-fill.ts). A declared `width`/`height` sizes the\nraster exactly; an undeclared axis (`fill-width`, a shrink-wrapped\nheight) is measured. Under forced colors the pattern goes flat Canvas.\n\n**It holds its box on the device-pixel grid** — with a `GridSnapController`.\nA container's box is itself the consumer's coordinate system, including for\nnon-`vf` content that cannot correct itself, so the box is the thing to\nhold on the grid. The shadow box below owns the `position: relative` anchor\nand the `vf-snap` class together, so the correction\nmoves the whole coordinate system — everything placed against it rides\nalong instead of being re-corrected child by child. (`vf-stack` shipped\nwithout a controller on the theory that slotted `vf-*` children correct\ntheir own origins; this component is where that theory's gap — consumer\ncontent — became visible, and the stack has since adopted the same\narrangement.)\n\nLike the stack it is **typographically transparent**: `vfBase`'s chrome\ndress is returned to `inherit` on the host, so wrapping content in a sized\nbox changes nothing about how that content reads.\n\n`fill-width` / `fill-height` work here the way they do everywhere: read\nabout the host (be as big as *its* parent allows), and compiled for slotted\nchildren — `width: 100%` in normal flow, so a child filling the cross of a\ndeclared box needs no stylesheet. A height fill needs a declared `height`\nto resolve against; with none it is inert, not an error.\n---\n\n\n### **Slots:**\n - _default_ - The content. In flow by default; `top`/`left` on a kit child places it against this box's origin. `fill-width` / `fill-height` on a child fills it to the declared box.",
|
|
158
158
|
"attributes": [
|
|
159
|
+
{
|
|
160
|
+
"name": "pattern",
|
|
161
|
+
"description": "A 1-bit fill for the box: a library pattern by name (`bricks`,\n`gray-50`, … — the 38 standard MacPaint patterns, docs/PATTERNS.md) or\nsixteen hex digits stating a custom 8×8 pattern row by row, bit 7 the\nleftmost pixel, 1 = ink (`\"DD 77 DD 77 DD 77 DD 77\"`). Painted in black\non a `--vf-white` ground under the content, anchored at the box's\ntop-left. Unset, the container paints nothing; an unrecognized value\npaints nothing and warns once.",
|
|
162
|
+
"values": []
|
|
163
|
+
},
|
|
159
164
|
{
|
|
160
165
|
"name": "width",
|
|
161
166
|
"description": "Width in whole system px. What the number means is the component's own\naffair — the measure a paragraph wraps to, a caption column, a window's\nbox — but the mechanics are shared: the length lands on the host as a\nlive `calc(var(--vf-scale, 1) * Npx)`, so the box scales with the\ndisplay and sits on the device-pixel grid by construction. Remove it\nand the width returns to layout.",
|
|
@@ -181,7 +186,7 @@
|
|
|
181
186
|
},
|
|
182
187
|
{
|
|
183
188
|
"name": "vf-desktop",
|
|
184
|
-
"description": "`<vf-desktop>` — the full-bleed classic desktop container.\n\nRenders the 50
|
|
189
|
+
"description": "`<vf-desktop>` — the full-bleed classic desktop container.\n\nRenders the desktop pattern — the classic 50% dither by default, or any of\nthe standard patterns by name (`pattern`, System 7's General Controls\nsetting) — and manages the stacking order and `active` state of slotted\n`vf-window` children: a `pointerdown` or\n`focusin` (keyboard focus) anywhere inside a window brings it to the front\nand makes it the single active window. The windows' light-DOM order is kept\nin step with the stacking order (bottom-most first, at pointer-gesture\nends), so tabbing walks the stack the way the eye does and Shift+Tab is\nits exact mirror.\n\nUtility windows (`vf-window[variant=\"utility\"]`) stack in a floating tier\nabove every document-tier window, restack only among themselves, and stand\noutside the single-active invariant entirely — clicking a palette neither\ndeactivates the active document window nor greys the palette, exactly as\nSystem 7's floating windoids behaved while their application was frontmost.\n\n**Deactivation.** On a real System 7 machine clicking the desktop clicked\nthe *Finder* — the frontmost application's windows lost their stripes.\nclearActive is that gesture's handler: it clears `active` from\nthe whole document tier, and **zero active windows is a legal state**,\nheld until a press or keyboard focus re-enters a document window (or a\nnew one is slotted, which activates it — opening a window brings its\napplication forward). The desktop never takes this decision itself: its\nfurniture is slotted light DOM (an icon layer, say), so only the page\nknows which of its children — or which presses on the bare dither — mean\n\"the Finder\", and it routes those through `clearActive()`. Left alone,\nthe classic always-one-active behavior is unchanged. activeWindow\nreads the current holder, and every change of holder — including to and\nfrom none — fires `vf-activate`.\n\nThe desktop is a raster with an explicit size, always: **`width` and\n`height`**, in system px, the way a WIND resource declared a window's —\nthe host box renders at the declared screen plus `2 × bezel` per axis, a\nwhole number of system pixels by construction (default 512×342, the\ncompact Mac's screen). Pure CSS sizing is not supported; the page sets\nthe numbers — directly, or via VfDesktop.fitWithin on\n`resize`/`onScaleChange` for a viewport-filling desktop — and positions\nthe sized box with its own stylesheet, keeping any sub-system-pixel\nslack on its side. `bezel` (system px) draws the black screen surround —\nthe CRT's unlit margin — around the screen, rounding its top corners\nwith the classic corner mask.\n\n**`pattern`** names the desktop pattern — `gray-50` (the dither) by\ndefault, any of the 38 standard patterns (docs/PATTERNS.md), or sixteen\nhex digits stating a custom 8×8 pattern, as on `vf-container`. It is\npainted as the screen's own background: black ink on an opaque white\npaper, one whole-surface raster at one image px per system px, 1-bit at\nevery density and zoom (src/pattern-fill.ts).\n\nCustom properties:\n- `--vf-desktop-pattern` — a consumer's own tile art in place of the\n pattern (the kit's default is a 1-bit 50% checker, opaque\n black-on-white on a 30-system-px tile). Set, it wins over `pattern` and\n renders as a placed tile grid at that same 30-px geometry\n (src/tile-grid.ts); a token swapped at runtime without touching the\n component wants a `requestUpdate()`.\n- `--vf-desktop` — base color painted *under* the pattern (default\n `#808080`). The pattern's paper is opaque, so this only becomes visible\n when `--vf-desktop-pattern` is overridden with a tile that has\n transparent cells (or with `none`).\n---\n\n\n### **Events:**\n - **vf-activate** - The active document-tier window changed. Detail `{ window: HTMLElement | null }` — the new holder, or `null` when the document tier deactivated (a {@link clearActive} call, or the active window leaving the DOM with none behind it). Fired once per change of holder, never for a re-assertion of the same one.\n\n### **Slots:**\n - _default_ - Default slot: menu bar, windows, anything.\n\n### **CSS Properties:**\n - **--vf-desktop** - base color under the desktop pattern — occluded by the pattern's opaque paper, so it only shows through a custom `--vf-desktop-pattern` _(default: #808080)_\n- **--vf-desktop-pattern** - a consumer's own desktop tile, in place of `pattern` — the kit's default is the 50% checker drawn as opaque black-on-white rects on a 30-system-px tile. Override the whole tile; consumer art renders as a placed tile grid at that same geometry (raster art magnifies nearest-neighbor, the `vf-img` idiom) _(default: undefined)_\n\n### **CSS Parts:**\n - **desktop** - The patterned screen surface — the whole-system-px raster (inset by `bezel` when one is set).",
|
|
185
190
|
"attributes": [
|
|
186
191
|
{
|
|
187
192
|
"name": "width",
|
|
@@ -198,6 +203,11 @@
|
|
|
198
203
|
"description": "Width of the black screen bezel, in system px (`0` = none), added onto\nthe declared screen on every side — a `width=\"502\" bezel=\"5\"` desktop\nrenders a 512-system-px host box. The compact Mac's CRT showed an\nunlit black margin between the desktop's raster and the case; `bezel`\ndraws it around the screen and puts the classic screen-corner mask on\nthe screen's two *top* corners — only the top pair was rounded in the\nframebuffer. Flow, window coordinates and the drag clip all belong to\nthe screen, so windows crop at its edge. Inside a bezeled desktop a\nmenu bar needs no `rounded` of its own — the desktop's mask lands on\nthe same pixels.",
|
|
199
204
|
"values": []
|
|
200
205
|
},
|
|
206
|
+
{
|
|
207
|
+
"name": "pattern",
|
|
208
|
+
"description": "The desktop pattern — System 7's General Controls setting. A library\npattern by name (`gray-50`, the classic dither, by default; `gray-75`,\n`bricks`, … — docs/PATTERNS.md) or sixteen hex digits stating a custom\n8×8 pattern, as on `vf-container`. Painted black on opaque white over\nthe whole screen, 1-bit at every density and zoom. A\n`--vf-desktop-pattern` token override still wins and renders the\nconsumer's tile as a placed grid; an unrecognized value warns once and\nkeeps the dither.",
|
|
209
|
+
"values": []
|
|
210
|
+
},
|
|
201
211
|
{
|
|
202
212
|
"name": "top",
|
|
203
213
|
"description": "Offset from the top of the positioning parent, in whole system px.\nSetting this (or `left`) absolutely positions the element within its\nparent; the coordinate left unset is 0. Remove both to return the\nelement to normal flow.",
|
|
@@ -213,7 +223,7 @@
|
|
|
213
223
|
},
|
|
214
224
|
{
|
|
215
225
|
"name": "vf-dialog",
|
|
216
|
-
"description": "`<vf-dialog>` — the System 7 modal dialog shell.\n\nTwo chromes, one modal lifecycle (native `<dialog>` for top-layer rendering\nand focus trapping, with a fully transparent backdrop — no dimming):\n\n- **Default:**
|
|
226
|
+
"description": "`<vf-dialog>` — the System 7 modal dialog shell.\n\nTwo chromes, one modal lifecycle (native `<dialog>` for top-layer rendering\nand focus trapping, with a fully transparent backdrop — no dimming). Both\nare the same dBoxProc double frame — 1px outer rule, 2px gap, 2px inner\nband, no shadow (vfModalFrame):\n\n- **Default:** the movable modal (movableDBoxProc) — the striped title bar\n set into the top of that frame, with a centered title over a white body.\n Drag the title bar to move it. `closable` adds the standard close box\n (left of the bar) — the HIG's own figures disagree on whether a movable\n modal carries one (Figure 5-1 says yes, Figure 6-1 and the Chapter 6 text\n say no), so the component enables either reading rather than enforcing\n one.\n- **`frame=\"plain\"`:** the modal dialog box — the bare frame, no title bar —\n and immovable, like the original. A `heading` renders as a centered\n display-face heading at the top of the body (the reference art's \"Dialog\n title\"); `closable` is ignored, there being no bar to carry the widget.\n\nOpen it with `show()` (or set the `open` attribute/property); close with\n`close()`. Escape closes it and fires `vf-close` with\n`{ reason: 'escape' }`; the close box and programmatic closing fire\n`{ reason: 'close' }`.\n---\n\n\n### **Events:**\n - **vf-close** - Dialog closed. Detail `{ reason: 'escape' | 'close' }`.\n\n### **Slots:**\n - _default_ - Default slot: dialog body content.\n- **buttons** - Optional action buttons. Rendered as a bottom-right `vf-button-group` (equal-width, faces aligned); the footer only takes space when the slot is populated.\n\n### **CSS Properties:**\n - **--vf-dots-pattern** - the windoid bar's dot-grid dither — a 2×2 tile, one black pixel at the origin (`vfDots`; override the whole pattern like `--vf-desktop-pattern`) _(default: undefined)_\n- **--vf-titlebar-height** - window/dialog title bars _(default: undefined)_\n- **--vf-scrollbar-thumb** - scrollbar thumb/elevator (white) _(default: undefined)_\n- **--vf-scrollbar-track** - the scroll trough's base color under the dot-dither (white) _(default: undefined)_\n\n### **CSS Parts:**\n - **frame** - The outer frame (the double frame's 1px rule; the bar and the inner band sit inside it).\n- **title-bar** - The striped title bar (default chrome only).\n- **title** - The centered title patch (or the plain-frame heading).\n- **close-box** - The close widget (`closable`, default chrome only).\n- **body** - The white content area.\n- **content** - The scrolling region inside the body (heading + slotted content, not the footer). Inert while the content fits; over-stuffed, it scrolls under a System 7 rail and becomes a keyboard stop.\n- **footer** - The action row wrapping the buttons.\n- **buttons** - The button group inside the footer.",
|
|
217
227
|
"attributes": [
|
|
218
228
|
{
|
|
219
229
|
"name": "heading",
|
|
@@ -232,7 +242,7 @@
|
|
|
232
242
|
},
|
|
233
243
|
{
|
|
234
244
|
"name": "frame",
|
|
235
|
-
"description": "Frame chrome. Omit for the
|
|
245
|
+
"description": "Frame chrome. Omit for the movable modal (the double frame with the\nstriped title bar set into it); `'plain'` for the immovable modal dialog\nbox (the bare double frame, no bar).",
|
|
236
246
|
"values": [{ "name": "plain" }]
|
|
237
247
|
},
|
|
238
248
|
{
|