vintage-frames 0.19.1 → 0.20.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 (55) hide show
  1. package/README.md +1 -18
  2. package/custom-elements.json +8 -8
  3. package/dist/text-control.d.ts +3 -3
  4. package/docs/SPEC.md +3 -3
  5. package/editor/vscode.html-custom-data.json +3 -3
  6. package/editor/web-types.json +7 -7
  7. package/package.json +4 -34
  8. package/dist/build/app-file.d.ts +0 -21
  9. package/dist/build/app-file.js +0 -42
  10. package/dist/build/box.d.ts +0 -57
  11. package/dist/build/box.js +0 -93
  12. package/dist/build/client.d.ts +0 -12
  13. package/dist/build/frame-picture.d.ts +0 -6
  14. package/dist/build/frame-picture.js +0 -4
  15. package/dist/build/frame.d.ts +0 -11
  16. package/dist/build/frame.js +0 -44
  17. package/dist/build/index.d.ts +0 -14
  18. package/dist/build/index.js +0 -4
  19. package/dist/build/plugins.d.ts +0 -41
  20. package/dist/build/plugins.js +0 -134
  21. package/dist/build/png.d.ts +0 -24
  22. package/dist/build/png.js +0 -116
  23. package/dist/shell/alert.d.ts +0 -40
  24. package/dist/shell/alert.js +0 -41
  25. package/dist/shell/app-file.d.ts +0 -115
  26. package/dist/shell/app-file.js +0 -189
  27. package/dist/shell/catalog.d.ts +0 -250
  28. package/dist/shell/catalog.js +0 -349
  29. package/dist/shell/clock.d.ts +0 -16
  30. package/dist/shell/clock.js +0 -36
  31. package/dist/shell/filing.d.ts +0 -39
  32. package/dist/shell/filing.js +0 -68
  33. package/dist/shell/finder.d.ts +0 -180
  34. package/dist/shell/finder.js +0 -676
  35. package/dist/shell/geometry.d.ts +0 -203
  36. package/dist/shell/geometry.js +0 -257
  37. package/dist/shell/index.d.ts +0 -30
  38. package/dist/shell/index.js +0 -12
  39. package/dist/shell/pure.d.ts +0 -13
  40. package/dist/shell/pure.js +0 -6
  41. package/dist/shell/shell.d.ts +0 -177
  42. package/dist/shell/shell.js +0 -277
  43. package/dist/shell/state.d.ts +0 -88
  44. package/dist/shell/state.js +0 -103
  45. package/dist/shell/storage.d.ts +0 -14
  46. package/dist/shell/storage.js +0 -41
  47. package/dist/shell/version.d.ts +0 -2
  48. package/dist/shell/version.js +0 -4
  49. package/dist/shell/windows.d.ts +0 -278
  50. package/dist/shell/windows.js +0 -447
  51. package/dist/styles/body-strike.d.ts +0 -12
  52. package/dist/styles/body-strike.js +0 -3961
  53. package/dist/styles/display-strike.d.ts +0 -12
  54. package/dist/styles/display-strike.js +0 -4343
  55. package/dist/styles/strike.d.ts +0 -30
package/README.md CHANGED
@@ -23,24 +23,7 @@ import 'vintage-frames/vf-checkbox.js'
23
23
 
24
24
  There is no need to import a stylesheet. The components carry their styles with them.
25
25
 
26
- ## The shell
27
-
28
- `vintage-frames/shell` is an experimental second entry point that runs applications over one desktop: a window manager, a menu bar that shows the front application's menus, and a Finder with folders, aliases, filing, Clean Up and the Trash. A System Folder's contents set the Apple menu and what opens at startup.
29
-
30
- ```ts
31
- import 'vintage-frames'
32
- import { createShell, finder, memoryStorage } from 'vintage-frames/shell'
33
-
34
- createShell(document.querySelector('vf-desktop'), {
35
- apps: [finder({ storage: memoryStorage(), seed: 'markup', art })],
36
- fit: 'viewport',
37
- })
38
- ```
39
-
40
- See [docs/SHELL.md](https://github.com/aportilla/vintage-frames/blob/main/docs/SHELL.md). Its API may change in any minor release while it is experimental.
41
-
42
- An application on the shell can ship as one PNG, its app file. `vintage-frames/build` holds the Vite plugins that write app files and build them into a site; it runs in the build only. See [docs/APP-FILES.md](https://github.com/aportilla/vintage-frames/blob/main/docs/APP-FILES.md).
43
-
26
+ To run applications over a desktop of these elements, with a window manager, a menu bar and a Finder, see [system-online-sdk](https://github.com/aportilla/system-online-sdk).
44
27
 
45
28
  ## Sizing
46
29
 
@@ -10567,7 +10567,7 @@
10567
10567
  "text": "boolean"
10568
10568
  },
10569
10569
  "default": "false",
10570
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
10570
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
10571
10571
  "attribute": "autoselect",
10572
10572
  "reflects": true,
10573
10573
  "inheritedFrom": {
@@ -11581,7 +11581,7 @@
11581
11581
  "text": "boolean"
11582
11582
  },
11583
11583
  "default": "false",
11584
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
11584
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
11585
11585
  "fieldName": "autoselect",
11586
11586
  "inheritedFrom": {
11587
11587
  "name": "VfTextControlBase",
@@ -18667,7 +18667,7 @@
18667
18667
  "text": "boolean"
18668
18668
  },
18669
18669
  "default": "false",
18670
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
18670
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
18671
18671
  "attribute": "autoselect",
18672
18672
  "reflects": true,
18673
18673
  "inheritedFrom": {
@@ -19695,7 +19695,7 @@
19695
19695
  "text": "boolean"
19696
19696
  },
19697
19697
  "default": "false",
19698
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
19698
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
19699
19699
  "fieldName": "autoselect",
19700
19700
  "inheritedFrom": {
19701
19701
  "name": "VfTextControlBase",
@@ -19997,7 +19997,7 @@
19997
19997
  "text": "boolean"
19998
19998
  },
19999
19999
  "default": "false",
20000
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
20000
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
20001
20001
  "attribute": "autoselect",
20002
20002
  "reflects": true,
20003
20003
  "inheritedFrom": {
@@ -21032,7 +21032,7 @@
21032
21032
  "text": "boolean"
21033
21033
  },
21034
21034
  "default": "false",
21035
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
21035
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
21036
21036
  "fieldName": "autoselect",
21037
21037
  "inheritedFrom": {
21038
21038
  "name": "VfTextControlBase",
@@ -23375,7 +23375,7 @@
23375
23375
  "text": "boolean"
23376
23376
  },
23377
23377
  "default": "false",
23378
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
23378
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
23379
23379
  "attribute": "autoselect",
23380
23380
  "reflects": true
23381
23381
  },
@@ -24250,7 +24250,7 @@
24250
24250
  "text": "boolean"
24251
24251
  },
24252
24252
  "default": "false",
24253
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
24253
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
24254
24254
  "fieldName": "autoselect"
24255
24255
  },
24256
24256
  {
@@ -33,9 +33,9 @@ export declare class VfTextControlBase extends VfShadowRoleControl {
33
33
  /**
34
34
  * Opens with the whole text selected, so typing replaces it. Implies
35
35
  * `autofocus`: the first control carrying either takes the opening focus of
36
- * its `vf-dialog`, or of a window the shell's window manager opens. A dialog
37
- * selects the text on every open, a window on its first open only. A click
38
- * still places the caret.
36
+ * its `vf-dialog`, or of a window system-online-sdk's window manager opens.
37
+ * A dialog selects the text on every open, a window on its first open only.
38
+ * A click still places the caret.
39
39
  */
40
40
  autoselect: boolean;
41
41
  /** Form field name used when submitting the associated form. */
package/docs/SPEC.md CHANGED
@@ -196,7 +196,7 @@ Every recipe also declares its size — `width`, and `height` where the window i
196
196
 
197
197
  #### `vf-desktop` (`VfDesktop`, vf-desktop.ts)
198
198
  Full-bleed classic desktop container.
199
- - **Attributes/props:** `width`, `height: number` (system px, defaults 512×342 — the compact Mac's raster) — the screen's own size, the way a WIND resource declared a window's. The host box renders at exactly `width + 2·bezel` per axis (a live `sysLength` written as host inline size on every update), always a whole number of system px. **Pure CSS sizing is not supported**: the inline size wins over any stylesheet, so the page sets the numbers — directly or via `fitWithin` — and positions the explicitly sized desktop with its own CSS, keeping any sub-system-pixel slack in the page's own layout. `bezel: number` (system px, default 0) — the black screen surround, the CRT's unlit margin between raster and case, added onto the declared screen on every side. The screen owns flow, an absolute window's containing block and the drag clip, so a dragged window crops at the raster's edge. Its four corners wear the `SCREEN_CORNER` mask (the bottom pair mirrored vertically — `steppedCornerClip`'s `edge`), painted above everything like the hardware mask — the top pair lands over a slotted menu bar's corners, so the bar needs no `rounded` of its own inside a bezeled desktop. The bezel width is written onto the host as `--vf-desktop-bezel` (self-set geometry, like `--vf-scale` — component-owned, not a theming token). `pattern: string` (default `'gray-50'`, reflected) — the desktop pattern, System 7's General Controls setting: a library name (docs/PATTERNS.md) or sixteen hex digits stating a custom 8×8; an unrecognized value warns once and keeps the dither. A `--vf-desktop-pattern` token override wins over it. It reflects so a page can observe a change on the attribute; the shell saves it that way. `window-top: number` (`windowTop`, system px from the screen's top, unset by default) — the least `top` a window may take, for a page that keeps a strip of its own under the menu bar, over the windows: a window dragged on the desktop stops there and `windowArea` starts there; icons still stop at the menu bar; below the bar's bottom it changes nothing.
199
+ - **Attributes/props:** `width`, `height: number` (system px, defaults 512×342 — the compact Mac's raster) — the screen's own size, the way a WIND resource declared a window's. The host box renders at exactly `width + 2·bezel` per axis (a live `sysLength` written as host inline size on every update), always a whole number of system px. **Pure CSS sizing is not supported**: the inline size wins over any stylesheet, so the page sets the numbers — directly or via `fitWithin` — and positions the explicitly sized desktop with its own CSS, keeping any sub-system-pixel slack in the page's own layout. `bezel: number` (system px, default 0) — the black screen surround, the CRT's unlit margin between raster and case, added onto the declared screen on every side. The screen owns flow, an absolute window's containing block and the drag clip, so a dragged window crops at the raster's edge. Its four corners wear the `SCREEN_CORNER` mask (the bottom pair mirrored vertically — `steppedCornerClip`'s `edge`), painted above everything like the hardware mask — the top pair lands over a slotted menu bar's corners, so the bar needs no `rounded` of its own inside a bezeled desktop. The bezel width is written onto the host as `--vf-desktop-bezel` (self-set geometry, like `--vf-scale` — component-owned, not a theming token). `pattern: string` (default `'gray-50'`, reflected) — the desktop pattern, System 7's General Controls setting: a library name (docs/PATTERNS.md) or sixteen hex digits stating a custom 8×8; an unrecognized value warns once and keeps the dither. A `--vf-desktop-pattern` token override wins over it. It reflects so a page can observe a change on the attribute; system-online-sdk's shell saves it that way. `window-top: number` (`windowTop`, system px from the screen's top, unset by default) — the least `top` a window may take, for a page that keeps a strip of its own under the menu bar, over the windows: a window dragged on the desktop stops there and `windowArea` starts there; icons still stop at the menu bar; below the bar's bottom it changes nothing.
200
200
  - **Methods:** `fitWithin(maxWidth, maxHeight)` (CSS px) → sets `width`/ `height` to the largest whole-system-px raster whose host box — bezel included — fits the bound, per the current effective scale, and returns `{ width, height }`. The page's half of the sizing contract: call it on `resize` and `onScaleChange` (zoom and density moves change what a system px costs in CSS px), as a full-screen faux desktop does. `clearActive()` → deactivate the whole document tier (see *Deactivation* under Behavior). `activeWindow: HTMLElement | null` (read-only) → the active document-tier window, or `null` while the tier is deactivated or has no shown window; utility windows and hidden windows are never the holder. `stackingOrder: HTMLElement[]` (read-only) → the slotted windows in stacking order, bottom-most first — the document tier, then the floating tier, each by its last raise — hidden windows included where they stand; a fresh array per read. The light-DOM order catches up with it only after a gesture, and a raise by keyboard focus moves no node, so a page that saves window depths or picks the window behind another reads this rather than the DOM. `workArea` (read-only) → `{ left, top, width, height }` in system px from the screen's corner, bezel excluded: the screen less a slotted, shown `vf-menu-bar` along its top edge (measured at the call; the whole screen without one). `windowArea` (read-only) → the work area deepened to `window-top` where that is lower — the area windows are dragged in. `placementAt(clientX, clientY, child?)` → a viewport point as a placement on the screen, `{ left, top }` in whole system px on the placement lattice, measured from the raster's corner with the bezel excluded — the pair a child dropped onto the desktop is written with; with `child`, the pair to write to that element, its `origin` offset folded in (`placementIn`, src/position.ts; the anchor sits in shadow, which is why it is a method).
201
201
  - **Visual:** `display: block; position: relative;` — the paint lives on an inner screen surface (part `desktop`, `overflow: hidden` — the whole-system-px raster, inset by `bezel` when one is set). Screen surface = the `pattern` (the classic 50% dither, `gray-50`, by default), painted as the screen's own background by the pattern fill (*Patterns* under *Tiled fills*): black ink on the fill's opaque `--vf-white` paper, a repeating tile — the authentic black-on-white dither. A consumer `--vf-desktop-pattern` renders instead as a placed tile grid on the token's 30-system-px tile with no paper, so `var(--vf-desktop, #808080)` beneath shows only under a custom tile with transparent cells (or `none`). Forced colors: flat Canvas.
202
202
  - **Slots:** default (menu bar, windows, anything).
@@ -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. **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.
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()` (system-online-sdk'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.
@@ -314,7 +314,7 @@ The color-swatch button: a well of solid color — a palette cell.
314
314
  - **Attributes/props:** `value`, `placeholder`, `disabled`, `readonly`, `autoselect`, `type: string` (default `'text'`; pass through to input), `name`. The input-behavior attributes — `autocomplete`, `inputmode`, `enterkeyhint`, `maxlength`, `pattern`, `spellcheck`, `autocapitalize` — are **forwarded verbatim** from the host onto the inner input (observed attributes, not reactive properties: four of them are globals with IDL accessors already on `HTMLElement`, and a Lit `@property` would shadow the platform member — the `align`/`draggable` trap). The platform only honors them on the element that actually takes the input, which is in the shadow root.
315
315
  - **Visual:** inner `<input>`: white bg, `1px solid black`, NO radius, height `var(--vf-control-height, 22px)`, `padding: 0 6px`, font tokens but `font-weight: var(--vf-font-weight, 700)`. `user-select: text`. Focus: for a **keyboard** focus, the kit's 1px dashed rule (`vfFocusUnderline`, §4) one blank system px row under the well — no dotted outline and no thickened border. A click leaves it unmarked: the insertion point is already the answer to where focus went (see §4 on why this can't be `:focus-visible`). Disabled: the text dims to gray; the black border stays. Selected text inverts to solid black-on-white (`.vf-field::selection`, using `--vf-highlight`/`--vf-highlight-text`) — the 1-bit System 7 selection, shared by all three editable fields via the `vfField` skin.
316
316
  - **Structure:** the `<input>` sits in a `.vf-field-well` wrapper, which is what the focus rule hangs from and what carries `vf-snap`. A replaced element draws no pseudo-element of its own, and the host is never the thing grid snapping moves, so the wrapper is the only box that is both the well's exact shape and on the corrected grid. Same wrapper in all three fields, assembled by `VfTextControlBase.wellClass` so the focus gate can't drift between them. Two consequences when embedding: the rule paints 2 system px **below the host's own box** (`pointer-events: none`, so it never takes a click meant for what sits under it, but a tight `overflow: hidden` ancestor clips it), and a `width` set on `::part(input)` sizes the control without moving the wrapper the rule spans — a field's width belongs on the host or on `--vf-field-width`.
317
- - **Behavior:** form-associated; syncs `value` on input; `formResetCallback` restores default. `select()` selects the whole value, as a native field's does, on the inner input; with a new value not yet rendered it selects once the value is in, so `field.value = x; field.select()` selects `x`. `focus()` focuses the inner input and keeps its caret (Chromium selects all of an input's text when a host's `delegatesFocus` hands the focus on). `autoselect` is `autofocus` plus a selection: the first control carrying either takes the opening focus of its `vf-dialog`, which selects the field's whole text on every open, or of a window the shell's window manager opens, which selects it on the window's first open only (SHELL.md). A click still places the caret. A commit — native `change`, leaving the field, or Return in a form — is announced once: a commit that brings nothing new since the field took focus or since the last commit fires nothing. In a form, Return commits first, then runs the form's implicit submission, the order a native input keeps, so `change` comes before the form's `submit`. That submission works the way HTML defines it — activating the form's default button (first submit button in tree order, `vf-button` included), so the submission carries a real `submitter` and that button's `name`/`value`, a disabled default button submits nothing, and only a form with no submit button falls back to a bare `requestSubmit()` (`requestImplicitSubmit`, text-control.ts). When a form owner took the press the keydown is cancelled, so an enclosing `vf-dialog` does not route the same Enter to its default button a second time; with no form the press goes on to the dialog, whose default button it activates.
317
+ - **Behavior:** form-associated; syncs `value` on input; `formResetCallback` restores default. `select()` selects the whole value, as a native field's does, on the inner input; with a new value not yet rendered it selects once the value is in, so `field.value = x; field.select()` selects `x`. `focus()` focuses the inner input and keeps its caret (Chromium selects all of an input's text when a host's `delegatesFocus` hands the focus on). `autoselect` is `autofocus` plus a selection: the first control carrying either takes the opening focus of its `vf-dialog`, which selects the field's whole text on every open, or of a window system-online-sdk's window manager opens, which selects it on the window's first open only (that SDK's docs/SHELL.md). A click still places the caret. A commit — native `change`, leaving the field, or Return in a form — is announced once: a commit that brings nothing new since the field took focus or since the last commit fires nothing. In a form, Return commits first, then runs the form's implicit submission, the order a native input keeps, so `change` comes before the form's `submit`. That submission works the way HTML defines it — activating the form's default button (first submit button in tree order, `vf-button` included), so the submission carries a real `submitter` and that button's `name`/`value`, a disabled default button submits nothing, and only a form with no submit button falls back to a bare `requestSubmit()` (`requestImplicitSubmit`, text-control.ts). When a form owner took the press the keydown is cancelled, so an enclosing `vf-dialog` does not route the same Enter to its default button a second time; with no form the press goes on to the dialog, whose default button it activates.
318
318
  - **Parts:** `input`.
319
319
  - **Events:** `vf-input` detail `{ value }` on every keystroke; `vf-change` detail `{ value }` on commit. Plus the native pair per §2: the inner input's own composed `input` crosses the boundary itself; `change` is re-dispatched from the host.
320
320
 
@@ -966,7 +966,7 @@
966
966
  },
967
967
  {
968
968
  "name": "autoselect",
969
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
969
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
970
970
  "values": []
971
971
  },
972
972
  {
@@ -1624,7 +1624,7 @@
1624
1624
  },
1625
1625
  {
1626
1626
  "name": "autoselect",
1627
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
1627
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
1628
1628
  "values": []
1629
1629
  },
1630
1630
  {
@@ -1701,7 +1701,7 @@
1701
1701
  },
1702
1702
  {
1703
1703
  "name": "autoselect",
1704
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
1704
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
1705
1705
  "values": []
1706
1706
  },
1707
1707
  {
@@ -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.19.1",
4
+ "version": "0.20.0",
5
5
  "description-markup": "markdown",
6
6
  "contributions": {
7
7
  "html": {
@@ -2217,7 +2217,7 @@
2217
2217
  },
2218
2218
  {
2219
2219
  "name": "autoselect",
2220
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
2220
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
2221
2221
  "value": { "type": "boolean", "default": "false" }
2222
2222
  },
2223
2223
  {
@@ -2318,7 +2318,7 @@
2318
2318
  },
2319
2319
  {
2320
2320
  "name": "autoselect",
2321
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
2321
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
2322
2322
  "type": "boolean"
2323
2323
  },
2324
2324
  {
@@ -3790,7 +3790,7 @@
3790
3790
  },
3791
3791
  {
3792
3792
  "name": "autoselect",
3793
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
3793
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
3794
3794
  "value": { "type": "boolean", "default": "false" }
3795
3795
  },
3796
3796
  {
@@ -3881,7 +3881,7 @@
3881
3881
  },
3882
3882
  {
3883
3883
  "name": "autoselect",
3884
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
3884
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
3885
3885
  "type": "boolean"
3886
3886
  },
3887
3887
  {
@@ -3997,7 +3997,7 @@
3997
3997
  },
3998
3998
  {
3999
3999
  "name": "autoselect",
4000
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
4000
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
4001
4001
  "value": { "type": "boolean", "default": "false" }
4002
4002
  },
4003
4003
  {
@@ -4088,7 +4088,7 @@
4088
4088
  },
4089
4089
  {
4090
4090
  "name": "autoselect",
4091
- "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window the shell's window manager opens. A dialog\nselects the text on every open, a window on its first open only. A click\nstill places the caret.",
4091
+ "description": "Opens with the whole text selected, so typing replaces it. Implies\n`autofocus`: the first control carrying either takes the opening focus of\nits `vf-dialog`, or of a window system-online-sdk's window manager opens.\nA dialog selects the text on every open, a window on its first open only.\nA click still places the caret.",
4092
4092
  "type": "boolean"
4093
4093
  },
4094
4094
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vintage-frames",
3
- "version": "0.19.1",
3
+ "version": "0.20.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",
@@ -38,21 +38,6 @@
38
38
  "types": "./dist/components/vf-*.d.ts",
39
39
  "import": "./dist/components/vf-*.js"
40
40
  },
41
- "./shell": {
42
- "types": "./dist/shell/index.d.ts",
43
- "import": "./dist/shell/index.js"
44
- },
45
- "./shell/pure": {
46
- "types": "./dist/shell/pure.d.ts",
47
- "import": "./dist/shell/pure.js"
48
- },
49
- "./build": {
50
- "types": "./dist/build/index.d.ts",
51
- "import": "./dist/build/index.js"
52
- },
53
- "./build/client": {
54
- "types": "./dist/build/client.d.ts"
55
- },
56
41
  "./package.json": "./package.json"
57
42
  },
58
43
  "files": [
@@ -76,15 +61,12 @@
76
61
  "build:pages": "vite build --config vite.pages.config.ts",
77
62
  "preview:pages": "vite preview --config vite.pages.config.ts",
78
63
  "prepack": "npm run build && npm run verify:manifest",
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",
64
+ "version": "npm run analyze && git add custom-elements.json editor",
65
+ "analyze": "cem analyze",
83
66
  "verify:manifest": "node scripts/verify-manifest.mjs",
84
67
  "typecheck": "tsc --noEmit",
85
68
  "preview": "vite preview",
86
69
  "embed:cursors": "node scripts/embed-cursor-art.mjs",
87
- "embed:frame": "node scripts/embed-frame.mjs",
88
70
  "verify:buttons": "tsc src/pixel-frame.ts --outDir scripts/.tmp --target es2022 --module esnext --moduleResolution bundler --skipLibCheck && node scripts/emit-clips.mjs && python3 scripts/verify-button-clips.py",
89
71
  "shot:buttons": "npx playwright screenshot --wait-for-timeout=2000 http://localhost:5173/pixel-test.html shots/buttons.png",
90
72
  "shot:verify": "python3 scripts/verify-button-screenshot.py shots/buttons.png",
@@ -128,23 +110,11 @@
128
110
  "verify:snap": "node scripts/verify-snap.mjs",
129
111
  "verify:contract": "node scripts/verify-contract.mjs",
130
112
  "verify:names": "node scripts/verify-names.mjs",
131
- "verify:forced-colors": "node scripts/verify-forced-colors.mjs",
132
- "verify:shell-unit": "tsc -p tsconfig.unit.json && node --test --test-reporter=tap \"test/*.test.mjs\"",
133
- "verify:shell-front": "node scripts/verify-shell-front.mjs",
134
- "verify:shell-windows": "node scripts/verify-shell-windows.mjs",
135
- "verify:shell-finder": "node scripts/verify-shell-finder.mjs"
113
+ "verify:forced-colors": "node scripts/verify-forced-colors.mjs"
136
114
  },
137
115
  "dependencies": {
138
116
  "lit": "^3.2.0"
139
117
  },
140
- "peerDependencies": {
141
- "vite": ">=8.0.0"
142
- },
143
- "peerDependenciesMeta": {
144
- "vite": {
145
- "optional": true
146
- }
147
- },
148
118
  "devDependencies": {
149
119
  "@custom-elements-manifest/analyzer": "^0.11.0",
150
120
  "@types/node": "^24.19.1",
@@ -1,21 +0,0 @@
1
- /**
2
- * Writing app files. The box's own chunks are copied as they are, so the
3
- * picture is never re-encoded; any app chunks already there are dropped,
4
- * and the manifest's and the code's go in before IEND.
5
- */
6
- import type { AppManifest } from '../shell/app-file.js';
7
- /** What {@link packApp} packs. */
8
- export interface PackAppInput {
9
- /** The manifest's fields but its format, app API and icon, which the packer fills in. */
10
- manifest: Omit<AppManifest, 'format' | 'api' | 'icon'>;
11
- /** The icon's PNG, 32 × 32. */
12
- icon: Uint8Array;
13
- /** The application's code: one ES module whose default export is its factory. */
14
- code: string;
15
- /** The artwork's PNG, for the box. Without it the icon is stamped there. */
16
- artwork?: Uint8Array;
17
- }
18
- /** `box` with `manifest` and `code` in its chunks. */
19
- export declare function writeAppFile(box: Uint8Array, manifest: AppManifest, code: string): Promise<Uint8Array>;
20
- /** The whole app file: the box in the kit's frame, then the manifest and the code. */
21
- export declare function packApp({ manifest, icon, code, artwork }: PackAppInput): Promise<Uint8Array>;
@@ -1,42 +0,0 @@
1
- import { CODE_KEYWORD as e, MANIFEST_KEYWORD as t, PNG_SIGNATURE as n, deflate as r, itxtKeyword as i, manifestOf as a, pngChunks as o } from "../shell/app-file.js";
2
- import { concat as s, pngChunk as c } from "./png.js";
3
- import { composeBox as l } from "./box.js";
4
- //#region src/build/app-file.ts
5
- var u = new TextEncoder();
6
- function d(e, t, n) {
7
- let r = Uint8Array.from([...e].map((e) => e.charCodeAt(0)));
8
- return c("iTXt", s([
9
- r,
10
- Uint8Array.of(0, +!!n, 0, 0, 0),
11
- t
12
- ]));
13
- }
14
- async function f(c, l, f) {
15
- let p = o(c);
16
- if (!p) throw Error("vintage-frames/build: the box is not a whole PNG");
17
- let m = a(l);
18
- if (typeof m == "string") throw Error(`vintage-frames/build: the manifest won't read back: ${m}`);
19
- let h = [d(t, u.encode(JSON.stringify(m)), !1), d(e, await r(u.encode(f)), !0)], g = [n];
20
- for (let e of p) e.type === "iTXt" && ["vintage-frames.app", "vintage-frames.code"].includes(i(e.data)) || (e.type === "IEND" && g.push(...h), g.push(c.subarray(e.start, e.end)));
21
- return s(g);
22
- }
23
- async function p({ manifest: e, icon: t, code: n, artwork: r }) {
24
- let i = {
25
- ...e,
26
- format: 1,
27
- api: 2,
28
- icon: `data:image/png;base64,${m(t)}`
29
- };
30
- return f(await l({
31
- manifest: i,
32
- icon: t,
33
- artwork: r
34
- }), i, n);
35
- }
36
- function m(e) {
37
- let t = "";
38
- for (let n = 0; n < e.length; n += 32768) t += String.fromCharCode(...e.subarray(n, n + 32768));
39
- return btoa(t);
40
- }
41
- //#endregion
42
- export { p as packApp, f as writeAppFile };
@@ -1,57 +0,0 @@
1
- /**
2
- * The box: the picture an app file shows. The kit's frame, with the
3
- * application's artwork in the artwork slot, or its icon stamped large on
4
- * the slot's backdrop when there is none, and lines of text set from the
5
- * kit's strikes with the manifest's fields in them. Artwork and the stamp
6
- * are pasted by their alpha. Nothing is drawn outside the slots.
7
- */
8
- import type { AppManifest } from '../shell/app-file.js';
9
- import type { Strike } from '../styles/strike.js';
10
- import type { Pixels } from './png.js';
11
- /** A box in a frame's picture, in its px. */
12
- export interface Slot {
13
- left: number;
14
- top: number;
15
- width: number;
16
- height: number;
17
- }
18
- /** A frame: a picture with everything fixed drawn in, and its slots. */
19
- export interface BoxFrame {
20
- /** The picture, a PNG. */
21
- picture: Uint8Array;
22
- /** One system px, in the picture's px. Each of the artwork's px is a whole number of them. */
23
- scale: number;
24
- /** Where the artwork goes. Without artwork the icon goes there, `stamp` times its size and centered. */
25
- artwork: Slot & {
26
- stamp: number;
27
- };
28
- /** The lines of text. */
29
- text: readonly TextSlot[];
30
- }
31
- /** A line of text on the box. */
32
- export interface TextSlot {
33
- /** The top-left of its em box, and the width it fits in. */
34
- left: number;
35
- top: number;
36
- width: number;
37
- strike: Strike;
38
- /** Picture px per design px. */
39
- scale: number;
40
- /** `#rrggbb`. */
41
- ink: string;
42
- align?: 'left' | 'center' | 'right';
43
- /** The line, with manifest fields in braces: `{name}`, `Version {version}`, `by {author}`, or fixed text. */
44
- text: string;
45
- }
46
- /** What a box is composed from. */
47
- export interface BoxInput {
48
- manifest: AppManifest;
49
- /** The icon's PNG, 32 × 32. */
50
- icon: Uint8Array;
51
- /** The artwork's PNG: the artwork slot's size in system px, or that divided by a whole number. */
52
- artwork?: Uint8Array;
53
- }
54
- /** The box in the kit's frame, as a PNG. */
55
- export declare function composeBox(input: BoxInput): Promise<Uint8Array>;
56
- /** The box in `frame`, as pixels. */
57
- export declare function drawBox(frame: BoxFrame, { manifest, icon, artwork }: BoxInput): Promise<Pixels>;
package/dist/build/box.js DELETED
@@ -1,93 +0,0 @@
1
- import { FRAME as e } from "./frame.js";
2
- import { decodePng as t, encodePng as n } from "./png.js";
3
- //#region src/build/box.ts
4
- var r = [
5
- "id",
6
- "name",
7
- "version",
8
- "requires",
9
- "author"
10
- ];
11
- async function i(t) {
12
- return n(await a(e, t));
13
- }
14
- async function a(e, { manifest: n, icon: r, artwork: i }) {
15
- let a = await t(e.picture, "the frame"), c = await t(r, "the icon");
16
- if (c.width !== 32 || c.height !== 32) throw Error(`the icon is ${c.width} × ${c.height}, and an icon is 32 × 32`);
17
- let u = e.artwork;
18
- if (i) {
19
- let n = await t(i, "the artwork"), r = {
20
- width: u.width / e.scale,
21
- height: u.height / e.scale
22
- }, c = r.width / n.width;
23
- if (!Number.isInteger(c) || n.height * c !== r.height) throw Error(`the artwork is ${n.width} × ${n.height}: draw it at ${r.width} × ${r.height}, or that divided by a whole number (${o(r)})`);
24
- s(a, n, u.left, u.top, c * e.scale, u);
25
- } else {
26
- let e = 32 * u.stamp;
27
- s(a, c, u.left + Math.floor((u.width - e) / 2), u.top + Math.floor((u.height - e) / 2), u.stamp, u);
28
- }
29
- for (let t of e.text) p(a, t, l(t.text, n));
30
- return a;
31
- }
32
- function o({ width: e, height: t }) {
33
- let n = [];
34
- for (let r = 2; r <= e && n.length < 3; r++) e % r === 0 && t % r === 0 && n.push(`${e / r} × ${t / r}`);
35
- return [...n, "…"].join(", ");
36
- }
37
- function s(e, t, n, r, i, a) {
38
- let o = Math.max(n, a.left, 0), s = Math.max(r, a.top, 0), l = Math.min(n + t.width * i, a.left + a.width, e.width), u = Math.min(r + t.height * i, a.top + a.height, e.height);
39
- for (let a = s; a < u; a++) for (let s = o; s < l; s++) {
40
- let o = (Math.floor((a - r) / i) * t.width + Math.floor((s - n) / i)) * 4;
41
- c(e, s, a, t.data.subarray(o, o + 4));
42
- }
43
- }
44
- function c(e, t, n, [r, i, a, o]) {
45
- if (!o) return;
46
- let s = e.data, c = (n * e.width + t) * 4;
47
- if (o === 255) {
48
- s[c] = r, s[c + 1] = i, s[c + 2] = a, s[c + 3] = 255;
49
- return;
50
- }
51
- let l = s[c + 3] * (255 - o) / 255, u = o + l;
52
- s[c] = Math.round((r * o + s[c] * l) / u), s[c + 1] = Math.round((i * o + s[c + 1] * l) / u), s[c + 2] = Math.round((a * o + s[c + 2] * l) / u), s[c + 3] = Math.round(u);
53
- }
54
- function l(e, t) {
55
- return e.replace(/\{(\w+)\}/g, (n, i) => {
56
- let a = r.find((e) => e === i);
57
- if (!a) throw Error(`the frame's line "${e}" names {${i}}, which a manifest doesn't have`);
58
- return t[a];
59
- });
60
- }
61
- var u = (e) => `"${e}" (U+${e.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")})`, d = (e, t) => [...t].reduce((t, n) => t + e.glyphs[n].advance, 0);
62
- function f(e, t, n) {
63
- if (d(e, t) <= n) return t;
64
- if (!e.glyphs["…"]) throw Error(`${e.family} has no ${u("…")} to shorten "${t}" with`);
65
- let r = [...t];
66
- for (; r.length && d(e, r.join("").trimEnd() + "…") > n;) r.pop();
67
- return r.join("").trimEnd() + "…";
68
- }
69
- function p(e, t, n) {
70
- let { strike: r, scale: i } = t;
71
- for (let e of n) if (!r.glyphs[e]) throw Error(`${r.family} has no ${u(e)}, in "${n}"`);
72
- let a = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(t.ink);
73
- if (!a) throw Error(`the frame's ink "${t.ink}" is not #rrggbb`);
74
- let o = Uint8Array.of(parseInt(a[1], 16), parseInt(a[2], 16), parseInt(a[3], 16), 255), s = f(r, n, t.width / i), l = d(r, s) * i, p = t.align === "center" ? Math.floor((t.width - l) / 2 / i) * i : t.align === "right" ? t.width - l : 0, m = {
75
- left: t.left,
76
- top: t.top,
77
- width: t.width,
78
- height: (r.ascent + r.descent) * i
79
- }, h = t.top + r.ascent * i, g = t.left + p;
80
- for (let t of s) {
81
- let n = r.glyphs[t];
82
- n.rows.forEach((t, r) => {
83
- let a = h - (n.y0 + n.height - r) * i;
84
- for (let r = 0; r < t.length; r++) {
85
- if (t[r] !== "#") continue;
86
- let s = g + (n.x0 + r) * i;
87
- for (let t = Math.max(a, m.top, 0); t < Math.min(a + i, m.top + m.height, e.height); t++) for (let n = Math.max(s, m.left, 0); n < Math.min(s + i, m.left + m.width, e.width); n++) c(e, n, t, o);
88
- }
89
- }), g += n.advance * i;
90
- }
91
- }
92
- //#endregion
93
- export { i as composeBox, a as drawBox };
@@ -1,12 +0,0 @@
1
- /**
2
- * `vintage-frames/build/client`: the types of a `*.png?app` import, which
3
- * `appFiles()` serves. A site adds it to tsconfig's `types` beside
4
- * `vite/client`.
5
- */
6
- declare module '*.png?app' {
7
- /** The application's factory: its definition, given the manifest's icon. */
8
- const app: (...args: any[]) => import('../shell/index.js').AppDefinition<any>;
9
- export default app;
10
- /** The app file's manifest. */
11
- export const manifest: import('../shell/pure.js').AppManifest;
12
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * The kit's frame's picture: box/frame.png, 480 × 615, 3095 bytes.
3
- * GENERATED by `npm run embed:frame`: redraw box/frame.png and re-run rather
4
- * than editing this file. Its slots are src/build/frame.ts's.
5
- */
6
- export declare const FRAME_PNG: Uint8Array;