vintage-frames 0.12.8 → 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 (62) hide show
  1. package/custom-elements.json +26 -26
  2. package/dist/chrome.d.ts +1 -1
  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 +16 -20
  6. package/dist/components/vf-container.js +25 -17
  7. package/dist/components/vf-desktop.d.ts +4 -5
  8. package/dist/components/vf-desktop.js +13 -18
  9. package/dist/components/vf-fieldset.js +30 -22
  10. package/dist/components/vf-icon.js +55 -55
  11. package/dist/components/vf-list.js +30 -29
  12. package/dist/components/vf-menu.js +24 -22
  13. package/dist/components/vf-number-field.js +32 -30
  14. package/dist/components/vf-progress-bar.d.ts +2 -2
  15. package/dist/components/vf-progress-bar.js +65 -52
  16. package/dist/components/vf-radio.js +3 -4
  17. package/dist/components/vf-scroll-area.js +27 -26
  18. package/dist/components/vf-select.js +108 -90
  19. package/dist/components/vf-separator.d.ts +5 -5
  20. package/dist/components/vf-separator.js +36 -10
  21. package/dist/components/vf-slider.js +11 -11
  22. package/dist/components/vf-swatch.d.ts +2 -2
  23. package/dist/components/vf-swatch.js +73 -72
  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 +6 -9
  27. package/dist/components/vf-window.js +106 -87
  28. package/dist/index.d.ts +20 -10
  29. package/dist/index.js +63 -62
  30. package/dist/pattern-fill.d.ts +21 -50
  31. package/dist/pattern-fill.js +19 -54
  32. package/dist/scroll-rail.d.ts +6 -6
  33. package/dist/scroll-rail.js +24 -29
  34. package/dist/styles/base.d.ts +3 -1
  35. package/dist/styles/recipes/field.d.ts +10 -6
  36. package/dist/styles/recipes/field.js +8 -7
  37. package/dist/styles/recipes/focus.d.ts +7 -3
  38. package/dist/styles/recipes/pattern.d.ts +19 -10
  39. package/dist/styles/recipes/pattern.js +27 -18
  40. package/dist/styles/recipes/rule.d.ts +11 -8
  41. package/dist/styles/recipes/rule.js +31 -13
  42. package/dist/styles/recipes/scroll-rail.d.ts +21 -13
  43. package/dist/styles/recipes/scroll-rail.js +27 -37
  44. package/dist/styles/recipes/shadow.d.ts +10 -6
  45. package/dist/styles/recipes/shadow.js +4 -2
  46. package/dist/styles/recipes/stroke.d.ts +85 -0
  47. package/dist/styles/recipes/stroke.js +53 -0
  48. package/dist/styles/recipes/surface.d.ts +8 -4
  49. package/dist/styles/recipes/surface.js +23 -17
  50. package/dist/styles/recipes/tile.d.ts +11 -3
  51. package/dist/styles/recipes/tile.js +12 -12
  52. package/dist/styles/recipes/title-bar.d.ts +7 -0
  53. package/dist/styles/recipes/title-bar.js +25 -22
  54. package/dist/styles/recipes/toggle.js +3 -3
  55. package/dist/tile-grid.d.ts +98 -64
  56. package/dist/tile-grid.js +63 -30
  57. package/dist/track-width.d.ts +1 -2
  58. package/dist/zoom.d.ts +9 -9
  59. package/docs/SPEC.md +33 -32
  60. package/editor/vscode.html-custom-data.json +5 -5
  61. package/editor/web-types.json +8 -8
  62. package/package.json +1 -1
@@ -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
  }
@@ -1,51 +1,39 @@
1
- import { type CSSResult, type TemplateResult } from 'lit';
1
+ import { type CSSResult, type ReactiveController, type ReactiveControllerHost, type TemplateResult } from 'lit';
2
2
  import { type TileRect } from './styles/recipes/tile.js';
3
3
  /**
4
- * The tiled-fill machinery that replaces CSS `background-repeat` on the kit's
5
- * convertible tiled surfaces (the desktop dither, the windoid dots, the swatch
6
- * checker, the barber stripes).
4
+ * The tiled-fill machinery behind the kit's patterned surfaces — the desktop
5
+ * and container patterns, the windoid dots, the swatch checker, the barber
6
+ * stripes and the scroll trough.
7
7
  *
8
- * A CSS repeating fill is ONE paint-snapped box holding N *unsnapped* repeats,
9
- * each placed at `k × tileSize` where the tile size is a single stored length
10
- * the engine quantizes to its layout grid (Chromium 1/64 CSS px, Gecko 1/60).
11
- * The error compounds linearly with `k` until a motif boundary has walked a
12
- * whole device pixel and the 1-bit art smears to gray. The span construction
13
- * (`tileSpan`, src/styles/recipes/tile.ts) makes the stored length exact for
14
- * every scale the *density* ladder derives — but zoom mints scales with
15
- * arbitrary prime denominators (20/17 at Safari's 85%, 30/23 at its 115%),
16
- * and no finite lattice can hold those. ZOOM-TILE-DRIFT.md is the analysis;
17
- * TILE-GRID-PLAN.md the plan this module implements.
8
+ * **Kit art is a CSS repeat of one pre-scaled tile** ({@link repeatTile}):
9
+ * the motif laid over {@link REPEAT_TILE} system px square and drawn at n
10
+ * image px per system px, n = `--vf-scale × trueDpr` — whole by the scale
11
+ * contract — so the engine copies the tile to the device grid 1:1 and has no
12
+ * filtering to choose. The surface states the tile's size as
13
+ * {@link vfRepeatTileSize}, a length every engine holds exactly at every
14
+ * density and zoom, so each repeat lands exactly. The tile depends only on
15
+ * the art and n: it is encoded once per pair for the whole page, drawn again
16
+ * when zoom or the display changes n, and never on a resize.
17
+ * {@link RepeatTileController} keeps a box's tile current.
18
18
  *
19
- * Two rendering paths, chosen by whether the surface's pattern token is
20
- * overridden ({@link patternOverride}):
19
+ * Why 120, and why pre-scaled (TILE-REPEAT-PLAN.md, in git history at
20
+ * commit 5bc45c2, has the measurements): Safari smooths a repeated image it
21
+ * has to magnify, whatever
22
+ * `image-rendering` says, so the image must already be at device
23
+ * resolution; and WebKit on a 3× display now and then draws one small tile a
24
+ * device px narrow and resamples it, which tiles of 192 device px and up
25
+ * never showed. The repeat replaced one raster the size of the whole surface,
26
+ * re-encoded as it was resized.
21
27
  *
22
- * **Kit art (token unset): one whole-surface raster** — a single `.vf-tile-raster`
23
- * element carrying the motif tiled over the surface's declared system-px size
24
- * as a raster image at one image px per system px (`tileRaster`), stretched
25
- * `background-size: 100% 100%` under `image-rendering: pixelated`. Exact at
26
- * every scale, holdable or not, for two measured reasons: nearest-neighbor
27
- * sampling can only produce source colors, and ONE box has no interior
28
- * boundaries — Chromium paints an image background to the box's *unsnapped*
29
- * layout rect with antialiased edges, so any fill assembled from several
30
- * boxes blends a one-device-px line at every interior seam at a zoom-minted
31
- * scale, no matter how exactly the boxes were placed. (That measurement is
32
- * what buried both this plan's original per-tile form and SVG art of any
33
- * kind: Chromium rasterizes SVG images at the fractional layout size and
34
- * ignores `image-rendering` for them.) The box's own edge is the one place a
35
- * partial device pixel remains — the same status every component box already
36
- * has. The sampling stays exact because the box's stored length errs at most
37
- * 1/128 CSS px over the WHOLE surface: the relative error shrinks as the
38
- * surface grows, so the nearest-neighbor grid cannot slip a pixel at any
39
- * realistic size.
40
- *
41
- * **Consumer art (token set): the placed tile grid** — a flat set of
42
- * absolutely positioned `.vf-tile` boxes, each `T × T` system px at
28
+ * **Consumer art (a pattern token set): the placed tile grid** — a flat set
29
+ * of absolutely positioned `.vf-tile` boxes, each `T × T` system px at
43
30
  * `left: calc(var(--vf-scale, 1) * kT px)`: one single-multiplication length
44
31
  * per tile, quantized once, so each box paint-snaps onto the device grid
45
32
  * independently and the documented tile geometry (`--vf-*-pattern` art on a
46
- * 30- or 60-system-px tile) renders verbatim. Positions never accumulate
47
- * error; at a zoom-minted scale what remains is the bounded per-seam blend
48
- * above — a hairline per tile boundary, never surface-wide smear. Raster
33
+ * 30- or 60-system-px tile) renders verbatim. A token is any CSS image —
34
+ * SVG included — so it cannot be pre-scaled the way kit art is. Positions
35
+ * never accumulate error; at a scale the layout grid cannot hold, what
36
+ * remains is a hairline per tile boundary, never surface-wide smear. Raster
49
37
  * consumer art additionally magnifies nearest-neighbor (the `vf-img` idiom).
50
38
  *
51
39
  * The trap that must not come back: NEVER lay tiles out with CSS grid,
@@ -55,22 +43,18 @@ import { type TileRect } from './styles/recipes/tile.js';
55
43
  * returns. Flat absolute placement with one multiplication per tile is the
56
44
  * correctness, not a style choice.
57
45
  *
58
- * Wiring: the *container* — the layer the fill used to paint on, or a
46
+ * Wiring the grid: the *container* — the layer the fill paints on, or a
59
47
  * dedicated child filling it — takes the `vf-tile-grid` class (clip and
60
48
  * pointer transparency; it must be positioned, which every kit layer already
61
- * is). For the grid path it resolves `--_vf-tile-image` — the pattern-token
62
- * `var()` each tile stretches over its box, stated once so no data URI is
63
- * repeated per tile — and {@link tileGrid} renders the tiles. For the raster
64
- * path the component renders a `.vf-tile-raster` child whose
65
- * `background-image` it writes from `tileRaster(...)`, regenerated only when
66
- * the declared system-px size changes (density and zoom re-render nothing:
67
- * the box lengths are live against `--vf-scale`).
49
+ * is). It resolves `--_vf-tile-image` — the pattern-token `var()` each tile
50
+ * stretches over its box, stated once so no data URI is repeated per tile —
51
+ * and {@link tileGrid} renders the tiles.
68
52
  *
69
- * Forced colors keeps each surface's existing layer-level branches (flat
70
- * Canvas desktop, span-masked dots and barber, the swatch's
71
- * forced-color-adjust) — no mask pipeline rasterizes exactly at a zoom-minted
72
- * scale (image-rendering does not reach masks), so forced-colors-plus-zoom
73
- * remains the one accepted residual, unchanged from before this work.
53
+ * Forced colors keeps each surface's existing branches (flat Canvas desktop,
54
+ * span-masked dots, barber and trough, the swatch's forced-color-adjust) — no
55
+ * mask pipeline rasterizes exactly at a zoom-minted scale (image-rendering
56
+ * does not reach masks), so forced-colors-plus-zoom remains the one accepted
57
+ * residual.
74
58
  */
75
59
  export declare const vfTileGrid: CSSResult;
76
60
  /**
@@ -89,9 +73,9 @@ export declare function tileGrid({ cols, rows, tile, }: {
89
73
  }): TemplateResult[];
90
74
  /**
91
75
  * The consumer's override of a pattern token, or `''` when the token is unset
92
- * and the surface should paint its own art — how a converted surface picks
93
- * between its whole-surface raster (kit art) and its placed tile grid
94
- * (consumer art at the documented tile geometry).
76
+ * and the surface should paint its own art — how a surface picks between its
77
+ * repeating kit tile and its placed tile grid (consumer art at the
78
+ * documented tile geometry).
95
79
  *
96
80
  * Read with `getComputedStyle` at update time, not observed live: a pattern
97
81
  * token swapped at runtime without touching the component wants a nudge (any
@@ -99,15 +83,65 @@ export declare function tileGrid({ cols, rows, tile, }: {
99
83
  * contract the component's other measured inputs follow.
100
84
  */
101
85
  export declare function patternOverride(el: Element, token: string): string;
86
+ /** A motif as rect data over the cell it tiles at, in system px. */
87
+ export interface TileMotif {
88
+ readonly width: number;
89
+ readonly height: number;
90
+ readonly rects: readonly TileRect[];
91
+ }
92
+ /**
93
+ * The side of a kit repeat tile in system px: a whole number of every motif
94
+ * the kit draws (8 × 15 — the 2-, 4-, 8- and 12-px cells all divide it), and
95
+ * a whole number of layout px in every engine at every density and zoom
96
+ * (120n device px over the engine's ratio: 40n at 3×, 60n at 2×).
97
+ */
98
+ export declare const REPEAT_TILE = 120;
99
+ /**
100
+ * `background-size` for a {@link repeatTile}: {@link REPEAT_TILE} system px
101
+ * square, live against `--vf-scale`.
102
+ */
103
+ export declare const vfRepeatTileSize: CSSResult;
102
104
  /**
103
- * A one-entry cache for a surface's whole-surface raster, keyed by its
104
- * system-px size — so every render with an unchanged size (all of them except
105
- * a declared-size change) reuses the encoded image instead of re-running
106
- * `tileRaster`'s canvas encode. A surface whose *art* can change under the
107
- * same size (a `pattern` attribute, src/pattern-fill.ts) passes an `id` for
108
- * it — the fixed-motif surfaces leave it empty.
105
+ * The motif laid over {@link REPEAT_TILE} system px square and drawn at `n`
106
+ * image px per system px, as a CSS `url()` — the image a
107
+ * {@link vfRepeatTileSize} repeat copies 1:1 at that density. Cached for the
108
+ * page by the motif's content and `n`.
109
+ */
110
+ export declare function repeatTile(motif: TileMotif, n: number): string;
111
+ /**
112
+ * Device px per system px at `el`: its `--vf-scale` (a consumer override
113
+ * included) times the true device pixel ratio — the `n` a {@link repeatTile}
114
+ * shown there is drawn at.
115
+ */
116
+ export declare function devicePxAt(el: Element): number;
117
+ /** What a component hands {@link RepeatTileController}. */
118
+ export interface RepeatTileOptions {
119
+ /** The element the tile's custom property is written on. */
120
+ getBox: () => HTMLElement | null | undefined;
121
+ /** The art to tile, or null to write nothing (a consumer token owns the
122
+ * surface, or the component isn't showing it). Return the same object for
123
+ * the same art: an unchanged motif and density cost nothing. */
124
+ getArt: () => TileMotif | null | undefined;
125
+ /** The custom property the surface's `background-image` reads. */
126
+ property: string;
127
+ }
128
+ /**
129
+ * Keeps a box's {@link repeatTile} current: writes it as a custom property on
130
+ * the box's inline style after each host update, drawn for the density at
131
+ * the box, and again when zoom or the display changes that density
132
+ * ({@link onScaleChange}); removes it the moment there is nothing to paint.
133
+ * A custom-property channel rather than `background-image` itself, so a
134
+ * surface's forced-colors rule can out-cascade it without `!important`.
135
+ *
136
+ * The surface's stylesheet does the rest: `background-image` from the
137
+ * property, {@link vfRepeatTileSize}, `background-repeat: repeat`, and
138
+ * `image-rendering: pixelated` for the frame between a zoom and the new
139
+ * tile.
109
140
  */
110
- export declare class TileRasterCache {
141
+ export declare class RepeatTileController implements ReactiveController {
111
142
  #private;
112
- for(motifWidth: number, motifHeight: number, rects: readonly TileRect[], width: number, height: number, id?: string): string;
143
+ constructor(host: ReactiveControllerHost, opts: RepeatTileOptions);
144
+ hostConnected(): void;
145
+ hostDisconnected(): void;
146
+ hostUpdated(): void;
113
147
  }
package/dist/tile-grid.js CHANGED
@@ -1,53 +1,86 @@
1
- import { sysLength as e } from "./scale.js";
2
- import { tileRaster as t } from "./styles/recipes/tile.js";
3
- import { css as n, html as r } from "lit";
1
+ import { truePixelRatio as e } from "./zoom.js";
2
+ import { effectiveScale as t, onScaleChange as n, sysLength as r } from "./scale.js";
3
+ import { tileRaster as i } from "./styles/recipes/tile.js";
4
+ import { css as a, html as o } from "lit";
4
5
  //#region src/tile-grid.ts
5
- var i = n`
6
+ var s = a`
6
7
  .vf-tile-grid {
7
8
  /* Clip the overdraw: a fill overdrawn past a box that is not a whole
8
- number of tiles (or a raster ceiled up to one) crops here. */
9
+ number of tiles crops here. */
9
10
  overflow: hidden;
10
11
  pointer-events: none;
11
12
  }
12
- .vf-tile,
13
- .vf-tile-raster {
13
+ .vf-tile {
14
14
  position: absolute;
15
15
  /* The box IS the size — never the image's intrinsic size, and never an
16
16
  intra-box repeat: a consumer tile smaller than its box repeating
17
17
  inside it would re-import the accumulation this module removes. */
18
+ background-image: var(--_vf-tile-image);
18
19
  background-size: 100% 100%;
19
20
  background-repeat: no-repeat;
20
- /* Raster art samples nearest-neighbor — the exactness mechanism for the
21
- kit's own tiles and the vf-img idiom for consumer raster art. A no-op
22
- for SVG (which is why kit art is raster; see tileRaster). */
21
+ /* Consumer raster art samples nearest-neighbor — the vf-img idiom. A
22
+ no-op for SVG. */
23
23
  image-rendering: pixelated;
24
24
  }
25
- .vf-tile {
26
- background-image: var(--_vf-tile-image);
27
- }
28
- .vf-tile-raster {
29
- top: 0;
30
- left: 0;
31
- }
32
25
  `;
33
- function a({ cols: t, rows: n, tile: i }) {
34
- let a = e(i), o = [];
35
- for (let s = 0; s < n; s++) for (let n = 0; n < t; n++) o.push(r`<span
26
+ function c({ cols: e, rows: t, tile: n }) {
27
+ let i = r(n), a = [];
28
+ for (let s = 0; s < t; s++) for (let t = 0; t < e; t++) a.push(o`<span
36
29
  class="vf-tile"
37
- style="left:${e(n * i)};top:${e(s * i)};width:${a};height:${a}"
30
+ style="left:${r(t * n)};top:${r(s * n)};width:${i};height:${i}"
38
31
  ></span>`);
39
- return o;
32
+ return a;
40
33
  }
41
- function o(e, t) {
34
+ function l(e, t) {
42
35
  return typeof getComputedStyle > "u" ? "" : getComputedStyle(e).getPropertyValue(t).trim();
43
36
  }
44
- var s = class {
45
- #e = "";
46
- #t = "";
47
- for(e, n, r, i, a, o = "") {
48
- let s = `${o}|${i}x${a}`;
49
- return this.#e !== s && (this.#e = s, this.#t = t(e, n, r, i, a)), this.#t;
37
+ var u = 120, d = a`
38
+ background-size: calc(var(--vf-scale, 1) * ${120}px)
39
+ calc(var(--vf-scale, 1) * ${120}px);
40
+ `, f = /* @__PURE__ */ new Map();
41
+ function p(e, t) {
42
+ let n = `${e.width}×${e.height}|${t}|${e.rects.join(";")}`, r = f.get(n);
43
+ return r || (r = i(e.width, e.height, e.rects, 120, 120, t), f.set(n, r)), r;
44
+ }
45
+ function m(n) {
46
+ return Math.max(1, Math.round(t(n) * e()));
47
+ }
48
+ var h = class {
49
+ #e;
50
+ #t;
51
+ #n = null;
52
+ #r = null;
53
+ #i = 0;
54
+ #a = "";
55
+ constructor(e, t) {
56
+ this.#e = t, e.addController(this);
57
+ }
58
+ hostConnected() {
59
+ this.#t = n(() => this.#o());
60
+ }
61
+ hostDisconnected() {
62
+ this.#t?.(), this.#t = void 0;
63
+ }
64
+ hostUpdated() {
65
+ this.#o();
66
+ }
67
+ #o() {
68
+ let e = this.#e.getBox() ?? null;
69
+ e !== this.#n && (this.#s(), this.#n = e);
70
+ let t = this.#e.getArt() ?? null;
71
+ if (!e || !t) {
72
+ this.#s();
73
+ return;
74
+ }
75
+ let n = m(e);
76
+ if (t === this.#r && n === this.#i && this.#a) return;
77
+ this.#r = t, this.#i = n;
78
+ let r = p(t, n);
79
+ r !== this.#a && (this.#a = r, e.style.setProperty(this.#e.property, r));
80
+ }
81
+ #s() {
82
+ this.#r = null, this.#i = 0, this.#a && (this.#a = "", this.#n?.style.removeProperty(this.#e.property));
50
83
  }
51
84
  };
52
85
  //#endregion
53
- export { s as TileRasterCache, o as patternOverride, a as tileGrid, i as vfTileGrid };
86
+ export { u as REPEAT_TILE, h as RepeatTileController, m as devicePxAt, l as patternOverride, p as repeatTile, c as tileGrid, d as vfRepeatTileSize, s as vfTileGrid };
@@ -22,8 +22,7 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit';
22
22
  * runs on every update.
23
23
  *
24
24
  * The height comes along from the same observation, for a box measured on
25
- * both axes: `PatternFillController` (src/pattern-fill.ts) sizes a pattern
26
- * raster to a box whose extent nobody declared.
25
+ * both axes.
27
26
  */
28
27
  export declare class TrackWidthController implements ReactiveController {
29
28
  private readonly host;
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