vintage-frames 0.1.0 → 0.2.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 (47) hide show
  1. package/README.md +12 -233
  2. package/custom-elements.json +290 -38
  3. package/dist/components/vf-button.d.ts +1 -1
  4. package/dist/components/vf-container.d.ts +2 -2
  5. package/dist/components/vf-desktop.d.ts +1 -1
  6. package/dist/components/vf-fieldset.d.ts +1 -1
  7. package/dist/components/vf-fieldset.js +6 -1
  8. package/dist/components/vf-grid.d.ts +1 -1
  9. package/dist/components/vf-icon.d.ts +1 -1
  10. package/dist/components/vf-img.d.ts +1 -1
  11. package/dist/components/vf-label.d.ts +1 -1
  12. package/dist/components/vf-list-item.d.ts +9 -1
  13. package/dist/components/vf-list-item.js +15 -14
  14. package/dist/components/vf-list.d.ts +1 -1
  15. package/dist/components/vf-menu-bar.d.ts +1 -1
  16. package/dist/components/vf-menu-item.d.ts +10 -1
  17. package/dist/components/vf-menu-item.js +35 -34
  18. package/dist/components/vf-menu.d.ts +11 -2
  19. package/dist/components/vf-menu.js +33 -32
  20. package/dist/components/vf-option.d.ts +11 -1
  21. package/dist/components/vf-option.js +16 -15
  22. package/dist/components/vf-paragraph.d.ts +3 -3
  23. package/dist/components/vf-progress-bar.d.ts +1 -1
  24. package/dist/components/vf-scroll-area.d.ts +1 -1
  25. package/dist/components/vf-select.d.ts +1 -1
  26. package/dist/components/vf-separator.d.ts +1 -1
  27. package/dist/components/vf-slider.d.ts +1 -1
  28. package/dist/components/vf-stack.d.ts +27 -9
  29. package/dist/components/vf-stack.js +30 -8
  30. package/dist/components/vf-swatch.d.ts +1 -1
  31. package/dist/components/vf-window.d.ts +1 -1
  32. package/dist/cross-center.d.ts +76 -0
  33. package/dist/cross-center.js +69 -0
  34. package/dist/define.d.ts +2 -2
  35. package/dist/define.js +1 -1
  36. package/dist/grid-snap.d.ts +2 -24
  37. package/dist/grid-snap.js +7 -33
  38. package/dist/index.d.ts +5 -5
  39. package/dist/index.js +50 -50
  40. package/dist/position.d.ts +11 -2
  41. package/dist/styles/recipes/host.js +14 -1
  42. package/dist/styles/recipes/toggle.js +16 -0
  43. package/dist/text-control.d.ts +1 -1
  44. package/docs/SPEC.md +287 -2249
  45. package/editor/vscode.html-custom-data.json +48 -8
  46. package/editor/web-types.json +92 -12
  47. package/package.json +2 -4
@@ -1,18 +1,19 @@
1
1
  import { vfElement as e } from "../define.js";
2
2
  import { ScaleController as t } from "../scale.js";
3
3
  import n from "../_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
4
- import { vfDisplay as r } from "../styles/recipes/display-face.js";
5
- import { vfBase as i } from "../styles/recipes/host.js";
6
- import { CHECKMARK as a, glyphSvg as o } from "../glyphs.js";
7
- import { LitElement as s, css as c, html as l } from "lit";
8
- import { property as u } from "lit/decorators.js";
4
+ import { VfPositioned as r } from "../position.js";
5
+ import { vfDisplay as i } from "../styles/recipes/display-face.js";
6
+ import { vfBase as a } from "../styles/recipes/host.js";
7
+ import { CHECKMARK as o, glyphSvg as s } from "../glyphs.js";
8
+ import { LitElement as c, css as l, html as u } from "lit";
9
+ import { property as d } from "lit/decorators.js";
9
10
  //#region src/components/vf-option.ts
10
- var d = class extends s {
11
+ var f = class extends r(c) {
11
12
  static {
12
13
  this.styles = [
14
+ a,
13
15
  i,
14
- r,
15
- c`
16
+ l`
16
17
  :host {
17
18
  position: relative;
18
19
  display: flex;
@@ -97,23 +98,23 @@ var d = class extends s {
97
98
  e.has("selected") && (this.#e.ariaSelected = this.selected ? "true" : "false"), e.has("disabled") && (this.#e.ariaDisabled = this.disabled ? "true" : null);
98
99
  }
99
100
  render() {
100
- return l`
101
+ return u`
101
102
  <span class="check" part="check" aria-hidden="true"
102
- >${o(a, "checkmark")}</span
103
+ >${s(o, "checkmark")}</span
103
104
  >
104
105
  <slot></slot>
105
106
  `;
106
107
  }
107
108
  };
108
- n([u()], d.prototype, "value", void 0), n([u({
109
+ n([d()], f.prototype, "value", void 0), n([d({
109
110
  type: Boolean,
110
111
  reflect: !0
111
- })], d.prototype, "disabled", void 0), n([u({
112
+ })], f.prototype, "disabled", void 0), n([d({
112
113
  type: Boolean,
113
114
  reflect: !0
114
- })], d.prototype, "selected", void 0), n([u({
115
+ })], f.prototype, "selected", void 0), n([d({
115
116
  type: Boolean,
116
117
  reflect: !0
117
- })], d.prototype, "active", void 0), d = n([e("vf-option")], d);
118
+ })], f.prototype, "active", void 0), f = n([e("vf-option")], f);
118
119
  //#endregion
119
- export { d as VfOption };
120
+ export { f as VfOption };
@@ -19,8 +19,8 @@ declare const VfParagraph_base: (new (...args: any[]) => import("../size.js").Vf
19
19
  * everything after it further off the device-pixel grid, smearing 1-bit
20
20
  * borders and bitmap glyph stems (docs/SIZING.md rule 2). A
21
21
  * whole-pixel line box accumulates whole offsets;
22
- * - its own {@link GridSnapController}, so it holds its own origin once the
23
- * page opts in with `applyGridSnap()`;
22
+ * - its own {@link GridSnapController}, so it holds its own origin
23
+ * wherever the page puts it;
24
24
  * - a **declared box** when the layout wants one — `width`/`height` in whole
25
25
  * system px ({@link VfSized}). In flow a paragraph takes its container's
26
26
  * width, which is usually right; a *placed* one (`top`/`left`) shrink-wraps
@@ -52,7 +52,7 @@ export declare class VfParagraph extends VfParagraph_base {
52
52
  static styles: import("lit").CSSResult[];
53
53
  /** Default-on display scaling (true 72dpi size); see src/scale.ts. */
54
54
  private readonly scale;
55
- /** Device-pixel grid snapping (opt in with applyGridSnap()); see src/grid-snap.ts. */
55
+ /** Device-pixel grid snapping; see src/grid-snap.ts. */
56
56
  private readonly gridSnap;
57
57
  /**
58
58
  * Which embedded face to set the copy in — `'body'` (Geneva, the
@@ -28,7 +28,7 @@ export declare class VfProgressBar extends VfProgressBar_base {
28
28
  #private;
29
29
  static styles: import("lit").CSSResult[];
30
30
  private readonly scale;
31
- /** Device-pixel grid snapping (opt in with applyGridSnap()); see src/grid-snap.ts. */
31
+ /** Device-pixel grid snapping; see src/grid-snap.ts. */
32
32
  private readonly gridSnap;
33
33
  /** Current progress, from 0 to `max`. Clamped for display and ARIA. */
34
34
  value: number;
@@ -30,7 +30,7 @@ declare const VfScrollArea_base: (new (...args: any[]) => import("../position.js
30
30
  */
31
31
  export declare class VfScrollArea extends VfScrollArea_base {
32
32
  private readonly scale;
33
- /** Device-pixel grid snapping (opt in with applyGridSnap()); see src/grid-snap.ts. */
33
+ /** Device-pixel grid snapping; see src/grid-snap.ts. */
34
34
  private readonly gridSnap;
35
35
  /**
36
36
  * Which scroll rails to reserve as permanent placeholders: `vertical`
@@ -116,7 +116,7 @@ export declare class VfSelect extends VfSelect_base {
116
116
  private cachedOptions;
117
117
  /** Default-on display scaling (true 72dpi size); see src/scale.ts. */
118
118
  private readonly scale;
119
- /** Device-pixel grid snapping (opt in with applyGridSnap()); see src/grid-snap.ts. */
119
+ /** Device-pixel grid snapping; see src/grid-snap.ts. */
120
120
  private readonly gridSnap;
121
121
  /**
122
122
  * Whether the closed pill wears the kit's dashed focus rule — keyboard focus
@@ -22,7 +22,7 @@ export declare class VfSeparator extends VfSeparator_base {
22
22
  #private;
23
23
  static styles: import("lit").CSSResult[];
24
24
  private readonly scale;
25
- /** Device-pixel grid snapping (opt in with applyGridSnap()); see src/grid-snap.ts. */
25
+ /** Device-pixel grid snapping; see src/grid-snap.ts. */
26
26
  private readonly gridSnap;
27
27
  /** Render as a vertical rule (1px wide) instead of horizontal. */
28
28
  vertical: boolean;
@@ -48,7 +48,7 @@ export declare class VfSlider extends VfSlider_base {
48
48
  private readonly trackSize;
49
49
  /** Default-on display scaling (true 72dpi size); see src/scale.ts. */
50
50
  private readonly scale;
51
- /** Device-pixel grid snapping (opt in with applyGridSnap()); see src/grid-snap.ts. */
51
+ /** Device-pixel grid snapping; see src/grid-snap.ts. */
52
52
  private readonly gridSnap;
53
53
  /** True when this component owns the host `tabindex`. */
54
54
  private selfManagedTabIndex;
@@ -93,12 +93,16 @@ declare const VfStack_base: (new (...args: any[]) => import("../size.js").VfSize
93
93
  * and a layout box for *anything*, and consumer content inside it — a div, an
94
94
  * `<img>`, a run of text — has no controller of its own. So the flex
95
95
  * container, the placed-child anchor and the `vf-snap` class live on one
96
- * shadow box, and under `applyGridSnap()` the correction moves the stack's
96
+ * shadow box, and the correction moves the stack's
97
97
  * whole coordinate system; children (kit or not) ride it, and the sweep's
98
98
  * outermost-first order means the kit children then find nothing left to
99
99
  * correct. What this deliberately does not fix: a text-governed child width
100
100
  * mid-row still shifts later siblings fractionally (their own controllers
101
- * cover that), and centering still can't land on a whole pixel by itself.
101
+ * cover that).
102
+ *
103
+ * **Centering lands on whole system px as well** — halving an odd count of free
104
+ * system px gives a half, which no container can round in CSS. See
105
+ * {@link CrossCenterController}.
102
106
  *
103
107
  * It is also the kit's one **typographically transparent** component: `vfBase`'s
104
108
  * chrome face, ratio line box, color and non-selectability are all reset to
@@ -148,18 +152,18 @@ export declare class VfStack extends VfStack_base {
148
152
  * `text-align` on any element, so the cross-axis switch used to re-align every
149
153
  * run of copy inside the stack (see the `text-align` reset above).
150
154
  *
151
- * Note the one thing centering cannot do: land on a whole pixel by itself. A
152
- * 16px caption centered in a row set by the 25-system-px `vf-number-field`
153
- * sits at 4.5 system px, and no container can round that — it would have to
154
- * read each child's height. `applyGridSnap()` keeps the caption's own ink
155
- * crisp regardless (it corrects the origin inside the child's shadow root);
156
- * `place="start"` is the deterministic escape.
155
+ * Centering divides the free space in two, so an odd count of system px would
156
+ * land a child on a half — a 16px caption centered in a row set by the
157
+ * 25-system-px `vf-number-field` sits at 4.5. {@link CrossCenterController}
158
+ * steps that back onto whole system px, the exact half going toward the start
159
+ * the way QuickDraw's `div 2` did; `place="start"` is still the way to ask
160
+ * for no centering at all.
157
161
  */
158
162
  place?: VfStackPlace;
159
163
  /** Default-on display scaling (true 72dpi size); see src/scale.ts. */
160
164
  private readonly scale;
161
165
  /**
162
- * Hold the box on the device-pixel grid under `applyGridSnap()` — see the
166
+ * Hold the box on the device-pixel grid — see the
163
167
  * class doc for why the original no-controller decision was reversed. The
164
168
  * host is what gets measured; `.box` (vf-snap) is where the correction
165
169
  * lands, taking the whole coordinate system with it.
@@ -167,6 +171,20 @@ export declare class VfStack extends VfStack_base {
167
171
  private readonly gridSnap;
168
172
  /** The shadow flex box `gap`/`pad` write to; exists from the first render. */
169
173
  private readonly box;
174
+ /** The slot the children arrive through. (`slot` itself is HTMLElement's.) */
175
+ private readonly childSlot;
176
+ /**
177
+ * Keeps cross-axis centering on whole system px (src/cross-center.ts).
178
+ * A null axis is the inert state — no observers, nothing held — which is
179
+ * every column that starts its children, i.e. the default.
180
+ */
181
+ private readonly crossCenter;
182
+ /**
183
+ * Mirrors the stylesheet's alignment selectors: a row centers unless `place`
184
+ * names something else, a column only when it says `center`, and an
185
+ * unrecognized value lands on the direction's default in both places.
186
+ */
187
+ private get centersChildren();
170
188
  /**
171
189
  * `gap` and `pad` go on the shadow box's inline style as
172
190
  * `calc(var(--vf-scale, 1) * Npx)`, the way VfSized writes the declared
@@ -5,15 +5,20 @@ import { VfPositioned as a } from "../position.js";
5
5
  import { vfBase as o } from "../styles/recipes/host.js";
6
6
  import { GridSnapController as s } from "../grid-snap.js";
7
7
  import { VfSized as c } from "../size.js";
8
- import { LitElement as l, css as u, html as d } from "lit";
9
- import { property as f, query as p } from "lit/decorators.js";
8
+ import { CrossCenterController as l } from "../cross-center.js";
9
+ import { LitElement as u, css as d, html as f } from "lit";
10
+ import { property as p, query as m } from "lit/decorators.js";
10
11
  //#region src/components/vf-stack.ts
11
- var m = class extends c(a(l)) {
12
+ var h = class extends c(a(u)) {
12
13
  constructor(...e) {
13
- super(...e), this.direction = "column", this.gap = 0, this.scale = new t(this), this.gridSnap = new s(this);
14
+ super(...e), this.direction = "column", this.gap = 0, this.scale = new t(this), this.gridSnap = new s(this), this.crossCenter = new l(this, () => ({
15
+ box: this.box,
16
+ slot: this.childSlot,
17
+ axis: this.centersChildren ? this.direction === "row" ? "y" : "x" : null
18
+ }));
14
19
  }
15
20
  static {
16
- this.styles = [o, u`
21
+ this.styles = [o, d`
17
22
  :host {
18
23
  /* Shrink to the content, but stay BLOCK-level while doing it: the stack
19
24
  is exactly as big as what it holds, so it can't hand a size it never
@@ -126,6 +131,20 @@ var m = class extends c(a(l)) {
126
131
  align-items: flex-end;
127
132
  }
128
133
 
134
+ /* The centering tie: CrossCenterController steps a centered child onto
135
+ whole system px through these variables (src/cross-center.ts has the
136
+ why). Keyed off the marker because a ::slotted rule outranks a slotted
137
+ element's own :host — a blanket position would restyle vf-window and
138
+ vf-menu. */
139
+ :host(:not([direction='row'])) ::slotted([data-vf-tie]) {
140
+ position: relative;
141
+ left: var(--vf-stack-dx, 0px);
142
+ }
143
+ :host([direction='row']) ::slotted([data-vf-tie]) {
144
+ position: relative;
145
+ top: var(--vf-stack-dy, 0px);
146
+ }
147
+
129
148
  /* --- Children ----------------------------------------------------- */
130
149
  /* Neither grow nor shrink: the content governs the box, not the other way
131
150
  round (see the class doc). A light-DOM declaration beats a ::slotted
@@ -156,13 +175,16 @@ var m = class extends c(a(l)) {
156
175
  }
157
176
  `];
158
177
  }
178
+ get centersChildren() {
179
+ return this.place === "center" ? !0 : this.place === "start" || this.place === "end" ? !1 : this.direction === "row";
180
+ }
159
181
  updated(e) {
160
182
  e.has("gap") && (this.box.style.gap = n(this.gap)), e.has("pad") && (this.box.style.padding = r(this.pad));
161
183
  }
162
184
  render() {
163
- return d`<div class="vf-snap box"><slot></slot></div>`;
185
+ return f`<div class="vf-snap box"><slot></slot></div>`;
164
186
  }
165
187
  };
166
- i([f({ reflect: !0 })], m.prototype, "direction", void 0), i([f({ type: Number })], m.prototype, "gap", void 0), i([f()], m.prototype, "pad", void 0), i([f({ reflect: !0 })], m.prototype, "place", void 0), i([p(".box")], m.prototype, "box", void 0), m = i([e("vf-stack")], m);
188
+ i([p({ reflect: !0 })], h.prototype, "direction", void 0), i([p({ type: Number })], h.prototype, "gap", void 0), i([p()], h.prototype, "pad", void 0), i([p({ reflect: !0 })], h.prototype, "place", void 0), i([m(".box")], h.prototype, "box", void 0), i([m("slot")], h.prototype, "childSlot", void 0), h = i([e("vf-stack")], h);
167
189
  //#endregion
168
- export { m as VfStack };
190
+ export { h as VfStack };
@@ -79,7 +79,7 @@ export declare class VfSwatch extends VfSwatch_base {
79
79
  disabled: boolean;
80
80
  /** Default-on display scaling (true 72dpi size); see src/scale.ts. */
81
81
  private readonly scale;
82
- /** Device-pixel grid snapping (opt in with applyGridSnap()); see src/grid-snap.ts. */
82
+ /** Device-pixel grid snapping; see src/grid-snap.ts. */
83
83
  private readonly gridSnap;
84
84
  /**
85
85
  * The consumer's `--vf-swatch-checker` override, or `''` for the kit
@@ -92,7 +92,7 @@ export declare class VfWindow extends VfWindow_base {
92
92
  scrollbars?: 'vertical' | 'horizontal' | 'both';
93
93
  /** Default-on display scaling (true 72dpi size); see src/scale.ts. */
94
94
  private readonly scale;
95
- /** Device-pixel grid snapping (opt in with applyGridSnap()); see src/grid-snap.ts. */
95
+ /** Device-pixel grid snapping; see src/grid-snap.ts. */
96
96
  private readonly gridSnap;
97
97
  /** Holds the centered title patch on the placement lattice (src/chrome.ts). */
98
98
  private readonly titleCenter;
@@ -0,0 +1,76 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ /** The axis a container centers its children across — `null` when it doesn't. */
3
+ export type CrossAxis = 'x' | 'y' | null;
4
+ /** What {@link CrossCenterController} reads off its host before each measure. */
5
+ export interface CrossCenterParts {
6
+ /** The flex box the children are centered in. */
7
+ box: HTMLElement | null | undefined;
8
+ /** The slot they arrive through. */
9
+ slot: HTMLSlotElement | null | undefined;
10
+ /** Which axis that centering happens on, if it happens at all. */
11
+ axis: CrossAxis;
12
+ }
13
+ /**
14
+ * Marks a held child; the container's `::slotted()` rule keys off it. The
15
+ * container owns the correction, the child carries only the fact of it.
16
+ */
17
+ export declare const CENTER_TIE_ATTRIBUTE = "data-vf-tie";
18
+ /**
19
+ * Holds cross-axis centering on the system-pixel grid.
20
+ *
21
+ * Centering halves the free space, and half of an odd count of system px is a
22
+ * half: a 71-system-px child centered in 200 sits at 64.5 — half a *system*
23
+ * px, so the child's whole 1-bit interior misses the device grid at every
24
+ * density. CSS can't round it: no rule can say "half my child's width,
25
+ * floored", and a fractional transform leaves the composite-time fringe
26
+ * grid-snap.ts measured. So the offset is measured here and corrected through
27
+ * layout: each centered child steps to the nearest whole system px, the exact
28
+ * half going toward the start as QuickDraw's `div 2` did.
29
+ *
30
+ * It measures where flexbox actually put each child rather than deriving from
31
+ * the two widths — a stack holds anything, including a child whose width isn't
32
+ * whole system px (`vf-select` is 87⅓ at 1.5×), and deriving would move such a
33
+ * child *off* a grid position it already held. Everything is relative to the
34
+ * box, so a stack the page put on a fractional origin is untouched: that stays
35
+ * the page's fault, or the grid snapper's to absorb (src/grid-snap.ts).
36
+ *
37
+ * The correction lands as {@link CENTER_TIE_ATTRIBUTE} plus a length in
38
+ * `--vf-stack-dx` / `--vf-stack-dy`, applied by the container's own
39
+ * `::slotted([data-vf-tie])` rule (see vf-stack.ts). A marker plus a variable
40
+ * is the weakest channel available — any inline style or page rule beats it —
41
+ * and the marker matters because a `::slotted` rule outranks a slotted
42
+ * element's own `:host` (measured): a blanket `position` would restyle
43
+ * `vf-window` and `vf-menu`.
44
+ *
45
+ * Re-measures after every host render and on any resize of the box or a child
46
+ * — a density change, a late-registering face, an edited label. A relative
47
+ * offset changes no box's size, so the measurement stays stable: the applied
48
+ * correction is subtracted back out before the next one is derived, and a
49
+ * child already on the grid is left exactly as it is.
50
+ */
51
+ export declare class CrossCenterController implements ReactiveController {
52
+ private readonly host;
53
+ private readonly read;
54
+ private observer?;
55
+ private observed;
56
+ private slot?;
57
+ private readonly held;
58
+ constructor(host: ReactiveControllerHost & HTMLElement, read: () => CrossCenterParts);
59
+ hostConnected(): void;
60
+ hostUpdated(): void;
61
+ hostDisconnected(): void;
62
+ /** Re-attach to the current box, slot and children, then re-measure. */
63
+ private readonly sync;
64
+ /**
65
+ * Detach from everything and give every held child its box back. Keeps the
66
+ * observer instance, so a re-parented stack doesn't allocate a fresh one.
67
+ */
68
+ private rest;
69
+ /** Observe the box and every child, once per distinct set. */
70
+ private watch;
71
+ /** Put every centered child on the nearest whole system px, ties to the start. */
72
+ private hold;
73
+ private apply;
74
+ /** Give a child back exactly the box it arrived with. */
75
+ private clear;
76
+ }
@@ -0,0 +1,69 @@
1
+ import { effectiveScale as e } from "./scale.js";
2
+ //#region src/cross-center.ts
3
+ var t = "data-vf-tie", n = {
4
+ x: "--vf-stack-dx",
5
+ y: "--vf-stack-dy"
6
+ }, r = 1 / 32, i = .05, a = (e) => parseFloat(e) || 0;
7
+ function o(e, t) {
8
+ let n = getComputedStyle(e), r = e.getBoundingClientRect();
9
+ return t === "x" ? r.left + a(n.borderLeftWidth) + a(n.paddingLeft) : r.top + a(n.borderTopWidth) + a(n.paddingTop);
10
+ }
11
+ var s = class {
12
+ constructor(e, t) {
13
+ this.host = e, this.read = t, this.observed = [], this.held = /* @__PURE__ */ new Map(), this.sync = () => {
14
+ let e = this.read(), { box: t, slot: n, axis: r } = e;
15
+ if (!t || !n || !r) return this.rest();
16
+ n !== this.slot && (this.slot?.removeEventListener("slotchange", this.sync), n.addEventListener("slotchange", this.sync), this.slot = n), this.watch(t, n), this.hold(e);
17
+ }, e.addController(this);
18
+ }
19
+ hostConnected() {
20
+ this.sync();
21
+ }
22
+ hostUpdated() {
23
+ this.sync();
24
+ }
25
+ hostDisconnected() {
26
+ this.rest();
27
+ }
28
+ rest() {
29
+ this.observer?.disconnect(), this.observed = [], this.slot?.removeEventListener("slotchange", this.sync), this.slot = void 0;
30
+ for (let e of [...this.held.keys()]) this.clear(e);
31
+ }
32
+ watch(e, t) {
33
+ if (typeof ResizeObserver > "u") return;
34
+ let n = [e, ...t.assignedElements()];
35
+ if (!(n.length === this.observed.length && n.every((e, t) => e === this.observed[t]))) {
36
+ this.observer ??= new ResizeObserver(() => this.hold(this.read())), this.observer.disconnect();
37
+ for (let e of n) this.observer.observe(e);
38
+ this.observed = n;
39
+ }
40
+ }
41
+ hold({ box: t, slot: n, axis: a }) {
42
+ if (!t || !n || !a) return;
43
+ let s = e(this.host), c = o(t, a), l = new Set(this.held.keys());
44
+ for (let e of n.assignedElements()) {
45
+ if (!(e instanceof HTMLElement)) continue;
46
+ l.delete(e);
47
+ let t = getComputedStyle(e).position;
48
+ if (t === "absolute" || t === "fixed") {
49
+ this.clear(e);
50
+ continue;
51
+ }
52
+ let n = e.getBoundingClientRect(), o = a === "x" ? n.left : n.top, u = this.held.get(e), d = o - c - (u?.axis === a ? u.offset : 0), f = Math.ceil(d / s - .5 - i) * s - d;
53
+ Math.abs(f) < r ? this.clear(e) : this.apply(e, a, f);
54
+ }
55
+ for (let e of l) this.clear(e);
56
+ }
57
+ apply(e, r, i) {
58
+ let a = this.held.get(e);
59
+ (a?.axis !== r || a.offset !== i) && (e.style.setProperty(n[r], `${i}px`), e.style.removeProperty(n[r === "x" ? "y" : "x"]), e.setAttribute(t, ""), this.held.set(e, {
60
+ axis: r,
61
+ offset: i
62
+ }));
63
+ }
64
+ clear(e) {
65
+ this.held.delete(e) && (e.style.removeProperty(n.x), e.style.removeProperty(n.y), e.removeAttribute(t), e.getAttribute("style") || e.removeAttribute("style"));
66
+ }
67
+ };
68
+ //#endregion
69
+ export { t as CENTER_TIE_ATTRIBUTE, s as CrossCenterController };
package/dist/define.d.ts CHANGED
@@ -14,8 +14,8 @@
14
14
  * working. The warning matters as much as the guard, because a duplicated copy
15
15
  * is not benign even once the page survives it: the scaling, grid-snapping and
16
16
  * focus-modality registries are all module-scoped singletons, so the second
17
- * copy's `applyGridSnap()` reaches none of the first copy's components and the
18
- * two disagree silently. Better to say so than to let it look fine.
17
+ * copy's schedulers reach none of the first copy's components and the two
18
+ * disagree silently. Better to say so than to let it look fine.
19
19
  */
20
20
  /**
21
21
  * Register `ctor` under `tagName` unless the name is already taken.
package/dist/define.js CHANGED
@@ -3,7 +3,7 @@ var e = (e, t) => {
3
3
  let n = customElements.get(e);
4
4
  if (n !== t) {
5
5
  if (n !== void 0) {
6
- console.warn(`[vintage-frames] <${e}> is already registered on this page, so this second registration was skipped and the first one kept. Two copies of the library also mean two of every module-scoped singleton — applyGridSnap(), applyScale() and the focus-modality tracker each only reach the components built from their own copy. Dedupe vintage-frames to a single version.`);
6
+ console.warn(`[vintage-frames] <${e}> is already registered on this page, so this second registration was skipped and the first one kept. Two copies of the library also mean two of every module-scoped singleton — the grid-snap scheduler, applyScale() and the focus-modality tracker each only reach the components built from their own copy. Dedupe vintage-frames to a single version.`);
7
7
  return;
8
8
  }
9
9
  customElements.define(e, t);
@@ -9,9 +9,8 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit';
9
9
  *
10
10
  * The component's template must put the `vf-snap` class (see `vfBase`) on its
11
11
  * top-level painted element(s); the host box is what gets measured, so an
12
- * authored offset inside it (a toggle's centered box) stays put. Dormant
13
- * until the app calls {@link applyGridSnap}, and skipped per-element by a
14
- * `nosnap` attribute on the host.
12
+ * authored offset inside it (a toggle's centered box) stays put. Always
13
+ * active; skipped per-element by a `nosnap` attribute on the host.
15
14
  */
16
15
  export declare class GridSnapController implements ReactiveController {
17
16
  readonly host: ReactiveControllerHost & HTMLElement;
@@ -44,27 +43,6 @@ export declare class GridSnapController implements ReactiveController {
44
43
  /** Drop the correction: delete the two variables and forget everything. */
45
44
  reset(): void;
46
45
  }
47
- /**
48
- * Opt the page into automatic device-pixel-grid snapping: every mounted
49
- * component (and every one mounted afterwards) holds its paint on whole device
50
- * pixels, whatever the surrounding layout does. Returns a cleanup function
51
- * that turns it back off. Calls share one switch: snapping stays on until
52
- * every caller's cleanup has run, and running a cleanup twice releases only
53
- * once — so two widgets on one page can each opt in and out without turning
54
- * the other's snapping off.
55
- *
56
- * ```ts
57
- * import { applyGridSnap } from 'vintage-frames'
58
- * applyGridSnap()
59
- * ```
60
- *
61
- * The whole footprint on your DOM is two reserved custom properties
62
- * (`--vf-snap-dx`/`--vf-snap-dy`) on each corrected host's inline style; the
63
- * offset they drive is applied inside the component's own shadow root. Worth
64
- * knowing: a component's painted box can sit up to half a device pixel outside
65
- * its layout box while corrected.
66
- */
67
- export declare function applyGridSnap(): () => void;
68
46
  /**
69
47
  * Re-check every component before the next paint. Call it after moving
70
48
  * components in a way nothing observable changed size — the one case the
package/dist/grid-snap.js CHANGED
@@ -6,29 +6,14 @@ var n = .05, r = 1 / 64, i = () => e() || 1, a = (e, t) => {
6
6
  return n - Math.round(n);
7
7
  }, o = (e) => Math.round(e / r) * r, s = new class {
8
8
  constructor() {
9
- this.controllers = /* @__PURE__ */ new Set(), this.enabled = !1, this.frame = 0, this.teardown = [];
9
+ this.controllers = /* @__PURE__ */ new Set(), this.enabled = !1, this.frame = 0;
10
10
  }
11
11
  register(e) {
12
- if (this.controllers.add(e), !this.enabled) {
13
- e.reset();
14
- return;
15
- }
16
- this.resizes?.observe(e.host), this.request();
12
+ this.controllers.add(e), !this.enabled && typeof window < "u" ? (this.enabled = !0, this.install()) : this.resizes?.observe(e.host), this.request();
17
13
  }
18
14
  unregister(e) {
19
15
  this.controllers.delete(e), this.resizes?.unobserve(e.host);
20
16
  }
21
- enable() {
22
- this.enabled || typeof window > "u" || (this.enabled = !0, this.install(), this.request());
23
- }
24
- disable() {
25
- if (this.enabled) {
26
- this.enabled = !1, this.frame && cancelAnimationFrame(this.frame), this.frame = 0;
27
- for (let e of this.teardown.splice(0)) e();
28
- this.resizes?.disconnect(), this.resizes = void 0;
29
- for (let e of this.controllers) e.reset();
30
- }
31
- }
32
17
  request() {
33
18
  !this.enabled || this.frame || typeof window > "u" || (this.frame = requestAnimationFrame(() => {
34
19
  this.frame = 0, this.sweep();
@@ -45,17 +30,13 @@ var n = .05, r = 1 / 64, i = () => e() || 1, a = (e, t) => {
45
30
  window.addEventListener("resize", e), window.addEventListener("orientationchange", e), window.addEventListener("scroll", e, {
46
31
  capture: !0,
47
32
  passive: !0
48
- }), this.teardown.push(() => {
49
- window.removeEventListener("resize", e), window.removeEventListener("orientationchange", e), window.removeEventListener("scroll", e, { capture: !0 });
50
33
  });
51
34
  let n = document.fonts;
52
- n && (n.addEventListener("loadingdone", e), this.teardown.push(() => n.removeEventListener("loadingdone", e)), n.ready.then(e)), this.teardown.push(t(e));
53
- let r = new MutationObserver(e);
54
- r.observe(document.documentElement, {
35
+ n && (n.addEventListener("loadingdone", e), n.ready.then(e)), t(e), new MutationObserver(e).observe(document.documentElement, {
55
36
  subtree: !0,
56
37
  attributes: !0,
57
38
  attributeFilter: ["nosnap"]
58
- }), this.teardown.push(() => r.disconnect());
39
+ });
59
40
  }
60
41
  }(), c = class {
61
42
  constructor(e) {
@@ -120,16 +101,9 @@ var n = .05, r = 1 / 64, i = () => e() || 1, a = (e, t) => {
120
101
  let t = 0;
121
102
  for (let n = l(e); n; n = l(n)) n.tagName.toLowerCase().startsWith("vf-") && t++;
122
103
  return t;
123
- }, d = 0;
124
- function f() {
125
- d++, s.enable();
126
- let e = !1;
127
- return () => {
128
- e || (e = !0, --d === 0 && s.disable());
129
- };
130
- }
131
- function p() {
104
+ };
105
+ function d() {
132
106
  s.request();
133
107
  }
134
108
  //#endregion
135
- export { c as GridSnapController, f as applyGridSnap, p as requestGridSnap };
109
+ export { c as GridSnapController, d as requestGridSnap };
package/dist/index.d.ts CHANGED
@@ -65,12 +65,12 @@ export { applyScale, ScaleController, effectiveScale, getScale, onScaleChange, s
65
65
  */
66
66
  export { getZoom, truePixelRatio, onZoomChange, devicePxPerSystemPx, resetZoomBaseline, } from './zoom.js';
67
67
  /**
68
- * Automatic device-pixel-grid snapping. `applyGridSnap()` is the one call an
69
- * app makes to have every component hold its own origin on whole device pixels
70
- * whatever the surrounding layout does the half of the layout contract the
71
- * components can keep for you.
68
+ * Automatic device-pixel-grid snapping. Always on: every component holds its
69
+ * own origin on whole device pixels whatever the surrounding layout does the
70
+ * half of the layout contract the components keep for you. `requestGridSnap()`
71
+ * re-checks before the next paint, for a move nothing observable resized.
72
72
  */
73
- export { applyGridSnap, requestGridSnap, GridSnapController, } from './grid-snap.js';
73
+ export { requestGridSnap, GridSnapController } from './grid-snap.js';
74
74
  /**
75
75
  * The page-drawn cursor. `applyCursor()` is the one call an app makes to
76
76
  * replace the native pointer with the embedded System 7 set — arrow, I-beam,