vintage-frames 0.12.8 → 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 +26 -26
- package/dist/chrome.d.ts +1 -1
- package/dist/components/vf-checkbox.d.ts +6 -5
- package/dist/components/vf-checkbox.js +41 -37
- package/dist/components/vf-container.d.ts +16 -20
- package/dist/components/vf-container.js +25 -17
- package/dist/components/vf-desktop.d.ts +4 -5
- package/dist/components/vf-desktop.js +13 -18
- 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.d.ts +2 -2
- package/dist/components/vf-progress-bar.js +65 -52
- 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-slider.js +11 -11
- package/dist/components/vf-swatch.d.ts +2 -2
- package/dist/components/vf-swatch.js +73 -72
- package/dist/components/vf-text-area.js +37 -35
- package/dist/components/vf-text-field.js +20 -18
- package/dist/components/vf-window.d.ts +6 -9
- package/dist/components/vf-window.js +106 -87
- package/dist/index.d.ts +20 -10
- package/dist/index.js +63 -62
- package/dist/pattern-fill.d.ts +21 -50
- package/dist/pattern-fill.js +19 -54
- package/dist/scroll-rail.d.ts +6 -6
- package/dist/scroll-rail.js +24 -29
- 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/pattern.d.ts +19 -10
- package/dist/styles/recipes/pattern.js +27 -18
- 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 +21 -13
- package/dist/styles/recipes/scroll-rail.js +27 -37
- 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/tile.d.ts +11 -3
- package/dist/styles/recipes/tile.js +12 -12
- 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/tile-grid.d.ts +98 -64
- package/dist/tile-grid.js +63 -30
- package/dist/track-width.d.ts +1 -2
- package/dist/zoom.d.ts +9 -9
- package/docs/SPEC.md +33 -32
- package/editor/vscode.html-custom-data.json +5 -5
- package/editor/web-types.json +8 -8
- package/package.json +1 -1
package/docs/SPEC.md
CHANGED
|
@@ -127,14 +127,14 @@ Every length in this doc is a **system pixel** value; components multiply it by
|
|
|
127
127
|
|
|
128
128
|
**Tiled fills.** A repeating `background-image` is the one place where "one system pixel is a whole number of device pixels" is not enough on its own. That contract holds — but the CSS *length* expressing it need not be one the engine can store: Chromium lays out in 1/64 CSS px (Gecko in 1/60), and at `--vf-scale` 4/3 a 2-system-px tile is 2.6667 CSS px, held as 2.65625. A single edge survives that, because paint snaps each box to the device grid on its own — which is why every border, stepped corner and magnified icon rasterizes exactly at 4/3. A tiled fill does not: it is ONE snapped box holding N *unsnapped* repeats, each placed at `k × tileSize`, so the error compounds until the tile boundary has walked a whole device pixel. Unfixed, 75% of the desktop dither rasterizes to mid-gray at dpr 1.5.
|
|
129
129
|
|
|
130
|
-
The
|
|
130
|
+
The kit's tiled surfaces — the desktop and container patterns, the windoid dots, the swatch checker, the barber stripes and the scroll trough — answer it with one tile (`src/tile-grid.ts`):
|
|
131
131
|
|
|
132
|
-
- **Kit art
|
|
133
|
-
- **A consumer pattern token renders as a flat grid of placed tiles** at the token's documented 30/60-system-px tile geometry: each tile positioned by one single-multiplication `calc()` quantized once, every box paint-snapped independently — never laid out with CSS grid/flex/flow, whose summed track sizes would re-import the accumulation. The token still overrides the whole **tile**, not the motif; a token swapped at runtime without touching the component needs a `requestUpdate()`.
|
|
132
|
+
- **Kit art is a CSS repeat of one tile 120 system px square, drawn at the display's resolution** (`repeatTile`, kept current on a box by `RepeatTileController`, sized by `vfRepeatTileSize`). Each system px of the motif is an n × n block of image px, n = `--vf-scale × trueDpr`, so the engine copies the tile to the device grid 1:1 and has no filtering to choose — Safari smooths a repeated image it has to magnify, whatever `image-rendering` says. 120 is a whole number of every motif the kit draws (2, 4, 8 and 12), and 120n device px is a whole number of layout px in every engine at every density and zoom (40n at 3×, 60n at 2×), so each repeat lands exactly. The size is large on purpose: WebKit on a 3× iPhone now and then draws a small tile one device px narrow and resamples it, even at an exact length; tiles of 192 device px and up never showed it (TILE-REPEAT-PLAN.md, in git history at commit 5bc45c2). The tile depends only on the art and n: it is encoded once per pair for the whole page, drawn again when zoom or the display changes n, and never on a resize. Each surface keeps `image-rendering: pixelated`, which covers the frame between a zoom and the new tile. (Raster, not SVG, because Chromium rasterizes an SVG image at the box's *stored* fractional size and ignores `image-rendering` for SVG.)
|
|
133
|
+
- **A consumer pattern token renders as a flat grid of placed tiles** at the token's documented 30/60-system-px tile geometry: each tile positioned by one single-multiplication `calc()` quantized once, every box paint-snapped independently — never laid out with CSS grid/flex/flow, whose summed track sizes would re-import the accumulation. A token is any CSS image, SVG included, so it cannot be pre-scaled the way kit art is. The token still overrides the whole **tile**, not the motif; a token swapped at runtime without touching the component needs a `requestUpdate()`.
|
|
134
134
|
|
|
135
|
-
The span construction
|
|
135
|
+
The span construction (`vfTileSize`, `tileImage`, `tileSpan`) still sizes what isn't a kit tile: a token's documented tile, and the forced-colors mask branches that repaint a surface's art in the ink token (no mask pipeline rasterizes exactly at a zoom-minted scale, so forced-colors-plus-zoom remains the one accepted residual). A fill authored as a **motif** spans `lcm(motif, 15)` system px — a whole number of motifs, and holdable at every scale the density ladder derives, because a scale `p/q` needs `q` to divide `64 × span`, the 64 absorbs every power of two, and every derived scale (1, 3/2, 4/3, 8/5, 6/5, 5/4) has an odd part of 1, 3 or 5. The scales zoom mints (20/17 at Safari's 85%, 30/23 at its 115%) no span holds. `npm run verify:tile` asserts each kit surface's tile geometry and zero gray at eight densities — the ladder plus 1.7 and 2.3, the emulated stand-ins for Safari's zoom rungs; `npm run verify:scrollbars` asserts the trough's lattice pixel by pixel. No headless engine reproduces Safari's smoothing; `tile-repeat-probe.html` is the check in real browsers (docs/DEVELOPING.md).
|
|
136
136
|
|
|
137
|
-
**Patterns.** The 38 standard patterns (`src/patterns.ts`; the table is docs/PATTERNS.md) are 8-byte QuickDraw `Pattern`s in palette order and QuickDraw phase. `gray-50`, `gray-25` and `dots` are the desktop dither, the scroll trough and the windoid bar's motifs, and those surfaces draw them by reference — `patternMotif` on the minimal cell, so the 2×2 / 4×2 rect data and the 30/60-px token spans are the ones they always had. `vf-container pattern` and `vf-desktop pattern` take a name or sixteen hex digits and paint the pattern as the box's **own background** (`PatternFillController` + `vfPatternFill`, src/pattern-fill.ts): the
|
|
137
|
+
**Patterns.** The 38 standard patterns (`src/patterns.ts`; the table is docs/PATTERNS.md) are 8-byte QuickDraw `Pattern`s in palette order and QuickDraw phase. `gray-50`, `gray-25` and `dots` are the desktop dither, the scroll trough and the windoid bar's motifs, and those surfaces draw them by reference — `patternMotif` on the minimal cell, so the 2×2 / 4×2 rect data and the 30/60-px token spans are the ones they always had. `vf-container pattern` and `vf-desktop pattern` take a name or sixteen hex digits and paint the pattern as the box's **own background** (`PatternFillController` + `vfPatternFill`, src/pattern-fill.ts): the pattern's motif as a kit tile (above), literal black ink on the `--vf-white` paper the recipe supplies. A background rather than a child element, so a patterned box is no stacking context (a slotted control's fixed panel is never trapped under a later sibling) and keeps `overflow: visible`; `image-rendering` inherits, so the recipe hands `auto` back to the slot. Phase is anchored at the box's padding-box origin. Forced colors: flat Canvas, the desktop's posture. `npm run verify:pattern` asserts the library, the three motif equivalences, the tile's geometry and zero gray at the same eight densities.
|
|
138
138
|
|
|
139
139
|
**`--vf-cursor` (hiding the pointer for a page-drawn cursor).** A page that draws its own cursor — a JS-positioned image on the system-pixel grid, which is what `applyCursor()` (`src/cursor.ts`) sets up with the kit's embedded System 7 pointer set — needs the native pointer gone from every surface, and `html { cursor: none }` alone cannot get it there: inheritance stops wherever a shadow stylesheet states a cursor of its own, and the kit states one deliberately on its chrome — control hosts, the title-bar widgets, the number stepper, the scrollbar rails, the grow box, the editable wells (whose I-beam Firefox's UA sheet would otherwise pin), and a modal's top-layer backdrop, which no page rule reaches at all. Every one of those declarations reads this token first, so `--vf-cursor: none` on `:root` (custom properties inherit through every shadow root) covers all of them. The page's own side must be a blanket, not a root rule: the UA sheets put `cursor: default` on `<button>` and `cursor: text` on `<input>`, and an element's own declaration beats anything it would inherit — so a bare `html { cursor: none }` leaves the OS arrow alive over every native control in the light DOM. The pair that empties everything is `* { cursor: none !important }` plus the token — `applyCursor()` applies both itself, once its art has decoded; state the pair by hand only under a hand-rolled overlay. Unset, each spot keeps its classic pointer — the arrow on chrome, the I-beam in an enabled well.
|
|
140
140
|
|
|
@@ -144,26 +144,27 @@ The span construction stays load-bearing for the CSS-repeated underlays beneath
|
|
|
144
144
|
- `vfBodyDecls` — the three declarations that put text on the Geneva body face (family, 16px, smoothing), for composing into one rule; the mirror of `vfDisplayDecls`, and the body face's single definition (`vfBase` applies it to every host; `face="body"` switches back to it).
|
|
145
145
|
- `vfStaticText` — the `face` / `size` / `dim` host switches shared by `vf-label` and `vf-paragraph`. All `:host([attr])`, one specificity step above the plain `:host` rule each component sets its own default face in.
|
|
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
|
-
- `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).
|
|
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
|
|
149
|
-
- `
|
|
150
|
-
- `
|
|
151
|
-
- `
|
|
152
|
-
- `
|
|
153
|
-
- `
|
|
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
|
+
- `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
|
+
- `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.
|
|
159
160
|
- The markup and the four DragController pointer bindings that go with it are `chromeTitleBar()` in `src/chrome.ts` (internal — it bakes in the kit's own `part` names, so it is not part of the public toolkit). Its third argument picks the texture layer class: `'vf-stripes'` (default) or `'vf-dots'` (`vf-window variant="utility"`).
|
|
160
|
-
- `vfScrollRail` — the System 7 scroll rail, drawn by the kit as ordinary shadow DOM (`src/scroll-rail.ts` renders the subtree and drives it; this is its skin). The native scrollbar is hidden — `scrollbar-width: none` plus a `::-webkit-scrollbar { display: none }` for older WebKit, the last scrollbar pseudo in the kit — never the native scrolling: wheel, trackpad momentum, keyboard, touch and AT scrolling stay the platform's — minus the rubber-band: the scroller sets `overscroll-behavior: none` (a bounce moves content the rail cannot express; System 7 stopped hard), which also cuts scroll chaining, so a document that scrolls restores it where it wants it through the scroller's part (`viewport`, `textarea`, `list`, `content`; the reference page does, dialogs excepted) — and `ScrollRailController` syncs the rail to `scrollTop`/`scrollLeft` while driving the classic interactions (thumb drag; trough paging by a viewport minus one line, repeating until the thumb reaches the pointer; arrow lines with auto-repeat and the hollow→solid pressed glyph). Geometry: the classic 16px cell whose outermost line is the component's own 1px frame border — the rail element is the 15 inside it, a 1px divider on the content side plus the 14px channel; 15px arrow cells whose glyphs are the 16-unit sprites windowed to their 14×14 interior; the **fixed** 16px thumb (System 7's box, never proportional; whole-system-px travel); the 25% dot-dither trough
|
|
161
|
+
- `vfScrollRail` — the System 7 scroll rail, drawn by the kit as ordinary shadow DOM (`src/scroll-rail.ts` renders the subtree and drives it; this is its skin). The native scrollbar is hidden — `scrollbar-width: none` plus a `::-webkit-scrollbar { display: none }` for older WebKit, the last scrollbar pseudo in the kit — never the native scrolling: wheel, trackpad momentum, keyboard, touch and AT scrolling stay the platform's — minus the rubber-band: the scroller sets `overscroll-behavior: none` (a bounce moves content the rail cannot express; System 7 stopped hard), which also cuts scroll chaining, so a document that scrolls restores it where it wants it through the scroller's part (`viewport`, `textarea`, `list`, `content`; the reference page does, dialogs excepted) — and `ScrollRailController` syncs the rail to `scrollTop`/`scrollLeft` while driving the classic interactions (thumb drag; trough paging by a viewport minus one line, repeating until the thumb reaches the pointer; arrow lines with auto-repeat and the hollow→solid pressed glyph). Geometry: the classic 16px cell whose outermost line is the component's own 1px frame border — the rail element is the 15 inside it, a 1px divider on the content side plus the 14px channel; 15px arrow cells whose glyphs are the 16-unit sprites windowed to their 14×14 interior; the **fixed** 16px thumb (System 7's box, never proportional; whole-system-px travel); the 25% dot-dither trough repeating the kit tile (*Tiled fills*; 1-bit at every scale, zoom included) on `var(--vf-scrollbar-track)`. States key off the attributes `ScrollStateController` writes — an idle axis keeps its arrows (drawn but inert) and drops the dither and thumb, an inactive window blanks both axes to the bare white channel, arrows included, a degenerate track drops the thumb, then everything (the Control Manager's decision table). The rail subtree is `aria-hidden` and pointer-only; scrolling's keyboard/AT contract stays on the viewport. Every engine renders the same rail — no Firefox fallback — and it is pixel-asserted headless (`npm run verify:scrollbars`), which the `::-webkit-scrollbar` skin it replaced never could be.
|
|
161
162
|
- `vfFocusRing` / `vfFocus` / `vfFocusUnderline` — the two focus indicators. Neither is a System 7 reproduction: the machine drew no keyboard focus at all (see §1), so both are additions rendered in its vocabulary.
|
|
162
163
|
- `vfFocusRing` is the dotted-outline declaration pair — `outline: var(--vf-focus-outline, 1px dotted currentColor)` plus a scaled `--vf-focus-offset` (default +2px) — interpolated into whatever selector a component focuses on; `vfFocus` wraps it as a `.vf-focus:focus-visible` class for the controls where focus and ring share one element. `currentColor`, not black, for the same reason the underline uses it: the ring must invert with the ink it sits on, or the multi-select keyboard cursor disappears riding a selected list row's black bar.
|
|
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:
|
|
@@ -197,7 +198,7 @@ Every recipe also declares its size — `width`, and `height` where the window i
|
|
|
197
198
|
Full-bleed classic desktop container.
|
|
198
199
|
- **Attributes/props:** `width`, `height: number` (system px, defaults 512×342 — the compact Mac's raster) — the screen's own size, the way a WIND resource declared a window's. The host box renders at exactly `width + 2·bezel` per axis (a live `sysLength` written as host inline size on every update), always a whole number of system px. **Pure CSS sizing is not supported**: the inline size wins over any stylesheet, so the page sets the numbers — directly or via `fitWithin` — and positions the explicitly sized desktop with its own CSS, keeping any sub-system-pixel slack in the page's own layout. `bezel: number` (system px, default 0) — the black screen surround, the CRT's unlit margin between raster and case, added onto the declared screen on every side. The screen owns flow, an absolute window's containing block and the drag clip, so a dragged window crops at the raster's edge. Its four corners wear the `SCREEN_CORNER` mask (the bottom pair mirrored vertically — `steppedCornerClip`'s `edge`), painted above everything like the hardware mask — the top pair lands over a slotted menu bar's corners, so the bar needs no `rounded` of its own inside a bezeled desktop. The bezel width is written onto the host as `--vf-desktop-bezel` (self-set geometry, like `--vf-scale` — component-owned, not a theming token). `pattern: string` (default `'gray-50'`) — the desktop pattern, System 7's General Controls setting: a library name (docs/PATTERNS.md) or sixteen hex digits stating a custom 8×8; an unrecognized value warns once and keeps the dither. A `--vf-desktop-pattern` token override wins over it.
|
|
199
200
|
- **Methods:** `fitWithin(maxWidth, maxHeight)` (CSS px) → sets `width`/ `height` to the largest whole-system-px raster whose host box — bezel included — fits the bound, per the current effective scale, and returns `{ width, height }`. The page's half of the sizing contract: call it on `resize` and `onScaleChange` (zoom and density moves change what a system px costs in CSS px), as a full-screen faux desktop does. `clearActive()` → deactivate the whole document tier (see *Deactivation* under Behavior). `activeWindow: HTMLElement | null` (read-only) → the active document-tier window, or `null` while the tier is deactivated or empty; utility windows are never the holder. `placementAt(clientX, clientY, child?)` → a viewport point as a placement on the screen, `{ left, top }` in whole system px on the placement lattice, measured from the raster's corner with the bezel excluded — the pair a child dropped onto the desktop is written with; with `child`, the pair to write to that element, its `origin` offset folded in (`placementIn`, src/position.ts; the anchor sits in shadow, which is why it is a method).
|
|
200
|
-
- **Visual:** `display: block; position: relative;` — the paint lives on an inner screen surface (part `desktop`, `overflow: hidden` — the whole-system-px raster, inset by `bezel` when one is set). Screen surface = the `pattern` (the classic 50% dither, `gray-50`, by default), painted as the screen's own background by the pattern fill (*Patterns* under *Tiled fills*): black ink on the fill's opaque `--vf-white` paper,
|
|
201
|
+
- **Visual:** `display: block; position: relative;` — the paint lives on an inner screen surface (part `desktop`, `overflow: hidden` — the whole-system-px raster, inset by `bezel` when one is set). Screen surface = the `pattern` (the classic 50% dither, `gray-50`, by default), painted as the screen's own background by the pattern fill (*Patterns* under *Tiled fills*): black ink on the fill's opaque `--vf-white` paper, a repeating tile — the authentic black-on-white dither. A consumer `--vf-desktop-pattern` renders instead as a placed tile grid on the token's 30-system-px tile with no paper, so `var(--vf-desktop, #808080)` beneath shows only under a custom tile with transparent cells (or `none`). Forced colors: flat Canvas.
|
|
201
202
|
- **Slots:** default (menu bar, windows, anything).
|
|
202
203
|
- **Behavior:** manages stacking of slotted `vf-window` children: `pointerdown` *or `focusin`* on a window brings it to front (incrementing z-index counter) and sets its `active` attribute, clearing `active` on the others — the `focusin` half is the keyboard route to activation: Tab landing anywhere in a background window (its undrawn-but-focusable widgets included) raises it. Listens via delegated pointerdown/focusin listeners + `slotchange`. Windows slotted before `vf-window` is defined are re-normalized once `customElements.whenDefined('vf-window')` settles, since the upgrade reflects each window's `active = true` default back out and upgrading a slotted node doesn't re-fire `slotchange`.
|
|
203
204
|
- **DOM order follows z-order** (bottom-most first), so sequential focus order matches the visual stack and Shift+Tab mirrors Tab exactly. The sync runs in a task after a pointer gesture ends, or after a programmatic `bringToFront` — never mid-gesture (a node move would clear the pointer capture a title-bar drag or grow-box resize holds), never between a press's release and its `click` (Chromium drops a click whose mousedown node was re-inserted first, which used to cost a control in a background window its click), and never from a focus-driven raise (moving the window focus just entered would re-order the tab sequence mid-traversal); focus surviving its own window's move is restored without re-raising that window. Non-window children (a menu bar, page content) keep their positions. `npm run verify:window-a11y`.
|
|
@@ -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`.
|
|
@@ -396,7 +397,7 @@ Classic list box.
|
|
|
396
397
|
#### `vf-scroll-area` (`VfScrollArea`, vf-scroll-area.ts)
|
|
397
398
|
A container whose scrollbars look like System 7.
|
|
398
399
|
- **Attributes/props:** `axis: 'vertical' | 'horizontal' | 'both'` (default `'vertical'`, reflected) — which scroll rails to reserve as permanent placeholders (see "always-a-rail" below); `corner: boolean` (default false, reflected) — reserves the bottom-right corner cell on a single-axis rail (the rail stops 15px short of the frame, the viewport spans the rest), for a grow box to land in; inert on `both`, which always has the cell; `label: string` — accessible name for the viewport (`aria-label` on the viewport, since an `aria-label` on the host cannot reach into the shadow DOM). The viewport is a **Tab stop only while its content actually overflows** — the same state `ScrollStateController` measures for the rails; a fitting scroll area used to be a focusable stop with `role: generic` and no name, a dead Tab press. Whenever it is a stop it carries a role: `role="region"` when `label` names it (a named landmark), `role="group"` when not (an unnamed region is inert, so that role is reserved for the labelled case).
|
|
399
|
-
- **Visual:** `display: block`; the snapped wrapper carries a real 1px black frame and a grid reserving each rail as its own edge column/row — `[viewport | vertical rail]` over `[horizontal rail | corner]` — with the white inner viewport carrying no inset of its own, so slotted content and the (0,0) of placed children sit exactly 1 system px from the frame box at every scale; an inset is the content's (a `vf-stack pad`). Consumer sets width/height on host. The scrolled plane — the wrapper the slot renders into, and the positioned anchor for placed children — is `width: fit-content; min-width: 100%`: never narrower than the viewport, as wide as content that cannot wrap, so the controllers' ResizeObserver on it sees a row growing sideways the way it sees copy growing down (a block wrapper's auto width was the viewport's whatever the row did, and a row gaining a cell left the thumb and the overflow state stale until something else re-measured); copy wraps as before, and a `position: sticky; left: 0` child holds across the scroll, the plane being its containing block. `min-height: 100%` likewise: a `fixed` child (§2 Explicit placement) is a sticky box the plane contains, and a plane shorter than the child's stated `top` would clamp it to the plane's bottom edge; content that fits still fits. In the overflowing case a percentage-width or centered block child resolves against the grown plane, not the viewport. A `slotchange` re-measures; `measure()` covers a scroll range that changes with no box changing. The rails are the drawn `vfScrollRail` subtree (§4 — the classic 16px cell counting the frame line: divider, 14px channel, 15px arrow cells, the fixed 16px thumb, the
|
|
400
|
+
- **Visual:** `display: block`; the snapped wrapper carries a real 1px black frame and a grid reserving each rail as its own edge column/row — `[viewport | vertical rail]` over `[horizontal rail | corner]` — with the white inner viewport carrying no inset of its own, so slotted content and the (0,0) of placed children sit exactly 1 system px from the frame box at every scale; an inset is the content's (a `vf-stack pad`). Consumer sets width/height on host. The scrolled plane — the wrapper the slot renders into, and the positioned anchor for placed children — is `width: fit-content; min-width: 100%`: never narrower than the viewport, as wide as content that cannot wrap, so the controllers' ResizeObserver on it sees a row growing sideways the way it sees copy growing down (a block wrapper's auto width was the viewport's whatever the row did, and a row gaining a cell left the thumb and the overflow state stale until something else re-measured); copy wraps as before, and a `position: sticky; left: 0` child holds across the scroll, the plane being its containing block. `min-height: 100%` likewise: a `fixed` child (§2 Explicit placement) is a sticky box the plane contains, and a plane shorter than the child's stated `top` would clamp it to the plane's bottom edge; content that fits still fits. In the overflowing case a percentage-width or centered block child resolves against the grown plane, not the viewport. A `slotchange` re-measures; `measure()` covers a scroll range that changes with no box changing. The rails are the drawn `vfScrollRail` subtree (§4 — the classic 16px cell counting the frame line: divider, 14px channel, 15px arrow cells, the fixed 16px thumb, the dot-dither trough), rendered as later siblings of the viewport and synced to its native scrolling by `ScrollRailController`; the viewport carries the `vf-scroll` class, which hides the native bar without touching the native scrolling. When both rails are reserved, the corner cell joins them, supplying the interior dividers the adjacent arrow cells leave to it; `corner` reserves the same 15×15 cell beside a single rail, its other divider continuing the rail's line across the cell — DrawGrowIcon's form, for the grow box. Every engine renders the same rail; the old `::-webkit-scrollbar` skin, its `.vf-scroll-frame` overlay contortion (WebKit quantized native scrollbar rects to whole CSS px) and the Firefox `scrollbar-color` fallback are all retired.
|
|
400
401
|
- **Always-a-rail behavior:** each *reserved* axis (per `axis`) renders its rail element as a permanent placeholder — arrow buttons on an empty white channel (dither off, no thumb; System 7 drew the arrows on any bar in an active window, and a scroller outside a window always counts as active) — until the content overflows that axis, when the dither and thumb fill in and the arrows go live (an idle axis's arrows are drawn but inert — the press guard in `ScrollRailController` skips them). `ScrollStateController` (`src/scroll-state.ts`) measures both axes and writes `data-overflow-x` / `data-overflow-y` (`"true"` / `"false"`) on the scroll element; the recipe keys the dither and thumb off those attributes. The unreserved axis still scrolls natively (wheel, keyboard) but draws no rail. Shared by vf-list and vf-text-area; a future `@container scroll-state(scrollable)` query could replace the JS for slotted-content components.
|
|
401
402
|
- **Inactive-window blanking:** the HIG's non-frontmost window must not display interactive scroll UX, so the controller also finds the nearest `vf-window` up the composed tree (light-DOM ancestor for a slotted scroller, shadow ancestor for `vf-window[scrollbars]`'s own edge rails), watches its reflected `active` attribute, and toggles a presence-only `data-window-inactive` on the scroll element. While present, the recipe empties dither/thumb/arrows on BOTH axes regardless of overflow — the bare channel, arrows included (unlike the idle rail, which keeps them), exactly as System 7 blanked a deactivated window's bars (its List Manager/TextEdit deactivated in-window scrollbars too). No `vf-window` ancestor → the attribute never appears: dialogs have no inactive state and a bare scroll component always draws live. Like the overflow half, this signal could one day go declarative — a custom property cascaded under `vf-window:not([active])` gating the recipe via an `@container style()` query; the rails being ordinary DOM, that migration is a plain selector swap (see the FUTURE note in scroll-state.ts).
|
|
402
403
|
- **Document-window (TeachText) composition:** to put the rails on a window's edge rather than inset in its body, slot the scroll area into a `vf-window` sized `calc(100% + 2px·scale)` with `margin: -1px·scale` — one system pixel under the window frame on every side. Its own frame border then repaints the window's border lines exactly (no doubled frame), and a resizable window's grow box lands exactly over the rail's corner cell, giving the classic System 7 document window. `vf-window[scrollbars]` renders exactly this composition from its own shadow tree (forwarding `corner` for a resizable window without a status strip), so the one-liner and the slotted form are geometrically identical; slot it yourself when the well should sit inside the body instead of on its edge (the installer's read-me well, placed with `top`/`left`).
|
|
@@ -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
|
|
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
|
{
|
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
},
|
|
252
252
|
{
|
|
253
253
|
"name": "vf-desktop",
|
|
254
|
-
"description": "`<vf-desktop>` — the full-bleed classic desktop container.\n\nRenders the desktop pattern — the classic 50% dither by default, or any of\nthe standard patterns by name (`pattern`, System 7's General Controls\nsetting) — and manages the stacking order and `active` state of slotted\n`vf-window` children: a `pointerdown` or\n`focusin` (keyboard focus) anywhere inside a window brings it to the front\nand makes it the single active window. The windows' light-DOM order is kept\nin step with the stacking order (bottom-most first, synced in a task once\nany pointer gesture has ended), so tabbing walks the stack the way the eye\ndoes and Shift+Tab is its exact mirror.\n\nUtility windows (`vf-window[variant=\"utility\"]`) stack in a floating tier\nabove every document-tier window, restack only among themselves, and stand\noutside the single-active invariant entirely — clicking a palette neither\ndeactivates the active document window nor greys the palette, exactly as\nSystem 7's floating windoids behaved while their application was frontmost.\nA slotted `vf-menu-bar` (or a free-standing `vf-menu`) sits on a tier\nabove both, so its dropped menus cover palettes and document windows\nalike; only the screen-corner mask is in front of it.\n\n**Deactivation.** On a real System 7 machine clicking the desktop clicked\nthe *Finder* — the frontmost application's windows lost their stripes.\nclearActive is that gesture's handler: it clears `active` from\nthe whole document tier, and **zero active windows is a legal state**,\nheld until a press or keyboard focus re-enters a document window (or a\nnew one is slotted, which activates it — opening a window brings its\napplication forward). The desktop never takes this decision itself: its\nfurniture is slotted light DOM (an icon layer, say), so only the page\nknows which of its children — or which presses on the bare dither — mean\n\"the Finder\", and it routes those through `clearActive()`. Left alone,\nthe classic always-one-active behavior is unchanged. activeWindow\nreads the current holder, and every change of holder — including to and\nfrom none — fires `vf-activate`.\n\nThe desktop is a raster with an explicit size, always: **`width` and\n`height`**, in system px, the way a WIND resource declared a window's —\nthe host box renders at the declared screen plus `2 × bezel` per axis, a\nwhole number of system pixels by construction (default 512×342, the\ncompact Mac's screen). Pure CSS sizing is not supported; the page sets\nthe numbers — directly, or via VfDesktop.fitWithin on\n`resize`/`onScaleChange` for a viewport-filling desktop — and positions\nthe sized box with its own stylesheet, keeping any sub-system-pixel\nslack on its side. `bezel` (system px) draws the black screen surround —\nthe CRT's unlit margin — around the screen, rounding its four corners\nwith the classic corner mask.\n\n**`pattern`** names the desktop pattern — `gray-50` (the dither) by\ndefault, any of the 38 standard patterns (docs/PATTERNS.md), or sixteen\nhex digits stating a custom 8×8 pattern, as on `vf-container`. It is\npainted as the screen's own background: black ink on an opaque white\npaper,
|
|
254
|
+
"description": "`<vf-desktop>` — the full-bleed classic desktop container.\n\nRenders the desktop pattern — the classic 50% dither by default, or any of\nthe standard patterns by name (`pattern`, System 7's General Controls\nsetting) — and manages the stacking order and `active` state of slotted\n`vf-window` children: a `pointerdown` or\n`focusin` (keyboard focus) anywhere inside a window brings it to the front\nand makes it the single active window. The windows' light-DOM order is kept\nin step with the stacking order (bottom-most first, synced in a task once\nany pointer gesture has ended), so tabbing walks the stack the way the eye\ndoes and Shift+Tab is its exact mirror.\n\nUtility windows (`vf-window[variant=\"utility\"]`) stack in a floating tier\nabove every document-tier window, restack only among themselves, and stand\noutside the single-active invariant entirely — clicking a palette neither\ndeactivates the active document window nor greys the palette, exactly as\nSystem 7's floating windoids behaved while their application was frontmost.\nA slotted `vf-menu-bar` (or a free-standing `vf-menu`) sits on a tier\nabove both, so its dropped menus cover palettes and document windows\nalike; only the screen-corner mask is in front of it.\n\n**Deactivation.** On a real System 7 machine clicking the desktop clicked\nthe *Finder* — the frontmost application's windows lost their stripes.\nclearActive is that gesture's handler: it clears `active` from\nthe whole document tier, and **zero active windows is a legal state**,\nheld until a press or keyboard focus re-enters a document window (or a\nnew one is slotted, which activates it — opening a window brings its\napplication forward). The desktop never takes this decision itself: its\nfurniture is slotted light DOM (an icon layer, say), so only the page\nknows which of its children — or which presses on the bare dither — mean\n\"the Finder\", and it routes those through `clearActive()`. Left alone,\nthe classic always-one-active behavior is unchanged. activeWindow\nreads the current holder, and every change of holder — including to and\nfrom none — fires `vf-activate`.\n\nThe desktop is a raster with an explicit size, always: **`width` and\n`height`**, in system px, the way a WIND resource declared a window's —\nthe host box renders at the declared screen plus `2 × bezel` per axis, a\nwhole number of system pixels by construction (default 512×342, the\ncompact Mac's screen). Pure CSS sizing is not supported; the page sets\nthe numbers — directly, or via VfDesktop.fitWithin on\n`resize`/`onScaleChange` for a viewport-filling desktop — and positions\nthe sized box with its own stylesheet, keeping any sub-system-pixel\nslack on its side. `bezel` (system px) draws the black screen surround —\nthe CRT's unlit margin — around the screen, rounding its four corners\nwith the classic corner mask.\n\n**`pattern`** names the desktop pattern — `gray-50` (the dither) by\ndefault, any of the 38 standard patterns (docs/PATTERNS.md), or sixteen\nhex digits stating a custom 8×8 pattern, as on `vf-container`. It is\npainted as the screen's own background: black ink on an opaque white\npaper, a repeating tile drawn at the display's resolution, 1-bit at every\ndensity and zoom (src/pattern-fill.ts).\n\n**The drag surface.** The Finder dragged an icon as a dotted outline drawn\non the *screen*, clipped at its edge and phase-locked to its raster, so\nthe screen hosts the layer that outline draws on: a full-bleed child of\nthe screen, `pointer-events: none` (never a hit for the page's\n`elementsFromPoint`), whose children paint one tier above the menu bar\nand under the corner mask — the layer itself is deliberately no stacking\ncontext, since a blend inside one would composite against nothing. An\nicon reaches it without an import, the menu-handshake idiom: on a drag's\nfirst step it dispatches `vf-drag-surface-request` (bubbles, non-composed,\n`detail.surface` null), and the nearest desktop on its light-DOM path\nfills the detail with the layer. A window asks the same way and draws\nthere with the same pen: its zoom rects when it opens from a rect\n(`vf-window.show({ from })`), its outline under an `outline-drag`. Not the\ntop layer: stable Safari cannot\nblend an element there against the page (src/cursor.ts), and a second\nkit entry in the top layer would need a re-promotion handshake.\n\nCustom properties:\n- `--vf-desktop-pattern` — a consumer's own tile art in place of the\n pattern (the kit's default is a 1-bit 50% checker, opaque\n black-on-white on a 30-system-px tile). Set, it wins over `pattern` and\n renders as a placed tile grid at that same 30-px geometry\n (src/tile-grid.ts); a token swapped at runtime without touching the\n component wants a `requestUpdate()`.\n- `--vf-desktop` — base color painted *under* the pattern (default\n `#808080`). The pattern's paper is opaque, so this only becomes visible\n when `--vf-desktop-pattern` is overridden with a tile that has\n transparent cells (or with `none`).\n---\n\n\n### **Events:**\n - **vf-activate** - The active document-tier window changed. Detail `{ window: HTMLElement | null }` — the new holder, or `null` when the document tier deactivated (a {@link clearActive} call, or the active window leaving the DOM with none behind it). Fired once per change of holder, never for a re-assertion of the same one.\n\n### **Slots:**\n - _default_ - Default slot: menu bar, windows, anything.\n\n### **CSS Properties:**\n - **--vf-desktop** - base color under the desktop pattern — occluded by the pattern's opaque paper, so it only shows through a custom `--vf-desktop-pattern` _(default: #808080)_\n- **--vf-desktop-pattern** - a consumer's own desktop tile, in place of `pattern` — the kit's default is the 50% checker drawn as opaque black-on-white rects on a 30-system-px tile. Override the whole tile; consumer art renders as a placed tile grid at that same geometry (raster art magnifies nearest-neighbor, the `vf-img` idiom) _(default: undefined)_\n\n### **CSS Parts:**\n - **desktop** - The patterned screen surface — the whole-system-px raster (inset by `bezel` when one is set).",
|
|
255
255
|
"attributes": [
|
|
256
256
|
{
|
|
257
257
|
"name": "width",
|
|
@@ -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",
|