vintage-frames 0.13.0 → 0.14.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 +8 -8
- package/dist/components/vf-checkbox.d.ts +6 -5
- package/dist/components/vf-checkbox.js +41 -37
- package/dist/components/vf-container.d.ts +11 -13
- package/dist/components/vf-container.js +24 -12
- package/dist/components/vf-fieldset.js +30 -22
- package/dist/components/vf-icon.js +55 -55
- package/dist/components/vf-list.js +30 -29
- package/dist/components/vf-menu.js +24 -22
- package/dist/components/vf-number-field.js +32 -30
- package/dist/components/vf-progress-bar.js +28 -29
- package/dist/components/vf-radio.js +3 -4
- package/dist/components/vf-scroll-area.js +27 -26
- package/dist/components/vf-select.js +108 -90
- package/dist/components/vf-separator.d.ts +5 -5
- package/dist/components/vf-separator.js +36 -10
- package/dist/components/vf-swatch.js +47 -45
- package/dist/components/vf-text-area.js +37 -35
- package/dist/components/vf-text-field.js +20 -18
- package/dist/components/vf-window.js +92 -72
- package/dist/index.d.ts +10 -1
- package/dist/index.js +60 -59
- package/dist/styles/base.d.ts +3 -1
- package/dist/styles/recipes/field.d.ts +10 -6
- package/dist/styles/recipes/field.js +8 -7
- package/dist/styles/recipes/focus.d.ts +7 -3
- package/dist/styles/recipes/rule.d.ts +11 -8
- package/dist/styles/recipes/rule.js +31 -13
- package/dist/styles/recipes/scroll-rail.d.ts +7 -6
- package/dist/styles/recipes/scroll-rail.js +18 -18
- package/dist/styles/recipes/shadow.d.ts +10 -6
- package/dist/styles/recipes/shadow.js +4 -2
- package/dist/styles/recipes/stroke.d.ts +85 -0
- package/dist/styles/recipes/stroke.js +53 -0
- package/dist/styles/recipes/surface.d.ts +8 -4
- package/dist/styles/recipes/surface.js +23 -17
- package/dist/styles/recipes/title-bar.d.ts +7 -0
- package/dist/styles/recipes/title-bar.js +25 -22
- package/dist/styles/recipes/toggle.js +3 -3
- package/dist/zoom.d.ts +9 -9
- package/docs/SPEC.md +23 -22
- package/editor/vscode.html-custom-data.json +4 -4
- package/editor/web-types.json +7 -7
- package/package.json +1 -1
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { vfDisplayDecls as e } from "./display-face.js";
|
|
2
|
-
import {
|
|
2
|
+
import { vfStrokeDecls as t, vfStrokeInset as n } from "./stroke.js";
|
|
3
|
+
import { css as r } from "lit";
|
|
3
4
|
//#region src/styles/recipes/title-bar.ts
|
|
4
|
-
var
|
|
5
|
+
var i = r`
|
|
5
6
|
.vf-title-bar {
|
|
6
7
|
position: relative;
|
|
7
8
|
/* Inert unless the frame is a flex column (vf-window); on vf-dialog's plain
|
|
8
9
|
block frame the shorthand simply doesn't apply. */
|
|
9
10
|
flex: none;
|
|
10
11
|
height: calc(var(--vf-scale, 1) * var(--vf-titlebar-height, 18px));
|
|
11
|
-
border-bottom: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
|
|
12
12
|
display: flex;
|
|
13
13
|
/* flex-start, not center: the title states its own whole-pixel row (see
|
|
14
14
|
the margin below); centering 16 into 17 would land it on a half. */
|
|
15
15
|
align-items: flex-start;
|
|
16
16
|
justify-content: center;
|
|
17
17
|
overflow: hidden;
|
|
18
|
+
${t({ edges: ["bottom"] })}
|
|
19
|
+
}
|
|
20
|
+
.vf-title-bar > .vf-stripes {
|
|
21
|
+
bottom: ${n(3)};
|
|
22
|
+
}
|
|
23
|
+
.vf-title-bar > .vf-dots {
|
|
24
|
+
bottom: ${n(2)};
|
|
18
25
|
}
|
|
19
26
|
.vf-title {
|
|
20
27
|
/* Chicago-style title (chrome); slotted body copy keeps the body face. */
|
|
@@ -36,7 +43,7 @@ var n = t`
|
|
|
36
43
|
overflow: hidden;
|
|
37
44
|
text-overflow: ellipsis;
|
|
38
45
|
}
|
|
39
|
-
`,
|
|
46
|
+
`, a = r`
|
|
40
47
|
.box {
|
|
41
48
|
position: absolute;
|
|
42
49
|
/* 11×11 box with 3px of clear white above and below it (title-bar
|
|
@@ -45,21 +52,18 @@ var n = t`
|
|
|
45
52
|
z-index: 1;
|
|
46
53
|
width: calc(var(--vf-scale, 1) * 11px);
|
|
47
54
|
height: calc(var(--vf-scale, 1) * 11px);
|
|
48
|
-
padding: 0;
|
|
49
55
|
margin: 0;
|
|
50
|
-
border: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000000);
|
|
51
56
|
background: var(--vf-white, #ffffff);
|
|
52
|
-
/* A 1px white patch ring that interrupts the stripes around the
|
|
53
|
-
box (no bevel — flat 1-bit), same buffer the stripes keep from the
|
|
54
|
-
frame edges. --vf-widget-ring is internal geometry, not a theming
|
|
55
|
-
knob: vf-window's utility variant widens it to 2px, the clearance
|
|
56
|
-
the windoid sheet cuts into its dither. */
|
|
57
|
-
box-shadow: 0 0 0 calc(var(--vf-scale, 1) * var(--vf-widget-ring, 1px))
|
|
58
|
-
var(--vf-white, #ffffff);
|
|
59
57
|
font: inherit;
|
|
60
58
|
cursor: var(--vf-cursor, default);
|
|
61
59
|
-webkit-appearance: none;
|
|
62
60
|
appearance: none;
|
|
61
|
+
/* After the stroke, a 1px white patch ring that interrupts the stripes
|
|
62
|
+
around the box (no bevel — flat 1-bit), same buffer the stripes keep
|
|
63
|
+
from the frame edges. --vf-widget-ring is internal geometry, not a
|
|
64
|
+
theming knob: vf-window's utility variant widens it to 2px, the
|
|
65
|
+
clearance the windoid sheet cuts into its dither. */
|
|
66
|
+
${t({ shadows: "0 0 0 calc(var(--vf-scale, 1) * var(--vf-widget-ring, 1px)) var(--vf-white, #ffffff)" })}
|
|
63
67
|
}
|
|
64
68
|
.close {
|
|
65
69
|
left: calc(var(--vf-scale, 1) * 8px);
|
|
@@ -73,7 +77,7 @@ var n = t`
|
|
|
73
77
|
traced pixel-for-pixel from the UI kit's close-button-active-state
|
|
74
78
|
sprite. Both widgets flash the identical graphic while pressed. That
|
|
75
79
|
sprite is the whole 11×11 box; its outer ring is this element's own 1px
|
|
76
|
-
|
|
80
|
+
stroke, so the SVG draws just the 9×9 interior, centered in the box. */
|
|
77
81
|
.box:active {
|
|
78
82
|
background-color: var(--vf-white, #ffffff);
|
|
79
83
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='9'%3E%3Cpath d='M4 0h1v1h-1zM1 1h1v1h-1zM4 1h1v1h-1zM7 1h1v1h-1zM2 2h1v1h-1zM4 2h1v1h-1zM6 2h1v1h-1zM0 4h3v1h-3zM6 4h3v1h-3zM2 6h1v1h-1zM4 6h1v1h-1zM6 6h1v1h-1zM1 7h1v1h-1zM4 7h1v1h-1zM7 7h1v1h-1zM4 8h1v1h-1z'/%3E%3C/svg%3E");
|
|
@@ -85,17 +89,16 @@ var n = t`
|
|
|
85
89
|
content: '';
|
|
86
90
|
position: absolute;
|
|
87
91
|
/* A small box nested in the TOP-LEFT corner of the widget (classic
|
|
88
|
-
System 7 zoom box). It shares the widget's own top and left
|
|
89
|
-
only its right and bottom edges are drawn: a 6×6 box anchored
|
|
90
|
-
|
|
92
|
+
System 7 zoom box). It shares the widget's own top and left line, so
|
|
93
|
+
only its right and bottom edges are drawn: a 6×6 box anchored inside
|
|
94
|
+
the widget's stroke whose 1px right/bottom strokes land the vertical at
|
|
91
95
|
sprite col 6 and the horizontal at row 6. Traced from the UI kit
|
|
92
96
|
zoom-button rest sprite. */
|
|
93
|
-
top: 0;
|
|
94
|
-
left: 0;
|
|
97
|
+
top: ${n(0)};
|
|
98
|
+
left: ${n(0)};
|
|
95
99
|
width: calc(var(--vf-scale, 1) * 6px);
|
|
96
100
|
height: calc(var(--vf-scale, 1) * 6px);
|
|
97
|
-
|
|
98
|
-
border-bottom: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000000);
|
|
101
|
+
${t({ edges: ["right", "bottom"] })}
|
|
99
102
|
}
|
|
100
103
|
/* While pressed the zoom box shows the same sunburst as the close box, so
|
|
101
104
|
its inner detail square gives way to it. */
|
|
@@ -131,4 +134,4 @@ var n = t`
|
|
|
131
134
|
}
|
|
132
135
|
`;
|
|
133
136
|
//#endregion
|
|
134
|
-
export {
|
|
137
|
+
export { i as vfTitleBar, a as vfWindowWidgets };
|
|
@@ -7,7 +7,7 @@ var t = e`
|
|
|
7
7
|
gap: calc(var(--vf-scale, 1) * 6px);
|
|
8
8
|
cursor: var(--vf-cursor, default);
|
|
9
9
|
}
|
|
10
|
-
/* The well PAINTS 3 system px below the row top. Centering
|
|
10
|
+
/* The radio's 13px well PAINTS 3 system px below the row top. Centering it
|
|
11
11
|
in the 20px row is the layout — kept, because the host's height and its
|
|
12
12
|
exported baseline are consumer-visible (moving them off the well's
|
|
13
13
|
centered bottom reflowed every line holding a toggle) — but centering
|
|
@@ -18,8 +18,8 @@ var t = e`
|
|
|
18
18
|
compose the variable), landing the ink on row 3: ties resolve toward the
|
|
19
19
|
start, as QuickDraw's "div 2" did — the title-bar and vf-stack
|
|
20
20
|
convention. The half-pixel gap between layout box and painted box is the
|
|
21
|
-
documented snap idiom, not a fault.
|
|
22
|
-
|
|
21
|
+
documented snap idiom, not a fault. The checkbox's 12px box centers on
|
|
22
|
+
row 4 exactly and needs no step; both wells end on row 15. */
|
|
23
23
|
.circle {
|
|
24
24
|
top: calc(var(--vf-snap-dy, 0px) - var(--vf-scale, 1) * 0.5px);
|
|
25
25
|
}
|
package/dist/zoom.d.ts
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
*
|
|
18
18
|
* The work is knowing the zoom at all. Zoom multiplies device-px-per-CSS-px in
|
|
19
19
|
* every engine — that is what zoom is — but the engines differ in whether they
|
|
20
|
-
* report it: Chrome and
|
|
21
|
-
* `devicePixelRatio` to the hardware and moves `innerWidth`
|
|
22
|
-
* tracker watches two signals:
|
|
20
|
+
* report it: Chrome, Firefox and Safari 27 fold it into `devicePixelRatio`;
|
|
21
|
+
* older Safari pins `devicePixelRatio` to the hardware and moves `innerWidth`
|
|
22
|
+
* instead. So the tracker watches two signals:
|
|
23
23
|
*
|
|
24
24
|
* 1. `devicePixelRatio`, via a resolution media query re-armed at each new
|
|
25
25
|
* value. A dpr change is *either* zoom or the window moving to a
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* report those members in zoom-affected CSS px) means zoom; anything else
|
|
32
32
|
* means a display change, which rebases the baseline and reports nothing.
|
|
33
33
|
* 2. `innerWidth`/`innerHeight` against `outerWidth`/`outerHeight`, for
|
|
34
|
-
* Safari: zoom rescales the inner (CSS-px) viewport on both axes by the
|
|
34
|
+
* older Safari: zoom rescales the inner (CSS-px) viewport on both axes by the
|
|
35
35
|
* same factor while the outer (screen-px) window holds still. A change in
|
|
36
36
|
* the outer size is a real window resize and rebases; a one-axis inner
|
|
37
37
|
* change (an edge drag, devtools docking, a sidebar) is never zoom and
|
|
@@ -170,12 +170,12 @@ export declare function getZoom(): number;
|
|
|
170
170
|
/**
|
|
171
171
|
* Device px per CSS px *right now* — the number every "snap to the device
|
|
172
172
|
* grid" computation must divide by. `window.devicePixelRatio` is that number
|
|
173
|
-
* only in engines that fold zoom into it; Safari's is simply wrong about
|
|
174
|
-
* current rasterization density at any non-100% zoom. Reading the live dpr
|
|
175
|
-
* folding the tracked zoom back in is correct in both:
|
|
173
|
+
* only in engines that fold zoom into it; older Safari's is simply wrong about
|
|
174
|
+
* the current rasterization density at any non-100% zoom. Reading the live dpr
|
|
175
|
+
* and folding the tracked zoom back in is correct in both:
|
|
176
176
|
*
|
|
177
|
-
* Chrome/Firefox (latched) dpr already includes zoom → dpr
|
|
178
|
-
* Safari (never latches)
|
|
177
|
+
* Chrome/Firefox/Safari 27 (latched) dpr already includes zoom → dpr
|
|
178
|
+
* older Safari (never latches) dpr is the hardware → dpr × zoom
|
|
179
179
|
*
|
|
180
180
|
* The live read matters: a monitor move changes the true density whether or
|
|
181
181
|
* not the tracker is running, and this stays exactly as correct as the old
|
package/docs/SPEC.md
CHANGED
|
@@ -146,13 +146,14 @@ The span construction (`vfTileSize`, `tileImage`, `tileSpan`) still sizes what i
|
|
|
146
146
|
- `vfStripes` — a `.vf-stripes` class for the racing-stripe layer. Position it absolutely inside the title bar, inset `3px 1px` (top/bottom 3px, left/right 1px) so exactly six 1px stripes show at the 18px bar height, their top and bottom edges aligned with the close box's and one system px of clear white between the stripes and the frame border — the same buffer the widgets' patch ring keeps. The paint is split by engine, each side the measured best (the full three-engine matrix is in `scripts/verify-tile.mjs`): Blink — and any engine failing both gate properties — draws a 12-unit SVG (the band's 11 rows plus an empty pad row, viewBox stretched onto a 12px-tall box, crispEdges). Twelve, not eleven, because Blink pixel-snaps painted boxes to whole CSS px and an 11-system-px box has no legal CSS height at scale 3/2; 12 divides by 2 and 3, so the box never rounds at any integer display's scale, and the SVG measured whole-rhythm, close-box-registered and zero-gray at dpr 1/1.5/2/3 — retiring the `repeating-linear-gradient`, which it strictly dominates. Gecko and WebKit (gated by `@supports (-moz-appearance: none) or (-webkit-backdrop-filter: blur(1px))`, each property parsing in its one engine only) render six placed solid rows (`chromeTitleBar` supplies the spans): both engines device-snap solid quads exactly, and each misrendered the gradient — Gecko's GPU pipeline softens a hard stop at default zoom, WebKit landed the sixth stripe a device row thin at dpr 3 and the zoom-minted scales.
|
|
147
147
|
- `vfDots` — the windoid bar's counterpart to `vfStripes`: a `.vf-dots` layer inset `2px` top/bottom and **flush left/right** (the close-up reference runs the dots into the side borders; the `Windows/` sheet's 2px side inset is the artist's, not the bar's), a 2×2 motif carrying one black pixel at its origin (`--vf-dots-pattern` to retheme). The layer's background is the kit tile `vf-window` writes (see *Tiled fills*); a `--vf-dots-pattern` token renders as the placed tile grid in a window with a declared width, and as a CSS repeat on its 30-px span in one without.
|
|
148
148
|
- `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 consumer tokens' documented tiles and the forced-colors masks; the kit's own art repeats a 120-system-px tile instead (`vfRepeatTileSize`, `repeatTile`). See *Tiled fills* below.
|
|
149
|
-
- `
|
|
150
|
-
- `
|
|
151
|
-
- `
|
|
152
|
-
- `
|
|
153
|
-
- `
|
|
149
|
+
- `vfStrokeDecls` / `vfStrokeInset` / `vfStrokeShadow` — every 1-bit line the kit draws (frames, wells, rules, rails, widgets) is a **stroke**, not a CSS `border`: Safari rounds a border width to device px before it applies page zoom, so a `calc(var(--vf-scale, 1) * 1px)` border draws thinner than the art around it at most zoom levels and moves everything inside the box off the device grid. `vfStrokeDecls({ edges, width, padding, shadows, ink })` puts the line's space in padding on each stroked edge (added to the element's own `padding`) and paints it with an inset `box-shadow`, `shadows` after it in the same list; neither length is rounded before zoom, so the line lands on whole device px at every zoom. The declarations own the element's padding on the stroked edges and its `box-shadow`; the color goes through a private property the declarations set, so a `var()` passed as `ink` recolors it from a state rule. What moves with the space: a child placed against the box measures from its outer edge, so its inset on a stroked edge adds the stroke — `vfStrokeInset(base)`; an `overflow: hidden` box clips at its outer edge, so a box whose content could paint over its stroke clips on an inner element (vf-select's panel, vf-window's header and status slots). Under `forced-colors: active`, which paints no box-shadow, each stroke is a border again and the padding the element's own — the box a border draws, in the engines that have forced colors (Safari has none). `vfStrokeShadow(width)` restates only the painted width (vf-checkbox's press). Exported for consumer boxes, for the same reason the kit uses it.
|
|
150
|
+
- `vfHardShadow` / `vfHardShadowDecls` — the hard 1-bit drop 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. `vfHardShadow` is the value, which a stroked surface passes to `vfStrokeDecls` as `shadows`; `vfHardShadowDecls` is the whole declaration, for a box with no stroke. Every raised surface in the kit uses this one value (`vfPanel`, `vfChromeFrame`).
|
|
151
|
+
- `vfPanel` — a `.vf-panel` class for menus/popups: white bg, a 1px black stroke with `vfHardShadow` after it.
|
|
152
|
+
- `vfChromeFrame` — a `.vf-frame` class for the desktop window (`vf-window`): the same white face, 1px black stroke 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`.
|
|
153
|
+
- `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 a stroke on that edge (`vfStrokeDecls`) scaled by `--vf-scale`. It sits inside the box's `border-box` size, and in-flow content begins inside the rule, as a rectangle's interior begins inside FrameRect's line; children placed against the box and percentage fills measure from its outer edge, so a component that promises them the interior gives them an inner box (vf-container's slot). The classes compose on one element — each edge's shadow goes through its own private property and one rule paints all four — so they own the element's `box-shadow` and its padding on their edges. `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. `npm run verify:rule` asserts a container's rule against the menu bar's own at four densities.
|
|
154
|
+
- `vfModalFrame` — the dBoxProc modal-dialog frame, both `vf-dialog` chromes: `.vf-modal-frame` (white face, 1px black stroke, **no shadow**) around `.vf-modal-frame-inner` (`margin: 2px`, a 2px stroke). **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 stroke to 1px, so the bar's own 1px rule plus that stroke 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.
|
|
154
155
|
- `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.
|
|
155
|
-
- `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.
|
|
156
|
+
- `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 — a stroke, so the recipe adds it to the texture layers' bottom insets — `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.
|
|
156
157
|
- 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`.
|
|
157
158
|
- The title's clearance for anything else in the bar is `--vf-title-inset` (default 16px); `vf-window` sets 60px so an ellipsized title can't run under its close/zoom widgets.
|
|
158
159
|
- `touch-action` is deliberately NOT in the recipe: `vf-dialog`'s bar is always a drag handle, `vf-window`'s only when `[movable]`, and suppressing touch scrolling on a bar that can't be dragged would be a behavior change.
|
|
@@ -163,7 +164,7 @@ The span construction (`vfTileSize`, `tileImage`, `tileSpan`) still sizes what i
|
|
|
163
164
|
- `vfFocusUnderline` is the alternative for a control that can carry the mark on its own face: an `::after` on that element, 1 system px tall, spanning its box, dashed 1px on / 1px off via a `repeating-linear-gradient` in `currentColor` — so it inverts to white with the label on a pressed face. The component suppresses the UA outline in the same rule set, and the underlined element needs `position: relative`. Two placements:
|
|
164
165
|
- **Inside the control**, under the ink it marks: `vf-button` underlines its label, `vf-checkbox` its box, `vf-radio` its circle (narrowed to 9 of its 13px, since that shape is round — §5), the three editable fields their well (via `vfField`'s `.vf-field-well`) and `vf-menu` its bar title.
|
|
165
166
|
- **Below the control**, under its whole box, where there is no room inside: a `vf-select`'s single line is already the label and the ▼, every pixel inside a `vf-swatch` is the color it exists to show, and a `vf-slider`'s only interior is the handle — which moves, so marking it marked the *value* rather than the control. `vf-select` always casts the hard shadow and `vf-swatch` does under `shadow`, which the rule has to clear — ink lying outside every box the pseudo-element could size itself to; the slider's runs the rail's full width and the handle occludes it in passing (z-index 1), exactly as it occludes the rail behind it.
|
|
166
|
-
- `--vf-focus-underline-offset` places it, measured (like any `bottom`) from the element's **padding** box up to the rule's bottom edge, so positive insets it and negative drops it below. The contract is one blank system px row between the rule and the ink above it, so a negative offset counts every row of ink in between: `4px` (the default) for a text box, whose ink stops at the baseline 6px above its bottom (2px half-leading over the 16px em + the 4px descent); `-2px` for a well whose ink ends at its own bottom edge (the radio's circle, a field's wrapper) and for `vf-menu`'s title box; `-3px` for
|
|
167
|
+
- `--vf-focus-underline-offset` places it, measured (like any `bottom`) from the element's **padding** box up to the rule's bottom edge, so positive insets it and negative drops it below. The contract is one blank system px row between the rule and the ink above it, so a negative offset counts every row of ink in between: `4px` (the default) for a text box, whose ink stops at the baseline 6px above its bottom (2px half-leading over the 16px em + the 4px descent); `-2px` for a well whose ink ends at its own bottom edge (the radio's circle, a field's wrapper, the checkbox's box and the text area's well — a stroke is inside the padding box the pseudo-element sizes to) and for `vf-menu`'s title box; `-3px` for `vf-select`, adding its 1px hard shadow; `3px` for `vf-slider`, counting back up from the track's bottom to a row under the rail; and for `vf-swatch` a `calc()` that composes the depth actually in play — `0px` flat, a rethemeable `--vf-shadow-offset` under `shadow` — rather than hard-coding one. The stroked carriers state their `bottom`/`left`/`right` with `vfStrokeInset`, which adds the stroke back under forced colors, where it is a border and the padding box sits inside it. `npm run verify:focus`.
|
|
167
168
|
- `vf-menu` is the one that anchors to a **box** rather than to the baseline the button uses: its title box is shrunk to the face's own em (`line-height: 1`), whose bottom edge is the descent line, so one offset serves both a descender and the Apple menu's slotted 16px `vf-img`. The button's rule, one row under the baseline, is crossed by both.
|
|
168
169
|
- The two controls that **drop open** — `vf-menu` and `vf-select` — draw the rule only while closed. An open menu or list already shows where focus is (a whole inverted cell, a dropped panel), so the rule is reserved for the closed state. Both keep the underlying keyboard-focus state through the open state, so the rule returns by itself on close.
|
|
169
170
|
- The mark is **keyboard-only** everywhere, but four controls can't say so with `:focus-visible`, for two opposite reasons:
|
|
@@ -218,8 +219,8 @@ The desktop-window shell: the classic document window (see DragThing screenshot)
|
|
|
218
219
|
- 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.
|
|
219
220
|
- Body: no inset of its own — content starts at the content region's corner, the frame's inner edge below the title bar, where a placed child's (0,0) is; an inset is the content's (a `vf-stack pad`) — 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.
|
|
220
221
|
- 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.
|
|
221
|
-
- Header (`header` slot): a band between the title bar and the body, the full width of the window — the Finder window's header line — a white interior over a 1px black rule (`vfRule`'s `.vf-rule-bottom`), part `header` (a `<div>`, never a `<header>` element, for the banner-landmark reason the title bar is a `<div>`). Like the body it carries no inset and is a positioning anchor: (0,0) is the header's own corner, flow content starts there, an inset is the content's (a `vf-stack pad`). As tall as its content unless `header-height` states it (rule included, the way every kit bar counts its rule). Clipped like the body; a drop-open panel still escapes. Takes **no space** until the slot is populated, and collapses again when it empties. Composes with `scrollbars`: the edge rails sit in the body below it, the area's top overhang landing its frame line exactly on the header's rule, so the header spans the vertical rail's column and the rail's top arrow begins under it.
|
|
222
|
-
- 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, 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.
|
|
222
|
+
- Header (`header` slot): a band between the title bar and the body, the full width of the window — the Finder window's header line — a white interior over a 1px black rule (`vfRule`'s `.vf-rule-bottom`), part `header` (a `<div>`, never a `<header>` element, for the banner-landmark reason the title bar is a `<div>`). Like the body it carries no inset and is a positioning anchor: (0,0) is the header's own corner, flow content starts there, an inset is the content's (a `vf-stack pad`). As tall as its content unless `header-height` states it (rule included, the way every kit bar counts its rule). Clipped like the body, above the rule — the rule is a stroke, so the slot is the box content lays out, places and clips in; a drop-open panel still escapes. Takes **no space** until the slot is populated, and collapses again when it empties. Composes with `scrollbars`: the edge rails sit in the body below it, the area's top overhang landing its frame line exactly on the header's rule, so the header spans the vertical rail's column and the rail's top arrow begins under it.
|
|
223
|
+
- 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 — vertically by the slot, below the rule. Takes **no space** until the slot is populated, 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.
|
|
223
224
|
- 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. the area's viewport carries no inset, so content and the (0,0) of placed children sit at the content region's corner; a `resizable` window sets the area's `corner`, so a single-axis rail stops at the corner cell too — unless the status strip is populated, when the grow box sits in the strip and the rail runs edge to edge onto the strip's rule. Same caveats as the slotted composition (see vf-scroll-area §5).
|
|
224
225
|
- 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.
|
|
225
226
|
- **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`.
|
|
@@ -245,7 +246,7 @@ The modal-dialog shell: movable modal by default (the dBoxProc double frame with
|
|
|
245
246
|
System 7's fixed modal alert (double frame: 2px outer, 2px gap, 1px inner rule, *with* the hard shadow) is deliberately not shipped as a component. What separates an alert from a modal dialog is a *picture* — the 32×32 icon — and pictures are the consumer's assets, never the library's (see the glyph-sprites note at the top of this spec). An alert box is composed from the shells above: `vf-dialog frame="plain"` with `label` stated (there is no title bar to name it), a row `vf-stack` slotting the consumer's own 32×32 art through `vf-img`, display-face copy (System 7 alerts used chrome type), and a `vf-button-group`, each placed with `top`/`left`. The reference page carries the live recipe, composed from `demo/icons/alert.png`.
|
|
246
247
|
|
|
247
248
|
#### `vf-separator` (`VfSeparator`, vf-separator.ts)
|
|
248
|
-
- 1px black rule. `vertical: boolean` attr → 1px wide, auto height. Horizontal default: `display: block; height: 1px
|
|
249
|
+
- 1px black rule. `vertical: boolean` attr → 1px wide, auto height. Horizontal default: `display: block; height: 1px`, the line the inner element's own background, `var(--vf-separator-color, var(--vf-black, #000))`. `--vf-separator-style` is `solid` (default) or `dotted` — one system px on, one off, from the rule's start, a `repeating-linear-gradient` selected by a style query on the token. Inside menus it renders as the classic dimmed **dotted** rule spanning the full panel width (see Menus.png): the menu panel sets the two props to `--vf-disabled` / `dotted`, and spends a full `--vf-menu-row-height` on the divider: 8px margin above the rule, 7 below. Under forced colors the line is a border in the token's style, as every stroke is. `role="separator"`.
|
|
249
250
|
|
|
250
251
|
### Group B — buttons & toggles
|
|
251
252
|
|
|
@@ -279,23 +280,23 @@ The color-swatch button: a well of solid color — a palette cell.
|
|
|
279
280
|
- **Visual:** inner `<button>` sized `width × height`: 1px black border, 1px white inset (the button's own padding + background) and a `fill` span carrying the checker as an exact fill with the color painted over it in a `tint` child (see *Tiled fills*); `--vf-swatch-checker` overrides the pattern.
|
|
280
281
|
- `shadow`: adds the shared hard shadow (`vfHardShadowDecls` — the same `--vf-shadow-offset` token as windows and menus, painting outside the box like theirs). **Opt-in, because the swatch's usual home is a table of them** (a `vf-grid`, a picker row), where every cell shadowing its neighbour reads as noise rather than depth; the lone well standing in for a current color is the case that wants the raised reading. The depth in play is resolved once into a private `--_shadow-depth` (`0px` unset), so the focus rule below composes what is actually there.
|
|
281
282
|
- `:active` (pressed, not disabled): the white inset inverts to black — the inset counterpart of vf-button's face inversion.
|
|
282
|
-
- `:focus-visible`: **no ring** — the UA outline is off on both the inner button and the (`delegatesFocus`) host, replaced by `vfFocusUnderline` (§4) below the whole box: a `calc()` offset that clears
|
|
283
|
+
- `:focus-visible`: **no ring** — the UA outline is off on both the inner button and the (`delegatesFocus`) host, replaced by `vfFocusUnderline` (§4) below the whole box: a `calc()` offset that clears `--_shadow-depth`, then leaves the blank row — so the rule lands one row under a flat swatch's frame and one under a shadowed one's shadow. Not inside the box the way vf-button underlines its label — every pixel in there is the color the swatch exists to show, and a rule over the fill would read as part of it. Spans the border box, which is the padding box the pseudo-element sizes to (the frame is a stroke). `npm run verify:focus` checks both depths.
|
|
283
284
|
- `disabled`: interaction stops; nothing dims. The kit dims *labels* when disabled, and a swatch's only label is its fill, which must keep reading as its color.
|
|
284
285
|
- **Behavior:** "basically a button": native `click` retargets to the host, Enter/Space activate via the inner button, `delegatesFocus`. `label` feeds the inner button's `aria-label`, so `vf-label for` reaches it like every control. Form-associated (`VfFormControl`) **for the disabled contract alone**: an ancestor `<fieldset disabled>` reaches the palette through `formDisabledCallback` (and `:state(form-disabled)`) like every other control — but it submits nothing (a palette cell picks, it doesn't submit; no value is ever set, so no `FormData` entry and no `formResetCallback`). Carries a `ScaleController` and a `GridSnapController`.
|
|
285
286
|
- **Slots:** none. **Parts:** `button`, `fill`. **Events:** none custom.
|
|
286
287
|
|
|
287
288
|
#### `vf-checkbox` (`VfCheckbox`, vf-checkbox.ts)
|
|
288
289
|
- **Attributes/props:** `checked`, `disabled`, `name`, `value` (default `'on'`).
|
|
289
|
-
- **Visual:**
|
|
290
|
+
- **Visual:** 12×12 white box, `1px solid black`, no radius — the sprite sheet's own size; checked = classic ✕: the pixel-exact cross from the sprite sheet filling the 10×10 interior corner to corner, rendered as the `CHECKBOX_X` inline-SVG fill path (`shape-rendering: crispEdges`, `fill: currentColor`) — no anti-aliased strokes. The 12×12 glyph covers the box exactly, its transparent 1px margin over the frame. The box is centered in the 20px row, which puts it on row 4 exactly; it ends on row 15, as the radio's well does. Label (slot) sits right with 6px gap, bold. Disabled: only the label dims to `var(--vf-disabled, #c0c0c0)`; the box border and ✕ glyph stay black. Pressed (`:active` on box): border thickens to 2px (classic press feedback).
|
|
290
291
|
- **Behavior:** form-associated; toggles on click and Space; `role="checkbox"`, `aria-checked`; focusable (tabindex 0 on the host).
|
|
291
|
-
- **When the toggle runs.** At the *end* of the click's propagation, like a native checkbox's activation behavior — so `preventDefault()` on the control or anything above it stops the state change, in either phase. `stopPropagation()` cancels nothing (HTML doesn't), and the disabled gate sits at the far end of the deferral, so a control disabled *during* propagation never acts. Space synthesises a cancelable click and inherits all of it. Shared with `vf-radio` through `VfToggleControl`, and with `vf-button` and the menus through `deferActivation` (src/events.ts). `npm run verify:toggle`, CANCELLATION group. `:focus-visible` marks the **box**, not the label and not either with a ring: `vfFocusUnderline` (§4) at
|
|
292
|
+
- **When the toggle runs.** At the *end* of the click's propagation, like a native checkbox's activation behavior — so `preventDefault()` on the control or anything above it stops the state change, in either phase. `stopPropagation()` cancels nothing (HTML doesn't), and the disabled gate sits at the far end of the deferral, so a control disabled *during* propagation never acts. Space synthesises a cancelable click and inherits all of it. Shared with `vf-radio` through `VfToggleControl`, and with `vf-button` and the menus through `deferActivation` (src/events.ts). `npm run verify:toggle`, CANCELLATION group. `:focus-visible` marks the **box**, not the label and not either with a ring: `vfFocusUnderline` (§4) at `-2px`, a dashed rule spanning the box's full 12px, one blank row under its border — the box's 1px line is a stroke, inside the padding box the pseudo-element sizes to. `npm run verify:focus`.
|
|
292
293
|
- **Slots:** default (label). **Parts:** `box`, `label`.
|
|
293
294
|
- **Events:** `vf-change` detail `{ checked: boolean }`, plus the native `input`/`change` pair per user toggle (§2).
|
|
294
295
|
|
|
295
296
|
#### `vf-radio` (`VfRadio`, vf-radio.ts)
|
|
296
297
|
- **Attributes/props:** `checked`, `disabled`, `value: string`.
|
|
297
|
-
- **Visual:** 13×13 pixel circle drawn as inline SVG — the hand-tuned 1-bit `RADIO_RING` outline over a white `RADIO_FACE` disc (not `border-radius`, which anti-aliases); checked = the centered `RADIO_DOT` pixel disc. Pressed (`:active`): the ring swaps to `RADIO_RING_PRESSED` (2px-thick). The circle
|
|
298
|
-
- **Behavior:** `role="radio"`, `aria-checked`. Click → asks parent group to select it (dispatch internal event or parent listens). NOT itself form-associated — the group is. Selection defers to the end of the click's propagation and is cancellable exactly as `vf-checkbox`'s is (same `VfToggleControl` skeleton). `:focus-visible` marks the **circle** with `vfFocusUnderline` (§4) at `--vf-focus-underline-offset: -2px` — one blank row below the
|
|
298
|
+
- **Visual:** 13×13 pixel circle drawn as inline SVG — the hand-tuned 1-bit `RADIO_RING` outline over a white `RADIO_FACE` disc (not `border-radius`, which anti-aliases); checked = the centered `RADIO_DOT` pixel disc. Pressed (`:active`): the ring swaps to `RADIO_RING_PRESSED` (2px-thick). The circle's well PAINTS 3 system px below the row top: centering the 13px well in the 20px row stays the layout (host height and exported baseline unchanged), but its 3.5 tie is half a system px off the pixel grid, so the paint steps back half a pixel through the same relative `top` the grid-snap correction rides — ties resolve toward the start, QuickDraw `div 2`. It ends on row 15, as the checkbox's box does. Label right, 6px gap. Disabled dims like checkbox (label only; ring + dot stay black).
|
|
299
|
+
- **Behavior:** `role="radio"`, `aria-checked`. Click → asks parent group to select it (dispatch internal event or parent listens). NOT itself form-associated — the group is. Selection defers to the end of the click's propagation and is cancellable exactly as `vf-checkbox`'s is (same `VfToggleControl` skeleton). `:focus-visible` marks the **circle** with `vfFocusUnderline` (§4) at `--vf-focus-underline-offset: -2px` — one blank row below the 13px well, which ends on the row the checkbox's box does, so the two rules share a row in a mixed list. The 12px sprite sits half a system px proud of the well, so the gap to the circle itself reads as one row or two depending on how that rounds; the well is the anchor. The rule is also narrowed to **9** of the well's 13px (2px inset each side, 5 dashes): full width reads wider than the round shape above it, and 9 is the closest to two thirds that keeps whole-px insets and ink at both ends. Focus: inside a `vf-radio-group` the group owns the roving tabindex and is the single source of truth for `checked`; standalone, the radio self-manages its own tabindex (otherwise it would be keyboard-dead) and self-checks on activation. A consumer-authored `tabindex` always wins, and that ownership is latched on first connect so a reconnect can't mistake our own write for the consumer's.
|
|
299
300
|
- **Slots:** default (label). **Parts:** `circle`, `label`.
|
|
300
301
|
- **Events:** `vf-change` detail `{ value }` (fired by user interaction only).
|
|
301
302
|
|
|
@@ -317,7 +318,7 @@ The color-swatch button: a well of solid color — a palette cell.
|
|
|
317
318
|
- **Events:** `vf-input` detail `{ value }` on every keystroke; `vf-change` detail `{ value }` on commit (native change). Plus the native pair per §2: the inner input's own composed `input` crosses the boundary itself; `change` is re-dispatched from the host.
|
|
318
319
|
|
|
319
320
|
#### `vf-text-area` (`VfTextArea`, vf-text-area.ts)
|
|
320
|
-
Same as vf-text-field but wrapping `<textarea>`; extra prop `rows: number` (default 4). No resize grip (`resize: none`) — System 7 fields don't resize. Wrapped entry text sits on the display face's native line (`--vf-line-height-display`, 16px — Chicago 12's; `rows` buys one line each) — the single-line well's 20px box is control geometry, the 22px field trace, and stays; a multi-line well is typesetting. Reserves a permanent System 7 vertical scroll rail (the shared "always-a-rail" behavior — see vf-scroll-area): arrows on an empty white channel until the text overflows, then the dither and thumb fill in. The rail is the drawn `vfScrollRail` subtree (§4), a shadow sibling of the `<textarea>` synced to its native scrolling by `ScrollRailController`; the textarea carries the `vf-scroll` class (native bar hidden), and both controllers re-measure on each keystroke (a textarea's scrollHeight grows without a box resize). Unlike vf-text-field, the 1px frame sits on the `.vf-field-well` wrapper rather than the field itself — the rail has to live inside the frame beside the text — with the borderless `<textarea>` keeping vf-text-field's own `3px/6px` padding, so the text and the outer box sit exactly where the bordered field puts them. The well is the same wrapper vf-text-field uses; the focus rule spans the full frame, scroll rail included (the `-
|
|
321
|
+
Same as vf-text-field but wrapping `<textarea>`; extra prop `rows: number` (default 4). No resize grip (`resize: none`) — System 7 fields don't resize. Wrapped entry text sits on the display face's native line (`--vf-line-height-display`, 16px — Chicago 12's; `rows` buys one line each) — the single-line well's 20px box is control geometry, the 22px field trace, and stays; a multi-line well is typesetting. Reserves a permanent System 7 vertical scroll rail (the shared "always-a-rail" behavior — see vf-scroll-area): arrows on an empty white channel until the text overflows, then the dither and thumb fill in. The rail is the drawn `vfScrollRail` subtree (§4), a shadow sibling of the `<textarea>` synced to its native scrolling by `ScrollRailController`; the textarea carries the `vf-scroll` class (native bar hidden), and both controllers re-measure on each keystroke (a textarea's scrollHeight grows without a box resize). Unlike vf-text-field, the 1px frame sits on the `.vf-field-well` wrapper rather than the field itself — the rail has to live inside the frame beside the text — with the borderless `<textarea>` keeping vf-text-field's own `3px/6px` padding, so the text and the outer box sit exactly where the bordered field puts them. The well is the same wrapper vf-text-field uses; the focus rule spans the full frame, scroll rail included (the frame is a stroke inside the padding box, so the `-2px` well offset, §4 vfFocusUnderline). Forwards the same input-behavior attributes as vf-text-field, minus `pattern` (only an `<input>` takes it). Parts: `textarea`. Events: `vf-input`, `vf-change`, plus the native pair (§2).
|
|
321
322
|
|
|
322
323
|
#### `vf-number-field` (`VfNumberField`, vf-number-field.ts)
|
|
323
324
|
A numeric text field paired with the classic "little arrows" stepper.
|
|
@@ -331,9 +332,9 @@ A numeric text field paired with the classic "little arrows" stepper.
|
|
|
331
332
|
The classic popup menu control ("Macintosh HD ▼").
|
|
332
333
|
- **Attributes/props:** `value: string`, `disabled`, `name`, `size: 'regular' | 'small'` (reflected; any other value renders as `regular`), `no-shadow` (`noShadow: boolean`, reflected).
|
|
333
334
|
- **Children:** `<vf-option value="...">Label</vf-option>` elements (default slot). `vf-option` (`VfOption`, vf-option.ts): props `value`, `disabled`, `selected` and `size` (both managed by parent — the select stamps its `size` on every option, including ones added later); renders its slot; `role="option"`.
|
|
334
|
-
- **Visual (closed control):** height `var(--vf-popup-height, 18px)`, white bg, `1px solid black`, NO radius, `box-shadow: 1px 1px 0 0 var(--vf-black, #000)` (the small hard shadow visible in the screenshot; `no-shadow` removes it from the pill, and the open panel keeps its own), `padding: 0 8px 0 var(--vf-select-gutter, 16px)` — the left inset equals the option checkmark gutter so the selected label sits at the same x closed or open. *One documented 1px deviation:* the reference sheets put a closed pill's label ink 16px from its border box but an open menu's at 17 (three pill instances and four menu panels, all consistent, in the reference art). System 7 reconciled that by drawing the open panel 1px left of the pill; the kit instead uses one shared gutter with no panel offset, which keeps the closed↔open alignment exact and puts the closed pill's ink at 17. Bold label left, the black `CARET_DOWN` ▼ pixel glyph (inline SVG) right with 8px gap. The ▼ stays black even when the control is disabled (only the label dims). **Width:** the control hugs the *widest* option — no intrinsic min-width — via an invisible, height-collapsed stack of every option's text sharing the label's grid cell; so the closed pill and the open panel are always exactly the same width and the value never shifts as the selection changes. Authors wanting a floor set `min-width` on the host (or grow it in their layout, e.g. `flex: 1`). **Keyboard focus: no ring** — `vfFocusUnderline` (§4) at
|
|
335
|
+
- **Visual (closed control):** height `var(--vf-popup-height, 18px)`, white bg, `1px solid black`, NO radius, `box-shadow: 1px 1px 0 0 var(--vf-black, #000)` (the small hard shadow visible in the screenshot; `no-shadow` removes it from the pill, and the open panel keeps its own), `padding: 0 8px 0 var(--vf-select-gutter, 16px)` — the left inset equals the option checkmark gutter so the selected label sits at the same x closed or open. *One documented 1px deviation:* the reference sheets put a closed pill's label ink 16px from its border box but an open menu's at 17 (three pill instances and four menu panels, all consistent, in the reference art). System 7 reconciled that by drawing the open panel 1px left of the pill; the kit instead uses one shared gutter with no panel offset, which keeps the closed↔open alignment exact and puts the closed pill's ink at 17. Bold label left, the black `CARET_DOWN` ▼ pixel glyph (inline SVG) right with 8px gap. The ▼ stays black even when the control is disabled (only the label dims). **Width:** the control hugs the *widest* option — no intrinsic min-width — via an invisible, height-collapsed stack of every option's text sharing the label's grid cell; so the closed pill and the open panel are always exactly the same width and the value never shifts as the selection changes. Authors wanting a floor set `min-width` on the host (or grow it in their layout, e.g. `flex: 1`). **Keyboard focus: no ring** — `vfFocusUnderline` (§4) at `calc(-2px - depth)`, where depth is the pill's shadow (1px, 0 under `no-shadow`): a dashed rule under the *whole* pill (hard shadow + blank row below the border box), not inside the face the way vf-button underlines its label: the pill's one line already holds the label and the ▼. Spans the border box, which is the padding box the pseudo-element sizes to (the pill's line is a stroke). **Closed only**, as `vf-menu`'s is: the open list already shows where focus is, and a panel short enough not to cover the rule — a one-option menu overlays the pill exactly — would leave a stray dashed line below it. Gated on a `.vf-focus-rule` class from the page's input modality, not `:focus-visible` — see §4 for why this control can't use the selector either. `npm run verify:focus`.
|
|
335
336
|
- **Visual (open):** panel uses the `.vf-panel` recipe but overrides `--vf-shadow-offset: 1px` so its hard shadow matches the pill's (not the 2px menu shadow). Positioned `position: fixed` to the control's exact width and left (unsnapped, so the panel's left/right edges and shadow coincide with the pill's). Item rows are height 16px — the pill's *content* height, derived as `calc(var(--vf-popup-height, 18px) - 2px)` so a re-themed pill moves its rows with it — `padding: 0 20px 0 var(--vf-select-gutter, 16px)`; the panel opens with the selected row's cell laid directly over the closed pill (its top border on the pill's top border, extending downward), so the selected label's position and surrounding whitespace are identical closed and open. The currently-selected item shows a ✓ checkmark in the left `--vf-select-gutter` column; hovered/active item inverts (black bg, white text); disabled options gray.
|
|
336
|
-
- **Visual (open, clipped):** a list taller than the screen is **clipped, never scrolled** — System 7 put no scrollbar on a menu. The panel is `overflow: hidden`
|
|
337
|
+
- **Visual (open, clipped):** a list taller than the screen is **clipped, never scrolled** — System 7 put no scrollbar on a menu. The panel is drawn once at a whole number of row slots, and a `.clip` element inside its stroke is `overflow: hidden` (the panel itself would clip at its outer edge, over its frame); the rows ride a `.rows` strip rolled by `transform` inside the clip, and the edge slot with items beyond it is covered by an opaque white **arrow slot** (`part="scroll-arrow"`, `aria-hidden`) carrying `CARET_UP` / `CARET_DOWN`. Traced from a real System 7 popup clipped at the screen edge (Find File's criteria menu under Infinite Mac, 2×): the 11×6 triangle sits 13px in from the panel's content edge and 5px down its 16px row, and the arrow occupies a **full** row slot. The pointer resting on an arrow rolls the list one row per `MENU_SCROLL_INTERVAL_MS` (66ms, motion.ts — the interaction itself, so *not* reduced-motion gated); the arrow retires when its direction runs out. Both can show at once. Four invariants, all in `src/popup-overflow.ts`:
|
|
337
338
|
- **The clamp is quantized to the pill lattice.** Rows are clipped a whole row at a time off `pillTop − selectedIndex × rowHeight`, so a clipped popup still opens with its selected row exactly over the closed pill — what an un-quantized pixel clamp loses. One deliberate exception: when the selected row would land *under* an arrow (the pill within one row of a screen edge with items beyond it), the scroll shifts by one and the overlay gives way by exactly one row. Rare, and purely geometric.
|
|
338
339
|
- **The panel is as tall as the *list* asked for, not as tall as the rows it can currently show** — capped only by the screen band. A list that fits the band but not where the pill would put it keeps every slot and slides whole rows, while the item strip stays welded to the pill; the slots the strip no longer reaches are drawn as **empty white**. The blank is the exact travel the list rolls through: scrolling to that end lands the strip flush with the panel, precisely full, both arrows retired. This is what System 7 drew — 5 empty rows above `name` in Find File's criteria popup, 2 above `Athens` in Character Set's font menu. **The two directions are one rule:** a panel pushed up (the pill is low) reserves its blank at the top and rolls down into it; a panel pushed down (the pill is high with a late item selected) reserves it at the bottom and rolls up into it. Only the sign of the scroll integer distinguishes them — it is free to leave the `[0, rowCount − visibleSlots]` range that would fill every slot, and that departure *is* the blank. It follows that the blank is a one-way starting position: an arrow means "rows are really hidden this way", so whichever end the blank is on, the arrow pointing back at it is off.
|
|
339
340
|
- **The panel box never moves or resizes while open:** scrolling rolls the items inside it.
|
|
@@ -368,7 +369,7 @@ The classic popup menu control ("Macintosh HD ▼").
|
|
|
368
369
|
|
|
369
370
|
#### `vf-menu` (`VfMenu`, vf-menu.ts)
|
|
370
371
|
- **Attributes/props:** `label: string` (the menu title in the bar; may contain e.g. an apple glyph), `open: boolean` (reflect, managed by menu-bar or self), `shortcuts: boolean` (reflect — the per-menu key-equivalent grant, for a standalone menu; in a bar, declare it once on the bar instead. See vf-menu-item).
|
|
371
|
-
- **Visual:** label: bold, height of menubar, `padding-inline: 9px 8px` — the label box is the title's black plate and hit rect, and Menus.png puts the plate 10px left / 9px right of the title ink (Chicago carries a 1px bearing on each side inside the text box, so 9/8 in layout lands −10/+9 around ink); open → inverted (black
|
|
372
|
+
- **Visual:** label: bold, height of menubar, `padding-inline: 9px 8px` — the label box is the title's black plate and hit rect, and Menus.png puts the plate 10px left / 9px right of the title ink (Chicago carries a 1px bearing on each side inside the text box, so 9/8 in layout lands −10/+9 around ink); open → inverted (black plate / white text), the plate a `::before` layer inset one system px top and bottom — the hilite is rows 1..18 of the 20px bar, leaving the bar's top row white and its bottom rule showing through, while the hit rect stays the full bar height. The title itself sits in a `.title` box inside that cell, so the focus rule can span the title and not the padding. In a bar the label is `role="menuitem"`; standalone it is `role="button"` — a collapsed standalone dropdown *is* the APG menu-button pattern, and `aria-haspopup` + `aria-expanded` are already right for it (the host itself is `role="none"` in a bar and role-less standalone, behind vf-menu-item's first-connect ownership latch). Panel: `.vf-panel`, `position: absolute` below the label (`top: 100%; left: 0;`), no inset beyond its 1px stroke, `min-width: 100%` — a menu is as wide as its widest row, the way the MDEF sized it (`Menus.png`'s File pulldown is 141px, its cm/inches popup 94), never narrower than its own bar title; `role="menu"`.
|
|
372
373
|
- **Keyboard focus: no ring** — `vfFocusUnderline` (§4) at `--vf-focus-underline-offset: -2px`, a dashed rule one blank system px row under the `.title` box. That box is `line-height: 1`, i.e. the face's own em, whose bottom edge is the descent line — so the one offset clears a descender *and* a slotted 16px `vf-img` (the Apple menu), where the button's baseline-anchored rule is crossed by both. In `currentColor`, so it inverts with the title on an open menu's black cell.
|
|
373
374
|
- **Closed only** (`:host(:not([open]))`). A dropped menu inverts its whole cell, which already shows where focus is; the rule marks the state the inversion can't — focused but not yet open — and drawing both would put a second mark (in white, since it is `currentColor`) under the first. The class stays on through the open state, so the rule returns by itself when the menu closes and hands focus back.
|
|
374
375
|
- Gated on a `.vf-focus-rule` class from the page's input modality, not `:focus-visible` (§4): `MenuPressController` `preventDefault`s the opening `pointerdown` and calls `focusLabel()` itself. The menu also calls the controller's `suppress()` from its own host `pointerdown`, so a press on an already-focused title — which moves no focus, and so fires no `focusin` — still drops the mark, as does mousing into the dropped panel. `npm run verify:focus`.
|
|
@@ -407,7 +408,7 @@ A container whose scrollbars look like System 7.
|
|
|
407
408
|
#### `vf-fieldset` (`VfFieldset`, vf-fieldset.ts)
|
|
408
409
|
The "Install Location" group box.
|
|
409
410
|
- **Attributes/props:** `legend: string`.
|
|
410
|
-
- **Visual:**
|
|
411
|
+
- **Visual:** a 1px black stroke (`vfStrokeDecls`), no radius, `padding: 14px 12px 10px` inside it, `margin-top: 8px` (room for legend). The padding is the slot's: the slot is the box inside the stroke, so children placed with `top`/`left` measure from the rule's inner edge. Legend: bold, positioned overlapping the top rule (absolute, `top: -11px; left: 8px;` from the rule's inner edge — whole system px, so a slotted `vf-label` legend lands on the device-pixel grid), `padding: 0 5px`, `background: var(--vf-surface, var(--vf-white, #fff))` so it punches out the rule to match its surface.
|
|
411
412
|
- **Slots:** default, plus named slot `legend` (overrides attr).
|
|
412
413
|
- **Parts:** `fieldset`, `legend`.
|
|
413
414
|
|
|
@@ -439,9 +440,9 @@ The kit's layout primitive: a flexbox whose `gap`, `pad`, `width` and `height` a
|
|
|
439
440
|
|
|
440
441
|
#### `vf-container` (`VfContainer`, vf-container.ts)
|
|
441
442
|
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.
|
|
442
|
-
- **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
|
|
443
|
+
- **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 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.
|
|
443
444
|
- **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.
|
|
444
|
-
- **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 tile repeats over whatever size the box has, declared or not. With `rule`, the same box wears `vfRule`'s per-edge classes (§4): the rule is
|
|
445
|
+
- **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 tile repeats over whatever size the box has, declared or not. With `rule`, the same box wears `vfRule`'s per-edge classes (§4): the rule is a stroke 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. A pattern's paper reaches the rule and its phase anchors inside it (`background-origin: content-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 shadow box coincides with the host box (`.vf-snap`, `height: 100%`), and the slot is the box inside its rules (`position: relative`, `display: flow-root`, `height: 100%`) — the coordinate system: the anchor for `top`/`left` children, the component's purpose, and what percentage fills resolve against; flow-root so a slotted margin cannot collapse through the top edge and push the coordinate origin off the corner. The snap correction on the shadow box carries it. Content that outgrows the declared box overflows it rather than growing it.
|
|
445
446
|
- **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.)
|
|
446
447
|
- **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.
|
|
447
448
|
- **`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.
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
154
|
"name": "vf-checkbox",
|
|
155
|
-
"description": "The classic System 7 checkbox: a
|
|
155
|
+
"description": "The classic System 7 checkbox: a 12×12 white square with a 1px black\nborder whose checked state is the corner-to-corner ✕ glyph — the pixel-exact\ncross traced from the Classic Macintosh UI Kit sprite, filling the 10×10\ninterior. The border \"thickens\" while pressed, exactly like the original\ncontrol.\n\nForm-associated: submits `value` under `name` when checked (like a native\ncheckbox) and restores its initial checked state on form reset. Toggles on\nclick and Space.\n---\n\n\n### **Events:**\n - **vf-change** - When toggled by user interaction. `detail: { checked: boolean }`.\n- **input** - Native event, dispatched from the host per user toggle (with `change`, the pair a native checkbox fires). A programmatic `checked` set fires nothing, as on a native checkbox.\n- **change** - Native event, dispatched from the host per user toggle so form delegation and framework bindings hear it.\n\n### **Slots:**\n - _default_ - The label, rendered to the right of the box with a 6px gap. Left empty, the label and its gap collapse: the control is the bare 12×12 box.\n\n### **CSS Parts:**\n - **box** - The 12×12 checkbox square.\n- **label** - The label wrapper around the slot.",
|
|
156
156
|
"attributes": [
|
|
157
157
|
{
|
|
158
158
|
"name": "checked",
|
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
},
|
|
205
205
|
{
|
|
206
206
|
"name": "vf-container",
|
|
207
|
-
"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` or `rule` say\nwhat to paint. No role, keyboard behavior or selection; what it holds\ndecides what it is.\n\n**`rule` draws the 1px rule on the edges it names** — `rule=\"bottom\"` is\nthe menu bar's anatomy (the box's rows over one row of ink), `rule=\"top\"`\na status strip's, all four a framed box. The rule is the
|
|
207
|
+
"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` or `rule` say\nwhat to paint. No role, keyboard behavior or selection; what it holds\ndecides what it is.\n\n**`rule` draws the 1px rule on the edges it names** — `rule=\"bottom\"` is\nthe menu bar's anatomy (the box's rows over one row of ink), `rule=\"top\"`\na status strip's, all four a framed box. The rule is drawn inside the\ndeclared size (vfRule), so a 24px `rule=\"bottom\"` strip is 23 rows\nof box over the line; content, `fill-width` children and placed children\nbegin inside it, as a rectangle's interior begins inside FrameRect's line.\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 repeating tile\nas the desktop dither, so it is 1-bit at every density and zoom and a box\nof any size, declared or not, costs nothing more (src/pattern-fill.ts).\nUnder 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 correction lands on the shadow box below, and the\ncoordinate system sits inside it, so the correction moves the whole\ncoordinate system — everything placed against it rides along instead of\nbeing 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.",
|
|
208
208
|
"attributes": [
|
|
209
209
|
{
|
|
210
210
|
"name": "pattern",
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
},
|
|
214
214
|
{
|
|
215
215
|
"name": "rule",
|
|
216
|
-
"description": "The 1px rule on the box's edges: edge names separated by spaces —\n`\"bottom\"`, `\"top bottom\"`, up to all four in any order. Drawn
|
|
216
|
+
"description": "The 1px rule on the box's edges: edge names separated by spaces —\n`\"bottom\"`, `\"top bottom\"`, up to all four in any order. Drawn inside\nthe declared size, in `--vf-black`, scaled with the display like every\nkit frame; content and placed children begin inside it.\nUnset, no rule; a value naming anything but an edge draws none and\nwarns once.",
|
|
217
217
|
"values": []
|
|
218
218
|
},
|
|
219
219
|
{
|
|
@@ -1332,7 +1332,7 @@
|
|
|
1332
1332
|
},
|
|
1333
1333
|
{
|
|
1334
1334
|
"name": "vf-separator",
|
|
1335
|
-
"description": "`<vf-separator>` — a 1px System 7 rule.\n\nHorizontal by default (1px tall, full width); set the `vertical` attribute\nfor a 1px-wide vertical rule (give it a height, or let a flex parent\nstretch it).\n\nContainers may restyle it via custom properties:\n- `--vf-separator-color` — line color (default `var(--vf-black, #000)`).\n- `--vf-separator-style` —
|
|
1335
|
+
"description": "`<vf-separator>` — a 1px System 7 rule.\n\nHorizontal by default (1px tall, full width); set the `vertical` attribute\nfor a 1px-wide vertical rule (give it a height, or let a flex parent\nstretch it).\n\nContainers may restyle it via custom properties:\n- `--vf-separator-color` — line color (default `var(--vf-black, #000)`).\n- `--vf-separator-style` — `solid` (the default) or `dotted`, one system px\n on and one off. `vf-menu` sets these so slotted separators render as the\n classic dimmed dotted menu rule (see Menus.png).\n---\n\n\n### **CSS Properties:**\n - **--vf-separator-color** - `vf-separator` rule color — `vf-menu` sets it to `--vf-disabled` for the dimmed menu rule _(default: undefined)_\n- **--vf-separator-style** - `vf-separator` rule style: `solid` or `dotted` — `vf-menu` sets `dotted` (see `Menus.png`) _(default: solid)_",
|
|
1336
1336
|
"attributes": [
|
|
1337
1337
|
{
|
|
1338
1338
|
"name": "vertical",
|
package/editor/web-types.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
|
|
3
3
|
"name": "vintage-frames",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.14.0",
|
|
5
5
|
"description-markup": "markdown",
|
|
6
6
|
"contributions": {
|
|
7
7
|
"html": {
|
|
@@ -322,7 +322,7 @@
|
|
|
322
322
|
},
|
|
323
323
|
{
|
|
324
324
|
"name": "vf-checkbox",
|
|
325
|
-
"description": "The classic System 7 checkbox: a
|
|
325
|
+
"description": "The classic System 7 checkbox: a 12×12 white square with a 1px black\nborder whose checked state is the corner-to-corner ✕ glyph — the pixel-exact\ncross traced from the Classic Macintosh UI Kit sprite, filling the 10×10\ninterior. The border \"thickens\" while pressed, exactly like the original\ncontrol.\n\nForm-associated: submits `value` under `name` when checked (like a native\ncheckbox) and restores its initial checked state on form reset. Toggles on\nclick and Space.\n---\n\n\n### **Events:**\n - **vf-change** - When toggled by user interaction. `detail: { checked: boolean }`.\n- **input** - Native event, dispatched from the host per user toggle (with `change`, the pair a native checkbox fires). A programmatic `checked` set fires nothing, as on a native checkbox.\n- **change** - Native event, dispatched from the host per user toggle so form delegation and framework bindings hear it.\n\n### **Slots:**\n - _default_ - The label, rendered to the right of the box with a 6px gap. Left empty, the label and its gap collapse: the control is the bare 12×12 box.\n\n### **CSS Parts:**\n - **box** - The 12×12 checkbox square.\n- **label** - The label wrapper around the slot.",
|
|
326
326
|
"doc-url": "",
|
|
327
327
|
"attributes": [
|
|
328
328
|
{
|
|
@@ -374,7 +374,7 @@
|
|
|
374
374
|
"slots": [
|
|
375
375
|
{
|
|
376
376
|
"name": "",
|
|
377
|
-
"description": "The label, rendered to the right of the box with a 6px gap. Left empty, the label and its gap collapse: the control is the bare
|
|
377
|
+
"description": "The label, rendered to the right of the box with a 6px gap. Left empty, the label and its gap collapse: the control is the bare 12×12 box."
|
|
378
378
|
}
|
|
379
379
|
],
|
|
380
380
|
"events": [
|
|
@@ -478,7 +478,7 @@
|
|
|
478
478
|
},
|
|
479
479
|
{
|
|
480
480
|
"name": "vf-container",
|
|
481
|
-
"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` or `rule` say\nwhat to paint. No role, keyboard behavior or selection; what it holds\ndecides what it is.\n\n**`rule` draws the 1px rule on the edges it names** — `rule=\"bottom\"` is\nthe menu bar's anatomy (the box's rows over one row of ink), `rule=\"top\"`\na status strip's, all four a framed box. The rule is the
|
|
481
|
+
"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` or `rule` say\nwhat to paint. No role, keyboard behavior or selection; what it holds\ndecides what it is.\n\n**`rule` draws the 1px rule on the edges it names** — `rule=\"bottom\"` is\nthe menu bar's anatomy (the box's rows over one row of ink), `rule=\"top\"`\na status strip's, all four a framed box. The rule is drawn inside the\ndeclared size (vfRule), so a 24px `rule=\"bottom\"` strip is 23 rows\nof box over the line; content, `fill-width` children and placed children\nbegin inside it, as a rectangle's interior begins inside FrameRect's line.\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 repeating tile\nas the desktop dither, so it is 1-bit at every density and zoom and a box\nof any size, declared or not, costs nothing more (src/pattern-fill.ts).\nUnder 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 correction lands on the shadow box below, and the\ncoordinate system sits inside it, so the correction moves the whole\ncoordinate system — everything placed against it rides along instead of\nbeing 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.",
|
|
482
482
|
"doc-url": "",
|
|
483
483
|
"attributes": [
|
|
484
484
|
{
|
|
@@ -488,7 +488,7 @@
|
|
|
488
488
|
},
|
|
489
489
|
{
|
|
490
490
|
"name": "rule",
|
|
491
|
-
"description": "The 1px rule on the box's edges: edge names separated by spaces —\n`\"bottom\"`, `\"top bottom\"`, up to all four in any order. Drawn
|
|
491
|
+
"description": "The 1px rule on the box's edges: edge names separated by spaces —\n`\"bottom\"`, `\"top bottom\"`, up to all four in any order. Drawn inside\nthe declared size, in `--vf-black`, scaled with the display like every\nkit frame; content and placed children begin inside it.\nUnset, no rule; a value naming anything but an edge draws none and\nwarns once.",
|
|
492
492
|
"value": { "type": "string | null | undefined" }
|
|
493
493
|
},
|
|
494
494
|
{
|
|
@@ -538,7 +538,7 @@
|
|
|
538
538
|
},
|
|
539
539
|
{
|
|
540
540
|
"name": "rule",
|
|
541
|
-
"description": "The 1px rule on the box's edges: edge names separated by spaces —\n`\"bottom\"`, `\"top bottom\"`, up to all four in any order. Drawn
|
|
541
|
+
"description": "The 1px rule on the box's edges: edge names separated by spaces —\n`\"bottom\"`, `\"top bottom\"`, up to all four in any order. Drawn inside\nthe declared size, in `--vf-black`, scaled with the display like every\nkit frame; content and placed children begin inside it.\nUnset, no rule; a value naming anything but an edge draws none and\nwarns once.",
|
|
542
542
|
"type": "string | null | undefined"
|
|
543
543
|
},
|
|
544
544
|
{
|
|
@@ -3110,7 +3110,7 @@
|
|
|
3110
3110
|
},
|
|
3111
3111
|
{
|
|
3112
3112
|
"name": "vf-separator",
|
|
3113
|
-
"description": "`<vf-separator>` — a 1px System 7 rule.\n\nHorizontal by default (1px tall, full width); set the `vertical` attribute\nfor a 1px-wide vertical rule (give it a height, or let a flex parent\nstretch it).\n\nContainers may restyle it via custom properties:\n- `--vf-separator-color` — line color (default `var(--vf-black, #000)`).\n- `--vf-separator-style` —
|
|
3113
|
+
"description": "`<vf-separator>` — a 1px System 7 rule.\n\nHorizontal by default (1px tall, full width); set the `vertical` attribute\nfor a 1px-wide vertical rule (give it a height, or let a flex parent\nstretch it).\n\nContainers may restyle it via custom properties:\n- `--vf-separator-color` — line color (default `var(--vf-black, #000)`).\n- `--vf-separator-style` — `solid` (the default) or `dotted`, one system px\n on and one off. `vf-menu` sets these so slotted separators render as the\n classic dimmed dotted menu rule (see Menus.png).\n---\n\n\n### **CSS Properties:**\n - **--vf-separator-color** - `vf-separator` rule color — `vf-menu` sets it to `--vf-disabled` for the dimmed menu rule _(default: undefined)_\n- **--vf-separator-style** - `vf-separator` rule style: `solid` or `dotted` — `vf-menu` sets `dotted` (see `Menus.png`) _(default: solid)_",
|
|
3114
3114
|
"doc-url": "",
|
|
3115
3115
|
"attributes": [
|
|
3116
3116
|
{
|