vintage-frames 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,15 @@ import './vf-button-group.js';
27
27
  * closes it too, with `{ reason: 'outside' }` — for the About box; off by
28
28
  * default, since the classic modal ignored an outside click.
29
29
  *
30
+ * Keyboard, the classic Dialog Manager's two rules ({@link VfModalDialog}):
31
+ * on open, focus goes to the first text-entry control — a slotted control
32
+ * with `autofocus` first — or, with none, to the default button
33
+ * (`vf-button variant="default"`). Return or Enter activates the default
34
+ * button from anywhere in the dialog, a focused Cancel included; Space
35
+ * presses the focused control. A link keeps its own Enter, and in a
36
+ * multi-line editor Return inserts the newline while the keypad's Enter
37
+ * activates the button.
38
+ *
30
39
  * @slot - Default slot: dialog body content.
31
40
  * @slot buttons - Optional action buttons. Rendered as a bottom-right
32
41
  * `vf-button-group` (equal-width, faces aligned); the footer only takes
@@ -26,7 +26,7 @@ var y = class extends r(l) {
26
26
  this.#c(), this.pressed = null;
27
27
  }, this.#d = (e) => {
28
28
  if (this.isSubmitEnter(e)) {
29
- this.requestImplicitSubmit();
29
+ this.requestImplicitSubmit() && e.preventDefault();
30
30
  return;
31
31
  }
32
32
  if (!(this.isDisabled || this.readonly) && !(e.isComposing || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) switch (e.key) {
@@ -115,11 +115,19 @@ var v = class extends r(d) {
115
115
  fixed-width box, a wide image) grows the plane — precisely the
116
116
  content that overflows sideways. It also makes this box the
117
117
  containing block of a sticky child, so a sticky left: 0 strip holds
118
- across the scroll. */
118
+ across the scroll.
119
+
120
+ Never shorter than the viewport either: a fixed child (position.ts)
121
+ is a sticky box whose containing block is this plane, and a plane
122
+ shorter than the child's stated top clamps the child to the plane's
123
+ bottom edge (measured: top 100 over 50px of content landed at 50).
124
+ Content that fits still fits — the plane grows to the viewport, not
125
+ past it, so nothing overflows that didn't. */
119
126
  .content {
120
127
  position: relative;
121
128
  width: fit-content;
122
129
  min-width: 100%;
130
+ min-height: 100%;
123
131
  }
124
132
  /* Focusable so keyboard users can scroll; inset ring to stay in-box. */
125
133
  .viewport:focus-visible {
@@ -40,7 +40,8 @@ export declare class VfTextField extends VfTextField_base {
40
40
  /**
41
41
  * Enter in a single-line field triggers the associated form's implicit
42
42
  * submission. The native `<input>` is shadow-encapsulated, so its form owner
43
- * is null and the browser won't do this itself.
43
+ * is null and the browser won't do this itself. A press the form took is
44
+ * cancelled, so an enclosing modal does not route it again.
44
45
  */
45
46
  private handleKeydown;
46
47
  protected render(): import("lit-html").TemplateResult<1>;
@@ -41,7 +41,7 @@ var d = class extends n(a) {
41
41
  this.forwardedAttributes = [...a.forwardedAttributes, "pattern"];
42
42
  }
43
43
  handleKeydown(e) {
44
- this.isSubmitEnter(e) && this.requestImplicitSubmit();
44
+ this.isSubmitEnter(e) && this.requestImplicitSubmit() && e.preventDefault();
45
45
  }
46
46
  render() {
47
47
  return s`
@@ -30,6 +30,13 @@ declare const VfWindow_base: (new (...args: any[]) => import("../size.js").VfSiz
30
30
  * says so once in the console.
31
31
  *
32
32
  * @slot - Default slot: window body content.
33
+ * @slot header - Optional header content — a band between the title bar and
34
+ * the body, the full width of the window (the Finder window's header
35
+ * line): a white interior over a 1px rule, no inset of its own, a
36
+ * positioning anchor for placed children. As tall as its content unless
37
+ * `header-height` states it. Under `scrollbars` the vertical rail begins
38
+ * below it, so the header spans the rail's column. Takes no space until
39
+ * populated.
33
40
  * @slot status - Optional status-bar content — the classic bottom readout
34
41
  * strip ("40px x 40px"): a 1px rule over a 15px white band under the body,
35
42
  * body-face text on its native line. Takes no space until populated; a
@@ -40,6 +47,8 @@ declare const VfWindow_base: (new (...args: any[]) => import("../size.js").VfSiz
40
47
  * @csspart close-box - The close widget (left).
41
48
  * @csspart zoom-box - The zoom widget (right).
42
49
  * @csspart body - The content area.
50
+ * @csspart header - The header strip between the title bar and the body
51
+ * (when the `header` slot is populated).
43
52
  * @csspart status-bar - The bottom status strip (when the `status` slot is
44
53
  * populated).
45
54
  * @csspart grow-box - The resize widget (bottom-right, when `resizable`).
@@ -134,6 +143,15 @@ export declare class VfWindow extends VfWindow_base {
134
143
  * placed inside the body.
135
144
  */
136
145
  scrollbars?: 'vertical' | 'horizontal' | 'both';
146
+ /**
147
+ * The header's height in whole system px, rule included — the way every
148
+ * kit bar counts its rule (an 18px title bar is 17 + 1). Unset, the header
149
+ * is as tall as what is slotted into it, plus the rule. Only matters while
150
+ * the `header` slot is populated.
151
+ */
152
+ headerHeight?: number | null;
153
+ /** Whether the `header` slot has assigned content (drives the header). */
154
+ private _hasHeader;
137
155
  /** Whether the `status` slot has assigned content (drives the strip). */
138
156
  private _hasStatus;
139
157
  /** Default-on display scaling (true 72dpi size); see src/scale.ts. */
@@ -190,6 +208,8 @@ export declare class VfWindow extends VfWindow_base {
190
208
  disconnectedCallback(): void;
191
209
  /** The `.empty` gate: the strip renders only while the slot is populated. */
192
210
  private _onStatusSlotChange;
211
+ /** The header's own gate, on the same terms. */
212
+ private _onHeaderSlotChange;
193
213
  private _onCloseClick;
194
214
  private _onZoomClick;
195
215
  private _onGrowPointerDown;
@@ -25,7 +25,7 @@ function V(e, t, n) {
25
25
  }
26
26
  var H = 24, U = 8, W = class extends E(c(N)) {
27
27
  constructor(...e) {
28
- super(...e), this.heading = "", this.active = !0, this.closable = !0, this.zoomable = !1, this.movable = !1, this.resizable = !1, this._hasStatus = !1, this.scale = new n(this), this.gridSnap = new T(this), this.titleCenter = new O(this), this._dotsPattern = "", this.#e = new x(), this.#t = (e, t, n) => {
28
+ super(...e), this.heading = "", this.active = !0, this.closable = !0, this.zoomable = !1, this.movable = !1, this.resizable = !1, this._hasHeader = !1, this._hasStatus = !1, this.scale = new n(this), this.gridSnap = new T(this), this.titleCenter = new O(this), this._dotsPattern = "", this.#e = new x(), this.#t = (e, t, n) => {
29
29
  let r = a(this.offsetWidth, this);
30
30
  return {
31
31
  x: Math.min(Math.max(e, H - r), n.width - H),
@@ -204,13 +204,39 @@ var H = 24, U = 8, W = class extends E(c(N)) {
204
204
  and a resizable window's grow box (z-index 1) lands over the rail
205
205
  corner cell — which the render reserves on a single-axis rail too
206
206
  (the area's corner flag), except when the status strip holds the
207
- grow box instead. */
207
+ grow box instead. The same overhang lands the area's top frame line
208
+ on the header's rule, so the vertical rail's top arrow begins
209
+ under the header. */
208
210
  .edge-scroll {
209
211
  width: calc(100% + var(--vf-scale, 1) * 2px);
210
212
  height: calc(100% + var(--vf-scale, 1) * 2px);
211
213
  margin: calc(var(--vf-scale, 1) * -1px);
212
214
  }
213
215
 
216
+ /* --- Header (slot="header") --------------------------------------- */
217
+ /* A band between the title bar and the body, the full width of the
218
+ window — the Finder window's header line: a white interior over a
219
+ 1px rule (vfRule's vf-rule-bottom on the element). Like the body it
220
+ carries no inset and is a positioning anchor: (0,0) is the header's
221
+ own corner, flow content starts there too, an inset is the content's.
222
+ As tall as its content unless header-height states it — rule
223
+ included, the way every kit bar counts its rule (an 18px title bar
224
+ is 17 + 1). Under the scrollbars parameter the edge rails sit in the
225
+ body below it, so the header spans the rail's column and the
226
+ vertical rail's arrows begin under it. Takes no space until the slot
227
+ is populated. Clipped like the body; a drop-open panel still escapes
228
+ (see the body's note). A div with a class, never a <header>: that
229
+ element maps to a banner landmark (see render()). */
230
+ .header {
231
+ position: relative;
232
+ flex: none;
233
+ background: var(--vf-white, #ffffff);
234
+ overflow: hidden;
235
+ }
236
+ .header.empty {
237
+ display: none;
238
+ }
239
+
214
240
  /* --- Status bar (slot="status") ---------------------------------- */
215
241
  /* The classic bottom readout strip: a 1px rule over a white interior,
216
242
  15px in all — the grow box's own height, so a resizable window's
@@ -329,6 +355,10 @@ var H = 24, U = 8, W = class extends E(c(N)) {
329
355
  let t = e.target;
330
356
  this._hasStatus = t.assignedElements().length > 0;
331
357
  }
358
+ _onHeaderSlotChange(e) {
359
+ let t = e.target;
360
+ this._hasHeader = t.assignedElements().length > 0;
361
+ }
332
362
  _onCloseClick() {
333
363
  e(this, "vf-close", { reason: "close" });
334
364
  }
@@ -378,6 +408,13 @@ var H = 24, U = 8, W = class extends E(c(N)) {
378
408
  <span class="vf-title" part="title" id="title">${this.heading}</span>
379
409
  ${this.zoomable ? M(j("Zoom", this.heading), this._onZoomClick) : I}
380
410
  `, this.variant === "utility" ? "vf-dots" : "vf-stripes", this.variant === "utility" ? this._dotsTexture() : void 0)}
411
+ <div
412
+ class="header vf-rule-bottom ${this._hasHeader ? "" : "empty"}"
413
+ part="header"
414
+ style=${this.headerHeight == null ? I : `height:${i(this.headerHeight)}`}
415
+ >
416
+ <slot name="header" @slotchange=${this._onHeaderSlotChange}></slot>
417
+ </div>
381
418
  <div class="body" part="body">
382
419
  ${this.scrollbars ? F`
383
420
  <vf-scroll-area
@@ -436,6 +473,9 @@ o([L({ reflect: !0 })], W.prototype, "variant", void 0), o([L()], W.prototype, "
436
473
  })], W.prototype, "maxWidth", void 0), o([L({
437
474
  type: Number,
438
475
  attribute: "max-height"
439
- })], W.prototype, "maxHeight", void 0), o([L({ reflect: !0 })], W.prototype, "scrollbars", void 0), o([R()], W.prototype, "_hasStatus", void 0), W = o([t("vf-window")], W);
476
+ })], W.prototype, "maxHeight", void 0), o([L({ reflect: !0 })], W.prototype, "scrollbars", void 0), o([L({
477
+ type: Number,
478
+ attribute: "header-height"
479
+ })], W.prototype, "headerHeight", void 0), o([R()], W.prototype, "_hasHeader", void 0), o([R()], W.prototype, "_hasStatus", void 0), W = o([t("vf-window")], W);
440
480
  //#endregion
441
481
  export { W as VfWindow };
@@ -32,6 +32,12 @@ export declare const modalDialogStyles: import("lit").CSSResult;
32
32
  * event, an Escape-close no longer leaves a stale origin behind, so the next
33
33
  * open re-derives it.
34
34
  *
35
+ * It also owns the two keyboard rules every classic dialog followed (see
36
+ * {@link defaultButton} and {@link initialFocusTarget}): Return or Enter
37
+ * activates the default button from anywhere in the dialog, and the box opens
38
+ * with the insertion point in its first text field — or, with none, focus on
39
+ * the default button.
40
+ *
35
41
  * Removing an open modal from the DOM is a close path too. HTML's dialog
36
42
  * *removing steps* take the element out of the top layer **without** running
37
43
  * the close algorithm — no `close` event, no focus restoration — which is
@@ -124,6 +130,33 @@ export declare class VfModalDialog extends LitElement {
124
130
  * drag from squeezing the box.
125
131
  */
126
132
  protected get dialogSize(): Record<string, string | null>;
133
+ /**
134
+ * The dialog's default button — the one wearing the bold ring
135
+ * (`vf-button[variant="default"]`) — or null with none, or none enabled.
136
+ * Looked for among the slotted content first, then in the shadow tree, so a
137
+ * consumer's own modal that renders its buttons itself is covered too.
138
+ * Override to name it some other way.
139
+ */
140
+ protected get defaultButton(): HTMLElement | null;
141
+ /**
142
+ * Where focus lands on open. The classic Dialog Manager put the insertion
143
+ * point in the first editable text item and gave nothing else focus at all;
144
+ * the web needs a focused control for the keyboard to have anywhere to be,
145
+ * so with no text field the default button takes it (which is also what
146
+ * makes Return and Space work there without a Tab). In order:
147
+ *
148
+ * 1. a slotted control carrying `autofocus` — the author's say;
149
+ * 2. the first enabled text-entry control (`vf-text-field`,
150
+ * `vf-number-field`, `vf-text-area`, or a native text input/textarea);
151
+ * 3. the {@link defaultButton};
152
+ * 4. null — leave the browser's own choice alone.
153
+ *
154
+ * Left to itself, `showModal()` focuses the first focusable thing in flat
155
+ * tree order, which for a Cancel/OK row is Cancel, and for a body with a
156
+ * link in it is the link — so Return did the one thing a classic dialog's
157
+ * Return never did. Override to choose differently.
158
+ */
159
+ protected get initialFocusTarget(): HTMLElement | null;
127
160
  /** Open the modal (native `showModal()`), pinned onto the device grid. */
128
161
  show(): void;
129
162
  /** Close the modal. Fires `vf-close` with `{ reason: 'close' }`. */
@@ -4,7 +4,15 @@ import o from "./_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js"
4
4
  import { DocumentListenersController as s } from "./document-listeners.js";
5
5
  import { LitElement as c, css as l } from "lit";
6
6
  import { property as u, query as d } from "lit/decorators.js";
7
- var f = 24, p = l`
7
+ var f = 24, p = /* @__PURE__ */ new Set([
8
+ "text",
9
+ "search",
10
+ "url",
11
+ "tel",
12
+ "email",
13
+ "password",
14
+ "number"
15
+ ]), m = (e) => e instanceof HTMLInputElement ? p.has(e.type) : !0, h = l`
8
16
  dialog {
9
17
  padding: 0;
10
18
  margin: auto;
@@ -43,32 +51,43 @@ var f = 24, p = l`
43
51
  the fallback is the arrow it always showed. */
44
52
  cursor: var(--vf-cursor, default);
45
53
  }
46
- `, m = class extends c {
54
+ `, g = class extends c {
47
55
  constructor(...e) {
48
56
  super(...e), this.scale = new t(this), this.open = !1, this.lightDismiss = !1, this.#e = !1, this.#n = null, this.#r = null, this.#i = new s(this, () => [
49
57
  [
50
58
  this._dialog,
51
59
  "pointerdown",
52
- this.#a
60
+ this.#s
53
61
  ],
54
62
  [
55
63
  this._dialog,
56
64
  "pointerup",
57
- this.#o
65
+ this.#c
58
66
  ],
59
67
  [
60
68
  this._dialog,
61
69
  "pointercancel",
62
- this.#s
70
+ this.#l
71
+ ],
72
+ [
73
+ this._dialog,
74
+ "keydown",
75
+ this.#o
63
76
  ]
64
- ]), this.#a = (e) => {
77
+ ]), this.#o = (e) => {
78
+ if (e.key !== "Enter" || e.defaultPrevented || e.isComposing || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
79
+ let t = e.composedPath();
80
+ if (t.some((e) => e instanceof HTMLAnchorElement && e.hasAttribute("href")) || t.some((e) => e instanceof HTMLTextAreaElement || e instanceof HTMLElement && e.isContentEditable) && e.code !== "NumpadEnter") return;
81
+ let n = this.defaultButton;
82
+ n && (e.preventDefault(), n.click());
83
+ }, this.#s = (e) => {
65
84
  this.#r = e.target === this._dialog ? e.pointerId : null;
66
- }, this.#o = (e) => {
85
+ }, this.#c = (e) => {
67
86
  let t = this.#r === e.pointerId;
68
87
  this.#r = null, !(!t || e.target !== this._dialog || !this.lightDismiss) && (this.#n = "outside", this.close());
69
- }, this.#s = () => {
88
+ }, this.#l = () => {
70
89
  this.#r = null;
71
- }, this.#c = null, this.#m = () => this.settle();
90
+ }, this.#u = null, this.#g = () => this.settle();
72
91
  }
73
92
  get dialogSize() {
74
93
  return {
@@ -88,42 +107,53 @@ var f = 24, p = l`
88
107
  #n;
89
108
  #r;
90
109
  #i;
91
- #a;
110
+ get defaultButton() {
111
+ let e = "vf-button[variant=\"default\"]";
112
+ return [...this.querySelectorAll(e), ...this.renderRoot.querySelectorAll(e)].find((e) => !e.matches(":disabled")) ?? null;
113
+ }
114
+ get initialFocusTarget() {
115
+ return this.querySelector("[autofocus]") || ([...this.querySelectorAll("vf-text-field, vf-number-field, vf-text-area, textarea, input")].find((e) => m(e) && !e.matches(":disabled")) ?? this.defaultButton);
116
+ }
117
+ #a() {
118
+ this.initialFocusTarget?.focus();
119
+ }
92
120
  #o;
93
121
  #s;
94
122
  #c;
123
+ #l;
124
+ #u;
95
125
  show() {
96
- this.open = !0, this.hasUpdated && this.#p();
126
+ this.open = !0, this.hasUpdated && this.#h();
97
127
  }
98
128
  close() {
99
- this.open = !1, this.hasUpdated && this.#p();
129
+ this.open = !1, this.hasUpdated && this.#h();
100
130
  }
101
131
  updated(e) {
102
- e.has("open") && this.#p(), (e.has("top") || e.has("left")) && this.settle(), this.#t();
132
+ e.has("open") && this.#h(), (e.has("top") || e.has("left")) && this.settle(), this.#t();
103
133
  }
104
- #l() {
134
+ #d() {
105
135
  let e = document.documentElement;
106
136
  return {
107
137
  width: a(e.clientWidth || window.innerWidth, this),
108
138
  height: a(e.clientHeight || window.innerHeight, this)
109
139
  };
110
140
  }
111
- #u(e, t) {
112
- let n = this.#l(), r = a(this._dialog?.offsetWidth ?? 0, this);
141
+ #f(e, t) {
142
+ let n = this.#d(), r = a(this._dialog?.offsetWidth ?? 0, this);
113
143
  return {
114
144
  x: Math.min(Math.max(e, f - r), n.width - f),
115
145
  y: Math.min(Math.max(t, 0), Math.max(0, n.height - f))
116
146
  };
117
147
  }
118
- #d() {
119
- let e = this.#l(), t = this._dialog?.getBoundingClientRect();
148
+ #p() {
149
+ let e = this.#d(), t = this._dialog?.getBoundingClientRect();
120
150
  return {
121
151
  x: (e.width - a(t?.width ?? 0, this)) / 2,
122
152
  y: (e.height - a(t?.height ?? 0, this)) / 2
123
153
  };
124
154
  }
125
155
  placeAt(e, t) {
126
- let n = this.#u(e, t);
156
+ let n = this.#f(e, t);
127
157
  this.left = r(n.x, this), this.top = r(n.y, this);
128
158
  }
129
159
  settle() {
@@ -132,10 +162,10 @@ var f = 24, p = l`
132
162
  let t = this.left != null || this.top != null ? {
133
163
  x: this.left ?? 0,
134
164
  y: this.top ?? 0
135
- } : this.#d(), n = this.#u(t.x, t.y);
165
+ } : this.#p(), n = this.#f(t.x, t.y);
136
166
  e.style.left = i(r(n.x, this)), e.style.top = i(r(n.y, this)), e.style.right = "auto", e.style.bottom = "auto", e.style.margin = "0";
137
167
  }
138
- #f() {
168
+ #m() {
139
169
  let e = this._dialog?.style;
140
170
  if (e) for (let t of [
141
171
  "left",
@@ -145,44 +175,44 @@ var f = 24, p = l`
145
175
  "margin"
146
176
  ]) e.removeProperty(t);
147
177
  }
148
- #p() {
178
+ #h() {
149
179
  let e = this._dialog;
150
- e && (this.open && !e.open ? (this.#c = document.activeElement, e.showModal(), this.settle(), this.#_(e), this.#i.attach()) : !this.open && e.open && e.close());
180
+ e && (this.open && !e.open ? (this.#u = document.activeElement, e.showModal(), this.#a(), this.settle(), this.#y(e), this.#i.attach()) : !this.open && e.open && e.close());
151
181
  }
152
- #m;
153
- #h;
154
182
  #g;
155
- #_(e) {
156
- typeof ResizeObserver < "u" && !this.#h && (this.#h = new ResizeObserver(this.#m), this.#h.observe(e)), window.addEventListener("resize", this.#m), this.#g ??= n(this.#m);
183
+ #_;
184
+ #v;
185
+ #y(e) {
186
+ typeof ResizeObserver < "u" && !this.#_ && (this.#_ = new ResizeObserver(this.#g), this.#_.observe(e)), window.addEventListener("resize", this.#g), this.#v ??= n(this.#g);
157
187
  }
158
- #v() {
159
- this.#h?.disconnect(), this.#h = void 0, window.removeEventListener("resize", this.#m), this.#g?.(), this.#g = void 0;
188
+ #b() {
189
+ this.#_?.disconnect(), this.#_ = void 0, window.removeEventListener("resize", this.#g), this.#v?.(), this.#v = void 0;
160
190
  }
161
191
  disconnectedCallback() {
162
- super.disconnectedCallback(), this.#v();
192
+ super.disconnectedCallback(), this.#b();
163
193
  let e = this._dialog;
164
194
  if (e?.open) {
165
195
  e.close();
166
- let t = this.#c, n = document.activeElement;
196
+ let t = this.#u, n = document.activeElement;
167
197
  t instanceof HTMLElement && t.isConnected && (n === null || n === document.body) && t.focus();
168
198
  }
169
- this.#c = null;
199
+ this.#u = null;
170
200
  }
171
201
  _onNativeCancel() {
172
202
  this.#n = "escape";
173
203
  }
174
204
  _onNativeClose() {
175
205
  let t = this.#n ?? "close";
176
- this.#n = null, this.#c = null, this.#r = null, this.#i.detach(), this.#v(), this.open = !1, this.#f(), e(this, "vf-close", { reason: t });
206
+ this.#n = null, this.#u = null, this.#r = null, this.#i.detach(), this.#b(), this.open = !1, this.#m(), e(this, "vf-close", { reason: t });
177
207
  }
178
208
  };
179
209
  o([u({
180
210
  type: Boolean,
181
211
  reflect: !0
182
- })], m.prototype, "open", void 0), o([u({ type: Number })], m.prototype, "width", void 0), o([u({ type: Number })], m.prototype, "height", void 0), o([u({ type: Number })], m.prototype, "top", void 0), o([u({ type: Number })], m.prototype, "left", void 0), o([u({
212
+ })], g.prototype, "open", void 0), o([u({ type: Number })], g.prototype, "width", void 0), o([u({ type: Number })], g.prototype, "height", void 0), o([u({ type: Number })], g.prototype, "top", void 0), o([u({ type: Number })], g.prototype, "left", void 0), o([u({
183
213
  type: Boolean,
184
214
  reflect: !0,
185
215
  attribute: "light-dismiss"
186
- })], m.prototype, "lightDismiss", void 0), o([d("dialog")], m.prototype, "_dialog", void 0);
216
+ })], g.prototype, "lightDismiss", void 0), o([d("dialog")], g.prototype, "_dialog", void 0);
187
217
  //#endregion
188
- export { m as VfModalDialog, p as modalDialogStyles };
218
+ export { g as VfModalDialog, h as modalDialogStyles };
@@ -9,7 +9,14 @@ type Constructor<T = object> = new (...args: any[]) => T;
9
9
  export declare abstract class VfPositionedInterface extends LitElement {
10
10
  top?: number | null;
11
11
  left?: number | null;
12
+ fixed: boolean;
12
13
  }
14
+ /** A host that takes the placement trio. */
15
+ type PositionedHost = HTMLElement & {
16
+ top?: number | null;
17
+ left?: number | null;
18
+ fixed?: boolean;
19
+ };
13
20
  /**
14
21
  * Explicit placement — `top`/`left` in whole system px, on **every** component.
15
22
  *
@@ -76,6 +83,24 @@ export declare abstract class VfPositionedInterface extends LitElement {
76
83
  * element. The controller re-applies **only when the values changed**, so an
77
84
  * unrelated update — a heading change, a desktop toggling `active` — never
78
85
  * re-asserts a coordinate and costs nothing.
86
+ *
87
+ * **`fixed`** holds the placement against the *visible* region of the nearest
88
+ * scrolling ancestor instead of its scrolled plane — a tool strip over a
89
+ * document, a column header over rows — and the flag alone places at (0,0).
90
+ * The engine underneath is `position: sticky`, the one way the platform holds
91
+ * a box against a scrollport from inside it on the compositor (a scroll
92
+ * listener counter-translating lags a frame; a layer outside the scroller
93
+ * stops wheel and touch over it). Sticky boxes stay in flow, which is not
94
+ * what a placement is, so the controller erases the footprint: the host is
95
+ * blockified (no line box), shrink-wrapped, and given a negative right/bottom
96
+ * margin equal to its own box — a 0×0 margin box that flow content lays out
97
+ * as if it weren't there, measured by a ResizeObserver so a relabel or a zoom
98
+ * step keeps it exact. `z-index: 1` puts it over the plane's placed children,
99
+ * which is where a fixed strip belongs. Two facts of the engine that the
100
+ * docs state as rules: sticky only ever pushes a box *down* from where the
101
+ * flow put it, so a fixed child comes before the flow content in its parent;
102
+ * and a scroll container that never scrolls (a plain window body is
103
+ * `overflow: hidden`) holds it exactly where placement would.
79
104
  */
80
105
  export declare const VfPositioned: <T extends Constructor<LitElement>>(Base: T) => Constructor<VfPositionedInterface> & T;
81
106
  /** The box a gesture is clamped into, in system px — see {@link PlacementClamp}. */
@@ -127,10 +152,7 @@ export type PlacementClamp = (x: number, y: number, bounds: PlacementBounds) =>
127
152
  */
128
153
  export declare class PlacementController {
129
154
  #private;
130
- constructor(host: HTMLElement & {
131
- top?: number | null;
132
- left?: number | null;
133
- }, clamp: PlacementClamp);
155
+ constructor(host: PositionedHost, clamp: PlacementClamp);
134
156
  /** Whether a gesture has placed this host (as opposed to markup or CSS). */
135
157
  get placed(): boolean;
136
158
  /**
@@ -189,8 +211,5 @@ export declare class PlacementController {
189
211
  * Returns whether it warned, so the caller can latch — one warning per element,
190
212
  * not per render.
191
213
  */
192
- export declare function warnMovableContract(host: HTMLElement & {
193
- top?: number | null;
194
- left?: number | null;
195
- }, what: string, example: string): boolean;
214
+ export declare function warnMovableContract(host: PositionedHost, what: string, example: string): boolean;
196
215
  export {};
package/dist/position.js CHANGED
@@ -2,31 +2,83 @@ import { snapSys as e, sysLength as t, toSysExact as n } from "./scale.js";
2
2
  import r from "./_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
3
3
  import { property as i } from "lit/decorators.js";
4
4
  //#region src/position.ts
5
- var a = (e) => {
5
+ var a = {
6
+ inline: "block",
7
+ "inline-block": "block",
8
+ "inline-flex": "flex",
9
+ "inline-grid": "grid",
10
+ "inline-table": "table"
11
+ }, o = (e) => {
6
12
  class t extends e {
7
13
  constructor(...e) {
8
- super(...e), new o(this);
14
+ super(...e), this.fixed = !1, new s(this);
9
15
  }
10
16
  }
11
- return r([i({ type: Number })], t.prototype, "top", void 0), r([i({ type: Number })], t.prototype, "left", void 0), t;
12
- }, o = class {
17
+ return r([i({ type: Number })], t.prototype, "top", void 0), r([i({ type: Number })], t.prototype, "left", void 0), r([i({
18
+ type: Boolean,
19
+ reflect: !0
20
+ })], t.prototype, "fixed", void 0), t;
21
+ }, s = class {
13
22
  #e;
14
23
  #t = null;
15
24
  #n = null;
16
25
  #r = !1;
26
+ #i = !1;
27
+ #a = null;
17
28
  constructor(e) {
18
29
  this.#e = e, e.addController(this);
19
30
  }
31
+ hostConnected() {
32
+ this.#i && this.#r && this.#c();
33
+ }
34
+ hostDisconnected() {
35
+ this.#a?.disconnect(), this.#a = null;
36
+ }
20
37
  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");
38
+ let e = this.#e.top ?? null, n = this.#e.left ?? null, r = this.#e.fixed === !0, i = this.#e.style;
39
+ if (e === null && n === null && !r) {
40
+ if (!this.#i) return;
41
+ this.#r && this.#s(), this.#i = !1, this.#r = !1, i.removeProperty("position"), i.removeProperty("top"), i.removeProperty("left"), i.removeProperty("right"), i.removeProperty("bottom"), i.removeProperty("margin");
25
42
  return;
26
43
  }
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");
44
+ if (this.#i && e === this.#t && n === this.#n && r === this.#r) return;
45
+ let a = this.#i && this.#r;
46
+ this.#i = !0, this.#t = e, this.#n = n, this.#r = r, i.position = r ? "sticky" : "absolute", i.top = t(e ?? 0), i.left = t(n ?? 0), i.right = "auto", i.bottom = "auto", r ? a || this.#o() : (a && this.#s(), i.margin = "0");
28
47
  }
29
- }, s = class {
48
+ #o() {
49
+ let e = this.#e.style;
50
+ e.maxWidth = "fit-content", e.zIndex = "1", this.#l = !1;
51
+ let t = this.#e.getBoundingClientRect();
52
+ (t.width > 0 || t.height > 0) && this.#u(t.width, t.height), this.#c();
53
+ }
54
+ #s() {
55
+ this.#a?.disconnect(), this.#a = null, this.#l = !1;
56
+ let e = this.#e.style;
57
+ e.removeProperty("display"), e.removeProperty("max-width"), e.removeProperty("z-index");
58
+ }
59
+ #c() {
60
+ this.#a ??= new ResizeObserver((e) => {
61
+ let t = e[e.length - 1]?.borderBoxSize?.[0];
62
+ if (t) this.#u(t.inlineSize, t.blockSize);
63
+ else {
64
+ let e = this.#e.getBoundingClientRect();
65
+ this.#u(e.width, e.height);
66
+ }
67
+ }), this.#a.observe(this.#e);
68
+ }
69
+ #l = !1;
70
+ #u(e, t) {
71
+ let n = this.#e.style;
72
+ if (!this.#l) {
73
+ let e = getComputedStyle(this.#e).display;
74
+ if (e) {
75
+ let t = a[e];
76
+ t && (n.display = t), this.#l = !0;
77
+ }
78
+ }
79
+ n.margin = `0 ${-e}px ${-t}px 0`;
80
+ }
81
+ }, c = class {
30
82
  #e;
31
83
  #t;
32
84
  #n = !1;
@@ -43,7 +95,7 @@ var a = (e) => {
43
95
  x: t.left ?? 0,
44
96
  y: t.top ?? 0
45
97
  };
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;
98
+ let r = getComputedStyle(t), i = l.has(r.position), a = i ? parseFloat(r.left) || 0 : t.offsetLeft, o = i ? parseFloat(r.top) || 0 : t.offsetTop;
47
99
  return {
48
100
  x: e(n(a, t), t),
49
101
  y: e(n(o, t), t)
@@ -60,15 +112,19 @@ var a = (e) => {
60
112
  height: n(r?.clientHeight ?? window.innerHeight, e)
61
113
  };
62
114
  }
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";
115
+ }, l = /* @__PURE__ */ new Set([
116
+ "absolute",
117
+ "fixed",
118
+ "sticky"
119
+ ]);
120
+ function u(e, t, n) {
121
+ let r = e.top != null || e.left != null || e.fixed === !0, i = l.has(getComputedStyle(e).position), a = null;
122
+ if (!r && !i) a = "unplaced";
67
123
  else {
68
124
  let t = e.offsetParent;
69
- t && t !== e.ownerDocument.body && (t.clientWidth === 0 || t.clientHeight === 0) && (o = "unsized-parent");
125
+ t && t !== e.ownerDocument.body && (t.clientWidth === 0 || t.clientHeight === 0) && (a = "unsized-parent");
70
126
  }
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);
127
+ return a !== null && (console.warn(a === "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
128
  }
73
129
  //#endregion
74
- export { s as PlacementController, a as VfPositioned, c as warnMovableContract };
130
+ export { c as PlacementController, o as VfPositioned, u as warnMovableContract };