vintage-frames 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/custom-elements.json +96 -7
  3. package/dist/build/app-file.d.ts +2 -2
  4. package/dist/build/app-file.js +1 -0
  5. package/dist/build/box.d.ts +3 -1
  6. package/dist/build/box.js +6 -3
  7. package/dist/build/frame.js +19 -17
  8. package/dist/build/plugins.d.ts +5 -4
  9. package/dist/build/plugins.js +69 -64
  10. package/dist/components/vf-icon.d.ts +7 -0
  11. package/dist/components/vf-icon.js +101 -81
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.js +15 -14
  14. package/dist/modal-dialog.d.ts +7 -6
  15. package/dist/modal-dialog.js +30 -22
  16. package/dist/shell/app-file.d.ts +43 -5
  17. package/dist/shell/app-file.js +90 -62
  18. package/dist/shell/catalog.d.ts +41 -1
  19. package/dist/shell/catalog.js +174 -153
  20. package/dist/shell/finder.d.ts +27 -9
  21. package/dist/shell/finder.js +335 -277
  22. package/dist/shell/geometry.d.ts +7 -0
  23. package/dist/shell/geometry.js +19 -3
  24. package/dist/shell/index.d.ts +4 -4
  25. package/dist/shell/index.js +12 -12
  26. package/dist/shell/pure.d.ts +1 -1
  27. package/dist/shell/pure.js +6 -6
  28. package/dist/shell/shell.d.ts +14 -10
  29. package/dist/shell/shell.js +144 -117
  30. package/dist/shell/version.d.ts +1 -1
  31. package/dist/shell/version.js +1 -1
  32. package/dist/styles/body-italic-font.d.ts +2 -0
  33. package/dist/styles/body-italic-font.js +10 -0
  34. package/dist/styles/register-embedded-font.d.ts +5 -4
  35. package/dist/styles/register-embedded-font.js +11 -10
  36. package/docs/SPEC.md +2 -2
  37. package/editor/vscode.html-custom-data.json +5 -0
  38. package/editor/web-types.json +11 -1
  39. package/package.json +7 -3
@@ -0,0 +1,10 @@
1
+ import { PIXEL_GRID_METRICS as e, registerEmbeddedFont as t } from "./register-embedded-font.js";
2
+ import { VF_BODY_FAMILY as n } from "./body-font.js";
3
+ //#region src/styles/body-italic-font.ts
4
+ var r = "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";
5
+ function i() {
6
+ t(n, r, e, "italic");
7
+ }
8
+ i();
9
+ //#endregion
10
+ export { i as registerBodyItalicFace };
@@ -53,12 +53,13 @@ export interface EmbeddedFontMetrics {
53
53
  */
54
54
  export declare const PIXEL_GRID_METRICS: EmbeddedFontMetrics;
55
55
  /**
56
- * Register `family` from an inlined base64 WOFF2 exactly once per session.
56
+ * Register `family` from an inlined base64 WOFF2 exactly once per session —
57
+ * its `style` face, `normal` unless the WOFF2 is the family's italic.
57
58
  *
58
59
  * Idempotent and environment-safe: it no-ops under SSR (no `document`), when
59
- * the CSS Font Loading API is unavailable, or when the family is already
60
- * present (duplicate bundles / HMR). A failed decode releases the family so a
60
+ * the CSS Font Loading API is unavailable, or when the face is already
61
+ * present (duplicate bundles / HMR). A failed decode releases the face so a
61
62
  * later explicit call can retry, and on any failure components simply keep the
62
63
  * fallback stack — so this can never break rendering.
63
64
  */
64
- export declare function registerEmbeddedFont(family: string, woff2Base64: string, metrics?: EmbeddedFontMetrics): void;
65
+ export declare function registerEmbeddedFont(family: string, woff2Base64: string, metrics?: EmbeddedFontMetrics, style?: 'normal' | 'italic'): void;
@@ -4,26 +4,27 @@ var e = /* @__PURE__ */ new Set(), t = {
4
4
  descentOverride: "25%",
5
5
  lineGapOverride: "0%"
6
6
  };
7
- function n(t, n, r) {
8
- if (e.has(t) || typeof document > "u" || !("fonts" in document)) return;
9
- for (let n of document.fonts) if (n.family === t) {
10
- e.add(t);
7
+ function n(t, n, r, i = "normal") {
8
+ let a = `${t} ${i}`;
9
+ if (e.has(a) || typeof document > "u" || !("fonts" in document)) return;
10
+ for (let n of document.fonts) if (n.family === t && n.style === i) {
11
+ e.add(a);
11
12
  return;
12
13
  }
13
- e.add(t);
14
- let i = `url(data:font/woff2;base64,${n}) format('woff2')`;
14
+ e.add(a);
15
+ let o = `url(data:font/woff2;base64,${n}) format('woff2')`;
15
16
  try {
16
- let n = new FontFace(t, i, {
17
- style: "normal",
17
+ let n = new FontFace(t, o, {
18
+ style: i,
18
19
  weight: "100 900",
19
20
  display: "swap",
20
21
  ...r
21
22
  });
22
23
  document.fonts.add(n), n.load().then(() => {}, () => {
23
- document.fonts.delete(n), e.delete(t);
24
+ document.fonts.delete(n), e.delete(a);
24
25
  });
25
26
  } catch {
26
- e.delete(t);
27
+ e.delete(a);
27
28
  }
28
29
  }
29
30
  //#endregion
package/docs/SPEC.md CHANGED
@@ -234,7 +234,7 @@ The desktop-window shell: the classic document window (see DragThing screenshot)
234
234
  #### `vf-dialog` (`VfDialog`, vf-dialog.ts)
235
235
  The modal-dialog shell: movable modal by default (the dBoxProc double frame with the striped title bar set into it — traced from a 2× System 7 capture of a movable modal), the modal dialog box with `frame="plain"` (the bare double frame; see the Group A recipe table).
236
236
  - **Attributes/props:** `open: boolean` (reflect), `heading: string`, `top: number` / `left: number` (whole system px, in **viewport** coordinates — see §1 Explicit placement; unset means centered), `width: number` / `height: number` (**declare them both** — whole system px, the same fixed box `vf-window` is. A native `<dialog>` is `width: fit-content` measured against the space left beside its own offsets, and stating an offset is how the movable modal is positioned, so an undeclared dialog squeezes itself and reflows as it is dragged toward an edge. The two fall back differently — width to 260 system px, height to the content — and it names whichever are missing, once, on the open that first shows it), `label: string` (accessible name for a dialog with no `heading`), `closable: boolean` (default **false** — the bare movable-modal bar; the close box is opt-in because the HIG's Chapter 6 text denies a movable modal one while its Figure 5-1 grants it — the parameter enables either reading), `light-dismiss: boolean` (`lightDismiss`; default **false** — a click outside the frame closes the dialog with `vf-close` reason `'outside'`. Off by default because the classic modal ignored an outside click, and a dialog that asks a question should keep ignoring it; opt in for the About box and the splash, which the classic Mac did dismiss on a click. Reflected, and read at the release, so it can be toggled on an open dialog), `frame?: 'plain'`, `returnValue: string` (property only, as on `<dialog>`: the dialog's answer, cleared on each open).
237
- - **Implementation:** wraps a native `<dialog>` (for top-layer + focus trap). `show()` → `showModal()`; `close()` closes. Keep `open` attr in sync both directions. Drag the title bar to move it (shared `DragController` with `vf-window`), stating `top`/`left` in system px; drags starting on the close widget are ignored (same composedPath guard as `vf-window`). Escape → close + `vf-close` detail `{ reason: 'escape' }`; close box/programmatic/close() → `{ reason: 'close' }`; a click outside under `light-dismiss` → `{ reason: 'outside' }`. **Answers** (`VfModalDialog`): `close(value)` closes with `value` as `returnValue`, and a `<form method="dialog">` inside the dialog closes it on submit with the submitter's `value`, a submitter's `formmethod="dialog"` too. The platform's own rule looks for a `<dialog>` around the form, and this one is in the shadow root, so the base listens for `submit` on the host and closes at the end of the submit's path (`deferActivation`), not at all if anything cancelled it. `vf-button`'s submit proxy carries its `value` with or without a `name`. `vf-close`'s detail carries `returnValue`: the value that close carried, or null (Escape, a click outside, the close box, `close()` alone, removal). Return in a text field runs the form's implicit submission through the ringed `variant="default"` submit button when the form has one, ahead of the first in tree order, so a Cancel/OK row answers OK (`requestImplicitSubmit`). `npm run verify:dialog`, ANSWERS. No backdrop dimming: `::backdrop { background: transparent; }`. **Light dismiss is target identity, not geometry** (`VfModalDialog`): the `::backdrop` hit-tests as its `<dialog>`, and the frame fills the dialog's box, so the dialog is a pointer event's *target* exactly when the pointer was outside the frame — the listeners sit on the `<dialog>` itself, attached while open. It takes both halves of the click: a `pointerdown` on the backdrop arms (any other press disarms, so a right-click macOS never releases can't leave a stale arm), and a `pointerup` on the backdrop with the same `pointerId` closes — the platform's own `closedby="any"` two-step, so a press that starts on a control and slides off, or a title-bar drag released past the frame, leaves the dialog open. Deliberately not the `click` event: UI Events dispatches a press-drag-release click at the common ancestor of the two targets, which for a press on the frame released outside is the dialog itself. The platform's `closedby` attribute is not used because its dismissal arrives as `cancel`, which the base already reads as Escape. The press is consumed either way — a modal's backdrop lets nothing beneath it see the click — which is what lets a page dismiss its About box without also selecting the icon under the pointer. `npm run verify:dialog`. **Keyboard: the Dialog Manager's two rules** (`VfModalDialog`). *Initial focus:* right after `showModal()`'s own focusing steps, focus is handed to `initialFocusTarget` — the first slotted control carrying `autofocus` or `autoselect`, else the first enabled text-entry control (`vf-text-field`, `vf-number-field`, `vf-text-area`, or a native text input/textarea), else the default button (`defaultButton`: the first enabled `vf-button[variant="default"]`, looked for among the slotted content and then the shadow tree), else nothing, leaving the browser's pick. A field marked `autoselect` has its whole text selected on every open, right after `vf-show` and once the field has rendered, so a value set before `show()` or in a `vf-show` handler is the text selected. Otherwise the modal selects no text: whether a field opens selected is the page's call. The classic dialog put the insertion point in its first text item and gave nothing else focus; the web needs a focused control, and the default button is the one that makes Return and Space work without a Tab. Left alone, `showModal()` focuses the first focusable thing in flat-tree order — Cancel in a Cancel/OK row, or a link in the body — so Return pressed the wrong thing. Both getters are `protected`, for a consumer's own modal to override. *Return/Enter:* a `keydown` listener on the `<dialog>`, bubble phase, attached while open with the light-dismiss trio. An uncancelled, unmodified Enter activates `defaultButton` (its `click()`, so a `type="submit"` default runs its proxy path) and cancels the keydown, which is what keeps a focused Cancel from firing itself — Space presses the focused control, as on the classic Mac. Anything that took Enter for itself cancelled the keydown first and is left alone: `vf-select`'s list, `vf-list`, an icon's rename, and a text field whose form ran its implicit submission (`requestImplicitSubmit` now reports whether a form owner took the press, and the fields cancel on true — so the submission runs once, not once per layer). A link keeps its own Enter. A multi-line editor (`<textarea>`, contenteditable) keeps Return for the newline; only `code === 'NumpadEnter'` fires the button there — the split a TextEdit item drew between the two keys. With no enabled default button the press is left native. Escape is unchanged (the `cancel` path above). **Removal while open is a close path** (`VfModalDialog.disconnectedCallback`): HTML's dialog removing steps skip the close algorithm entirely, which is exactly what a framework unmount does — the teardown routes through the same native-`close` funnel (`vf-close` fires on the removed element; nothing bubbles, it left the tree), `open` and the written origin reconcile so a re-append mounts closed, and focus returns to the element focused at open time. **The placement re-settles while open** whenever the dialog's own box resizes — slotted content upgrading after `showModal()`, `--vf-scale` moving under zoom — the viewport resizes, or the scale changes. What survives depends on how the modal got where it is: an unplaced one re-centers (it never claimed a spot, and the stranded case is exactly this one), while a dragged or authored origin is kept and only re-clamped on screen.
237
+ - **Implementation:** wraps a native `<dialog>` (for top-layer + focus trap). `show()` → `showModal()`; `close()` closes. Keep `open` attr in sync both directions. Drag the title bar to move it (shared `DragController` with `vf-window`), stating `top`/`left` in system px; drags starting on the close widget are ignored (same composedPath guard as `vf-window`). Escape → close + `vf-close` detail `{ reason: 'escape' }`; close box/programmatic/close() → `{ reason: 'close' }`; a click outside under `light-dismiss` → `{ reason: 'outside' }`. **Answers** (`VfModalDialog`): `close(value)` closes with `value` as `returnValue`, and a `<form method="dialog">` inside the dialog closes it on submit with the submitter's `value`, a submitter's `formmethod="dialog"` too. The platform's own rule looks for a `<dialog>` around the form, and this one is in the shadow root, so the base listens for `submit` on the host and closes at the end of the submit's path (`deferActivation`), not at all if anything cancelled it. `vf-button`'s submit proxy carries its `value` with or without a `name`. `vf-close`'s detail carries `returnValue`: the value that close carried, or null (Escape, a click outside, the close box, `close()` alone, removal). Return in a text field runs the form's implicit submission through the ringed `variant="default"` submit button when the form has one, ahead of the first in tree order, so a Cancel/OK row answers OK (`requestImplicitSubmit`). `npm run verify:dialog`, ANSWERS. No backdrop dimming: `::backdrop { background: transparent; }`. **Light dismiss is target identity, not geometry** (`VfModalDialog`): the `::backdrop` hit-tests as its `<dialog>`, and the frame fills the dialog's box, so the dialog is a pointer event's *target* exactly when the pointer was outside the frame — the listeners sit on the `<dialog>` itself, attached while open. It takes both halves of the click: a `pointerdown` on the backdrop arms (any other press disarms, so a right-click macOS never releases can't leave a stale arm), and a `pointerup` on the backdrop with the same `pointerId` closes — the platform's own `closedby="any"` two-step, so a press that starts on a control and slides off, or a title-bar drag released past the frame, leaves the dialog open. Deliberately not the `click` event: UI Events dispatches a press-drag-release click at the common ancestor of the two targets, which for a press on the frame released outside is the dialog itself. The platform's `closedby` attribute is not used because its dismissal arrives as `cancel`, which the base already reads as Escape. The press is consumed either way — a modal's backdrop lets nothing beneath it see the click — which is what lets a page dismiss its About box without also selecting the icon under the pointer. `npm run verify:dialog`. **Keyboard: the Dialog Manager's two rules** (`VfModalDialog`). *Initial focus:* right after `showModal()`'s own focusing steps, focus is handed to `initialFocusTarget` — the first slotted control carrying `autofocus` or `autoselect`, else the first enabled text-entry control (`vf-text-field`, `vf-number-field`, `vf-text-area`, or a native text input/textarea), else the default button (`defaultButton`: the first enabled `vf-button[variant="default"]`, looked for among the slotted content and then the shadow tree), else nothing, leaving the browser's pick. A field marked `autoselect` has its whole text selected on every open, right after `vf-show` and once the field has rendered, so a value set before `show()` or in a `vf-show` handler is the text selected. Otherwise the modal selects no text: whether a field opens selected is the page's call. The classic dialog put the insertion point in its first text item and gave nothing else focus; the web needs a focused control, and the default button is the one that makes Return and Space work without a Tab. Left alone, `showModal()` focuses the first focusable thing in flat-tree order — Cancel in a Cancel/OK row, or a link in the body — so Return pressed the wrong thing. Both getters are `protected`, for a consumer's own modal to override. *Return/Enter:* a `keydown` listener on the `<dialog>`, bubble phase, attached while open with the light-dismiss trio. An uncancelled, unmodified Enter activates `defaultButton` (its `click()`, so a `type="submit"` default runs its proxy path) and cancels the keydown, which is what keeps a focused Cancel from firing itself — Space presses the focused control, as on the classic Mac. Anything that took Enter for itself cancelled the keydown first and is left alone: `vf-select`'s list, `vf-list`, an icon's rename, and a text field whose form ran its implicit submission (`requestImplicitSubmit` now reports whether a form owner took the press, and the fields cancel on true — so the submission runs once, not once per layer). A link keeps its own Enter. A multi-line editor (`<textarea>`, contenteditable) keeps Return for the newline; only `code === 'NumpadEnter'` fires the button there — the split a TextEdit item drew between the two keys. With no enabled default button the press is left native. Escape is unchanged (the `cancel` path above). **Removal while open is a close path** (`VfModalDialog.disconnectedCallback`): HTML's dialog removing steps skip the close algorithm entirely, which is exactly what a framework unmount does — the teardown routes through the same native-`close` funnel (`vf-close` fires on the removed element; nothing bubbles, it left the tree), `open` and the written origin reconcile so a re-append mounts closed, and focus returns to the element focused at open time. **Opened again before a close's `close` event comes** — the native event is queued, so a `close()` and a `show()` in one turn do it — the open runs the funnel for that close first: its `vf-close` comes before the open's `vf-show`, and the late event finds the modal open and changes nothing, where it used to close the dialog that had just opened. That `vf-close` fires inside `show()`, so a one-time listener for the open's own answer goes on after `show()` (the shell's `ask()` does). The open drops any close value, reason or keys still pending, so a close asked for in that `vf-close` leaves nothing for a later one. `npm run verify:dialog`, REOPEN. **The placement re-settles while open** whenever the dialog's own box resizes — slotted content upgrading after `showModal()`, `--vf-scale` moving under zoom — the viewport resizes, or the scale changes. What survives depends on how the modal got where it is: an unplaced one re-centers (it never claimed a spot, and the stranded case is exactly this one), while a dragged or authored origin is kept and only re-clamped on screen.
238
238
  - **Visual (default chrome):** `vfModalFrame` with `vfTitleBar` set into it (§4) — the bar is literally the recipe `vf-window` uses, so it is identical by construction (stripes + centered title) rather than by matching copies; the frame is the modal double frame, **not** the window's shadowed `vfChromeFrame`. Outside → in: 1px outer rule; the 18px bar directly under it, 2px in at either end with its stripes starting 2px from the rule; the 2px band as the bar's floor (the bar's rule + the inner box's 1px top border); 2px gap + 2px band down the sides and along the bottom; no shadow. The body begins 20px below the dialog's top and 5px inside its other three edges. The bar takes the default `--vf-title-inset` (16px) — 60px when `closable`, the same clearance as `vf-window`, since the centered title needs symmetric room for the widget — and sets `touch-action: none` unconditionally, having no immovable state. `closable` renders the shared close box (`vfWindowWidgets` + `closeBox()` — byte-identical to `vf-window`'s, per the `moveable modal dialog.png` reference). Body is WHITE (`--vf-surface: #fff`) and is the content region, with no inset of its own: the positioning anchor, so a child placed with `top`/`left` measures from the frame's inner edge, and flow content starts at the same corner. Both chromes are full-height flex columns and the body takes the slack, for the same reason `vf-window`'s frame is: the declared `height` lands on the `<dialog>` (see `dialogSize`), and the recipes are skin only. The frame is the flex child of the `<dialog>` itself (`dialog[open]` is a flex column in `modalDialogStyles` — `[open]`-scoped, or it would out-cascade the UA's `dialog:not([open]) { display: none }`), not a `height: 100%` block: a percentage can't resolve against the undeclared-height dialog that only the UA's `dialog:modal` max-height caps, and that spill was how a viewport-tall modal stranded its buttons off-screen. **The box never grows, but over-stuffed content scrolls instead of clipping**: the body holds a `.content` scroll region (the default slot; part `content`). Placed children anchor to the body, outside that scroller, so they hold still while it scrolls. While the content fits, nothing matches. Once it overflows (`ScrollStateController`, the always-a-rail machinery), the region reserves the rail's 15px as its own right padding, wears the drawn `vfScrollRail` rail (§4) flush against the frame's inner band, which supplies the cell's outer line, and becomes a keyboard stop (`tabindex="0"`, `role="group"`, the kit's dotted ring) so the copy is scrollable without a pointer. The rail rides the body as an overlay pinned to its right edge, deliberately out of the layout flow: a rail *column*'s two fixed 15px arrow cells would hand the region a 32px minimum height, and a short dialog would then measure as fitting with the rail shown and overflowing without it — flip-flopping forever. The drop-open exemption is unchanged.
239
239
  - **The CSS and the controller state the same thing.** `.content` is `overflow-y: hidden` until the controller flags overflow, then `overflow-y: scroll`. It is deliberately never `auto`: the controller ignores the body face's negative half-leading (`LEADING_SPILL_SYS`, src/scroll-state.ts — `vf-paragraph` sets a 12-system-px line box under a 16-system-px em, so the inline box spills 2 inkless system px past the block box and `scrollHeight` counts it), and `auto` has no way to know that. With `auto` the two disagreed, and a fixed info dialog whose copy ends in a `vf-paragraph` rubber-banded under the wheel with no rail shown — 6 CSS px at scale 3. `hidden` still scrolls programmatically, so `scrollIntoView` on a focused control is unaffected. `npm run verify:contract`, OVERFLOW group.
240
240
  - **Visual (`frame="plain"`):** the same `vfModalFrame` (§4 — 1px outer, 2px gap, 2px inner band, no shadow, per `Windows/modal dialog.png`) with no title bar, and immovable like the original dBoxProc dialog (nothing renders a drag handle). The body begins 5px inside every edge. With no bar, `heading` only names the dialog and `closable` is ignored; a title in the body is the composer's, placed like any other item.
@@ -480,7 +480,7 @@ A raster image — the reference art's pixel icons — on the kit's grid, treati
480
480
 
481
481
  #### `vf-icon` (`VfIcon`, vf-icon.ts)
482
482
  The Finder icon — pixel art in a reserved cell with its name on a plate below, as one selectable, movable, renameable unit. `vf-img` puts a picture on the grid; this makes it the thing the Finder manipulates.
483
- - **Attributes/props:** `label`: string (the name; empty draws no plate — that is the "no label" setting, rather than a second attribute that could disagree with it). `size`: `large` (default, 32×32) | `small` (16×16) — picks the slot that paints *and* the cell it paints in. `selectable`, `selected`, `movable`, `editable`: boolean. `color`: boolean — declares the art a **color icon**, so selection darkens instead of inverting (see Visual). `open`: boolean — the icon's window is on screen, and the art paints as the derived open ghost (below). `target`: boolean (reflected) — the destination highlight under a drag: the selected treatment of the *art* alone (`invert(1)`, or `brightness(0.5)` under `color`) with none of `selected`'s semantics — no `vf-select`, no outside listener, no `aria-selected`, the plate untouched; the page sets it on the folder a `vf-drag` reports the pointer over. `width`: number (system px, **even**; the cell/grid pitch, which a longer name overflows rather than being bound by). `maxlength`: number (31).
483
+ - **Attributes/props:** `label`: string (the name; empty draws no plate — that is the "no label" setting, rather than a second attribute that could disagree with it). `size`: `large` (default, 32×32) | `small` (16×16) — picks the slot that paints *and* the cell it paints in. `selectable`, `selected`, `movable`, `editable`: boolean. `color`: boolean — declares the art a **color icon**, so selection darkens instead of inverting (see Visual). `open`: boolean — the icon's window is on screen, and the art paints as the derived open ghost (below). `alias`: boolean (reflected) — the icon stands for another item: its name, plate and rename field alike, is set in the body face's italic (`registerBodyItalicFace`, built from the face's own glyphs, each row shifted right on the grid, never a synthesized oblique), and the plate widens to the slant's last pixel, read from the run's ink in a canvas set in the plate's font. `target`: boolean (reflected) — the destination highlight under a drag: the selected treatment of the *art* alone (`invert(1)`, or `brightness(0.5)` under `color`) with none of `selected`'s semantics — no `vf-select`, no outside listener, no `aria-selected`, the plate untouched; the page sets it on the folder a `vf-drag` reports the pointer over. `width`: number (system px, **even**; the cell/grid pitch, which a longer name overflows rather than being bound by). `maxlength`: number (31).
484
484
  - **Visual:** `display: inline-block`. A column: the art cell (`--_cell` system px square, `overflow: hidden`, art centered), then, directly under it (`--vf-icon-gap`, 0 by default), the name plate — body face on a `--vf-icon-label-height` (12px) line box, the Finder's own plate height: the face's 16px em centers in it, 3px above the ascenders and descenders on the bottom edge, exactly as the Finder drew it — 1px horizontal padding, **opaque `--vf-white`** in its own right rather than `--vf-surface`: on the desktop dither the name reads because it sits on a plate, and `--vf-surface` is unset out there (`Example screen (1-bit).png`, the "Macintosh HD" icon). Selected: the plate takes the `--vf-highlight` pair and the art takes `filter: invert(1)` — a System 7 icon is ink and opaque white on a transparent surround, i.e. precisely an image plus its mask, so inverting flips the two and leaves the surround alone. That is the complete classic selected appearance for 1-bit art. Color art would invert into a photographic negative, which System 7 never showed: declare `color` and selection **darkens** instead — Icon Utilities' ttSelected transform, every color blended halfway toward black (`filter: brightness(0.5)`), whites going gray, the surround still untouched. The declaration is the consumer's, since the component cannot tell color art from a mask by looking at a slotted image; it covers whatever the cell shows (the open ghost included, so a color icon has one selected treatment open or closed), and the plate inverts either way — the selected name was inverted whatever the art.
485
485
  - **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.
486
486
  - **Behavior:** clicking selects, Shift/⌘ toggles, and a press elsewhere 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. For an icon alone, elsewhere is anywhere outside it. For an icon in a field, only a press on another icon or on a field (`vf-icon-field`, or a `role="listbox"` element of the page's own) clears it; a press on the menu bar, a menu, a dialog, a window's title bar or another window leaves the selection, so a menu command still has its icons to act on and a selection survives an application switch. Either way the press calls off a pending rename and a half-finished tap pair. Double-click fires `vf-open`, and its keyboard route is ⌘O / ⌘↓ (Ctrl off the Mac) — the System 7 Open shortcuts, added because a double-click is a pointer-only gesture (§1). **The instrument never changes what the icon means**, so a finger or a pen opens it too, on a *tap pair* the icon classifies from its own pointer events rather than from `dblclick`: two taps on the icon, the second pressed within `TAP_PAIR_MS` (500) of the first and no further than `TAP_PAIR_SLOP_PX` (24 CSS px) from it, `vf-open` firing on the second release. A platform reserves the double-tap for its zoom and may deliver no `dblclick` for one, and a movable icon's press is `preventDefault()`ed for the drag, which can suppress it as well; the pair depends on neither. A tap is a press released where it landed — one that became a drag, or that the platform cancelled, is not half of a pair — and a pair does not chain. The mouse keeps the platform's `dblclick`, which follows the user's own double-click setting; each press takes exactly one of the two routes, so a platform that also delivers a `dblclick` for a double-tap opens the icon once. The frame sets `touch-action: manipulation`, which takes the double-tap zoom off the icon and leaves panning and pinching to the page. Both are additions in the kit's idiom — System 7 had the mouse alone. Return is deliberately not one: the Finder's Return renamed, never opened, so it starts the edit on an editable icon and does nothing on a non-editable one. A double-click opens the icon **wherever it lands, the name included** — the handler is on the frame, and the name is as much the icon as the art is. `editable`: a press on the plate of an ALREADY-selected icon opens a rename field overlaying it (the press that does the selecting never does); the whole name starts selected, Return commits, Escape reverts, and the hidden plate keeps rendering the draft so the box widens as you type. **The two pointer gestures on a name are the same press**, and only the second one tells them apart, so the rename *waits*: the press arms a field that opens `RENAME_DELAY_MS` (800ms, `src/motion.ts`) later, and a press landing inside that window — wherever in the icon it falls — calls it off, leaving the double-click to open with no rename flashing up behind it. The window is long deliberately: no API reports what the browser counts as a double-click (it follows a user-movable platform setting), and reading a lone click as a pair costs only a wait, while reading a pair as a lone click renames when the user meant to open. A press that travels into a drag, a press outside, and any key call it off too — so a name drags like the art does, and Return opens the field at once, having no second half to wait for. `movable` — **not `draggable`**, which is a global HTML attribute and an `HTMLElement` accessor (the same trap as `align`, §5 Group G) — drags via `DragController` on the `vf-window` delegate shape (the frame is the handle and sets `touch-action: none` while movable, as the title bars do), plus arrow-key nudging (1 system px, 8 with Shift) because a pointer-only gesture is the kind of gap the kit closes (§1). Focus is the dashed rule below the plate, gated on `FocusRuleController` rather than `:focus-visible`: the host focuses itself so the press-drag can own the pointer.
@@ -538,6 +538,11 @@
538
538
  "description": "The icon's window is on screen, so the art paints as the Finder's open\nghost — outline held, interior re-filled with the kit's loose dither —\nderived in the client from the slotted art itself (see the class doc).\nSet it when handling `vf-open`, clear it when the window goes away.\nSelection inverts the ghost exactly as it inverts the art.",
539
539
  "values": []
540
540
  },
541
+ {
542
+ "name": "alias",
543
+ "description": "The icon is an alias, an item standing for another: its name is set in\nitalics, the body face's own, and the plate widens to the slant's last\npixel.",
544
+ "values": []
545
+ },
541
546
  {
542
547
  "name": "movable",
543
548
  "description": "Drag to move — `movable`, never `draggable`, which is a platform attribute\nand accessor (see the class doc). Arrow keys move a focused icon too, which\nmeans pairing this with selectable: focus is what that grants.",
@@ -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.17.0",
4
+ "version": "0.19.0",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
@@ -1191,6 +1191,11 @@
1191
1191
  "description": "The icon's window is on screen, so the art paints as the Finder's open\nghost — outline held, interior re-filled with the kit's loose dither —\nderived in the client from the slotted art itself (see the class doc).\nSet it when handling `vf-open`, clear it when the window goes away.\nSelection inverts the ghost exactly as it inverts the art.",
1192
1192
  "value": { "type": "boolean", "default": "false" }
1193
1193
  },
1194
+ {
1195
+ "name": "alias",
1196
+ "description": "The icon is an alias, an item standing for another: its name is set in\nitalics, the body face's own, and the plate widens to the slant's last\npixel.",
1197
+ "value": { "type": "boolean", "default": "false" }
1198
+ },
1194
1199
  {
1195
1200
  "name": "movable",
1196
1201
  "description": "Drag to move — `movable`, never `draggable`, which is a platform attribute\nand accessor (see the class doc). Arrow keys move a focused icon too, which\nmeans pairing this with selectable: focus is what that grants.",
@@ -1317,6 +1322,11 @@
1317
1322
  "description": "The icon's window is on screen, so the art paints as the Finder's open\nghost — outline held, interior re-filled with the kit's loose dither —\nderived in the client from the slotted art itself (see the class doc).\nSet it when handling `vf-open`, clear it when the window goes away.\nSelection inverts the ghost exactly as it inverts the art.",
1318
1323
  "type": "boolean"
1319
1324
  },
1325
+ {
1326
+ "name": "alias",
1327
+ "description": "The icon is an alias, an item standing for another: its name is set in\nitalics, the body face's own, and the plate widens to the slant's last\npixel.",
1328
+ "type": "boolean"
1329
+ },
1320
1330
  {
1321
1331
  "name": "movable",
1322
1332
  "description": "Drag to move — `movable`, never `draggable`, which is a platform attribute\nand accessor (see the class doc). Arrow keys move a focused icon too, which\nmeans pairing this with selectable: focus is what that grants.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vintage-frames",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Lit web components that emulate the look and feel of classic Mac OS System 7",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,6 +20,8 @@
20
20
  "**/display-font.ts",
21
21
  "**/body-font.js",
22
22
  "**/body-font.ts",
23
+ "**/body-italic-font.js",
24
+ "**/body-italic-font.ts",
23
25
  "**/*.css"
24
26
  ],
25
27
  "main": "./dist/index.js",
@@ -74,8 +76,10 @@
74
76
  "build:pages": "vite build --config vite.pages.config.ts",
75
77
  "preview:pages": "vite preview --config vite.pages.config.ts",
76
78
  "prepack": "npm run build && npm run verify:manifest",
77
- "version": "npm run analyze && node scripts/write-version.mjs && git add custom-elements.json editor src/shell/version.ts",
78
- "analyze": "cem analyze",
79
+ "preversion": "npm run check:app-api",
80
+ "version": "npm run analyze && node scripts/write-version.mjs && git add custom-elements.json editor app-api.json src/shell/version.ts",
81
+ "analyze": "cem analyze && node scripts/app-api.mjs",
82
+ "check:app-api": "npm run analyze && node scripts/check-app-api.mjs",
79
83
  "verify:manifest": "node scripts/verify-manifest.mjs",
80
84
  "typecheck": "tsc --noEmit",
81
85
  "preview": "vite preview",