vintage-frames 0.13.0 → 0.14.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 (44) hide show
  1. package/custom-elements.json +8 -8
  2. package/dist/components/vf-checkbox.d.ts +6 -5
  3. package/dist/components/vf-checkbox.js +41 -37
  4. package/dist/components/vf-container.d.ts +11 -13
  5. package/dist/components/vf-container.js +24 -12
  6. package/dist/components/vf-fieldset.js +30 -22
  7. package/dist/components/vf-icon.js +55 -55
  8. package/dist/components/vf-list.js +30 -29
  9. package/dist/components/vf-menu.js +24 -22
  10. package/dist/components/vf-number-field.js +32 -30
  11. package/dist/components/vf-progress-bar.js +28 -29
  12. package/dist/components/vf-radio.js +3 -4
  13. package/dist/components/vf-scroll-area.js +27 -26
  14. package/dist/components/vf-select.js +108 -90
  15. package/dist/components/vf-separator.d.ts +5 -5
  16. package/dist/components/vf-separator.js +36 -10
  17. package/dist/components/vf-swatch.js +47 -45
  18. package/dist/components/vf-text-area.js +37 -35
  19. package/dist/components/vf-text-field.js +20 -18
  20. package/dist/components/vf-window.js +92 -72
  21. package/dist/index.d.ts +10 -1
  22. package/dist/index.js +60 -59
  23. package/dist/styles/base.d.ts +3 -1
  24. package/dist/styles/recipes/field.d.ts +10 -6
  25. package/dist/styles/recipes/field.js +8 -7
  26. package/dist/styles/recipes/focus.d.ts +7 -3
  27. package/dist/styles/recipes/rule.d.ts +11 -8
  28. package/dist/styles/recipes/rule.js +31 -13
  29. package/dist/styles/recipes/scroll-rail.d.ts +7 -6
  30. package/dist/styles/recipes/scroll-rail.js +18 -18
  31. package/dist/styles/recipes/shadow.d.ts +10 -6
  32. package/dist/styles/recipes/shadow.js +4 -2
  33. package/dist/styles/recipes/stroke.d.ts +85 -0
  34. package/dist/styles/recipes/stroke.js +53 -0
  35. package/dist/styles/recipes/surface.d.ts +8 -4
  36. package/dist/styles/recipes/surface.js +23 -17
  37. package/dist/styles/recipes/title-bar.d.ts +7 -0
  38. package/dist/styles/recipes/title-bar.js +25 -22
  39. package/dist/styles/recipes/toggle.js +3 -3
  40. package/dist/zoom.d.ts +9 -9
  41. package/docs/SPEC.md +23 -22
  42. package/editor/vscode.html-custom-data.json +4 -4
  43. package/editor/web-types.json +7 -7
  44. package/package.json +1 -1
@@ -64,13 +64,17 @@ export declare const vfFocus: import("lit").CSSResult;
64
64
  * the box continues 6px past it (2px half-leading from vfBase's 1.25 line box
65
65
  * over the 16px em, plus the face's 4px descent): 6 − 2 = 4.
66
66
  * - `-2px` is a well whose ink runs to its own bottom edge — the radio's circle,
67
- * a field's wrapper — putting the rule in the second row below it.
68
- * - `-3px` adds a 1px border the pseudo-element sits inside of (vf-checkbox).
67
+ * a field's wrapper, a box framed by a stroke (vf-checkbox), whose line is
68
+ * inside the padding box — putting the rule in the second row below it.
69
69
  * - past that, the rule also clears a hard shadow, which is ink no box the
70
- * pseudo-element can size to contains: `-4px` for vf-select's 1px one, and
70
+ * pseudo-element can size to contains: `-3px` for vf-select's 1px one, and
71
71
  * for vf-swatch an offset that composes `--vf-shadow-offset` rather than
72
72
  * hard-coding a depth its consumer can retheme.
73
73
  *
74
+ * Under forced colors a stroke is a border again and the padding box sits
75
+ * inside it, so a stroked carrier states its offset and sides with
76
+ * `vfStrokeInset`, which adds the stroke back there.
77
+ *
74
78
  * Drawn in `currentColor`, so it inverts with the label on a pressed face.
75
79
  */
76
80
  export declare const vfFocusUnderline: import("lit").CSSResult;
@@ -9,15 +9,18 @@ export type RuleEdge = (typeof RULE_EDGES)[number];
9
9
  * floor is `.vf-rule-bottom`, a window's status strip's ceiling is
10
10
  * `.vf-rule-top`, and `vf-container rule="…"` takes the edges by name.
11
11
  *
12
- * A border, not a pseudo-element or a painted line, so it lives inside the
13
- * box's own `border-box` size (vfBase) and insets the padding box the way
14
- * FrameRect insets a rectangle's interior: content, percentage fills and
15
- * children placed against the box all begin inside the rule. Scales with
16
- * `--vf-scale`, so it is one system px at every density, and paints in
17
- * `--vf-black`, so it remaps under forced colors with the rest of the ink.
12
+ * A stroke (see {@link vfStrokeDecls}), inside the box's own `border-box`
13
+ * size (vfBase): the rule's row is padding and an inset shadow paints it
14
+ * black, so it is one system px at every density and zoom, and in-flow
15
+ * content begins inside it. Children placed against the box, and percentage
16
+ * fills, measure from the box's outer edge; a component whose contract puts
17
+ * them inside the rule gives them an inner box to lay out in (vf-container
18
+ * uses its slot). Paints in `--vf-black`, and under forced colors turns back
19
+ * into a border, so it remaps with the rest of the ink.
18
20
  *
19
- * One declaration per edge, so the kit cannot grow two rules — the
20
- * {@link vfHardShadowDecls} principle, applied to the line.
21
+ * The classes compose on one element: each edge's shadow goes through its
22
+ * own private property and one rule paints all four, so the classes own the
23
+ * element's `box-shadow`, and its padding on their edges.
21
24
  *
22
25
  * `npm run verify:rule` asserts a container's rule against the menu bar's.
23
26
  */
@@ -1,32 +1,50 @@
1
- import { css as e, unsafeCSS as t } from "lit";
1
+ import { STROKE_FORCED_COLORS as e, strokeBorder as t, strokeEdgeShadow as n, strokePadding as r } from "./stroke.js";
2
+ import { css as i, unsafeCSS as a } from "lit";
2
3
  //#region src/styles/recipes/rule.ts
3
- var n = [
4
+ var o = [
4
5
  "top",
5
6
  "right",
6
7
  "bottom",
7
8
  "left"
8
- ], r = t("calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000)"), i = e`
9
+ ], s = (e) => a(`
10
+ --_vf-rule-${e}: ${n(e, 1)};
11
+ padding-${e}: ${r(0, 1)};
12
+ border-${e}: ${t(1)};
13
+ `), c = i`
14
+ .vf-rule-top,
15
+ .vf-rule-right,
16
+ .vf-rule-bottom,
17
+ .vf-rule-left {
18
+ --_vf-stroke-ink: var(--vf-black, #000);
19
+ --_vf-rule-top: 0 0 transparent;
20
+ --_vf-rule-right: 0 0 transparent;
21
+ --_vf-rule-bottom: 0 0 transparent;
22
+ --_vf-rule-left: 0 0 transparent;
23
+ box-shadow: var(--_vf-rule-top), var(--_vf-rule-right), var(--_vf-rule-bottom),
24
+ var(--_vf-rule-left);
25
+ ${a(e)}
26
+ }
9
27
  .vf-rule-top {
10
- border-top: ${r};
28
+ ${s("top")}
11
29
  }
12
30
  .vf-rule-right {
13
- border-right: ${r};
31
+ ${s("right")}
14
32
  }
15
33
  .vf-rule-bottom {
16
- border-bottom: ${r};
34
+ ${s("bottom")}
17
35
  }
18
36
  .vf-rule-left {
19
- border-left: ${r};
37
+ ${s("left")}
20
38
  }
21
39
  `;
22
- function a(e) {
40
+ function l(e) {
23
41
  if (e == null) return [];
24
- let t = e.trim().split(/\s+/).filter(Boolean), r = new Set(t);
25
- for (let e of r) if (!n.includes(e)) return null;
26
- return n.filter((e) => r.has(e));
42
+ let t = e.trim().split(/\s+/).filter(Boolean), n = new Set(t);
43
+ for (let e of n) if (!o.includes(e)) return null;
44
+ return o.filter((e) => n.has(e));
27
45
  }
28
- function o(e) {
46
+ function u(e) {
29
47
  return e.map((e) => `vf-rule-${e}`).join(" ");
30
48
  }
31
49
  //#endregion
32
- export { n as RULE_EDGES, a as parseRule, o as ruleClasses, i as vfRule };
50
+ export { o as RULE_EDGES, l as parseRule, u as ruleClasses, c as vfRule };
@@ -28,16 +28,17 @@ export declare const TROUGH: import("../../patterns.js").PatternMotif;
28
28
  *
29
29
  * Geometry (system px) — the classic 16px cell, of which the outermost line
30
30
  * is the component's own 1px frame: the rail element is the 15 inside it —
31
- * a 1px divider on the content side (the rail's own border) plus the 14px
31
+ * a 1px divider on the content side (the rail's own stroke) plus the 14px
32
32
  * channel. Arrow cells are 15 long (14 interior + the 1px divider facing the
33
33
  * track); their glyphs are the 16-unit sprites windowed to the 14×14
34
34
  * interior (`viewBox="1 1 14 14"`), since the sprite's outer ring is exactly
35
- * the frame/divider lines the borders draw. The thumb is a fixed 16px white
36
- * box spanning the channel, its border reading as inset 1px from each
35
+ * the frame/divider lines the strokes draw. The thumb is a fixed 16px white
36
+ * box spanning the channel, its stroke reading as inset 1px from each
37
37
  * channel rail over its extent. The component supplies the outer frame as a
38
- * real border and lays the rail out as a sibling of the scroller — AFTER it,
39
- * so the state selectors below can reach the rail from the scroller's
40
- * attributes.
38
+ * stroke of its own and lays the rail out as a sibling of the scroller —
39
+ * AFTER it, so the state selectors below can reach the rail from the
40
+ * scroller's attributes. Every line here is a stroke (vfStrokeDecls):
41
+ * padding holds it and an inset shadow paints it.
41
42
  *
42
43
  * States, keyed off the attributes `ScrollStateController` writes on the
43
44
  * scroller (now styling real elements, so there is no scrollbar-pseudo
@@ -1,9 +1,10 @@
1
1
  import { tileImage as e, tileRects as t, vfTileMaskSize as n } from "./tile.js";
2
2
  import { vfRepeatTileSize as r } from "../../tile-grid.js";
3
3
  import { PATTERNS as i, patternMotif as a } from "../../patterns.js";
4
- import { css as o, unsafeCSS as s } from "lit";
4
+ import { vfStrokeDecls as o } from "./stroke.js";
5
+ import { css as s, unsafeCSS as c } from "lit";
5
6
  //#region src/styles/recipes/scroll-rail.ts
6
- var c = a(i["gray-25"]), l = e(c.width, c.height, t(c.rects)), u = o`
7
+ var l = a(i["gray-25"]), u = e(l.width, l.height, t(l.rects)), d = s`
7
8
  /* Hide the native bar, keep the native scrolling. Both lines ship:
8
9
  scrollbar-width reaches Chromium 121+/Firefox/Safari 18.2+, the pseudo
9
10
  covers older WebKit — the last scrollbar pseudo left in the kit.
@@ -37,7 +38,7 @@ var c = a(i["gray-25"]), l = e(c.width, c.height, t(c.rects)), u = o`
37
38
  calc(var(--vf-scale, 1) * 15px)
38
39
  1fr
39
40
  calc(var(--vf-scale, 1) * 15px);
40
- border-left: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
41
+ ${o({ edges: ["left"] })}
41
42
  }
42
43
  .vf-rail--horizontal {
43
44
  height: calc(var(--vf-scale, 1) * 15px);
@@ -45,7 +46,7 @@ var c = a(i["gray-25"]), l = e(c.width, c.height, t(c.rects)), u = o`
45
46
  calc(var(--vf-scale, 1) * 15px)
46
47
  1fr
47
48
  calc(var(--vf-scale, 1) * 15px);
48
- border-top: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
49
+ ${o({ edges: ["top"] })}
49
50
  }
50
51
  /* Explicit placement, so hiding the arrows (inactive-window/degenerate
51
52
  states) never reflows the track into an arrow cell — its box holds still
@@ -70,11 +71,11 @@ var c = a(i["gray-25"]), l = e(c.width, c.height, t(c.rects)), u = o`
70
71
  }
71
72
 
72
73
  /* Arrow cells: white boxes carrying only the 1px divider that faces the
73
- track — the frame edges are the component's border, the content edge the
74
- rail's own divider — so no line ever doubles to 2px. Each cell's padding
74
+ track — the frame edges are the component's stroke, the content edge the
75
+ rail's own divider — so no line ever doubles to 2px. Each cell's content
75
76
  box is by construction the sprite's 14×14 interior, wherever the cell
76
77
  sits (flush to the frame, or against the corner cell), so the glyph
77
- lands at the padding origin with no per-variant offsets. */
78
+ lands at the content origin with no per-variant offsets. */
78
79
  .vf-rail-button {
79
80
  display: grid;
80
81
  place-items: start;
@@ -82,16 +83,16 @@ var c = a(i["gray-25"]), l = e(c.width, c.height, t(c.rects)), u = o`
82
83
  overflow: hidden;
83
84
  }
84
85
  .vf-rail--vertical .vf-rail-button--decrement {
85
- border-bottom: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
86
+ ${o({ edges: ["bottom"] })}
86
87
  }
87
88
  .vf-rail--vertical .vf-rail-button--increment {
88
- border-top: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
89
+ ${o({ edges: ["top"] })}
89
90
  }
90
91
  .vf-rail--horizontal .vf-rail-button--decrement {
91
- border-right: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
92
+ ${o({ edges: ["right"] })}
92
93
  }
93
94
  .vf-rail--horizontal .vf-rail-button--increment {
94
- border-left: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
95
+ ${o({ edges: ["left"] })}
95
96
  }
96
97
  .vf-rail-arrow {
97
98
  display: block;
@@ -130,7 +131,7 @@ var c = a(i["gray-25"]), l = e(c.width, c.height, t(c.rects)), u = o`
130
131
  pointer-events: none;
131
132
  }
132
133
  /* The fixed System 7 thumb (never proportional): a white 16px box spanning
133
- the channel. Its border reads as inset 1px from each channel rail — the
134
+ the channel. Its stroke reads as inset 1px from each channel rail — the
134
135
  divider and the frame — over its extent; the controller writes its
135
136
  travel as a whole-system-px translate. */
136
137
  .vf-rail-thumb {
@@ -138,7 +139,7 @@ var c = a(i["gray-25"]), l = e(c.width, c.height, t(c.rects)), u = o`
138
139
  top: 0;
139
140
  left: 0;
140
141
  background: var(--vf-scrollbar-thumb, #ffffff);
141
- border: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
142
+ ${o()}
142
143
  }
143
144
  .vf-rail--vertical .vf-rail-thumb {
144
145
  width: 100%;
@@ -161,8 +162,7 @@ var c = a(i["gray-25"]), l = e(c.width, c.height, t(c.rects)), u = o`
161
162
  width: calc(var(--vf-scale, 1) * 15px);
162
163
  height: calc(var(--vf-scale, 1) * 15px);
163
164
  background: var(--vf-white, #fff);
164
- border-top: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
165
- border-left: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000);
165
+ ${o({ edges: ["top", "left"] })}
166
166
  }
167
167
 
168
168
  /* ── Always-a-rail (driven by ScrollStateController) ─────────────────────
@@ -208,10 +208,10 @@ var c = a(i["gray-25"]), l = e(c.width, c.height, t(c.rects)), u = o`
208
208
  .vf-rail-trough {
209
209
  background-image: none;
210
210
  background-color: var(--vf-black, #000);
211
- mask-image: ${s(l)};
212
- ${n(c.width, c.height)}
211
+ mask-image: ${c(u)};
212
+ ${n(l.width, l.height)}
213
213
  }
214
214
  }
215
215
  `;
216
216
  //#endregion
217
- export { c as TROUGH, u as vfScrollRail };
217
+ export { l as TROUGH, d as vfScrollRail };
@@ -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;