vintage-frames 0.10.0 → 0.10.1

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.
@@ -13149,7 +13149,7 @@
13149
13149
  }
13150
13150
  }
13151
13151
  ],
13152
- "description": "A viewport point (CSS px) as a placement on the scrolled plane —\n`{ left, top }` in whole system px on the placement lattice, the pair a\nchild dropped here is written with. The plane travels with the scroll,\nso its box already carries the scroll offset and the border-floor\npadding; nothing is left for the caller to add. With `child`, the pair\nto write to that element: the offset its `origin` adds is folded in, so\nits box's corner lands on the point."
13152
+ "description": "A viewport point (CSS px) as a placement on the scrolled plane —\n`{ left, top }` in whole system px on the placement lattice, the pair a\nchild dropped here is written with. The plane travels with the scroll,\nso its box already carries the scroll offset; nothing is left for the\ncaller to add. With `child`, the pair to write to that element: the\noffset its `origin` adds is folded in, so its box's corner lands on the\npoint."
13153
13153
  },
13154
13154
  {
13155
13155
  "kind": "method",
@@ -44,19 +44,8 @@ var x = (e, t) => e.length === t.length && e.every((e, n) => e === t[n]), S = cl
44
44
  max-height: calc(var(--vf-scale, 1) * var(--vf-list-max-height, 200px));
45
45
  /* Native scrolling; the bar itself is hidden by the recipe
46
46
  (.vf-scroll) and the reservation is the rail element, not a native
47
- gutter. The rows run to the rail on the right.
48
-
49
- The mod() padding is border-floor compensation: engines floor a
50
- fractional border-width to whole CSS px (1.5px renders 1px), which
51
- would put the rows a fraction of a system px inside the frame at
52
- scale 1.5 or 4/3 — and the rows are LIGHT-DOM components, so their
53
- origins are the page's device-pixel-grid contract, not just ours.
54
- Padding is stored exactly, so border + mod(border's fraction)
55
- restores the exact 1-system-px inset the borderless-scroller
56
- construction used to give them, at every scale (mod is 0 at whole
57
- ones). */
58
- padding: mod(calc(var(--vf-scale, 1) * 1px), 1px);
59
- padding-right: 0;
47
+ gutter. The rows run to the rail on the right and start at the
48
+ frame's inner edge. */
60
49
  overflow-y: auto;
61
50
  }
62
51
  .vf-rail--vertical {
@@ -88,10 +88,10 @@ export declare class VfScrollArea extends VfScrollArea_base {
88
88
  * A viewport point (CSS px) as a placement on the scrolled plane —
89
89
  * `{ left, top }` in whole system px on the placement lattice, the pair a
90
90
  * child dropped here is written with. The plane travels with the scroll,
91
- * so its box already carries the scroll offset and the border-floor
92
- * padding; nothing is left for the caller to add. With `child`, the pair
93
- * to write to that element: the offset its `origin` adds is folded in, so
94
- * its box's corner lands on the point.
91
+ * so its box already carries the scroll offset; nothing is left for the
92
+ * caller to add. With `child`, the pair to write to that element: the
93
+ * offset its `origin` adds is folded in, so its box's corner lands on the
94
+ * point.
95
95
  */
96
96
  placementAt(clientX: number, clientY: number, child?: Element): {
97
97
  left: number;
@@ -74,16 +74,12 @@ var y = class extends r(f) {
74
74
  recipe (.vf-scroll) and the reservation is the rail element, not a
75
75
  native gutter. The unreserved axis still scrolls, railless.
76
76
 
77
- No inset of its own — only the border-floor compensation: engines
78
- floor the fractional frame border to whole CSS px, and the mod()
79
- term is exactly what they floored away, so slotted content (and
80
- the (0,0) of placed children) sits exactly one system px from the
81
- frame box at every scale (mod is 0 at whole scales). An inset is
82
- the content's (a vf-stack pad). */
77
+ No inset of its own: slotted content (and the (0,0) of placed
78
+ children) starts at the frame's inner edge, one system px from the
79
+ frame box. An inset is the content's (a vf-stack pad). */
83
80
  overflow: auto;
84
81
  min-width: 0;
85
82
  min-height: 0;
86
- padding: mod(var(--vf-scale, 1) * 1px, 1px);
87
83
  }
88
84
  /* A single-axis rail with a reserved corner: the viewport spans the
89
85
  track the missing rail would have taken, up to the corner cell, so
@@ -57,15 +57,9 @@ var g = class extends n(l) {
57
57
  min-width: 0;
58
58
  /* Borderless — the frame is the well's (above). vf-text-field's own
59
59
  3px/6px padding plus that border holds the text exactly where the
60
- bordered field puts it; the mod() term is border-floor
61
- compensation (engines floor the fractional border to whole CSS px;
62
- this is exactly what they floored away), keeping the text on the
63
- 4px/7px system-px inset from the frame box at every scale. */
60
+ bordered field puts it, 4px/7px from the frame box. */
64
61
  border: 0;
65
- padding: calc(
66
- var(--vf-scale, 1) * 3px + mod(var(--vf-scale, 1) * 1px, 1px)
67
- )
68
- calc(var(--vf-scale, 1) * 6px + mod(var(--vf-scale, 1) * 1px, 1px));
62
+ padding: calc(var(--vf-scale, 1) * 3px) calc(var(--vf-scale, 1) * 6px);
69
63
  /* Wrapped entry copy on the display face's native line (editable
70
64
  text is display type) — the same face token the static-text
71
65
  components read, so a display retheme moves this well too. The
@@ -34,10 +34,17 @@ export declare class GridSnapController implements ReactiveController {
34
34
  * roots consuming the variables sit at *authored* offsets inside the host —
35
35
  * including deliberate half-pixel ones like a toggle's centered box — and
36
36
  * measuring one of them would make the controller "correct" the component's
37
- * own design. Because the host never moves from its own correction, the
38
- * applied offset is folded into the error arithmetically rather than being
39
- * expected in the measured rect; an ancestor's correction, which does move
40
- * this host, is picked up the normal way.
37
+ * own design.
38
+ *
39
+ * The correction is a function of the host's position alone: the offset
40
+ * from its origin to the nearest whole device pixel. The host never moves
41
+ * from its own correction, so the correction already applied only decides
42
+ * whether a write is needed. Derived from the painted position instead —
43
+ * host plus the applied offset — every move would be rounded from wherever
44
+ * the previous correction left the paint, keeping each move's rounding
45
+ * error: a host carried along by an animation, or shifted half a pixel a
46
+ * few times, painted whole device pixels outside its box. An ancestor's
47
+ * correction, which does move this host, is picked up the normal way.
41
48
  */
42
49
  correct(dpr: number): void;
43
50
  /** Drop the correction: delete the two variables and forget everything. */
package/dist/grid-snap.js CHANGED
@@ -73,15 +73,13 @@ var n = .05, r = 1 / 64, i = () => e() || 1, a = (e, t) => {
73
73
  x: "",
74
74
  y: ""
75
75
  });
76
- let s = a(r.left + this.applied.x, e), c = a(r.top + this.applied.y, e);
77
- if (Math.abs(s) <= n && Math.abs(c) <= n) return;
78
- let l = {
79
- x: o(this.applied.x - s / e),
80
- y: o(this.applied.y - c / e)
76
+ let s = {
77
+ x: o(-a(r.left, e) / e),
78
+ y: o(-a(r.top, e) / e)
81
79
  };
82
- (l.x !== this.applied.x || l.y !== this.applied.y) && (this.applied = l, this.written = {
83
- x: `${l.x}px`,
84
- y: `${l.y}px`
80
+ Math.abs(s.x - this.applied.x) * e <= n && Math.abs(s.y - this.applied.y) * e <= n || (this.applied = s, this.written = {
81
+ x: `${s.x}px`,
82
+ y: `${s.y}px`
85
83
  }, i.setProperty("--vf-snap-dx", this.written.x), i.setProperty("--vf-snap-dy", this.written.y));
86
84
  }
87
85
  reset() {
@@ -19,11 +19,6 @@ export type RuleEdge = (typeof RULE_EDGES)[number];
19
19
  * One declaration per edge, so the kit cannot grow two rules — the
20
20
  * {@link vfHardShadowDecls} principle, applied to the line.
21
21
  *
22
- * Being a border, it shares the kit's border-floor residual
23
- * (docs/THREE-X-DISPLAYS.md): Chromium floors a fractional border-width to
24
- * whole CSS px, so above 1× the line paints thinner than a system px (2
25
- * device px of 3 at 2×) and the padding box begins that much inside — the
26
- * same as every kit frame, and the same line the menu bar always drew.
27
22
  * `npm run verify:rule` asserts a container's rule against the menu bar's.
28
23
  */
29
24
  export declare const vfRule: import("lit").CSSResult;
package/docs/SPEC.md CHANGED
@@ -148,7 +148,7 @@ The span construction stays load-bearing for the CSS-repeated underlays beneath
148
148
  - `vfHardShadowDecls` — the hard 1-bit drop shadow on its own, for composing into a surface that supplies its own border: `box-shadow: var(--vf-shadow-offset, 2px) var(--vf-shadow-offset, 2px) 0 0 var(--vf-black, #000)`. No blur, no spread — System 7's only depth cue. Every raised surface in the kit composes this one declaration (`vfPanel`, `vfChromeFrame`; `vf-swatch` composes it without either surface class).
149
149
  - `vfPanel` — a `.vf-panel` class for menus/popups: white bg, `border: 1px solid var(--vf-black, #000)`, `vfHardShadowDecls`.
150
150
  - `vfChromeFrame` — a `.vf-frame` class for the desktop window (`vf-window`): the same white face, 1px black border and hard offset shadow as `vfPanel`, no bevels. Skin only — the component supplies its own layout (a full-size flex column). `vf-dialog` does not use it: both of its chromes are `vfModalFrame`.
151
- - `vfRule` — the 1px rule on one edge of a box, one class per edge: `.vf-rule-top`, `.vf-rule-right`, `.vf-rule-bottom`, `.vf-rule-left`, each `border-<edge>: 1px solid var(--vf-black, #000)` scaled by `--vf-scale`. A border, so it sits inside the box's `border-box` size and insets the padding box — content, percentage fills and children placed against the box begin inside the rule, as a rectangle's interior begins inside FrameRect's line. One declaration per edge, the `vfHardShadowDecls` principle applied to the line: `vf-menu-bar`'s floor and `vf-window`'s status-strip ceiling are these classes, and `vf-container rule="…"` takes the edges by name. The attribute grammar ships beside it (`RULE_EDGES`, `parseRule`, `ruleClasses`): edge names separated by whitespace in any order, repeats dropped, returned in shorthand order; a blank or unset value is no edges; any other token refuses the whole value, as `parsePattern` refuses an unrecognized pattern. Being a border it shares the kit's border-floor residual (docs/THREE-X-DISPLAYS.md — thin above 1× in Chromium, the padding box that much inside), exactly as every kit frame does. `npm run verify:rule` asserts a container's rule against the menu bar's own at four densities.
151
+ - `vfRule` — the 1px rule on one edge of a box, one class per edge: `.vf-rule-top`, `.vf-rule-right`, `.vf-rule-bottom`, `.vf-rule-left`, each `border-<edge>: 1px solid var(--vf-black, #000)` scaled by `--vf-scale`. A border, so it sits inside the box's `border-box` size and insets the padding box — content, percentage fills and children placed against the box begin inside the rule, as a rectangle's interior begins inside FrameRect's line. One declaration per edge, the `vfHardShadowDecls` principle applied to the line: `vf-menu-bar`'s floor and `vf-window`'s status-strip ceiling are these classes, and `vf-container rule="…"` takes the edges by name. The attribute grammar ships beside it (`RULE_EDGES`, `parseRule`, `ruleClasses`): edge names separated by whitespace in any order, repeats dropped, returned in shorthand order; a blank or unset value is no edges; any other token refuses the whole value, as `parsePattern` refuses an unrecognized pattern. `npm run verify:rule` asserts a container's rule against the menu bar's own at four densities.
152
152
  - `vfModalFrame` — the dBoxProc modal-dialog frame, both `vf-dialog` chromes: `.vf-modal-frame` (white face, 1px black border, **no shadow**) around `.vf-modal-frame-inner` (`margin: 2px; border: 2px solid`). **The movable modal is this frame with the striped bar set into it** (traced from a 2× System 7 capture): a `.vf-title-bar` as the frame's first child sits directly under the outer rule, `margin: 0 2px` so its stripes — flush to the bar's own edge here, `inset: 3px 0` — keep the same 2px of white from the outer rule the band keeps; the inner box that follows it drops its top margin and thins its top border to 1px, so the bar's own 1px rule plus that border make the 2px band the bar's floor, continuing down the sides and along the bottom. System 7's alert box drew the mirror trace — 2px outer, 2px gap, 1px inner rule, *with* the hard shadow (`Windows/modal dialog.png` vs the alert reference) — a different chrome the kit does not ship: an alert box is a consumer composition over this frame, not a component.
153
153
  - `vfWindowWidgets` — the title-bar window widgets (`.box` base, `.close` left / `.zoom` right at 11×11 with the 1px white patch ring, the pressed go-away sunburst, the nested zoom square), shared by `vf-window` and a `closable` `vf-dialog` so the two components' widgets match by construction. The templates that pair with it (`closeBox`/`zoomBox`/`widgetLabel`) live in `src/chrome.ts` with `chromeTitleBar`. Geometry is the standard bar's; `vf-window`'s utility variant overrides sizes under its own selector.
154
154
  - `vfTitleBar` — the striped title bar shared by `vf-window` and `vf-dialog`: a `.vf-title-bar` row (`height: var(--vf-titlebar-height, 18px)`, 1px bottom rule, `overflow: hidden`) and the `.vf-title` patch inside it (display face, white bg, `padding: 0 6px`, `z-index: 1` over the stripes, `nowrap` + ellipsis). Put a `.vf-stripes` layer in as the bar's first child.
@@ -219,11 +219,11 @@ The desktop-window shell: the classic document window (see DragThing screenshot)
219
219
  - 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.
220
220
  - 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.
221
221
  - 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
- - 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 (the border-floor term alone), 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).
222
+ - 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).
223
223
  - 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.
224
224
  - **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`.
225
225
  - **Behavior:** close box click → `vf-close` (does NOT remove itself; consumer decides). Zoom box click → `vf-zoom`. If `movable`: dragging the title bar moves the window — the drag seeds its origin from the current offset position (once, converting to system px) and then states `left`/`top` via pointer capture, in whole system px like any authored placement. If `resizable`: dragging the grow box states `width`/`height` the same way, each axis clamped into the sizeRect (`min-width`/`max-width`, `min-height`/`max-height` — GrowWindow's: the app stated the rectangle of sizes and the Window Manager clamped the drag to it; the mins default to the 80×54 floor a window can still be worked at, the maxes are unbounded, and a min equal to its max locks the axis — the strip that scrolls sideways and never grows taller). The clamp runs after the lattice snap, so a bound lands exactly the way an authored size does even off the lattice (an odd height at 2×); where a min and max cross the max wins, so a window authored under the floor and held there never jumps to it. The rect bounds the gesture only: a size authored outside it renders as declared, and the first grow-box move brings it inside. The drag fires `vf-resize` (detail `{ width, height, commit }`, sizes in whole system px) — one event per size the drag actually writes (`commit: false`), fired *after* the new box is applied so a handler that measures reads the resized layout, then a final `commit: true` as the gesture settles (release or cancel), only when it changed the size. Window content can follow the grow box from this event alone, no ResizeObserver. Fired by the gesture only: a programmatic `width`/`height` write fires nothing, the way a value set fires no `vf-change`.
226
- - **Methods:** `placementAt(clientX, clientY, child?)` → a viewport point as a placement in the content region — `{ left, top }` in whole system px on the placement lattice: the body's corner, or under `scrollbars` the scrolled plane with the scroll offset and the border-floor padding already in it (the built-in area's own `placementAt`). The pair a child dropped into the window is written with; with `child`, the pair to write to that element, its `origin` offset folded in. `measure()` → the built-in scroll area's `measure()`, a no-op without `scrollbars`; the area re-measures by itself on its content's box, its slot and every placement write (`vf-placement-change`, §2), so this is the escape hatch for a range that changes with none of those.
226
+ - **Methods:** `placementAt(clientX, clientY, child?)` → a viewport point as a placement in the content region — `{ left, top }` in whole system px on the placement lattice: the body's corner, or under `scrollbars` the scrolled plane with the scroll offset already in it (the built-in area's own `placementAt`). The pair a child dropped into the window is written with; with `child`, the pair to write to that element, its `origin` offset folded in. `measure()` → the built-in scroll area's `measure()`, a no-op without `scrollbars`; the area re-measures by itself on its content's box, its slot and every placement write (`vf-placement-change`, §2), so this is the escape hatch for a range that changes with none of those.
227
227
  - **Slots:** default (body content), `header` (the header strip between the title bar and the body), `status` (the bottom status strip — see Visual).
228
228
  - **Parts:** `frame`, `title-bar`, `title`, `close-box`, `zoom-box`, `header`, `body`, `status-bar`, `grow-box`, plus `viewport` re-exported from the built-in scroll area when `scrollbars` is set.
229
229
  - **Events:** `vf-close`, `vf-zoom` (detail `{}`), `vf-resize` (detail `{ width, height, commit }`).
@@ -315,7 +315,7 @@ The color-swatch button: a well of solid color — a palette cell.
315
315
  - **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.
316
316
 
317
317
  #### `vf-text-area` (`VfTextArea`, vf-text-area.ts)
318
- 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 (plus the border-floor compensation, a `mod()` term restoring what engines floor off a fractional border-width), 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 `-3px` bordered-carrier 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).
318
+ 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 `-3px` bordered-carrier 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).
319
319
 
320
320
  #### `vf-number-field` (`VfNumberField`, vf-number-field.ts)
321
321
  A numeric text field paired with the classic "little arrows" stepper.
@@ -386,19 +386,19 @@ The classic popup menu control ("Macintosh HD ▼").
386
386
  Classic list box.
387
387
  - **Attributes/props:** `multiple: boolean`, `value: string` / `values: string[]` (multiple), `disabled`, `label: string` (accessible name → host `aria-label`).
388
388
  - **Children:** `<vf-list-item value="...">` (`VfListItem`, vf-list-item.ts): props `value`, `selected` (reflect), `disabled`; height 20px, `padding: 0 6px`; selected = inverted row (full width). Slots: default (the row text, in a `text` part that ellipsizes), `icon` (a leading graphic — usually a 16×16 `vf-img` small icon; flex-centered in the row with the reference art's 4px gap to the text, no text contributed so type-ahead still reads the words; keep row-height − icon-height even so the centering offset is a whole pixel).
389
- - **Visual (list):** white bg inside a real 1px black frame on the snapped wrapper, a `[rows | rail]` grid: the scrolling row viewport (class `vf-scroll`, native bar hidden) beside the drawn `vfScrollRail` subtree (§4), synced by `ScrollRailController`. The rows sit exactly 1 system px inside the frame lines (a `mod()` border-floor compensation pads back what engines floor off the fractional border-width — the rows are light-DOM components, so their origins are the page's grid contract); default `max-height: 200px` overridable via `--vf-list-max-height` (the frame adds its 2px on top). Reserves a permanent vertical scroll rail (the "always-a-rail" behavior — see vf-scroll-area): arrows on an empty white channel until the rows overflow.
389
+ - **Visual (list):** white bg inside a real 1px black frame on the snapped wrapper, a `[rows | rail]` grid: the scrolling row viewport (class `vf-scroll`, native bar hidden) beside the drawn `vfScrollRail` subtree (§4), synced by `ScrollRailController`. The rows sit exactly 1 system px inside the frame lines; default `max-height: 200px` overridable via `--vf-list-max-height` (the frame adds its 2px on top). Reserves a permanent vertical scroll rail (the "always-a-rail" behavior — see vf-scroll-area): arrows on an empty white channel until the rows overflow.
390
390
  - **Behavior:** `role="listbox"` (+`aria-multiselectable`, + `aria-label` from `label`), items `role="option"`. A disabled list pushes `aria-disabled` down onto every row (as `vf-radio-group` does), so AT is never shown enabled-looking options inside a disabled listbox; the flag is tracked separately from each row's own `disabled`, so re-enabling the list leaves individually disabled rows disabled. Click selects (Shift/Cmd extend when `multiple`). Roving tabindex; Arrow keys move the selection, Space toggles in multiple mode. The full multiple-mode model: Shift+Arrow extends from the anchor, Ctrl+Arrow moves the cursor without touching the selection, plain Home/End and type-ahead jumps are cursor-only moves too (a jump that rewrote a hand-built selection would destroy it — Space is how the reached row joins it; in single mode they select, since there the selection *is* the cursor), Shift(+Ctrl)+ Home/End extend through to the ends, Shift+Space selects the contiguous run from the anchor to the cursor, and Ctrl/Cmd+A selects every enabled row. Printable keys drive classic Finder **first-letter type-ahead** (`src/type-ahead.ts`, the shared buffer `vf-select` and the menus also run): keystrokes accumulate into a prefix matched against each row's text, jumping to the next match, wrapping and skipping disabled rows. The prefix resets after 1s of silence; repeating a single character cycles the rows starting with it. Modified keys are left to the consumer, and Space stays the multiple-mode toggle, so neither joins the prefix. A **disabled** list keeps its rows rendered and readable (System 7 dims a list, it doesn't hide it) — when they overflow the box, the viewport itself becomes the Tab stop so the dimmed rows stay reachable by keyboard scroll.
391
391
  - **Parts:** `list`. **Events:** `vf-change` detail `{ value, values }`.
392
392
 
393
393
  #### `vf-scroll-area` (`VfScrollArea`, vf-scroll-area.ts)
394
394
  A container whose scrollbars look like System 7.
395
395
  - **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).
396
- - **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 — only the border-floor compensation (a `mod()` term restoring what engines floor off the fractional border-width), 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 `tileRaster` 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.
396
+ - **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 `tileRaster` 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.
397
397
  - **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.
398
398
  - **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).
399
399
  - **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`).
400
400
  - **Re-measures under a moved child.** A placed child moved through `top`/`left` changes the scroll range with no box changing anywhere the ResizeObserver can see — the plane's box is never grown by an absolutely positioned child — so the placement controller announces every write as `vf-placement-change` (§2) and the area re-measures on it, heard on the host: the event is non-composed, but from a slotted child it still travels through the slot it is assigned to, which is how the window's built-in area (this element, in the window's shadow) hears an icon slotted into the window body. A drop, an arrow nudge and a page's own `icon.left = …` all go through the one funnel.
401
- - **Methods:** `measure()` → re-measure overflow and re-sync the rails, for a scroll range that changes with no box, no slot and no placement write — a transform — the way the kit's own fields re-measure on input. `placementAt(clientX, clientY, child?)` → a viewport point as a placement on the scrolled plane, `{ left, top }` in whole system px on the placement lattice; the plane travels with the scroll, so its box already carries the offset and the border-floor padding. With `child`, the pair to write to that element, its `origin` offset folded in.
401
+ - **Methods:** `measure()` → re-measure overflow and re-sync the rails, for a scroll range that changes with no box, no slot and no placement write — a transform — the way the kit's own fields re-measure on input. `placementAt(clientX, clientY, child?)` → a viewport point as a placement on the scrolled plane, `{ left, top }` in whole system px on the placement lattice; the plane travels with the scroll, so its box already carries the offset. With `child`, the pair to write to that element, its `origin` offset folded in.
402
402
  - **Slots:** default. **Parts:** `viewport`.
403
403
 
404
404
  #### `vf-fieldset` (`VfFieldset`, vf-fieldset.ts)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
3
3
  "name": "vintage-frames",
4
- "version": "0.10.0",
4
+ "version": "0.10.1",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vintage-frames",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Lit web components that emulate the look and feel of classic Mac OS System 7",
5
5
  "type": "module",
6
6
  "license": "MIT",