vintage-frames 0.1.1 → 0.2.1

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 (50) hide show
  1. package/custom-elements.json +348 -40
  2. package/dist/components/vf-button.d.ts +1 -1
  3. package/dist/components/vf-checkbox.d.ts +5 -0
  4. package/dist/components/vf-checkbox.js +18 -10
  5. package/dist/components/vf-container.d.ts +2 -2
  6. package/dist/components/vf-desktop.d.ts +1 -1
  7. package/dist/components/vf-fieldset.d.ts +1 -1
  8. package/dist/components/vf-fieldset.js +6 -1
  9. package/dist/components/vf-grid.d.ts +1 -1
  10. package/dist/components/vf-icon.d.ts +1 -1
  11. package/dist/components/vf-img.d.ts +1 -1
  12. package/dist/components/vf-label.d.ts +1 -1
  13. package/dist/components/vf-list-item.d.ts +9 -1
  14. package/dist/components/vf-list-item.js +15 -14
  15. package/dist/components/vf-list.d.ts +1 -1
  16. package/dist/components/vf-menu-bar.d.ts +1 -1
  17. package/dist/components/vf-menu-item.d.ts +10 -1
  18. package/dist/components/vf-menu-item.js +35 -34
  19. package/dist/components/vf-menu.d.ts +11 -2
  20. package/dist/components/vf-menu.js +33 -32
  21. package/dist/components/vf-option.d.ts +11 -1
  22. package/dist/components/vf-option.js +16 -15
  23. package/dist/components/vf-paragraph.d.ts +3 -3
  24. package/dist/components/vf-progress-bar.d.ts +1 -1
  25. package/dist/components/vf-radio.d.ts +5 -0
  26. package/dist/components/vf-radio.js +16 -11
  27. package/dist/components/vf-scroll-area.d.ts +1 -1
  28. package/dist/components/vf-select.d.ts +1 -1
  29. package/dist/components/vf-separator.d.ts +1 -1
  30. package/dist/components/vf-slider.d.ts +1 -1
  31. package/dist/components/vf-stack.d.ts +27 -9
  32. package/dist/components/vf-stack.js +30 -8
  33. package/dist/components/vf-swatch.d.ts +1 -1
  34. package/dist/components/vf-window.d.ts +1 -1
  35. package/dist/cross-center.d.ts +76 -0
  36. package/dist/cross-center.js +69 -0
  37. package/dist/define.d.ts +2 -2
  38. package/dist/define.js +1 -1
  39. package/dist/grid-snap.d.ts +2 -24
  40. package/dist/grid-snap.js +7 -33
  41. package/dist/index.d.ts +5 -5
  42. package/dist/index.js +50 -50
  43. package/dist/position.d.ts +11 -2
  44. package/dist/styles/recipes/host.js +14 -1
  45. package/dist/styles/recipes/toggle.js +24 -0
  46. package/dist/text-control.d.ts +1 -1
  47. package/docs/SPEC.md +10 -9
  48. package/editor/vscode.html-custom-data.json +50 -10
  49. package/editor/web-types.json +96 -16
  50. package/package.json +1 -3
@@ -1,22 +1,23 @@
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 { vfPanel as a } from "../styles/recipes/surface.js";
7
- import { vfFocusUnderline as o } from "../styles/recipes/focus.js";
8
- import { GridSnapController as s } from "../grid-snap.js";
9
- import { DocumentListenersController as c, releaseAfterGesture as l } from "../document-listeners.js";
10
- import { deferActivation as u, emit as d } from "../events.js";
11
- import { FocusRuleController as f } from "../focus-modality.js";
12
- import { TypeAheadBuffer as p } from "../type-ahead.js";
13
- import { MenuPressController as m } from "../menu-press.js";
14
- import { LitElement as h, css as g, html as _, nothing as v } from "lit";
15
- import { property as y, query as b, queryAssignedElements as x } 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 { vfPanel as o } from "../styles/recipes/surface.js";
8
+ import { vfFocusUnderline as s } from "../styles/recipes/focus.js";
9
+ import { GridSnapController as c } from "../grid-snap.js";
10
+ import { DocumentListenersController as l, releaseAfterGesture as u } from "../document-listeners.js";
11
+ import { deferActivation as d, emit as f } from "../events.js";
12
+ import { FocusRuleController as p } from "../focus-modality.js";
13
+ import { TypeAheadBuffer as m } from "../type-ahead.js";
14
+ import { MenuPressController as h } from "../menu-press.js";
15
+ import { LitElement as g, css as _, html as v, nothing as y } from "lit";
16
+ import { property as b, query as x, queryAssignedElements as S } from "lit/decorators.js";
16
17
  //#region src/components/vf-menu.ts
17
- var S = class extends h {
18
+ var C = class extends r(g) {
18
19
  constructor(...e) {
19
- super(...e), this.scale = new t(this), this.gridSnap = new s(this), this.#e = new f(this), this.label = "", this.open = !1, this.barTabIndex = 0, this.#t = new m(this, {
20
+ super(...e), this.scale = new t(this), this.gridSnap = new c(this), this.#e = new p(this), this.label = "", this.open = !1, this.barTabIndex = 0, this.#t = new h(this, {
20
21
  menus: () => [this],
21
22
  open: (e) => {
22
23
  e.open = !0;
@@ -24,7 +25,7 @@ var S = class extends h {
24
25
  close: () => {
25
26
  this.open = !1;
26
27
  }
27
- }), this.#r = this.attachInternals(), this.#i = new p(), this.#a = !1, this.#o = () => {
28
+ }), this.#r = this.attachInternals(), this.#i = new m(), this.#a = !1, this.#o = () => {
28
29
  this.#n || this.focus(), this.open = !1;
29
30
  }, this.#s = (e) => {
30
31
  e.composedPath().includes(this) || (this.open = !1);
@@ -58,7 +59,7 @@ var S = class extends h {
58
59
  break;
59
60
  }
60
61
  }
61
- }, this.#u = new c(this, () => [[
62
+ }, this.#u = new l(this, () => [[
62
63
  document,
63
64
  "pointerdown",
64
65
  this.#s,
@@ -78,10 +79,10 @@ var S = class extends h {
78
79
  }
79
80
  static {
80
81
  this.styles = [
81
- i,
82
- r,
83
82
  a,
84
- g`
83
+ i,
84
+ o,
85
+ _`
85
86
  :host {
86
87
  display: inline-block;
87
88
  position: relative;
@@ -168,7 +169,7 @@ var S = class extends h {
168
169
  }
169
170
  :host(:not([open])) .label.vf-focus-rule .title::after {
170
171
  --vf-focus-underline-offset: -2px;
171
- ${o}
172
+ ${s}
172
173
  }
173
174
  .panel {
174
175
  position: absolute;
@@ -270,7 +271,7 @@ var S = class extends h {
270
271
  this.focus();
271
272
  }
272
273
  render() {
273
- return _`
274
+ return v`
274
275
  <div
275
276
  class="label vf-snap ${this.#e.marked ? "vf-focus-rule" : ""}"
276
277
  part="label"
@@ -278,7 +279,7 @@ var S = class extends h {
278
279
  tabindex=${this.barTabIndex}
279
280
  aria-haspopup="menu"
280
281
  aria-expanded=${this.open ? "true" : "false"}
281
- aria-label=${this.label || v}
282
+ aria-label=${this.label || y}
282
283
  @pointerdown=${this.#h}
283
284
  @click=${this.#g}
284
285
  @pointerenter=${this.#_}
@@ -290,7 +291,7 @@ var S = class extends h {
290
291
  class="panel vf-panel"
291
292
  part="panel"
292
293
  role="menu"
293
- aria-label=${this.label || v}
294
+ aria-label=${this.label || y}
294
295
  >
295
296
  <slot></slot>
296
297
  </div>
@@ -301,13 +302,13 @@ var S = class extends h {
301
302
  this.open = !this.open;
302
303
  return;
303
304
  }
304
- d(this, "vf-menu-toggle-request", { menu: this }, {
305
+ f(this, "vf-menu-toggle-request", { menu: this }, {
305
306
  cancelable: !0,
306
307
  composed: !1
307
308
  }) && (this.open = !this.open);
308
309
  }
309
310
  #h() {
310
- this.#a = !0, l(() => {
311
+ this.#a = !0, u(() => {
311
312
  this.#a = !1;
312
313
  });
313
314
  }
@@ -316,10 +317,10 @@ var S = class extends h {
316
317
  this.#a = !1;
317
318
  return;
318
319
  }
319
- u(this, e, () => this.#m());
320
+ d(this, e, () => this.#m());
320
321
  }
321
322
  #_() {
322
- d(this, "vf-menu-hover", { menu: this }, { composed: !1 });
323
+ f(this, "vf-menu-hover", { menu: this }, { composed: !1 });
323
324
  }
324
325
  #v(e) {
325
326
  if (e.key === "Enter" || e.key === " ") {
@@ -332,15 +333,15 @@ var S = class extends h {
332
333
  await this.updateComplete, this.items[0]?.focus();
333
334
  }
334
335
  };
335
- n([y()], S.prototype, "label", void 0), n([y({
336
+ n([b()], C.prototype, "label", void 0), n([b({
336
337
  type: Boolean,
337
338
  reflect: !0
338
- })], S.prototype, "open", void 0), n([y({
339
+ })], C.prototype, "open", void 0), n([b({
339
340
  type: Number,
340
341
  attribute: !1
341
- })], S.prototype, "barTabIndex", void 0), n([b(".label")], S.prototype, "_labelEl", void 0), n([x({
342
+ })], C.prototype, "barTabIndex", void 0), n([x(".label")], C.prototype, "_labelEl", void 0), n([S({
342
343
  selector: "vf-menu-item",
343
344
  flatten: !0
344
- })], S.prototype, "_assignedItems", void 0), S = n([e("vf-menu")], S);
345
+ })], C.prototype, "_assignedItems", void 0), C = n([e("vf-menu")], C);
345
346
  //#endregion
346
- export { S as VfMenu };
347
+ export { C as VfMenu };
@@ -1,5 +1,6 @@
1
1
  import { LitElement } from 'lit';
2
2
  import type { PropertyValues } from 'lit';
3
+ declare const VfOption_base: (new (...args: any[]) => import("../position.js").VfPositionedInterface) & typeof LitElement;
3
4
  /**
4
5
  * `<vf-option>` — a single choice inside a `<vf-select>` popup menu.
5
6
  *
@@ -12,6 +13,14 @@ import type { PropertyValues } from 'lit';
12
13
  *
13
14
  * The host carries `role="option"` with `aria-selected`/`aria-disabled`.
14
15
  *
16
+ * Takes `top`/`left` like every other element ({@link VfPositioned}), for the
17
+ * consumer who wants one somewhere other than a popup row. Inside its parent
18
+ * `<vf-select>` it is a row the panel measures and scrolls in flow, so stating
19
+ * an origin there takes it out of that measurement: the panel stops counting it
20
+ * toward its own width and its scroll clamp, and the rows below close the gap.
21
+ * That is the placement doing exactly what it says — not a bug — but it is not
22
+ * how a popup is laid out.
23
+ *
15
24
  * @csspart check - The ✓ checkmark shown in the left gutter when selected.
16
25
  * @cssprop [--vf-popup-height=18px] - `vf-select` pill (border box; its 1px
17
26
  * hard shadow makes the sheet's 157×19 ink box)
@@ -19,7 +28,7 @@ import type { PropertyValues } from 'lit';
19
28
  * / `vf-option` + `vf-menu-item` ✓ column (shared so the value doesn't shift
20
29
  * on open)
21
30
  */
22
- export declare class VfOption extends LitElement {
31
+ export declare class VfOption extends VfOption_base {
23
32
  #private;
24
33
  static styles: import("lit").CSSResult[];
25
34
  private readonly scale;
@@ -47,3 +56,4 @@ declare global {
47
56
  'vf-option': VfOption;
48
57
  }
49
58
  }
59
+ export {};
@@ -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;
@@ -13,11 +13,14 @@ declare const VfRadio_base: (new (...args: any[]) => import("../position.js").Vf
13
13
  * unselects its siblings.
14
14
  *
15
15
  * @slot - The label, rendered to the right of the circle with a 6px gap.
16
+ * Left empty, the label and its gap collapse: the control is the bare
17
+ * 13×13 circle.
16
18
  * @csspart circle - The 13×13 radio circle.
17
19
  * @csspart label - The label wrapper around the slot.
18
20
  * @fires vf-change - When selected by user interaction. `detail: { value: string }`.
19
21
  */
20
22
  export declare class VfRadio extends VfRadio_base {
23
+ #private;
21
24
  static styles: import("lit").CSSResult[];
22
25
  /** Whether this radio is selected. Managed by the containing group. */
23
26
  checked: boolean;
@@ -30,6 +33,8 @@ export declare class VfRadio extends VfRadio_base {
30
33
  * group — not intended to be set by consumers.
31
34
  */
32
35
  groupDisabled: boolean;
36
+ /** Whether the slot holds non-whitespace content; gates the label row. */
37
+ private _hasLabel;
33
38
  private readonly internals;
34
39
  constructor();
35
40
  /**
@@ -9,10 +9,10 @@ import { emit as s } from "../events.js";
9
9
  import { RADIO_DOT as c, RADIO_FACE as l, RADIO_RING as u, RADIO_RING_PRESSED as d } from "../glyphs.js";
10
10
  import { VfToggleControl as f } from "../toggle-control.js";
11
11
  import { LitElement as p, css as m, html as h } from "lit";
12
- import { property as g } from "lit/decorators.js";
13
- import { classMap as _ } from "lit/directives/class-map.js";
12
+ import { property as g, state as _ } from "lit/decorators.js";
13
+ import { classMap as v } from "lit/directives/class-map.js";
14
14
  //#region src/components/vf-radio.ts
15
- var v = class extends n(f(p)) {
15
+ var y = class extends n(f(p)) {
16
16
  static {
17
17
  this.styles = [
18
18
  i,
@@ -82,7 +82,7 @@ var v = class extends n(f(p)) {
82
82
  ];
83
83
  }
84
84
  constructor() {
85
- super(), this.checked = !1, this.disabled = !1, this.value = "", this.groupDisabled = !1, this.internals = this.attachInternals(), this.internals.role = "radio";
85
+ super(), this.checked = !1, this.disabled = !1, this.value = "", this.groupDisabled = !1, this._hasLabel = !1, this.internals = this.attachInternals(), this.internals.role = "radio";
86
86
  }
87
87
  get toggleInternals() {
88
88
  return this.internals;
@@ -97,7 +97,7 @@ var v = class extends n(f(p)) {
97
97
  let e = this.toggleDisabled;
98
98
  return h`
99
99
  <span
100
- class=${_({
100
+ class=${v({
101
101
  circle: !0,
102
102
  dim: e,
103
103
  "vf-snap": !0
@@ -112,14 +112,19 @@ var v = class extends n(f(p)) {
112
112
  <path class="dot" d=${c.d}></path>
113
113
  </svg>
114
114
  </span>
115
- <span class=${_({
115
+ <span class=${v({
116
116
  label: !0,
117
- dim: e
117
+ dim: e,
118
+ empty: !this._hasLabel
118
119
  })} part="label">
119
- <slot></slot>
120
+ <slot @slotchange=${this.#e}></slot>
120
121
  </span>
121
122
  `;
122
123
  }
124
+ #e(e) {
125
+ let t = e.target;
126
+ this._hasLabel = t.assignedNodes({ flatten: !0 }).some((e) => e.nodeType === Node.ELEMENT_NODE || !!e.textContent?.trim());
127
+ }
123
128
  activate() {
124
129
  this.checked || (this.externallyCoordinated || (this.checked = !0), this.focus(), s(this, "vf-change", { value: this.value }));
125
130
  }
@@ -127,9 +132,9 @@ var v = class extends n(f(p)) {
127
132
  t([g({
128
133
  type: Boolean,
129
134
  reflect: !0
130
- })], v.prototype, "checked", void 0), t([g({
135
+ })], y.prototype, "checked", void 0), t([g({
131
136
  type: Boolean,
132
137
  reflect: !0
133
- })], v.prototype, "disabled", void 0), t([g()], v.prototype, "value", void 0), t([g({ attribute: !1 })], v.prototype, "groupDisabled", void 0), v = t([e("vf-radio")], v);
138
+ })], y.prototype, "disabled", void 0), t([g()], y.prototype, "value", void 0), t([g({ attribute: !1 })], y.prototype, "groupDisabled", void 0), t([_()], y.prototype, "_hasLabel", void 0), y = t([e("vf-radio")], y);
134
139
  //#endregion
135
- export { v as VfRadio };
140
+ export { y as VfRadio };
@@ -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
+ }