vintage-frames 0.7.0 → 0.7.2
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 +29 -4
- package/dist/components/vf-icon-field.d.ts +5 -2
- package/dist/components/vf-icon-field.js +41 -27
- package/dist/components/vf-icon.d.ts +5 -3
- package/dist/components/vf-icon.js +28 -28
- package/dist/components/vf-window.js +2 -2
- package/dist/open-art.d.ts +14 -4
- package/dist/open-art.js +7 -4
- package/docs/SPEC.md +2 -2
- package/editor/vscode.html-custom-data.json +2 -2
- package/editor/web-types.json +3 -3
- package/package.json +1 -1
package/custom-elements.json
CHANGED
|
@@ -4449,7 +4449,7 @@
|
|
|
4449
4449
|
"declarations": [
|
|
4450
4450
|
{
|
|
4451
4451
|
"kind": "class",
|
|
4452
|
-
"description": "`<vf-icon-field>` — the container a field of `vf-icon`s sits in: a desktop's\nicons, a folder window's contents.\n\nEvery other set in the kit has its container — `vf-list` owns\n`vf-list-item`, `vf-radio-group` owns `vf-radio`, `vf-menu` owns\n`vf-menu-item`, `vf-select` owns `vf-option` — and `vf-icon` was the one\nitem whose container the consumer wrote by hand: a bare\n`<div role=\"listbox\" aria-multiselectable=\"true\">`, because an `option` is\nonly valid inside a `listbox` that owns it, and every kit container is a\npositioning anchor by design, so nothing in the kit was neutral enough to\nstand in. A consumer who forgot the role got icons that silently degraded\nto pictures. This is that container, as kit furniture:\n\n```html\n<vf-desktop width=\"512\" height=\"342\">\n <vf-icon-field label=\"Desktop\" fill-width fill-height>\n <vf-icon label=\"Macintosh HD\" width=\"64\" selectable movable editable left=\"16\" top=\"24\">…</vf-icon>\n <vf-icon label=\"Trash\" width=\"64\" selectable movable editable left=\"16\" top=\"280\">…</vf-icon>\n </vf-icon-field>\n <vf-window heading=\"Documents\" width=\"320\" height=\"201\" scrollbars=\"both\" top=\"40\" left=\"120\">\n <vf-icon-field label=\"Documents\">\n <vf-icon label=\"Read Me\" width=\"64\" selectable movable editable left=\"16\" top=\"16\">…</vf-icon>\n </vf-icon-field>\n </vf-window>\n</vf-desktop>\n```\n\n### Semantics through internals\n\n`role=\"listbox\"` and `aria-multiselectable=\"true\"` always — the Finder was\nalways multi-select, so there is no attribute for it — and `label` as the\naccessible name, the way `vf-scroll-area` takes one. Internals values are\ndefaults (SPEC §2): a consumer's own `role`/`aria-*` on the tag still wins.\nA `selectable` icon inside becomes a real `option` carrying `aria-selected`;\n`vf-icon` matches this tag directly, since a role written through internals\nnever lands as an attribute for `closest()` to find.\n\n### Layout-neutral, exactly what the div was\n\nAn in-flow block with no size, no inset and no position of its own,\npainting nothing — the `vf-stack` posture. A field whose icons are all\nplaced is a zero-height block, so the icons keep anchoring to the desktop's\nraster or the window's plane, stored coordinates mean what they meant, and\na press on the bare dither still reaches the desktop. It takes `top`/`left`\nand `width`/`height` like the other declared-box containers, so a page that\nwants the field to be a box places and sizes it, at which point it becomes\nthe anchor — the placement working, as documented for everything.\n`fill-width`/`fill-height` are the other way to give it a box, read about\nthe host the way `vf-stack` and `vf-container` read them: a filled field\nstays in flow and static, so its icons keep their anchor, and it has a\nsurface to press — which is what the rubber band needs.\n\n### The rubber band\n\nA press on the field's own background — not on an icon — dragged across\nthe field draws the Finder's dotted selection rectangle from the press to\nthe pointer, and the selection follows it live by the Finder's one rule:\n**an icon is selected exactly when it was selected at the press XOR the\nrectangle touches it.** The press fixes that anchor: a plain press has\nalready cleared everything (the way any press outside an icon does), so\nthe rectangle simply selects what it touches and releases what it leaves;\nwith Shift or ⌘ held the selection survives the press and the rectangle\n*toggles* against it — an icon already selected deselects while the\nrectangle covers it and comes back when it leaves, one that was not\nselects. Each change reports `vf-select` as a press would. \"Touches\"\nmeans the icon's art cell or its name plate (VfIcon.touches), not\nthe empty cell beside them, so a rectangle through the gap between two\nicons selects neither. The release keeps the result; Escape or a\n`pointercancel` cancels, the rectangle vanishes and the selection goes\nback to the anchor. A press that never leaves its system pixel draws\nnothing.\n\nThe rectangle is drawn with the drag outline's pen (* paintSelectionRect: the dotted one-pixel outline, `filter: invert(1)`\nunder `mix-blend-mode: difference`) at the field's own level, which is\nwhere the Finder drew its marquee — on the desktop under the windows, in a\nwindow body over that window's content and under any window above it —\npositioned against the viewport from the field's own shadow, since a\nlayout-neutral field has no box of its own to anchor to. It reaches no\nfurther than the field, inside every clip above the field (a window body,\na scroll viewport, the raster's edge), so it never paints outside the\nregion the icons are visible in. Its dots
|
|
4452
|
+
"description": "`<vf-icon-field>` — the container a field of `vf-icon`s sits in: a desktop's\nicons, a folder window's contents.\n\nEvery other set in the kit has its container — `vf-list` owns\n`vf-list-item`, `vf-radio-group` owns `vf-radio`, `vf-menu` owns\n`vf-menu-item`, `vf-select` owns `vf-option` — and `vf-icon` was the one\nitem whose container the consumer wrote by hand: a bare\n`<div role=\"listbox\" aria-multiselectable=\"true\">`, because an `option` is\nonly valid inside a `listbox` that owns it, and every kit container is a\npositioning anchor by design, so nothing in the kit was neutral enough to\nstand in. A consumer who forgot the role got icons that silently degraded\nto pictures. This is that container, as kit furniture:\n\n```html\n<vf-desktop width=\"512\" height=\"342\">\n <vf-icon-field label=\"Desktop\" fill-width fill-height>\n <vf-icon label=\"Macintosh HD\" width=\"64\" selectable movable editable left=\"16\" top=\"24\">…</vf-icon>\n <vf-icon label=\"Trash\" width=\"64\" selectable movable editable left=\"16\" top=\"280\">…</vf-icon>\n </vf-icon-field>\n <vf-window heading=\"Documents\" width=\"320\" height=\"201\" scrollbars=\"both\" top=\"40\" left=\"120\">\n <vf-icon-field label=\"Documents\">\n <vf-icon label=\"Read Me\" width=\"64\" selectable movable editable left=\"16\" top=\"16\">…</vf-icon>\n </vf-icon-field>\n </vf-window>\n</vf-desktop>\n```\n\n### Semantics through internals\n\n`role=\"listbox\"` and `aria-multiselectable=\"true\"` always — the Finder was\nalways multi-select, so there is no attribute for it — and `label` as the\naccessible name, the way `vf-scroll-area` takes one. Internals values are\ndefaults (SPEC §2): a consumer's own `role`/`aria-*` on the tag still wins.\nA `selectable` icon inside becomes a real `option` carrying `aria-selected`;\n`vf-icon` matches this tag directly, since a role written through internals\nnever lands as an attribute for `closest()` to find.\n\n### Layout-neutral, exactly what the div was\n\nAn in-flow block with no size, no inset and no position of its own,\npainting nothing — the `vf-stack` posture. A field whose icons are all\nplaced is a zero-height block, so the icons keep anchoring to the desktop's\nraster or the window's plane, stored coordinates mean what they meant, and\na press on the bare dither still reaches the desktop. It takes `top`/`left`\nand `width`/`height` like the other declared-box containers, so a page that\nwants the field to be a box places and sizes it, at which point it becomes\nthe anchor — the placement working, as documented for everything.\n`fill-width`/`fill-height` are the other way to give it a box, read about\nthe host the way `vf-stack` and `vf-container` read them: a filled field\nstays in flow and static, so its icons keep their anchor, and it has a\nsurface to press — which is what the rubber band needs.\n\n### The rubber band\n\nA press on the field's own background — not on an icon — dragged across\nthe field draws the Finder's dotted selection rectangle from the press to\nthe pointer, and the selection follows it live by the Finder's one rule:\n**an icon is selected exactly when it was selected at the press XOR the\nrectangle touches it.** The press fixes that anchor: a plain press has\nalready cleared everything (the way any press outside an icon does), so\nthe rectangle simply selects what it touches and releases what it leaves;\nwith Shift or ⌘ held the selection survives the press and the rectangle\n*toggles* against it — an icon already selected deselects while the\nrectangle covers it and comes back when it leaves, one that was not\nselects. Each change reports `vf-select` as a press would. \"Touches\"\nmeans the icon's art cell or its name plate (VfIcon.touches), not\nthe empty cell beside them, so a rectangle through the gap between two\nicons selects neither. The release keeps the result; Escape or a\n`pointercancel` cancels, the rectangle vanishes and the selection goes\nback to the anchor. A press that never leaves its system pixel draws\nnothing.\n\nThe rectangle is drawn with the drag outline's pen (* paintSelectionRect: the dotted one-pixel outline, `filter: invert(1)`\nunder `mix-blend-mode: difference`) at the field's own level, which is\nwhere the Finder drew its marquee — on the desktop under the windows, in a\nwindow body over that window's content and under any window above it —\npositioned against the viewport from the field's own shadow, since a\nlayout-neutral field has no box of its own to anchor to. It reaches no\nfurther than the field, inside every clip above the field (a window body,\na scroll viewport, the raster's edge), so it never paints outside the\nregion the icons are visible in. Its dots take the pen's phase on the\n*screen* — the desktop's, found by the `vf-drag-surface-request`\nhandshake the drag outline uses (penPhase) — so over the desktop\ndither the rectangle reads as a black line wherever the field sits; with\nno desktop, the field's own box is the reference.\n\nOnly a field with a box can be pressed: fill it, or place and size it. An\nunplaced, unfilled field of placed icons is zero-height and draws no band.\n`touch-action` is left alone, so on a touch screen a drag on the\nbackground pans a scrolling window as it always did; the band is a mouse\nand pen gesture.\n\n### `size` is the view's\n\n`large` or `small`, written onto every icon in the field when set and\nwhenever it changes, so View → by Small Icon is one attribute instead of a\nloop over every icon. Unset, each icon keeps its own.\n\n### The desktop uses it, and renders none\n\nA desktop's icons sit in a field slotted into `vf-desktop` beside the\nwindows, written by the page; a folder window's icons sit in one slotted\ninto the window body. `vf-desktop` builds no field of its own: its furniture\nis slotted light DOM and the page decides what it is, a desktop with no\nicons should carry no empty listbox, and a field inside the desktop's\nshadow is one `closest()` could not reach.",
|
|
4453
4453
|
"name": "VfIconField",
|
|
4454
4454
|
"slots": [
|
|
4455
4455
|
{
|
|
@@ -4642,6 +4642,31 @@
|
|
|
4642
4642
|
],
|
|
4643
4643
|
"description": "How far the band may reach: the field's own box, intersected with every\nancestor that clips — a window body, a scroll viewport, the desktop's\nraster — found by walking the flat tree, slots and shadow hosts\nincluded. In system px from the field's box, whole, rounded inward."
|
|
4644
4644
|
},
|
|
4645
|
+
{
|
|
4646
|
+
"kind": "method",
|
|
4647
|
+
"name": "#screenOffset",
|
|
4648
|
+
"privacy": "private",
|
|
4649
|
+
"return": {
|
|
4650
|
+
"type": {
|
|
4651
|
+
"text": "{ x: number; y: number }"
|
|
4652
|
+
}
|
|
4653
|
+
},
|
|
4654
|
+
"parameters": [
|
|
4655
|
+
{
|
|
4656
|
+
"name": "box",
|
|
4657
|
+
"type": {
|
|
4658
|
+
"text": "DOMRect"
|
|
4659
|
+
}
|
|
4660
|
+
},
|
|
4661
|
+
{
|
|
4662
|
+
"name": "scale",
|
|
4663
|
+
"type": {
|
|
4664
|
+
"text": "number"
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
],
|
|
4668
|
+
"description": "The field's box on the desktop's screen, whole system px, through the\n`vf-drag-surface-request` handshake `vf-icon` drags by (bubbles,\nnon-composed — it travels up the slots the field is assigned to). The\norigin with no desktop on the path: the field's own box stands in."
|
|
4669
|
+
},
|
|
4645
4670
|
{
|
|
4646
4671
|
"kind": "method",
|
|
4647
4672
|
"name": "#makeCanvas",
|
|
@@ -4676,7 +4701,7 @@
|
|
|
4676
4701
|
}
|
|
4677
4702
|
}
|
|
4678
4703
|
],
|
|
4679
|
-
"description": "Draw the rectangle where it lands: whole system px from the field's box,\nwhich sits on the device grid, so the band does too. The
|
|
4704
|
+
"description": "Draw the rectangle where it lands: whole system px from the field's box,\nwhich sits on the device grid, so the band does too. The pen's phase\nfollows the rectangle's corner on the screen."
|
|
4680
4705
|
},
|
|
4681
4706
|
{
|
|
4682
4707
|
"kind": "method",
|
|
@@ -4910,7 +4935,7 @@
|
|
|
4910
4935
|
"declarations": [
|
|
4911
4936
|
{
|
|
4912
4937
|
"kind": "class",
|
|
4913
|
-
"description": "`<vf-icon>` — a Finder icon: pixel art in a reserved cell with its name on a\nplate below, selectable, movable and renameable in place.\n\nIt is the one composite in the kit whose parts already existed separately —\n`vf-img` draws the art, `vf-list-item` inverts on selection, `vf-window`\ndrags — and the reason to have it anyway is that the *icon* is the unit the\nFinder actually manipulates. A picture and a caption that select together,\nmove together and rename together is a control, not a layout.\n\n### The art arrives by slot\n\nTwo slots, one per resource size — `large` is the 32×32 `ICN#` and `small`\nthe 16×16 `ics#` — each holding a `vf-img` around the consumer's own `<img>`:\n\n```html\n<vf-icon label=\"Macintosh HD\" selectable movable editable>\n <vf-img slot=\"large\"><img src=\"hd-32.png\" alt=\"\"></vf-img>\n <vf-img slot=\"small\"><img src=\"hd-16.png\" alt=\"\"></vf-img>\n</vf-icon>\n```\n\nSlots rather than `src` properties, for the reason `vf-img` exists at all:\nthe kit ships no raster files and never builds an `<img>` on a consumer's\nbehalf, so the graphic stays a real element in the light DOM with its own\n`alt`, `srcset`, loading behavior and asset URLs. A `src` string can express\nnone of that, and it could not hold an inline `<svg>` or a `<canvas>` either.\n`vf-list-item`'s `icon` slot makes the same trade.\n\nThe cost is that both files fetch even though one paints — pay it with two\ndata URIs, or slot only the size that view uses.\n\n### The cell is reserved, the art is registered in it\n\n`size` picks which slot paints *and* the cell it paints in: 32×32 or 16×16,\nheld whether or not there is art for it. A folder is 32×20 of ink and a\ndocument 25×32, and a row of them keeps one baseline only because the cell —\nnot the ink — is the unit, which is what an icon resource always was.\n\n### Everything centered lands on a whole pixel, by parity\n\nThe frame centers two things over one axis — the art cell and the name — and\na centered child sits at `(box − child) / 2`. That is a whole number exactly\nwhen the box and the child have the **same parity**. Half a system pixel is\nwhat fringes 1-bit art: the glyph stems smear across two device columns and\ngo gray, while the plate behind them stays sharp, because backgrounds are\npixel-snapped by the compositor and glyphs are not. A crisp plate under a\ngrey name is the signature of exactly this.\n\nSo the component makes the parities agree instead of correcting afterwards.\nThe cell is 32 or 16, and #measurePlate sizes the name plate to a\nwhole **even** number of system px — measured from the text, rounded up. Both\nchildren are then even, and so is every offset, at every display density and\nwhether or not a `width` is declared. Nothing is snapped, nothing leans on\nthe rasterizer, and `verify:icon` asserts both the geometry and that a field\nof names renders without a single gray pixel — with the kit's normal\nantialiasing left on, since a run on whole pixels gives it nothing to smooth.\n\nYour side of it is one rule: **a declared `width` must be even**, since the\ncomponent cannot round a number you chose. Supply art at the cell size too —\nwhich \"one image pixel is one system pixel\" already implies — because art\ndiffering from its cell by an odd number centers the same way.\n\n### Selection inverts, because the art is a 1-bit mask\n\nA System 7 icon is black ink and opaque white on a transparent surround —\nprecisely an image plus its mask — so inverting it flips ink and fill and\nleaves the surround alone, which is the whole of the classic selected\nappearance. `filter: invert(1)` reproduces it exactly for 1-bit art. Color\nart would invert into a photographic negative, which was never what\nSystem 7 showed: declare `color` and selection **darkens** the art\ninstead — Icon Utilities' ttSelected transform, every color blended\nhalfway toward black (`brightness(0.5)`), whites going gray, the\ntransparent surround still untouched. The declaration is the consumer's\nbecause the component cannot tell color art from a mask by looking at a\nslotted image. It covers whatever the cell shows, the derived open ghost\nincluded, so a color icon has one selected treatment open or closed. The\nlabel plate inverts to the `--vf-highlight` pair either way, sharing one\nselection color with `vf-list-item` — the selected name was inverted\nwhatever the art.\n\n### Open is derived, not shipped\n\nWith `open`, the art redraws as the Finder's open ghost: the outline held\nin solid black, the interior re-filled with the kit's loose 25% dither (the\nscrollbar trough's lattice), the transparent surround untouched. There is\nno second raster and no second fetch — the same alpha channel that makes\nselection an inversion makes the ghost derivable, and `src/open-art.ts`\nderives it from the slotted art by canvas compositing alone. No pixels are\never read back, so a cross-origin image that taints its canvas still works:\ntaint forbids reading, not drawing or displaying.\n\nThe ghost keeps the shape selection expects — ink and opaque white on a\ntransparent surround — so a selected open icon inverts exactly as a closed\none does, with no second treatment. The slot stays in the tree while the\nghost paints, hidden (it is where the art loads, and re-loads, from), and\nart the pipeline cannot draw — nothing slotted yet, a failed load, an\ninline `<svg>` — keeps rendering as itself rather than vanishing behind a\nstate it cannot show.\n\n### `movable`, not `draggable`\n\n`draggable` is a global HTML attribute *and* an `HTMLElement` accessor, so\ndeclaring it would both shadow a platform member and hand the element to the\nbrowser's own drag-and-drop machinery. This is the `align` trap from\n`vf-stack` in a second costume — `align`, `hidden`, `dir`, `draggable` and\n`title` all carry behavior a custom element never asked for. The kit already\nspells this parameter `movable` on `vf-window`, so the icon does too, and it\nmoves the same way: `DragController` tracks the gesture and\n`PlacementController` writes the result into `left`/`top` in whole system px,\nthe art's own unit — the same pair markup places an icon with, so a moved\nicon is still where it was dropped after a zoom.\n\nDragging is a pointer gesture with no keyboard equivalent, which is the kind\nof gap the kit closes rather than inherits (SPEC §1): a focused movable icon\nalso moves under the arrow keys, one system px at a time and eight with\nShift. Focus is what `selectable` grants, so the keyboard half of `movable`\nand `editable` presupposes it — see the role section below.\n\n### The drag is an outline; the icon stays put\n\nThe Finder dragged an icon as a dotted outline — the mask's boundary and\nthe name's rectangle — drawn over everything with the XOR pen, the icon\nitself staying put until the drop. The kit draws it from what it already\nhas: deriveDragOutline finds the ring the way the open ghost does\n(the silhouette from the art's alpha, eroded by one pixel, the ring the\nerosion removes), adds the plate's rectangle, and dotOutline dots\nit against a 2×2 checker — compositing only, so cross-origin art derives\ntoo, and art the pipeline cannot draw falls back to the cell's rectangle.\nThe canvas paints with the cursor's XOR recipe, `filter: invert(1)` under\n`mix-blend-mode: difference`: a dotted black line over a white window\nbody, and over the desktop dither the composition QuickDraw's pattern pen\ngave, because the checker's phase is taken from where the outline lands\non the *screen* — its dots share the dither's own phase wherever it goes.\n\nIt draws on the desktop's drag surface: a layer in the screen's shadow,\nover windows, palettes and the menu bar alike, clipped at the raster's\nedge like everything else, reached through the `vf-drag-surface-request`\nhandshake (bubbles, non-composed — it travels up the slots the icon is\nassigned to, so it works from inside a window body). With no desktop the\ncanvas lives in the icon's own frame instead, translated by the delta,\nclipped by whatever clips the icon; the gesture and its events are the\nsame either way. Its origin is a whole count of system px from the\nsurface's, written as a live `sysLength()`, so a scale change mid-gesture\ncannot smear it.\n\n### The drag reports, the page files\n\nThe kit ships no filing semantics — folders are the page's own catalog —\nso a drag is a stream of events and a cancelable commit, on the icon:\n\n- `vf-drag-start` `{ left, top }` on the first lattice step away from the\n press (a plain click never starts one, and never fires a drop);\n- `vf-drag` `{ clientX, clientY, left, top, x, y }` on every step that\n changed the snapped proposal;\n- `vf-drop`, the same shape at the release, **cancelable**: its default\n action is the move, the proposal written through `left`/`top` and\n clamped whole in the container measured at the press. `preventDefault()`\n writes nothing — the page re-parents or places the icon itself, the\n outline's origin converted with the destination's `placementAt`;\n- `vf-drag-cancel` `{}` on Escape or a `pointercancel`: nothing written.\n\n`left`/`top` in the detail are the *proposal*, unclamped — the origin plus\nthe lattice-snapped delta — and only the default action clamps, so a page\nthat cancels gets the raw gesture. `x`/`y` are the outline's top-left in\nviewport CSS px: the frame's box at the press translated by the delta,\nthe press offset already inside it, so an icon placed at the\ndestination's `placementAt(x, y)` lands exactly where the outline was.\n\n### The selection travels\n\nA drag that begins on a *selected* icon takes every other selected,\nmovable icon of its field with it — the Finder outlined the whole\nselection under a drag on any member. Each follower draws its own outline\n(followDrag), moved by the same lattice delta as the leader's, and\nthe leader's events carry the set as `detail.icons`, itself first. The\ndefault action of the drop clamps the delta once for the whole group —\nthe arrangement is kept, and the group stops when its outermost member\nmeets the container's edge — then writes each member's origin plus that\ndelta. A cancel takes every outline down and writes nothing. A press on\nan unselected icon makes it the selection first (the outside listeners\nclear the others), so it drags alone; a Shift press that deselects the\npressed icon drags it alone too, the rest staying where they are.\n\nFor the selection to travel, a plain press on one of its members must not\ncollapse it: the other icons' outside-press listeners leave a selection\nalone when the press landed on an icon already selected in the same\nfield. A *click* on such an icon — released with no drag — collapses the\nselection to it on the release, so a click still single-selects and the\nsame press could have dragged them all.\n\nEscape is heard by a document listener scoped to the gesture rather than\nby the icon's own key handler, because a `movable`-only icon never takes\nfocus — an addition in the kit's idiom (a pointer-only gesture with no way\nout is the kind of gap SPEC §1 closes), not a claim about the Finder.\n\n`target` is the destination highlight the page sets on the folder under a\ndrag: the selected treatment of the art alone — inverted, or darkened\nunder `color` — with none of `selected`'s semantics: no event, no outside\nlistener, no `aria-selected`, the plate untouched.\n\nOpening gets the same treatment. The double-click is the pointer gesture,\nand its keyboard route is ⌘O / ⌘↓ — the System 7 Open shortcuts, with Ctrl\nstanding in for ⌘ off the Mac. Return is deliberately not one of them: the\nFinder's Return renamed, never opened, so on an editable icon it starts the\nedit and on a non-editable one it does nothing at all.\n\n### The name and the art are one target, and the second click decides\n\nA double-click opens the icon *wherever it lands* — the name is as much the\nicon as the picture is, and the Finder never made you aim at the 32 pixels of\nart. But the name is also where a single click renames, so the two gestures\nbegin with the same press and only the second one tells them apart.\n\nSo the rename waits for it. A press on the plate of an already-selected icon\narms the field rather than opening it, and the next press inside\nRENAME_DELAY_MS calls it off — leaving the double-click to open, with\nno rename box flashing up behind it. Nothing needs to *undo* an edit that\nbegan: the press that starts one and the press that opens are the same\npress, so the only thing that can be got right is not committing early.\n\nThe window is generous in the one direction that is cheap. Reading a lone\nclick as a pair costs a wait before the box appears; reading a pair as a lone\nclick renames when the user asked to open. The same reasoning covers a press\nthat turns into a drag, a press elsewhere, and any key — each calls the\npending rename off, because none of them is the click it is waiting for.\n\n### An icon alone is a picture; an icon in a field is an option\n\n`role=\"option\"` is only meaningful inside a `listbox` that owns it. Written\nunconditionally it is not merely untidy — the browser *drops* it, and\n`aria-selected` with it, so a `selectable` icon announced as a bare generic\nand its selection state reached assistive tech nowhere at all.\n\nSo the role follows the container. Owned, the icon is an `option` that names\nitself from its plate and publishes `aria-selected`. Unowned, it degrades to\n`role=\"img\"` with a name — the same vocabulary the derived open ghost uses,\nand true of what it is. Deliberately not `button`: that would promise Enter\nand Space activate, and here Return *renames* while the open route is ⌘O / ⌘↓.\n\nThe owner is `vf-icon-field`, the container a field of icons sits in, and\nit is what buys the selection state back:\n\n```html\n<vf-icon-field label=\"Desktop\">\n <vf-icon label=\"Macintosh HD\" selectable movable editable>…</vf-icon>\n <vf-icon label=\"Trash\" selectable movable editable>…</vf-icon>\n</vf-icon-field>\n```\n\nA `vf-desktop` cannot be that container itself: it also holds windows and a\nmenu bar, and a non-`option` child of a listbox is invalid the same way the\norphaned option was. The field is layout-neutral — placed icons anchor to\nthe nearest *positioned* ancestor, which is still the desktop's raster. An\nelement of your own carrying `role=\"listbox\"` still owns an icon the same\nway. One divergence from the APG listbox is deliberate: its options share\na single roving tab stop, while these stay one stop each, the way a Finder\nicon is reached on its own.\n\n**`selectable` is what makes an icon focusable**, and `movable`/`editable`\npresuppose it. That is the Finder's own model — you cannot move or rename\nwhat you have not selected — and the pointer path already assumed it: the\nrename opens on a press on the plate of an *already-selected* icon. A\n`movable`-only icon is a picture you can drag, not a widget.\n\n### The label is a property, because it is editable\n\nThe caption is `label` rather than slotted content: renaming means the\ncomponent owns the string and hands it back on `vf-change`, and it cannot own\ntext that lives in the consumer's DOM. An empty `label` draws no plate at\nall — that *is* the \"no label\" parameter, in preference to a second attribute\nthat could disagree with it. `editable` then lets a click on the plate of an\nalready-selected icon open the rename box a moment later, as the Finder's\ndoes (see above), with Return committing, Escape reverting, and the plate\nwidening as you type.\n\n### A name is never abbreviated, and never folded\n\nThere is no ellipsis, no clipping and no wrapping: one line, always. System 7\nsolved the long-name problem at the *other* end — HFS capped a filename at 31\ncharacters — so the Finder could afford to always draw the name in full, and\ndid. A name wider than its cell simply overflows it, centered, the way a name\nwider than a 32-pixel icon always did.\n\nStaying on one line is also what keeps the name on the grid: a single run has\na single measured width, so the even-plate rule above is one number rather\nthan one per line, each with its own parity. `width` is therefore the cell —\nthe grid pitch — not a bound on the name.\n\n`maxlength` (31) bounds the rename field rather than the `label` property:\nthe name belongs to the consumer's model, and truncating one handed to us\nwould lose data.\n\nThe box hugs its text while you type it, not only once you are done, which is\nwhat keeps the name from moving when the edit commits: the plate is the same\nwidth either side of it, so the glyphs stay exactly where they were.\n\nA file also has to be called *something*, so a rename committed empty — or\nas nothing but spaces — is refused rather than applied, and the previous name\ncomes back, which is what System 7 did. An empty `label` is still a perfectly\ngood state to *start* in, though: a freshly made icon has no name until it is\ngiven one. Such an icon draws no plate but stays selectable, focusable and\nrenameable — Return opens a field, and an edit with nothing in it yet is the\none time the box stops hugging and reserves a cell's width, since a field you\ncannot see is one you cannot type into.",
|
|
4938
|
+
"description": "`<vf-icon>` — a Finder icon: pixel art in a reserved cell with its name on a\nplate below, selectable, movable and renameable in place.\n\nIt is the one composite in the kit whose parts already existed separately —\n`vf-img` draws the art, `vf-list-item` inverts on selection, `vf-window`\ndrags — and the reason to have it anyway is that the *icon* is the unit the\nFinder actually manipulates. A picture and a caption that select together,\nmove together and rename together is a control, not a layout.\n\n### The art arrives by slot\n\nTwo slots, one per resource size — `large` is the 32×32 `ICN#` and `small`\nthe 16×16 `ics#` — each holding a `vf-img` around the consumer's own `<img>`:\n\n```html\n<vf-icon label=\"Macintosh HD\" selectable movable editable>\n <vf-img slot=\"large\"><img src=\"hd-32.png\" alt=\"\"></vf-img>\n <vf-img slot=\"small\"><img src=\"hd-16.png\" alt=\"\"></vf-img>\n</vf-icon>\n```\n\nSlots rather than `src` properties, for the reason `vf-img` exists at all:\nthe kit ships no raster files and never builds an `<img>` on a consumer's\nbehalf, so the graphic stays a real element in the light DOM with its own\n`alt`, `srcset`, loading behavior and asset URLs. A `src` string can express\nnone of that, and it could not hold an inline `<svg>` or a `<canvas>` either.\n`vf-list-item`'s `icon` slot makes the same trade.\n\nThe cost is that both files fetch even though one paints — pay it with two\ndata URIs, or slot only the size that view uses.\n\n### The cell is reserved, the art is registered in it\n\n`size` picks which slot paints *and* the cell it paints in: 32×32 or 16×16,\nheld whether or not there is art for it. A folder is 32×20 of ink and a\ndocument 25×32, and a row of them keeps one baseline only because the cell —\nnot the ink — is the unit, which is what an icon resource always was.\n\n### Everything centered lands on a whole pixel, by parity\n\nThe frame centers two things over one axis — the art cell and the name — and\na centered child sits at `(box − child) / 2`. That is a whole number exactly\nwhen the box and the child have the **same parity**. Half a system pixel is\nwhat fringes 1-bit art: the glyph stems smear across two device columns and\ngo gray, while the plate behind them stays sharp, because backgrounds are\npixel-snapped by the compositor and glyphs are not. A crisp plate under a\ngrey name is the signature of exactly this.\n\nSo the component makes the parities agree instead of correcting afterwards.\nThe cell is 32 or 16, and #measurePlate sizes the name plate to a\nwhole **even** number of system px — measured from the text, rounded up. Both\nchildren are then even, and so is every offset, at every display density and\nwhether or not a `width` is declared. Nothing is snapped, nothing leans on\nthe rasterizer, and `verify:icon` asserts both the geometry and that a field\nof names renders without a single gray pixel — with the kit's normal\nantialiasing left on, since a run on whole pixels gives it nothing to smooth.\n\nYour side of it is one rule: **a declared `width` must be even**, since the\ncomponent cannot round a number you chose. Supply art at the cell size too —\nwhich \"one image pixel is one system pixel\" already implies — because art\ndiffering from its cell by an odd number centers the same way.\n\n### Selection inverts, because the art is a 1-bit mask\n\nA System 7 icon is black ink and opaque white on a transparent surround —\nprecisely an image plus its mask — so inverting it flips ink and fill and\nleaves the surround alone, which is the whole of the classic selected\nappearance. `filter: invert(1)` reproduces it exactly for 1-bit art. Color\nart would invert into a photographic negative, which was never what\nSystem 7 showed: declare `color` and selection **darkens** the art\ninstead — Icon Utilities' ttSelected transform, every color blended\nhalfway toward black (`brightness(0.5)`), whites going gray, the\ntransparent surround still untouched. The declaration is the consumer's\nbecause the component cannot tell color art from a mask by looking at a\nslotted image. It covers whatever the cell shows, the derived open ghost\nincluded, so a color icon has one selected treatment open or closed. The\nlabel plate inverts to the `--vf-highlight` pair either way, sharing one\nselection color with `vf-list-item` — the selected name was inverted\nwhatever the art.\n\n### Open is derived, not shipped\n\nWith `open`, the art redraws as the Finder's open ghost: the outline held\nin solid black, the interior re-filled with the kit's loose 25% dither (the\nscrollbar trough's lattice), the transparent surround untouched. There is\nno second raster and no second fetch — the same alpha channel that makes\nselection an inversion makes the ghost derivable, and `src/open-art.ts`\nderives it from the slotted art by canvas compositing alone. No pixels are\never read back, so a cross-origin image that taints its canvas still works:\ntaint forbids reading, not drawing or displaying.\n\nThe ghost keeps the shape selection expects — ink and opaque white on a\ntransparent surround — so a selected open icon inverts exactly as a closed\none does, with no second treatment. The slot stays in the tree while the\nghost paints, hidden (it is where the art loads, and re-loads, from), and\nart the pipeline cannot draw — nothing slotted yet, a failed load, an\ninline `<svg>` — keeps rendering as itself rather than vanishing behind a\nstate it cannot show.\n\n### `movable`, not `draggable`\n\n`draggable` is a global HTML attribute *and* an `HTMLElement` accessor, so\ndeclaring it would both shadow a platform member and hand the element to the\nbrowser's own drag-and-drop machinery. This is the `align` trap from\n`vf-stack` in a second costume — `align`, `hidden`, `dir`, `draggable` and\n`title` all carry behavior a custom element never asked for. The kit already\nspells this parameter `movable` on `vf-window`, so the icon does too, and it\nmoves the same way: `DragController` tracks the gesture and\n`PlacementController` writes the result into `left`/`top` in whole system px,\nthe art's own unit — the same pair markup places an icon with, so a moved\nicon is still where it was dropped after a zoom.\n\nDragging is a pointer gesture with no keyboard equivalent, which is the kind\nof gap the kit closes rather than inherits (SPEC §1): a focused movable icon\nalso moves under the arrow keys, one system px at a time and eight with\nShift. Focus is what `selectable` grants, so the keyboard half of `movable`\nand `editable` presupposes it — see the role section below.\n\n### The drag is an outline; the icon stays put\n\nThe Finder dragged an icon as a dotted outline — the mask's boundary and\nthe name's rectangle — drawn over everything with the XOR pen, the icon\nitself staying put until the drop. The kit draws it from what it already\nhas: deriveDragOutline finds the ring the way the open ghost does\n(the silhouette from the art's alpha, eroded by one pixel, the ring the\nerosion removes), adds the plate's rectangle, and dotOutline dots\nit against a 2×2 checker — compositing only, so cross-origin art derives\ntoo, and art the pipeline cannot draw falls back to the cell's rectangle.\nThe canvas paints with the cursor's XOR recipe, `filter: invert(1)` under\n`mix-blend-mode: difference`: a dotted black line over a white window\nbody, and over the desktop dither a black line, the composition\nQuickDraw's pattern pen gave — the checker's phase is taken from where\nthe outline lands on the *screen* (penPhase), so its dots fall on\nthe pixels the dither leaves white wherever it goes, and the dither's own\nink fills in between them.\n\nIt draws on the desktop's drag surface: a layer in the screen's shadow,\nover windows, palettes and the menu bar alike, clipped at the raster's\nedge like everything else, reached through the `vf-drag-surface-request`\nhandshake (bubbles, non-composed — it travels up the slots the icon is\nassigned to, so it works from inside a window body). With no desktop the\ncanvas lives in the icon's own frame instead, translated by the delta,\nclipped by whatever clips the icon; the gesture and its events are the\nsame either way. Its origin is a whole count of system px from the\nsurface's, written as a live `sysLength()`, so a scale change mid-gesture\ncannot smear it.\n\n### The drag reports, the page files\n\nThe kit ships no filing semantics — folders are the page's own catalog —\nso a drag is a stream of events and a cancelable commit, on the icon:\n\n- `vf-drag-start` `{ left, top }` on the first lattice step away from the\n press (a plain click never starts one, and never fires a drop);\n- `vf-drag` `{ clientX, clientY, left, top, x, y }` on every step that\n changed the snapped proposal;\n- `vf-drop`, the same shape at the release, **cancelable**: its default\n action is the move, the proposal written through `left`/`top` and\n clamped whole in the container measured at the press. `preventDefault()`\n writes nothing — the page re-parents or places the icon itself, the\n outline's origin converted with the destination's `placementAt`;\n- `vf-drag-cancel` `{}` on Escape or a `pointercancel`: nothing written.\n\n`left`/`top` in the detail are the *proposal*, unclamped — the origin plus\nthe lattice-snapped delta — and only the default action clamps, so a page\nthat cancels gets the raw gesture. `x`/`y` are the outline's top-left in\nviewport CSS px: the frame's box at the press translated by the delta,\nthe press offset already inside it, so an icon placed at the\ndestination's `placementAt(x, y)` lands exactly where the outline was.\n\n### The selection travels\n\nA drag that begins on a *selected* icon takes every other selected,\nmovable icon of its field with it — the Finder outlined the whole\nselection under a drag on any member. Each follower draws its own outline\n(followDrag), moved by the same lattice delta as the leader's, and\nthe leader's events carry the set as `detail.icons`, itself first. The\ndefault action of the drop clamps the delta once for the whole group —\nthe arrangement is kept, and the group stops when its outermost member\nmeets the container's edge — then writes each member's origin plus that\ndelta. A cancel takes every outline down and writes nothing. A press on\nan unselected icon makes it the selection first (the outside listeners\nclear the others), so it drags alone; a Shift press that deselects the\npressed icon drags it alone too, the rest staying where they are.\n\nFor the selection to travel, a plain press on one of its members must not\ncollapse it: the other icons' outside-press listeners leave a selection\nalone when the press landed on an icon already selected in the same\nfield. A *click* on such an icon — released with no drag — collapses the\nselection to it on the release, so a click still single-selects and the\nsame press could have dragged them all.\n\nEscape is heard by a document listener scoped to the gesture rather than\nby the icon's own key handler, because a `movable`-only icon never takes\nfocus — an addition in the kit's idiom (a pointer-only gesture with no way\nout is the kind of gap SPEC §1 closes), not a claim about the Finder.\n\n`target` is the destination highlight the page sets on the folder under a\ndrag: the selected treatment of the art alone — inverted, or darkened\nunder `color` — with none of `selected`'s semantics: no event, no outside\nlistener, no `aria-selected`, the plate untouched.\n\nOpening gets the same treatment. The double-click is the pointer gesture,\nand its keyboard route is ⌘O / ⌘↓ — the System 7 Open shortcuts, with Ctrl\nstanding in for ⌘ off the Mac. Return is deliberately not one of them: the\nFinder's Return renamed, never opened, so on an editable icon it starts the\nedit and on a non-editable one it does nothing at all.\n\n### The name and the art are one target, and the second click decides\n\nA double-click opens the icon *wherever it lands* — the name is as much the\nicon as the picture is, and the Finder never made you aim at the 32 pixels of\nart. But the name is also where a single click renames, so the two gestures\nbegin with the same press and only the second one tells them apart.\n\nSo the rename waits for it. A press on the plate of an already-selected icon\narms the field rather than opening it, and the next press inside\nRENAME_DELAY_MS calls it off — leaving the double-click to open, with\nno rename box flashing up behind it. Nothing needs to *undo* an edit that\nbegan: the press that starts one and the press that opens are the same\npress, so the only thing that can be got right is not committing early.\n\nThe window is generous in the one direction that is cheap. Reading a lone\nclick as a pair costs a wait before the box appears; reading a pair as a lone\nclick renames when the user asked to open. The same reasoning covers a press\nthat turns into a drag, a press elsewhere, and any key — each calls the\npending rename off, because none of them is the click it is waiting for.\n\n### An icon alone is a picture; an icon in a field is an option\n\n`role=\"option\"` is only meaningful inside a `listbox` that owns it. Written\nunconditionally it is not merely untidy — the browser *drops* it, and\n`aria-selected` with it, so a `selectable` icon announced as a bare generic\nand its selection state reached assistive tech nowhere at all.\n\nSo the role follows the container. Owned, the icon is an `option` that names\nitself from its plate and publishes `aria-selected`. Unowned, it degrades to\n`role=\"img\"` with a name — the same vocabulary the derived open ghost uses,\nand true of what it is. Deliberately not `button`: that would promise Enter\nand Space activate, and here Return *renames* while the open route is ⌘O / ⌘↓.\n\nThe owner is `vf-icon-field`, the container a field of icons sits in, and\nit is what buys the selection state back:\n\n```html\n<vf-icon-field label=\"Desktop\">\n <vf-icon label=\"Macintosh HD\" selectable movable editable>…</vf-icon>\n <vf-icon label=\"Trash\" selectable movable editable>…</vf-icon>\n</vf-icon-field>\n```\n\nA `vf-desktop` cannot be that container itself: it also holds windows and a\nmenu bar, and a non-`option` child of a listbox is invalid the same way the\norphaned option was. The field is layout-neutral — placed icons anchor to\nthe nearest *positioned* ancestor, which is still the desktop's raster. An\nelement of your own carrying `role=\"listbox\"` still owns an icon the same\nway. One divergence from the APG listbox is deliberate: its options share\na single roving tab stop, while these stay one stop each, the way a Finder\nicon is reached on its own.\n\n**`selectable` is what makes an icon focusable**, and `movable`/`editable`\npresuppose it. That is the Finder's own model — you cannot move or rename\nwhat you have not selected — and the pointer path already assumed it: the\nrename opens on a press on the plate of an *already-selected* icon. A\n`movable`-only icon is a picture you can drag, not a widget.\n\n### The label is a property, because it is editable\n\nThe caption is `label` rather than slotted content: renaming means the\ncomponent owns the string and hands it back on `vf-change`, and it cannot own\ntext that lives in the consumer's DOM. An empty `label` draws no plate at\nall — that *is* the \"no label\" parameter, in preference to a second attribute\nthat could disagree with it. `editable` then lets a click on the plate of an\nalready-selected icon open the rename box a moment later, as the Finder's\ndoes (see above), with Return committing, Escape reverting, and the plate\nwidening as you type.\n\n### A name is never abbreviated, and never folded\n\nThere is no ellipsis, no clipping and no wrapping: one line, always. System 7\nsolved the long-name problem at the *other* end — HFS capped a filename at 31\ncharacters — so the Finder could afford to always draw the name in full, and\ndid. A name wider than its cell simply overflows it, centered, the way a name\nwider than a 32-pixel icon always did.\n\nStaying on one line is also what keeps the name on the grid: a single run has\na single measured width, so the even-plate rule above is one number rather\nthan one per line, each with its own parity. `width` is therefore the cell —\nthe grid pitch — not a bound on the name.\n\n`maxlength` (31) bounds the rename field rather than the `label` property:\nthe name belongs to the consumer's model, and truncating one handed to us\nwould lose data.\n\nThe box hugs its text while you type it, not only once you are done, which is\nwhat keeps the name from moving when the edit commits: the plate is the same\nwidth either side of it, so the glyphs stay exactly where they were.\n\nA file also has to be called *something*, so a rename committed empty — or\nas nothing but spaces — is refused rather than applied, and the previous name\ncomes back, which is what System 7 did. An empty `label` is still a perfectly\ngood state to *start* in, though: a freshly made icon has no name until it is\ngiven one. Such an icon draws no plate but stays selectable, focusable and\nrenameable — Return opens a field, and an edit with nothing in it yet is the\none time the box stops hugging and reserves a cell's width, since a field you\ncannot see is one you cannot type into.",
|
|
4914
4939
|
"name": "VfIcon",
|
|
4915
4940
|
"cssProperties": [
|
|
4916
4941
|
{
|
|
@@ -5500,7 +5525,7 @@
|
|
|
5500
5525
|
}
|
|
5501
5526
|
}
|
|
5502
5527
|
],
|
|
5503
|
-
"description": "Move an outline by the drag's delta: the frame's offset plus the delta,\na whole count of system px from the surface's origin, re-dotted at the\
|
|
5528
|
+
"description": "Move an outline by the drag's delta: the frame's offset plus the delta,\na whole count of system px from the surface's origin, re-dotted at the\npen's phase for where that lands on the screen — the dots between the\ndesktop dither's ink wherever the outline goes."
|
|
5504
5529
|
},
|
|
5505
5530
|
{
|
|
5506
5531
|
"kind": "method",
|
|
@@ -83,8 +83,11 @@ declare const VfIconField_base: (new (...args: any[]) => import("../size.js").Vf
|
|
|
83
83
|
* layout-neutral field has no box of its own to anchor to. It reaches no
|
|
84
84
|
* further than the field, inside every clip above the field (a window body,
|
|
85
85
|
* a scroll viewport, the raster's edge), so it never paints outside the
|
|
86
|
-
* region the icons are visible in. Its dots
|
|
87
|
-
*
|
|
86
|
+
* region the icons are visible in. Its dots take the pen's phase on the
|
|
87
|
+
* *screen* — the desktop's, found by the `vf-drag-surface-request`
|
|
88
|
+
* handshake the drag outline uses ({@link penPhase}) — so over the desktop
|
|
89
|
+
* dither the rectangle reads as a black line wherever the field sits; with
|
|
90
|
+
* no desktop, the field's own box is the reference.
|
|
88
91
|
*
|
|
89
92
|
* Only a field with a box can be pressed: fill it, or place and size it. An
|
|
90
93
|
* unplaced, unfilled field of placed icons is zero-height and draws no band.
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import r from "../
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
1
|
+
import { emit as e } from "../events.js";
|
|
2
|
+
import { vfElement as t } from "../define.js";
|
|
3
|
+
import { ScaleController as n, effectiveScale as r } from "../scale.js";
|
|
4
|
+
import i from "../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
|
|
5
|
+
import { VfPositioned as a } from "../position.js";
|
|
6
|
+
import { vfBase as o } from "../styles/recipes/host.js";
|
|
7
|
+
import { DocumentListenersController as s } from "../document-listeners.js";
|
|
8
|
+
import { VfSized as c } from "../size.js";
|
|
9
|
+
import { paintSelectionRect as l, penPhase as u } from "../open-art.js";
|
|
10
|
+
import { LitElement as d, css as f, html as p } from "lit";
|
|
11
|
+
import { property as m } from "lit/decorators.js";
|
|
11
12
|
//#region src/components/vf-icon-field.ts
|
|
12
|
-
var
|
|
13
|
+
var h = (e, t, n) => Math.min(Math.max(e, t), n), g = class extends c(a(d)) {
|
|
13
14
|
static {
|
|
14
|
-
this.styles = [
|
|
15
|
+
this.styles = [o, f`
|
|
15
16
|
:host {
|
|
16
17
|
/* A block that shrink-wraps: the field is exactly as big as what it
|
|
17
18
|
holds in flow — nothing, for a field of placed icons — so it can
|
|
@@ -48,7 +49,7 @@ var p = (e, t, n) => Math.min(Math.max(e, t), n), m = class extends s(i(l)) {
|
|
|
48
49
|
#t;
|
|
49
50
|
#n;
|
|
50
51
|
constructor() {
|
|
51
|
-
super(), this.label = "", this.scale = new
|
|
52
|
+
super(), this.label = "", this.scale = new n(this), this.#e = this.attachInternals(), this.#t = null, this.#n = new s(this, () => [[
|
|
52
53
|
document,
|
|
53
54
|
"keydown",
|
|
54
55
|
this.#l,
|
|
@@ -59,12 +60,13 @@ var p = (e, t, n) => Math.min(Math.max(e, t), n), m = class extends s(i(l)) {
|
|
|
59
60
|
if (this.#t || e.button !== 0 || e.target !== this) return;
|
|
60
61
|
let t = this.getBoundingClientRect();
|
|
61
62
|
if (t.width <= 0 || t.height <= 0) return;
|
|
62
|
-
let
|
|
63
|
+
let n = r(this), i = this.#f(t, n);
|
|
63
64
|
this.#t = {
|
|
64
65
|
pointerId: e.pointerId,
|
|
65
66
|
box: t,
|
|
66
67
|
reach: i,
|
|
67
|
-
origin: this.#d(e, t,
|
|
68
|
+
origin: this.#d(e, t, n, i),
|
|
69
|
+
screen: this.#p(t, n),
|
|
68
70
|
anchor: new Set(this.#a().filter((e) => e.selected)),
|
|
69
71
|
rect: null,
|
|
70
72
|
canvas: null,
|
|
@@ -73,7 +75,7 @@ var p = (e, t, n) => Math.min(Math.max(e, t), n), m = class extends s(i(l)) {
|
|
|
73
75
|
}, this.#s = (e) => {
|
|
74
76
|
let t = this.#t;
|
|
75
77
|
if (!t || e.pointerId !== t.pointerId) return;
|
|
76
|
-
let
|
|
78
|
+
let n = r(this), i = this.#d(e, t.box, n, t.reach);
|
|
77
79
|
if (!t.rect && i.x === t.origin.x && i.y === t.origin.y) return;
|
|
78
80
|
let a = {
|
|
79
81
|
x: Math.min(t.origin.x, i.x),
|
|
@@ -81,7 +83,7 @@ var p = (e, t, n) => Math.min(Math.max(e, t), n), m = class extends s(i(l)) {
|
|
|
81
83
|
width: Math.abs(i.x - t.origin.x),
|
|
82
84
|
height: Math.abs(i.y - t.origin.y)
|
|
83
85
|
}, o = t.rect;
|
|
84
|
-
o && o.x === a.x && o.y === a.y && o.width === a.width && o.height === a.height || (o || (this.#n.attach(), t.canvas = this.#
|
|
86
|
+
o && o.x === a.x && o.y === a.y && o.width === a.width && o.height === a.height || (o || (this.#n.attach(), t.canvas = this.#m(), this.renderRoot.append(t.canvas)), t.rect = a, this.#h(t, n), this.#g(t, n));
|
|
85
87
|
}, this.#c = (e) => {
|
|
86
88
|
let t = this.#t;
|
|
87
89
|
!t || e.pointerId !== t.pointerId || this.#u(e.type === "pointercancel");
|
|
@@ -116,8 +118,8 @@ var p = (e, t, n) => Math.min(Math.max(e, t), n), m = class extends s(i(l)) {
|
|
|
116
118
|
}
|
|
117
119
|
#d(e, t, n, r) {
|
|
118
120
|
return {
|
|
119
|
-
x:
|
|
120
|
-
y:
|
|
121
|
+
x: h(Math.round((e.clientX - t.left) / n), r.minX, r.maxX),
|
|
122
|
+
y: h(Math.round((e.clientY - t.top) / n), r.minY, r.maxY)
|
|
121
123
|
};
|
|
122
124
|
}
|
|
123
125
|
#f(e, t) {
|
|
@@ -138,19 +140,31 @@ var p = (e, t, n) => Math.min(Math.max(e, t), n), m = class extends s(i(l)) {
|
|
|
138
140
|
maxY: Math.max(0, Math.floor(s((a - e.top) / t)))
|
|
139
141
|
};
|
|
140
142
|
}
|
|
141
|
-
#p() {
|
|
143
|
+
#p(t, n) {
|
|
144
|
+
let r = { surface: null };
|
|
145
|
+
if (e(this, "vf-drag-surface-request", r, { composed: !1 }), !r.surface) return {
|
|
146
|
+
x: 0,
|
|
147
|
+
y: 0
|
|
148
|
+
};
|
|
149
|
+
let i = r.surface.getBoundingClientRect();
|
|
150
|
+
return {
|
|
151
|
+
x: Math.round((t.left - i.left) / n),
|
|
152
|
+
y: Math.round((t.top - i.top) / n)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
#m() {
|
|
142
156
|
let e = document.createElement("canvas");
|
|
143
157
|
e.className = "selection-rect";
|
|
144
158
|
let t = e.style;
|
|
145
159
|
return t.position = "fixed", t.display = "block", t.pointerEvents = "none", t.filter = "invert(1)", t.mixBlendMode = "difference", t.imageRendering = "pixelated", e;
|
|
146
160
|
}
|
|
147
|
-
#
|
|
161
|
+
#h(e, t) {
|
|
148
162
|
let { rect: n, canvas: r } = e;
|
|
149
163
|
if (!n || !r) return;
|
|
150
|
-
let i = Math.max(1, n.width), a = Math.max(1, n.height), o = (
|
|
151
|
-
|
|
164
|
+
let i = Math.max(1, n.width), a = Math.max(1, n.height), o = u(e.screen.x + n.x, e.screen.y + n.y);
|
|
165
|
+
l(r, i, a, o), e.phase = o, r.style.left = `${e.box.left + n.x * t}px`, r.style.top = `${e.box.top + n.y * t}px`, r.style.width = `${i * t}px`, r.style.height = `${a * t}px`;
|
|
152
166
|
}
|
|
153
|
-
#
|
|
167
|
+
#g(e, t) {
|
|
154
168
|
let { rect: n } = e;
|
|
155
169
|
if (!n) return;
|
|
156
170
|
let r = {
|
|
@@ -165,9 +179,9 @@ var p = (e, t, n) => Math.min(Math.max(e, t), n), m = class extends s(i(l)) {
|
|
|
165
179
|
}
|
|
166
180
|
}
|
|
167
181
|
render() {
|
|
168
|
-
return
|
|
182
|
+
return p`<slot @slotchange=${this.#r}></slot>`;
|
|
169
183
|
}
|
|
170
184
|
};
|
|
171
|
-
|
|
185
|
+
i([m()], g.prototype, "label", void 0), i([m({ reflect: !0 })], g.prototype, "size", void 0), g = i([t("vf-icon-field")], g);
|
|
172
186
|
//#endregion
|
|
173
|
-
export {
|
|
187
|
+
export { g as VfIconField };
|
|
@@ -189,9 +189,11 @@ declare const VfIcon_base: (new (...args: any[]) => import("../position.js").VfP
|
|
|
189
189
|
* too, and art the pipeline cannot draw falls back to the cell's rectangle.
|
|
190
190
|
* The canvas paints with the cursor's XOR recipe, `filter: invert(1)` under
|
|
191
191
|
* `mix-blend-mode: difference`: a dotted black line over a white window
|
|
192
|
-
* body, and over the desktop dither
|
|
193
|
-
* gave
|
|
194
|
-
* on the *screen*
|
|
192
|
+
* body, and over the desktop dither a black line, the composition
|
|
193
|
+
* QuickDraw's pattern pen gave — the checker's phase is taken from where
|
|
194
|
+
* the outline lands on the *screen* ({@link penPhase}), so its dots fall on
|
|
195
|
+
* the pixels the dither leaves white wherever it goes, and the dither's own
|
|
196
|
+
* ink fills in between them.
|
|
195
197
|
*
|
|
196
198
|
* It draws on the desktop's drag surface: a layer in the screen's shadow,
|
|
197
199
|
* over windows, palettes and the menu bar alike, clipped at the raster's
|
|
@@ -11,16 +11,16 @@ import { DocumentListenersController as m } from "../document-listeners.js";
|
|
|
11
11
|
import { DragController as h } from "../drag.js";
|
|
12
12
|
import "../motion.js";
|
|
13
13
|
import { FocusRuleController as g } from "../focus-modality.js";
|
|
14
|
-
import { deriveDragOutline as _, deriveOpenArt as v, dotOutline as y } from "../open-art.js";
|
|
15
|
-
import { LitElement as
|
|
16
|
-
import { property as
|
|
14
|
+
import { deriveDragOutline as _, deriveOpenArt as v, dotOutline as y, penPhase as b } from "../open-art.js";
|
|
15
|
+
import { LitElement as x, css as S, html as C, nothing as w } from "lit";
|
|
16
|
+
import { property as T, state as E } from "lit/decorators.js";
|
|
17
17
|
//#region src/components/vf-icon.ts
|
|
18
|
-
var
|
|
18
|
+
var D, O = {
|
|
19
19
|
large: 32,
|
|
20
20
|
small: 16
|
|
21
|
-
},
|
|
21
|
+
}, k = 1, A = 8, j = (e, t) => t <= 0 ? Math.max(e, 0) : Math.min(Math.max(e, 0), t), M = class extends c(x) {
|
|
22
22
|
static {
|
|
23
|
-
|
|
23
|
+
D = this;
|
|
24
24
|
}
|
|
25
25
|
constructor(...t) {
|
|
26
26
|
super(...t), this.label = "", this.size = "large", this.selectable = !1, this.selected = !1, this.target = !1, this.color = !1, this.open = !1, this.movable = !1, this.editable = !1, this.maxlength = 31, this.scale = new n(this), this.gridSnap = new p(this), this.focusRule = new g(this), this._editing = !1, this._draft = "", this.#e = 0, this._ghost = null, this.#t = null, this.#n = this.attachInternals(), this.#a = !1, this.#o = "", this.#s = "", this.#c = 0, this.#l = -Infinity, this.#u = new m(this, () => [[
|
|
@@ -29,8 +29,8 @@ var E, D = {
|
|
|
29
29
|
this.#K,
|
|
30
30
|
!0
|
|
31
31
|
]]), this.#d = (e, t, n) => ({
|
|
32
|
-
x:
|
|
33
|
-
y:
|
|
32
|
+
x: j(e, n.width - a(this.offsetWidth, this)),
|
|
33
|
+
y: j(t, n.height - a(this.offsetHeight, this))
|
|
34
34
|
}), this.#f = new s(this, (e, t, n) => this.#d(e, t, n)), this.#p = null, this.#m = new h(this, {
|
|
35
35
|
onDragStart: (e) => {
|
|
36
36
|
if (!this.movable || e.button !== 0 || this._editing) return null;
|
|
@@ -83,7 +83,7 @@ var E, D = {
|
|
|
83
83
|
if (t.includes(this) || (this.#L(), e.shiftKey || e.metaKey)) return;
|
|
84
84
|
let n = this.#i;
|
|
85
85
|
if (n !== null) {
|
|
86
|
-
let e = t.find((e) => e instanceof
|
|
86
|
+
let e = t.find((e) => e instanceof D);
|
|
87
87
|
if (e?.selected && e.#i === n) return;
|
|
88
88
|
}
|
|
89
89
|
this.#H(!1);
|
|
@@ -122,7 +122,7 @@ var E, D = {
|
|
|
122
122
|
case "ArrowDown": {
|
|
123
123
|
if (!this.movable) return;
|
|
124
124
|
t.preventDefault(), this.focusRule.reveal();
|
|
125
|
-
let e = t.shiftKey ?
|
|
125
|
+
let e = t.shiftKey ? A : k, n = t.key === "ArrowLeft" ? -e : t.key === "ArrowRight" ? e : 0, r = t.key === "ArrowUp" ? -e : t.key === "ArrowDown" ? e : 0;
|
|
126
126
|
this.#k();
|
|
127
127
|
let i = this.#f.seed();
|
|
128
128
|
this.#f.moveTo(i.x + n, i.y + r);
|
|
@@ -132,7 +132,7 @@ var E, D = {
|
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
static {
|
|
135
|
-
this.styles = [d,
|
|
135
|
+
this.styles = [d, S`
|
|
136
136
|
:host {
|
|
137
137
|
display: inline-block;
|
|
138
138
|
cursor: var(--vf-cursor, default);
|
|
@@ -533,7 +533,7 @@ var E, D = {
|
|
|
533
533
|
#E(e, t, n) {
|
|
534
534
|
let r = e.base.x + t, a = e.base.y + n;
|
|
535
535
|
e.canvas.style.left = i(r), e.canvas.style.top = i(a);
|
|
536
|
-
let o = (
|
|
536
|
+
let o = b(r, a);
|
|
537
537
|
o !== e.phase && (e.phase = o, y(e.ring, e.canvas, o));
|
|
538
538
|
}
|
|
539
539
|
#D(e) {
|
|
@@ -585,7 +585,7 @@ var E, D = {
|
|
|
585
585
|
n.selectNodeContents(e), t = n.getBoundingClientRect().width, n.detach();
|
|
586
586
|
}
|
|
587
587
|
let n = t / r(this) + 2, i = Math.max(2, Math.ceil(n / 2) * 2);
|
|
588
|
-
this.#e = this._editing && t === 0 ? Math.max(i,
|
|
588
|
+
this.#e = this._editing && t === 0 ? Math.max(i, O[this.size]) : i, e.style.width = this.#j;
|
|
589
589
|
}
|
|
590
590
|
get #j() {
|
|
591
591
|
return this.#e ? `calc(var(--vf-scale, 1) * ${this.#e}px)` : "";
|
|
@@ -652,7 +652,7 @@ var E, D = {
|
|
|
652
652
|
}
|
|
653
653
|
render() {
|
|
654
654
|
let e = this._editing, t = e ? this._draft : this.label, n = this.focusRule.marked && !e, r = t !== "" || e, i = `caption${e ? " editing" : ""}${n && t ? " focus-rule" : ""}`, a = `art${this.open && this._ghost ? " open" : ""}${n && !t ? " focus-rule" : ""}`, o = this.width ? `width: calc(var(--vf-scale, 1) * ${this.width}px)` : "";
|
|
655
|
-
return
|
|
655
|
+
return C`<div
|
|
656
656
|
class="frame vf-snap"
|
|
657
657
|
part="frame"
|
|
658
658
|
style=${o}
|
|
@@ -663,9 +663,9 @@ var E, D = {
|
|
|
663
663
|
@dblclick=${this.#W}
|
|
664
664
|
>
|
|
665
665
|
<div class=${a} part="icon">
|
|
666
|
-
${this.size === "small" ?
|
|
666
|
+
${this.size === "small" ? C`<slot name="small" @slotchange=${this.#M}></slot>` : C`<slot name="large" @slotchange=${this.#M}></slot>`}${this._ghost ?? w}
|
|
667
667
|
</div>
|
|
668
|
-
${r ?
|
|
668
|
+
${r ? C`<div class=${i}>
|
|
669
669
|
<!-- No whitespace around the span: it is the measured run, and a
|
|
670
670
|
stray space would widen it. -->
|
|
671
671
|
<div class="label" part="label"><span
|
|
@@ -674,7 +674,7 @@ var E, D = {
|
|
|
674
674
|
style=${this.#e ? `width: ${this.#j}` : ""}
|
|
675
675
|
>${t}</span
|
|
676
676
|
></div>
|
|
677
|
-
${e ?
|
|
677
|
+
${e ? C`<div class="rename-box">
|
|
678
678
|
<input
|
|
679
679
|
class="rename"
|
|
680
680
|
part="input"
|
|
@@ -686,8 +686,8 @@ var E, D = {
|
|
|
686
686
|
@keydown=${this.#X}
|
|
687
687
|
@blur=${this.commitEditing}
|
|
688
688
|
/>
|
|
689
|
-
</div>` :
|
|
690
|
-
</div>` :
|
|
689
|
+
</div>` : w}
|
|
690
|
+
</div>` : w}
|
|
691
691
|
</div>`;
|
|
692
692
|
}
|
|
693
693
|
#R() {
|
|
@@ -743,27 +743,27 @@ var E, D = {
|
|
|
743
743
|
#X;
|
|
744
744
|
#Z;
|
|
745
745
|
};
|
|
746
|
-
o([
|
|
746
|
+
o([T()], M.prototype, "label", void 0), o([T({ reflect: !0 })], M.prototype, "size", void 0), o([T({
|
|
747
747
|
type: Boolean,
|
|
748
748
|
reflect: !0
|
|
749
|
-
})],
|
|
749
|
+
})], M.prototype, "selectable", void 0), o([T({
|
|
750
750
|
type: Boolean,
|
|
751
751
|
reflect: !0
|
|
752
|
-
})],
|
|
752
|
+
})], M.prototype, "selected", void 0), o([T({
|
|
753
753
|
type: Boolean,
|
|
754
754
|
reflect: !0
|
|
755
|
-
})],
|
|
755
|
+
})], M.prototype, "target", void 0), o([T({
|
|
756
756
|
type: Boolean,
|
|
757
757
|
reflect: !0
|
|
758
|
-
})],
|
|
758
|
+
})], M.prototype, "color", void 0), o([T({
|
|
759
759
|
type: Boolean,
|
|
760
760
|
reflect: !0
|
|
761
|
-
})],
|
|
761
|
+
})], M.prototype, "open", void 0), o([T({
|
|
762
762
|
type: Boolean,
|
|
763
763
|
reflect: !0
|
|
764
|
-
})],
|
|
764
|
+
})], M.prototype, "movable", void 0), o([T({
|
|
765
765
|
type: Boolean,
|
|
766
766
|
reflect: !0
|
|
767
|
-
})],
|
|
767
|
+
})], M.prototype, "editable", void 0), o([T({ type: Number })], M.prototype, "width", void 0), o([T({ type: Number })], M.prototype, "maxlength", void 0), o([E()], M.prototype, "_editing", void 0), o([E()], M.prototype, "_draft", void 0), o([E()], M.prototype, "_ghost", void 0), M = D = o([t("vf-icon")], M);
|
|
768
768
|
//#endregion
|
|
769
|
-
export {
|
|
769
|
+
export { M as VfIcon };
|
|
@@ -284,8 +284,8 @@ var W = 24, G = 8, K = class extends D(c(P)) {
|
|
|
284
284
|
.grow::before {
|
|
285
285
|
content: '';
|
|
286
286
|
position: absolute;
|
|
287
|
-
right: calc(var(--vf-scale, 1) *
|
|
288
|
-
bottom: calc(var(--vf-scale, 1) *
|
|
287
|
+
right: calc(var(--vf-scale, 1) * 1px);
|
|
288
|
+
bottom: calc(var(--vf-scale, 1) * 1px);
|
|
289
289
|
width: calc(var(--vf-scale, 1) * 9px);
|
|
290
290
|
height: calc(var(--vf-scale, 1) * 9px);
|
|
291
291
|
border: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000000);
|
package/dist/open-art.d.ts
CHANGED
|
@@ -81,6 +81,16 @@ export interface DragOutlineGeometry {
|
|
|
81
81
|
* context.
|
|
82
82
|
*/
|
|
83
83
|
export declare function deriveDragOutline(art: HTMLImageElement | HTMLCanvasElement | null, geometry: DragOutlineGeometry): HTMLCanvasElement | null;
|
|
84
|
+
/**
|
|
85
|
+
* The XOR pen's dot phase for a ring whose top-left lands at (`x`, `y`) on
|
|
86
|
+
* the screen, whole system px, for {@link dotOutline}: the dots take the
|
|
87
|
+
* diagonal the desktop dither leaves white — `x + y` odd on the screen — the
|
|
88
|
+
* way the Finder's did. Over the dither each dot flips a white pixel black
|
|
89
|
+
* and the dither's own ink fills the gaps, so the ring reads as a solid
|
|
90
|
+
* black line; over paper it is a dotted black line. The other diagonal
|
|
91
|
+
* would flip the dither's ink to paper, and the ring would read white.
|
|
92
|
+
*/
|
|
93
|
+
export declare function penPhase(x: number, y: number): number;
|
|
84
94
|
/**
|
|
85
95
|
* Paint a selection rectangle — the rubber band `vf-icon-field` drags on its
|
|
86
96
|
* background — into `into`: a one-pixel outline `width` × `height` system px,
|
|
@@ -91,9 +101,9 @@ export declare function paintSelectionRect(into: HTMLCanvasElement, width: numbe
|
|
|
91
101
|
/**
|
|
92
102
|
* Paint `ring` into `into` dotted with the checker at `phase` — 0 keeps the
|
|
93
103
|
* dots where `x + y` is even in the canvas's own coordinates, 1 where it is
|
|
94
|
-
* odd. The caller
|
|
95
|
-
* so the dots
|
|
96
|
-
* as QuickDraw's screen-anchored pattern pen
|
|
97
|
-
* clears it, so each step is a fresh paint.
|
|
104
|
+
* odd. The caller takes the phase from where the outline lands on the
|
|
105
|
+
* screen ({@link penPhase}), so the dots sit between the desktop dither's
|
|
106
|
+
* ink wherever the outline goes, as QuickDraw's screen-anchored pattern pen
|
|
107
|
+
* did. Sizing `into` to the ring clears it, so each step is a fresh paint.
|
|
98
108
|
*/
|
|
99
109
|
export declare function dotOutline(ring: HTMLCanvasElement, into: HTMLCanvasElement, phase: number): void;
|
package/dist/open-art.js
CHANGED
|
@@ -61,16 +61,19 @@ function s(t, n) {
|
|
|
61
61
|
} else o(a.ctx, n.cell);
|
|
62
62
|
return n.plate && o(a.ctx, n.plate), a.canvas;
|
|
63
63
|
}
|
|
64
|
-
function c(
|
|
64
|
+
function c(e, t) {
|
|
65
|
+
return +(((e + t) % 2 + 2) % 2 == 0);
|
|
66
|
+
}
|
|
67
|
+
function l(t, n, r, i) {
|
|
65
68
|
let a = Math.max(1, Math.round(n)), s = Math.max(1, Math.round(r)), c = e(a, s);
|
|
66
69
|
c && (o(c.ctx, {
|
|
67
70
|
x: 0,
|
|
68
71
|
y: 0,
|
|
69
72
|
width: a,
|
|
70
73
|
height: s
|
|
71
|
-
}),
|
|
74
|
+
}), u(c.canvas, t, i));
|
|
72
75
|
}
|
|
73
|
-
function
|
|
76
|
+
function u(e, t, n) {
|
|
74
77
|
let r = a(), i = t.getContext("2d");
|
|
75
78
|
if (!r || !i) return;
|
|
76
79
|
t.width = e.width, t.height = e.height, i.imageSmoothingEnabled = !1, i.drawImage(e, 0, 0);
|
|
@@ -78,4 +81,4 @@ function l(e, t, n) {
|
|
|
78
81
|
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");
|
|
79
82
|
}
|
|
80
83
|
//#endregion
|
|
81
|
-
export { s as deriveDragOutline, r as deriveOpenArt,
|
|
84
|
+
export { s as deriveDragOutline, r as deriveOpenArt, u as dotOutline, l as paintSelectionRect, c as penPhase };
|
package/docs/SPEC.md
CHANGED
|
@@ -479,7 +479,7 @@ The Finder icon — pixel art in a reserved cell with its name on a plate below,
|
|
|
479
479
|
- **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.
|
|
480
480
|
- **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.
|
|
481
481
|
- **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 away from the press (the jitter test: a stationary press snaps back onto the same system px and never starts a drag, so a plain click never fires a drop); `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 — 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).
|
|
482
|
-
- **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 one-pixel erosion by four `destination-in` draws on a frame-sized layer (the frame is transparent around the art, so its edge erodes as the off-canvas edge did), the ring the erosion removes via `destination-out`, plus the plate's rectangle as fills (a 1px stroke straddles the pixel); 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 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
|
|
482
|
+
- **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 one-pixel erosion by four `destination-in` draws on a frame-sized layer (the frame is transparent around the art, so its edge erodes as the off-canvas edge did), the ring the erosion removes via `destination-out`, plus the plate's rectangle as fills (a 1px stroke straddles the pixel); 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 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 the frame'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`.
|
|
483
483
|
- **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).
|
|
484
484
|
- **An icon alone is a picture; an icon in a field is an option.** `option` is invalid without a `listbox` that owns it — written unconditionally the browser drops the role *and* `aria-selected`, which is how a `selectable` icon reached AT as a bare generic in every configuration the kit shipped. So the role follows the container: `role="option"` + `aria-selected` when a `vf-icon-field` or a `[role="listbox"]` ancestor claims it (`closest('vf-icon-field, [role="listbox"]')` — the field's role lives in internals and never lands as an attribute), otherwise `role="img"` named from `label` (else the art's `alt`), since `img` is not a name-from-content role. Not `button` — that promises Enter/Space activate, while here Return renames and the open route is ⌘O / ⌘↓. Re-derived on every connect, so re-parenting between the two contexts re-grades the icon. **The container for a field of icons is `vf-icon-field`** (below); a consumer's own element carrying `role="listbox"` still owns an icon the same way. `vf-desktop` cannot be it, since it also holds windows and a menu bar and a non-`option` child of a listbox is invalid the same way. Divergence from APG, recorded: its listbox options share one roving tab stop, the kit's stay one stop each.
|
|
485
485
|
- **`selectable` is what makes an icon focusable** — `movable` and `editable` require it, as the Finder did (you cannot move or rename what you have not selected; the rename already opens only on an ALREADY-selected icon). A `movable`-only icon is draggable but not a tab stop, rather than a tab stop that announces nothing.
|
|
@@ -496,7 +496,7 @@ The container a field of `vf-icon`s sits in — a desktop's icons, a folder wind
|
|
|
496
496
|
- **Attributes/props:** `label`: string — the listbox's accessible name, through internals. `size?: 'large' | 'small'` (reflected) — the view's icon size, written onto every icon in the field (descendants, so a wrapper is no barrier) on slotchange and whenever it changes — View → by Small Icon as one attribute; unset, each icon keeps its own. `top`/`left` (`VfPositioned`) and `width`/`height` (`VfSized`), like the other declared-box containers. `fill-width` / `fill-height`, bare attributes read about the host the way `vf-stack` and `vf-container` read them (`:host([fill-width]) { width: 100% }`): 100% of a desktop's screen or a window body (a definite flex item), the field staying static so its icons keep their anchor — what it gains is a surface to press.
|
|
497
497
|
- **Semantics through internals:** `role="listbox"` and `aria-multiselectable="true"` always — the Finder was always multi-select, so there is no attribute for it — so a consumer's own `role`/`aria-*` on the tag wins (SPEC §2). A `selectable` icon inside is an `option` carrying `aria-selected`.
|
|
498
498
|
- **Visual:** none. `display: block; width: fit-content` — an in-flow block with no size, no inset and no position of its own, painting nothing (the `vf-stack` posture, with its typographic-transparency reset). A field whose icons are all placed is a zero-height block, so the icons keep anchoring to the desktop's raster or the window's plane, stored coordinates mean what they meant, and a press on the bare dither still reaches the desktop. Stated `top`/`left` and `width`/`height` make it a box and the anchor its icons place against — the placement working, as documented for everything. No `GridSnapController`: it paints nothing and its icons correct their own origins. Carries a `ScaleController` for a declared size on a plain page (the `vf-stack` reasoning).
|
|
499
|
-
- **The rubber band** — the kit's first piece of a container-level selection model. A `pointerdown` on the host whose `target` is the host itself (nothing slotted was hit; a press on an icon arrives with the icon as target and is the icon's own gesture), button 0, on a field with a box: the pointer is captured on the host, the press's default suppressed (no text selection across the page), and the **anchor** read — the icons selected *as the press found them*; the icons' outside-press listeners are document capture, so they have already run: a plain press cleared everything (an empty anchor), Shift or ⌘ kept the selection. The first move whose point (whole system px from the field's box) differs from the press starts the band; each changed rectangle — press to point, both held inside the band's *reach* — is repainted (`paintSelectionRect`, `open-art.ts`: the drag outline's dotted one-pixel ring at the rectangle's size, a degenerate axis as a one-pixel line, phased on the rectangle's
|
|
499
|
+
- **The rubber band** — the kit's first piece of a container-level selection model. A `pointerdown` on the host whose `target` is the host itself (nothing slotted was hit; a press on an icon arrives with the icon as target and is the icon's own gesture), button 0, on a field with a box: the pointer is captured on the host, the press's default suppressed (no text selection across the page), and the **anchor** read — the icons selected *as the press found them*; the icons' outside-press listeners are document capture, so they have already run: a plain press cleared everything (an empty anchor), Shift or ⌘ kept the selection. The first move whose point (whole system px from the field's box) differs from the press starts the band; each changed rectangle — press to point, both held inside the band's *reach* — is repainted (`paintSelectionRect`, `open-art.ts`: the drag outline's dotted one-pixel ring at the rectangle's size, a degenerate axis as a one-pixel line, phased on the rectangle's corner on the *screen* — the field's offset from the desktop's drag surface, read at the press through the same `vf-drag-surface-request` handshake the outline uses, plus the corner in the field; the field's own box with no desktop — so over the dither the band reads as a black line whatever the field's offset, `penPhase`) and the icons re-read by the Finder's one rule: **a `selectable` icon is selected exactly when it is in the anchor XOR the rectangle touches it** — so a plain band selects what it touches and releases what it leaves, and a Shift band *toggles*: a selected icon it covers deselects while covered and comes back when it leaves, an unselected one selects. "Touches" is the icon's own reading (`vf-icon.touches(box)`: its art cell or its name plate, not the empty cell around narrower art or the frame's margin beside the plate, so a rectangle through the gap between two icons selects neither). Each change goes through `vf-icon.setSelected()` so it fires `vf-select` as a press would; the release keeps the result, a cancel restores the anchor. **The reach** is the field's own box intersected with every ancestor that clips (`overflow` other than `visible` on either axis — a window body, a scroll viewport, the desktop's raster), found by walking the flat tree through assigned slots and shadow hosts, rounded inward to whole system px; so the band never paints outside the region the icons are visible in. **Where it draws:** a canvas in the field's own shadow, `position: fixed` against the viewport (a layout-neutral field has no positioned box to anchor to; the field's box sits on the device grid, so whole system px from it land on the grid too), carrying the drag outline's XOR pen inline (`filter: invert(1)`, `mix-blend-mode: difference`, `image-rendering: pixelated`, `pointer-events: none`). That is the field's own stacking level, which is where the Finder drew its marquee: on the desktop, under the windows (the field precedes them in flow and takes no z-index) and over the dither and icons; in a window body, inside that window's stacking context — over its content, under any window above it — not the desktop's drag surface, which would put a window's marquee over every other window. Escape (a gesture-scoped document capture listener, stopped so a dialog never dismisses) and `pointercancel` cancel: the canvas goes and the selection is put back to the kept set; a release keeps what the band made. `touch-action` is deliberately left alone: a touch drag on a scrolling window's background pans it as before, and the `pointercancel` the browser then sends cancels the band. A field with no box (unplaced, unfilled) receives no press and draws none. `npm run verify:icon-drag` (BAND).
|
|
500
500
|
- **The desktop uses it and renders none.** Desktop icons sit in a field slotted into `vf-desktop` beside the windows, written by the page; a folder window's icons in one slotted into the window body. `vf-desktop` builds no field of its own: its furniture is slotted light DOM and the page decides what it is (the `clearActive` rule), a desktop with no icons should carry no empty listbox, and a field inside the desktop's shadow is one `closest()` could not reach.
|
|
501
501
|
- **Slots:** default (the icons). `npm run verify:icon-drag` (FIELD); `verify:position`'s UNIVERSAL group counts it as the 32nd element.
|
|
502
502
|
|
|
@@ -394,7 +394,7 @@
|
|
|
394
394
|
},
|
|
395
395
|
{
|
|
396
396
|
"name": "vf-icon-field",
|
|
397
|
-
"description": "`<vf-icon-field>` — the container a field of `vf-icon`s sits in: a desktop's\nicons, a folder window's contents.\n\nEvery other set in the kit has its container — `vf-list` owns\n`vf-list-item`, `vf-radio-group` owns `vf-radio`, `vf-menu` owns\n`vf-menu-item`, `vf-select` owns `vf-option` — and `vf-icon` was the one\nitem whose container the consumer wrote by hand: a bare\n`<div role=\"listbox\" aria-multiselectable=\"true\">`, because an `option` is\nonly valid inside a `listbox` that owns it, and every kit container is a\npositioning anchor by design, so nothing in the kit was neutral enough to\nstand in. A consumer who forgot the role got icons that silently degraded\nto pictures. This is that container, as kit furniture:\n\n```html\n<vf-desktop width=\"512\" height=\"342\">\n <vf-icon-field label=\"Desktop\" fill-width fill-height>\n <vf-icon label=\"Macintosh HD\" width=\"64\" selectable movable editable left=\"16\" top=\"24\">…</vf-icon>\n <vf-icon label=\"Trash\" width=\"64\" selectable movable editable left=\"16\" top=\"280\">…</vf-icon>\n </vf-icon-field>\n <vf-window heading=\"Documents\" width=\"320\" height=\"201\" scrollbars=\"both\" top=\"40\" left=\"120\">\n <vf-icon-field label=\"Documents\">\n <vf-icon label=\"Read Me\" width=\"64\" selectable movable editable left=\"16\" top=\"16\">…</vf-icon>\n </vf-icon-field>\n </vf-window>\n</vf-desktop>\n```\n\n### Semantics through internals\n\n`role=\"listbox\"` and `aria-multiselectable=\"true\"` always — the Finder was\nalways multi-select, so there is no attribute for it — and `label` as the\naccessible name, the way `vf-scroll-area` takes one. Internals values are\ndefaults (SPEC §2): a consumer's own `role`/`aria-*` on the tag still wins.\nA `selectable` icon inside becomes a real `option` carrying `aria-selected`;\n`vf-icon` matches this tag directly, since a role written through internals\nnever lands as an attribute for `closest()` to find.\n\n### Layout-neutral, exactly what the div was\n\nAn in-flow block with no size, no inset and no position of its own,\npainting nothing — the `vf-stack` posture. A field whose icons are all\nplaced is a zero-height block, so the icons keep anchoring to the desktop's\nraster or the window's plane, stored coordinates mean what they meant, and\na press on the bare dither still reaches the desktop. It takes `top`/`left`\nand `width`/`height` like the other declared-box containers, so a page that\nwants the field to be a box places and sizes it, at which point it becomes\nthe anchor — the placement working, as documented for everything.\n`fill-width`/`fill-height` are the other way to give it a box, read about\nthe host the way `vf-stack` and `vf-container` read them: a filled field\nstays in flow and static, so its icons keep their anchor, and it has a\nsurface to press — which is what the rubber band needs.\n\n### The rubber band\n\nA press on the field's own background — not on an icon — dragged across\nthe field draws the Finder's dotted selection rectangle from the press to\nthe pointer, and the selection follows it live by the Finder's one rule:\n**an icon is selected exactly when it was selected at the press XOR the\nrectangle touches it.** The press fixes that anchor: a plain press has\nalready cleared everything (the way any press outside an icon does), so\nthe rectangle simply selects what it touches and releases what it leaves;\nwith Shift or ⌘ held the selection survives the press and the rectangle\n*toggles* against it — an icon already selected deselects while the\nrectangle covers it and comes back when it leaves, one that was not\nselects. Each change reports `vf-select` as a press would. \"Touches\"\nmeans the icon's art cell or its name plate (VfIcon.touches), not\nthe empty cell beside them, so a rectangle through the gap between two\nicons selects neither. The release keeps the result; Escape or a\n`pointercancel` cancels, the rectangle vanishes and the selection goes\nback to the anchor. A press that never leaves its system pixel draws\nnothing.\n\nThe rectangle is drawn with the drag outline's pen (* paintSelectionRect: the dotted one-pixel outline, `filter: invert(1)`\nunder `mix-blend-mode: difference`) at the field's own level, which is\nwhere the Finder drew its marquee — on the desktop under the windows, in a\nwindow body over that window's content and under any window above it —\npositioned against the viewport from the field's own shadow, since a\nlayout-neutral field has no box of its own to anchor to. It reaches no\nfurther than the field, inside every clip above the field (a window body,\na scroll viewport, the raster's edge), so it never paints outside the\nregion the icons are visible in. Its dots
|
|
397
|
+
"description": "`<vf-icon-field>` — the container a field of `vf-icon`s sits in: a desktop's\nicons, a folder window's contents.\n\nEvery other set in the kit has its container — `vf-list` owns\n`vf-list-item`, `vf-radio-group` owns `vf-radio`, `vf-menu` owns\n`vf-menu-item`, `vf-select` owns `vf-option` — and `vf-icon` was the one\nitem whose container the consumer wrote by hand: a bare\n`<div role=\"listbox\" aria-multiselectable=\"true\">`, because an `option` is\nonly valid inside a `listbox` that owns it, and every kit container is a\npositioning anchor by design, so nothing in the kit was neutral enough to\nstand in. A consumer who forgot the role got icons that silently degraded\nto pictures. This is that container, as kit furniture:\n\n```html\n<vf-desktop width=\"512\" height=\"342\">\n <vf-icon-field label=\"Desktop\" fill-width fill-height>\n <vf-icon label=\"Macintosh HD\" width=\"64\" selectable movable editable left=\"16\" top=\"24\">…</vf-icon>\n <vf-icon label=\"Trash\" width=\"64\" selectable movable editable left=\"16\" top=\"280\">…</vf-icon>\n </vf-icon-field>\n <vf-window heading=\"Documents\" width=\"320\" height=\"201\" scrollbars=\"both\" top=\"40\" left=\"120\">\n <vf-icon-field label=\"Documents\">\n <vf-icon label=\"Read Me\" width=\"64\" selectable movable editable left=\"16\" top=\"16\">…</vf-icon>\n </vf-icon-field>\n </vf-window>\n</vf-desktop>\n```\n\n### Semantics through internals\n\n`role=\"listbox\"` and `aria-multiselectable=\"true\"` always — the Finder was\nalways multi-select, so there is no attribute for it — and `label` as the\naccessible name, the way `vf-scroll-area` takes one. Internals values are\ndefaults (SPEC §2): a consumer's own `role`/`aria-*` on the tag still wins.\nA `selectable` icon inside becomes a real `option` carrying `aria-selected`;\n`vf-icon` matches this tag directly, since a role written through internals\nnever lands as an attribute for `closest()` to find.\n\n### Layout-neutral, exactly what the div was\n\nAn in-flow block with no size, no inset and no position of its own,\npainting nothing — the `vf-stack` posture. A field whose icons are all\nplaced is a zero-height block, so the icons keep anchoring to the desktop's\nraster or the window's plane, stored coordinates mean what they meant, and\na press on the bare dither still reaches the desktop. It takes `top`/`left`\nand `width`/`height` like the other declared-box containers, so a page that\nwants the field to be a box places and sizes it, at which point it becomes\nthe anchor — the placement working, as documented for everything.\n`fill-width`/`fill-height` are the other way to give it a box, read about\nthe host the way `vf-stack` and `vf-container` read them: a filled field\nstays in flow and static, so its icons keep their anchor, and it has a\nsurface to press — which is what the rubber band needs.\n\n### The rubber band\n\nA press on the field's own background — not on an icon — dragged across\nthe field draws the Finder's dotted selection rectangle from the press to\nthe pointer, and the selection follows it live by the Finder's one rule:\n**an icon is selected exactly when it was selected at the press XOR the\nrectangle touches it.** The press fixes that anchor: a plain press has\nalready cleared everything (the way any press outside an icon does), so\nthe rectangle simply selects what it touches and releases what it leaves;\nwith Shift or ⌘ held the selection survives the press and the rectangle\n*toggles* against it — an icon already selected deselects while the\nrectangle covers it and comes back when it leaves, one that was not\nselects. Each change reports `vf-select` as a press would. \"Touches\"\nmeans the icon's art cell or its name plate (VfIcon.touches), not\nthe empty cell beside them, so a rectangle through the gap between two\nicons selects neither. The release keeps the result; Escape or a\n`pointercancel` cancels, the rectangle vanishes and the selection goes\nback to the anchor. A press that never leaves its system pixel draws\nnothing.\n\nThe rectangle is drawn with the drag outline's pen (* paintSelectionRect: the dotted one-pixel outline, `filter: invert(1)`\nunder `mix-blend-mode: difference`) at the field's own level, which is\nwhere the Finder drew its marquee — on the desktop under the windows, in a\nwindow body over that window's content and under any window above it —\npositioned against the viewport from the field's own shadow, since a\nlayout-neutral field has no box of its own to anchor to. It reaches no\nfurther than the field, inside every clip above the field (a window body,\na scroll viewport, the raster's edge), so it never paints outside the\nregion the icons are visible in. Its dots take the pen's phase on the\n*screen* — the desktop's, found by the `vf-drag-surface-request`\nhandshake the drag outline uses (penPhase) — so over the desktop\ndither the rectangle reads as a black line wherever the field sits; with\nno desktop, the field's own box is the reference.\n\nOnly a field with a box can be pressed: fill it, or place and size it. An\nunplaced, unfilled field of placed icons is zero-height and draws no band.\n`touch-action` is left alone, so on a touch screen a drag on the\nbackground pans a scrolling window as it always did; the band is a mouse\nand pen gesture.\n\n### `size` is the view's\n\n`large` or `small`, written onto every icon in the field when set and\nwhenever it changes, so View → by Small Icon is one attribute instead of a\nloop over every icon. Unset, each icon keeps its own.\n\n### The desktop uses it, and renders none\n\nA desktop's icons sit in a field slotted into `vf-desktop` beside the\nwindows, written by the page; a folder window's icons sit in one slotted\ninto the window body. `vf-desktop` builds no field of its own: its furniture\nis slotted light DOM and the page decides what it is, a desktop with no\nicons should carry no empty listbox, and a field inside the desktop's\nshadow is one `closest()` could not reach.\n---\n\n\n### **Slots:**\n - _default_ - The icons.",
|
|
398
398
|
"attributes": [
|
|
399
399
|
{
|
|
400
400
|
"name": "label",
|
|
@@ -436,7 +436,7 @@
|
|
|
436
436
|
},
|
|
437
437
|
{
|
|
438
438
|
"name": "vf-icon",
|
|
439
|
-
"description": "`<vf-icon>` — a Finder icon: pixel art in a reserved cell with its name on a\nplate below, selectable, movable and renameable in place.\n\nIt is the one composite in the kit whose parts already existed separately —\n`vf-img` draws the art, `vf-list-item` inverts on selection, `vf-window`\ndrags — and the reason to have it anyway is that the *icon* is the unit the\nFinder actually manipulates. A picture and a caption that select together,\nmove together and rename together is a control, not a layout.\n\n### The art arrives by slot\n\nTwo slots, one per resource size — `large` is the 32×32 `ICN#` and `small`\nthe 16×16 `ics#` — each holding a `vf-img` around the consumer's own `<img>`:\n\n```html\n<vf-icon label=\"Macintosh HD\" selectable movable editable>\n <vf-img slot=\"large\"><img src=\"hd-32.png\" alt=\"\"></vf-img>\n <vf-img slot=\"small\"><img src=\"hd-16.png\" alt=\"\"></vf-img>\n</vf-icon>\n```\n\nSlots rather than `src` properties, for the reason `vf-img` exists at all:\nthe kit ships no raster files and never builds an `<img>` on a consumer's\nbehalf, so the graphic stays a real element in the light DOM with its own\n`alt`, `srcset`, loading behavior and asset URLs. A `src` string can express\nnone of that, and it could not hold an inline `<svg>` or a `<canvas>` either.\n`vf-list-item`'s `icon` slot makes the same trade.\n\nThe cost is that both files fetch even though one paints — pay it with two\ndata URIs, or slot only the size that view uses.\n\n### The cell is reserved, the art is registered in it\n\n`size` picks which slot paints *and* the cell it paints in: 32×32 or 16×16,\nheld whether or not there is art for it. A folder is 32×20 of ink and a\ndocument 25×32, and a row of them keeps one baseline only because the cell —\nnot the ink — is the unit, which is what an icon resource always was.\n\n### Everything centered lands on a whole pixel, by parity\n\nThe frame centers two things over one axis — the art cell and the name — and\na centered child sits at `(box − child) / 2`. That is a whole number exactly\nwhen the box and the child have the **same parity**. Half a system pixel is\nwhat fringes 1-bit art: the glyph stems smear across two device columns and\ngo gray, while the plate behind them stays sharp, because backgrounds are\npixel-snapped by the compositor and glyphs are not. A crisp plate under a\ngrey name is the signature of exactly this.\n\nSo the component makes the parities agree instead of correcting afterwards.\nThe cell is 32 or 16, and #measurePlate sizes the name plate to a\nwhole **even** number of system px — measured from the text, rounded up. Both\nchildren are then even, and so is every offset, at every display density and\nwhether or not a `width` is declared. Nothing is snapped, nothing leans on\nthe rasterizer, and `verify:icon` asserts both the geometry and that a field\nof names renders without a single gray pixel — with the kit's normal\nantialiasing left on, since a run on whole pixels gives it nothing to smooth.\n\nYour side of it is one rule: **a declared `width` must be even**, since the\ncomponent cannot round a number you chose. Supply art at the cell size too —\nwhich \"one image pixel is one system pixel\" already implies — because art\ndiffering from its cell by an odd number centers the same way.\n\n### Selection inverts, because the art is a 1-bit mask\n\nA System 7 icon is black ink and opaque white on a transparent surround —\nprecisely an image plus its mask — so inverting it flips ink and fill and\nleaves the surround alone, which is the whole of the classic selected\nappearance. `filter: invert(1)` reproduces it exactly for 1-bit art. Color\nart would invert into a photographic negative, which was never what\nSystem 7 showed: declare `color` and selection **darkens** the art\ninstead — Icon Utilities' ttSelected transform, every color blended\nhalfway toward black (`brightness(0.5)`), whites going gray, the\ntransparent surround still untouched. The declaration is the consumer's\nbecause the component cannot tell color art from a mask by looking at a\nslotted image. It covers whatever the cell shows, the derived open ghost\nincluded, so a color icon has one selected treatment open or closed. The\nlabel plate inverts to the `--vf-highlight` pair either way, sharing one\nselection color with `vf-list-item` — the selected name was inverted\nwhatever the art.\n\n### Open is derived, not shipped\n\nWith `open`, the art redraws as the Finder's open ghost: the outline held\nin solid black, the interior re-filled with the kit's loose 25% dither (the\nscrollbar trough's lattice), the transparent surround untouched. There is\nno second raster and no second fetch — the same alpha channel that makes\nselection an inversion makes the ghost derivable, and `src/open-art.ts`\nderives it from the slotted art by canvas compositing alone. No pixels are\never read back, so a cross-origin image that taints its canvas still works:\ntaint forbids reading, not drawing or displaying.\n\nThe ghost keeps the shape selection expects — ink and opaque white on a\ntransparent surround — so a selected open icon inverts exactly as a closed\none does, with no second treatment. The slot stays in the tree while the\nghost paints, hidden (it is where the art loads, and re-loads, from), and\nart the pipeline cannot draw — nothing slotted yet, a failed load, an\ninline `<svg>` — keeps rendering as itself rather than vanishing behind a\nstate it cannot show.\n\n### `movable`, not `draggable`\n\n`draggable` is a global HTML attribute *and* an `HTMLElement` accessor, so\ndeclaring it would both shadow a platform member and hand the element to the\nbrowser's own drag-and-drop machinery. This is the `align` trap from\n`vf-stack` in a second costume — `align`, `hidden`, `dir`, `draggable` and\n`title` all carry behavior a custom element never asked for. The kit already\nspells this parameter `movable` on `vf-window`, so the icon does too, and it\nmoves the same way: `DragController` tracks the gesture and\n`PlacementController` writes the result into `left`/`top` in whole system px,\nthe art's own unit — the same pair markup places an icon with, so a moved\nicon is still where it was dropped after a zoom.\n\nDragging is a pointer gesture with no keyboard equivalent, which is the kind\nof gap the kit closes rather than inherits (SPEC §1): a focused movable icon\nalso moves under the arrow keys, one system px at a time and eight with\nShift. Focus is what `selectable` grants, so the keyboard half of `movable`\nand `editable` presupposes it — see the role section below.\n\n### The drag is an outline; the icon stays put\n\nThe Finder dragged an icon as a dotted outline — the mask's boundary and\nthe name's rectangle — drawn over everything with the XOR pen, the icon\nitself staying put until the drop. The kit draws it from what it already\nhas: deriveDragOutline finds the ring the way the open ghost does\n(the silhouette from the art's alpha, eroded by one pixel, the ring the\nerosion removes), adds the plate's rectangle, and dotOutline dots\nit against a 2×2 checker — compositing only, so cross-origin art derives\ntoo, and art the pipeline cannot draw falls back to the cell's rectangle.\nThe canvas paints with the cursor's XOR recipe, `filter: invert(1)` under\n`mix-blend-mode: difference`: a dotted black line over a white window\nbody, and over the desktop dither the composition QuickDraw's pattern pen\ngave, because the checker's phase is taken from where the outline lands\non the *screen* — its dots share the dither's own phase wherever it goes.\n\nIt draws on the desktop's drag surface: a layer in the screen's shadow,\nover windows, palettes and the menu bar alike, clipped at the raster's\nedge like everything else, reached through the `vf-drag-surface-request`\nhandshake (bubbles, non-composed — it travels up the slots the icon is\nassigned to, so it works from inside a window body). With no desktop the\ncanvas lives in the icon's own frame instead, translated by the delta,\nclipped by whatever clips the icon; the gesture and its events are the\nsame either way. Its origin is a whole count of system px from the\nsurface's, written as a live `sysLength()`, so a scale change mid-gesture\ncannot smear it.\n\n### The drag reports, the page files\n\nThe kit ships no filing semantics — folders are the page's own catalog —\nso a drag is a stream of events and a cancelable commit, on the icon:\n\n- `vf-drag-start` `{ left, top }` on the first lattice step away from the\n press (a plain click never starts one, and never fires a drop);\n- `vf-drag` `{ clientX, clientY, left, top, x, y }` on every step that\n changed the snapped proposal;\n- `vf-drop`, the same shape at the release, **cancelable**: its default\n action is the move, the proposal written through `left`/`top` and\n clamped whole in the container measured at the press. `preventDefault()`\n writes nothing — the page re-parents or places the icon itself, the\n outline's origin converted with the destination's `placementAt`;\n- `vf-drag-cancel` `{}` on Escape or a `pointercancel`: nothing written.\n\n`left`/`top` in the detail are the *proposal*, unclamped — the origin plus\nthe lattice-snapped delta — and only the default action clamps, so a page\nthat cancels gets the raw gesture. `x`/`y` are the outline's top-left in\nviewport CSS px: the frame's box at the press translated by the delta,\nthe press offset already inside it, so an icon placed at the\ndestination's `placementAt(x, y)` lands exactly where the outline was.\n\n### The selection travels\n\nA drag that begins on a *selected* icon takes every other selected,\nmovable icon of its field with it — the Finder outlined the whole\nselection under a drag on any member. Each follower draws its own outline\n(followDrag), moved by the same lattice delta as the leader's, and\nthe leader's events carry the set as `detail.icons`, itself first. The\ndefault action of the drop clamps the delta once for the whole group —\nthe arrangement is kept, and the group stops when its outermost member\nmeets the container's edge — then writes each member's origin plus that\ndelta. A cancel takes every outline down and writes nothing. A press on\nan unselected icon makes it the selection first (the outside listeners\nclear the others), so it drags alone; a Shift press that deselects the\npressed icon drags it alone too, the rest staying where they are.\n\nFor the selection to travel, a plain press on one of its members must not\ncollapse it: the other icons' outside-press listeners leave a selection\nalone when the press landed on an icon already selected in the same\nfield. A *click* on such an icon — released with no drag — collapses the\nselection to it on the release, so a click still single-selects and the\nsame press could have dragged them all.\n\nEscape is heard by a document listener scoped to the gesture rather than\nby the icon's own key handler, because a `movable`-only icon never takes\nfocus — an addition in the kit's idiom (a pointer-only gesture with no way\nout is the kind of gap SPEC §1 closes), not a claim about the Finder.\n\n`target` is the destination highlight the page sets on the folder under a\ndrag: the selected treatment of the art alone — inverted, or darkened\nunder `color` — with none of `selected`'s semantics: no event, no outside\nlistener, no `aria-selected`, the plate untouched.\n\nOpening gets the same treatment. The double-click is the pointer gesture,\nand its keyboard route is ⌘O / ⌘↓ — the System 7 Open shortcuts, with Ctrl\nstanding in for ⌘ off the Mac. Return is deliberately not one of them: the\nFinder's Return renamed, never opened, so on an editable icon it starts the\nedit and on a non-editable one it does nothing at all.\n\n### The name and the art are one target, and the second click decides\n\nA double-click opens the icon *wherever it lands* — the name is as much the\nicon as the picture is, and the Finder never made you aim at the 32 pixels of\nart. But the name is also where a single click renames, so the two gestures\nbegin with the same press and only the second one tells them apart.\n\nSo the rename waits for it. A press on the plate of an already-selected icon\narms the field rather than opening it, and the next press inside\nRENAME_DELAY_MS calls it off — leaving the double-click to open, with\nno rename box flashing up behind it. Nothing needs to *undo* an edit that\nbegan: the press that starts one and the press that opens are the same\npress, so the only thing that can be got right is not committing early.\n\nThe window is generous in the one direction that is cheap. Reading a lone\nclick as a pair costs a wait before the box appears; reading a pair as a lone\nclick renames when the user asked to open. The same reasoning covers a press\nthat turns into a drag, a press elsewhere, and any key — each calls the\npending rename off, because none of them is the click it is waiting for.\n\n### An icon alone is a picture; an icon in a field is an option\n\n`role=\"option\"` is only meaningful inside a `listbox` that owns it. Written\nunconditionally it is not merely untidy — the browser *drops* it, and\n`aria-selected` with it, so a `selectable` icon announced as a bare generic\nand its selection state reached assistive tech nowhere at all.\n\nSo the role follows the container. Owned, the icon is an `option` that names\nitself from its plate and publishes `aria-selected`. Unowned, it degrades to\n`role=\"img\"` with a name — the same vocabulary the derived open ghost uses,\nand true of what it is. Deliberately not `button`: that would promise Enter\nand Space activate, and here Return *renames* while the open route is ⌘O / ⌘↓.\n\nThe owner is `vf-icon-field`, the container a field of icons sits in, and\nit is what buys the selection state back:\n\n```html\n<vf-icon-field label=\"Desktop\">\n <vf-icon label=\"Macintosh HD\" selectable movable editable>…</vf-icon>\n <vf-icon label=\"Trash\" selectable movable editable>…</vf-icon>\n</vf-icon-field>\n```\n\nA `vf-desktop` cannot be that container itself: it also holds windows and a\nmenu bar, and a non-`option` child of a listbox is invalid the same way the\norphaned option was. The field is layout-neutral — placed icons anchor to\nthe nearest *positioned* ancestor, which is still the desktop's raster. An\nelement of your own carrying `role=\"listbox\"` still owns an icon the same\nway. One divergence from the APG listbox is deliberate: its options share\na single roving tab stop, while these stay one stop each, the way a Finder\nicon is reached on its own.\n\n**`selectable` is what makes an icon focusable**, and `movable`/`editable`\npresuppose it. That is the Finder's own model — you cannot move or rename\nwhat you have not selected — and the pointer path already assumed it: the\nrename opens on a press on the plate of an *already-selected* icon. A\n`movable`-only icon is a picture you can drag, not a widget.\n\n### The label is a property, because it is editable\n\nThe caption is `label` rather than slotted content: renaming means the\ncomponent owns the string and hands it back on `vf-change`, and it cannot own\ntext that lives in the consumer's DOM. An empty `label` draws no plate at\nall — that *is* the \"no label\" parameter, in preference to a second attribute\nthat could disagree with it. `editable` then lets a click on the plate of an\nalready-selected icon open the rename box a moment later, as the Finder's\ndoes (see above), with Return committing, Escape reverting, and the plate\nwidening as you type.\n\n### A name is never abbreviated, and never folded\n\nThere is no ellipsis, no clipping and no wrapping: one line, always. System 7\nsolved the long-name problem at the *other* end — HFS capped a filename at 31\ncharacters — so the Finder could afford to always draw the name in full, and\ndid. A name wider than its cell simply overflows it, centered, the way a name\nwider than a 32-pixel icon always did.\n\nStaying on one line is also what keeps the name on the grid: a single run has\na single measured width, so the even-plate rule above is one number rather\nthan one per line, each with its own parity. `width` is therefore the cell —\nthe grid pitch — not a bound on the name.\n\n`maxlength` (31) bounds the rename field rather than the `label` property:\nthe name belongs to the consumer's model, and truncating one handed to us\nwould lose data.\n\nThe box hugs its text while you type it, not only once you are done, which is\nwhat keeps the name from moving when the edit commits: the plate is the same\nwidth either side of it, so the glyphs stay exactly where they were.\n\nA file also has to be called *something*, so a rename committed empty — or\nas nothing but spaces — is refused rather than applied, and the previous name\ncomes back, which is what System 7 did. An empty `label` is still a perfectly\ngood state to *start* in, though: a freshly made icon has no name until it is\ngiven one. Such an icon draws no plate but stays selectable, focusable and\nrenameable — Return opens a field, and an edit with nothing in it yet is the\none time the box stops hugging and reserves a cell's width, since a field you\ncannot see is one you cannot type into.\n---\n\n\n### **Events:**\n - **vf-select** - Selection changed by user interaction. `detail: { selected: boolean }`.\n- **vf-change** - The name was committed. `detail: { label: string, previous: string }`.\n- **vf-open** - The icon was opened — double-clicked anywhere on it, its name included, or ⌘O / ⌘↓ from the keyboard (Ctrl off the Mac), the System 7 shortcuts. Return renames instead, as the Finder's did. `detail: {}`.\n- **vf-name-too-long** - A rename was typed or pasted past `maxlength`, and the field refused the excess. `detail: { attempted, accepted, limit }` — enough to raise the alert System 7 raised rather than drop the characters silently. Not fired for a `label` set from your own code, which is never truncated in the first place.\n- **vf-name-rejected** - A rename was committed with no name in it, so the edit was dropped and the old name put back. `detail: { attempted, kept, reason: 'empty' }`. A `vf-change` is *not* fired alongside it — nothing changed.\n- **vf-drag-start** - A drag began: the first lattice step away from the press. `detail: { left, top, icons }` — the origin, whole system px in the icon's container, and every icon the drag carries (this one first).\n- **vf-drag** - A step that changed the proposal. `detail: { clientX, clientY, left, top, x, y, icons }` — the pointer in viewport CSS px, the proposed origin in the container (whole system px on the lattice), the outline's top-left in viewport CSS px, and the icons travelling.\n- **vf-drop** - The release; cancelable. The same detail as `vf-drag`. Its default action writes the proposal through `left`/`top` — every icon in `icons` by the same delta, clamped whole in the container as a group; `preventDefault()` writes nothing.\n- **vf-drag-cancel** - The drag was abandoned — Escape, or a `pointercancel` — and nothing was written. `detail: {}`.\n\n### **Slots:**\n - **large** - The 32×32 art, normally a `vf-img` around an `<img>`.\n- **small** - The 16×16 art, shown under `size=\"small\"`.\n\n### **CSS Properties:**\n - **--vf-icon-gap** - Space between the art cell and the name plate _(default: undefined)_\n- **--vf-icon-label-height** - The name plate's line box _(default: undefined)_\n\n### **CSS Parts:**\n - **frame** - The icon and its label plate, stacked.\n- **icon** - The reserved art cell.\n- **label** - The name block; its lines are centered under the art.\n- **plate** - The inked run behind the name, which each wrapped line gets its own of (inverts when selected).\n- **input** - The rename field, while editing.",
|
|
439
|
+
"description": "`<vf-icon>` — a Finder icon: pixel art in a reserved cell with its name on a\nplate below, selectable, movable and renameable in place.\n\nIt is the one composite in the kit whose parts already existed separately —\n`vf-img` draws the art, `vf-list-item` inverts on selection, `vf-window`\ndrags — and the reason to have it anyway is that the *icon* is the unit the\nFinder actually manipulates. A picture and a caption that select together,\nmove together and rename together is a control, not a layout.\n\n### The art arrives by slot\n\nTwo slots, one per resource size — `large` is the 32×32 `ICN#` and `small`\nthe 16×16 `ics#` — each holding a `vf-img` around the consumer's own `<img>`:\n\n```html\n<vf-icon label=\"Macintosh HD\" selectable movable editable>\n <vf-img slot=\"large\"><img src=\"hd-32.png\" alt=\"\"></vf-img>\n <vf-img slot=\"small\"><img src=\"hd-16.png\" alt=\"\"></vf-img>\n</vf-icon>\n```\n\nSlots rather than `src` properties, for the reason `vf-img` exists at all:\nthe kit ships no raster files and never builds an `<img>` on a consumer's\nbehalf, so the graphic stays a real element in the light DOM with its own\n`alt`, `srcset`, loading behavior and asset URLs. A `src` string can express\nnone of that, and it could not hold an inline `<svg>` or a `<canvas>` either.\n`vf-list-item`'s `icon` slot makes the same trade.\n\nThe cost is that both files fetch even though one paints — pay it with two\ndata URIs, or slot only the size that view uses.\n\n### The cell is reserved, the art is registered in it\n\n`size` picks which slot paints *and* the cell it paints in: 32×32 or 16×16,\nheld whether or not there is art for it. A folder is 32×20 of ink and a\ndocument 25×32, and a row of them keeps one baseline only because the cell —\nnot the ink — is the unit, which is what an icon resource always was.\n\n### Everything centered lands on a whole pixel, by parity\n\nThe frame centers two things over one axis — the art cell and the name — and\na centered child sits at `(box − child) / 2`. That is a whole number exactly\nwhen the box and the child have the **same parity**. Half a system pixel is\nwhat fringes 1-bit art: the glyph stems smear across two device columns and\ngo gray, while the plate behind them stays sharp, because backgrounds are\npixel-snapped by the compositor and glyphs are not. A crisp plate under a\ngrey name is the signature of exactly this.\n\nSo the component makes the parities agree instead of correcting afterwards.\nThe cell is 32 or 16, and #measurePlate sizes the name plate to a\nwhole **even** number of system px — measured from the text, rounded up. Both\nchildren are then even, and so is every offset, at every display density and\nwhether or not a `width` is declared. Nothing is snapped, nothing leans on\nthe rasterizer, and `verify:icon` asserts both the geometry and that a field\nof names renders without a single gray pixel — with the kit's normal\nantialiasing left on, since a run on whole pixels gives it nothing to smooth.\n\nYour side of it is one rule: **a declared `width` must be even**, since the\ncomponent cannot round a number you chose. Supply art at the cell size too —\nwhich \"one image pixel is one system pixel\" already implies — because art\ndiffering from its cell by an odd number centers the same way.\n\n### Selection inverts, because the art is a 1-bit mask\n\nA System 7 icon is black ink and opaque white on a transparent surround —\nprecisely an image plus its mask — so inverting it flips ink and fill and\nleaves the surround alone, which is the whole of the classic selected\nappearance. `filter: invert(1)` reproduces it exactly for 1-bit art. Color\nart would invert into a photographic negative, which was never what\nSystem 7 showed: declare `color` and selection **darkens** the art\ninstead — Icon Utilities' ttSelected transform, every color blended\nhalfway toward black (`brightness(0.5)`), whites going gray, the\ntransparent surround still untouched. The declaration is the consumer's\nbecause the component cannot tell color art from a mask by looking at a\nslotted image. It covers whatever the cell shows, the derived open ghost\nincluded, so a color icon has one selected treatment open or closed. The\nlabel plate inverts to the `--vf-highlight` pair either way, sharing one\nselection color with `vf-list-item` — the selected name was inverted\nwhatever the art.\n\n### Open is derived, not shipped\n\nWith `open`, the art redraws as the Finder's open ghost: the outline held\nin solid black, the interior re-filled with the kit's loose 25% dither (the\nscrollbar trough's lattice), the transparent surround untouched. There is\nno second raster and no second fetch — the same alpha channel that makes\nselection an inversion makes the ghost derivable, and `src/open-art.ts`\nderives it from the slotted art by canvas compositing alone. No pixels are\never read back, so a cross-origin image that taints its canvas still works:\ntaint forbids reading, not drawing or displaying.\n\nThe ghost keeps the shape selection expects — ink and opaque white on a\ntransparent surround — so a selected open icon inverts exactly as a closed\none does, with no second treatment. The slot stays in the tree while the\nghost paints, hidden (it is where the art loads, and re-loads, from), and\nart the pipeline cannot draw — nothing slotted yet, a failed load, an\ninline `<svg>` — keeps rendering as itself rather than vanishing behind a\nstate it cannot show.\n\n### `movable`, not `draggable`\n\n`draggable` is a global HTML attribute *and* an `HTMLElement` accessor, so\ndeclaring it would both shadow a platform member and hand the element to the\nbrowser's own drag-and-drop machinery. This is the `align` trap from\n`vf-stack` in a second costume — `align`, `hidden`, `dir`, `draggable` and\n`title` all carry behavior a custom element never asked for. The kit already\nspells this parameter `movable` on `vf-window`, so the icon does too, and it\nmoves the same way: `DragController` tracks the gesture and\n`PlacementController` writes the result into `left`/`top` in whole system px,\nthe art's own unit — the same pair markup places an icon with, so a moved\nicon is still where it was dropped after a zoom.\n\nDragging is a pointer gesture with no keyboard equivalent, which is the kind\nof gap the kit closes rather than inherits (SPEC §1): a focused movable icon\nalso moves under the arrow keys, one system px at a time and eight with\nShift. Focus is what `selectable` grants, so the keyboard half of `movable`\nand `editable` presupposes it — see the role section below.\n\n### The drag is an outline; the icon stays put\n\nThe Finder dragged an icon as a dotted outline — the mask's boundary and\nthe name's rectangle — drawn over everything with the XOR pen, the icon\nitself staying put until the drop. The kit draws it from what it already\nhas: deriveDragOutline finds the ring the way the open ghost does\n(the silhouette from the art's alpha, eroded by one pixel, the ring the\nerosion removes), adds the plate's rectangle, and dotOutline dots\nit against a 2×2 checker — compositing only, so cross-origin art derives\ntoo, and art the pipeline cannot draw falls back to the cell's rectangle.\nThe canvas paints with the cursor's XOR recipe, `filter: invert(1)` under\n`mix-blend-mode: difference`: a dotted black line over a white window\nbody, and over the desktop dither a black line, the composition\nQuickDraw's pattern pen gave — the checker's phase is taken from where\nthe outline lands on the *screen* (penPhase), so its dots fall on\nthe pixels the dither leaves white wherever it goes, and the dither's own\nink fills in between them.\n\nIt draws on the desktop's drag surface: a layer in the screen's shadow,\nover windows, palettes and the menu bar alike, clipped at the raster's\nedge like everything else, reached through the `vf-drag-surface-request`\nhandshake (bubbles, non-composed — it travels up the slots the icon is\nassigned to, so it works from inside a window body). With no desktop the\ncanvas lives in the icon's own frame instead, translated by the delta,\nclipped by whatever clips the icon; the gesture and its events are the\nsame either way. Its origin is a whole count of system px from the\nsurface's, written as a live `sysLength()`, so a scale change mid-gesture\ncannot smear it.\n\n### The drag reports, the page files\n\nThe kit ships no filing semantics — folders are the page's own catalog —\nso a drag is a stream of events and a cancelable commit, on the icon:\n\n- `vf-drag-start` `{ left, top }` on the first lattice step away from the\n press (a plain click never starts one, and never fires a drop);\n- `vf-drag` `{ clientX, clientY, left, top, x, y }` on every step that\n changed the snapped proposal;\n- `vf-drop`, the same shape at the release, **cancelable**: its default\n action is the move, the proposal written through `left`/`top` and\n clamped whole in the container measured at the press. `preventDefault()`\n writes nothing — the page re-parents or places the icon itself, the\n outline's origin converted with the destination's `placementAt`;\n- `vf-drag-cancel` `{}` on Escape or a `pointercancel`: nothing written.\n\n`left`/`top` in the detail are the *proposal*, unclamped — the origin plus\nthe lattice-snapped delta — and only the default action clamps, so a page\nthat cancels gets the raw gesture. `x`/`y` are the outline's top-left in\nviewport CSS px: the frame's box at the press translated by the delta,\nthe press offset already inside it, so an icon placed at the\ndestination's `placementAt(x, y)` lands exactly where the outline was.\n\n### The selection travels\n\nA drag that begins on a *selected* icon takes every other selected,\nmovable icon of its field with it — the Finder outlined the whole\nselection under a drag on any member. Each follower draws its own outline\n(followDrag), moved by the same lattice delta as the leader's, and\nthe leader's events carry the set as `detail.icons`, itself first. The\ndefault action of the drop clamps the delta once for the whole group —\nthe arrangement is kept, and the group stops when its outermost member\nmeets the container's edge — then writes each member's origin plus that\ndelta. A cancel takes every outline down and writes nothing. A press on\nan unselected icon makes it the selection first (the outside listeners\nclear the others), so it drags alone; a Shift press that deselects the\npressed icon drags it alone too, the rest staying where they are.\n\nFor the selection to travel, a plain press on one of its members must not\ncollapse it: the other icons' outside-press listeners leave a selection\nalone when the press landed on an icon already selected in the same\nfield. A *click* on such an icon — released with no drag — collapses the\nselection to it on the release, so a click still single-selects and the\nsame press could have dragged them all.\n\nEscape is heard by a document listener scoped to the gesture rather than\nby the icon's own key handler, because a `movable`-only icon never takes\nfocus — an addition in the kit's idiom (a pointer-only gesture with no way\nout is the kind of gap SPEC §1 closes), not a claim about the Finder.\n\n`target` is the destination highlight the page sets on the folder under a\ndrag: the selected treatment of the art alone — inverted, or darkened\nunder `color` — with none of `selected`'s semantics: no event, no outside\nlistener, no `aria-selected`, the plate untouched.\n\nOpening gets the same treatment. The double-click is the pointer gesture,\nand its keyboard route is ⌘O / ⌘↓ — the System 7 Open shortcuts, with Ctrl\nstanding in for ⌘ off the Mac. Return is deliberately not one of them: the\nFinder's Return renamed, never opened, so on an editable icon it starts the\nedit and on a non-editable one it does nothing at all.\n\n### The name and the art are one target, and the second click decides\n\nA double-click opens the icon *wherever it lands* — the name is as much the\nicon as the picture is, and the Finder never made you aim at the 32 pixels of\nart. But the name is also where a single click renames, so the two gestures\nbegin with the same press and only the second one tells them apart.\n\nSo the rename waits for it. A press on the plate of an already-selected icon\narms the field rather than opening it, and the next press inside\nRENAME_DELAY_MS calls it off — leaving the double-click to open, with\nno rename box flashing up behind it. Nothing needs to *undo* an edit that\nbegan: the press that starts one and the press that opens are the same\npress, so the only thing that can be got right is not committing early.\n\nThe window is generous in the one direction that is cheap. Reading a lone\nclick as a pair costs a wait before the box appears; reading a pair as a lone\nclick renames when the user asked to open. The same reasoning covers a press\nthat turns into a drag, a press elsewhere, and any key — each calls the\npending rename off, because none of them is the click it is waiting for.\n\n### An icon alone is a picture; an icon in a field is an option\n\n`role=\"option\"` is only meaningful inside a `listbox` that owns it. Written\nunconditionally it is not merely untidy — the browser *drops* it, and\n`aria-selected` with it, so a `selectable` icon announced as a bare generic\nand its selection state reached assistive tech nowhere at all.\n\nSo the role follows the container. Owned, the icon is an `option` that names\nitself from its plate and publishes `aria-selected`. Unowned, it degrades to\n`role=\"img\"` with a name — the same vocabulary the derived open ghost uses,\nand true of what it is. Deliberately not `button`: that would promise Enter\nand Space activate, and here Return *renames* while the open route is ⌘O / ⌘↓.\n\nThe owner is `vf-icon-field`, the container a field of icons sits in, and\nit is what buys the selection state back:\n\n```html\n<vf-icon-field label=\"Desktop\">\n <vf-icon label=\"Macintosh HD\" selectable movable editable>…</vf-icon>\n <vf-icon label=\"Trash\" selectable movable editable>…</vf-icon>\n</vf-icon-field>\n```\n\nA `vf-desktop` cannot be that container itself: it also holds windows and a\nmenu bar, and a non-`option` child of a listbox is invalid the same way the\norphaned option was. The field is layout-neutral — placed icons anchor to\nthe nearest *positioned* ancestor, which is still the desktop's raster. An\nelement of your own carrying `role=\"listbox\"` still owns an icon the same\nway. One divergence from the APG listbox is deliberate: its options share\na single roving tab stop, while these stay one stop each, the way a Finder\nicon is reached on its own.\n\n**`selectable` is what makes an icon focusable**, and `movable`/`editable`\npresuppose it. That is the Finder's own model — you cannot move or rename\nwhat you have not selected — and the pointer path already assumed it: the\nrename opens on a press on the plate of an *already-selected* icon. A\n`movable`-only icon is a picture you can drag, not a widget.\n\n### The label is a property, because it is editable\n\nThe caption is `label` rather than slotted content: renaming means the\ncomponent owns the string and hands it back on `vf-change`, and it cannot own\ntext that lives in the consumer's DOM. An empty `label` draws no plate at\nall — that *is* the \"no label\" parameter, in preference to a second attribute\nthat could disagree with it. `editable` then lets a click on the plate of an\nalready-selected icon open the rename box a moment later, as the Finder's\ndoes (see above), with Return committing, Escape reverting, and the plate\nwidening as you type.\n\n### A name is never abbreviated, and never folded\n\nThere is no ellipsis, no clipping and no wrapping: one line, always. System 7\nsolved the long-name problem at the *other* end — HFS capped a filename at 31\ncharacters — so the Finder could afford to always draw the name in full, and\ndid. A name wider than its cell simply overflows it, centered, the way a name\nwider than a 32-pixel icon always did.\n\nStaying on one line is also what keeps the name on the grid: a single run has\na single measured width, so the even-plate rule above is one number rather\nthan one per line, each with its own parity. `width` is therefore the cell —\nthe grid pitch — not a bound on the name.\n\n`maxlength` (31) bounds the rename field rather than the `label` property:\nthe name belongs to the consumer's model, and truncating one handed to us\nwould lose data.\n\nThe box hugs its text while you type it, not only once you are done, which is\nwhat keeps the name from moving when the edit commits: the plate is the same\nwidth either side of it, so the glyphs stay exactly where they were.\n\nA file also has to be called *something*, so a rename committed empty — or\nas nothing but spaces — is refused rather than applied, and the previous name\ncomes back, which is what System 7 did. An empty `label` is still a perfectly\ngood state to *start* in, though: a freshly made icon has no name until it is\ngiven one. Such an icon draws no plate but stays selectable, focusable and\nrenameable — Return opens a field, and an edit with nothing in it yet is the\none time the box stops hugging and reserves a cell's width, since a field you\ncannot see is one you cannot type into.\n---\n\n\n### **Events:**\n - **vf-select** - Selection changed by user interaction. `detail: { selected: boolean }`.\n- **vf-change** - The name was committed. `detail: { label: string, previous: string }`.\n- **vf-open** - The icon was opened — double-clicked anywhere on it, its name included, or ⌘O / ⌘↓ from the keyboard (Ctrl off the Mac), the System 7 shortcuts. Return renames instead, as the Finder's did. `detail: {}`.\n- **vf-name-too-long** - A rename was typed or pasted past `maxlength`, and the field refused the excess. `detail: { attempted, accepted, limit }` — enough to raise the alert System 7 raised rather than drop the characters silently. Not fired for a `label` set from your own code, which is never truncated in the first place.\n- **vf-name-rejected** - A rename was committed with no name in it, so the edit was dropped and the old name put back. `detail: { attempted, kept, reason: 'empty' }`. A `vf-change` is *not* fired alongside it — nothing changed.\n- **vf-drag-start** - A drag began: the first lattice step away from the press. `detail: { left, top, icons }` — the origin, whole system px in the icon's container, and every icon the drag carries (this one first).\n- **vf-drag** - A step that changed the proposal. `detail: { clientX, clientY, left, top, x, y, icons }` — the pointer in viewport CSS px, the proposed origin in the container (whole system px on the lattice), the outline's top-left in viewport CSS px, and the icons travelling.\n- **vf-drop** - The release; cancelable. The same detail as `vf-drag`. Its default action writes the proposal through `left`/`top` — every icon in `icons` by the same delta, clamped whole in the container as a group; `preventDefault()` writes nothing.\n- **vf-drag-cancel** - The drag was abandoned — Escape, or a `pointercancel` — and nothing was written. `detail: {}`.\n\n### **Slots:**\n - **large** - The 32×32 art, normally a `vf-img` around an `<img>`.\n- **small** - The 16×16 art, shown under `size=\"small\"`.\n\n### **CSS Properties:**\n - **--vf-icon-gap** - Space between the art cell and the name plate _(default: undefined)_\n- **--vf-icon-label-height** - The name plate's line box _(default: undefined)_\n\n### **CSS Parts:**\n - **frame** - The icon and its label plate, stacked.\n- **icon** - The reserved art cell.\n- **label** - The name block; its lines are centered under the art.\n- **plate** - The inked run behind the name, which each wrapped line gets its own of (inverts when selected).\n- **input** - The rename field, while editing.",
|
|
440
440
|
"attributes": [
|
|
441
441
|
{
|
|
442
442
|
"name": "label",
|
package/editor/web-types.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
|
|
3
3
|
"name": "vintage-frames",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.2",
|
|
5
5
|
"description-markup": "markdown",
|
|
6
6
|
"contributions": {
|
|
7
7
|
"html": {
|
|
@@ -913,7 +913,7 @@
|
|
|
913
913
|
},
|
|
914
914
|
{
|
|
915
915
|
"name": "vf-icon-field",
|
|
916
|
-
"description": "`<vf-icon-field>` — the container a field of `vf-icon`s sits in: a desktop's\nicons, a folder window's contents.\n\nEvery other set in the kit has its container — `vf-list` owns\n`vf-list-item`, `vf-radio-group` owns `vf-radio`, `vf-menu` owns\n`vf-menu-item`, `vf-select` owns `vf-option` — and `vf-icon` was the one\nitem whose container the consumer wrote by hand: a bare\n`<div role=\"listbox\" aria-multiselectable=\"true\">`, because an `option` is\nonly valid inside a `listbox` that owns it, and every kit container is a\npositioning anchor by design, so nothing in the kit was neutral enough to\nstand in. A consumer who forgot the role got icons that silently degraded\nto pictures. This is that container, as kit furniture:\n\n```html\n<vf-desktop width=\"512\" height=\"342\">\n <vf-icon-field label=\"Desktop\" fill-width fill-height>\n <vf-icon label=\"Macintosh HD\" width=\"64\" selectable movable editable left=\"16\" top=\"24\">…</vf-icon>\n <vf-icon label=\"Trash\" width=\"64\" selectable movable editable left=\"16\" top=\"280\">…</vf-icon>\n </vf-icon-field>\n <vf-window heading=\"Documents\" width=\"320\" height=\"201\" scrollbars=\"both\" top=\"40\" left=\"120\">\n <vf-icon-field label=\"Documents\">\n <vf-icon label=\"Read Me\" width=\"64\" selectable movable editable left=\"16\" top=\"16\">…</vf-icon>\n </vf-icon-field>\n </vf-window>\n</vf-desktop>\n```\n\n### Semantics through internals\n\n`role=\"listbox\"` and `aria-multiselectable=\"true\"` always — the Finder was\nalways multi-select, so there is no attribute for it — and `label` as the\naccessible name, the way `vf-scroll-area` takes one. Internals values are\ndefaults (SPEC §2): a consumer's own `role`/`aria-*` on the tag still wins.\nA `selectable` icon inside becomes a real `option` carrying `aria-selected`;\n`vf-icon` matches this tag directly, since a role written through internals\nnever lands as an attribute for `closest()` to find.\n\n### Layout-neutral, exactly what the div was\n\nAn in-flow block with no size, no inset and no position of its own,\npainting nothing — the `vf-stack` posture. A field whose icons are all\nplaced is a zero-height block, so the icons keep anchoring to the desktop's\nraster or the window's plane, stored coordinates mean what they meant, and\na press on the bare dither still reaches the desktop. It takes `top`/`left`\nand `width`/`height` like the other declared-box containers, so a page that\nwants the field to be a box places and sizes it, at which point it becomes\nthe anchor — the placement working, as documented for everything.\n`fill-width`/`fill-height` are the other way to give it a box, read about\nthe host the way `vf-stack` and `vf-container` read them: a filled field\nstays in flow and static, so its icons keep their anchor, and it has a\nsurface to press — which is what the rubber band needs.\n\n### The rubber band\n\nA press on the field's own background — not on an icon — dragged across\nthe field draws the Finder's dotted selection rectangle from the press to\nthe pointer, and the selection follows it live by the Finder's one rule:\n**an icon is selected exactly when it was selected at the press XOR the\nrectangle touches it.** The press fixes that anchor: a plain press has\nalready cleared everything (the way any press outside an icon does), so\nthe rectangle simply selects what it touches and releases what it leaves;\nwith Shift or ⌘ held the selection survives the press and the rectangle\n*toggles* against it — an icon already selected deselects while the\nrectangle covers it and comes back when it leaves, one that was not\nselects. Each change reports `vf-select` as a press would. \"Touches\"\nmeans the icon's art cell or its name plate (VfIcon.touches), not\nthe empty cell beside them, so a rectangle through the gap between two\nicons selects neither. The release keeps the result; Escape or a\n`pointercancel` cancels, the rectangle vanishes and the selection goes\nback to the anchor. A press that never leaves its system pixel draws\nnothing.\n\nThe rectangle is drawn with the drag outline's pen (* paintSelectionRect: the dotted one-pixel outline, `filter: invert(1)`\nunder `mix-blend-mode: difference`) at the field's own level, which is\nwhere the Finder drew its marquee — on the desktop under the windows, in a\nwindow body over that window's content and under any window above it —\npositioned against the viewport from the field's own shadow, since a\nlayout-neutral field has no box of its own to anchor to. It reaches no\nfurther than the field, inside every clip above the field (a window body,\na scroll viewport, the raster's edge), so it never paints outside the\nregion the icons are visible in. Its dots
|
|
916
|
+
"description": "`<vf-icon-field>` — the container a field of `vf-icon`s sits in: a desktop's\nicons, a folder window's contents.\n\nEvery other set in the kit has its container — `vf-list` owns\n`vf-list-item`, `vf-radio-group` owns `vf-radio`, `vf-menu` owns\n`vf-menu-item`, `vf-select` owns `vf-option` — and `vf-icon` was the one\nitem whose container the consumer wrote by hand: a bare\n`<div role=\"listbox\" aria-multiselectable=\"true\">`, because an `option` is\nonly valid inside a `listbox` that owns it, and every kit container is a\npositioning anchor by design, so nothing in the kit was neutral enough to\nstand in. A consumer who forgot the role got icons that silently degraded\nto pictures. This is that container, as kit furniture:\n\n```html\n<vf-desktop width=\"512\" height=\"342\">\n <vf-icon-field label=\"Desktop\" fill-width fill-height>\n <vf-icon label=\"Macintosh HD\" width=\"64\" selectable movable editable left=\"16\" top=\"24\">…</vf-icon>\n <vf-icon label=\"Trash\" width=\"64\" selectable movable editable left=\"16\" top=\"280\">…</vf-icon>\n </vf-icon-field>\n <vf-window heading=\"Documents\" width=\"320\" height=\"201\" scrollbars=\"both\" top=\"40\" left=\"120\">\n <vf-icon-field label=\"Documents\">\n <vf-icon label=\"Read Me\" width=\"64\" selectable movable editable left=\"16\" top=\"16\">…</vf-icon>\n </vf-icon-field>\n </vf-window>\n</vf-desktop>\n```\n\n### Semantics through internals\n\n`role=\"listbox\"` and `aria-multiselectable=\"true\"` always — the Finder was\nalways multi-select, so there is no attribute for it — and `label` as the\naccessible name, the way `vf-scroll-area` takes one. Internals values are\ndefaults (SPEC §2): a consumer's own `role`/`aria-*` on the tag still wins.\nA `selectable` icon inside becomes a real `option` carrying `aria-selected`;\n`vf-icon` matches this tag directly, since a role written through internals\nnever lands as an attribute for `closest()` to find.\n\n### Layout-neutral, exactly what the div was\n\nAn in-flow block with no size, no inset and no position of its own,\npainting nothing — the `vf-stack` posture. A field whose icons are all\nplaced is a zero-height block, so the icons keep anchoring to the desktop's\nraster or the window's plane, stored coordinates mean what they meant, and\na press on the bare dither still reaches the desktop. It takes `top`/`left`\nand `width`/`height` like the other declared-box containers, so a page that\nwants the field to be a box places and sizes it, at which point it becomes\nthe anchor — the placement working, as documented for everything.\n`fill-width`/`fill-height` are the other way to give it a box, read about\nthe host the way `vf-stack` and `vf-container` read them: a filled field\nstays in flow and static, so its icons keep their anchor, and it has a\nsurface to press — which is what the rubber band needs.\n\n### The rubber band\n\nA press on the field's own background — not on an icon — dragged across\nthe field draws the Finder's dotted selection rectangle from the press to\nthe pointer, and the selection follows it live by the Finder's one rule:\n**an icon is selected exactly when it was selected at the press XOR the\nrectangle touches it.** The press fixes that anchor: a plain press has\nalready cleared everything (the way any press outside an icon does), so\nthe rectangle simply selects what it touches and releases what it leaves;\nwith Shift or ⌘ held the selection survives the press and the rectangle\n*toggles* against it — an icon already selected deselects while the\nrectangle covers it and comes back when it leaves, one that was not\nselects. Each change reports `vf-select` as a press would. \"Touches\"\nmeans the icon's art cell or its name plate (VfIcon.touches), not\nthe empty cell beside them, so a rectangle through the gap between two\nicons selects neither. The release keeps the result; Escape or a\n`pointercancel` cancels, the rectangle vanishes and the selection goes\nback to the anchor. A press that never leaves its system pixel draws\nnothing.\n\nThe rectangle is drawn with the drag outline's pen (* paintSelectionRect: the dotted one-pixel outline, `filter: invert(1)`\nunder `mix-blend-mode: difference`) at the field's own level, which is\nwhere the Finder drew its marquee — on the desktop under the windows, in a\nwindow body over that window's content and under any window above it —\npositioned against the viewport from the field's own shadow, since a\nlayout-neutral field has no box of its own to anchor to. It reaches no\nfurther than the field, inside every clip above the field (a window body,\na scroll viewport, the raster's edge), so it never paints outside the\nregion the icons are visible in. Its dots take the pen's phase on the\n*screen* — the desktop's, found by the `vf-drag-surface-request`\nhandshake the drag outline uses (penPhase) — so over the desktop\ndither the rectangle reads as a black line wherever the field sits; with\nno desktop, the field's own box is the reference.\n\nOnly a field with a box can be pressed: fill it, or place and size it. An\nunplaced, unfilled field of placed icons is zero-height and draws no band.\n`touch-action` is left alone, so on a touch screen a drag on the\nbackground pans a scrolling window as it always did; the band is a mouse\nand pen gesture.\n\n### `size` is the view's\n\n`large` or `small`, written onto every icon in the field when set and\nwhenever it changes, so View → by Small Icon is one attribute instead of a\nloop over every icon. Unset, each icon keeps its own.\n\n### The desktop uses it, and renders none\n\nA desktop's icons sit in a field slotted into `vf-desktop` beside the\nwindows, written by the page; a folder window's icons sit in one slotted\ninto the window body. `vf-desktop` builds no field of its own: its furniture\nis slotted light DOM and the page decides what it is, a desktop with no\nicons should carry no empty listbox, and a field inside the desktop's\nshadow is one `closest()` could not reach.\n---\n\n\n### **Slots:**\n - _default_ - The icons.",
|
|
917
917
|
"doc-url": "",
|
|
918
918
|
"attributes": [
|
|
919
919
|
{
|
|
@@ -999,7 +999,7 @@
|
|
|
999
999
|
},
|
|
1000
1000
|
{
|
|
1001
1001
|
"name": "vf-icon",
|
|
1002
|
-
"description": "`<vf-icon>` — a Finder icon: pixel art in a reserved cell with its name on a\nplate below, selectable, movable and renameable in place.\n\nIt is the one composite in the kit whose parts already existed separately —\n`vf-img` draws the art, `vf-list-item` inverts on selection, `vf-window`\ndrags — and the reason to have it anyway is that the *icon* is the unit the\nFinder actually manipulates. A picture and a caption that select together,\nmove together and rename together is a control, not a layout.\n\n### The art arrives by slot\n\nTwo slots, one per resource size — `large` is the 32×32 `ICN#` and `small`\nthe 16×16 `ics#` — each holding a `vf-img` around the consumer's own `<img>`:\n\n```html\n<vf-icon label=\"Macintosh HD\" selectable movable editable>\n <vf-img slot=\"large\"><img src=\"hd-32.png\" alt=\"\"></vf-img>\n <vf-img slot=\"small\"><img src=\"hd-16.png\" alt=\"\"></vf-img>\n</vf-icon>\n```\n\nSlots rather than `src` properties, for the reason `vf-img` exists at all:\nthe kit ships no raster files and never builds an `<img>` on a consumer's\nbehalf, so the graphic stays a real element in the light DOM with its own\n`alt`, `srcset`, loading behavior and asset URLs. A `src` string can express\nnone of that, and it could not hold an inline `<svg>` or a `<canvas>` either.\n`vf-list-item`'s `icon` slot makes the same trade.\n\nThe cost is that both files fetch even though one paints — pay it with two\ndata URIs, or slot only the size that view uses.\n\n### The cell is reserved, the art is registered in it\n\n`size` picks which slot paints *and* the cell it paints in: 32×32 or 16×16,\nheld whether or not there is art for it. A folder is 32×20 of ink and a\ndocument 25×32, and a row of them keeps one baseline only because the cell —\nnot the ink — is the unit, which is what an icon resource always was.\n\n### Everything centered lands on a whole pixel, by parity\n\nThe frame centers two things over one axis — the art cell and the name — and\na centered child sits at `(box − child) / 2`. That is a whole number exactly\nwhen the box and the child have the **same parity**. Half a system pixel is\nwhat fringes 1-bit art: the glyph stems smear across two device columns and\ngo gray, while the plate behind them stays sharp, because backgrounds are\npixel-snapped by the compositor and glyphs are not. A crisp plate under a\ngrey name is the signature of exactly this.\n\nSo the component makes the parities agree instead of correcting afterwards.\nThe cell is 32 or 16, and #measurePlate sizes the name plate to a\nwhole **even** number of system px — measured from the text, rounded up. Both\nchildren are then even, and so is every offset, at every display density and\nwhether or not a `width` is declared. Nothing is snapped, nothing leans on\nthe rasterizer, and `verify:icon` asserts both the geometry and that a field\nof names renders without a single gray pixel — with the kit's normal\nantialiasing left on, since a run on whole pixels gives it nothing to smooth.\n\nYour side of it is one rule: **a declared `width` must be even**, since the\ncomponent cannot round a number you chose. Supply art at the cell size too —\nwhich \"one image pixel is one system pixel\" already implies — because art\ndiffering from its cell by an odd number centers the same way.\n\n### Selection inverts, because the art is a 1-bit mask\n\nA System 7 icon is black ink and opaque white on a transparent surround —\nprecisely an image plus its mask — so inverting it flips ink and fill and\nleaves the surround alone, which is the whole of the classic selected\nappearance. `filter: invert(1)` reproduces it exactly for 1-bit art. Color\nart would invert into a photographic negative, which was never what\nSystem 7 showed: declare `color` and selection **darkens** the art\ninstead — Icon Utilities' ttSelected transform, every color blended\nhalfway toward black (`brightness(0.5)`), whites going gray, the\ntransparent surround still untouched. The declaration is the consumer's\nbecause the component cannot tell color art from a mask by looking at a\nslotted image. It covers whatever the cell shows, the derived open ghost\nincluded, so a color icon has one selected treatment open or closed. The\nlabel plate inverts to the `--vf-highlight` pair either way, sharing one\nselection color with `vf-list-item` — the selected name was inverted\nwhatever the art.\n\n### Open is derived, not shipped\n\nWith `open`, the art redraws as the Finder's open ghost: the outline held\nin solid black, the interior re-filled with the kit's loose 25% dither (the\nscrollbar trough's lattice), the transparent surround untouched. There is\nno second raster and no second fetch — the same alpha channel that makes\nselection an inversion makes the ghost derivable, and `src/open-art.ts`\nderives it from the slotted art by canvas compositing alone. No pixels are\never read back, so a cross-origin image that taints its canvas still works:\ntaint forbids reading, not drawing or displaying.\n\nThe ghost keeps the shape selection expects — ink and opaque white on a\ntransparent surround — so a selected open icon inverts exactly as a closed\none does, with no second treatment. The slot stays in the tree while the\nghost paints, hidden (it is where the art loads, and re-loads, from), and\nart the pipeline cannot draw — nothing slotted yet, a failed load, an\ninline `<svg>` — keeps rendering as itself rather than vanishing behind a\nstate it cannot show.\n\n### `movable`, not `draggable`\n\n`draggable` is a global HTML attribute *and* an `HTMLElement` accessor, so\ndeclaring it would both shadow a platform member and hand the element to the\nbrowser's own drag-and-drop machinery. This is the `align` trap from\n`vf-stack` in a second costume — `align`, `hidden`, `dir`, `draggable` and\n`title` all carry behavior a custom element never asked for. The kit already\nspells this parameter `movable` on `vf-window`, so the icon does too, and it\nmoves the same way: `DragController` tracks the gesture and\n`PlacementController` writes the result into `left`/`top` in whole system px,\nthe art's own unit — the same pair markup places an icon with, so a moved\nicon is still where it was dropped after a zoom.\n\nDragging is a pointer gesture with no keyboard equivalent, which is the kind\nof gap the kit closes rather than inherits (SPEC §1): a focused movable icon\nalso moves under the arrow keys, one system px at a time and eight with\nShift. Focus is what `selectable` grants, so the keyboard half of `movable`\nand `editable` presupposes it — see the role section below.\n\n### The drag is an outline; the icon stays put\n\nThe Finder dragged an icon as a dotted outline — the mask's boundary and\nthe name's rectangle — drawn over everything with the XOR pen, the icon\nitself staying put until the drop. The kit draws it from what it already\nhas: deriveDragOutline finds the ring the way the open ghost does\n(the silhouette from the art's alpha, eroded by one pixel, the ring the\nerosion removes), adds the plate's rectangle, and dotOutline dots\nit against a 2×2 checker — compositing only, so cross-origin art derives\ntoo, and art the pipeline cannot draw falls back to the cell's rectangle.\nThe canvas paints with the cursor's XOR recipe, `filter: invert(1)` under\n`mix-blend-mode: difference`: a dotted black line over a white window\nbody, and over the desktop dither the composition QuickDraw's pattern pen\ngave, because the checker's phase is taken from where the outline lands\non the *screen* — its dots share the dither's own phase wherever it goes.\n\nIt draws on the desktop's drag surface: a layer in the screen's shadow,\nover windows, palettes and the menu bar alike, clipped at the raster's\nedge like everything else, reached through the `vf-drag-surface-request`\nhandshake (bubbles, non-composed — it travels up the slots the icon is\nassigned to, so it works from inside a window body). With no desktop the\ncanvas lives in the icon's own frame instead, translated by the delta,\nclipped by whatever clips the icon; the gesture and its events are the\nsame either way. Its origin is a whole count of system px from the\nsurface's, written as a live `sysLength()`, so a scale change mid-gesture\ncannot smear it.\n\n### The drag reports, the page files\n\nThe kit ships no filing semantics — folders are the page's own catalog —\nso a drag is a stream of events and a cancelable commit, on the icon:\n\n- `vf-drag-start` `{ left, top }` on the first lattice step away from the\n press (a plain click never starts one, and never fires a drop);\n- `vf-drag` `{ clientX, clientY, left, top, x, y }` on every step that\n changed the snapped proposal;\n- `vf-drop`, the same shape at the release, **cancelable**: its default\n action is the move, the proposal written through `left`/`top` and\n clamped whole in the container measured at the press. `preventDefault()`\n writes nothing — the page re-parents or places the icon itself, the\n outline's origin converted with the destination's `placementAt`;\n- `vf-drag-cancel` `{}` on Escape or a `pointercancel`: nothing written.\n\n`left`/`top` in the detail are the *proposal*, unclamped — the origin plus\nthe lattice-snapped delta — and only the default action clamps, so a page\nthat cancels gets the raw gesture. `x`/`y` are the outline's top-left in\nviewport CSS px: the frame's box at the press translated by the delta,\nthe press offset already inside it, so an icon placed at the\ndestination's `placementAt(x, y)` lands exactly where the outline was.\n\n### The selection travels\n\nA drag that begins on a *selected* icon takes every other selected,\nmovable icon of its field with it — the Finder outlined the whole\nselection under a drag on any member. Each follower draws its own outline\n(followDrag), moved by the same lattice delta as the leader's, and\nthe leader's events carry the set as `detail.icons`, itself first. The\ndefault action of the drop clamps the delta once for the whole group —\nthe arrangement is kept, and the group stops when its outermost member\nmeets the container's edge — then writes each member's origin plus that\ndelta. A cancel takes every outline down and writes nothing. A press on\nan unselected icon makes it the selection first (the outside listeners\nclear the others), so it drags alone; a Shift press that deselects the\npressed icon drags it alone too, the rest staying where they are.\n\nFor the selection to travel, a plain press on one of its members must not\ncollapse it: the other icons' outside-press listeners leave a selection\nalone when the press landed on an icon already selected in the same\nfield. A *click* on such an icon — released with no drag — collapses the\nselection to it on the release, so a click still single-selects and the\nsame press could have dragged them all.\n\nEscape is heard by a document listener scoped to the gesture rather than\nby the icon's own key handler, because a `movable`-only icon never takes\nfocus — an addition in the kit's idiom (a pointer-only gesture with no way\nout is the kind of gap SPEC §1 closes), not a claim about the Finder.\n\n`target` is the destination highlight the page sets on the folder under a\ndrag: the selected treatment of the art alone — inverted, or darkened\nunder `color` — with none of `selected`'s semantics: no event, no outside\nlistener, no `aria-selected`, the plate untouched.\n\nOpening gets the same treatment. The double-click is the pointer gesture,\nand its keyboard route is ⌘O / ⌘↓ — the System 7 Open shortcuts, with Ctrl\nstanding in for ⌘ off the Mac. Return is deliberately not one of them: the\nFinder's Return renamed, never opened, so on an editable icon it starts the\nedit and on a non-editable one it does nothing at all.\n\n### The name and the art are one target, and the second click decides\n\nA double-click opens the icon *wherever it lands* — the name is as much the\nicon as the picture is, and the Finder never made you aim at the 32 pixels of\nart. But the name is also where a single click renames, so the two gestures\nbegin with the same press and only the second one tells them apart.\n\nSo the rename waits for it. A press on the plate of an already-selected icon\narms the field rather than opening it, and the next press inside\nRENAME_DELAY_MS calls it off — leaving the double-click to open, with\nno rename box flashing up behind it. Nothing needs to *undo* an edit that\nbegan: the press that starts one and the press that opens are the same\npress, so the only thing that can be got right is not committing early.\n\nThe window is generous in the one direction that is cheap. Reading a lone\nclick as a pair costs a wait before the box appears; reading a pair as a lone\nclick renames when the user asked to open. The same reasoning covers a press\nthat turns into a drag, a press elsewhere, and any key — each calls the\npending rename off, because none of them is the click it is waiting for.\n\n### An icon alone is a picture; an icon in a field is an option\n\n`role=\"option\"` is only meaningful inside a `listbox` that owns it. Written\nunconditionally it is not merely untidy — the browser *drops* it, and\n`aria-selected` with it, so a `selectable` icon announced as a bare generic\nand its selection state reached assistive tech nowhere at all.\n\nSo the role follows the container. Owned, the icon is an `option` that names\nitself from its plate and publishes `aria-selected`. Unowned, it degrades to\n`role=\"img\"` with a name — the same vocabulary the derived open ghost uses,\nand true of what it is. Deliberately not `button`: that would promise Enter\nand Space activate, and here Return *renames* while the open route is ⌘O / ⌘↓.\n\nThe owner is `vf-icon-field`, the container a field of icons sits in, and\nit is what buys the selection state back:\n\n```html\n<vf-icon-field label=\"Desktop\">\n <vf-icon label=\"Macintosh HD\" selectable movable editable>…</vf-icon>\n <vf-icon label=\"Trash\" selectable movable editable>…</vf-icon>\n</vf-icon-field>\n```\n\nA `vf-desktop` cannot be that container itself: it also holds windows and a\nmenu bar, and a non-`option` child of a listbox is invalid the same way the\norphaned option was. The field is layout-neutral — placed icons anchor to\nthe nearest *positioned* ancestor, which is still the desktop's raster. An\nelement of your own carrying `role=\"listbox\"` still owns an icon the same\nway. One divergence from the APG listbox is deliberate: its options share\na single roving tab stop, while these stay one stop each, the way a Finder\nicon is reached on its own.\n\n**`selectable` is what makes an icon focusable**, and `movable`/`editable`\npresuppose it. That is the Finder's own model — you cannot move or rename\nwhat you have not selected — and the pointer path already assumed it: the\nrename opens on a press on the plate of an *already-selected* icon. A\n`movable`-only icon is a picture you can drag, not a widget.\n\n### The label is a property, because it is editable\n\nThe caption is `label` rather than slotted content: renaming means the\ncomponent owns the string and hands it back on `vf-change`, and it cannot own\ntext that lives in the consumer's DOM. An empty `label` draws no plate at\nall — that *is* the \"no label\" parameter, in preference to a second attribute\nthat could disagree with it. `editable` then lets a click on the plate of an\nalready-selected icon open the rename box a moment later, as the Finder's\ndoes (see above), with Return committing, Escape reverting, and the plate\nwidening as you type.\n\n### A name is never abbreviated, and never folded\n\nThere is no ellipsis, no clipping and no wrapping: one line, always. System 7\nsolved the long-name problem at the *other* end — HFS capped a filename at 31\ncharacters — so the Finder could afford to always draw the name in full, and\ndid. A name wider than its cell simply overflows it, centered, the way a name\nwider than a 32-pixel icon always did.\n\nStaying on one line is also what keeps the name on the grid: a single run has\na single measured width, so the even-plate rule above is one number rather\nthan one per line, each with its own parity. `width` is therefore the cell —\nthe grid pitch — not a bound on the name.\n\n`maxlength` (31) bounds the rename field rather than the `label` property:\nthe name belongs to the consumer's model, and truncating one handed to us\nwould lose data.\n\nThe box hugs its text while you type it, not only once you are done, which is\nwhat keeps the name from moving when the edit commits: the plate is the same\nwidth either side of it, so the glyphs stay exactly where they were.\n\nA file also has to be called *something*, so a rename committed empty — or\nas nothing but spaces — is refused rather than applied, and the previous name\ncomes back, which is what System 7 did. An empty `label` is still a perfectly\ngood state to *start* in, though: a freshly made icon has no name until it is\ngiven one. Such an icon draws no plate but stays selectable, focusable and\nrenameable — Return opens a field, and an edit with nothing in it yet is the\none time the box stops hugging and reserves a cell's width, since a field you\ncannot see is one you cannot type into.\n---\n\n\n### **Events:**\n - **vf-select** - Selection changed by user interaction. `detail: { selected: boolean }`.\n- **vf-change** - The name was committed. `detail: { label: string, previous: string }`.\n- **vf-open** - The icon was opened — double-clicked anywhere on it, its name included, or ⌘O / ⌘↓ from the keyboard (Ctrl off the Mac), the System 7 shortcuts. Return renames instead, as the Finder's did. `detail: {}`.\n- **vf-name-too-long** - A rename was typed or pasted past `maxlength`, and the field refused the excess. `detail: { attempted, accepted, limit }` — enough to raise the alert System 7 raised rather than drop the characters silently. Not fired for a `label` set from your own code, which is never truncated in the first place.\n- **vf-name-rejected** - A rename was committed with no name in it, so the edit was dropped and the old name put back. `detail: { attempted, kept, reason: 'empty' }`. A `vf-change` is *not* fired alongside it — nothing changed.\n- **vf-drag-start** - A drag began: the first lattice step away from the press. `detail: { left, top, icons }` — the origin, whole system px in the icon's container, and every icon the drag carries (this one first).\n- **vf-drag** - A step that changed the proposal. `detail: { clientX, clientY, left, top, x, y, icons }` — the pointer in viewport CSS px, the proposed origin in the container (whole system px on the lattice), the outline's top-left in viewport CSS px, and the icons travelling.\n- **vf-drop** - The release; cancelable. The same detail as `vf-drag`. Its default action writes the proposal through `left`/`top` — every icon in `icons` by the same delta, clamped whole in the container as a group; `preventDefault()` writes nothing.\n- **vf-drag-cancel** - The drag was abandoned — Escape, or a `pointercancel` — and nothing was written. `detail: {}`.\n\n### **Slots:**\n - **large** - The 32×32 art, normally a `vf-img` around an `<img>`.\n- **small** - The 16×16 art, shown under `size=\"small\"`.\n\n### **CSS Properties:**\n - **--vf-icon-gap** - Space between the art cell and the name plate _(default: undefined)_\n- **--vf-icon-label-height** - The name plate's line box _(default: undefined)_\n\n### **CSS Parts:**\n - **frame** - The icon and its label plate, stacked.\n- **icon** - The reserved art cell.\n- **label** - The name block; its lines are centered under the art.\n- **plate** - The inked run behind the name, which each wrapped line gets its own of (inverts when selected).\n- **input** - The rename field, while editing.",
|
|
1002
|
+
"description": "`<vf-icon>` — a Finder icon: pixel art in a reserved cell with its name on a\nplate below, selectable, movable and renameable in place.\n\nIt is the one composite in the kit whose parts already existed separately —\n`vf-img` draws the art, `vf-list-item` inverts on selection, `vf-window`\ndrags — and the reason to have it anyway is that the *icon* is the unit the\nFinder actually manipulates. A picture and a caption that select together,\nmove together and rename together is a control, not a layout.\n\n### The art arrives by slot\n\nTwo slots, one per resource size — `large` is the 32×32 `ICN#` and `small`\nthe 16×16 `ics#` — each holding a `vf-img` around the consumer's own `<img>`:\n\n```html\n<vf-icon label=\"Macintosh HD\" selectable movable editable>\n <vf-img slot=\"large\"><img src=\"hd-32.png\" alt=\"\"></vf-img>\n <vf-img slot=\"small\"><img src=\"hd-16.png\" alt=\"\"></vf-img>\n</vf-icon>\n```\n\nSlots rather than `src` properties, for the reason `vf-img` exists at all:\nthe kit ships no raster files and never builds an `<img>` on a consumer's\nbehalf, so the graphic stays a real element in the light DOM with its own\n`alt`, `srcset`, loading behavior and asset URLs. A `src` string can express\nnone of that, and it could not hold an inline `<svg>` or a `<canvas>` either.\n`vf-list-item`'s `icon` slot makes the same trade.\n\nThe cost is that both files fetch even though one paints — pay it with two\ndata URIs, or slot only the size that view uses.\n\n### The cell is reserved, the art is registered in it\n\n`size` picks which slot paints *and* the cell it paints in: 32×32 or 16×16,\nheld whether or not there is art for it. A folder is 32×20 of ink and a\ndocument 25×32, and a row of them keeps one baseline only because the cell —\nnot the ink — is the unit, which is what an icon resource always was.\n\n### Everything centered lands on a whole pixel, by parity\n\nThe frame centers two things over one axis — the art cell and the name — and\na centered child sits at `(box − child) / 2`. That is a whole number exactly\nwhen the box and the child have the **same parity**. Half a system pixel is\nwhat fringes 1-bit art: the glyph stems smear across two device columns and\ngo gray, while the plate behind them stays sharp, because backgrounds are\npixel-snapped by the compositor and glyphs are not. A crisp plate under a\ngrey name is the signature of exactly this.\n\nSo the component makes the parities agree instead of correcting afterwards.\nThe cell is 32 or 16, and #measurePlate sizes the name plate to a\nwhole **even** number of system px — measured from the text, rounded up. Both\nchildren are then even, and so is every offset, at every display density and\nwhether or not a `width` is declared. Nothing is snapped, nothing leans on\nthe rasterizer, and `verify:icon` asserts both the geometry and that a field\nof names renders without a single gray pixel — with the kit's normal\nantialiasing left on, since a run on whole pixels gives it nothing to smooth.\n\nYour side of it is one rule: **a declared `width` must be even**, since the\ncomponent cannot round a number you chose. Supply art at the cell size too —\nwhich \"one image pixel is one system pixel\" already implies — because art\ndiffering from its cell by an odd number centers the same way.\n\n### Selection inverts, because the art is a 1-bit mask\n\nA System 7 icon is black ink and opaque white on a transparent surround —\nprecisely an image plus its mask — so inverting it flips ink and fill and\nleaves the surround alone, which is the whole of the classic selected\nappearance. `filter: invert(1)` reproduces it exactly for 1-bit art. Color\nart would invert into a photographic negative, which was never what\nSystem 7 showed: declare `color` and selection **darkens** the art\ninstead — Icon Utilities' ttSelected transform, every color blended\nhalfway toward black (`brightness(0.5)`), whites going gray, the\ntransparent surround still untouched. The declaration is the consumer's\nbecause the component cannot tell color art from a mask by looking at a\nslotted image. It covers whatever the cell shows, the derived open ghost\nincluded, so a color icon has one selected treatment open or closed. The\nlabel plate inverts to the `--vf-highlight` pair either way, sharing one\nselection color with `vf-list-item` — the selected name was inverted\nwhatever the art.\n\n### Open is derived, not shipped\n\nWith `open`, the art redraws as the Finder's open ghost: the outline held\nin solid black, the interior re-filled with the kit's loose 25% dither (the\nscrollbar trough's lattice), the transparent surround untouched. There is\nno second raster and no second fetch — the same alpha channel that makes\nselection an inversion makes the ghost derivable, and `src/open-art.ts`\nderives it from the slotted art by canvas compositing alone. No pixels are\never read back, so a cross-origin image that taints its canvas still works:\ntaint forbids reading, not drawing or displaying.\n\nThe ghost keeps the shape selection expects — ink and opaque white on a\ntransparent surround — so a selected open icon inverts exactly as a closed\none does, with no second treatment. The slot stays in the tree while the\nghost paints, hidden (it is where the art loads, and re-loads, from), and\nart the pipeline cannot draw — nothing slotted yet, a failed load, an\ninline `<svg>` — keeps rendering as itself rather than vanishing behind a\nstate it cannot show.\n\n### `movable`, not `draggable`\n\n`draggable` is a global HTML attribute *and* an `HTMLElement` accessor, so\ndeclaring it would both shadow a platform member and hand the element to the\nbrowser's own drag-and-drop machinery. This is the `align` trap from\n`vf-stack` in a second costume — `align`, `hidden`, `dir`, `draggable` and\n`title` all carry behavior a custom element never asked for. The kit already\nspells this parameter `movable` on `vf-window`, so the icon does too, and it\nmoves the same way: `DragController` tracks the gesture and\n`PlacementController` writes the result into `left`/`top` in whole system px,\nthe art's own unit — the same pair markup places an icon with, so a moved\nicon is still where it was dropped after a zoom.\n\nDragging is a pointer gesture with no keyboard equivalent, which is the kind\nof gap the kit closes rather than inherits (SPEC §1): a focused movable icon\nalso moves under the arrow keys, one system px at a time and eight with\nShift. Focus is what `selectable` grants, so the keyboard half of `movable`\nand `editable` presupposes it — see the role section below.\n\n### The drag is an outline; the icon stays put\n\nThe Finder dragged an icon as a dotted outline — the mask's boundary and\nthe name's rectangle — drawn over everything with the XOR pen, the icon\nitself staying put until the drop. The kit draws it from what it already\nhas: deriveDragOutline finds the ring the way the open ghost does\n(the silhouette from the art's alpha, eroded by one pixel, the ring the\nerosion removes), adds the plate's rectangle, and dotOutline dots\nit against a 2×2 checker — compositing only, so cross-origin art derives\ntoo, and art the pipeline cannot draw falls back to the cell's rectangle.\nThe canvas paints with the cursor's XOR recipe, `filter: invert(1)` under\n`mix-blend-mode: difference`: a dotted black line over a white window\nbody, and over the desktop dither a black line, the composition\nQuickDraw's pattern pen gave — the checker's phase is taken from where\nthe outline lands on the *screen* (penPhase), so its dots fall on\nthe pixels the dither leaves white wherever it goes, and the dither's own\nink fills in between them.\n\nIt draws on the desktop's drag surface: a layer in the screen's shadow,\nover windows, palettes and the menu bar alike, clipped at the raster's\nedge like everything else, reached through the `vf-drag-surface-request`\nhandshake (bubbles, non-composed — it travels up the slots the icon is\nassigned to, so it works from inside a window body). With no desktop the\ncanvas lives in the icon's own frame instead, translated by the delta,\nclipped by whatever clips the icon; the gesture and its events are the\nsame either way. Its origin is a whole count of system px from the\nsurface's, written as a live `sysLength()`, so a scale change mid-gesture\ncannot smear it.\n\n### The drag reports, the page files\n\nThe kit ships no filing semantics — folders are the page's own catalog —\nso a drag is a stream of events and a cancelable commit, on the icon:\n\n- `vf-drag-start` `{ left, top }` on the first lattice step away from the\n press (a plain click never starts one, and never fires a drop);\n- `vf-drag` `{ clientX, clientY, left, top, x, y }` on every step that\n changed the snapped proposal;\n- `vf-drop`, the same shape at the release, **cancelable**: its default\n action is the move, the proposal written through `left`/`top` and\n clamped whole in the container measured at the press. `preventDefault()`\n writes nothing — the page re-parents or places the icon itself, the\n outline's origin converted with the destination's `placementAt`;\n- `vf-drag-cancel` `{}` on Escape or a `pointercancel`: nothing written.\n\n`left`/`top` in the detail are the *proposal*, unclamped — the origin plus\nthe lattice-snapped delta — and only the default action clamps, so a page\nthat cancels gets the raw gesture. `x`/`y` are the outline's top-left in\nviewport CSS px: the frame's box at the press translated by the delta,\nthe press offset already inside it, so an icon placed at the\ndestination's `placementAt(x, y)` lands exactly where the outline was.\n\n### The selection travels\n\nA drag that begins on a *selected* icon takes every other selected,\nmovable icon of its field with it — the Finder outlined the whole\nselection under a drag on any member. Each follower draws its own outline\n(followDrag), moved by the same lattice delta as the leader's, and\nthe leader's events carry the set as `detail.icons`, itself first. The\ndefault action of the drop clamps the delta once for the whole group —\nthe arrangement is kept, and the group stops when its outermost member\nmeets the container's edge — then writes each member's origin plus that\ndelta. A cancel takes every outline down and writes nothing. A press on\nan unselected icon makes it the selection first (the outside listeners\nclear the others), so it drags alone; a Shift press that deselects the\npressed icon drags it alone too, the rest staying where they are.\n\nFor the selection to travel, a plain press on one of its members must not\ncollapse it: the other icons' outside-press listeners leave a selection\nalone when the press landed on an icon already selected in the same\nfield. A *click* on such an icon — released with no drag — collapses the\nselection to it on the release, so a click still single-selects and the\nsame press could have dragged them all.\n\nEscape is heard by a document listener scoped to the gesture rather than\nby the icon's own key handler, because a `movable`-only icon never takes\nfocus — an addition in the kit's idiom (a pointer-only gesture with no way\nout is the kind of gap SPEC §1 closes), not a claim about the Finder.\n\n`target` is the destination highlight the page sets on the folder under a\ndrag: the selected treatment of the art alone — inverted, or darkened\nunder `color` — with none of `selected`'s semantics: no event, no outside\nlistener, no `aria-selected`, the plate untouched.\n\nOpening gets the same treatment. The double-click is the pointer gesture,\nand its keyboard route is ⌘O / ⌘↓ — the System 7 Open shortcuts, with Ctrl\nstanding in for ⌘ off the Mac. Return is deliberately not one of them: the\nFinder's Return renamed, never opened, so on an editable icon it starts the\nedit and on a non-editable one it does nothing at all.\n\n### The name and the art are one target, and the second click decides\n\nA double-click opens the icon *wherever it lands* — the name is as much the\nicon as the picture is, and the Finder never made you aim at the 32 pixels of\nart. But the name is also where a single click renames, so the two gestures\nbegin with the same press and only the second one tells them apart.\n\nSo the rename waits for it. A press on the plate of an already-selected icon\narms the field rather than opening it, and the next press inside\nRENAME_DELAY_MS calls it off — leaving the double-click to open, with\nno rename box flashing up behind it. Nothing needs to *undo* an edit that\nbegan: the press that starts one and the press that opens are the same\npress, so the only thing that can be got right is not committing early.\n\nThe window is generous in the one direction that is cheap. Reading a lone\nclick as a pair costs a wait before the box appears; reading a pair as a lone\nclick renames when the user asked to open. The same reasoning covers a press\nthat turns into a drag, a press elsewhere, and any key — each calls the\npending rename off, because none of them is the click it is waiting for.\n\n### An icon alone is a picture; an icon in a field is an option\n\n`role=\"option\"` is only meaningful inside a `listbox` that owns it. Written\nunconditionally it is not merely untidy — the browser *drops* it, and\n`aria-selected` with it, so a `selectable` icon announced as a bare generic\nand its selection state reached assistive tech nowhere at all.\n\nSo the role follows the container. Owned, the icon is an `option` that names\nitself from its plate and publishes `aria-selected`. Unowned, it degrades to\n`role=\"img\"` with a name — the same vocabulary the derived open ghost uses,\nand true of what it is. Deliberately not `button`: that would promise Enter\nand Space activate, and here Return *renames* while the open route is ⌘O / ⌘↓.\n\nThe owner is `vf-icon-field`, the container a field of icons sits in, and\nit is what buys the selection state back:\n\n```html\n<vf-icon-field label=\"Desktop\">\n <vf-icon label=\"Macintosh HD\" selectable movable editable>…</vf-icon>\n <vf-icon label=\"Trash\" selectable movable editable>…</vf-icon>\n</vf-icon-field>\n```\n\nA `vf-desktop` cannot be that container itself: it also holds windows and a\nmenu bar, and a non-`option` child of a listbox is invalid the same way the\norphaned option was. The field is layout-neutral — placed icons anchor to\nthe nearest *positioned* ancestor, which is still the desktop's raster. An\nelement of your own carrying `role=\"listbox\"` still owns an icon the same\nway. One divergence from the APG listbox is deliberate: its options share\na single roving tab stop, while these stay one stop each, the way a Finder\nicon is reached on its own.\n\n**`selectable` is what makes an icon focusable**, and `movable`/`editable`\npresuppose it. That is the Finder's own model — you cannot move or rename\nwhat you have not selected — and the pointer path already assumed it: the\nrename opens on a press on the plate of an *already-selected* icon. A\n`movable`-only icon is a picture you can drag, not a widget.\n\n### The label is a property, because it is editable\n\nThe caption is `label` rather than slotted content: renaming means the\ncomponent owns the string and hands it back on `vf-change`, and it cannot own\ntext that lives in the consumer's DOM. An empty `label` draws no plate at\nall — that *is* the \"no label\" parameter, in preference to a second attribute\nthat could disagree with it. `editable` then lets a click on the plate of an\nalready-selected icon open the rename box a moment later, as the Finder's\ndoes (see above), with Return committing, Escape reverting, and the plate\nwidening as you type.\n\n### A name is never abbreviated, and never folded\n\nThere is no ellipsis, no clipping and no wrapping: one line, always. System 7\nsolved the long-name problem at the *other* end — HFS capped a filename at 31\ncharacters — so the Finder could afford to always draw the name in full, and\ndid. A name wider than its cell simply overflows it, centered, the way a name\nwider than a 32-pixel icon always did.\n\nStaying on one line is also what keeps the name on the grid: a single run has\na single measured width, so the even-plate rule above is one number rather\nthan one per line, each with its own parity. `width` is therefore the cell —\nthe grid pitch — not a bound on the name.\n\n`maxlength` (31) bounds the rename field rather than the `label` property:\nthe name belongs to the consumer's model, and truncating one handed to us\nwould lose data.\n\nThe box hugs its text while you type it, not only once you are done, which is\nwhat keeps the name from moving when the edit commits: the plate is the same\nwidth either side of it, so the glyphs stay exactly where they were.\n\nA file also has to be called *something*, so a rename committed empty — or\nas nothing but spaces — is refused rather than applied, and the previous name\ncomes back, which is what System 7 did. An empty `label` is still a perfectly\ngood state to *start* in, though: a freshly made icon has no name until it is\ngiven one. Such an icon draws no plate but stays selectable, focusable and\nrenameable — Return opens a field, and an edit with nothing in it yet is the\none time the box stops hugging and reserves a cell's width, since a field you\ncannot see is one you cannot type into.\n---\n\n\n### **Events:**\n - **vf-select** - Selection changed by user interaction. `detail: { selected: boolean }`.\n- **vf-change** - The name was committed. `detail: { label: string, previous: string }`.\n- **vf-open** - The icon was opened — double-clicked anywhere on it, its name included, or ⌘O / ⌘↓ from the keyboard (Ctrl off the Mac), the System 7 shortcuts. Return renames instead, as the Finder's did. `detail: {}`.\n- **vf-name-too-long** - A rename was typed or pasted past `maxlength`, and the field refused the excess. `detail: { attempted, accepted, limit }` — enough to raise the alert System 7 raised rather than drop the characters silently. Not fired for a `label` set from your own code, which is never truncated in the first place.\n- **vf-name-rejected** - A rename was committed with no name in it, so the edit was dropped and the old name put back. `detail: { attempted, kept, reason: 'empty' }`. A `vf-change` is *not* fired alongside it — nothing changed.\n- **vf-drag-start** - A drag began: the first lattice step away from the press. `detail: { left, top, icons }` — the origin, whole system px in the icon's container, and every icon the drag carries (this one first).\n- **vf-drag** - A step that changed the proposal. `detail: { clientX, clientY, left, top, x, y, icons }` — the pointer in viewport CSS px, the proposed origin in the container (whole system px on the lattice), the outline's top-left in viewport CSS px, and the icons travelling.\n- **vf-drop** - The release; cancelable. The same detail as `vf-drag`. Its default action writes the proposal through `left`/`top` — every icon in `icons` by the same delta, clamped whole in the container as a group; `preventDefault()` writes nothing.\n- **vf-drag-cancel** - The drag was abandoned — Escape, or a `pointercancel` — and nothing was written. `detail: {}`.\n\n### **Slots:**\n - **large** - The 32×32 art, normally a `vf-img` around an `<img>`.\n- **small** - The 16×16 art, shown under `size=\"small\"`.\n\n### **CSS Properties:**\n - **--vf-icon-gap** - Space between the art cell and the name plate _(default: undefined)_\n- **--vf-icon-label-height** - The name plate's line box _(default: undefined)_\n\n### **CSS Parts:**\n - **frame** - The icon and its label plate, stacked.\n- **icon** - The reserved art cell.\n- **label** - The name block; its lines are centered under the art.\n- **plate** - The inked run behind the name, which each wrapped line gets its own of (inverts when selected).\n- **input** - The rename field, while editing.",
|
|
1003
1003
|
"doc-url": "",
|
|
1004
1004
|
"attributes": [
|
|
1005
1005
|
{
|