vintage-frames 0.12.1 → 0.12.3
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 +402 -11
- package/dist/components/vf-desktop.d.ts +9 -5
- package/dist/components/vf-icon-field.js +12 -14
- package/dist/components/vf-icon.d.ts +55 -10
- package/dist/components/vf-icon.js +216 -182
- package/dist/components/vf-window.d.ts +90 -0
- package/dist/components/vf-window.js +248 -77
- package/dist/index.d.ts +2 -1
- package/dist/index.js +43 -43
- package/dist/motion.d.ts +44 -0
- package/dist/motion.js +13 -5
- package/dist/open-art.d.ts +17 -4
- package/dist/open-art.js +12 -6
- package/docs/SPEC.md +9 -9
- package/editor/vscode.html-custom-data.json +8 -3
- package/editor/web-types.json +18 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30,46 +30,46 @@ import { BUTTON_FACE as Ee, BUTTON_FRAME as De, RING_FRAME as Oe, RING_HOLE as k
|
|
|
30
30
|
import { VfDesktop as Fe } from "./components/vf-desktop.js";
|
|
31
31
|
import { VfSized as Ie } from "./size.js";
|
|
32
32
|
import { DragController as Le } from "./drag.js";
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import { CARET_DOWN as
|
|
36
|
-
import { ScrollRailController as
|
|
37
|
-
import { VfScrollArea as
|
|
38
|
-
import { VfWindow as
|
|
39
|
-
import { VfModalDialog as
|
|
40
|
-
import { VfDialog as
|
|
41
|
-
import { VfSeparator as
|
|
42
|
-
import { VfFormControl as
|
|
43
|
-
import { VfButton as
|
|
44
|
-
import { VfButtonGroup as
|
|
45
|
-
import { VfSwatch as
|
|
46
|
-
import { VfToggleControl as
|
|
47
|
-
import { VfCheckbox as
|
|
48
|
-
import { VfRadio as
|
|
49
|
-
import { VfRadioGroup as
|
|
50
|
-
import { FocusRuleController as
|
|
51
|
-
import { VfTextControlBase as
|
|
52
|
-
import { VfTextField as
|
|
53
|
-
import { VfTextArea as
|
|
54
|
-
import { VfNumberField as
|
|
55
|
-
import { VfOption as
|
|
56
|
-
import { VfSelect as
|
|
57
|
-
import { VfProgressBar as
|
|
58
|
-
import { VfSlider as
|
|
59
|
-
import { VfLabel as
|
|
60
|
-
import { VfParagraph as
|
|
61
|
-
import { VfImg as
|
|
62
|
-
import { VfIcon as
|
|
63
|
-
import { VfIconField as
|
|
64
|
-
import { VfMenuBar as
|
|
65
|
-
import { VfMenu as
|
|
66
|
-
import { VfMenuItem as
|
|
67
|
-
import { VfList as
|
|
68
|
-
import { VfListItem as
|
|
69
|
-
import { VfFieldset as
|
|
70
|
-
import { VfGrid as
|
|
71
|
-
import { VfStack as
|
|
72
|
-
import { VfContainer as
|
|
73
|
-
import { CURSOR_ARROW as
|
|
74
|
-
import { applyCursor as
|
|
75
|
-
export {
|
|
33
|
+
import { BLINK_FLIPS as Re, BLINK_INTERVAL_MS as ze, DRAG_SLOP_COARSE_PX as Be, DRAG_SLOP_PX as Ve, PRESS_HOLD_MS as He, RENAME_DELAY_MS as Ue, TAP_PAIR_MS as We, TAP_PAIR_SLOP_PX as Ge, WALK_BEAT_MS as Ke, WALK_STEP_MS as qe, WALK_STEP_PX as Je, WINDOW_RECTS_VISIBLE as Ye, WINDOW_RECT_RATIO as Xe, WINDOW_RECT_STEPS as Ze, WINDOW_RECT_STEP_MS as Qe, dragSlop as $e, prefersReducedMotion as et, runOutlineTravel as tt, runSelectionBlink as nt, windowRectFraction as rt } from "./motion.js";
|
|
34
|
+
import { ScrollStateController as it } from "./scroll-state.js";
|
|
35
|
+
import { CARET_DOWN as at, CARET_DOWN_SMALL as ot, CHECKBOX_X as st, CHECKMARK as ct, RADIO_DOT as lt, RADIO_FACE as ut, RADIO_RING as dt, RADIO_RING_PRESSED as ft, SLIDER_THUMB as pt, SLIDER_THUMB_FACE as mt, STEPPER as ht, STEPPER_DOWN_FILL as gt, STEPPER_UP_FILL as _t, glyphSvg as vt } from "./glyphs.js";
|
|
36
|
+
import { ScrollRailController as yt, renderScrollRail as bt } from "./scroll-rail.js";
|
|
37
|
+
import { VfScrollArea as xt } from "./components/vf-scroll-area.js";
|
|
38
|
+
import { VfWindow as St } from "./components/vf-window.js";
|
|
39
|
+
import { VfModalDialog as Ct, modalDialogStyles as wt } from "./modal-dialog.js";
|
|
40
|
+
import { VfDialog as Tt } from "./components/vf-dialog.js";
|
|
41
|
+
import { VfSeparator as Et } from "./components/vf-separator.js";
|
|
42
|
+
import { VfFormControl as Dt } from "./form-control.js";
|
|
43
|
+
import { VfButton as Ot } from "./components/vf-button.js";
|
|
44
|
+
import { VfButtonGroup as kt } from "./components/vf-button-group.js";
|
|
45
|
+
import { VfSwatch as At } from "./components/vf-swatch.js";
|
|
46
|
+
import { VfToggleControl as jt } from "./toggle-control.js";
|
|
47
|
+
import { VfCheckbox as Mt } from "./components/vf-checkbox.js";
|
|
48
|
+
import { VfRadio as Nt } from "./components/vf-radio.js";
|
|
49
|
+
import { VfRadioGroup as Pt } from "./components/vf-radio-group.js";
|
|
50
|
+
import { FocusRuleController as Ft, focusModality as It, trackFocusModality as Lt } from "./focus-modality.js";
|
|
51
|
+
import { VfTextControlBase as Rt } from "./text-control.js";
|
|
52
|
+
import { VfTextField as zt } from "./components/vf-text-field.js";
|
|
53
|
+
import { VfTextArea as Bt } from "./components/vf-text-area.js";
|
|
54
|
+
import { VfNumberField as Vt } from "./components/vf-number-field.js";
|
|
55
|
+
import { VfOption as Ht } from "./components/vf-option.js";
|
|
56
|
+
import { VfSelect as Ut } from "./components/vf-select.js";
|
|
57
|
+
import { VfProgressBar as Wt } from "./components/vf-progress-bar.js";
|
|
58
|
+
import { VfSlider as Gt } from "./components/vf-slider.js";
|
|
59
|
+
import { VfLabel as Kt } from "./components/vf-label.js";
|
|
60
|
+
import { VfParagraph as qt } from "./components/vf-paragraph.js";
|
|
61
|
+
import { VfImg as Jt } from "./components/vf-img.js";
|
|
62
|
+
import { VfIcon as Yt } from "./components/vf-icon.js";
|
|
63
|
+
import { VfIconField as Xt } from "./components/vf-icon-field.js";
|
|
64
|
+
import { VfMenuBar as Zt } from "./components/vf-menu-bar.js";
|
|
65
|
+
import { VfMenu as Qt } from "./components/vf-menu.js";
|
|
66
|
+
import { VfMenuItem as $t } from "./components/vf-menu-item.js";
|
|
67
|
+
import { VfList as en } from "./components/vf-list.js";
|
|
68
|
+
import { VfListItem as tn } from "./components/vf-list-item.js";
|
|
69
|
+
import { VfFieldset as nn } from "./components/vf-fieldset.js";
|
|
70
|
+
import { VfGrid as rn } from "./components/vf-grid.js";
|
|
71
|
+
import { VfStack as an } from "./components/vf-stack.js";
|
|
72
|
+
import { VfContainer as on } from "./components/vf-container.js";
|
|
73
|
+
import { CURSOR_ARROW as sn, CURSOR_CROSSHAIR as cn, CURSOR_I_BEAM as ln, CURSOR_WAIT as un } from "./cursor-art.js";
|
|
74
|
+
import { applyCursor as dn } from "./cursor.js";
|
|
75
|
+
export { Re as BLINK_FLIPS, ze as BLINK_INTERVAL_MS, Ee as BUTTON_FACE, De as BUTTON_FRAME, at as CARET_DOWN, ot as CARET_DOWN_SMALL, st as CHECKBOX_X, ct as CHECKMARK, r as CLASSIC_DPI, i as CSS_REFERENCE_DPI, sn as CURSOR_ARROW, cn as CURSOR_CROSSHAIR, ln as CURSOR_I_BEAM, un as CURSOR_WAIT, Be as DRAG_SLOP_COARSE_PX, Ve as DRAG_SLOP_PX, Te as DocumentListenersController, Le as DragController, Ft as FocusRuleController, Ce as GridSnapController, G as PATTERNS, K as PATTERN_NAMES, q as PATTERN_SIZE, D as PIXEL_GRID_METRICS, He as PRESS_HOLD_MS, xe as PatternFillController, T as PlacementController, lt as RADIO_DOT, ut as RADIO_FACE, dt as RADIO_RING, ft as RADIO_RING_PRESSED, Ue as RENAME_DELAY_MS, Oe as RING_FRAME, ke as RING_HOLE, Ae as RING_INSET, ie as RULE_EDGES, je as SCREEN_CORNER, pt as SLIDER_THUMB, mt as SLIDER_THUMB_FACE, ht as STEPPER, gt as STEPPER_DOWN_FILL, _t as STEPPER_UP_FILL, a as SYSTEM_PX_IN_CSS_PX, d as ScaleController, yt as ScrollRailController, it as ScrollStateController, We as TAP_PAIR_MS, Ge as TAP_PAIR_SLOP_PX, R as TILE_LATTICE, ge as TileRasterCache, be as TrackWidthController, k as VF_BODY_FAMILY, M as VF_DISPLAY_FAMILY, Ot as VfButton, kt as VfButtonGroup, Mt as VfCheckbox, on as VfContainer, Fe as VfDesktop, Tt as VfDialog, nn as VfFieldset, Dt as VfFormControl, rn as VfGrid, Yt as VfIcon, Xt as VfIconField, Jt as VfImg, Kt as VfLabel, en as VfList, tn as VfListItem, Qt as VfMenu, Zt as VfMenuBar, $t as VfMenuItem, Ct as VfModalDialog, Vt as VfNumberField, Ht as VfOption, qt as VfParagraph, E as VfPositioned, Wt as VfProgressBar, Nt as VfRadio, Pt as VfRadioGroup, xt as VfScrollArea, Ut as VfSelect, Et as VfSeparator, Ie as VfSized, Gt as VfSlider, an as VfStack, At as VfSwatch, Bt as VfTextArea, Rt as VfTextControlBase, zt as VfTextField, jt as VfToggleControl, St as VfWindow, Ke as WALK_BEAT_MS, qe as WALK_STEP_MS, Je as WALK_STEP_PX, Ye as WINDOW_RECTS_VISIBLE, Xe as WINDOW_RECT_RATIO, Ze as WINDOW_RECT_STEPS, Qe as WINDOW_RECT_STEP_MS, dn as applyCursor, f as applyScale, t as defineElement, o as devicePxPerSystemPx, $e as dragSlop, p as effectiveScale, e as emit, It as focusModality, m as getScale, s as getZoom, vt as glyphSvg, wt as modalDialogStyles, h as onScaleChange, c as onZoomChange, J as parsePattern, ae as parseRule, Y as patternHex, X as patternMotif, _e as patternOverride, Z as patternRects, et as prefersReducedMotion, A as registerBodyFace, N as registerDisplayFace, O as registerEmbeddedFont, bt as renderScrollRail, we as requestGridSnap, l as resetZoomBaseline, oe as ruleClasses, tt as runOutlineTravel, nt as runSelectionBlink, g as snapSys, _ as snapToDevicePx, v as snapToSystemPx, Me as steppedCornerClip, Ne as steppedRectClip, Pe as steppedRingClip, y as sys, b as sysLength, x as sysLengths, S as systemPxQuantum, ve as tileGrid, z as tileImage, B as tileRaster, V as tileRects, H as tileSpan, C as toSys, w as toSysExact, Lt as trackFocusModality, u as truePixelRatio, I as vfBase, j as vfBodyDecls, te as vfChromeFrame, P as vfDisplay, F as vfDisplayDecls, Q as vfDots, n as vfElement, me as vfField, ue as vfFocus, de as vfFocusRing, fe as vfFocusUnderline, ee as vfHardShadowDecls, ne as vfModalFrame, re as vfPanel, Se as vfPatternFill, se as vfRule, he as vfScrollRail, L as vfStaticText, $ as vfStripes, ye as vfTileGrid, U as vfTileMaskSize, W as vfTileSize, ce as vfTitleBar, pe as vfToggle, le as vfWindowWidgets, rt as windowRectFraction };
|
package/dist/motion.d.ts
CHANGED
|
@@ -38,6 +38,29 @@ export declare const PRESS_HOLD_MS = 200;
|
|
|
38
38
|
* open. So the number leans long deliberately.
|
|
39
39
|
*/
|
|
40
40
|
export declare const RENAME_DELAY_MS = 800;
|
|
41
|
+
/**
|
|
42
|
+
* The tap pair: how far apart two taps of a finger or pen on an icon may
|
|
43
|
+
* land — milliseconds from press to press, CSS px from point to point — and
|
|
44
|
+
* still open it. The double-click of an instrument the platform reports no
|
|
45
|
+
* `dblclick` for; the mouse keeps the platform's own, which follows a
|
|
46
|
+
* setting the user can move.
|
|
47
|
+
*
|
|
48
|
+
* Inside {@link RENAME_DELAY_MS}, so the second tap always lands before a
|
|
49
|
+
* rename the first one armed can open. The distance is CSS px rather than
|
|
50
|
+
* system px: it measures the hand, not the art.
|
|
51
|
+
*/
|
|
52
|
+
export declare const TAP_PAIR_MS = 500;
|
|
53
|
+
export declare const TAP_PAIR_SLOP_PX = 24;
|
|
54
|
+
/**
|
|
55
|
+
* The drag slop: how far, in CSS px, a pointer travels from its press before
|
|
56
|
+
* an icon's drag begins. A press released inside it is a click or a tap, and
|
|
57
|
+
* the icon stays where it was. Wider for a finger or pen, which never lands
|
|
58
|
+
* still. CSS px for the same reason as the tap pair's.
|
|
59
|
+
*/
|
|
60
|
+
export declare const DRAG_SLOP_PX = 4;
|
|
61
|
+
export declare const DRAG_SLOP_COARSE_PX = 10;
|
|
62
|
+
/** The {@link DRAG_SLOP_PX drag slop} for the instrument a pointer event reports. */
|
|
63
|
+
export declare function dragSlop(event: PointerEvent): number;
|
|
41
64
|
/**
|
|
42
65
|
* Milliseconds per row while the pointer rests on a menu scroll arrow — the
|
|
43
66
|
* pace a clipped popup rolls its list through the panel at (~15 rows/s).
|
|
@@ -101,3 +124,24 @@ export interface TravelHandle {
|
|
|
101
124
|
* `prefers-reduced-motion`, where it runs at once and the handle is a no-op.
|
|
102
125
|
*/
|
|
103
126
|
export declare function runOutlineTravel(distance: number, setProgress: (t: number) => void, onDone: () => void): TravelHandle;
|
|
127
|
+
/**
|
|
128
|
+
* The zoom rects a window opens with (`vf-window.show({ from })`): how many
|
|
129
|
+
* rects are drawn, how many are on screen at once, the ratio between the
|
|
130
|
+
* fractions of the way consecutive rects sit, and the milliseconds a step
|
|
131
|
+
* lasts. A fixed count rather than the walk's constant speed: every
|
|
132
|
+
* distance takes the same eighteen steps — one per rect, then four as the
|
|
133
|
+
* trail drains. Provisional. verify:window computes its expected frames
|
|
134
|
+
* from the first three; nothing asserts the step's length.
|
|
135
|
+
*/
|
|
136
|
+
export declare const WINDOW_RECT_STEPS = 14;
|
|
137
|
+
export declare const WINDOW_RECTS_VISIBLE = 4;
|
|
138
|
+
export declare const WINDOW_RECT_RATIO = 0.7;
|
|
139
|
+
export declare const WINDOW_RECT_STEP_MS = 17;
|
|
140
|
+
/**
|
|
141
|
+
* How far window rect `k` (0 … {@link WINDOW_RECT_STEPS} − 1) sits from the
|
|
142
|
+
* small rect toward the big one, as a fraction of the way:
|
|
143
|
+
* `WINDOW_RECT_RATIO ** (WINDOW_RECT_STEPS − k)`, 0.007 for the first and
|
|
144
|
+
* 0.7 for the last. The rects bunch at the small rect and open out toward
|
|
145
|
+
* the big one, and the last stops seven tenths of the way.
|
|
146
|
+
*/
|
|
147
|
+
export declare function windowRectFraction(k: number): number;
|
package/dist/motion.js
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
function e() {
|
|
3
3
|
return typeof window < "u" && typeof window.matchMedia == "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
4
4
|
}
|
|
5
|
-
var t = 200, n = 800, r =
|
|
6
|
-
function
|
|
5
|
+
var t = 200, n = 800, r = 500, i = 24, a = 4, o = 10;
|
|
6
|
+
function s(e) {
|
|
7
|
+
return e.pointerType === "mouse" ? 4 : 10;
|
|
8
|
+
}
|
|
9
|
+
var c = 42, l = 6;
|
|
10
|
+
function u(t, n) {
|
|
7
11
|
if (e()) return n(), { cancel() {} };
|
|
8
12
|
let r = 0;
|
|
9
13
|
t(!1);
|
|
@@ -18,8 +22,8 @@ function a(t, n) {
|
|
|
18
22
|
window.clearInterval(i);
|
|
19
23
|
} };
|
|
20
24
|
}
|
|
21
|
-
var
|
|
22
|
-
function
|
|
25
|
+
var d = 16, f = 25, p = 50;
|
|
26
|
+
function m(t, n, r) {
|
|
23
27
|
if (e()) return r(), { finish() {} };
|
|
24
28
|
let i = Math.max(1, Math.ceil(t / 16)), a = 0, o = !1, s = 0, c = () => {
|
|
25
29
|
o || (o = !0, window.clearTimeout(s), r());
|
|
@@ -32,5 +36,9 @@ function l(t, n, r) {
|
|
|
32
36
|
};
|
|
33
37
|
return s = window.setTimeout(l, 25), { finish: c };
|
|
34
38
|
}
|
|
39
|
+
var h = 14, g = 4, _ = .7, v = 17;
|
|
40
|
+
function y(e) {
|
|
41
|
+
return _ ** (14 - e);
|
|
42
|
+
}
|
|
35
43
|
//#endregion
|
|
36
|
-
export {
|
|
44
|
+
export { l as BLINK_FLIPS, c as BLINK_INTERVAL_MS, o as DRAG_SLOP_COARSE_PX, a as DRAG_SLOP_PX, t as PRESS_HOLD_MS, n as RENAME_DELAY_MS, r as TAP_PAIR_MS, i as TAP_PAIR_SLOP_PX, p as WALK_BEAT_MS, f as WALK_STEP_MS, d as WALK_STEP_PX, g as WINDOW_RECTS_VISIBLE, _ as WINDOW_RECT_RATIO, h as WINDOW_RECT_STEPS, v as WINDOW_RECT_STEP_MS, s as dragSlop, e as prefersReducedMotion, m as runOutlineTravel, u as runSelectionBlink, y as windowRectFraction };
|
package/dist/open-art.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Finder "open" ghost — an icon's art redrawn as outline plus dither,
|
|
3
3
|
* derived from the art itself in the client — and, on the same terms, the
|
|
4
|
-
* dotted outline an icon drags as ({@link deriveDragOutline})
|
|
4
|
+
* dotted outline an icon drags as ({@link deriveDragOutline}), and the XOR pen
|
|
5
|
+
* that outline, the rubber band and a window's zoom rects and drag outline
|
|
6
|
+
* draw with ({@link xorPenCanvas}).
|
|
5
7
|
*
|
|
6
8
|
* When a folder or application is open, the Finder redraws its icon as a
|
|
7
9
|
* ghost: the outline stays, and everything inside it becomes pattern. The kit
|
|
@@ -68,6 +70,16 @@ export interface DragOutlineGeometry {
|
|
|
68
70
|
/** The name plate, or null with no name. */
|
|
69
71
|
plate: DragOutlineBox | null;
|
|
70
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* A canvas that paints with the XOR pen — the cursor's recipe: its ink is
|
|
75
|
+
* turned white by `filter: invert(1)`, then differenced against whatever is
|
|
76
|
+
* beneath it (`mix-blend-mode: difference`), so a black pixel of the raster
|
|
77
|
+
* inverts the pixel under it and a transparent one leaves it alone.
|
|
78
|
+
* Nearest-neighbor at one image px per system px, and never a hit. The
|
|
79
|
+
* caller positions and sizes it: the drag outline, the rubber band, and a
|
|
80
|
+
* window's zoom rects and drag outline all draw with one.
|
|
81
|
+
*/
|
|
82
|
+
export declare function xorPenCanvas(className: string): HTMLCanvasElement;
|
|
71
83
|
/**
|
|
72
84
|
* Derive the outline an icon drags as: the boundary of the mask united with
|
|
73
85
|
* the name plate's rectangle — one shape, the one-pixel ring a 3×3 box
|
|
@@ -106,9 +118,10 @@ export declare function deriveDragOutline(art: HTMLImageElement | HTMLCanvasElem
|
|
|
106
118
|
export declare function penPhase(x: number, y: number): number;
|
|
107
119
|
/**
|
|
108
120
|
* Paint a selection rectangle — the rubber band `vf-icon-field` drags on its
|
|
109
|
-
* background
|
|
110
|
-
*
|
|
111
|
-
* a one-pixel line
|
|
121
|
+
* background, and each of a window's zoom rects and its drag outline — into
|
|
122
|
+
* `into`: a one-pixel outline `width` × `height` system px, dotted at `phase`
|
|
123
|
+
* the way the drag outline is. A degenerate axis paints as a one-pixel line
|
|
124
|
+
* rather than nothing.
|
|
112
125
|
*/
|
|
113
126
|
export declare function paintSelectionRect(into: HTMLCanvasElement, width: number, height: number, phase: number): void;
|
|
114
127
|
/**
|
package/dist/open-art.js
CHANGED
|
@@ -41,7 +41,13 @@ var i = null, a = () => {
|
|
|
41
41
|
}, o = (e, t) => {
|
|
42
42
|
t.width <= 0 || t.height <= 0 || (e.fillStyle = "#000", e.fillRect(t.x, t.y, t.width, t.height), t.width > 2 && t.height > 2 && e.clearRect(t.x + 1, t.y + 1, t.width - 2, t.height - 2));
|
|
43
43
|
};
|
|
44
|
-
function s(
|
|
44
|
+
function s(e) {
|
|
45
|
+
let t = document.createElement("canvas");
|
|
46
|
+
t.className = e;
|
|
47
|
+
let n = t.style;
|
|
48
|
+
return n.display = "block", n.pointerEvents = "none", n.filter = "invert(1)", n.mixBlendMode = "difference", n.imageRendering = "pixelated", t;
|
|
49
|
+
}
|
|
50
|
+
function c(t, n) {
|
|
45
51
|
let { width: r, height: i } = n;
|
|
46
52
|
if (!r || !i) return null;
|
|
47
53
|
let a = e(r, i), o = e(r, i), s = e(r, i);
|
|
@@ -63,19 +69,19 @@ function s(t, n) {
|
|
|
63
69
|
]) (e || t) && o.ctx.drawImage(a.canvas, e, t);
|
|
64
70
|
return s.ctx.drawImage(a.canvas, 0, 0), s.ctx.globalCompositeOperation = "destination-out", s.ctx.drawImage(o.canvas, 0, 0), s.ctx.globalCompositeOperation = "source-over", s.canvas;
|
|
65
71
|
}
|
|
66
|
-
function
|
|
72
|
+
function l(e, t) {
|
|
67
73
|
return +(((e + t) % 2 + 2) % 2 == 0);
|
|
68
74
|
}
|
|
69
|
-
function
|
|
75
|
+
function u(t, n, r, i) {
|
|
70
76
|
let a = Math.max(1, Math.round(n)), s = Math.max(1, Math.round(r)), c = e(a, s);
|
|
71
77
|
c && (o(c.ctx, {
|
|
72
78
|
x: 0,
|
|
73
79
|
y: 0,
|
|
74
80
|
width: a,
|
|
75
81
|
height: s
|
|
76
|
-
}),
|
|
82
|
+
}), d(c.canvas, t, i));
|
|
77
83
|
}
|
|
78
|
-
function
|
|
84
|
+
function d(e, t, n) {
|
|
79
85
|
let r = a(), i = t.getContext("2d");
|
|
80
86
|
if (!r || !i) return;
|
|
81
87
|
t.width = e.width, t.height = e.height, i.imageSmoothingEnabled = !1, i.drawImage(e, 0, 0);
|
|
@@ -83,4 +89,4 @@ function u(e, t, n) {
|
|
|
83
89
|
o && (i.globalCompositeOperation = "destination-in", i.fillStyle = o, i.translate(n & 1, 0), i.fillRect(-2, 0, e.width + 2, e.height), i.setTransform(1, 0, 0, 1, 0, 0), i.globalCompositeOperation = "source-over");
|
|
84
90
|
}
|
|
85
91
|
//#endregion
|
|
86
|
-
export {
|
|
92
|
+
export { c as deriveDragOutline, r as deriveOpenArt, d as dotOutline, u as paintSelectionRect, l as penPhase, s as xorPenCanvas };
|
package/docs/SPEC.md
CHANGED
|
@@ -13,7 +13,7 @@ The System 7 look, distilled:
|
|
|
13
13
|
- **Racing-stripe title bars** — 6 horizontal 1px black pinstripes on the white title bar; the title text and window widgets sit on solid white patches that interrupt the stripes.
|
|
14
14
|
- **Chicago-style type** — bold, dark, tight. One size for almost everything.
|
|
15
15
|
- **1-bit monochrome palette** — black and white only, plus two grays: light gray (`#C0C0C0`, the kit's dim tone) for dimmed/disabled chrome, and mid gray (`#808080`) as the base tone under the desktop's 50% dither. Scroll troughs use a looser 25% black-on-white dot dither. Surfaces are flat solid white — no bevels.
|
|
16
|
-
- **No gradients, no border-radius, no CSS transitions** — interactions are instant. The sanctioned animations are the classic menu-item "blink" on selection, the indeterminate progress stripes,
|
|
16
|
+
- **No gradients, no border-radius, no CSS transitions** — interactions are instant. The sanctioned animations are the classic menu-item "blink" on selection, the indeterminate progress stripes, the walk — an icon's drag outline travelling to where a page sends it (`vf-icon.dragTo`, `vf-icon-field.dragIcons`), the Finder's own Clean Up — and the zoom rects a window opens and closes with (`vf-window.show({ from })`, `hide({ to })`). The blink, the walk and the rects are suppressed under `prefers-reduced-motion: reduce` (the blink selects immediately, the walk lands immediately, the window shows or hides immediately). Even the button's rounded corners are not `border-radius` arcs: they are stepped `clip-path` silhouettes traced pixel-for-pixel from the reference sheet (`src/pixel-frame.ts`), so the 1-bit staircase renders with no antialiasing.
|
|
17
17
|
- **Selection inverts** — selected/active states are white-on-black inversion, not tinted highlights.
|
|
18
18
|
|
|
19
19
|
Modern requirements that we deliberately keep (accessibility over purity):
|
|
@@ -202,15 +202,15 @@ Full-bleed classic desktop container.
|
|
|
202
202
|
- **Behavior:** manages stacking of slotted `vf-window` children: `pointerdown` *or `focusin`* on a window brings it to front (incrementing z-index counter) and sets its `active` attribute, clearing `active` on the others — the `focusin` half is the keyboard route to activation: Tab landing anywhere in a background window (its undrawn-but-focusable widgets included) raises it. Listens via delegated pointerdown/focusin listeners + `slotchange`. Windows slotted before `vf-window` is defined are re-normalized once `customElements.whenDefined('vf-window')` settles, since the upgrade reflects each window's `active = true` default back out and upgrading a slotted node doesn't re-fire `slotchange`.
|
|
203
203
|
- **DOM order follows z-order** (bottom-most first), so sequential focus order matches the visual stack and Shift+Tab mirrors Tab exactly. The sync runs in a task after a pointer gesture ends, or after a programmatic `bringToFront` — never mid-gesture (a node move would clear the pointer capture a title-bar drag or grow-box resize holds), never between a press's release and its `click` (Chromium drops a click whose mousedown node was re-inserted first, which used to cost a control in a background window its click), and never from a focus-driven raise (moving the window focus just entered would re-order the tab sequence mid-traversal); focus surviving its own window's move is restored without re-raising that window. Non-window children (a menu bar, page content) keep their positions. `npm run verify:window-a11y`.
|
|
204
204
|
- **Floating tier:** `vf-window[variant="utility"]` children stack in a z band `1_000_000` above the document tier (one shared monotonic counter, so a palette stays above every document window), restack only among themselves, and stand outside the single-active invariant both ways: clicking a palette doesn't deactivate the active document window, and activating a document window never clears a palette's `active` — System 7 windoid behavior while the app is frontmost. The tier test reads the `variant` *attribute*, so a not-yet-upgraded element still lands right.
|
|
205
|
-
- **Menu tier:** a slotted `vf-menu-bar` — or a free-standing `vf-menu` placed on the desktop — is restated at `z-index: 2_000_000` by the desktop's own `::slotted(vf-menu-bar), ::slotted(vf-menu)` rule (an outer-tree `::slotted` declaration beats the bar's inner `:host { z-index: 1000 }`), a band above the floating tier, so a dropped menu paints over palettes and document windows alike — the Menu Manager drew menus over every window. Only the drag outline and the screen-corner mask are in front. The full stack inside the screen's isolated context: consumer tile grid (−1) < document windows (counter) < utility windows (counter + 1_000_000) < menu tier (2_000_000) < drag outline (2_000_001) < corner mask (max). A popup *inside* a window (`vf-select`'s fixed panel) is a different case: it stacks within its window's context, so a palette can still cover it — KNOWN-BUGS.md. `npm run verify:archetypes` (MENU TIER).
|
|
206
|
-
- **The drag surface:** the Finder drew a dragged icon's outline on the *screen*, clipped at its edge and phase-locked to its raster, so the screen renders the layer that outline draws on — `.drag-surface`, a full-bleed child of `.screen` after the slot, `pointer-events: none` (never a hit for the page's `elementsFromPoint`), inside the raster's clip. It is deliberately no stacking context (no z-index of its own): a stacking context is an isolated group, and the outline's `mix-blend-mode` would then composite against the empty layer instead of the windows and dither beneath; the tier goes on its children (`.drag-surface > * { z-index: 2_000_001 }`), which places the outline in `.screen`'s own context, above the menu bar and under the corner mask. An icon reaches it through the `vf-drag-surface-request` handshake (§5 vf-icon): the desktop listens on its host, and the nearest desktop fills a still-null `detail.surface`. Not the top layer: stable Safari cannot blend an element there against the page (`XOR_UNAVAILABLE`, src/cursor.ts), and a second kit entry in the top layer would need the re-promotion handshake KNOWN-BUGS.md #1 describes. `npm run verify:icon-drag` (OUTLINE).
|
|
205
|
+
- **Menu tier:** a slotted `vf-menu-bar` — or a free-standing `vf-menu` placed on the desktop — is restated at `z-index: 2_000_000` by the desktop's own `::slotted(vf-menu-bar), ::slotted(vf-menu)` rule (an outer-tree `::slotted` declaration beats the bar's inner `:host { z-index: 1000 }`), a band above the floating tier, so a dropped menu paints over palettes and document windows alike — the Menu Manager drew menus over every window. Only the drag outline, a window's opening rects and the screen-corner mask are in front. The full stack inside the screen's isolated context: consumer tile grid (−1) < document windows (counter) < utility windows (counter + 1_000_000) < menu tier (2_000_000) < drag outline and window rects (2_000_001) < corner mask (max). A popup *inside* a window (`vf-select`'s fixed panel) is a different case: it stacks within its window's context, so a palette can still cover it — KNOWN-BUGS.md. `npm run verify:archetypes` (MENU TIER).
|
|
206
|
+
- **The drag surface:** the Finder drew a dragged icon's outline on the *screen*, clipped at its edge and phase-locked to its raster, so the screen renders the layer that outline draws on — `.drag-surface`, a full-bleed child of `.screen` after the slot, `pointer-events: none` (never a hit for the page's `elementsFromPoint`), inside the raster's clip. It is deliberately no stacking context (no z-index of its own): a stacking context is an isolated group, and the outline's `mix-blend-mode` would then composite against the empty layer instead of the windows and dither beneath; the tier goes on its children (`.drag-surface > * { z-index: 2_000_001 }`), which places the outline in `.screen`'s own context, above the menu bar and under the corner mask. An icon reaches it through the `vf-drag-surface-request` handshake (§5 vf-icon): the desktop listens on its host, and the nearest desktop fills a still-null `detail.surface`. A window asks the same way and draws there with the same pen: its zoom rects when it opens from or closes to a rect (§5 vf-window, `show` and `hide`), its outline under `outline-drag`. Not the top layer: stable Safari cannot blend an element there against the page (`XOR_UNAVAILABLE`, src/cursor.ts), and a second kit entry in the top layer would need the re-promotion handshake KNOWN-BUGS.md #1 describes. `npm run verify:icon-drag` (OUTLINE).
|
|
207
207
|
- **Deactivation:** on a real System 7 machine clicking the desktop clicked the *Finder* — the frontmost application's windows lost their stripes. `clearActive()` is that gesture's handler: it clears `active` from the whole document tier, and **zero active windows is a legal state**, held until a press or `focusin` re-enters a document window or a new one is slotted (opening a window brings its application forward — a newly slotted window ends a deliberate deactivation, where a mere survivor never does: removing a *background* window while deactivated promotes nothing, while removing the holder outside a deactivation promotes the topmost survivor). The desktop never takes the decision itself — a press on its own bare dither changes nothing; desktop furniture is slotted light DOM, so only the page knows which of its children mean "the Finder". Utility windows keep their `active` through a deactivation (their dots stay drawn). Every change of holder — window to window, window to none, none to window — fires `vf-activate`, once per change; re-asserting the current holder is silent. `npm run verify:desktop-activate`.
|
|
208
208
|
- **Events:** `vf-activate` (detail `{ window: HTMLElement | null }` — the new holder, `null` on deactivation).
|
|
209
209
|
- **Parts:** `desktop`.
|
|
210
210
|
|
|
211
211
|
#### `vf-window` (`VfWindow`, vf-window.ts)
|
|
212
212
|
The desktop-window shell: the classic document window (see DragThing screenshot), parameterized down to the windoid (see the Group A recipe table).
|
|
213
|
-
- **Attributes/props:** `heading: string` (title text), `width: number` / `height: number` (**declare them both** — whole system px, so the window keeps its proportion to the chrome inside it at every density. A window is a fixed box in both axes, the way a WIND resource was: one that grows with its body is one the user can neither predict nor control via the grow box. Each missing dimension falls back to something different — width to block layout, height to the content — and the window names whichever are missing, once, in the console), `active: boolean` (default **true**; reflect), `closable: boolean` (default true), `zoomable: boolean` (default false), `movable: boolean` (default false), `resizable: boolean` (default false), `min-width` / `min-height: number` (default 80 / 54) and `max-width` / `max-height: number` (unbounded) — the grow box's sizeRect, whole system px (see Behavior), `variant?: 'utility'` (the slim windoid chrome), `scrollbars?: 'vertical' | 'horizontal' | 'both'` (edge scroll rails), `header-height: number` (`headerHeight` — the header's height in whole system px, rule included; unset, the header is as tall as its content plus the rule).
|
|
213
|
+
- **Attributes/props:** `heading: string` (title text), `width: number` / `height: number` (**declare them both** — whole system px, so the window keeps its proportion to the chrome inside it at every density. A window is a fixed box in both axes, the way a WIND resource was: one that grows with its body is one the user can neither predict nor control via the grow box. Each missing dimension falls back to something different — width to block layout, height to the content — and the window names whichever are missing, once, in the console), `active: boolean` (default **true**; reflect), `closable: boolean` (default true), `zoomable: boolean` (default false), `movable: boolean` (default false), `outline-drag: boolean` (`outlineDrag`, default false — the title-bar drag moves an outline and the release moves the window; see Behavior), `resizable: boolean` (default false), `min-width` / `min-height: number` (default 80 / 54) and `max-width` / `max-height: number` (unbounded) — the grow box's sizeRect, whole system px (see Behavior), `variant?: 'utility'` (the slim windoid chrome), `scrollbars?: 'vertical' | 'horizontal' | 'both'` (edge scroll rails), `header-height: number` (`headerHeight` — the header's height in whole system px, rule included; unset, the header is as tall as its content plus the rule).
|
|
214
214
|
- **Visual:** `vfChromeFrame` + `vfTitleBar` (§4), plus a full-size flex-column layout on the frame — title bar, header, body, status strip. `display: block`. Sets `--vf-surface: var(--vf-white, #fff)` on itself.
|
|
215
215
|
- Title bar: from `vfTitleBar` — height `var(--vf-titlebar-height, 18px)`, white bg, bottom `1px solid black`, contains `.vf-stripes` layer (only when `active`). `touch-action: none` only when `[movable]`.
|
|
216
216
|
- Title: centered, bold, on a white patch (`padding: 0 6px`; cap band on interior rows 4..12, 7px of white between ink and stripes — see §4 `vfTitleBar` for the traced geometry and the lattice hold) above the stripes, with `--vf-title-inset: 60px` of clearance so it ellipsizes before reaching the widgets. Inactive: no stripes, widgets undrawn (transparent ink — they keep their tab stops; see A11y below), the grow box's nested squares hidden, and every managed scroll rail inside the window blanked (see "always-a-rail" §5 vf-scroll-area) — but the title text stays black (System 7 never grayed the window title).
|
|
@@ -223,8 +223,8 @@ The desktop-window shell: the classic document window (see DragThing screenshot)
|
|
|
223
223
|
- Edge scroll rails (if `scrollbars`): the body slot renders inside a shadow `vf-scroll-area` (its `axis` = the attribute's value, `label` = the heading, `viewport` part re-exported) carrying the TeachText composition internally — `calc(100% + 2px·scale)` with `margin: -1px·scale`, one system px under the frame on every side, so the area's own frame border repaints the window's border lines and a `resizable` window's grow box (z-index 1) lands exactly over the rail-corner cell. the area's viewport carries no inset, so content and the (0,0) of placed children sit at the content region's corner; a `resizable` window sets the area's `corner`, so a single-axis rail stops at the corner cell too — unless the status strip is populated, when the grow box sits in the strip and the rail runs edge to edge onto the strip's rule. Same caveats as the slotted composition (see vf-scroll-area §5).
|
|
224
224
|
- Utility variant (`variant="utility"`): the slim windoid bar traced from `Windows/utility-window.png` — `--vf-titlebar-height-utility` (12px = 11px interior + 1px rule), the `vfDots` dither instead of stripes (flush to the side borders — see §4 vfDots), 7×7 widgets (`top: 2px`; close `left: 7px`, zoom `right: 8px` — the art is asymmetric by that pixel) with a 2px patch ring where the striped bar's is 1px (`--vf-widget-ring`, internal geometry: the windoid sheet clears two px of dither beside its widgets), and the nested zoom square shrunk so its edges land at sprite col/row 3. No title patch: the display face's 16px line box can't sit in an 11px interior, so `.vf-title` is `display: none` under the variant (a retheming consumer can re-show it via `::part(title)`) and the heading names the widgets. A pressed windoid widget inverts whole — black interior under a white (invisible) borderline — rather than flashing the big bar's 9×9 sunburst, which can't land on a 5×5 interior.
|
|
225
225
|
- **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`.
|
|
226
|
-
- **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`.
|
|
227
|
-
- **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.
|
|
226
|
+
- **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. With `outline-drag` the drag moves an outline instead: the press draws a one-pixel ring around the window's border box (the frame, shadow excluded) on the desktop's drag surface, with the drag outline's pen (`xorPenCanvas`, dotted at the screen phase — black over the dither), and each step moves it to the landing the live drag would write (`PlacementController.resolve()`: the grabbable-strip clamp, snapped), so the outline stops where the window can go; the release writes the last landing once (one `vf-placement-change`) and a release with no step writes nothing, and Escape — a gesture-scoped document capture listener armed at the press, the key stopped there — or a `pointercancel` takes the outline down and writes nothing. With no `vf-desktop` on its path the drag moves the window live. `npm run verify:window` (DRAG OUTLINE). 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`.
|
|
227
|
+
- **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. `show(options?)` → `Promise<boolean>`: clears `hidden`, and with `options.from` — a box in viewport CSS px (`{ left, top, right, bottom }`; a `DOMRect` fits, a zero-size box is a point), such as `vf-icon.cellRect()` — opens the window from it with the zoom rects. The window knows nothing about icons: `from` is any box. 14 rects, rect k at the fraction 0.7^(14−k) of the way from the box to the window's border box — each edge `from + trunc((frame − from) · t)`, so they bunch at the box and the last stops at 0.7 — four on screen at a time, 17 ms per step, then four steps draining; the window draws after the eighteenth (`WINDOW_RECT_*`, `windowRectFraction`, src/motion.ts). Each rect is a canvas of its own on the desktop's drag surface (the `vf-drag-surface-request` handshake), framed once with the drag outline's pen (`xorPenCanvas`, `paintSelectionRect`, src/open-art.ts) and dotted at the screen phase (`penPhase` — black lines over the dither), and a Web Animation (`element.animate()`) shows it for its four steps, every one padded with `endDelay` to the run's length so all of them finish when the window draws. Where two rects share a pixel the later one inverts it back: each canvas blends over those beneath it. Same-task writes to `left`/`top`/`width`/`height`, before or after the call, land first, so the rects end on the frame the page set. Until the rects drain the window matches `:state(opening)` and has `opacity: 0` — laid out, focusable and a hit target, so a page can focus into it at once and a press lands on it — never `visibility`, which would drop focus and pass the press through. A press anywhere or Escape (consumed in the capture phase) finishes the rects' animations (`finish()`), as they finish the walk; a second `show()` finishes the first, as it finishes a `hide()`'s; a removal, or a `hide()` before the window draws, cancels them (`cancel()`) and resolves `false`, while a move — `vf-desktop` re-inserting a raised window — is not a removal (the disconnect checks `isConnected` a microtask later, and the reconnect re-attaches the finishers). Resolves once the window draws: `true` when the rects ran, `false` when it showed at once — no `from`, `prefers-reduced-motion`, no `vf-desktop` on its path, nothing of the rects on the screen, or not connected. Raises and activates nothing; a window already on screen goes blank for the run, so a page brings an open window forward with `bringToFront` instead. `npm run verify:window` (OPEN). `hide(options?)` → `Promise<boolean>`: sets `hidden`, and with `options.to` — a box as for `show` — closes the window to it: the window's border box is read at the call, the window hidden, and the same 14 rects run the other way, rect k shown from step 13 − k, so the trail starts at the rect nearest the frame, closes in on the box and drains into it, eighteen steps in all. A press anywhere or Escape finishes them, a `show()` finishes them and shows the window, and a second `hide()` finishes the first. Removing the window leaves them running — they are the desktop's, and the window is already gone — so a page can remove a closed window straight after the call. A `show()` still opening is taken down (cancelled, so it resolves `false`) and the window hides at once. Resolves once the rects are done: `true` when they ran, `false` when the window hid at once — no `to`, `prefers-reduced-motion`, no `vf-desktop` on its path, nothing of the rects on the screen, not connected, already hidden, or still opening. `npm run verify:window` (CLOSE).
|
|
228
228
|
- **Slots:** default (body content), `header` (the header strip between the title bar and the body), `status` (the bottom status strip — see Visual).
|
|
229
229
|
- **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.
|
|
230
230
|
- **Events:** `vf-close`, `vf-zoom` (detail `{}`), `vf-resize` (detail `{ width, height, commit }`).
|
|
@@ -479,8 +479,8 @@ The Finder icon — pixel art in a reserved cell with its name on a plate below,
|
|
|
479
479
|
- **Attributes/props:** `label`: string (the name; empty draws no plate — that is the "no label" setting, rather than a second attribute that could disagree with it). `size`: `large` (default, 32×32) | `small` (16×16) — picks the slot that paints *and* the cell it paints in. `selectable`, `selected`, `movable`, `editable`: boolean. `color`: boolean — declares the art a **color icon**, so selection darkens instead of inverting (see Visual). `open`: boolean — the icon's window is on screen, and the art paints as the derived open ghost (below). `target`: boolean (reflected) — the destination highlight under a drag: the selected treatment of the *art* alone (`invert(1)`, or `brightness(0.5)` under `color`) with none of `selected`'s semantics — no `vf-select`, no outside listener, no `aria-selected`, the plate untouched; the page sets it on the folder a `vf-drag` reports the pointer over. `width`: number (system px, **even**; the cell/grid pitch, which a longer name overflows rather than being bound by). `maxlength`: number (31).
|
|
480
480
|
- **Visual:** `display: inline-block`. A column: the art cell (`--_cell` system px square, `overflow: hidden`, art centered), then, directly under it (`--vf-icon-gap`, 0 by default), the name plate — body face on a `--vf-icon-label-height` (12px) line box, the Finder's own plate height: the face's 16px em centers in it, 3px above the ascenders and descenders on the bottom edge, exactly as the Finder drew it — 1px horizontal padding, **opaque `--vf-white`** in its own right rather than `--vf-surface`: on the desktop dither the name reads because it sits on a plate, and `--vf-surface` is unset out there (`Example screen (1-bit).png`, the "Macintosh HD" icon). Selected: the plate takes the `--vf-highlight` pair and the art takes `filter: invert(1)` — a System 7 icon is ink and opaque white on a transparent surround, i.e. precisely an image plus its mask, so inverting flips the two and leaves the surround alone. That is the complete classic selected appearance for 1-bit art. Color art would invert into a photographic negative, which System 7 never showed: declare `color` and selection **darkens** instead — Icon Utilities' ttSelected transform, every color blended halfway toward black (`filter: brightness(0.5)`), whites going gray, the surround still untouched. The declaration is the consumer's, since the component cannot tell color art from a mask by looking at a slotted image; it covers whatever the cell shows (the open ghost included, so a color icon has one selected treatment open or closed), and the plate inverts either way — the selected name was inverted whatever the art.
|
|
481
481
|
- **The open ghost is derived, not shipped** (`open`; `open-art.ts`). The art redraws as the Finder's open-window ghost — outline held in solid black, interior re-filled with the scrollbar trough's 4×2 dot lattice (a dot at (0,0) and (2,1), 25%) on opaque white, surround untouched — derived from the slotted art itself, by compositing alone: a silhouette via `source-in`; a 1px 4-neighborhood erosion via four `destination-in` draws of the silhouette shifted one pixel each way (off-canvas composites as transparent, so the raster's own edge erodes too and an interior hole rings itself); the lattice via `createPattern` anchored at the art's top-left, where 32 and 16 being whole multiples of the tile keeps a field of icons in one phase; then the fill composed over the un-eroded silhouette, so the ring the erosion removed reads as the outline. Never `getImageData` — a canvas that drew a cross-origin image is tainted, which forbids *reading* pixels but not drawing or displaying them, so the ghost works for CORS-less sources too — and never `ctx.filter` (Safari's shaky corner; compositing operators are universal). Keeping the interior *opaque white* is what lets a selected open icon invert under the same `filter: invert(1)` with no second treatment. The slot stays in the tree while the ghost paints, hidden via a class gated on a successful derivation — art the pipeline cannot draw (nothing slotted, a failed load, an inline `<svg>`) keeps rendering as itself. The ghost re-derives on slotchange, the art's `load`/`error`, and `open`/`size` changes, keyed by `currentSrc` so a refresh that changed nothing is a string compare; the source's `alt` is carried onto the ghost (`role="img"` + `aria-label`) so a graphic that names its icon keeps doing so while its slot is hidden. The ghost displays on `vf-img`'s terms: natural raster size in system px, `image-rendering: pixelated`, one image pixel per system pixel.
|
|
482
|
-
- **Behavior:** clicking selects, Shift/⌘ toggles, and a press outside every icon clears — a `DocumentListenersController` capture-phase listener attached only while selectable *and* selected, which is what makes single-selection work with no container owning the set. Double-click fires `vf-open`, and its keyboard route is ⌘O / ⌘↓ (Ctrl off the Mac) — the System 7 Open shortcuts, added because a double-click is a pointer-only gesture (§1). Return is deliberately not one: the Finder's Return renamed, never opened, so it starts the edit on an editable icon and does nothing on a non-editable one. A double-click opens the icon **wherever it lands, the name included** — the handler is on the frame, and the name is as much the icon as the art is. `editable`: a press on the plate of an ALREADY-selected icon opens a rename field overlaying it (the press that does the selecting never does); the whole name starts selected, Return commits, Escape reverts, and the hidden plate keeps rendering the draft so the box widens as you type. **The two pointer gestures on a name are the same press**, and only the second one tells them apart, so the rename *waits*: the press arms a field that opens `RENAME_DELAY_MS` (800ms, `src/motion.ts`) later, and a press landing inside that window — wherever in the icon it falls — calls it off, leaving the double-click to open with no rename flashing up behind it. The window is long deliberately: no API reports what the browser counts as a double-click (it follows a user-movable platform setting), and reading a lone click as a pair costs only a wait, while reading a pair as a lone click renames when the user meant to open. A press that travels into a drag, a press outside, and any key call it off too — so a name drags like the art does, and Return opens the field at once, having no second half to wait for. `movable` — **not `draggable`**, which is a global HTML attribute and an `HTMLElement` accessor (the same trap as `align`, §5 Group G) — drags via `DragController` on the `vf-window` delegate shape (the frame is the handle and sets `touch-action: none` while movable, as the title bars do), plus arrow-key nudging (1 system px, 8 with Shift) because a pointer-only gesture is the kind of gap the kit closes (§1). Focus is the dashed rule below the plate, gated on `FocusRuleController` rather than `:focus-visible`: the host focuses itself so the press-drag can own the pointer.
|
|
483
|
-
- **The drag reports, the page files.** The kit ships no filing semantics — folders are the page's own catalog — so a drag is a stream of events on the icon and a cancelable commit, all bubbling and composed: `vf-drag-start` `{ left, top }` on the first lattice step
|
|
482
|
+
- **Behavior:** clicking selects, Shift/⌘ toggles, and a press outside every icon clears — a `DocumentListenersController` capture-phase listener attached only while selectable *and* selected, which is what makes single-selection work with no container owning the set. Double-click fires `vf-open`, and its keyboard route is ⌘O / ⌘↓ (Ctrl off the Mac) — the System 7 Open shortcuts, added because a double-click is a pointer-only gesture (§1). **The instrument never changes what the icon means**, so a finger or a pen opens it too, on a *tap pair* the icon classifies from its own pointer events rather than from `dblclick`: two taps on the icon, the second pressed within `TAP_PAIR_MS` (500) of the first and no further than `TAP_PAIR_SLOP_PX` (24 CSS px) from it, `vf-open` firing on the second release. A platform reserves the double-tap for its zoom and may deliver no `dblclick` for one, and a movable icon's press is `preventDefault()`ed for the drag, which can suppress it as well; the pair depends on neither. A tap is a press released where it landed — one that became a drag, or that the platform cancelled, is not half of a pair — and a pair does not chain. The mouse keeps the platform's `dblclick`, which follows the user's own double-click setting; each press takes exactly one of the two routes, so a platform that also delivers a `dblclick` for a double-tap opens the icon once. The frame sets `touch-action: manipulation`, which takes the double-tap zoom off the icon and leaves panning and pinching to the page. Both are additions in the kit's idiom — System 7 had the mouse alone. Return is deliberately not one: the Finder's Return renamed, never opened, so it starts the edit on an editable icon and does nothing on a non-editable one. A double-click opens the icon **wherever it lands, the name included** — the handler is on the frame, and the name is as much the icon as the art is. `editable`: a press on the plate of an ALREADY-selected icon opens a rename field overlaying it (the press that does the selecting never does); the whole name starts selected, Return commits, Escape reverts, and the hidden plate keeps rendering the draft so the box widens as you type. **The two pointer gestures on a name are the same press**, and only the second one tells them apart, so the rename *waits*: the press arms a field that opens `RENAME_DELAY_MS` (800ms, `src/motion.ts`) later, and a press landing inside that window — wherever in the icon it falls — calls it off, leaving the double-click to open with no rename flashing up behind it. The window is long deliberately: no API reports what the browser counts as a double-click (it follows a user-movable platform setting), and reading a lone click as a pair costs only a wait, while reading a pair as a lone click renames when the user meant to open. A press that travels into a drag, a press outside, and any key call it off too — so a name drags like the art does, and Return opens the field at once, having no second half to wait for. `movable` — **not `draggable`**, which is a global HTML attribute and an `HTMLElement` accessor (the same trap as `align`, §5 Group G) — drags via `DragController` on the `vf-window` delegate shape (the frame is the handle and sets `touch-action: none` while movable, as the title bars do), plus arrow-key nudging (1 system px, 8 with Shift) because a pointer-only gesture is the kind of gap the kit closes (§1). Focus is the dashed rule below the plate, gated on `FocusRuleController` rather than `:focus-visible`: the host focuses itself so the press-drag can own the pointer.
|
|
483
|
+
- **The drag reports, the page files.** The kit ships no filing semantics — folders are the page's own catalog — so a drag is a stream of events on the icon and a cancelable commit, all bubbling and composed: `vf-drag-start` `{ left, top }` on the first lattice step past the **drag slop** — `DRAG_SLOP_PX` (4 CSS px) of travel from the press for the mouse, `DRAG_SLOP_COARSE_PX` (10) for a finger or pen (`dragSlop(event)`, `src/motion.ts`). Inside it the press is a click or a tap: the icon stays on its spot, a rename the press armed stays armed, and no drag event fires, so a click never fires a drop and a tap never nudges an icon off its cell. The delta is measured from the press all the same, so the outline begins under the pointer. CSS px rather than system px, because the slop measures the hand and not the art; `vf-drag` `{ clientX, clientY, left, top, x, y }` on every step that changed the snapped proposal — the pointer in viewport CSS px for the page's hit test, the proposal in the icon's own container in whole system px on the lattice, and the frame's box at the press translated by the delta in viewport CSS px, so a drop that lands in another container converts with that container's `placementAt` and lands where it was let go, the press offset already inside it; `vf-drop`, the same shape at the release, **cancelable**, whose default action is the move — `PlacementController.moveTo` with the proposal, clamped whole by `#keepWhole` against the box measured at the press; in a box that *scrolls* (a `vf-scroll-area`'s plane, which a window body is under `scrollbars`) only the origin holds, since the rails reach anything placed past it and the Finder put an icon below a window's fold and scrolled to it — `#anchorScrolls`, a flat-tree walk from the icon toward its positioning parent through the slots it is assigned to, true when a scroll area is met before the parent — and whose `preventDefault()` writes nothing, the page re-parenting or placing the icon itself; `vf-drag-cancel` `{}` on Escape or a `pointercancel`, nothing written. `left`/`top` in the detail are the proposal *unclamped* (the origin plus the lattice-snapped delta); only the default action clamps, so a page that cancels gets the raw gesture. The drop is dispatched with the outline gone, the drag state cleared and the pointer capture released, so re-parenting inside the handler is safe (Chromium drops the `click` of a press whose node was re-inserted at `pointerup`; the icon listens for no click and times its double-click on presses).
|
|
484
484
|
- **The drag is an outline; the icon stays put** (`deriveDragOutline` / `dotOutline`, `open-art.ts`). The Finder dragged an icon as a dotted outline — the mask's boundary and the name's rectangle — drawn over everything with the XOR pen, the icon itself unmoved until the drop. Derived from the slotted art like the open ghost: the silhouette from the art's alpha (drawn at the box the art paints in, so magnified art outlines at its magnified size), the silhouette clipped to the cell as the cell clips the art, the one-pixel erosion by eight `destination-in` draws on a layer the outline's size (the layer is transparent around the art, so its edge erodes as the off-canvas edge did) — the 3×3 box, the region inset `FrameRgn` painted a frame from, rather than the ghost's four-neighbor plus: a 45° edge comes out two pixels wide with both parities of the checker, where a one-pixel diagonal is all one parity and the pen would keep all of it or none, and over the dither "none" is the dither's own ink diagonal — the plate's rectangle filled solid into the same silhouette before the erosion (the Finder's drag region was the mask united with the name's rect, framed once — where the two abut, the outline wraps around both and no edge runs between them; apart, each is ringed on its own), the ring the erosion removes via `destination-out`; compositing only, so cross-origin art derives too. Art the pipeline cannot draw falls back to the cell's rectangle — never a blank. One canvas the size of the frame widened to the plate — a name wider than a declared `width` overhangs the frame on both sides — at one image px per system px; the ring is derived once per drag, and each step re-dots it against a 2×2 checker (`destination-in`) at the pen's phase for where it lands on the *screen* (`penPhase`): ink where `x + y` is odd in screen coordinates, the diagonal the desktop dither (`gray-50`, ink at even) leaves white, so the dots fall between the dither's ink wherever the outline lands, as QuickDraw's screen-anchored pattern pen did — over a white body a dotted black line, over the dither a black line, each dot flipping a white pixel with the dither's own ink filling in between. Painted with the cursor's XOR recipe, `filter: invert(1)` under `mix-blend-mode: difference`, `image-rendering: pixelated`, `pointer-events: none`, sized and placed as live `sysLength()`s so a scale change mid-gesture cannot smear it; its origin is that box's whole-system-px offset from the surface plus the delta. **The surface is the desktop's, not the top layer:** the icon dispatches `vf-drag-surface-request` (bubbles, `composed: false`, `detail.surface` null) on the first step, and the nearest `vf-desktop` on its light-DOM path fills it with its drag surface (§5 vf-desktop); a non-composed event from a slotted icon still travels through the slots it is assigned to, so the handshake works from inside a window body. Without a desktop the canvas is appended to the icon's own `.frame` (the vf-snap box, `position: relative`), translated by the delta alone and clipped by whatever clips the icon — the honest fallback, with the same gesture and events. Not `position: fixed` inside the icon: a `vf-window` is a stacking context, so a fixed outline inside it would paint at the window's z, under every other window (KNOWN-BUGS.md #1 in another costume). One drag, no opt-in: the Finder never moved the icon itself, the drop lands where it always did, and the kit ships no taste knobs. Escape is heard by a document `keydown` listener scoped to the gesture (`DocumentListenersController`, capture phase, the key stopped there so an enclosing `vf-dialog` never reads it as a dismissal) rather than the icon's own handler, because a `movable`-only icon never takes focus — an addition in the kit's idiom (§1), not a claim about the Finder. `DragController` gained the two trailing parameters this needs (`onDrag(x, y, event)`, `onDragEnd(event, cancelled)`) and a `cancel()`; `vf-window` and `vf-dialog` ignore them. A selected icon re-parented at the drop keeps its outside-press listener: it re-attaches on connect, since the controller detaches on disconnect and `updated()` does not re-run for an unchanged property. `npm run verify:icon-drag`.
|
|
485
485
|
- **The selection travels.** A drag that begins on a *selected* icon carries every other selected, movable icon of its field (`closest('vf-icon-field, [role="listbox"]')`, document order) — the Finder outlined the whole selection under a drag on any member. Each follower is joined through `vf-icon.followDrag(surface)` (internal to the group drag): it seeds its own origin, builds its own outline on the surface the leader's handshake found (or in its own frame), and hands back a handle `{ icon, origin, size, move(dx, dy), drop(dx, dy), end() }`. The leader moves every outline by its lattice delta on each step; every event's detail carries the set as `icons`, the leader first (`VfIconDragDetail`). The drop's default action clamps the delta **once for the whole group** — per axis, the tightest of every member's room to its edges, so the arrangement is kept and the group stops when its outermost icon meets the container (a group no edge can hold is held off the near edge only, the single-icon rule; `PlacementController.bounds` is the box, measured at the press) — then writes each member's origin plus that delta through its own `moveTo`. A cancel, or a cancelled drop, takes every outline down and writes nothing for anyone. **The press keeps the selection:** an icon's outside-press listener leaves the selection alone when a plain press lands on an icon already selected in the same field (document capture runs before the pressed icon's own handler, so `selected` reads as the press found it); a click on such an icon, released with no drag, collapses the selection to it on the `pointerup` — so a click still single-selects, and the same press could have dragged them all. A press on an unselected icon makes it the selection first, so it drags alone; a Shift press that deselects the pressed icon drags it alone too; a selected icon in another field, or one that cannot move, stays. `npm run verify:icon-drag` (GROUP).
|
|
486
486
|
- **The page drags it too** (`moveTo`, `dragTo`; `vf-icon-field.dragIcons` below). The Finder's Clean Up walked a container's icons one at a time, each icon's outline travelling from where it sat to its cell and the icon landing when the outline arrived — the drag's vocabulary, with the Finder for a hand. `dragTo(left, top)` → `Promise<boolean>`: the landing is resolved first (`PlacementController.resolve` — the pair `moveTo` would write, clamped whole and snapped, without writing it — so the outline goes where the icon will land, not where a pair past the edge pointed), the outline built and placed as a drag's is (`#buildOutline`, on the desktop's surface through the handshake or in the frame), then stepped from the origin to the landing by `runOutlineTravel` (`src/motion.ts`): whole steps of `WALK_STEP_PX` (16) every `WALK_STEP_MS` (25), each re-dotted at the screen's phase through `#placeOutlineAt`, the last step exactly the landing, and one interval later the outline down and the icon written through `PlacementController.moveTo` — one `vf-placement-change`, no drag event (the page is the mover and already knows). Constant speed rather than a fixed frame count: most of a Clean Up's moves are short, and a fixed count would crawl a one-pixel correction across as many frames as a trip across the screen. A timer chain, not `requestAnimationFrame`, so a 120 Hz display does not double the speed. Resolves to whether the outline travelled — false when the icon landed at once: already at its landing, `prefers-reduced-motion` (the blink's posture), nothing to draw with, mid-rename or under the hand's own press, or not in the DOM (nothing written then). Not gated on `movable`, which says the hand may drag; the page may always place its icon. **Finished, never cancelled:** a document `pointerdown` or Escape (capture, scoped to the travel, the key stopped so a dialog never dismisses) lands the icon at once — the user wants the machine back, and the move was the page's; so does a second `dragTo`, a `moveTo`, or disconnecting (outline down, nothing written). `moveTo(left, top)` is the landing alone — the drop's write: the container measured now (`seed()`), clamped whole, snapped, announced once; a `left`/`top` write stays the authored, unclamped pair (the `selected` / `setSelected()` line). The three cadence numbers are set by eye against the Finder and asserted nowhere. `npm run verify:icon-drag` (WALK).
|
|
@@ -490,7 +490,7 @@ The Finder icon — pixel art in a reserved cell with its name on a plate below,
|
|
|
490
490
|
- **Every centered offset is whole, by parity.** The frame centers two things over one axis, and a centered child sits at `(box − child) / 2` — whole exactly when box and child share a parity. Half a system pixel is what fringes 1-bit art: glyph stems smear across two device columns and go gray while the plate behind them stays sharp, since backgrounds are pixel-snapped by the compositor and glyphs are not (hence a crisp plate under a gray name). So the component makes the parities agree rather than correcting afterwards. The cell is 32 or 16; `#measurePlate()` measures the text with a `Range` over the plate's own contents — the plate already carries the width this last computed, so reading the element back would only return it — and sizes the plate (an `inline-block`) to that plus its 1px padding, rounded UP to a whole **even** number of system px, so it never clips the run it was measured from. The measurement is in system px and therefore scale-invariant, which is why nothing re-runs on a density change; it re-runs on `label`, `_draft`, `_editing`, and once on `document.fonts.ready`, since a name measured against the fallback face is a different width. The text is NOT centered inside its own plate (`text-align: left` against the block's `center`) — that would re-introduce the half pixel one level down; it starts at the 1px padding edge, so its x is the plate's plus one whole pixel, and the rounding slack lands after it as a hairline of extra plate. The rename field puts its run exactly where the plate's is, so the name does not shift when editing starts; its 1px border and 1px white well are a wrapper around the input rather than the input's own border and padding, because the selection behind a highlighted name paints at the face's full 16px em and is clipped only at the input's own edge — split, the input stands the plate's 12px line box (clipping the selection to it) and the well stays white, for an edit box 16 system px tall in all, not the em plus trim. Staying on one line is part of the same argument: one run, one measured width, one parity. Nothing is snapped and nothing leans on the rasterizer — the kit's normal antialiasing stays on, and `verify:icon` asserts both the geometry and zero gray pixels across a field of names at dpr 1/2/3, declared width and auto, with a control that forces an odd plate and requires the fringe to return. The one thing the component cannot round is a `width` the consumer chose, so that must be even.
|
|
491
491
|
- **The 31-char cap** is `maxlength` on the rename field, and exceeding it fires `vf-name-too-long` rather than dropping characters silently. Detected in `beforeinput`, not `input`: with `maxlength` set, a blocked keystroke fires `beforeinput` and then *no* `input` at all, while an overshooting paste reaches `input` already trimmed — only the "about to change" event still knows what was attempted. `input` carries a trimming backstop for any route that skips it. The cap bounds typing only; a `label` set from consumer code is displayed as given and fires nothing (the name is the consumer's data).
|
|
492
492
|
- **An empty name is refused, not applied.** A file has to be called something, and System 7 would not commit a nameless one: a rename committed empty — or as nothing but spaces, which would read as blank either way — drops the edit and restores the previous name, firing `vf-name-rejected` and deliberately no `vf-change`. An empty `label` remains a valid *starting* state (a freshly made icon has no name yet): it draws no plate but stays selectable, focusable and renameable. The caption element is therefore rendered whenever there is a name **or** an edit is open — without that an unnamed icon has nowhere to put the field and could never be named at all. The plate hugs its text in BOTH states, which is what keeps a commit from moving the name: it is the same box either side, so the glyphs do not shift. The single exception is an edit with no text in it at all, which reserves the cell width so the field is usable rather than a 2px sliver, and goes back to hugging on the first character. That floor is the cell, which is even, so the parity rule holds.
|
|
493
|
-
- **Methods:** `startEditing()`, `commitEditing()`, `cancelEditing()` — the rename field's three routes. `setSelected(next)` → select or deselect as a press would, `vf-select` included (a `selected` write from code is silent, the way a value set fires no `vf-change`); the route a container's gesture takes — `vf-icon-field`'s rubber band. `touches(box)` → whether a viewport box (CSS px `left/top/right/bottom`) reaches the icon's art cell or its name plate — the Finder's selection-rectangle test, in which the empty cell around narrower art and the frame's margin beside the plate count for nothing; the rubber band reads it. `moveTo(left, top)` → place as a drop would: clamped whole in the container, snapped, one `vf-placement-change`. `dragTo(left, top)` → the outline travels and the icon lands, a promise — the walk, above.
|
|
493
|
+
- **Methods:** `startEditing()`, `commitEditing()`, `cancelEditing()` — the rename field's three routes. `setSelected(next)` → select or deselect as a press would, `vf-select` included (a `selected` write from code is silent, the way a value set fires no `vf-change`); the route a container's gesture takes — `vf-icon-field`'s rubber band. `touches(box)` → whether a viewport box (CSS px `left/top/right/bottom`) reaches the icon's art cell or its name plate — the Finder's selection-rectangle test, in which the empty cell around narrower art and the frame's margin beside the plate count for nothing; the rubber band reads it. `cellRect()` → the reserved art cell's box in viewport CSS px — 32×32 system px, 16×16 under `size="small"`, at the top of the frame — the same box whatever the art, the name, `width`, `open` or `selected` (the slotted art is no stand-in: `open` hides the slot, so it measures nothing); `null` while the icon isn't rendered — out of the document, or under `display: none` (`checkVisibility()`); the box a page hands a window opening from the icon or closing to it (`vf-window.show({ from })`, `hide({ to })`). `moveTo(left, top)` → place as a drop would: clamped whole in the container, snapped, one `vf-placement-change`. `dragTo(left, top)` → the outline travels and the icon lands, a promise — the walk, above.
|
|
494
494
|
- **Slots:** `large` (32×32 art, normally a `vf-img`), `small` (16×16). **Parts:** `frame`, `icon`, `label`, `plate`, `input`.
|
|
495
495
|
- **Events:** `vf-select` `{ selected }`, `vf-change` `{ label, previous }`, `vf-open` `{}`, `vf-name-too-long` `{ attempted, accepted, limit }`, `vf-name-rejected` `{ attempted, kept, reason }`, `vf-drag-start` `{ left, top, icons }`, `vf-drag` and `vf-drop` `{ clientX, clientY, left, top, x, y, icons }` (`vf-drop` cancelable), `vf-drag-cancel` `{}`.
|
|
496
496
|
|