vintage-frames 0.1.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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/custom-elements.json +19807 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
  5. package/dist/chrome.d.ts +73 -0
  6. package/dist/chrome.js +94 -0
  7. package/dist/components/vf-button-group.d.ts +50 -0
  8. package/dist/components/vf-button-group.js +68 -0
  9. package/dist/components/vf-button.d.ts +169 -0
  10. package/dist/components/vf-button.js +234 -0
  11. package/dist/components/vf-checkbox.d.ts +60 -0
  12. package/dist/components/vf-checkbox.js +135 -0
  13. package/dist/components/vf-container.d.ts +95 -0
  14. package/dist/components/vf-container.js +86 -0
  15. package/dist/components/vf-desktop.d.ts +243 -0
  16. package/dist/components/vf-desktop.js +310 -0
  17. package/dist/components/vf-dialog.d.ts +105 -0
  18. package/dist/components/vf-dialog.js +280 -0
  19. package/dist/components/vf-fieldset.d.ts +34 -0
  20. package/dist/components/vf-fieldset.js +76 -0
  21. package/dist/components/vf-grid.d.ts +124 -0
  22. package/dist/components/vf-grid.js +133 -0
  23. package/dist/components/vf-icon.d.ts +356 -0
  24. package/dist/components/vf-icon.js +507 -0
  25. package/dist/components/vf-img.d.ts +63 -0
  26. package/dist/components/vf-img.js +65 -0
  27. package/dist/components/vf-label.d.ts +117 -0
  28. package/dist/components/vf-label.js +151 -0
  29. package/dist/components/vf-list-item.d.ts +48 -0
  30. package/dist/components/vf-list-item.js +98 -0
  31. package/dist/components/vf-list.d.ts +77 -0
  32. package/dist/components/vf-list.js +256 -0
  33. package/dist/components/vf-menu-bar.d.ts +64 -0
  34. package/dist/components/vf-menu-bar.js +236 -0
  35. package/dist/components/vf-menu-item.d.ts +91 -0
  36. package/dist/components/vf-menu-item.js +252 -0
  37. package/dist/components/vf-menu.d.ts +95 -0
  38. package/dist/components/vf-menu.js +346 -0
  39. package/dist/components/vf-number-field.d.ts +60 -0
  40. package/dist/components/vf-number-field.js +248 -0
  41. package/dist/components/vf-option.d.ts +49 -0
  42. package/dist/components/vf-option.js +119 -0
  43. package/dist/components/vf-paragraph.d.ts +73 -0
  44. package/dist/components/vf-paragraph.js +66 -0
  45. package/dist/components/vf-progress-bar.d.ts +69 -0
  46. package/dist/components/vf-progress-bar.js +272 -0
  47. package/dist/components/vf-radio-group.d.ts +109 -0
  48. package/dist/components/vf-radio-group.js +105 -0
  49. package/dist/components/vf-radio.d.ts +59 -0
  50. package/dist/components/vf-radio.js +135 -0
  51. package/dist/components/vf-scroll-area.d.ts +75 -0
  52. package/dist/components/vf-scroll-area.js +124 -0
  53. package/dist/components/vf-select.d.ts +334 -0
  54. package/dist/components/vf-select.js +658 -0
  55. package/dist/components/vf-separator.d.ts +38 -0
  56. package/dist/components/vf-separator.js +55 -0
  57. package/dist/components/vf-slider.d.ts +80 -0
  58. package/dist/components/vf-slider.js +255 -0
  59. package/dist/components/vf-stack.d.ts +188 -0
  60. package/dist/components/vf-stack.js +168 -0
  61. package/dist/components/vf-swatch.d.ts +98 -0
  62. package/dist/components/vf-swatch.js +201 -0
  63. package/dist/components/vf-text-area.d.ts +77 -0
  64. package/dist/components/vf-text-area.js +121 -0
  65. package/dist/components/vf-text-field.d.ts +53 -0
  66. package/dist/components/vf-text-field.js +79 -0
  67. package/dist/components/vf-window.d.ts +157 -0
  68. package/dist/components/vf-window.js +377 -0
  69. package/dist/cursor-art.d.ts +37 -0
  70. package/dist/cursor-art.js +41 -0
  71. package/dist/cursor.d.ts +49 -0
  72. package/dist/cursor.js +171 -0
  73. package/dist/define.d.ts +45 -0
  74. package/dist/define.js +15 -0
  75. package/dist/document-listeners.d.ts +62 -0
  76. package/dist/document-listeners.js +29 -0
  77. package/dist/drag.d.ts +49 -0
  78. package/dist/drag.js +30 -0
  79. package/dist/events.d.ts +79 -0
  80. package/dist/events.js +25 -0
  81. package/dist/focus-modality.d.ts +94 -0
  82. package/dist/focus-modality.js +56 -0
  83. package/dist/form-control.d.ts +247 -0
  84. package/dist/form-control.js +127 -0
  85. package/dist/glyphs.d.ts +117 -0
  86. package/dist/glyphs.js +100 -0
  87. package/dist/grid-snap.d.ts +73 -0
  88. package/dist/grid-snap.js +135 -0
  89. package/dist/index.d.ts +145 -0
  90. package/dist/index.js +71 -0
  91. package/dist/menu-press.d.ts +60 -0
  92. package/dist/menu-press.js +97 -0
  93. package/dist/modal-dialog.d.ts +148 -0
  94. package/dist/modal-dialog.js +155 -0
  95. package/dist/motion.d.ts +72 -0
  96. package/dist/motion.js +22 -0
  97. package/dist/number.d.ts +24 -0
  98. package/dist/number.js +12 -0
  99. package/dist/open-art.d.ts +40 -0
  100. package/dist/open-art.js +35 -0
  101. package/dist/pixel-frame.d.ts +102 -0
  102. package/dist/pixel-frame.js +77 -0
  103. package/dist/popup-overflow.d.ts +157 -0
  104. package/dist/popup-overflow.js +45 -0
  105. package/dist/position.d.ts +187 -0
  106. package/dist/position.js +74 -0
  107. package/dist/scale.d.ts +259 -0
  108. package/dist/scale.js +133 -0
  109. package/dist/scroll-rail.d.ts +112 -0
  110. package/dist/scroll-rail.js +194 -0
  111. package/dist/scroll-state.d.ts +113 -0
  112. package/dist/scroll-state.js +55 -0
  113. package/dist/size.d.ts +47 -0
  114. package/dist/size.js +30 -0
  115. package/dist/styles/base.d.ts +38 -0
  116. package/dist/styles/body-font.d.ts +7 -0
  117. package/dist/styles/body-font.js +9 -0
  118. package/dist/styles/display-font.d.ts +7 -0
  119. package/dist/styles/display-font.js +9 -0
  120. package/dist/styles/recipes/body-face.d.ts +14 -0
  121. package/dist/styles/recipes/body-face.js +6 -0
  122. package/dist/styles/recipes/display-face.d.ts +23 -0
  123. package/dist/styles/recipes/display-face.js +10 -0
  124. package/dist/styles/recipes/field.d.ts +20 -0
  125. package/dist/styles/recipes/field.js +68 -0
  126. package/dist/styles/recipes/focus.d.ts +76 -0
  127. package/dist/styles/recipes/focus.js +39 -0
  128. package/dist/styles/recipes/host.d.ts +5 -0
  129. package/dist/styles/recipes/host.js +62 -0
  130. package/dist/styles/recipes/pattern.d.ts +80 -0
  131. package/dist/styles/recipes/pattern.js +82 -0
  132. package/dist/styles/recipes/scroll-rail.d.ts +69 -0
  133. package/dist/styles/recipes/scroll-rail.js +222 -0
  134. package/dist/styles/recipes/shadow.d.ts +13 -0
  135. package/dist/styles/recipes/shadow.js +5 -0
  136. package/dist/styles/recipes/static-text.d.ts +24 -0
  137. package/dist/styles/recipes/static-text.js +17 -0
  138. package/dist/styles/recipes/surface.d.ts +28 -0
  139. package/dist/styles/recipes/surface.js +27 -0
  140. package/dist/styles/recipes/tile.d.ts +122 -0
  141. package/dist/styles/recipes/tile.js +40 -0
  142. package/dist/styles/recipes/title-bar.d.ts +49 -0
  143. package/dist/styles/recipes/title-bar.js +134 -0
  144. package/dist/styles/recipes/toggle.d.ts +10 -0
  145. package/dist/styles/recipes/toggle.js +20 -0
  146. package/dist/styles/register-embedded-font.d.ts +64 -0
  147. package/dist/styles/register-embedded-font.js +30 -0
  148. package/dist/text-control.d.ts +131 -0
  149. package/dist/text-control.js +83 -0
  150. package/dist/tile-grid.d.ts +111 -0
  151. package/dist/tile-grid.js +53 -0
  152. package/dist/toggle-control.d.ts +60 -0
  153. package/dist/toggle-control.js +41 -0
  154. package/dist/track-width.d.ts +42 -0
  155. package/dist/track-width.js +30 -0
  156. package/dist/type-ahead.d.ts +35 -0
  157. package/dist/type-ahead.js +20 -0
  158. package/dist/zoom.d.ts +204 -0
  159. package/dist/zoom.js +105 -0
  160. package/docs/SPEC.md +2471 -0
  161. package/editor/vscode.html-custom-data.json +1320 -0
  162. package/editor/web-types.json +3237 -0
  163. package/package.json +118 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Pixel-exact stepped silhouettes for variable-size 1-bit chrome — the
3
+ * `clip-path` analog of the fixed-size sprite glyphs in `glyphs.ts`.
4
+ *
5
+ * A System 7 rounded rectangle is not a curve: it is a staircase of whole
6
+ * pixels. Native `border-radius` renders those corners as antialiased arcs,
7
+ * which is exactly the gray smudge the kit forbids. Instead we author each
8
+ * silhouette as a *corner trace* — the per-row left inset of the shape, in
9
+ * system pixels, read straight off the reference sheet
10
+ * read off the 1x button reference sheet — and compile it into a
11
+ * stepped `polygon()` whose every vertex is a `calc(var(--vf-scale, 1) * Npx)`
12
+ * multiple. Under display scaling each system pixel is a whole number of
13
+ * device pixels, so every edge lands on the device grid and nothing
14
+ * antialiases; the element's own `background` supplies the color, so the
15
+ * result stays `var(--vf-*)`-themeable like everything else (SPEC §1).
16
+ *
17
+ * Corners are anchored to their own edges (left/right via `100% - …`), so one
18
+ * polygon fits any width and height: the straight runs stretch, the traced
19
+ * corners never distort — the CSS equivalent of a 9-slice.
20
+ *
21
+ * A frame is drawn as a *difference of silhouettes*, exactly like QuickDraw's
22
+ * `FrameRoundRect`: paint the outer silhouette in the frame color, then paint
23
+ * the face silhouette (inset one pixel, with its own traced corners) on top.
24
+ * The 1px outline — including the 2px-wide diagonal step pixels at the
25
+ * corners — is simply the region the face doesn't cover. The reference's
26
+ * border pixels reproduce exactly; nothing is stroked.
27
+ */
28
+ /** A stepped silhouette: per-row corner insets traced from the reference. */
29
+ export interface SteppedProfile {
30
+ /**
31
+ * Left inset (system px) of each corner row, topmost first. Mirrored to all
32
+ * four corners; must be non-increasing (each row steps outward).
33
+ */
34
+ readonly corner: readonly number[];
35
+ /** Inset of the straight edges once the corner rows are exhausted. */
36
+ readonly edge: number;
37
+ /** Row (system px from the box top) where the silhouette begins. */
38
+ readonly start: number;
39
+ }
40
+ /**
41
+ * The push button's outer silhouette. Reference rows (80×20 sample, pressed
42
+ * state is solid ink so the trace is unambiguous): y0 inset 3, y1–y2 inset 1,
43
+ * then straight. Identical on the short row-3 buttons, so it holds for any
44
+ * control height.
45
+ */
46
+ export declare const BUTTON_FRAME: SteppedProfile;
47
+ /**
48
+ * The button's face (the white fill inside the 1px frame). Starts one row
49
+ * down: y1 from x3, y2 from x2, then 1px inside the straight edges. Painting
50
+ * this over {@link BUTTON_FRAME} leaves the reference's exact border pixels —
51
+ * row 0 full-width, `##`/`#` corner steps, 1px sides.
52
+ */
53
+ export declare const BUTTON_FACE: SteppedProfile;
54
+ /**
55
+ * Outer silhouette of the default-button ring. The reference ring box is the
56
+ * button box outset by {@link RING_INSET} on every side (88×28 around 80×20):
57
+ * y0 inset 5, y1 3, y2 2, y3–y4 1, then straight.
58
+ */
59
+ export declare const RING_FRAME: SteppedProfile;
60
+ /**
61
+ * The transparent hole inside the ring. The band is 3px thick: the hole opens
62
+ * at row 3 (inset 6, then 4, 4, then 3px inside the straight edges). Kept as
63
+ * a hole — not painted over — so the 1px gap between ring and button shows
64
+ * the surface behind, exactly like the sheet's alpha-0 gap pixels.
65
+ */
66
+ export declare const RING_HOLE: SteppedProfile;
67
+ /** How far the ring box outsets the button box on each side (system px). */
68
+ export declare const RING_INSET = 4;
69
+ /**
70
+ * The screen-corner mask: per-row width (system px) of the black staircase a
71
+ * compact Mac's ROM painted over the rounded corners of its CRT — what makes
72
+ * the System 7 menu bar's top corners read as rounded. Traced from a 1x
73
+ * screen capture (5×5 corner crop): y0 run 5, y1 3, y2 2, y3–y4 1. The same
74
+ * radius-5 staircase as {@link RING_FRAME}'s corner — QuickDraw had one
75
+ * vocabulary for round corners — but the opposite *object*: this is the ink
76
+ * outside the silhouette, not the silhouette, so it compiles through
77
+ * {@link steppedCornerClip} rather than the rect compilers.
78
+ */
79
+ export declare const SCREEN_CORNER: readonly number[];
80
+ /** Compile a profile into a stepped `polygon()` clip-path value. */
81
+ export declare const steppedRectClip: (p: SteppedProfile) => string;
82
+ /**
83
+ * Compile a corner-mask run list into the ink polygon for one *top* corner —
84
+ * the corner box minus the rounded silhouette, i.e. the region the screen
85
+ * mask blacked out. Runs are the mask's per-row width, topmost first; `side`
86
+ * mirrors the staircase (via `100% - …`) for the top-right corner. Clip an
87
+ * element `runs[0]` wide × `runs.length` tall (system px), anchored into its
88
+ * corner, and let its `background` supply the ink.
89
+ */
90
+ export declare const steppedCornerClip: (runs: readonly number[], side: "left" | "right") => string;
91
+ /**
92
+ * Compile an outer profile and a hole profile into a ring (a stepped donut).
93
+ *
94
+ * `polygon()` has no subpaths, so the hole uses the keyhole technique: walk
95
+ * the outer loop clockwise from the top-center, bridge straight down to the
96
+ * hole's top edge, walk the hole counter-clockwise, and bridge back up. The
97
+ * two bridge edges are coincident verticals at x 50%, so with the `evenodd`
98
+ * fill rule they cancel exactly — no visible seam — and the enclosed hole
99
+ * (crossed twice) stays transparent, keeping the classic gap between the
100
+ * default ring and the button see-through.
101
+ */
102
+ export declare const steppedRingClip: (outer: SteppedProfile, hole: SteppedProfile) => string;
@@ -0,0 +1,77 @@
1
+ //#region src/pixel-frame.ts
2
+ var e = {
3
+ corner: [
4
+ 3,
5
+ 1,
6
+ 1
7
+ ],
8
+ edge: 0,
9
+ start: 0
10
+ }, t = {
11
+ corner: [3, 2],
12
+ edge: 1,
13
+ start: 1
14
+ }, n = {
15
+ corner: [
16
+ 5,
17
+ 3,
18
+ 2,
19
+ 1,
20
+ 1
21
+ ],
22
+ edge: 0,
23
+ start: 0
24
+ }, r = {
25
+ corner: [
26
+ 6,
27
+ 4,
28
+ 4
29
+ ],
30
+ edge: 3,
31
+ start: 3
32
+ }, i = 4, a = [
33
+ 5,
34
+ 3,
35
+ 2,
36
+ 1,
37
+ 1
38
+ ], o = (e) => e === 0 ? "0" : `calc(var(--vf-scale, 1) * ${e}px)`, s = (e) => e === 0 ? "100%" : `calc(100% - var(--vf-scale, 1) * ${e}px)`, c = (e) => {
39
+ let t = [], n = e.corner[0] ?? e.edge;
40
+ for (let r = 1; r <= e.corner.length; r++) {
41
+ let i = e.corner[r] ?? e.edge;
42
+ i !== n && (t.push({
43
+ y: e.start + r,
44
+ from: n,
45
+ to: i
46
+ }), n = i);
47
+ }
48
+ return t;
49
+ }, l = (e) => {
50
+ let t = e.corner[0] ?? e.edge, n = c(e), r = [...n].reverse(), i = [];
51
+ i.push(`${o(t)} ${o(e.start)}`, `${s(t)} ${o(e.start)}`);
52
+ for (let e of n) i.push(`${s(e.from)} ${o(e.y)}`, `${s(e.to)} ${o(e.y)}`);
53
+ for (let e of r) i.push(`${s(e.to)} ${s(e.y)}`, `${s(e.from)} ${s(e.y)}`);
54
+ i.push(`${s(t)} ${s(e.start)}`, `${o(t)} ${s(e.start)}`);
55
+ for (let e of n) i.push(`${o(e.from)} ${s(e.y)}`, `${o(e.to)} ${s(e.y)}`);
56
+ for (let e of r) i.push(`${o(e.to)} ${o(e.y)}`, `${o(e.from)} ${o(e.y)}`);
57
+ return i;
58
+ }, u = (e) => `polygon(${l(e).join(", ")})`, d = (e, t) => {
59
+ let n = t === "left" ? o : s, r = e[0] ?? 0, i = [`${n(0)} ${o(0)}`, `${n(r)} ${o(0)}`];
60
+ for (let t = 1; t <= e.length; t++) {
61
+ let a = e[t] ?? 0;
62
+ a !== r && (i.push(`${n(r)} ${o(t)}`, `${n(a)} ${o(t)}`), r = a);
63
+ }
64
+ return `polygon(${i.join(", ")})`;
65
+ }, f = (e, t) => {
66
+ let n = l(e), r = l(t), i = `50% ${o(e.start)}`, a = `50% ${o(t.start)}`, s = [...n.slice(1), n[0]], c = [r[0], ...r.slice(1).reverse()];
67
+ return `polygon(evenodd, ${[
68
+ i,
69
+ ...s,
70
+ i,
71
+ a,
72
+ ...c,
73
+ a
74
+ ].join(", ")})`;
75
+ };
76
+ //#endregion
77
+ export { t as BUTTON_FACE, e as BUTTON_FRAME, n as RING_FRAME, r as RING_HOLE, i as RING_INSET, a as SCREEN_CORNER, d as steppedCornerClip, u as steppedRectClip, f as steppedRingClip };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Popup overflow — the geometry of a menu taller than the screen.
3
+ *
4
+ * System 7 never put a scrollbar on a menu. A popup whose list doesn't fit is
5
+ * **clipped**: the panel is drawn once, the overflowing items are simply not on
6
+ * screen, and the edge slot that has items beyond it shows a solid black arrow
7
+ * in place of a row. Resting the pointer on the arrow rolls the items through
8
+ * the panel one row at a time; once the last item is in view the arrow
9
+ * disappears. A list can overflow both edges at once, and the panel box itself
10
+ * never moves or resizes while open — scrolling rolls the items *inside* it.
11
+ *
12
+ * This module is the arithmetic of that, kept out of the component for the same
13
+ * reason `zoom.ts` is: it is pure, it is testable without a browser, and
14
+ * `vf-menu` will want the identical model when pull-down clamping lands (System
15
+ * 7 scrolled long pull-downs with the very same mechanism).
16
+ *
17
+ * Three ideas carry the whole thing.
18
+ *
19
+ * **The pill lattice.** A popup opens with the selected row's cell laid over
20
+ * the closed pill, which fixes an `idealTop` — the panel top at which that
21
+ * overlay is exact. Every clamp here is quantized to *whole rows* off that
22
+ * lattice, so a panel forced away from the screen edge lands one, two, five
23
+ * rows down rather than an arbitrary number of pixels: the row grid never
24
+ * shifts relative to the pill, and a clamped popup keeps the closed↔open label
25
+ * alignment that an un-quantized pixel clamp destroys.
26
+ *
27
+ * **The panel is as tall as the list wants, not as tall as the items it can
28
+ * currently show.** This is the part that looks wrong until you scroll it. A
29
+ * 12-item list that has to sit somewhere other than where the pill would put it
30
+ * keeps all 12 slots and slides whole rows to fit the screen; the item strip
31
+ * stays welded to the pill, so the slots the strip doesn't reach are drawn as
32
+ * empty white. That blank is not filler — it is the exact travel the list will
33
+ * roll through, so that scrolling to the end lands the strip flush with the
34
+ * panel, *precisely* full, both arrows retired. The kit reproduces it because
35
+ * System 7 did: it is visible in a screenshot of Find File's criteria popup
36
+ * (5 empty rows above `name`) and of the Character Set font menu (2 above
37
+ * `Athens`). Only when the whole list cannot fit the screen band at all does
38
+ * the panel stop at the band's capacity and keep an arrow permanently.
39
+ *
40
+ * **The two directions are one rule.** A panel pushed *up* the screen (the pill
41
+ * is low) reserves its blank at the **top** and rolls down into it; a panel
42
+ * pushed *down* (the pill is high, with a late item selected) reserves it at
43
+ * the **bottom** and rolls up into it. Nothing below distinguishes the cases —
44
+ * the sign of one integer does.
45
+ *
46
+ * **One integer.** `scroll` is how many rows the strip is rolled up past the
47
+ * panel's top edge, and slot `i` shows row `scroll + i`. It is free to leave
48
+ * the range `[0, rowCount − visibleSlots]` that would keep every slot filled,
49
+ * and *that* is the reserved blank: below 0 the first `−scroll` slots are
50
+ * empty, above `rowCount − visibleSlots` the last few are. The up arrow shows
51
+ * when `scroll > 0` (rows really are hidden above), the down arrow when
52
+ * `scroll < rowCount − visibleSlots` (rows really are hidden below).
53
+ * Everything else — which rows are pickable, where the highlight may legally
54
+ * sit, how far a keypress must roll the list — falls out of that. Note what the
55
+ * two conditions say about the opening state: whichever end the blank is on,
56
+ * the arrow pointing back at it is the one that is *off*. The blank is a
57
+ * one-way starting position, not somewhere the user can scroll back to.
58
+ */
59
+ /**
60
+ * The fewest row slots a clamped panel is drawn with. With both arrows shown,
61
+ * three slots is the minimum that leaves one *pickable* row between them — at
62
+ * two, the panel offers nothing to choose. A viewport too short for three rows
63
+ * (48 system px) is already outside the kit's world, so the floor is allowed to
64
+ * intrude on the screen-edge insets: a shaved inset beats a menu you cannot
65
+ * pick from.
66
+ */
67
+ export declare const MIN_VISIBLE_SLOTS = 3;
68
+ /** Inputs to {@link layoutClampedPopup}. Lengths in CSS px, already scaled. */
69
+ export interface PopupClampArgs {
70
+ /** Panel top at which the selected row overlays the pill exactly. */
71
+ idealTop: number;
72
+ /** Height of one option row. */
73
+ rowHeight: number;
74
+ /** The panel's border width (one side). */
75
+ border: number;
76
+ /** How many options the list holds. */
77
+ rowCount: number;
78
+ /** Top of the usable screen band (the top inset). */
79
+ viewTop: number;
80
+ /** Bottom of the usable screen band (viewport height minus the bottom inset). */
81
+ viewBottom: number;
82
+ }
83
+ /** The panel box and the scroll state a clamped popup opens with. */
84
+ export interface PopupLayout {
85
+ /** Panel border-box top, a whole number of rows off the pill lattice. */
86
+ panelTop: number;
87
+ /** Panel border-box height — a whole number of row slots plus both borders. */
88
+ panelHeight: number;
89
+ /** Row slots the panel is drawn with (`R`). */
90
+ visibleSlots: number;
91
+ /** Opening scroll (`S`) — negative when the panel opens with blank above. */
92
+ initialScroll: number;
93
+ }
94
+ /**
95
+ * Fit a popup's panel into the usable screen band, quantized to the pill
96
+ * lattice.
97
+ *
98
+ * The panel wants every row it has (`rowCount` slots) and wants to sit at
99
+ * `idealTop`. Both wishes bend a whole row at a time:
100
+ *
101
+ * - **Position** first. `scroll` *is* the panel's row offset from `idealTop`
102
+ * (`panelTop = idealTop + scroll × rowHeight`), because sliding the box
103
+ * while the strip stays on the lattice is the same act as scrolling the
104
+ * strip while the box stays put. So the search is for the integer `scroll`
105
+ * nearest 0 that puts the whole box inside the band — negative to slide the
106
+ * box up, positive to slide it down, with the reserved blank falling out at
107
+ * whichever end the strip no longer reaches.
108
+ * - **Height** only if position alone can't do it. Each row given up buys one
109
+ * row of freedom for `scroll`, so the loop hands back one slot at a time
110
+ * until an integer fits — which is at most a couple of iterations, and is
111
+ * also how a list far taller than the screen settles at the band's capacity.
112
+ *
113
+ * When neither has to bend — `initialScroll` 0 and `visibleSlots === rowCount`
114
+ * — the result is exactly the unclamped placement the popup has always used, so
115
+ * a list that fits is untouched by any of this.
116
+ */
117
+ export declare function layoutClampedPopup(args: PopupClampArgs): PopupLayout;
118
+ /**
119
+ * The lowest row index the user can pick right now. An arrow *overlays* the
120
+ * slot it occupies — an opaque white row covering the item beneath, which is
121
+ * the whole mechanism — so a shown up arrow costs the first slot its row. With
122
+ * blank reserved above (`scroll` negative) there is no up arrow and no hidden
123
+ * row, so the first item is simply row 0.
124
+ */
125
+ export declare function firstPickableRow(scroll: number): number;
126
+ /**
127
+ * The highest row index the user can pick right now — the mirror of the above,
128
+ * ending at the last row when the down arrow is absent (which covers both the
129
+ * flush case and blank reserved below).
130
+ */
131
+ export declare function lastPickableRow(scroll: number, rowCount: number, visibleSlots: number): number;
132
+ /**
133
+ * The scroll position that brings row `index` into a pickable slot, leaving it
134
+ * one slot clear of whichever arrow will still be showing. Already-pickable
135
+ * rows give the scroll back unchanged — including one outside the flush range,
136
+ * so opening a panel with reserved blank and then merely moving the highlight
137
+ * doesn't scrub that blank away.
138
+ *
139
+ * The two endpoints are the reason for the `+ 2` / `− 1` rather than the
140
+ * obvious `+ 1` / `− 0`: row 0 and row `rowCount − 1` are reached at
141
+ * `scroll = 0` and `scroll = rowCount − visibleSlots`, where the arrow in
142
+ * question has just disappeared and the row may take the edge slot itself.
143
+ *
144
+ * A deliberate move never re-enters the reserved blank at either end — it is a
145
+ * starting position the list rolls out of, not one it can roll back into.
146
+ */
147
+ export declare function ensureVisibleScroll(index: number, scroll: number, rowCount: number, visibleSlots: number): number;
148
+ /**
149
+ * Hold a scroll inside its legal range. Normally that is `0` (row 0 in slot 0)
150
+ * to `rowCount − visibleSlots` (the last row in the last slot) — but a panel
151
+ * that opened with reserved blank starts *outside* that range at whichever end
152
+ * the blank is on, and the widened bound (`min(from, 0)`, `max(from, …)`) is
153
+ * what lets it roll out of the blank while never letting it roll back in.
154
+ * Symmetric in both directions, and it narrows to the normal range the moment
155
+ * the list reaches it.
156
+ */
157
+ export declare function clampScroll(scroll: number, from: number, rowCount: number, visibleSlots: number): number;
@@ -0,0 +1,45 @@
1
+ //#region src/popup-overflow.ts
2
+ var e = .001;
3
+ function t(t) {
4
+ let { idealTop: n, rowHeight: r, border: i, rowCount: a, viewTop: o, viewBottom: s } = t;
5
+ if (a <= 0 || r <= 0) return {
6
+ panelTop: n,
7
+ panelHeight: 2 * i,
8
+ visibleSlots: 0,
9
+ initialScroll: 0
10
+ };
11
+ let c = Math.min(a, 3), l = Math.floor((s - o - 2 * i) / r + e), u = Math.min(a, Math.max(l, c)), d = 0;
12
+ for (;;) {
13
+ let t = u * r + 2 * i, a = Math.ceil((o - n) / r - e), l = Math.floor((s - t - n) / r + e);
14
+ if (a <= l) {
15
+ d = Math.min(Math.max(0, a), l);
16
+ break;
17
+ }
18
+ if (u <= c) {
19
+ d = Math.max(0, a);
20
+ break;
21
+ }
22
+ --u;
23
+ }
24
+ return {
25
+ panelTop: n + d * r,
26
+ panelHeight: u * r + 2 * i,
27
+ visibleSlots: u,
28
+ initialScroll: d
29
+ };
30
+ }
31
+ function n(e) {
32
+ return e > 0 ? e + 1 : 0;
33
+ }
34
+ function r(e, t, n) {
35
+ return e < t - n ? e + n - 2 : t - 1;
36
+ }
37
+ function i(e, t, i, o) {
38
+ let s = t;
39
+ return e < n(t) ? s = e === 0 ? 0 : e - 1 : e > r(t, i, o) && (s = e === i - 1 ? i - o : e - o + 2), a(s, t, i, o);
40
+ }
41
+ function a(e, t, n, r) {
42
+ return Math.max(Math.min(t, 0), Math.min(e, Math.max(t, n - r)));
43
+ }
44
+ //#endregion
45
+ export { a as clampScroll, i as ensureVisibleScroll, n as firstPickableRow, r as lastPickableRow, t as layoutClampedPopup };
@@ -0,0 +1,187 @@
1
+ import type { LitElement } from 'lit';
2
+ type Constructor<T = object> = new (...args: any[]) => T;
3
+ /**
4
+ * The mixin's surface, as a `declare class` for the same TS4094 reason
5
+ * {@link VfToggleControlInterface} states: TypeScript cannot name the
6
+ * anonymous class a mixin returns when emitting declarations. Keep it in sync
7
+ * with the implementation below.
8
+ */
9
+ export declare abstract class VfPositionedInterface extends LitElement {
10
+ top?: number | null;
11
+ left?: number | null;
12
+ }
13
+ /**
14
+ * Explicit placement — `top`/`left` in whole system px, on (nearly) every
15
+ * component.
16
+ *
17
+ * A DITL resource laid a dialog out as a list of items, each with a rectangle
18
+ * in the window's own coordinates; arranging controls by stating where they go
19
+ * is as native to System 7 as stacking them. This mixin is that mechanism:
20
+ * declaring `top` or `left` takes the element out of normal flow and
21
+ * absolutely positions it within its parent, both coordinates in the art's own
22
+ * unit so the position scales with the display like every other metric. A
23
+ * window or dialog body can be laid out either way — a tree of `vf-stack`s, or
24
+ * positioned children — with no stylesheet in either case.
25
+ *
26
+ * Mechanics, mirroring how `vf-stack` writes its declared size:
27
+ *
28
+ * - Setting either property writes `position: absolute` and both offsets onto
29
+ * the host's inline style as `calc(var(--vf-scale, 1) * Npx)` — live against
30
+ * the display, resolved at paint time. The coordinate left unset is 0.
31
+ * - `right`/`bottom` are released to `auto` and `margin` zeroed, so the stated
32
+ * offsets are the whole story — an auto-width box with both edges set would
33
+ * stretch rather than sit (the `vf-icon` seed logic, generalized).
34
+ * - Unsetting both returns the element to normal flow: every inline
35
+ * declaration this wrote is removed, and stylesheet values resume.
36
+ *
37
+ * The anchor is CSS's own: the nearest positioned ancestor's padding box. The
38
+ * kit's containers are all deliberate anchors — a desktop's raster, a window's
39
+ * content region (the frame's inner edge, below the title bar — the 12px body
40
+ * inset governs flow content only, exactly the DITL convention), a dialog's
41
+ * content area, a stack's box, a fieldset's border box, a scroll area's
42
+ * scrolled plane. In a non-kit parent, give the parent `position: relative`,
43
+ * the one line of CSS this feature can't write for you.
44
+ *
45
+ * The writing rides a {@link ReactiveController} (`hostUpdated`) rather than
46
+ * an `updated()` override, for a lifecycle reason worth keeping in the source:
47
+ * the mixin sits *under* each component class, and most components override
48
+ * `updated()` without calling `super.updated()` — Lit's base is a no-op, so
49
+ * nothing ever forced the call — which would silently shadow a mixin-level
50
+ * override. A controller is invoked by ReactiveElement itself, after every
51
+ * update, no matter what the subclass does.
52
+ *
53
+ * A gesture writes **through these properties**: `vf-window`'s title-bar drag
54
+ * and `vf-icon`'s drag and arrow nudge hand their new origin to a
55
+ * {@link PlacementController}, which snaps it onto the placement lattice and
56
+ * sets `left`/`top`. A moved element is therefore placed exactly the way an
57
+ * authored one is — a live `calc()` in the art's own unit — and stays where it
58
+ * was dropped when the zoom or the display changes what a system px costs.
59
+ *
60
+ * It did not always. Writing the resolved CSS px straight to the inline style
61
+ * froze the coordinate in the wrong unit: `--vf-scale` moved under it at every
62
+ * zoom step and the same constant read back as a different number of system px,
63
+ * so moved windows and icons slid off the grid the rest of the kit stayed on
64
+ * (by `3z / round(3z)` — 10% at 110% zoom, where nothing else moves at all).
65
+ *
66
+ * Setting a property yourself is still the deliberate way to re-place a moved
67
+ * element. The controller re-applies **only when the values changed**, so an
68
+ * unrelated update — a heading change, a desktop toggling `active` — never
69
+ * re-asserts a coordinate and costs nothing.
70
+ */
71
+ export declare const VfPositioned: <T extends Constructor<LitElement>>(Base: T) => Constructor<VfPositionedInterface> & T;
72
+ /** The box a gesture is clamped into, in system px — see {@link PlacementClamp}. */
73
+ export type PlacementBounds = {
74
+ width: number;
75
+ height: number;
76
+ };
77
+ /**
78
+ * A moved host's own containment rule, in system px, against the box measured
79
+ * once at the start of the gesture.
80
+ */
81
+ export type PlacementClamp = (x: number, y: number, bounds: PlacementBounds) => {
82
+ x: number;
83
+ y: number;
84
+ };
85
+ /**
86
+ * The gesture half of placement: the piece a *movable* host adds on top of
87
+ * {@link VfPositioned}, so a drag ends up in the same `top`/`left` system-px
88
+ * properties an author would have written.
89
+ *
90
+ * Two things it owns, both consequences of storing the origin in the art's own
91
+ * unit rather than in resolved CSS px:
92
+ *
93
+ * - **Seeding.** A gesture starts from wherever the host already is, which may
94
+ * be a coordinate nobody stated in system px — an authored `left: 10%`, a
95
+ * `right`-anchored Trash icon, or plain normal flow. {@link seed} reads the
96
+ * *used* position and converts it once; from then on the properties are the
97
+ * whole truth.
98
+ * - **The lattice.** Every write is snapped to {@link snapSys} — whole art
99
+ * pixels, the way QuickDraw moved windows, and the k-system-px run that also
100
+ * lands the edge on a whole CSS px (scale.ts explains why that second half
101
+ * matters to a scroll rail).
102
+ *
103
+ * It holds no state the host's own properties don't already hold, so it takes
104
+ * no lifecycle and is not a {@link ReactiveController}: the writes it makes are
105
+ * ordinary property sets, and `VfPositioned`'s controller renders them.
106
+ *
107
+ * And one thing it deliberately does **not** do: re-snap a placed host when the
108
+ * scale changes. The lattice moves with the scale (k is 2 at dpr 2, 3 at that
109
+ * display's 150%), so a coordinate dropped on one rung sits between two on the
110
+ * next — but re-rounding it there is worse than leaving it. It is lossy, and it
111
+ * compounds: 62 → 63 at 150% → 64 at 200%, a window walking away from where it
112
+ * was dropped one zoom step at a time, which is a milder version of the exact
113
+ * bug this class exists to fix (`verify:zoom` group (e) fails on it). Whole
114
+ * system px is whole *device* px at every rung by the scale contract, so the
115
+ * art stays crisp regardless; only the whole-CSS-px edge is given up, and an
116
+ * authored `left="10"` gives that up already. A dropped coordinate is therefore
117
+ * as immutable as an authored one — which is the whole claim.
118
+ */
119
+ export declare class PlacementController {
120
+ #private;
121
+ constructor(host: HTMLElement & {
122
+ top?: number | null;
123
+ left?: number | null;
124
+ }, clamp: PlacementClamp);
125
+ /** Whether a gesture has placed this host (as opposed to markup or CSS). */
126
+ get placed(): boolean;
127
+ /**
128
+ * The origin a move adds its delta to, in system px.
129
+ *
130
+ * A stated coordinate is authoritative and needs no measuring — including
131
+ * the one this controller wrote last time. Otherwise the host is wherever
132
+ * layout or a stylesheet put it, so the *used* position is read and
133
+ * converted: absolutely positioned hosts through their computed offsets
134
+ * (`left: 10%` and `left: 1em` are perfectly good ways to place one, and
135
+ * both resolve to px here), everything else through its in-flow offset,
136
+ * which is measured against the same padding box `left`/`top` will be.
137
+ */
138
+ seed(): {
139
+ x: number;
140
+ y: number;
141
+ };
142
+ /**
143
+ * Place the host at a system-px origin, clamped by its own rule and snapped.
144
+ *
145
+ * The clamp runs *here*, during the gesture, which is the moment the user is
146
+ * actually pushing against an edge — and nowhere else. Re-clamping later, on
147
+ * a parent that shrank under a zoom, would move a host nobody moved and would
148
+ * not give the position back when the parent grew again.
149
+ *
150
+ * It runs against the box {@link seed} measured, not a fresh one. The
151
+ * difference only shows when the movable contract is unmet: a host with no
152
+ * stated origin is in normal flow, so its first move takes it out — and an
153
+ * auto-height parent then collapses to whatever is left, which is a box the
154
+ * host never actually sat in. Re-measuring per move would clamp the rest of
155
+ * the gesture into that phantom, walking the host to the parent's origin
156
+ * while the user drags away from it. The box at the moment of the press is
157
+ * the one the user is pushing against.
158
+ */
159
+ moveTo(x: number, y: number): void;
160
+ }
161
+ /**
162
+ * The movable contract, as a one-time console warning.
163
+ *
164
+ * A host that moves under a gesture states its own origin, and its positioning
165
+ * parent is a box with a size. Neither half is something the component can
166
+ * supply for itself, and both fail quietly rather than loudly:
167
+ *
168
+ * - **Unplaced.** A host still in normal flow has to be taken out of it by its
169
+ * first move — which reflows everything after it and can collapse the very
170
+ * parent the clamp is about to measure. What matters is being out of flow,
171
+ * not which mechanism did it: `top`/`left` are the kit's way and the one that
172
+ * scales, but a stylesheet's own `position: absolute` satisfies it too, and
173
+ * {@link PlacementController.seed} reads that case from the computed offsets.
174
+ * - **Unsized parent.** A positioning parent with no box gives the clamp no
175
+ * range, and the host cannot be dragged away from the parent's origin at all.
176
+ * Only checked for a real ancestor: with no positioned ancestor the host
177
+ * resolves against the initial containing block and the clamp falls back to
178
+ * the viewport, which agree with each other and need no warning.
179
+ *
180
+ * Returns whether it warned, so the caller can latch — one warning per element,
181
+ * not per render.
182
+ */
183
+ export declare function warnMovableContract(host: HTMLElement & {
184
+ top?: number | null;
185
+ left?: number | null;
186
+ }, what: string, example: string): boolean;
187
+ export {};
@@ -0,0 +1,74 @@
1
+ import { snapSys as e, sysLength as t, toSysExact as n } from "./scale.js";
2
+ import r from "./_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
3
+ import { property as i } from "lit/decorators.js";
4
+ //#region src/position.ts
5
+ var a = (e) => {
6
+ class t extends e {
7
+ constructor(...e) {
8
+ super(...e), new o(this);
9
+ }
10
+ }
11
+ return r([i({ type: Number })], t.prototype, "top", void 0), r([i({ type: Number })], t.prototype, "left", void 0), t;
12
+ }, o = class {
13
+ #e;
14
+ #t = null;
15
+ #n = null;
16
+ #r = !1;
17
+ constructor(e) {
18
+ this.#e = e, e.addController(this);
19
+ }
20
+ hostUpdated() {
21
+ let e = this.#e.top ?? null, n = this.#e.left ?? null, r = this.#e.style;
22
+ if (e === null && n === null) {
23
+ if (!this.#r) return;
24
+ this.#r = !1, r.removeProperty("position"), r.removeProperty("top"), r.removeProperty("left"), r.removeProperty("right"), r.removeProperty("bottom"), r.removeProperty("margin");
25
+ return;
26
+ }
27
+ this.#r && e === this.#t && n === this.#n || (this.#r = !0, this.#t = e, this.#n = n, r.position = "absolute", r.top = t(e ?? 0), r.left = t(n ?? 0), r.right = "auto", r.bottom = "auto", r.margin = "0");
28
+ }
29
+ }, s = class {
30
+ #e;
31
+ #t;
32
+ #n = !1;
33
+ #r = null;
34
+ constructor(e, t) {
35
+ this.#e = e, this.#t = t;
36
+ }
37
+ get placed() {
38
+ return this.#n;
39
+ }
40
+ seed() {
41
+ let t = this.#e;
42
+ if (this.#r = this.#i(), t.left != null || t.top != null) return {
43
+ x: t.left ?? 0,
44
+ y: t.top ?? 0
45
+ };
46
+ let r = getComputedStyle(t), i = r.position === "absolute" || r.position === "fixed", a = i ? parseFloat(r.left) || 0 : t.offsetLeft, o = i ? parseFloat(r.top) || 0 : t.offsetTop;
47
+ return {
48
+ x: e(n(a, t), t),
49
+ y: e(n(o, t), t)
50
+ };
51
+ }
52
+ moveTo(t, n) {
53
+ let r = this.#e, i = this.#t(t, n, this.#r ?? this.#i());
54
+ r.left = e(i.x, r), r.top = e(i.y, r), this.#n = !0;
55
+ }
56
+ #i() {
57
+ let e = this.#e, t = e.offsetParent, r = t && t.clientWidth > 0 && t.clientHeight > 0 ? t : null;
58
+ return {
59
+ width: n(r?.clientWidth ?? window.innerWidth, e),
60
+ height: n(r?.clientHeight ?? window.innerHeight, e)
61
+ };
62
+ }
63
+ };
64
+ function c(e, t, n) {
65
+ let r = e.top != null || e.left != null, i = getComputedStyle(e).position, a = i === "absolute" || i === "fixed", o = null;
66
+ if (!r && !a) o = "unplaced";
67
+ else {
68
+ let t = e.offsetParent;
69
+ t && t !== e.ownerDocument.body && (t.clientWidth === 0 || t.clientHeight === 0) && (o = "unsized-parent");
70
+ }
71
+ return o !== null && (console.warn(o === "unplaced" ? `${t}: movable, but in normal flow — so the first drag has to pull it out, reflowing everything after it on the page. State the origin in system px and it never was in flow: ${n}` : `${t}: the positioning parent has no box, so there is nowhere to drag to. Give it a size (and \`position: relative\`, if it is not a kit container) — the clamp is falling back to the viewport meanwhile.`), !0);
72
+ }
73
+ //#endregion
74
+ export { s as PlacementController, a as VfPositioned, c as warnMovableContract };