vintage-frames 0.13.0 → 0.14.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 (79) hide show
  1. package/README.md +16 -0
  2. package/custom-elements.json +184 -20
  3. package/dist/components/vf-checkbox.d.ts +6 -5
  4. package/dist/components/vf-checkbox.js +41 -37
  5. package/dist/components/vf-container.d.ts +14 -14
  6. package/dist/components/vf-container.js +58 -28
  7. package/dist/components/vf-desktop.d.ts +114 -12
  8. package/dist/components/vf-desktop.js +119 -58
  9. package/dist/components/vf-fieldset.js +30 -22
  10. package/dist/components/vf-icon-field.d.ts +1 -1
  11. package/dist/components/vf-icon.js +111 -104
  12. package/dist/components/vf-list.js +30 -29
  13. package/dist/components/vf-menu-bar.d.ts +5 -0
  14. package/dist/components/vf-menu-bar.js +67 -43
  15. package/dist/components/vf-menu.js +24 -22
  16. package/dist/components/vf-number-field.js +32 -30
  17. package/dist/components/vf-progress-bar.js +28 -29
  18. package/dist/components/vf-radio.js +3 -4
  19. package/dist/components/vf-scroll-area.js +27 -26
  20. package/dist/components/vf-select.js +108 -90
  21. package/dist/components/vf-separator.d.ts +5 -5
  22. package/dist/components/vf-separator.js +36 -10
  23. package/dist/components/vf-swatch.js +47 -45
  24. package/dist/components/vf-text-area.js +37 -35
  25. package/dist/components/vf-text-field.js +20 -18
  26. package/dist/components/vf-window.d.ts +13 -0
  27. package/dist/components/vf-window.js +101 -72
  28. package/dist/index.d.ts +12 -1
  29. package/dist/index.js +61 -59
  30. package/dist/pattern-fill.d.ts +11 -8
  31. package/dist/pattern-fill.js +2 -4
  32. package/dist/position.d.ts +25 -7
  33. package/dist/position.js +6 -3
  34. package/dist/shell/alert.d.ts +32 -0
  35. package/dist/shell/alert.js +44 -0
  36. package/dist/shell/catalog.d.ts +168 -0
  37. package/dist/shell/catalog.js +278 -0
  38. package/dist/shell/clock.d.ts +16 -0
  39. package/dist/shell/clock.js +36 -0
  40. package/dist/shell/filing.d.ts +39 -0
  41. package/dist/shell/filing.js +68 -0
  42. package/dist/shell/finder.d.ts +116 -0
  43. package/dist/shell/finder.js +564 -0
  44. package/dist/shell/geometry.d.ts +196 -0
  45. package/dist/shell/geometry.js +241 -0
  46. package/dist/shell/index.d.ts +29 -0
  47. package/dist/shell/index.js +11 -0
  48. package/dist/shell/shell.d.ts +150 -0
  49. package/dist/shell/shell.js +213 -0
  50. package/dist/shell/state.d.ts +88 -0
  51. package/dist/shell/state.js +103 -0
  52. package/dist/shell/storage.d.ts +14 -0
  53. package/dist/shell/storage.js +41 -0
  54. package/dist/shell/windows.d.ts +215 -0
  55. package/dist/shell/windows.js +343 -0
  56. package/dist/styles/base.d.ts +3 -1
  57. package/dist/styles/recipes/field.d.ts +10 -6
  58. package/dist/styles/recipes/field.js +8 -7
  59. package/dist/styles/recipes/focus.d.ts +7 -3
  60. package/dist/styles/recipes/rule.d.ts +11 -8
  61. package/dist/styles/recipes/rule.js +31 -13
  62. package/dist/styles/recipes/scroll-rail.d.ts +7 -6
  63. package/dist/styles/recipes/scroll-rail.js +18 -18
  64. package/dist/styles/recipes/shadow.d.ts +10 -6
  65. package/dist/styles/recipes/shadow.js +4 -2
  66. package/dist/styles/recipes/stroke.d.ts +85 -0
  67. package/dist/styles/recipes/stroke.js +53 -0
  68. package/dist/styles/recipes/surface.d.ts +8 -4
  69. package/dist/styles/recipes/surface.js +23 -17
  70. package/dist/styles/recipes/title-bar.d.ts +7 -0
  71. package/dist/styles/recipes/title-bar.js +25 -22
  72. package/dist/styles/recipes/toggle.js +3 -3
  73. package/dist/window-chrome.d.ts +43 -0
  74. package/dist/window-chrome.js +13 -0
  75. package/dist/zoom.d.ts +9 -9
  76. package/docs/SPEC.md +34 -32
  77. package/editor/vscode.html-custom-data.json +12 -7
  78. package/editor/web-types.json +50 -14
  79. package/package.json +10 -2
@@ -3,11 +3,15 @@
3
3
  * with no blur and no spread, System 7's only depth cue. The offset is
4
4
  * tokenized via `--vf-shadow-offset` and scales with `--vf-scale`.
5
5
  *
6
- * Compose it into any raised surface. Every raised surface in the kit does:
7
- * {@link vfPanel} (menus, popups) and {@link vfChromeFrame} (window and dialog
8
- * frames). One declaration, so the kit can't grow two shadows.
9
- *
10
- * Kept in its own module because it is the one recipe consumed on its own:
11
- * vf-swatch composes the shadow without either surface class.
6
+ * The shadow value, for a list: a stroked surface passes it to
7
+ * `vfStrokeDecls({ shadows: vfHardShadow })`, which paints it after the
8
+ * stroke. Every raised surface in the kit does — {@link vfPanel} (menus,
9
+ * popups) and {@link vfChromeFrame} (window frames). One value, so the kit
10
+ * can't grow two shadows.
11
+ */
12
+ export declare const vfHardShadow: import("lit").CSSResult;
13
+ /**
14
+ * {@link vfHardShadow} as the whole `box-shadow` declaration, for a box that
15
+ * casts the shadow and draws no stroke.
12
16
  */
13
17
  export declare const vfHardShadowDecls: import("lit").CSSResult;
@@ -1,5 +1,7 @@
1
1
  import { unsafeCSS as e } from "lit";
2
2
  //#region src/styles/recipes/shadow.ts
3
- var t = e("\n box-shadow: calc(var(--vf-scale, 1) * var(--vf-shadow-offset, 2px))\n calc(var(--vf-scale, 1) * var(--vf-shadow-offset, 2px)) 0 0\n var(--vf-black, #000);\n");
3
+ var t = e("calc(var(--vf-scale, 1) * var(--vf-shadow-offset, 2px))\n calc(var(--vf-scale, 1) * var(--vf-shadow-offset, 2px)) 0 0\n var(--vf-black, #000)"), n = e(`
4
+ box-shadow: ${t};
5
+ `);
4
6
  //#endregion
5
- export { t as vfHardShadowDecls };
7
+ export { t as vfHardShadow, n as vfHardShadowDecls };
@@ -0,0 +1,85 @@
1
+ import { type CSSResult } from 'lit';
2
+ import type { RuleEdge } from './rule.js';
3
+ /** One to four values in shorthand order, as top/right/bottom/left. */
4
+ type Sides<T> = T | readonly T[];
5
+ /** The padding under a stroked edge: the element's own plus the stroke's. */
6
+ export declare function strokePadding(own: number | string, width: number): string;
7
+ /** The border a stroked edge becomes under forced colors; 0 wide otherwise. */
8
+ export declare function strokeBorder(width: number): string;
9
+ /** The inset shadow that paints one edge's stroke. */
10
+ export declare function strokeEdgeShadow(edge: RuleEdge, width: number): string;
11
+ /** The forced-colors switch every stroked element carries. */
12
+ export declare const STROKE_FORCED_COLORS = "@media (forced-colors: active) {\n --_vf-stroke-pad: 0;\n }";
13
+ /**
14
+ * The inset shadow of a stroke `width` system px wide on all four edges — the
15
+ * value to restate when only the painted width changes, as vf-checkbox's
16
+ * press does. Reads the ink and forced-colors switch the element's
17
+ * {@link vfStrokeDecls} declares.
18
+ */
19
+ export declare function vfStrokeShadow(width?: number): CSSResult;
20
+ /** The options of {@link vfStrokeDecls}. */
21
+ export interface VfStrokeOptions {
22
+ /** The stroked edges. Default all four. */
23
+ edges?: readonly RuleEdge[];
24
+ /**
25
+ * The stroke's width in whole system px, one to four values in shorthand
26
+ * order. Default 1.
27
+ */
28
+ width?: Sides<number>;
29
+ /**
30
+ * The element's own padding, in system px, one to four values in shorthand
31
+ * order; each stroked edge adds its stroke. A value is a number or a
32
+ * system-px CSS length (`'var(--vf-select-gutter, 16px)'`). Omitted, the
33
+ * declarations set padding on the stroked edges alone, to the stroke.
34
+ */
35
+ padding?: Sides<number | string>;
36
+ /**
37
+ * More shadows, painted with the stroke: the hard drop shadow
38
+ * ({@link vfHardShadow}), a white ring. The stroke comes first in the list.
39
+ */
40
+ shadows?: string | CSSResult;
41
+ /** The stroke's color. Default `var(--vf-black, #000)`. */
42
+ ink?: string;
43
+ }
44
+ /**
45
+ * A 1-bit stroke that isn't a line width. The declarations for a box whose
46
+ * edges carry a black line of whole system px — every frame, well, rule and
47
+ * box in the kit — for interpolating into the box's rule:
48
+ * `.card { ${vfStrokeDecls({ padding: [4, 8] })} }`.
49
+ *
50
+ * Safari rounds a border width to device px before it applies page zoom, so a
51
+ * `border` of `calc(var(--vf-scale, 1) * 1px)` draws thinner than the art
52
+ * around it at most zoom levels, and moves everything inside the box off the
53
+ * device grid. Padding and box-shadow lengths are not rounded that way. So the
54
+ * stroke's space is padding on each stroked edge, added to the element's own,
55
+ * and the black is an inset `box-shadow`, with `shadows` after it in the same
56
+ * list. Both land on whole device px at every zoom.
57
+ *
58
+ * What changes from a border:
59
+ * - The declarations own `padding` on the stroked edges (on all four, when
60
+ * `padding` is passed) and `box-shadow`. Pass the element's own padding and
61
+ * shadows here; a later `padding` or `box-shadow` on the element replaces
62
+ * the stroke's.
63
+ * - A child placed against the box measures from its outer edge. Offset it by
64
+ * the stroke with {@link vfStrokeInset}.
65
+ * - An `overflow: hidden` box clips at its outer edge, so its content can
66
+ * paint over the stroke. Clip on an inner element instead.
67
+ * - Forced colors paints no `box-shadow`, so there each stroke is a border
68
+ * again and the padding is the element's own: the same box a border draws.
69
+ *
70
+ * The color goes through a private property the declarations set, so pass a
71
+ * `var()` as `ink` to change it from a state rule without restating the
72
+ * shadow list.
73
+ */
74
+ export declare function vfStrokeDecls(options?: VfStrokeOptions): CSSResult;
75
+ /**
76
+ * A length `base` system px in from a stroke's inner edge, measured from the
77
+ * box's outer edge, on an edge carrying `stroke` system px of it (see
78
+ * {@link vfStrokeDecls}): the inset for a child placed against a stroked box
79
+ * (`right: ${vfStrokeInset(0)}` is flush inside a 1px frame), or a padding
80
+ * that restates the stroke's (`padding: ${vfStrokeInset(0)} ${vfStrokeInset(6)}`).
81
+ * `base` is a number or a system-px CSS length. Under forced colors, where
82
+ * the stroke is a border, it is `base`.
83
+ */
84
+ export declare function vfStrokeInset(base: number | string, stroke?: number): CSSResult;
85
+ export {};
@@ -0,0 +1,53 @@
1
+ import { unsafeCSS as e } from "lit";
2
+ //#region src/styles/recipes/stroke.ts
3
+ var t = [
4
+ "top",
5
+ "right",
6
+ "bottom",
7
+ "left"
8
+ ], n = "var(--_vf-stroke-pad, 1)", r = "var(--_vf-stroke-ink)";
9
+ function i(e) {
10
+ let t = Array.isArray(e) ? e : [e], n = t[0], r = t[1] ?? n;
11
+ return [
12
+ n,
13
+ r,
14
+ t[2] ?? n,
15
+ t[3] ?? r
16
+ ];
17
+ }
18
+ var a = (e) => typeof e == "number" ? `${e}px` : e, o = (e) => `calc(var(--vf-scale, 1) * ${e}px * ${n})`;
19
+ function s(e, t) {
20
+ return e === 0 ? o(t) : `calc(var(--vf-scale, 1) * (${a(e)} + ${t}px * ${n}))`;
21
+ }
22
+ function c(e) {
23
+ return `calc(var(--vf-scale, 1) * ${e}px * (1 - ${n})) solid ${r}`;
24
+ }
25
+ function l(e, t) {
26
+ let i = (e) => `calc(var(--vf-scale, 1) * ${e * t}px * ${n})`;
27
+ switch (e) {
28
+ case "top": return `inset 0 ${i(1)} 0 0 ${r}`;
29
+ case "right": return `inset ${i(-1)} 0 0 0 ${r}`;
30
+ case "bottom": return `inset 0 ${i(-1)} 0 0 ${r}`;
31
+ case "left": return `inset ${i(1)} 0 0 0 ${r}`;
32
+ }
33
+ }
34
+ var u = "@media (forced-colors: active) {\n --_vf-stroke-pad: 0;\n }";
35
+ function d(t = 1) {
36
+ return e(`inset 0 0 0 ${o(t)} ${r}`);
37
+ }
38
+ function f(n = {}) {
39
+ let r = new Set(n.edges ?? t), o = i(n.width ?? 1), f = i(n.padding ?? 0), p = t.map((e, t) => r.has(e) ? o[t] : 0), m = p.every((e) => e > 0 && e === p[0]), h = [`--_vf-stroke-ink: ${n.ink ?? "var(--vf-black, #000)"};`], g = [];
40
+ return t.forEach((e, t) => {
41
+ let r = p[t];
42
+ if (!r) {
43
+ n.padding !== void 0 && h.push(`padding-${e}: calc(var(--vf-scale, 1) * ${a(f[t])});`);
44
+ return;
45
+ }
46
+ h.push(`padding-${e}: ${s(f[t], r)};`), h.push(`border-${e}: ${c(r)};`), m || g.push(l(e, r));
47
+ }), m && g.push(String(d(p[0]))), n.shadows && g.push(String(n.shadows)), h.push(`box-shadow: ${g.length ? g.join(", ") : "none"};`), h.push(u), e(h.join("\n "));
48
+ }
49
+ function p(t, n = 1) {
50
+ return e(s(t, n));
51
+ }
52
+ //#endregion
53
+ export { u as STROKE_FORCED_COLORS, c as strokeBorder, l as strokeEdgeShadow, s as strokePadding, f as vfStrokeDecls, p as vfStrokeInset, d as vfStrokeShadow };
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Panel recipe for menus and popups: white face, 1px black border, hard
2
+ * Panel recipe for menus and popups: white face, 1px black frame, hard
3
3
  * offset shadow.
4
4
  */
5
5
  export declare const vfPanel: import("lit").CSSResult;
6
6
  /**
7
7
  * Chrome-frame recipe for the desktop window (vf-window): the same white
8
- * face, 1px black border and hard offset shadow as {@link vfPanel}. Apply
8
+ * face, 1px black frame and hard offset shadow as {@link vfPanel}. Apply
9
9
  * `.vf-frame` to the outer frame element.
10
10
  *
11
11
  * Skin only — the component adds its own layout (a full-size flex column: the
@@ -14,7 +14,7 @@ export declare const vfPanel: import("lit").CSSResult;
14
14
  */
15
15
  export declare const vfChromeFrame: import("lit").CSSResult;
16
16
  /**
17
- * The classic modal-dialog (dBoxProc) frame: 1px outer border, 2px white gap,
17
+ * The classic modal-dialog (dBoxProc) frame: 1px outer rule, 2px white gap,
18
18
  * 2px inner band — and NO drop shadow, per the `Windows/modal dialog.png`
19
19
  * reference. System 7's alert box drew the
20
20
  * *mirror* trace (2px outer, 2px gap, 1px inner rule, with the hard shadow) —
@@ -30,10 +30,14 @@ export declare const vfChromeFrame: import("lit").CSSResult;
30
30
  * six stripes / 3 white rows, the title patch's 7px margins) sits directly
31
31
  * under the outer rule, inset 2px at either end so its stripes keep the same
32
32
  * 2px of white from the outer rule that the inner band keeps; the bar's own
33
- * 1px rule plus the inner box's 1px top border make the 2px band its floor,
33
+ * 1px rule plus the inner box's 1px top stroke make the 2px band its floor,
34
34
  * which then continues down both sides and along the bottom as the band
35
35
  * around the body. No shadow anywhere. Put the `.vf-title-bar` between the
36
36
  * two elements, as the first child of `.vf-modal-frame`, and the three rules
37
37
  * below do the rest.
38
+ *
39
+ * Both lines are strokes ({@link vfStrokeDecls}): padding holds their space
40
+ * and an inset shadow paints them, so the recipe owns both elements' padding
41
+ * and box-shadow.
38
42
  */
39
43
  export declare const vfModalFrame: import("lit").CSSResult;
@@ -1,42 +1,48 @@
1
- import { vfHardShadowDecls as e } from "./shadow.js";
2
- import { css as t, unsafeCSS as n } from "lit";
1
+ import { vfHardShadow as e } from "./shadow.js";
2
+ import { vfStrokeDecls as t, vfStrokeInset as n } from "./stroke.js";
3
+ import { css as r, unsafeCSS as i } from "lit";
3
4
  //#region src/styles/recipes/surface.ts
4
- var r = n(`
5
+ var a = i(`
5
6
  background: var(--vf-white, #fff);
6
- border: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
7
- ${e}
8
- `), i = t`
7
+ ${t({ shadows: e })}
8
+ `), o = r`
9
9
  .vf-panel {
10
- ${r}
10
+ ${a}
11
11
  }
12
- `, a = t`
12
+ `, s = r`
13
13
  .vf-frame {
14
- ${r}
14
+ ${a}
15
15
  }
16
- `, o = t`
16
+ `, c = r`
17
17
  .vf-modal-frame {
18
18
  background: var(--vf-white, #fff);
19
- border: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
19
+ ${t()}
20
20
  }
21
21
  .vf-modal-frame-inner {
22
22
  margin: calc(var(--vf-scale, 1) * 2px);
23
- border: calc(var(--vf-scale, 1) * 2px) solid var(--vf-black, #000);
23
+ ${t({ width: 2 })}
24
24
  }
25
25
  /* The bar set into the frame: 2px in from the outer rule at either end, its
26
26
  stripes flush to the bar's own edge (vfStripes' 1px side inset is the
27
- window frame's buffer — here the frame's gap is that buffer). */
27
+ window frame's buffer — here the frame's gap is that buffer). The bottom
28
+ inset counts the bar's rule, which is padding (vfTitleBar). */
28
29
  .vf-modal-frame > .vf-title-bar {
29
30
  margin: 0 calc(var(--vf-scale, 1) * 2px);
30
31
  }
31
32
  .vf-modal-frame > .vf-title-bar .vf-stripes {
32
- inset: calc(var(--vf-scale, 1) * 3px) 0;
33
+ inset: calc(var(--vf-scale, 1) * 3px) 0 ${n(3)};
33
34
  }
34
- /* Under a bar, the inner box's top border is the second row of the band —
35
+ /* Under a bar, the inner box's top stroke is the second row of the band —
35
36
  the bar's 1px rule is the first — and there is no gap row between. */
36
37
  .vf-modal-frame > .vf-title-bar + .vf-modal-frame-inner {
37
38
  margin-top: 0;
38
- border-top-width: calc(var(--vf-scale, 1) * 1px);
39
+ ${t({ width: [
40
+ 1,
41
+ 2,
42
+ 2,
43
+ 2
44
+ ] })}
39
45
  }
40
46
  `;
41
47
  //#endregion
42
- export { a as vfChromeFrame, o as vfModalFrame, i as vfPanel };
48
+ export { s as vfChromeFrame, c as vfModalFrame, o as vfPanel };
@@ -32,6 +32,9 @@
32
32
  * drag handle, vf-window's only when `[movable]` — suppressing touch scrolling
33
33
  * on a bar that can't be dragged would be a real behavior change, so each
34
34
  * component keeps that one declaration under its own selector.
35
+ *
36
+ * The rule is a stroke ({@link vfStrokeDecls}): the bottom row is padding, so
37
+ * the texture layers placed inside the bar count it in their bottom insets.
35
38
  */
36
39
  export declare const vfTitleBar: import("lit").CSSResult;
37
40
  /**
@@ -45,5 +48,9 @@ export declare const vfTitleBar: import("lit").CSSResult;
45
48
  * Geometry is the standard 18px bar's; vf-window's utility variant overrides
46
49
  * the sizes under its own selector. Host-state rules (hiding widgets on an
47
50
  * inactive window) stay with the component — a dialog has no inactive state.
51
+ *
52
+ * Each box's line is a stroke ({@link vfStrokeDecls}), its white patch ring
53
+ * the next shadow in the same list. State rules recolor the line through
54
+ * `--_vf-stroke-ink`.
48
55
  */
49
56
  export declare const vfWindowWidgets: import("lit").CSSResult;
@@ -1,20 +1,27 @@
1
1
  import { vfDisplayDecls as e } from "./display-face.js";
2
- import { css as t } from "lit";
2
+ import { vfStrokeDecls as t, vfStrokeInset as n } from "./stroke.js";
3
+ import { css as r } from "lit";
3
4
  //#region src/styles/recipes/title-bar.ts
4
- var n = t`
5
+ var i = r`
5
6
  .vf-title-bar {
6
7
  position: relative;
7
8
  /* Inert unless the frame is a flex column (vf-window); on vf-dialog's plain
8
9
  block frame the shorthand simply doesn't apply. */
9
10
  flex: none;
10
11
  height: calc(var(--vf-scale, 1) * var(--vf-titlebar-height, 18px));
11
- border-bottom: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
12
12
  display: flex;
13
13
  /* flex-start, not center: the title states its own whole-pixel row (see
14
14
  the margin below); centering 16 into 17 would land it on a half. */
15
15
  align-items: flex-start;
16
16
  justify-content: center;
17
17
  overflow: hidden;
18
+ ${t({ edges: ["bottom"] })}
19
+ }
20
+ .vf-title-bar > .vf-stripes {
21
+ bottom: ${n(3)};
22
+ }
23
+ .vf-title-bar > .vf-dots {
24
+ bottom: ${n(2)};
18
25
  }
19
26
  .vf-title {
20
27
  /* Chicago-style title (chrome); slotted body copy keeps the body face. */
@@ -36,7 +43,7 @@ var n = t`
36
43
  overflow: hidden;
37
44
  text-overflow: ellipsis;
38
45
  }
39
- `, r = t`
46
+ `, a = r`
40
47
  .box {
41
48
  position: absolute;
42
49
  /* 11×11 box with 3px of clear white above and below it (title-bar
@@ -45,21 +52,18 @@ var n = t`
45
52
  z-index: 1;
46
53
  width: calc(var(--vf-scale, 1) * 11px);
47
54
  height: calc(var(--vf-scale, 1) * 11px);
48
- padding: 0;
49
55
  margin: 0;
50
- border: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000000);
51
56
  background: var(--vf-white, #ffffff);
52
- /* A 1px white patch ring that interrupts the stripes around the
53
- box (no bevel — flat 1-bit), same buffer the stripes keep from the
54
- frame edges. --vf-widget-ring is internal geometry, not a theming
55
- knob: vf-window's utility variant widens it to 2px, the clearance
56
- the windoid sheet cuts into its dither. */
57
- box-shadow: 0 0 0 calc(var(--vf-scale, 1) * var(--vf-widget-ring, 1px))
58
- var(--vf-white, #ffffff);
59
57
  font: inherit;
60
58
  cursor: var(--vf-cursor, default);
61
59
  -webkit-appearance: none;
62
60
  appearance: none;
61
+ /* After the stroke, a 1px white patch ring that interrupts the stripes
62
+ around the box (no bevel — flat 1-bit), same buffer the stripes keep
63
+ from the frame edges. --vf-widget-ring is internal geometry, not a
64
+ theming knob: vf-window's utility variant widens it to 2px, the
65
+ clearance the windoid sheet cuts into its dither. */
66
+ ${t({ shadows: "0 0 0 calc(var(--vf-scale, 1) * var(--vf-widget-ring, 1px)) var(--vf-white, #ffffff)" })}
63
67
  }
64
68
  .close {
65
69
  left: calc(var(--vf-scale, 1) * 8px);
@@ -73,7 +77,7 @@ var n = t`
73
77
  traced pixel-for-pixel from the UI kit's close-button-active-state
74
78
  sprite. Both widgets flash the identical graphic while pressed. That
75
79
  sprite is the whole 11×11 box; its outer ring is this element's own 1px
76
- border, so the SVG draws just the 9×9 interior into the padding box. */
80
+ stroke, so the SVG draws just the 9×9 interior, centered in the box. */
77
81
  .box:active {
78
82
  background-color: var(--vf-white, #ffffff);
79
83
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='9'%3E%3Cpath d='M4 0h1v1h-1zM1 1h1v1h-1zM4 1h1v1h-1zM7 1h1v1h-1zM2 2h1v1h-1zM4 2h1v1h-1zM6 2h1v1h-1zM0 4h3v1h-3zM6 4h3v1h-3zM2 6h1v1h-1zM4 6h1v1h-1zM6 6h1v1h-1zM1 7h1v1h-1zM4 7h1v1h-1zM7 7h1v1h-1zM4 8h1v1h-1z'/%3E%3C/svg%3E");
@@ -85,17 +89,16 @@ var n = t`
85
89
  content: '';
86
90
  position: absolute;
87
91
  /* A small box nested in the TOP-LEFT corner of the widget (classic
88
- System 7 zoom box). It shares the widget's own top and left border, so
89
- only its right and bottom edges are drawn: a 6×6 box anchored at the
90
- padding-box origin whose 1px right/bottom borders land the vertical at
92
+ System 7 zoom box). It shares the widget's own top and left line, so
93
+ only its right and bottom edges are drawn: a 6×6 box anchored inside
94
+ the widget's stroke whose 1px right/bottom strokes land the vertical at
91
95
  sprite col 6 and the horizontal at row 6. Traced from the UI kit
92
96
  zoom-button rest sprite. */
93
- top: 0;
94
- left: 0;
97
+ top: ${n(0)};
98
+ left: ${n(0)};
95
99
  width: calc(var(--vf-scale, 1) * 6px);
96
100
  height: calc(var(--vf-scale, 1) * 6px);
97
- border-right: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000000);
98
- border-bottom: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000000);
101
+ ${t({ edges: ["right", "bottom"] })}
99
102
  }
100
103
  /* While pressed the zoom box shows the same sunburst as the close box, so
101
104
  its inner detail square gives way to it. */
@@ -131,4 +134,4 @@ var n = t`
131
134
  }
132
135
  `;
133
136
  //#endregion
134
- export { n as vfTitleBar, r as vfWindowWidgets };
137
+ export { i as vfTitleBar, a as vfWindowWidgets };
@@ -7,7 +7,7 @@ var t = e`
7
7
  gap: calc(var(--vf-scale, 1) * 6px);
8
8
  cursor: var(--vf-cursor, default);
9
9
  }
10
- /* The well PAINTS 3 system px below the row top. Centering the 13px well
10
+ /* The radio's 13px well PAINTS 3 system px below the row top. Centering it
11
11
  in the 20px row is the layout — kept, because the host's height and its
12
12
  exported baseline are consumer-visible (moving them off the well's
13
13
  centered bottom reflowed every line holding a toggle) — but centering
@@ -18,8 +18,8 @@ var t = e`
18
18
  compose the variable), landing the ink on row 3: ties resolve toward the
19
19
  start, as QuickDraw's "div 2" did — the title-bar and vf-stack
20
20
  convention. The half-pixel gap between layout box and painted box is the
21
- documented snap idiom, not a fault. */
22
- .box,
21
+ documented snap idiom, not a fault. The checkbox's 12px box centers on
22
+ row 4 exactly and needs no step; both wells end on row 15. */
23
23
  .circle {
24
24
  top: calc(var(--vf-snap-dy, 0px) - var(--vf-scale, 1) * 0.5px);
25
25
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A window's chrome as numbers: the insets between its outer box and the
3
+ * region its content lays out in, and the floor its grow box stops at.
4
+ *
5
+ * A page sizing a window to its content, or content to its window, needs the
6
+ * chrome between the two — a folder window's field sized to the scrolled
7
+ * plane, a palette sized to hold its cells exactly. Restating `1 + 18 + 20 +
8
+ * 15 + 1` in the page was the alternative, with a note to re-derive it if
9
+ * the kit's chrome ever changed.
10
+ */
11
+ /** The grow box's default floors, in system px: a window smaller than this can't be worked. */
12
+ export declare const WINDOW_MIN_WIDTH = 80;
13
+ export declare const WINDOW_MIN_HEIGHT = 54;
14
+ /** The declared options that decide a window's chrome — `vf-window`'s own. */
15
+ export interface VfWindowChromeOptions {
16
+ /** `'utility'` for the windoid's 12px bar; anything else is the 18px one. */
17
+ variant?: 'utility' | null;
18
+ /** The edge scroll rails, as on `vf-window`. */
19
+ scrollbars?: 'vertical' | 'horizontal' | 'both' | null;
20
+ /** The header's height, rule included, while the `header` slot is populated. */
21
+ headerHeight?: number | null;
22
+ /** Whether the `status` slot is populated. */
23
+ status?: boolean;
24
+ }
25
+ /** Four insets in system px. */
26
+ export interface VfInsets {
27
+ top: number;
28
+ right: number;
29
+ bottom: number;
30
+ left: number;
31
+ }
32
+ /**
33
+ * The insets, in system px, between a window's outer box (its `width` and
34
+ * `height`) and the region its content lays out in: the body, or under
35
+ * `scrollbars` the scrolled plane's viewport. They go both ways — the content
36
+ * of a window `W` wide is `W − left − right`, and a window that holds content
37
+ * `w` wide is `w + left + right`. The frame's 1px stroke on every side, the
38
+ * title bar (18, or 12 for a utility window) and the header above, the status
39
+ * strip (15) and a horizontal rail (15) below, a vertical rail (15) on the
40
+ * right. The numbers are the default tokens': a page that restyles
41
+ * `--vf-titlebar-height` or `--vf-status-bar-height` restates them.
42
+ */
43
+ export declare function windowChrome({ variant, scrollbars, headerHeight, status, }?: VfWindowChromeOptions): VfInsets;
@@ -0,0 +1,13 @@
1
+ //#region src/window-chrome.ts
2
+ var e = 80, t = 54;
3
+ function n({ variant: e, scrollbars: t, headerHeight: n, status: r = !1 } = {}) {
4
+ let i = t === "vertical" || t === "both", a = t === "horizontal" || t === "both";
5
+ return {
6
+ top: 1 + (e === "utility" ? 12 : 18) + Math.max(0, n ?? 0),
7
+ right: 1 + (i ? 15 : 0),
8
+ bottom: 1 + (r ? 15 : 0) + (a ? 15 : 0),
9
+ left: 1
10
+ };
11
+ }
12
+ //#endregion
13
+ export { t as WINDOW_MIN_HEIGHT, e as WINDOW_MIN_WIDTH, n as windowChrome };
package/dist/zoom.d.ts CHANGED
@@ -17,9 +17,9 @@
17
17
  *
18
18
  * The work is knowing the zoom at all. Zoom multiplies device-px-per-CSS-px in
19
19
  * every engine — that is what zoom is — but the engines differ in whether they
20
- * report it: Chrome and Firefox fold it into `devicePixelRatio`, Safari pins
21
- * `devicePixelRatio` to the hardware and moves `innerWidth` instead. So the
22
- * tracker watches two signals:
20
+ * report it: Chrome, Firefox and Safari 27 fold it into `devicePixelRatio`;
21
+ * older Safari pins `devicePixelRatio` to the hardware and moves `innerWidth`
22
+ * instead. So the tracker watches two signals:
23
23
  *
24
24
  * 1. `devicePixelRatio`, via a resolution media query re-armed at each new
25
25
  * value. A dpr change is *either* zoom or the window moving to a
@@ -31,7 +31,7 @@
31
31
  * report those members in zoom-affected CSS px) means zoom; anything else
32
32
  * means a display change, which rebases the baseline and reports nothing.
33
33
  * 2. `innerWidth`/`innerHeight` against `outerWidth`/`outerHeight`, for
34
- * Safari: zoom rescales the inner (CSS-px) viewport on both axes by the
34
+ * older Safari: zoom rescales the inner (CSS-px) viewport on both axes by the
35
35
  * same factor while the outer (screen-px) window holds still. A change in
36
36
  * the outer size is a real window resize and rebases; a one-axis inner
37
37
  * change (an edge drag, devtools docking, a sidebar) is never zoom and
@@ -170,12 +170,12 @@ export declare function getZoom(): number;
170
170
  /**
171
171
  * Device px per CSS px *right now* — the number every "snap to the device
172
172
  * grid" computation must divide by. `window.devicePixelRatio` is that number
173
- * only in engines that fold zoom into it; Safari's is simply wrong about the
174
- * current rasterization density at any non-100% zoom. Reading the live dpr and
175
- * folding the tracked zoom back in is correct in both:
173
+ * only in engines that fold zoom into it; older Safari's is simply wrong about
174
+ * the current rasterization density at any non-100% zoom. Reading the live dpr
175
+ * and folding the tracked zoom back in is correct in both:
176
176
  *
177
- * Chrome/Firefox (latched) dpr already includes zoom → dpr
178
- * Safari (never latches) dpr is the hardware → dpr × zoom
177
+ * Chrome/Firefox/Safari 27 (latched) dpr already includes zoom → dpr
178
+ * older Safari (never latches) dpr is the hardware → dpr × zoom
179
179
  *
180
180
  * The live read matters: a monitor move changes the true density whether or
181
181
  * not the tracker is running, and this stays exactly as correct as the old