vintage-frames 0.1.1 → 0.2.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.
- package/custom-elements.json +348 -40
- package/dist/components/vf-button.d.ts +1 -1
- package/dist/components/vf-checkbox.d.ts +5 -0
- package/dist/components/vf-checkbox.js +18 -10
- package/dist/components/vf-container.d.ts +2 -2
- package/dist/components/vf-desktop.d.ts +1 -1
- package/dist/components/vf-fieldset.d.ts +1 -1
- package/dist/components/vf-fieldset.js +6 -1
- package/dist/components/vf-grid.d.ts +1 -1
- package/dist/components/vf-icon.d.ts +1 -1
- package/dist/components/vf-img.d.ts +1 -1
- package/dist/components/vf-label.d.ts +1 -1
- package/dist/components/vf-list-item.d.ts +9 -1
- package/dist/components/vf-list-item.js +15 -14
- package/dist/components/vf-list.d.ts +1 -1
- package/dist/components/vf-menu-bar.d.ts +1 -1
- package/dist/components/vf-menu-item.d.ts +10 -1
- package/dist/components/vf-menu-item.js +35 -34
- package/dist/components/vf-menu.d.ts +11 -2
- package/dist/components/vf-menu.js +33 -32
- package/dist/components/vf-option.d.ts +11 -1
- package/dist/components/vf-option.js +16 -15
- package/dist/components/vf-paragraph.d.ts +3 -3
- package/dist/components/vf-progress-bar.d.ts +1 -1
- package/dist/components/vf-radio.d.ts +5 -0
- package/dist/components/vf-radio.js +16 -11
- package/dist/components/vf-scroll-area.d.ts +1 -1
- package/dist/components/vf-select.d.ts +1 -1
- package/dist/components/vf-separator.d.ts +1 -1
- package/dist/components/vf-slider.d.ts +1 -1
- package/dist/components/vf-stack.d.ts +27 -9
- package/dist/components/vf-stack.js +30 -8
- package/dist/components/vf-swatch.d.ts +1 -1
- package/dist/components/vf-window.d.ts +1 -1
- package/dist/cross-center.d.ts +76 -0
- package/dist/cross-center.js +69 -0
- package/dist/define.d.ts +2 -2
- package/dist/define.js +1 -1
- package/dist/grid-snap.d.ts +2 -24
- package/dist/grid-snap.js +7 -33
- package/dist/index.d.ts +5 -5
- package/dist/index.js +50 -50
- package/dist/position.d.ts +11 -2
- package/dist/styles/recipes/host.js +14 -1
- package/dist/styles/recipes/toggle.js +24 -0
- package/dist/text-control.d.ts +1 -1
- package/docs/SPEC.md +10 -9
- package/editor/vscode.html-custom-data.json +50 -10
- package/editor/web-types.json +96 -16
- package/package.json +1 -3
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { effectiveScale as e } from "./scale.js";
|
|
2
|
+
//#region src/cross-center.ts
|
|
3
|
+
var t = "data-vf-tie", n = {
|
|
4
|
+
x: "--vf-stack-dx",
|
|
5
|
+
y: "--vf-stack-dy"
|
|
6
|
+
}, r = 1 / 32, i = .05, a = (e) => parseFloat(e) || 0;
|
|
7
|
+
function o(e, t) {
|
|
8
|
+
let n = getComputedStyle(e), r = e.getBoundingClientRect();
|
|
9
|
+
return t === "x" ? r.left + a(n.borderLeftWidth) + a(n.paddingLeft) : r.top + a(n.borderTopWidth) + a(n.paddingTop);
|
|
10
|
+
}
|
|
11
|
+
var s = class {
|
|
12
|
+
constructor(e, t) {
|
|
13
|
+
this.host = e, this.read = t, this.observed = [], this.held = /* @__PURE__ */ new Map(), this.sync = () => {
|
|
14
|
+
let e = this.read(), { box: t, slot: n, axis: r } = e;
|
|
15
|
+
if (!t || !n || !r) return this.rest();
|
|
16
|
+
n !== this.slot && (this.slot?.removeEventListener("slotchange", this.sync), n.addEventListener("slotchange", this.sync), this.slot = n), this.watch(t, n), this.hold(e);
|
|
17
|
+
}, e.addController(this);
|
|
18
|
+
}
|
|
19
|
+
hostConnected() {
|
|
20
|
+
this.sync();
|
|
21
|
+
}
|
|
22
|
+
hostUpdated() {
|
|
23
|
+
this.sync();
|
|
24
|
+
}
|
|
25
|
+
hostDisconnected() {
|
|
26
|
+
this.rest();
|
|
27
|
+
}
|
|
28
|
+
rest() {
|
|
29
|
+
this.observer?.disconnect(), this.observed = [], this.slot?.removeEventListener("slotchange", this.sync), this.slot = void 0;
|
|
30
|
+
for (let e of [...this.held.keys()]) this.clear(e);
|
|
31
|
+
}
|
|
32
|
+
watch(e, t) {
|
|
33
|
+
if (typeof ResizeObserver > "u") return;
|
|
34
|
+
let n = [e, ...t.assignedElements()];
|
|
35
|
+
if (!(n.length === this.observed.length && n.every((e, t) => e === this.observed[t]))) {
|
|
36
|
+
this.observer ??= new ResizeObserver(() => this.hold(this.read())), this.observer.disconnect();
|
|
37
|
+
for (let e of n) this.observer.observe(e);
|
|
38
|
+
this.observed = n;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
hold({ box: t, slot: n, axis: a }) {
|
|
42
|
+
if (!t || !n || !a) return;
|
|
43
|
+
let s = e(this.host), c = o(t, a), l = new Set(this.held.keys());
|
|
44
|
+
for (let e of n.assignedElements()) {
|
|
45
|
+
if (!(e instanceof HTMLElement)) continue;
|
|
46
|
+
l.delete(e);
|
|
47
|
+
let t = getComputedStyle(e).position;
|
|
48
|
+
if (t === "absolute" || t === "fixed") {
|
|
49
|
+
this.clear(e);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
let n = e.getBoundingClientRect(), o = a === "x" ? n.left : n.top, u = this.held.get(e), d = o - c - (u?.axis === a ? u.offset : 0), f = Math.ceil(d / s - .5 - i) * s - d;
|
|
53
|
+
Math.abs(f) < r ? this.clear(e) : this.apply(e, a, f);
|
|
54
|
+
}
|
|
55
|
+
for (let e of l) this.clear(e);
|
|
56
|
+
}
|
|
57
|
+
apply(e, r, i) {
|
|
58
|
+
let a = this.held.get(e);
|
|
59
|
+
(a?.axis !== r || a.offset !== i) && (e.style.setProperty(n[r], `${i}px`), e.style.removeProperty(n[r === "x" ? "y" : "x"]), e.setAttribute(t, ""), this.held.set(e, {
|
|
60
|
+
axis: r,
|
|
61
|
+
offset: i
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
clear(e) {
|
|
65
|
+
this.held.delete(e) && (e.style.removeProperty(n.x), e.style.removeProperty(n.y), e.removeAttribute(t), e.getAttribute("style") || e.removeAttribute("style"));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
//#endregion
|
|
69
|
+
export { t as CENTER_TIE_ATTRIBUTE, s as CrossCenterController };
|
package/dist/define.d.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* working. The warning matters as much as the guard, because a duplicated copy
|
|
15
15
|
* is not benign even once the page survives it: the scaling, grid-snapping and
|
|
16
16
|
* focus-modality registries are all module-scoped singletons, so the second
|
|
17
|
-
* copy's
|
|
18
|
-
*
|
|
17
|
+
* copy's schedulers reach none of the first copy's components and the two
|
|
18
|
+
* disagree silently. Better to say so than to let it look fine.
|
|
19
19
|
*/
|
|
20
20
|
/**
|
|
21
21
|
* Register `ctor` under `tagName` unless the name is already taken.
|
package/dist/define.js
CHANGED
|
@@ -3,7 +3,7 @@ var e = (e, t) => {
|
|
|
3
3
|
let n = customElements.get(e);
|
|
4
4
|
if (n !== t) {
|
|
5
5
|
if (n !== void 0) {
|
|
6
|
-
console.warn(`[vintage-frames] <${e}> is already registered on this page, so this second registration was skipped and the first one kept. Two copies of the library also mean two of every module-scoped singleton —
|
|
6
|
+
console.warn(`[vintage-frames] <${e}> is already registered on this page, so this second registration was skipped and the first one kept. Two copies of the library also mean two of every module-scoped singleton — the grid-snap scheduler, applyScale() and the focus-modality tracker each only reach the components built from their own copy. Dedupe vintage-frames to a single version.`);
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
9
|
customElements.define(e, t);
|
package/dist/grid-snap.d.ts
CHANGED
|
@@ -9,9 +9,8 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
|
9
9
|
*
|
|
10
10
|
* The component's template must put the `vf-snap` class (see `vfBase`) on its
|
|
11
11
|
* top-level painted element(s); the host box is what gets measured, so an
|
|
12
|
-
* authored offset inside it (a toggle's centered box) stays put.
|
|
13
|
-
*
|
|
14
|
-
* `nosnap` attribute on the host.
|
|
12
|
+
* authored offset inside it (a toggle's centered box) stays put. Always
|
|
13
|
+
* active; skipped per-element by a `nosnap` attribute on the host.
|
|
15
14
|
*/
|
|
16
15
|
export declare class GridSnapController implements ReactiveController {
|
|
17
16
|
readonly host: ReactiveControllerHost & HTMLElement;
|
|
@@ -44,27 +43,6 @@ export declare class GridSnapController implements ReactiveController {
|
|
|
44
43
|
/** Drop the correction: delete the two variables and forget everything. */
|
|
45
44
|
reset(): void;
|
|
46
45
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Opt the page into automatic device-pixel-grid snapping: every mounted
|
|
49
|
-
* component (and every one mounted afterwards) holds its paint on whole device
|
|
50
|
-
* pixels, whatever the surrounding layout does. Returns a cleanup function
|
|
51
|
-
* that turns it back off. Calls share one switch: snapping stays on until
|
|
52
|
-
* every caller's cleanup has run, and running a cleanup twice releases only
|
|
53
|
-
* once — so two widgets on one page can each opt in and out without turning
|
|
54
|
-
* the other's snapping off.
|
|
55
|
-
*
|
|
56
|
-
* ```ts
|
|
57
|
-
* import { applyGridSnap } from 'vintage-frames'
|
|
58
|
-
* applyGridSnap()
|
|
59
|
-
* ```
|
|
60
|
-
*
|
|
61
|
-
* The whole footprint on your DOM is two reserved custom properties
|
|
62
|
-
* (`--vf-snap-dx`/`--vf-snap-dy`) on each corrected host's inline style; the
|
|
63
|
-
* offset they drive is applied inside the component's own shadow root. Worth
|
|
64
|
-
* knowing: a component's painted box can sit up to half a device pixel outside
|
|
65
|
-
* its layout box while corrected.
|
|
66
|
-
*/
|
|
67
|
-
export declare function applyGridSnap(): () => void;
|
|
68
46
|
/**
|
|
69
47
|
* Re-check every component before the next paint. Call it after moving
|
|
70
48
|
* components in a way nothing observable changed size — the one case the
|
package/dist/grid-snap.js
CHANGED
|
@@ -6,29 +6,14 @@ var n = .05, r = 1 / 64, i = () => e() || 1, a = (e, t) => {
|
|
|
6
6
|
return n - Math.round(n);
|
|
7
7
|
}, o = (e) => Math.round(e / r) * r, s = new class {
|
|
8
8
|
constructor() {
|
|
9
|
-
this.controllers = /* @__PURE__ */ new Set(), this.enabled = !1, this.frame = 0
|
|
9
|
+
this.controllers = /* @__PURE__ */ new Set(), this.enabled = !1, this.frame = 0;
|
|
10
10
|
}
|
|
11
11
|
register(e) {
|
|
12
|
-
|
|
13
|
-
e.reset();
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
this.resizes?.observe(e.host), this.request();
|
|
12
|
+
this.controllers.add(e), !this.enabled && typeof window < "u" ? (this.enabled = !0, this.install()) : this.resizes?.observe(e.host), this.request();
|
|
17
13
|
}
|
|
18
14
|
unregister(e) {
|
|
19
15
|
this.controllers.delete(e), this.resizes?.unobserve(e.host);
|
|
20
16
|
}
|
|
21
|
-
enable() {
|
|
22
|
-
this.enabled || typeof window > "u" || (this.enabled = !0, this.install(), this.request());
|
|
23
|
-
}
|
|
24
|
-
disable() {
|
|
25
|
-
if (this.enabled) {
|
|
26
|
-
this.enabled = !1, this.frame && cancelAnimationFrame(this.frame), this.frame = 0;
|
|
27
|
-
for (let e of this.teardown.splice(0)) e();
|
|
28
|
-
this.resizes?.disconnect(), this.resizes = void 0;
|
|
29
|
-
for (let e of this.controllers) e.reset();
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
17
|
request() {
|
|
33
18
|
!this.enabled || this.frame || typeof window > "u" || (this.frame = requestAnimationFrame(() => {
|
|
34
19
|
this.frame = 0, this.sweep();
|
|
@@ -45,17 +30,13 @@ var n = .05, r = 1 / 64, i = () => e() || 1, a = (e, t) => {
|
|
|
45
30
|
window.addEventListener("resize", e), window.addEventListener("orientationchange", e), window.addEventListener("scroll", e, {
|
|
46
31
|
capture: !0,
|
|
47
32
|
passive: !0
|
|
48
|
-
}), this.teardown.push(() => {
|
|
49
|
-
window.removeEventListener("resize", e), window.removeEventListener("orientationchange", e), window.removeEventListener("scroll", e, { capture: !0 });
|
|
50
33
|
});
|
|
51
34
|
let n = document.fonts;
|
|
52
|
-
n && (n.addEventListener("loadingdone", e),
|
|
53
|
-
let r = new MutationObserver(e);
|
|
54
|
-
r.observe(document.documentElement, {
|
|
35
|
+
n && (n.addEventListener("loadingdone", e), n.ready.then(e)), t(e), new MutationObserver(e).observe(document.documentElement, {
|
|
55
36
|
subtree: !0,
|
|
56
37
|
attributes: !0,
|
|
57
38
|
attributeFilter: ["nosnap"]
|
|
58
|
-
})
|
|
39
|
+
});
|
|
59
40
|
}
|
|
60
41
|
}(), c = class {
|
|
61
42
|
constructor(e) {
|
|
@@ -120,16 +101,9 @@ var n = .05, r = 1 / 64, i = () => e() || 1, a = (e, t) => {
|
|
|
120
101
|
let t = 0;
|
|
121
102
|
for (let n = l(e); n; n = l(n)) n.tagName.toLowerCase().startsWith("vf-") && t++;
|
|
122
103
|
return t;
|
|
123
|
-
}
|
|
124
|
-
function
|
|
125
|
-
d++, s.enable();
|
|
126
|
-
let e = !1;
|
|
127
|
-
return () => {
|
|
128
|
-
e || (e = !0, --d === 0 && s.disable());
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
function p() {
|
|
104
|
+
};
|
|
105
|
+
function d() {
|
|
132
106
|
s.request();
|
|
133
107
|
}
|
|
134
108
|
//#endregion
|
|
135
|
-
export { c as GridSnapController,
|
|
109
|
+
export { c as GridSnapController, d as requestGridSnap };
|
package/dist/index.d.ts
CHANGED
|
@@ -65,12 +65,12 @@ export { applyScale, ScaleController, effectiveScale, getScale, onScaleChange, s
|
|
|
65
65
|
*/
|
|
66
66
|
export { getZoom, truePixelRatio, onZoomChange, devicePxPerSystemPx, resetZoomBaseline, } from './zoom.js';
|
|
67
67
|
/**
|
|
68
|
-
* Automatic device-pixel-grid snapping.
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
68
|
+
* Automatic device-pixel-grid snapping. Always on: every component holds its
|
|
69
|
+
* own origin on whole device pixels whatever the surrounding layout does — the
|
|
70
|
+
* half of the layout contract the components keep for you. `requestGridSnap()`
|
|
71
|
+
* re-checks before the next paint, for a move nothing observable resized.
|
|
72
72
|
*/
|
|
73
|
-
export {
|
|
73
|
+
export { requestGridSnap, GridSnapController } from './grid-snap.js';
|
|
74
74
|
/**
|
|
75
75
|
* The page-drawn cursor. `applyCursor()` is the one call an app makes to
|
|
76
76
|
* replace the native pointer with the embedded System 7 set — arrow, I-beam,
|
package/dist/index.js
CHANGED
|
@@ -19,53 +19,53 @@ import { vfToggle as te } from "./styles/recipes/toggle.js";
|
|
|
19
19
|
import { vfField as ne } from "./styles/recipes/field.js";
|
|
20
20
|
import { vfScrollRail as re } from "./styles/recipes/scroll-rail.js";
|
|
21
21
|
import { TileRasterCache as ie, patternOverride as ae, tileGrid as oe, vfTileGrid as se } from "./tile-grid.js";
|
|
22
|
-
import { GridSnapController as ce,
|
|
23
|
-
import { DocumentListenersController as
|
|
24
|
-
import { BUTTON_FACE as
|
|
25
|
-
import { VfDesktop as
|
|
26
|
-
import { VfSized as
|
|
27
|
-
import { DragController as
|
|
28
|
-
import { emit as
|
|
29
|
-
import { ScrollStateController as
|
|
30
|
-
import { BLINK_FLIPS as
|
|
31
|
-
import { CARET_DOWN as
|
|
32
|
-
import { ScrollRailController as
|
|
33
|
-
import { VfScrollArea as
|
|
34
|
-
import { VfWindow as
|
|
35
|
-
import { VfModalDialog as
|
|
36
|
-
import { VfButtonGroup as
|
|
37
|
-
import { VfDialog as
|
|
38
|
-
import { VfSeparator as
|
|
39
|
-
import { VfFormControl as
|
|
40
|
-
import { VfButton as
|
|
41
|
-
import { VfSwatch as
|
|
42
|
-
import { VfToggleControl as
|
|
43
|
-
import { VfCheckbox as
|
|
44
|
-
import { VfRadio as
|
|
45
|
-
import { VfRadioGroup as
|
|
46
|
-
import { FocusRuleController as
|
|
47
|
-
import { VfTextControlBase as
|
|
48
|
-
import { VfTextField as
|
|
49
|
-
import { VfTextArea as
|
|
50
|
-
import { VfNumberField as
|
|
51
|
-
import { VfOption as
|
|
52
|
-
import { VfSelect as
|
|
53
|
-
import { TrackWidthController as
|
|
54
|
-
import { VfProgressBar as
|
|
55
|
-
import { VfSlider as
|
|
56
|
-
import { VfLabel as
|
|
57
|
-
import { VfParagraph as
|
|
58
|
-
import { VfImg as
|
|
59
|
-
import { VfIcon as
|
|
60
|
-
import { VfMenuBar as
|
|
61
|
-
import { VfMenu as
|
|
62
|
-
import { VfMenuItem as
|
|
63
|
-
import { VfList as
|
|
64
|
-
import { VfListItem as
|
|
65
|
-
import { VfFieldset as
|
|
66
|
-
import { VfGrid as
|
|
67
|
-
import { VfStack as
|
|
68
|
-
import { VfContainer as
|
|
69
|
-
import { CURSOR_ARROW as
|
|
70
|
-
import { applyCursor as
|
|
71
|
-
export {
|
|
22
|
+
import { GridSnapController as ce, requestGridSnap as le } from "./grid-snap.js";
|
|
23
|
+
import { DocumentListenersController as ue } from "./document-listeners.js";
|
|
24
|
+
import { BUTTON_FACE as de, BUTTON_FRAME as fe, RING_FRAME as pe, RING_HOLE as me, RING_INSET as he, SCREEN_CORNER as ge, steppedCornerClip as _e, steppedRectClip as ve, steppedRingClip as ye } from "./pixel-frame.js";
|
|
25
|
+
import { VfDesktop as be } from "./components/vf-desktop.js";
|
|
26
|
+
import { VfSized as xe } from "./size.js";
|
|
27
|
+
import { DragController as Se } from "./drag.js";
|
|
28
|
+
import { emit as Ce } from "./events.js";
|
|
29
|
+
import { ScrollStateController as we } from "./scroll-state.js";
|
|
30
|
+
import { BLINK_FLIPS as Te, BLINK_INTERVAL_MS as Ee, PRESS_HOLD_MS as De, RENAME_DELAY_MS as Oe, prefersReducedMotion as ke, runSelectionBlink as Ae } from "./motion.js";
|
|
31
|
+
import { CARET_DOWN as je, CHECKBOX_X as Me, CHECKMARK as Ne, RADIO_DOT as Pe, RADIO_FACE as Fe, RADIO_RING as Ie, RADIO_RING_PRESSED as Le, SLIDER_THUMB as Re, SLIDER_THUMB_FACE as ze, STEPPER as Be, STEPPER_DOWN_FILL as Ve, STEPPER_UP_FILL as He, glyphSvg as Ue } from "./glyphs.js";
|
|
32
|
+
import { ScrollRailController as We, renderScrollRail as Ge } from "./scroll-rail.js";
|
|
33
|
+
import { VfScrollArea as Ke } from "./components/vf-scroll-area.js";
|
|
34
|
+
import { VfWindow as qe } from "./components/vf-window.js";
|
|
35
|
+
import { VfModalDialog as Je, modalDialogStyles as Ye } from "./modal-dialog.js";
|
|
36
|
+
import { VfButtonGroup as Xe } from "./components/vf-button-group.js";
|
|
37
|
+
import { VfDialog as Ze } from "./components/vf-dialog.js";
|
|
38
|
+
import { VfSeparator as Qe } from "./components/vf-separator.js";
|
|
39
|
+
import { VfFormControl as $e } from "./form-control.js";
|
|
40
|
+
import { VfButton as et } from "./components/vf-button.js";
|
|
41
|
+
import { VfSwatch as tt } from "./components/vf-swatch.js";
|
|
42
|
+
import { VfToggleControl as nt } from "./toggle-control.js";
|
|
43
|
+
import { VfCheckbox as rt } from "./components/vf-checkbox.js";
|
|
44
|
+
import { VfRadio as it } from "./components/vf-radio.js";
|
|
45
|
+
import { VfRadioGroup as at } from "./components/vf-radio-group.js";
|
|
46
|
+
import { FocusRuleController as ot, focusModality as st, trackFocusModality as ct } from "./focus-modality.js";
|
|
47
|
+
import { VfTextControlBase as lt } from "./text-control.js";
|
|
48
|
+
import { VfTextField as ut } from "./components/vf-text-field.js";
|
|
49
|
+
import { VfTextArea as dt } from "./components/vf-text-area.js";
|
|
50
|
+
import { VfNumberField as ft } from "./components/vf-number-field.js";
|
|
51
|
+
import { VfOption as pt } from "./components/vf-option.js";
|
|
52
|
+
import { VfSelect as mt } from "./components/vf-select.js";
|
|
53
|
+
import { TrackWidthController as ht } from "./track-width.js";
|
|
54
|
+
import { VfProgressBar as gt } from "./components/vf-progress-bar.js";
|
|
55
|
+
import { VfSlider as _t } from "./components/vf-slider.js";
|
|
56
|
+
import { VfLabel as vt } from "./components/vf-label.js";
|
|
57
|
+
import { VfParagraph as yt } from "./components/vf-paragraph.js";
|
|
58
|
+
import { VfImg as bt } from "./components/vf-img.js";
|
|
59
|
+
import { VfIcon as xt } from "./components/vf-icon.js";
|
|
60
|
+
import { VfMenuBar as St } from "./components/vf-menu-bar.js";
|
|
61
|
+
import { VfMenu as Ct } from "./components/vf-menu.js";
|
|
62
|
+
import { VfMenuItem as wt } from "./components/vf-menu-item.js";
|
|
63
|
+
import { VfList as Tt } from "./components/vf-list.js";
|
|
64
|
+
import { VfListItem as Et } from "./components/vf-list-item.js";
|
|
65
|
+
import { VfFieldset as Dt } from "./components/vf-fieldset.js";
|
|
66
|
+
import { VfGrid as Ot } from "./components/vf-grid.js";
|
|
67
|
+
import { VfStack as kt } from "./components/vf-stack.js";
|
|
68
|
+
import { VfContainer as At } from "./components/vf-container.js";
|
|
69
|
+
import { CURSOR_ARROW as jt, CURSOR_CROSSHAIR as Mt, CURSOR_I_BEAM as Nt, CURSOR_WAIT as Pt } from "./cursor-art.js";
|
|
70
|
+
import { applyCursor as Ft } from "./cursor.js";
|
|
71
|
+
export { Te as BLINK_FLIPS, Ee as BLINK_INTERVAL_MS, de as BUTTON_FACE, fe as BUTTON_FRAME, je as CARET_DOWN, Me as CHECKBOX_X, Ne as CHECKMARK, n as CLASSIC_DPI, r as CSS_REFERENCE_DPI, jt as CURSOR_ARROW, Mt as CURSOR_CROSSHAIR, Nt as CURSOR_I_BEAM, Pt as CURSOR_WAIT, ue as DocumentListenersController, Se as DragController, ot as FocusRuleController, ce as GridSnapController, E as PIXEL_GRID_METRICS, De as PRESS_HOLD_MS, w as PlacementController, Pe as RADIO_DOT, Fe as RADIO_FACE, Ie as RADIO_RING, Le as RADIO_RING_PRESSED, Oe as RENAME_DELAY_MS, pe as RING_FRAME, me as RING_HOLE, he as RING_INSET, ge as SCREEN_CORNER, Re as SLIDER_THUMB, ze as SLIDER_THUMB_FACE, Be as STEPPER, Ve as STEPPER_DOWN_FILL, He as STEPPER_UP_FILL, i as SYSTEM_PX_IN_CSS_PX, u as ScaleController, We as ScrollRailController, we as ScrollStateController, L as TILE_LATTICE, ie as TileRasterCache, ht as TrackWidthController, O as VF_BODY_FAMILY, j as VF_DISPLAY_FAMILY, et as VfButton, Xe as VfButtonGroup, rt as VfCheckbox, At as VfContainer, be as VfDesktop, Ze as VfDialog, Dt as VfFieldset, $e as VfFormControl, Ot as VfGrid, xt as VfIcon, bt as VfImg, vt as VfLabel, Tt as VfList, Et as VfListItem, Ct as VfMenu, St as VfMenuBar, wt as VfMenuItem, Je as VfModalDialog, ft as VfNumberField, pt as VfOption, yt as VfParagraph, T as VfPositioned, gt as VfProgressBar, it as VfRadio, at as VfRadioGroup, Ke as VfScrollArea, mt as VfSelect, Qe as VfSeparator, xe as VfSized, _t as VfSlider, kt as VfStack, tt as VfSwatch, dt as VfTextArea, lt as VfTextControlBase, ut as VfTextField, nt as VfToggleControl, qe as VfWindow, Ft as applyCursor, d as applyScale, e as defineElement, a as devicePxPerSystemPx, f as effectiveScale, Ce as emit, st as focusModality, p as getScale, o as getZoom, Ue as glyphSvg, Ye as modalDialogStyles, m as onScaleChange, s as onZoomChange, ae as patternOverride, ke as prefersReducedMotion, k as registerBodyFace, M as registerDisplayFace, D as registerEmbeddedFont, Ge as renderScrollRail, le as requestGridSnap, c as resetZoomBaseline, Ae as runSelectionBlink, h as snapSys, g as snapToDevicePx, _ as snapToSystemPx, _e as steppedCornerClip, ve as steppedRectClip, ye as steppedRingClip, v as sys, y as sysLength, b as sysLengths, x as systemPxQuantum, oe as tileGrid, R as tileImage, z as tileRaster, B as tileRects, V as tileSpan, S as toSys, C as toSysExact, ct as trackFocusModality, l as truePixelRatio, F as vfBase, A as vfBodyDecls, q as vfChromeFrame, N as vfDisplay, P as vfDisplayDecls, W as vfDots, t as vfElement, ne as vfField, Q as vfFocus, $ as vfFocusRing, ee as vfFocusUnderline, K as vfHardShadowDecls, J as vfModalFrame, Y as vfPanel, re as vfScrollRail, I as vfStaticText, G as vfStripes, se as vfTileGrid, H as vfTileMaskSize, U as vfTileSize, X as vfTitleBar, te as vfToggle, Z as vfWindowWidgets };
|
package/dist/position.d.ts
CHANGED
|
@@ -11,8 +11,17 @@ export declare abstract class VfPositionedInterface extends LitElement {
|
|
|
11
11
|
left?: number | null;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* Explicit placement — `top`/`left` in whole system px, on
|
|
15
|
-
*
|
|
14
|
+
* Explicit placement — `top`/`left` in whole system px, on **every** component.
|
|
15
|
+
*
|
|
16
|
+
* No exceptions, deliberately. These are web components, and a consumer may put
|
|
17
|
+
* one wherever they like; the kit does not get to decide that a `vf-option` is
|
|
18
|
+
* only ever a popup row. The rows a container normally owns (`vf-option`,
|
|
19
|
+
* `vf-menu-item`, `vf-list-item`) and a bar's `vf-menu` take the pair on the
|
|
20
|
+
* same terms as anything else — and stating an origin on one *inside* its
|
|
21
|
+
* managing parent does take it out of that parent's flow layout, which is the
|
|
22
|
+
* placement working, not failing. Each of those components documents what its
|
|
23
|
+
* container stops doing for it. `vf-dialog` takes the pair in viewport
|
|
24
|
+
* coordinates, the one difference the platform forces (see modal-dialog.ts).
|
|
16
25
|
*
|
|
17
26
|
* A DITL resource laid a dialog out as a list of items, each with a rectangle
|
|
18
27
|
* in the window's own coordinates; arranging controls by stating where they go
|
|
@@ -51,7 +51,20 @@ var n = t`
|
|
|
51
51
|
painted element(s). Absolutely positioned satellites that anchor to the
|
|
52
52
|
host (vf-menu's panel, the default button's ring) compose the same
|
|
53
53
|
variables into their own insets instead. Controller-owned — never set the
|
|
54
|
-
variables by hand.
|
|
54
|
+
variables by hand.
|
|
55
|
+
|
|
56
|
+
The reset below makes the two properties per-host: custom properties
|
|
57
|
+
inherit, so without it a host needing no correction of its own would
|
|
58
|
+
resolve an ancestor component's values here and its paint would leave the
|
|
59
|
+
grid by exactly that amount — invisibly, since the controller measures
|
|
60
|
+
the host box, which never moves. (Measured: a placed vf-button on a
|
|
61
|
+
corrected vf-desktop painted at the desktop's -0.39px offset.) The
|
|
62
|
+
controller's writes land on the host's inline style, which wins over
|
|
63
|
+
this rule. */
|
|
64
|
+
:host {
|
|
65
|
+
--vf-snap-dx: 0px;
|
|
66
|
+
--vf-snap-dy: 0px;
|
|
67
|
+
}
|
|
55
68
|
.vf-snap {
|
|
56
69
|
position: relative;
|
|
57
70
|
left: var(--vf-snap-dx, 0px);
|
|
@@ -7,10 +7,34 @@ var t = e`
|
|
|
7
7
|
gap: calc(var(--vf-scale, 1) * 6px);
|
|
8
8
|
cursor: var(--vf-cursor, default);
|
|
9
9
|
}
|
|
10
|
+
/* The well PAINTS 3 system px below the row top. Centering the 13px well
|
|
11
|
+
in the 20px row is the layout — kept, because the host's height and its
|
|
12
|
+
exported baseline are consumer-visible (moving them off the well's
|
|
13
|
+
centered bottom reflowed every line holding a toggle) — but centering
|
|
14
|
+
lands on 3.5, an exact tie, half a system px off the pixel grid at every
|
|
15
|
+
density where 3.5 system px isn't whole device px. So the paint steps
|
|
16
|
+
back half a pixel through the same relative "top" the grid-snap
|
|
17
|
+
correction rides (this rule overrides vfBase's .vf-snap, so it must
|
|
18
|
+
compose the variable), landing the ink on row 3: ties resolve toward the
|
|
19
|
+
start, as QuickDraw's "div 2" did — the title-bar and vf-stack
|
|
20
|
+
convention. The half-pixel gap between layout box and painted box is the
|
|
21
|
+
documented snap idiom, not a fault. */
|
|
22
|
+
.box,
|
|
23
|
+
.circle {
|
|
24
|
+
top: calc(var(--vf-snap-dy, 0px) - var(--vf-scale, 1) * 0.5px);
|
|
25
|
+
}
|
|
10
26
|
/* The focus rule is drawn under the box/circle, not around the host. */
|
|
11
27
|
:host(:focus-visible) {
|
|
12
28
|
outline: none;
|
|
13
29
|
}
|
|
30
|
+
/* An empty slot still generates the label flex item, and the host gap with
|
|
31
|
+
it — a bare (label-less) control would carry 6px of phantom trailing
|
|
32
|
+
width. Each control marks the wrapper from its slotchange; display: none
|
|
33
|
+
removes the item from the gap math. The slot stays in the DOM so content
|
|
34
|
+
added later still assigns and re-fires slotchange. */
|
|
35
|
+
.label.empty {
|
|
36
|
+
display: none;
|
|
37
|
+
}
|
|
14
38
|
/* Disabled dims the label only; the box/circle glyphs stay solid black. */
|
|
15
39
|
.label.dim {
|
|
16
40
|
color: var(--vf-disabled, #c0c0c0);
|
package/dist/text-control.d.ts
CHANGED
|
@@ -69,7 +69,7 @@ export declare class VfTextControlBase extends VfShadowRoleControl {
|
|
|
69
69
|
protected forwardedAttr(name: string): string | typeof nothing;
|
|
70
70
|
/** Default-on display scaling (true 72dpi size); see src/scale.ts. */
|
|
71
71
|
protected readonly scale: ScaleController;
|
|
72
|
-
/** Device-pixel grid snapping
|
|
72
|
+
/** Device-pixel grid snapping; see src/grid-snap.ts. */
|
|
73
73
|
protected readonly gridSnap: GridSnapController;
|
|
74
74
|
/**
|
|
75
75
|
* Whether the well wears the kit's dashed focus rule — keyboard focus only.
|
package/docs/SPEC.md
CHANGED
|
@@ -46,7 +46,7 @@ Modern requirements that we deliberately keep (accessibility over purity):
|
|
|
46
46
|
- **The name/description bridge** (`VfShadowRoleControl`, src/form-control.ts). On the controls whose role lives on a shadow-internal node (the three fields, `vf-select`, `vf-swatch`, `vf-button`), a host-level `aria-labelledby`, `aria-label` or associated `<label for>` resolves — in html-aam precedence — to the inner focusable element's `aria-label` whenever the `label` property is empty (`hostLabel`). Those six extend `VfShadowRoleControl`; the host-role controls (`vf-checkbox`, `vf-radio-group`, `vf-slider`) extend the plain `VfFormControl` and so never *carry* the bridge's API at all — an inherited `description` that renders nothing is the advertised-but-inert shape this split removes, and `verify:manifest` enforces it (a tag whose manifest lists `description` must call `renderDescription()`). `vf-button` takes the ARIA half alone (`hostAriaLabel`): a `<button>` is not a labelable element, so no caption names a native one and none names this one either — its slotted content is the name a bridge doesn't override. A `description` property (or, when it's empty, a host-level `aria-describedby`) renders as a hidden span in the control's own shadow root with the inner control's `aria-describedby` pointing at it — the shadow-internal IDREF idiom `vf-dialog`'s title patch uses — and a failing constraint's `validationMessage` joins the same node. Referenced text is flattened at render time, so an edit to a referenced element's *text* lands on the next render rather than instantly — the one divergence from native. Controls whose role sits on the host (the toggles, slider, radio group, bars) need none of this: the platform reads their host attributes directly.
|
|
47
47
|
- **Constraint validation** (`VfFormControl`): a reflected `required` fails validation with `valueMissing` while the control is empty by its own definition (fields/select `value === ''`, checkbox unchecked, radio group unselected — each with its native counterpart's message), through one `syncValidity()` funnel run before each render. The native surface is complete — `checkValidity`/`reportValidity`/`validity`/`validationMessage`/ `willValidate`/`setCustomValidity` — `:invalid` matches on the host, `form.reportValidity()` blocks, and `disabled`/`readonly` bar validation per HTML's own rules. One divergence the platform imposes: `willValidate` is `true` on a `vf-button type="button"`, where a native button is barred from constraint validation. A form-associated custom element cannot opt out of the candidate set, so this is a platform limit. AT wiring is `aria-required`/`aria-invalid` on the inner control (plus internals mirrors for host-role controls) — never a forwarded native `required`, which would put UA `:user-invalid` styling on the artwork. Enter's implicit submission routes through the browser's validation, so it cannot submit past a failing constraint.
|
|
48
48
|
- Components must render nothing surprising outside their box: no margins on `:host` by default.
|
|
49
|
-
- **Explicit placement** (`VfPositioned`, src/position.ts): every
|
|
49
|
+
- **Explicit placement** (`VfPositioned`, src/position.ts): every component takes `top`/`left` in whole system px — set either and the host is absolutely positioned within its parent (`position: absolute` plus both offsets as live `calc(var(--vf-scale, 1) * Npx)` inline styles; the unset coordinate is 0; `right`/`bottom` released to `auto` and `margin` zeroed). Set neither and the element renders in flow; removing both unwinds every inline declaration. The DITL model: a dialog can be laid out by positioning its items just as validly as by stacking them. **No component is excluded** — the rows a container normally owns (`vf-option`, `vf-menu-item`, `vf-list-item`) and a bar's `vf-menu` take the pair on the same terms, because a consumer may stand one of them up on its own and the kit does not get to rule that out. Placing such a row *inside* its managing parent takes it out of that parent's flow, so the parent stops measuring it (a `vf-select`/`vf-menu` panel is as wide as its widest **flowed** row, and `popup-overflow`'s clamp counts the same set) and the rows after it close the gap — the documented consequence of the placement, not a failure of it; each component's own doc comment states what its container stops doing. Containers are deliberate anchors — the desktop raster, a window's *content region* (the frame's inner edge below the title bar; the 12px body inset governs flow content only), a dialog's content area, a stack's box, a fieldset's border interior, a scroll area's scrolled plane. The style writing rides a ReactiveController (`hostUpdated`), not an `updated()` override — component subclasses routinely skip `super.updated()` — and re-applies only when the property values changed, so an unrelated update never re-asserts a coordinate. `npm run verify:position`.
|
|
50
50
|
- `vf-dialog` takes the pair in **viewport** coordinates: `showModal()` puts the box in the top layer, whose containing block is the viewport rather than the nearest positioned ancestor. Unset means centered — recomputed on open and on every box/viewport/scale change, so a modal whose content upgrades after opening is never stranded; `position` is left to the UA and only the four inset/margin declarations are written.
|
|
51
51
|
- **Gestures write through the same properties** (`PlacementController`): a `vf-window`/`vf-dialog` title-bar drag and a `vf-icon` drag or arrow nudge state `top`/`left`, and `vf-window`'s grow box states `width`/`height` (`VfSized`) — all in whole system px, so a moved element is placed the way an authored one is and holds its spot through a zoom. Writing resolved CSS px instead was the bug: `--vf-scale` moved under the constant and every zoom step re-read it as a different number of system px (by the ratio the scale itself moved — visible even at zoom levels where the target does not change, and nothing else on the page does). Values are snapped to `snapSys` at gesture time and never re-snapped afterwards: re-rounding onto each new lattice compounds (62 → 63 → 64), and whole system px is whole device px at every rung regardless. `npm run verify:zoom` group (e).
|
|
52
52
|
- **The movable contract.** A host that moves under a gesture states its own rectangle, and its positioning parent is a box with a size. Both halves are the consumer's — a component can supply neither for itself — and both used to fail quietly, so each is a one-time `console.warn` (`warnMovableContract`, latched per element like `vf-window`'s size warning). `npm run verify:position` group CONTRACT.
|
|
@@ -121,7 +121,7 @@ Every length in this doc is a **system pixel** value; components multiply it by
|
|
|
121
121
|
|
|
122
122
|
**One size.** Both embedded faces are single 16-design-px masters and render at exactly that size — one design px = one system px, always. "Smaller" is a *family* switch: System 7's fine print was Geneva 9 — the collection's smallest strike, which **is** the body face — so a dialog's disk-space caption is `face="body"` (usually `dim`), the way `vf-icon size="small"` swaps to the 16×16 art. A genuinely different size is a different strike, registered like the embedded ones and themed in through the font family/size tokens with its own whole-pixel metrics.
|
|
123
123
|
|
|
124
|
-
**Grid snapping.** Whole system pixels only put an edge on the device grid *relative to the component's own origin*; a page that lands that origin on a fractional device pixel smears the whole 1-bit interior.
|
|
124
|
+
**Grid snapping.** Whole system pixels only put an edge on the device grid *relative to the component's own origin*; a page that lands that origin on a fractional device pixel smears the whole 1-bit interior. Every component measures its own paint and cancels the fractional remainder, always (`src/grid-snap.ts`), so the origin is the component's responsibility rather than the page's. The correction is a `left`/`top` offset on the component's own paint root — the shadow element carrying the `.vf-snap` class (`vfBase`), relatively positioned, driven by two reserved custom properties (`--vf-snap-dx`/`-dy`) the controller writes on the host. A *layout*-stage shift, because a compositing-stage `transform` leaves the subtree rastered at its old position and only removes ~80% of the fringe. Nothing outside the shadow root is written but those two properties, so the correction cannot collide with a consumer's positioning or with `vf-window`'s drag coordinates. Absolutely positioned satellites that anchor to the host (`vf-menu`'s panel, the default button's ring) compose the same properties into their insets; rows and options inside a corrected container (`vf-list-item`, `vf-menu-item`, `vf-option`) and layout-only hosts (`vf-button-group`, `vf-radio-group`) carry no target and ride their surroundings. The two properties are reset to `0` per host (`vfBase`): custom properties inherit, and without the reset a host needing no correction of its own would resolve an ancestor component's values in its shadow root and paint off the grid by exactly that amount. It corrects the origin only: a fractional *size*, and a `--vf-scale × trueDpr` that isn't whole, are still the page's to get right.
|
|
125
125
|
|
|
126
126
|
**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.
|
|
127
127
|
|
|
@@ -273,7 +273,7 @@ The color-swatch button: a well of solid color — a palette cell.
|
|
|
273
273
|
|
|
274
274
|
#### `vf-checkbox` (`VfCheckbox`, vf-checkbox.ts)
|
|
275
275
|
- **Attributes/props:** `checked`, `disabled`, `name`, `value` (default `'on'`).
|
|
276
|
-
- **Visual:** 13×13 white box, `1px solid black`, no radius; checked = classic ✕: the pixel-exact corner-to-corner cross from the sprite sheet, rendered as the `CHECKBOX_X` inline-SVG fill path (`shape-rendering: crispEdges`, `fill: currentColor`) — no anti-aliased strokes. 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).
|
|
276
|
+
- **Visual:** 13×13 white box, `1px solid black`, no radius; checked = classic ✕: the pixel-exact corner-to-corner cross from the sprite sheet, rendered as the `CHECKBOX_X` inline-SVG fill path (`shape-rendering: crispEdges`, `fill: currentColor`) — no anti-aliased strokes. The box PAINTS 3 system px below the row top: centering the 13px box 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`. 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).
|
|
277
277
|
- **Behavior:** form-associated; toggles on click and Space; `role="checkbox"`, `aria-checked`; focusable (tabindex 0 on the host).
|
|
278
278
|
- **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 `--vf-focus-underline-offset: -3px`, a dashed rule spanning the well's full 13px, one blank row under its border. The −3 and the ±1px width growth both count that 1px border, which an absolutely positioned pseudo sizes inside of. `npm run verify:focus`.
|
|
279
279
|
- **Slots:** default (label). **Parts:** `box`, `label`.
|
|
@@ -281,7 +281,7 @@ The color-swatch button: a well of solid color — a palette cell.
|
|
|
281
281
|
|
|
282
282
|
#### `vf-radio` (`VfRadio`, vf-radio.ts)
|
|
283
283
|
- **Attributes/props:** `checked`, `disabled`, `value: string`.
|
|
284
|
-
- **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). Label right, 6px gap. Disabled dims like checkbox (label only; ring + dot stay black).
|
|
284
|
+
- **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 paints at the same 3-system-px registration below the row top as the checkbox's box (same half-pixel paint step off the centering tie). Label right, 6px gap. Disabled dims like checkbox (label only; ring + dot stay black).
|
|
285
285
|
- **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 same 13px well the checkbox uses, so the two rules share a row in a mixed list (the −1 difference is only that this well has no border). 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.
|
|
286
286
|
- **Slots:** default (label). **Parts:** `circle`, `label`.
|
|
287
287
|
- **Events:** `vf-change` detail `{ value }` (fired by user interaction only).
|
|
@@ -390,7 +390,7 @@ A container whose scrollbars look like System 7.
|
|
|
390
390
|
#### `vf-fieldset` (`VfFieldset`, vf-fieldset.ts)
|
|
391
391
|
The "Install Location" group box.
|
|
392
392
|
- **Attributes/props:** `legend: string`.
|
|
393
|
-
- **Visual:** `border: 1px solid var(--vf-black, #000)`, no radius, `padding: 14px 12px 10px`, `margin-top: 8px` (room for legend). Legend: bold, positioned overlapping the top border (absolute, `top: -
|
|
393
|
+
- **Visual:** `border: 1px solid var(--vf-black, #000)`, no radius, `padding: 14px 12px 10px`, `margin-top: 8px` (room for legend). Legend: bold, positioned overlapping the top border (absolute, `top: -11px; left: 8px;` — 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 border to match its surface.
|
|
394
394
|
- **Slots:** default, plus named slot `legend` (overrides attr).
|
|
395
395
|
- **Parts:** `fieldset`, `legend`.
|
|
396
396
|
|
|
@@ -409,14 +409,15 @@ A lattice of equal cells with 1px rules between them: the Figure 5-6 tool palett
|
|
|
409
409
|
The kit's layout primitive: a flexbox whose `gap`, `pad`, `width` and `height` are declared in whole system px and converted internally, so a window's insides are laid out in the art's own unit with no `calc(var(--vf-scale, 1) * Npx)` in the consumer's stylesheet.
|
|
410
410
|
- **Why a component and not a documented snippet:** scaling is default-on and *per component* — `ScaleController` sets `--vf-scale` on the component's own host, never on the document — so `var(--vf-scale, 1)` in page CSS resolves only where the rule's element happens to sit inside a `vf-*` ancestor and inherit it. Inside a window body it does (which is the only reason the demo's own hand-written rules ever worked); for a plain `<div>` holding two buttons on an ordinary page it does not, and the fallback `1` silently renders an 8px gap around 3×-sized controls. A page that hasn't called `applyScale()` cannot express "8 system px" at all. A component always can, because it *is* the scope — which is also why the stack carries a `ScaleController` of its own.
|
|
411
411
|
- **Attributes/props:** `direction: 'column' | 'row'` (default `'column'`, reflected), `gap: number` (whole system px, default `0`), `pad: string | number` (whole system px, one to four values in CSS shorthand order), `place: 'start' | 'center' | 'end'` (reflected; unset resolves per direction), `width` / `height`: number (whole system px, optional). Whole system px is the only expressible value — a fractional entry is truncated — so the gap half of the layout contract (docs/SIZING.md rule 2) holds by construction, and a declared `width` covers the size half of rule 3. On a **child**: `fill-width` and `fill-height`, bare attributes like `nosnap`.
|
|
412
|
-
- **Visual:** none. The stack paints nothing — no border, background or shadow — and takes no role, no keyboard behavior and no selection; what it holds decides what it is, as with `vf-grid`. `:host` is a plain block shell (`width: fit-content`); the flex container is one shadow box coinciding with the host box (`.vf-snap`, `height: 100%` so a declared or filled host height reaches the flex layout) — the `vf-container` arrangement, with the placed-child anchor and the grid-snap target on the same element. The box carries no padding until `pad` writes some, so its padding box IS the host box and the placed-child anchor ignores `pad` exactly as before (`verify:position` pins this). `gap`/`pad` are written to that box's inline style as `calc(var(--vf-scale, 1) * Npx)` via `sysLength`/`sysLengths` (the var resolves against the host's own `--vf-scale` by inheritance); `width`/`height` stay on the host via `VfSized` — each stays live against the display rather than freezing at write time. **Carries a `GridSnapController`** — a reversal of the original no-controller decision, which accounted only for kit children: a stack is a positioned ancestor and a layout box for *anything*, and consumer content inside it (a div, an `<img>`, a run of text) has no controller of its own.
|
|
412
|
+
- **Visual:** none. The stack paints nothing — no border, background or shadow — and takes no role, no keyboard behavior and no selection; what it holds decides what it is, as with `vf-grid`. `:host` is a plain block shell (`width: fit-content`); the flex container is one shadow box coinciding with the host box (`.vf-snap`, `height: 100%` so a declared or filled host height reaches the flex layout) — the `vf-container` arrangement, with the placed-child anchor and the grid-snap target on the same element. The box carries no padding until `pad` writes some, so its padding box IS the host box and the placed-child anchor ignores `pad` exactly as before (`verify:position` pins this). `gap`/`pad` are written to that box's inline style as `calc(var(--vf-scale, 1) * Npx)` via `sysLength`/`sysLengths` (the var resolves against the host's own `--vf-scale` by inheritance); `width`/`height` stay on the host via `VfSized` — each stays live against the display rather than freezing at write time. **Carries a `GridSnapController`** — a reversal of the original no-controller decision, which accounted only for kit children: a stack is a positioned ancestor and a layout box for *anything*, and consumer content inside it (a div, an `<img>`, a run of text) has no controller of its own. The correction moves the stack's whole coordinate system; kit children then find nothing left to correct (the sweep runs outermost-first). It does not fix what it never could: a text-governed child width mid-row still shifts later siblings fractionally (their own controllers cover that).
|
|
413
413
|
- **The content governs the box.** A column is as wide as its widest child and a row as tall as its tallest; children neither grow nor shrink (`::slotted(*) { flex: 0 0 auto }`). System 7 controls keep their natural sizes — a push button is as wide as its label, a popup menu hugs its widest option, a swatch is a fixed well — and a window is a fixed box whose overflow is clipped at the frame, not a layout that squeezes its controls. The stack distributes; it never resizes. `width: fit-content` is the same rule stated in the box model: a layout box that claimed its parent's whole width would be handing out a size nobody declared. It shrink-wraps while staying **block-level**, which is deliberate — `inline-flex` gives the same width but puts the box on a line box, and a line box can never be shorter than its parent's strut, so a stack shorter than the surrounding `line-height` silently gains the difference as leading (the showcase's swatch panel, an 18px row in a 20px line box, grew by exactly that). `width`/`height` override the content when an author says so, in system px, and beat `fit-content` from the host's inline style.
|
|
414
414
|
- **`place` defaults per direction** — `start` down a column (a field starts at the panel edge), `center` across a row (a caption sits beside its control). The two directions call for opposite defaults. Both are stated as the direction's own rather than as an `auto` value, so an unrecognized `place` — a stale `stretch` from before this API — lands on the sane one instead of on flexbox's `normal`, which stretches. It is the only placement the stack owns: there is no `justify`, and a right-aligned action row is a filled column whose one child sits at the end of it. **It is `place` and not `align` because `align` is a legacy HTML presentation attribute** — see the transparency bullet below.
|
|
415
415
|
- **`fill-width` / `fill-height` name the outcome, not an axis**, so each compiles to the main axis (`flex: 1 1 0` plus the `min-*: 0` that lets it shrink below its content) or the cross axis (`align-self: stretch`) depending on which way the stack runs — four static rules, no allowlist. What follows is one rule about geometry rather than vocabulary: **the cross axis always has a size, the main axis only has slack if one was declared.** `fill-width` always works in a column and needs a declared `width` in a row; `fill-height` is the other way round; a fill with nothing to take is inert rather than an error; and two children filling along the main axis come out equal, the zeroed flex basis being what lets them divide the slack instead of keeping their natural sizes. A child that declares its own size shouldn't also ask to fill it — in a column the declared size wins, in a row the fill does. The host reads both attributes about **itself** too (`width`/`height: 100%`), for the parents that aren't stacks — a window body, a fieldset, a scroll well, a grid cell — which is where a panel's width enters the tree. Three components have no width of their own and need a fill to take one: `vf-separator`, `vf-progress-bar` and `vf-slider` are drawn as a rule or a track that *is* the width. A light-DOM declaration beats a `::slotted` one, so `align-self: stretch` in page CSS remains the escape hatch for a cross-axis fill a direction doesn't offer.
|
|
416
416
|
- **Typographically transparent.** `vfBase` dresses a host as chrome (body face, a 1.25 ratio line box, black, unselectable); the stack returns `font`, `-webkit-font-smoothing`, `color`, `user-select` and `text-align` to `inherit`, because wrapping content in a layout box must not change how that content reads. `text-align` is on that list because **`align` is a legacy HTML presentation attribute**: Blink maps the `align` content attribute on any HTML element to `text-align` — "left" / "right" / "center" by name, anything else verbatim — so `align="end"` on an action row right-aligned every run of copy inside it. Renaming the attribute to `place` is the fix; the reset stays because it costs nothing and keeps markup written against the old spelling harmless. The hint loses to a `:host` rule, and the page's own `text-align` still inherits through (`verify:stack` asserts all three). Of every attribute name the kit uses, `align` was the only one that carried a hint — `width`, `height`, `color`, `size`, `direction`, `label` and `value` are all inert on a custom element. Inside a window it goes on inheriting the window's face and the SPEC §1 chrome selection rule; on an ordinary page it leaves the page's typography — and its whole-pixel line boxes — alone. A ratio line-height landing on slotted prose is the exact rule-2 fault the kit warns pages about.
|
|
417
417
|
- **The gap is the real CSS property, not a token.** Written as a custom property it would inherit, and a nested stack would silently pick up its parent's spacing (`--vf-button-group-gap` inherits on purpose; this must not).
|
|
418
418
|
- **What it deliberately doesn't do:** equalize its children's widths. A row of buttons still belongs in a `vf-button-group`, which sizes them to the widest and aligns their *faces* rather than the `variant="default"` ring boxes a plain flex row lines up. It is also not a grid: `grid-template-columns: 1fr auto 1fr` stays page CSS.
|
|
419
|
-
- **
|
|
419
|
+
- **Centering lands on whole system px** (`CrossCenterController`, src/cross-center.ts). Halving an odd count of free system px gives a half — a 16px caption centered against the 25-system-px `vf-number-field` sits at 4.5, a 71px button in a 200px stack at 64.5 — and that is half a *system* px, so the child's whole 1-bit interior misses the device grid rather than just its paint. No container can round it in CSS (percentages resolve against the container in `left`/`margin` and against the element itself only in a transform, and a fractional transform leaves the composite-time fringe grid-snap.ts measured), so the controller measures where flexbox put each child, relative to the box, and steps it back onto whole system px — the exact half going toward the start, as QuickDraw's `div 2` did. It measures rather than derives, because a stack holds anything, including a child whose own width isn't whole system px (`vf-select` is 88 at 1× and 87⅓ at 1.5×, where its metrics land on whole device px instead) — deriving would move such a child *off* a grid position it already held. Its whole footprint on consumer DOM is `data-vf-tie` and `--vf-stack-dx` / `--vf-stack-dy` on a corrected child, applied by a `::slotted([data-vf-tie])` rule so that any inline style (a `vf-window` drag writes its own `left`) or page rule wins. Re-measured on every render and on any resize of the box or a child; `place="start"` is still the way to opt out of centering altogether. It does not touch a stack the page itself put on a fractional origin — that stays the page's fault (docs/SIZING.md rule 3) or grid snapping's to absorb.
|
|
420
|
+
- **One trap it does not fix.** A flex container does not collapse margins, so `vf-fieldset`'s legend room — an 8px `margin-top` on its inner box that escapes the host in block flow — is genuinely reserved inside a stack instead of being donated by whatever precedes it.
|
|
420
421
|
- **Slots:** default (the children; `fill-width` / `fill-height` on any of them). **Parts:** none. **Events:** none.
|
|
421
422
|
|
|
422
423
|
#### `vf-container` (`VfContainer`, vf-container.ts)
|
|
@@ -424,7 +425,7 @@ A plain sized box: `width`/`height` in whole system px around a bare slot — no
|
|
|
424
425
|
- **Attributes/props:** `width` / `height`: number (whole system px, via `VfSized`); the `top` / `left` pair via `VfPositioned` like nearly every component. On a **child**: `fill-width` and `fill-height`, bare attributes as in `vf-stack`. That is the whole API.
|
|
425
426
|
- **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.
|
|
426
427
|
- **Visual:** none — no border, background, role, keyboard behavior or selection; what it holds decides what it is. `:host` is a block with `width: fit-content`, so an undeclared axis shrink-wraps rather than claiming the parent's width (the stack's rule — a layout box must not hand out a size nobody declared; a declared dimension lands on the host's inline style and beats it). The slot sits in one shadow box coinciding with the host box (`.vf-snap`, `display: flow-root`, `height: 100%`): flow-root so a slotted margin cannot collapse through the top edge and push the coordinate origin off the host's corner, 100% so percentage fills resolve against a declared height. That box owns `position: relative` — the anchor for `top`/`left` children — the component's purpose. Content that outgrows the declared box overflows it rather than growing it.
|
|
427
|
-
- **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
|
|
428
|
+
- **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.)
|
|
428
429
|
- **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.
|
|
429
430
|
- **`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.
|
|
430
431
|
- **Carries a `ScaleController`** for the stack's reason: a lone container on a plain page must resolve its declared size against the true scale, not the `var(--vf-scale, 1)` fallback its slotted children each escape on their own.
|
|
@@ -480,7 +481,7 @@ The Finder icon — pixel art in a reserved cell with its name on a plate below,
|
|
|
480
481
|
|
|
481
482
|
The full-viewport System 7 desktop that used to close this spec — menu bar, nine windows, the composed alert, the utility palette, the Finder icons — is no longer part of this repo. It lives in **[aportilla/system7web](https://github.com/aportilla/system7web)** and consumes `vintage-frames` from npm, so it exercises the same published API a consumer gets rather than reaching into `src/`. Its clause-by-clause spec moved with it, to that repo's `docs/SPEC.md`.
|
|
482
483
|
|
|
483
|
-
What the kit keeps as its own demo surface is `index.html`, the component reference (every element, its API, a live specimen of each state)
|
|
484
|
+
What the kit keeps as its own demo surface is `index.html`, the component reference (every element, its API, a live specimen of each state). See [DEVELOPING.md](DEVELOPING.md).
|
|
484
485
|
|
|
485
486
|
<details>
|
|
486
487
|
<summary>The original §7 clauses, for the record</summary>
|