vintage-frames 0.7.2 → 0.9.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.
@@ -156,9 +156,11 @@ export declare class VfDesktop extends VfDesktop_base {
156
156
  * in whole system px on the placement lattice, measured from the raster's
157
157
  * corner with the bezel excluded: the pair a child dropped onto the desktop
158
158
  * is written with. The screen sits in the shadow tree, which is why the
159
- * conversion is a method here.
159
+ * conversion is a method here. With `child`, the pair to write to that
160
+ * element: the offset its `origin` adds is folded in, so its box's corner
161
+ * lands on the point.
160
162
  */
161
- placementAt(clientX: number, clientY: number): {
163
+ placementAt(clientX: number, clientY: number, child?: Element): {
162
164
  left: number;
163
165
  top: number;
164
166
  };
@@ -199,8 +199,8 @@ var j = 1e6, M = 2 * j, N = 2000001, P = 512, F = 342, I = "gray-50", L = m(f[I]
199
199
  height: this.height
200
200
  };
201
201
  }
202
- placementAt(e, t) {
203
- return s(this.screen ?? this, e, t, this);
202
+ placementAt(e, t, n) {
203
+ return s(this.screen ?? this, e, t, this, n);
204
204
  }
205
205
  #e;
206
206
  connectedCallback() {
@@ -1,5 +1,4 @@
1
1
  import { VfModalDialog } from '../modal-dialog.js';
2
- import './vf-button-group.js';
3
2
  /**
4
3
  * `<vf-dialog>` — the System 7 modal dialog shell.
5
4
  *
@@ -16,9 +15,8 @@ import './vf-button-group.js';
16
15
  * say no), so the component enables either reading rather than enforcing
17
16
  * one.
18
17
  * - **`frame="plain"`:** the modal dialog box — the bare frame, no title bar —
19
- * and immovable, like the original. A `heading` renders as a centered
20
- * display-face heading at the top of the body (the reference art's "Dialog
21
- * title"); `closable` is ignored, there being no bar to carry the widget.
18
+ * and immovable, like the original. With no bar, `heading` only names the
19
+ * dialog and `closable` is ignored.
22
20
  *
23
21
  * Open it with `show()` (or set the `open` attribute/property); close with
24
22
  * `close()`. Escape closes it and fires `vf-close` with
@@ -36,21 +34,20 @@ import './vf-button-group.js';
36
34
  * multi-line editor Return inserts the newline while the keypad's Enter
37
35
  * activates the button.
38
36
  *
39
- * @slot - Default slot: dialog body content.
40
- * @slot buttons - Optional action buttons. Rendered as a bottom-right
41
- * `vf-button-group` (equal-width, faces aligned); the footer only takes
42
- * space when the slot is populated.
37
+ * Slotted children placed with `top`/`left` measure from the content region's
38
+ * corner — the frame's inner edge, below the title bar — and flow content
39
+ * starts at the same corner.
40
+ *
41
+ * @slot - Default slot: the dialog's content.
43
42
  * @csspart frame - The outer frame (the double frame's 1px rule; the bar and
44
43
  * the inner band sit inside it).
45
44
  * @csspart title-bar - The striped title bar (default chrome only).
46
- * @csspart title - The centered title patch (or the plain-frame heading).
45
+ * @csspart title - The centered title patch (default chrome only).
47
46
  * @csspart close-box - The close widget (`closable`, default chrome only).
48
- * @csspart body - The white content area.
49
- * @csspart content - The scrolling region inside the body (heading + slotted
50
- * content, not the footer). Inert while the content fits; over-stuffed, it
51
- * scrolls under a System 7 rail and becomes a keyboard stop.
52
- * @csspart footer - The action row wrapping the buttons.
53
- * @csspart buttons - The button group inside the footer.
47
+ * @csspart body - The white content region.
48
+ * @csspart content - The scrolling region inside the body (the slotted
49
+ * content). Inert while the content fits; over-stuffed, it scrolls under a
50
+ * System 7 rail and becomes a keyboard stop.
54
51
  * @fires vf-close - Dialog closed. Detail `{ reason: 'escape' | 'close' |
55
52
  * 'outside' }` — `'outside'` only under `light-dismiss`.
56
53
  * @cssprop --vf-dots-pattern - the windoid bar's dot-grid dither — a 2×2 tile,
@@ -75,12 +72,15 @@ export declare class VfDialog extends VfModalDialog {
75
72
  private readonly _drag;
76
73
  /** Holds the centered title patch on the placement lattice (src/chrome.ts). */
77
74
  private readonly _titleCenter;
78
- /** Title text: the bar's centered patch, or the plain frame's heading. */
75
+ /**
76
+ * Title text: the bar's centered patch, and the dialog's accessible name.
77
+ * With `frame="plain"`, which has no bar, it only names the dialog.
78
+ */
79
79
  heading: string;
80
80
  /**
81
81
  * Accessible name for the dialog (`aria-label`), for a dialog with no
82
82
  * `heading` — an untitled title bar has no text to be named by. Ignored when
83
- * empty and a `heading` is set (the title patch names the dialog then);
83
+ * empty and a `heading` is set (the heading names the dialog then);
84
84
  * defaults to `'Dialog'` when neither is given.
85
85
  */
86
86
  label: string;
@@ -96,8 +96,6 @@ export declare class VfDialog extends VfModalDialog {
96
96
  * box (the bare double frame, no bar).
97
97
  */
98
98
  frame?: 'plain';
99
- /** Whether the `buttons` slot has assigned content (drives the footer). */
100
- private _hasButtons;
101
99
  private _content;
102
100
  /** Whether the body content overflows its box (drives the content stop). */
103
101
  private _scrollable;
@@ -112,7 +110,6 @@ export declare class VfDialog extends VfModalDialog {
112
110
  private _onBodySlotChange;
113
111
  private _onCloseClick;
114
112
  protected render(): unknown;
115
- private _onButtonsSlotChange;
116
113
  }
117
114
  declare global {
118
115
  interface HTMLElementTagNameMap {
@@ -1,26 +1,24 @@
1
1
  import { vfElement as e } from "../define.js";
2
2
  import { snapSys as t, toSysExact as n } from "../scale.js";
3
3
  import r from "../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
4
- import { vfDisplayDecls as i } from "../styles/recipes/display-face.js";
5
- import { vfBase as a } from "../styles/recipes/host.js";
6
- import { vfStripes as o } from "../styles/recipes/pattern.js";
7
- import { vfModalFrame as s } from "../styles/recipes/surface.js";
8
- import { vfTitleBar as c, vfWindowWidgets as l } from "../styles/recipes/title-bar.js";
9
- import { vfFocus as u, vfFocusRing as d } from "../styles/recipes/focus.js";
10
- import { vfScrollRail as f } from "../styles/recipes/scroll-rail.js";
11
- import { DragController as p } from "../drag.js";
12
- import { TitleCenterController as m, chromeTitleBar as h, closeBox as g, widgetLabel as _ } from "../chrome.js";
13
- import { ScrollStateController as v } from "../scroll-state.js";
14
- import { ScrollRailController as y, renderScrollRail as b } from "../scroll-rail.js";
15
- import { VfModalDialog as x, modalDialogStyles as S } from "../modal-dialog.js";
16
- import "./vf-button-group.js";
17
- import { css as C, html as w, nothing as T } from "lit";
18
- import { property as E, query as D, state as O } from "lit/decorators.js";
19
- import { styleMap as k } from "lit/directives/style-map.js";
4
+ import { vfBase as i } from "../styles/recipes/host.js";
5
+ import { vfStripes as a } from "../styles/recipes/pattern.js";
6
+ import { vfModalFrame as o } from "../styles/recipes/surface.js";
7
+ import { vfTitleBar as s, vfWindowWidgets as c } from "../styles/recipes/title-bar.js";
8
+ import { vfFocus as l, vfFocusRing as u } from "../styles/recipes/focus.js";
9
+ import { vfScrollRail as d } from "../styles/recipes/scroll-rail.js";
10
+ import { DragController as f } from "../drag.js";
11
+ import { TitleCenterController as p, chromeTitleBar as m, closeBox as h, widgetLabel as g } from "../chrome.js";
12
+ import { ScrollStateController as _ } from "../scroll-state.js";
13
+ import { ScrollRailController as v, renderScrollRail as y } from "../scroll-rail.js";
14
+ import { VfModalDialog as b, modalDialogStyles as x } from "../modal-dialog.js";
15
+ import { css as S, html as C, nothing as w } from "lit";
16
+ import { property as T, query as E, state as D } from "lit/decorators.js";
17
+ import { styleMap as O } from "lit/directives/style-map.js";
20
18
  //#region src/components/vf-dialog.ts
21
- var A = class extends x {
19
+ var k = class extends b {
22
20
  constructor(...e) {
23
- super(...e), this._drag = new p(this, {
21
+ super(...e), this._drag = new f(this, {
24
22
  onDragStart: (e) => {
25
23
  if (e.button !== 0 || e.composedPath().some((e) => e instanceof HTMLElement && e.classList.contains("box"))) return null;
26
24
  let r = this._dialog;
@@ -36,21 +34,21 @@ var A = class extends x {
36
34
  };
37
35
  },
38
36
  onDrag: (e, t) => this.placeAt(e, t)
39
- }), this._titleCenter = new m(this), this.heading = "", this.label = "", this.closable = !1, this._hasButtons = !1, this._scrollable = !1, this._scrollState = new v(this, () => this._content, void 0, (e) => {
37
+ }), this._titleCenter = new p(this), this.heading = "", this.label = "", this.closable = !1, this._scrollable = !1, this._scrollState = new _(this, () => this._content, void 0, (e) => {
40
38
  this._scrollable = e.y;
41
- }), this._rail = new y(this, { getScroll: () => this._content });
39
+ }), this._rail = new v(this, { getScroll: () => this._content });
42
40
  }
43
41
  static {
44
42
  this.styles = [
43
+ i,
45
44
  a,
45
+ l,
46
46
  o,
47
- u,
48
47
  s,
49
48
  c,
50
- l,
51
- f,
52
- S,
53
- C`
49
+ d,
50
+ x,
51
+ S`
54
52
  :host {
55
53
  display: contents;
56
54
  }
@@ -86,34 +84,27 @@ var A = class extends x {
86
84
  :host([closable]) .vf-title {
87
85
  --vf-title-inset: 60px;
88
86
  }
89
- /* Takes the slack under the title bar. A modal is a fixed box — the
90
- frame never grows with its body — but content taller than the box is
91
- no longer silently clipped: the .content region below scrolls it
92
- under a System 7 rail, with the footer pinned outside the scroll so
93
- the action buttons stay reachable. overflow: hidden stays as the
94
- frame-level backstop. A slotted vf-select's list still escapes: it is
95
- position:fixed off the control's own rect (see vf-select.ts). */
87
+ /* The content region: takes the slack under the title bar, and is the
88
+ positioning anchor for slotted children placed with top/left
89
+ (src/position.ts) — coordinates measure from the frame's inner edge,
90
+ exactly the DITL convention. No inset of its own: flow content starts
91
+ at the same corner a placed child does. A modal is a fixed box — the
92
+ frame never grows with its body — but flow content taller than the
93
+ box is not silently clipped: the .content region below scrolls it
94
+ under a System 7 rail. Placed children anchor here, outside that
95
+ scroller, so they hold still while it scrolls. overflow: hidden stays
96
+ as the frame-level backstop. A slotted vf-select's list still
97
+ escapes: it is position:fixed off the control's own rect (see
98
+ vf-select.ts). */
96
99
  .body {
97
100
  --vf-surface: var(--vf-white, #ffffff);
98
101
  background: var(--vf-white, #ffffff);
102
+ position: relative;
99
103
  display: flex;
100
104
  flex-direction: column;
101
105
  flex: 1 1 auto;
102
106
  min-height: 0;
103
107
  overflow: hidden;
104
- padding: calc(var(--vf-scale, 1) * 16px);
105
- }
106
- /* The scroll region's positioned wrapper (the .scroll-frame overlay and
107
- the rail overlay anchor against it). Shrink-only (flex-grow 0): with
108
- slack in the box the footer sits right after the content, exactly
109
- where block flow put it before this wrapper existed — the wrap only
110
- gives height back when the content doesn't fit. */
111
- .content-wrap {
112
- position: relative;
113
- display: flex;
114
- flex-direction: column;
115
- flex: 0 1 auto;
116
- min-height: 0;
117
108
  }
118
109
  /* hidden, not auto, until the controller says otherwise — the two have
119
110
  to agree on what "overflowing" means or the dialog scrolls without a
@@ -134,73 +125,38 @@ var A = class extends x {
134
125
  overflow-y: hidden;
135
126
  }
136
127
  /* Only while genuinely over-stuffed (ScrollStateController's measured
137
- signal) does the region scroll — reserving the 16px channel as its
138
- own padding (what the native gutter used to reserve) — and only then
139
- do the rail and the 1px frame boxing it appear. While the content
140
- fits, none of this matches and the body renders exactly as it always
141
- has. */
128
+ signal) does the region scroll — reserving the rail's 15px as its
129
+ own padding — and only then does the rail appear. While the content
130
+ fits, none of this matches. */
142
131
  .content[data-overflow-y='true'] {
143
132
  overflow-y: scroll;
144
- padding-right: calc(var(--vf-scale, 1) * 16px);
145
- }
146
- /* The rail rides the wrapper as an OVERLAY pinned to its right edge,
147
- inside the boxing frame's lines (the 1px insets; the fourth side is
148
- its own divider) — deliberately out of the layout flow: a rail
149
- column's two fixed 15px arrow cells would hand the region a 32px
150
- minimum height, and a short dialog would then measure as fitting
151
- with the rail shown and overflowing without it, flip-flopping
152
- forever. An overlay cannot move the box; the channel the content
153
- pays for is the padding above. Hidden until the region actually
154
- scrolls. */
155
- .content-wrap .vf-rail {
133
+ padding-right: calc(var(--vf-scale, 1) * 15px);
134
+ }
135
+ /* The rail rides the body as an OVERLAY pinned to its right edge, flush
136
+ against the frame's inner band — the band is the classic 16px cell's
137
+ outer line on three sides; the fourth is the rail's own divider —
138
+ deliberately out of the layout flow: a rail column's two fixed 15px
139
+ arrow cells would hand the region a 32px minimum height, and a short
140
+ dialog would then measure as fitting with the rail shown and
141
+ overflowing without it, flip-flopping forever. An overlay cannot move
142
+ the box; the channel the content pays for is the padding above.
143
+ Hidden until the region actually scrolls. */
144
+ .body > .vf-rail {
156
145
  position: absolute;
157
- top: calc(var(--vf-scale, 1) * 1px);
158
- right: calc(var(--vf-scale, 1) * 1px);
159
- bottom: calc(var(--vf-scale, 1) * 1px);
146
+ top: 0;
147
+ right: 0;
148
+ bottom: 0;
160
149
  display: none;
161
150
  }
162
151
  .content[data-overflow-y='true'] ~ .vf-rail {
163
152
  display: grid;
164
153
  }
165
- /* The 1px frame boxing the scrolling region and its rail — an overlay
166
- (spanning both grid columns via inset 0) so the fitting dialog keeps
167
- no reserved line. */
168
- .scroll-frame {
169
- position: absolute;
170
- inset: 0;
171
- border: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
172
- pointer-events: none;
173
- display: none;
174
- }
175
- .content[data-overflow-y='true'] ~ .scroll-frame {
176
- display: block;
177
- }
178
154
  /* A scrollable region is a keyboard stop (tabindex in the template);
179
155
  mark it with the kit's dotted ring, inset to stay in-box — the same
180
156
  treatment as vf-scroll-area's viewport. */
181
157
  .content:focus-visible {
182
158
  --vf-focus-offset: -2px;
183
- ${d}
184
- }
185
- /* The plain frame's heading: centered chrome type at the top of the
186
- body, the way dBoxProc dialogs drew their title in content (see
187
- Windows/modal dialog.png). No patch, no ellipsis — it's body-top
188
- text, not a bar. */
189
- .plain-heading {
190
- ${i}
191
- display: block;
192
- text-align: center;
193
- margin-bottom: calc(var(--vf-scale, 1) * 16px);
194
- }
195
- /* Optional action row (slot="buttons"): a right-aligned vf-button-group
196
- that only takes space when populated. */
197
- .footer {
198
- display: flex;
199
- justify-content: flex-end;
200
- margin-top: calc(var(--vf-scale, 1) * 16px);
201
- }
202
- .footer.empty {
203
- display: none;
159
+ ${u}
204
160
  }
205
161
  `
206
162
  ];
@@ -212,44 +168,30 @@ var A = class extends x {
212
168
  this.close();
213
169
  }
214
170
  render() {
215
- let e = !this.label && this.heading !== "", t = this.frame === "plain", n = w`
171
+ let e = this.frame === "plain", t = !e && !this.label && this.heading !== "", n = C`
216
172
  <div class="body" part="body">
217
- <div class="content-wrap">
218
- <div
219
- class="content vf-scroll"
220
- part="content"
221
- tabindex=${this._scrollable ? "0" : T}
222
- role=${this._scrollable ? "group" : T}
223
- >
224
- ${t && this.heading !== "" ? w`<span class="plain-heading" part="title" id="title"
225
- >${this.heading}</span
226
- >` : T}
227
- <slot @slotchange=${this._onBodySlotChange}></slot>
228
- </div>
229
- ${b(this._rail, "vertical")}
230
- <div class="scroll-frame" aria-hidden="true"></div>
231
- </div>
232
- <div class="footer ${this._hasButtons ? "" : "empty"}" part="footer">
233
- <vf-button-group class="buttons" part="buttons">
234
- <slot
235
- name="buttons"
236
- @slotchange=${this._onButtonsSlotChange}
237
- ></slot>
238
- </vf-button-group>
173
+ <div
174
+ class="content vf-scroll"
175
+ part="content"
176
+ tabindex=${this._scrollable ? "0" : w}
177
+ role=${this._scrollable ? "group" : w}
178
+ >
179
+ <slot @slotchange=${this._onBodySlotChange}></slot>
239
180
  </div>
181
+ ${y(this._rail, "vertical")}
240
182
  </div>
241
183
  `;
242
- return w`
184
+ return C`
243
185
  <dialog
244
- style=${k(this.dialogSize)}
245
- aria-labelledby=${e ? "title" : T}
246
- aria-label=${e ? T : this.label || "Dialog"}
186
+ style=${O(this.dialogSize)}
187
+ aria-labelledby=${t ? "title" : w}
188
+ aria-label=${t ? w : this.label || this.heading || "Dialog"}
247
189
  @cancel=${this._onNativeCancel}
248
190
  @close=${this._onNativeClose}
249
191
  >
250
192
  <div class="vf-modal-frame" part="frame">
251
- ${t ? T : h(this._drag, w`
252
- ${this.closable ? g(_("Close", this.heading), this._onCloseClick) : T}
193
+ ${e ? w : m(this._drag, C`
194
+ ${this.closable ? h(g("Close", this.heading), this._onCloseClick) : w}
253
195
  <span class="vf-title" part="title" id="title"
254
196
  >${this.heading}</span
255
197
  >
@@ -259,14 +201,10 @@ var A = class extends x {
259
201
  </dialog>
260
202
  `;
261
203
  }
262
- _onButtonsSlotChange(e) {
263
- let t = e.target;
264
- this._hasButtons = t.assignedElements().length > 0;
265
- }
266
204
  };
267
- r([E()], A.prototype, "heading", void 0), r([E()], A.prototype, "label", void 0), r([E({
205
+ r([T()], k.prototype, "heading", void 0), r([T()], k.prototype, "label", void 0), r([T({
268
206
  type: Boolean,
269
207
  reflect: !0
270
- })], A.prototype, "closable", void 0), r([E({ reflect: !0 })], A.prototype, "frame", void 0), r([O()], A.prototype, "_hasButtons", void 0), r([D(".content")], A.prototype, "_content", void 0), r([O()], A.prototype, "_scrollable", void 0), A = r([e("vf-dialog")], A);
208
+ })], k.prototype, "closable", void 0), r([T({ reflect: !0 })], k.prototype, "frame", void 0), r([E(".content")], k.prototype, "_content", void 0), r([D()], k.prototype, "_scrollable", void 0), k = r([e("vf-dialog")], k);
271
209
  //#endregion
272
- export { A as VfDialog };
210
+ export { k as VfDialog };
@@ -45,6 +45,11 @@ export interface VfIconDragFollower {
45
45
  x: number;
46
46
  y: number;
47
47
  };
48
+ /** How far that origin sits from the follower's box corner (its `origin`). */
49
+ offset: {
50
+ x: number;
51
+ y: number;
52
+ };
48
53
  /** The follower's box, system px, for the group clamp. */
49
54
  size: {
50
55
  width: number;
@@ -183,10 +188,14 @@ declare const VfIcon_base: (new (...args: any[]) => import("../position.js").VfP
183
188
  * the name's rectangle — drawn over everything with the XOR pen, the icon
184
189
  * itself staying put until the drop. The kit draws it from what it already
185
190
  * has: {@link deriveDragOutline} finds the ring the way the open ghost does
186
- * (the silhouette from the art's alpha, eroded by one pixel, the ring the
187
- * erosion removes), adds the plate's rectangle, and {@link dotOutline} dots
188
- * it against a 2×2 checker — compositing only, so cross-origin art derives
189
- * too, and art the pipeline cannot draw falls back to the cell's rectangle.
191
+ * (the silhouette from the art's alpha, the plate's rectangle united with
192
+ * it, the whole inset by one pixel as `FrameRgn` inset a region, the ring
193
+ * the inset removes — so where the art and the plate abut, the outline
194
+ * wraps around both), and {@link dotOutline} dots it against a 2×2 checker
195
+ * — compositing only, so cross-origin art derives too, and art the
196
+ * pipeline cannot draw falls back to the cell's rectangle. The canvas
197
+ * covers the frame widened to the plate, since a name wider than a
198
+ * declared `width` overhangs the frame.
190
199
  * The canvas paints with the cursor's XOR recipe, `filter: invert(1)` under
191
200
  * `mix-blend-mode: difference`: a dotted black line over a white window
192
201
  * body, and over the desktop dither a black line, the composition
@@ -374,7 +383,8 @@ declare const VfIcon_base: (new (...args: any[]) => import("../position.js").VfP
374
383
  * @csspart plate - The inked run behind the name, which each wrapped line gets
375
384
  * its own of (inverts when selected).
376
385
  * @csspart input - The rename field, while editing.
377
- * @cssprop --vf-icon-gap - Space between the art cell and the name plate
386
+ * @cssprop --vf-icon-gap - Space between the art cell and the name plate;
387
+ * 0 by default, the plate directly under the cell as the Finder drew it
378
388
  * @cssprop --vf-icon-label-height - The name plate's line box
379
389
  * @fires vf-select - Selection changed by user interaction. `detail: { selected: boolean }`.
380
390
  * @fires vf-change - The name was committed. `detail: { label: string, previous: string }`.
@@ -213,7 +213,7 @@ var D, O = {
213
213
  centring below is still a whole number of system px. */
214
214
  .caption {
215
215
  position: relative;
216
- margin-top: calc(var(--vf-scale, 1) * var(--vf-icon-gap, 2px));
216
+ margin-top: calc(var(--vf-scale, 1) * var(--vf-icon-gap, 0px));
217
217
  }
218
218
  .label {
219
219
  ${u}
@@ -395,6 +395,7 @@ var D, O = {
395
395
  return {
396
396
  icon: this,
397
397
  origin: t,
398
+ offset: this.#f.offset,
398
399
  size: this.#v(),
399
400
  move: (e, t) => {
400
401
  n && this.#E(n, e, t);
@@ -469,19 +470,22 @@ var D, O = {
469
470
  this.#f.moveTo(t.left, t.top);
470
471
  return;
471
472
  }
472
- let n = this.#f.bounds, r = [{
473
- origin: e.origin,
473
+ let n = this.#f.bounds, r = (e, t) => ({
474
+ x: e.x - t.x,
475
+ y: e.y - t.y
476
+ }), i = [{
477
+ corner: r(e.origin, this.#f.offset),
474
478
  size: e.size
475
479
  }, ...e.followers.map((e) => ({
476
- origin: e.origin,
480
+ corner: r(e.origin, e.offset),
477
481
  size: e.size
478
- }))], i = (e, t, n, i) => {
482
+ }))], a = (e, t, n, r) => {
479
483
  let a = -Infinity, o = Infinity;
480
- for (let e of r) a = Math.max(a, -t(e)), o = Math.min(o, i - n(e) - t(e));
484
+ for (let e of i) a = Math.max(a, -t(e)), o = Math.min(o, r - n(e) - t(e));
481
485
  return o < a && (o = Infinity), Math.min(Math.max(e, a), o);
482
- }, a = i(t.left - e.origin.x, (e) => e.origin.x, (e) => e.size.width, n.width), o = i(t.top - e.origin.y, (e) => e.origin.y, (e) => e.size.height, n.height);
483
- this.#f.moveTo(e.origin.x + a, e.origin.y + o);
484
- for (let t of e.followers) t.drop(a, o);
486
+ }, o = a(t.left - e.origin.x, (e) => e.corner.x, (e) => e.size.width, n.width), s = a(t.top - e.origin.y, (e) => e.corner.y, (e) => e.size.height, n.height);
487
+ this.#f.moveTo(e.origin.x + o, e.origin.y + s);
488
+ for (let t of e.followers) t.drop(o, s);
485
489
  }
486
490
  #w() {
487
491
  let t = { surface: null };
@@ -490,43 +494,36 @@ var D, O = {
490
494
  #T(e, t) {
491
495
  let n = this.renderRoot, a = n?.querySelector(".frame");
492
496
  if (!a) return null;
493
- let o = r(this), s = (t) => ({
494
- x: Math.round((t.left - e.left) / o),
495
- y: Math.round((t.top - e.top) / o),
496
- width: Math.round(t.width / o),
497
- height: Math.round(t.height / o)
498
- }), c = Math.round(e.width / o), l = Math.round(e.height / o), u = n.querySelector(".art"), d = n.querySelector(".name"), f = this.open && this._ghost ? this._ghost : this.#t, p = f != null && (f instanceof HTMLCanvasElement ? f.width > 0 : f.complete && f.naturalWidth > 0), m = p ? f.getBoundingClientRect() : null, h = _(p ? f : null, {
499
- width: c,
500
- height: l,
501
- art: m && m.width > 0 ? s(m) : null,
502
- cell: u ? s(u.getBoundingClientRect()) : {
497
+ let o = r(this), s = n.querySelector(".art"), c = n.querySelector(".name")?.getBoundingClientRect() ?? null, l = c ? Math.min(e.left, c.left) : e.left, u = c ? Math.min(e.top, c.top) : e.top, d = c ? Math.max(e.right, c.right) : e.right, f = c ? Math.max(e.bottom, c.bottom) : e.bottom, p = (e) => ({
498
+ x: Math.round((e.left - l) / o),
499
+ y: Math.round((e.top - u) / o),
500
+ width: Math.round(e.width / o),
501
+ height: Math.round(e.height / o)
502
+ }), m = Math.round((d - l) / o), h = Math.round((f - u) / o), g = this.open && this._ghost ? this._ghost : this.#t, v = g != null && (g instanceof HTMLCanvasElement ? g.width > 0 : g.complete && g.naturalWidth > 0), y = v ? g.getBoundingClientRect() : null, b = _(v ? g : null, {
503
+ width: m,
504
+ height: h,
505
+ art: y && y.width > 0 ? p(y) : null,
506
+ cell: s ? p(s.getBoundingClientRect()) : {
503
507
  x: 0,
504
508
  y: 0,
505
- width: c,
506
- height: l
509
+ width: m,
510
+ height: h
507
511
  },
508
- plate: d ? s(d.getBoundingClientRect()) : null
512
+ plate: c ? p(c) : null
509
513
  });
510
- if (!h) return null;
511
- let g = document.createElement("canvas");
512
- g.className = "drag-outline";
513
- let v = g.style;
514
- v.position = "absolute", v.display = "block", v.pointerEvents = "none", v.filter = "invert(1)", v.mixBlendMode = "difference", v.imageRendering = "pixelated", v.width = i(h.width), v.height = i(h.height);
515
- let y = {
516
- x: 0,
517
- y: 0
514
+ if (!b) return null;
515
+ let x = document.createElement("canvas");
516
+ x.className = "drag-outline";
517
+ let S = x.style;
518
+ S.position = "absolute", S.display = "block", S.pointerEvents = "none", S.filter = "invert(1)", S.mixBlendMode = "difference", S.imageRendering = "pixelated", S.width = i(b.width), S.height = i(b.height);
519
+ let C = t ?? a, w = t ? t.getBoundingClientRect() : e, T = {
520
+ x: Math.round((l - w.left) / o),
521
+ y: Math.round((u - w.top) / o)
518
522
  };
519
- if (t) {
520
- let n = t.getBoundingClientRect();
521
- y = {
522
- x: Math.round((e.left - n.left) / o),
523
- y: Math.round((e.top - n.top) / o)
524
- }, t.append(g);
525
- } else a.append(g);
526
- return {
527
- ring: h,
528
- canvas: g,
529
- base: y,
523
+ return C.append(x), {
524
+ ring: b,
525
+ canvas: x,
526
+ base: T,
530
527
  phase: -1
531
528
  };
532
529
  }
@@ -89,9 +89,11 @@ export declare class VfScrollArea extends VfScrollArea_base {
89
89
  * `{ left, top }` in whole system px on the placement lattice, the pair a
90
90
  * child dropped here is written with. The plane travels with the scroll,
91
91
  * so its box already carries the scroll offset and the border-floor
92
- * padding; nothing is left for the caller to add.
92
+ * padding; nothing is left for the caller to add. With `child`, the pair
93
+ * to write to that element: the offset its `origin` adds is folded in, so
94
+ * its box's corner lands on the point.
93
95
  */
94
- placementAt(clientX: number, clientY: number): {
96
+ placementAt(clientX: number, clientY: number, child?: Element): {
95
97
  left: number;
96
98
  top: number;
97
99
  };
@@ -23,8 +23,8 @@ var y = class extends r(f) {
23
23
  }, this.addEventListener("vf-placement-change", this.#e);
24
24
  }
25
25
  #e;
26
- placementAt(e, t) {
27
- return i(this.content ?? this, e, t, this);
26
+ placementAt(e, t, n) {
27
+ return i(this.content ?? this, e, t, this, n);
28
28
  }
29
29
  focus(e) {
30
30
  this.viewport?.focus(e);
@@ -201,9 +201,11 @@ export declare class VfWindow extends VfWindow_base {
201
201
  * child dropped into this window is written with: the body's corner, or
202
202
  * under `scrollbars` the scrolled plane, scroll offset included. The anchor
203
203
  * sits in the shadow tree, which is why the conversion is a method here
204
- * rather than the page's own `toSysExact` against a rect.
204
+ * rather than the page's own `toSysExact` against a rect. With `child`,
205
+ * the pair to write to that element: the offset its `origin` adds is
206
+ * folded in, so its box's corner lands on the point.
205
207
  */
206
- placementAt(clientX: number, clientY: number): {
208
+ placementAt(clientX: number, clientY: number, child?: Element): {
207
209
  left: number;
208
210
  top: number;
209
211
  };
@@ -329,9 +329,9 @@ var W = 24, G = 8, K = class extends D(c(P)) {
329
329
  ></div>`;
330
330
  }
331
331
  #t;
332
- placementAt(e, t) {
333
- let n = this.scrollArea;
334
- return n ? n.placementAt(e, t) : l(this.body ?? this, e, t, this);
332
+ placementAt(e, t, n) {
333
+ let r = this.scrollArea;
334
+ return r ? r.placementAt(e, t, n) : l(this.body ?? this, e, t, this, n);
335
335
  }
336
336
  measure() {
337
337
  this.scrollArea?.measure();
package/dist/index.d.ts CHANGED
@@ -155,7 +155,7 @@ export { VfTextControlBase } from './text-control.js';
155
155
  export { VfToggleControl } from './toggle-control.js';
156
156
  export type { VfToggleControlInterface } from './toggle-control.js';
157
157
  export { PlacementController, VfPositioned } from './position.js';
158
- export type { PlacementBounds, PlacementClamp, VfPositionedInterface } from './position.js';
158
+ export type { PlacementBounds, PlacementClamp, PlacementOrigin, VfPositionedInterface, } from './position.js';
159
159
  export { VfSized } from './size.js';
160
160
  export type { VfSizedInterface } from './size.js';
161
161
  export { VfModalDialog, modalDialogStyles } from './modal-dialog.js';