vintage-frames 0.1.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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/custom-elements.json +19807 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
  5. package/dist/chrome.d.ts +73 -0
  6. package/dist/chrome.js +94 -0
  7. package/dist/components/vf-button-group.d.ts +50 -0
  8. package/dist/components/vf-button-group.js +68 -0
  9. package/dist/components/vf-button.d.ts +169 -0
  10. package/dist/components/vf-button.js +234 -0
  11. package/dist/components/vf-checkbox.d.ts +60 -0
  12. package/dist/components/vf-checkbox.js +135 -0
  13. package/dist/components/vf-container.d.ts +95 -0
  14. package/dist/components/vf-container.js +86 -0
  15. package/dist/components/vf-desktop.d.ts +243 -0
  16. package/dist/components/vf-desktop.js +310 -0
  17. package/dist/components/vf-dialog.d.ts +105 -0
  18. package/dist/components/vf-dialog.js +280 -0
  19. package/dist/components/vf-fieldset.d.ts +34 -0
  20. package/dist/components/vf-fieldset.js +76 -0
  21. package/dist/components/vf-grid.d.ts +124 -0
  22. package/dist/components/vf-grid.js +133 -0
  23. package/dist/components/vf-icon.d.ts +356 -0
  24. package/dist/components/vf-icon.js +507 -0
  25. package/dist/components/vf-img.d.ts +63 -0
  26. package/dist/components/vf-img.js +65 -0
  27. package/dist/components/vf-label.d.ts +117 -0
  28. package/dist/components/vf-label.js +151 -0
  29. package/dist/components/vf-list-item.d.ts +48 -0
  30. package/dist/components/vf-list-item.js +98 -0
  31. package/dist/components/vf-list.d.ts +77 -0
  32. package/dist/components/vf-list.js +256 -0
  33. package/dist/components/vf-menu-bar.d.ts +64 -0
  34. package/dist/components/vf-menu-bar.js +236 -0
  35. package/dist/components/vf-menu-item.d.ts +91 -0
  36. package/dist/components/vf-menu-item.js +252 -0
  37. package/dist/components/vf-menu.d.ts +95 -0
  38. package/dist/components/vf-menu.js +346 -0
  39. package/dist/components/vf-number-field.d.ts +60 -0
  40. package/dist/components/vf-number-field.js +248 -0
  41. package/dist/components/vf-option.d.ts +49 -0
  42. package/dist/components/vf-option.js +119 -0
  43. package/dist/components/vf-paragraph.d.ts +73 -0
  44. package/dist/components/vf-paragraph.js +66 -0
  45. package/dist/components/vf-progress-bar.d.ts +69 -0
  46. package/dist/components/vf-progress-bar.js +272 -0
  47. package/dist/components/vf-radio-group.d.ts +109 -0
  48. package/dist/components/vf-radio-group.js +105 -0
  49. package/dist/components/vf-radio.d.ts +59 -0
  50. package/dist/components/vf-radio.js +135 -0
  51. package/dist/components/vf-scroll-area.d.ts +75 -0
  52. package/dist/components/vf-scroll-area.js +124 -0
  53. package/dist/components/vf-select.d.ts +334 -0
  54. package/dist/components/vf-select.js +658 -0
  55. package/dist/components/vf-separator.d.ts +38 -0
  56. package/dist/components/vf-separator.js +55 -0
  57. package/dist/components/vf-slider.d.ts +80 -0
  58. package/dist/components/vf-slider.js +255 -0
  59. package/dist/components/vf-stack.d.ts +188 -0
  60. package/dist/components/vf-stack.js +168 -0
  61. package/dist/components/vf-swatch.d.ts +98 -0
  62. package/dist/components/vf-swatch.js +201 -0
  63. package/dist/components/vf-text-area.d.ts +77 -0
  64. package/dist/components/vf-text-area.js +121 -0
  65. package/dist/components/vf-text-field.d.ts +53 -0
  66. package/dist/components/vf-text-field.js +79 -0
  67. package/dist/components/vf-window.d.ts +157 -0
  68. package/dist/components/vf-window.js +377 -0
  69. package/dist/cursor-art.d.ts +37 -0
  70. package/dist/cursor-art.js +41 -0
  71. package/dist/cursor.d.ts +49 -0
  72. package/dist/cursor.js +171 -0
  73. package/dist/define.d.ts +45 -0
  74. package/dist/define.js +15 -0
  75. package/dist/document-listeners.d.ts +62 -0
  76. package/dist/document-listeners.js +29 -0
  77. package/dist/drag.d.ts +49 -0
  78. package/dist/drag.js +30 -0
  79. package/dist/events.d.ts +79 -0
  80. package/dist/events.js +25 -0
  81. package/dist/focus-modality.d.ts +94 -0
  82. package/dist/focus-modality.js +56 -0
  83. package/dist/form-control.d.ts +247 -0
  84. package/dist/form-control.js +127 -0
  85. package/dist/glyphs.d.ts +117 -0
  86. package/dist/glyphs.js +100 -0
  87. package/dist/grid-snap.d.ts +73 -0
  88. package/dist/grid-snap.js +135 -0
  89. package/dist/index.d.ts +145 -0
  90. package/dist/index.js +71 -0
  91. package/dist/menu-press.d.ts +60 -0
  92. package/dist/menu-press.js +97 -0
  93. package/dist/modal-dialog.d.ts +148 -0
  94. package/dist/modal-dialog.js +155 -0
  95. package/dist/motion.d.ts +72 -0
  96. package/dist/motion.js +22 -0
  97. package/dist/number.d.ts +24 -0
  98. package/dist/number.js +12 -0
  99. package/dist/open-art.d.ts +40 -0
  100. package/dist/open-art.js +35 -0
  101. package/dist/pixel-frame.d.ts +102 -0
  102. package/dist/pixel-frame.js +77 -0
  103. package/dist/popup-overflow.d.ts +157 -0
  104. package/dist/popup-overflow.js +45 -0
  105. package/dist/position.d.ts +187 -0
  106. package/dist/position.js +74 -0
  107. package/dist/scale.d.ts +259 -0
  108. package/dist/scale.js +133 -0
  109. package/dist/scroll-rail.d.ts +112 -0
  110. package/dist/scroll-rail.js +194 -0
  111. package/dist/scroll-state.d.ts +113 -0
  112. package/dist/scroll-state.js +55 -0
  113. package/dist/size.d.ts +47 -0
  114. package/dist/size.js +30 -0
  115. package/dist/styles/base.d.ts +38 -0
  116. package/dist/styles/body-font.d.ts +7 -0
  117. package/dist/styles/body-font.js +9 -0
  118. package/dist/styles/display-font.d.ts +7 -0
  119. package/dist/styles/display-font.js +9 -0
  120. package/dist/styles/recipes/body-face.d.ts +14 -0
  121. package/dist/styles/recipes/body-face.js +6 -0
  122. package/dist/styles/recipes/display-face.d.ts +23 -0
  123. package/dist/styles/recipes/display-face.js +10 -0
  124. package/dist/styles/recipes/field.d.ts +20 -0
  125. package/dist/styles/recipes/field.js +68 -0
  126. package/dist/styles/recipes/focus.d.ts +76 -0
  127. package/dist/styles/recipes/focus.js +39 -0
  128. package/dist/styles/recipes/host.d.ts +5 -0
  129. package/dist/styles/recipes/host.js +62 -0
  130. package/dist/styles/recipes/pattern.d.ts +80 -0
  131. package/dist/styles/recipes/pattern.js +82 -0
  132. package/dist/styles/recipes/scroll-rail.d.ts +69 -0
  133. package/dist/styles/recipes/scroll-rail.js +222 -0
  134. package/dist/styles/recipes/shadow.d.ts +13 -0
  135. package/dist/styles/recipes/shadow.js +5 -0
  136. package/dist/styles/recipes/static-text.d.ts +24 -0
  137. package/dist/styles/recipes/static-text.js +17 -0
  138. package/dist/styles/recipes/surface.d.ts +28 -0
  139. package/dist/styles/recipes/surface.js +27 -0
  140. package/dist/styles/recipes/tile.d.ts +122 -0
  141. package/dist/styles/recipes/tile.js +40 -0
  142. package/dist/styles/recipes/title-bar.d.ts +49 -0
  143. package/dist/styles/recipes/title-bar.js +134 -0
  144. package/dist/styles/recipes/toggle.d.ts +10 -0
  145. package/dist/styles/recipes/toggle.js +20 -0
  146. package/dist/styles/register-embedded-font.d.ts +64 -0
  147. package/dist/styles/register-embedded-font.js +30 -0
  148. package/dist/text-control.d.ts +131 -0
  149. package/dist/text-control.js +83 -0
  150. package/dist/tile-grid.d.ts +111 -0
  151. package/dist/tile-grid.js +53 -0
  152. package/dist/toggle-control.d.ts +60 -0
  153. package/dist/toggle-control.js +41 -0
  154. package/dist/track-width.d.ts +42 -0
  155. package/dist/track-width.js +30 -0
  156. package/dist/type-ahead.d.ts +35 -0
  157. package/dist/type-ahead.js +20 -0
  158. package/dist/zoom.d.ts +204 -0
  159. package/dist/zoom.js +105 -0
  160. package/docs/SPEC.md +2471 -0
  161. package/editor/vscode.html-custom-data.json +1320 -0
  162. package/editor/web-types.json +3237 -0
  163. package/package.json +118 -0
@@ -0,0 +1,134 @@
1
+ import { vfDisplayDecls as e } from "./display-face.js";
2
+ import { css as t } from "lit";
3
+ //#region src/styles/recipes/title-bar.ts
4
+ var n = t`
5
+ .vf-title-bar {
6
+ position: relative;
7
+ /* Inert unless the frame is a flex column (vf-window); on vf-dialog's plain
8
+ block frame the shorthand simply doesn't apply. */
9
+ flex: none;
10
+ 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
+ display: flex;
13
+ /* flex-start, not center: the title states its own whole-pixel row (see
14
+ the margin below); centering 16 into 17 would land it on a half. */
15
+ align-items: flex-start;
16
+ justify-content: center;
17
+ overflow: hidden;
18
+ }
19
+ .vf-title {
20
+ /* Chicago-style title (chrome); slotted body copy keeps the body face. */
21
+ ${e}
22
+ position: relative;
23
+ z-index: 1;
24
+ left: var(--vf-title-dx, 0px);
25
+ /* The face's own 16px line box on interior rows 1..16: cap band lands on
26
+ rows 4..12 (4px white above and below the 9px caps — the trace). The
27
+ inherited ratio line-height would give 20px and center fractionally. */
28
+ line-height: calc(var(--vf-scale, 1) * var(--vf-line-height-display, 16px));
29
+ margin-top: calc(var(--vf-scale, 1) * 1px);
30
+ /* 6px + the letters' own 1px side bearing = the traced 7px of white
31
+ between ink and stripes. */
32
+ padding: 0 calc(var(--vf-scale, 1) * 6px);
33
+ max-width: calc(100% - var(--vf-scale, 1) * var(--vf-title-inset, 16px));
34
+ background: var(--vf-white, #fff);
35
+ white-space: nowrap;
36
+ overflow: hidden;
37
+ text-overflow: ellipsis;
38
+ }
39
+ `, r = t`
40
+ .box {
41
+ position: absolute;
42
+ /* 11×11 box with 3px of clear white above and below it (title-bar
43
+ interior is 17px: 3 + 11 + 3). See SPEC §5 vf-window. */
44
+ top: calc(var(--vf-scale, 1) * 3px);
45
+ z-index: 1;
46
+ width: calc(var(--vf-scale, 1) * 11px);
47
+ height: calc(var(--vf-scale, 1) * 11px);
48
+ padding: 0;
49
+ margin: 0;
50
+ border: calc(var(--vf-scale, 1) * 1px) solid var(--vf-black, #000000);
51
+ 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
+ font: inherit;
60
+ cursor: var(--vf-cursor, default);
61
+ -webkit-appearance: none;
62
+ appearance: none;
63
+ }
64
+ .close {
65
+ left: calc(var(--vf-scale, 1) * 8px);
66
+ }
67
+ .zoom {
68
+ right: calc(var(--vf-scale, 1) * 8px);
69
+ }
70
+ /* Pressed box (close AND zoom): the interior fills with the classic
71
+ radiating "go-away" sunburst — black 1-bit spokes on the white face
72
+ (4 orthogonal 3px spokes + 4 diagonal 2px ones around an empty center),
73
+ traced pixel-for-pixel from the UI kit's close-button-active-state
74
+ sprite. Both widgets flash the identical graphic while pressed. That
75
+ 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. */
77
+ .box:active {
78
+ background-color: var(--vf-white, #ffffff);
79
+ 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");
80
+ background-repeat: no-repeat;
81
+ background-position: center;
82
+ background-size: calc(var(--vf-scale, 1) * 9px) calc(var(--vf-scale, 1) * 9px);
83
+ }
84
+ .zoom::after {
85
+ content: '';
86
+ position: absolute;
87
+ /* 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
91
+ sprite col 6 and the horizontal at row 6. Traced from the UI kit
92
+ zoom-button rest sprite. */
93
+ top: 0;
94
+ left: 0;
95
+ width: calc(var(--vf-scale, 1) * 6px);
96
+ 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);
99
+ }
100
+ /* While pressed the zoom box shows the same sunburst as the close box, so
101
+ its inner detail square gives way to it. */
102
+ .zoom:active::after {
103
+ display: none;
104
+ }
105
+ /* Forced colors: everything the widget draws is already token-routed —
106
+ Canvas face, CanvasText border — but its white patch ring is a
107
+ box-shadow, which forced colors never paints, leaving the box pressed
108
+ against the (rescued — see vfStripes) stripes. forced-color-adjust: none
109
+ turns the widget's own paint back on; with the tokens remapped it still
110
+ renders entirely in the user's pair. The one literal left is the pressed
111
+ sunburst tile (url() images are preserved with their black ink), so that
112
+ repaints as a mask over the ink token — into the padding box, where the
113
+ sprite's 9×9 interior already draws. */
114
+ @media (forced-colors: active) {
115
+ .box {
116
+ forced-color-adjust: none;
117
+ }
118
+ .box:active {
119
+ background-image: none;
120
+ }
121
+ .box:active::before {
122
+ content: '';
123
+ position: absolute;
124
+ inset: 0;
125
+ background: var(--vf-black, #000000);
126
+ mask-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");
127
+ mask-repeat: no-repeat;
128
+ mask-position: center;
129
+ mask-size: calc(var(--vf-scale, 1) * 9px) calc(var(--vf-scale, 1) * 9px);
130
+ }
131
+ }
132
+ `;
133
+ //#endregion
134
+ export { n as vfTitleBar, r as vfWindowWidgets };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Shared layout for the two toggle controls (vf-checkbox, vf-radio): the
3
+ * box-and-label row, the host focus suppressed (each control draws
4
+ * {@link vfFocusUnderline} under its own well instead), and the classic "dim
5
+ * the label, not the control" disabled treatment. The Chicago display face
6
+ * comes from {@link vfDisplay} (both controls compose it), so no font is set
7
+ * here. Each component adds only its own well (`.box`/`.circle`), glyphs,
8
+ * press feedback and focus rule.
9
+ */
10
+ export declare const vfToggle: import("lit").CSSResult;
@@ -0,0 +1,20 @@
1
+ import { css as e } from "lit";
2
+ //#region src/styles/recipes/toggle.ts
3
+ var t = e`
4
+ :host {
5
+ display: inline-flex;
6
+ align-items: center;
7
+ gap: calc(var(--vf-scale, 1) * 6px);
8
+ cursor: var(--vf-cursor, default);
9
+ }
10
+ /* The focus rule is drawn under the box/circle, not around the host. */
11
+ :host(:focus-visible) {
12
+ outline: none;
13
+ }
14
+ /* Disabled dims the label only; the box/circle glyphs stay solid black. */
15
+ .label.dim {
16
+ color: var(--vf-disabled, #c0c0c0);
17
+ }
18
+ `;
19
+ //#endregion
20
+ export { t as vfToggle };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Register an embedded bitmap webfont on the document.
3
+ *
4
+ * WHY THIS IS JS AND NOT AN `@font-face` RULE: font faces declared inside a
5
+ * component's shadow root are ignored by the browser — `@font-face` resolves
6
+ * only against the document. Every `vf-*` element renders into its own shadow
7
+ * root, so a stylesheet `@font-face` would never take effect. Registering the
8
+ * face imperatively on `document.fonts` makes it visible inside *every* shadow
9
+ * root at once, which preserves the library's "components need no global CSS"
10
+ * promise.
11
+ *
12
+ * Each face is inlined as a base64 WOFF2 data URL by its wrapper module (e.g.
13
+ * `display-font.ts`), so there is no separate asset to ship or to resolve
14
+ * through a consumer's bundler; the font is available the moment the wrapper is
15
+ * imported, with no network round trip.
16
+ */
17
+ /**
18
+ * Vertical-metric overrides for a registered face, applied via the CSS
19
+ * `ascent-override`/`descent-override`/`line-gap-override` descriptors.
20
+ * Percentages of the em (`'75%'`), same as the CSS properties.
21
+ */
22
+ export interface EmbeddedFontMetrics {
23
+ ascentOverride?: string;
24
+ descentOverride?: string;
25
+ lineGapOverride?: string;
26
+ }
27
+ /**
28
+ * The em box both embedded bitmap faces are drawn on: a 16-design-pixel grid
29
+ * split 12 px above the baseline and 4 px below (the classic Chicago-12
30
+ * ascent/descent), no line gap.
31
+ *
32
+ * WHY OVERRIDE AT ALL: browsers place a line box's baseline from the face's
33
+ * hhea metrics, and the retired converter-artifact faces (ChiKareGo and
34
+ * FindersKeepers, both since replaced by the kit's re-drawn strikes) carried hhea
35
+ * values — ascender 682, descender 192, line gap 92 of the 1024 upm, i.e.
36
+ * 10.66 px / 3 px / 1.44 px at 16px — that are NOT on the 64-unit
37
+ * design-pixel grid the glyphs are drawn on. Half-leading computed from them
38
+ * lands the baseline ~0.5px high in every whole-pixel line box, and
39
+ * rasterization snaps that to a whole device pixel: all chrome/body text sat
40
+ * one device px above its System 7 position (first seen as fractional gaps in
41
+ * the vf-select pill). Today both shipped strikes' own tables carry the
42
+ * grid-clean em (768/256/0 = 12/4/0 px — fonts/manifest-to-font.py writes
43
+ * them); the overrides restate it so every face the kit registers is pinned
44
+ * the same way regardless of provenance or which table a platform prefers.
45
+ *
46
+ * KNOWN LIMIT (dpr 2, scale 1.5): Chrome snaps aliased text baselines to whole
47
+ * ABSOLUTE CSS px. A pill's ideal baseline sits 13 system px (19.5 CSS px at
48
+ * scale 1.5) below its border-box top, so a host at a whole CSS-px position
49
+ * puts it on a half pixel and the snap misses by 1 device px (⅓ system px) —
50
+ * no metric value can reach it; only a host at a half-CSS-px position (still
51
+ * on the device grid) renders it exactly. dpr 1 and 3 land exactly at whole
52
+ * CSS-px positions, the common case.
53
+ */
54
+ export declare const PIXEL_GRID_METRICS: EmbeddedFontMetrics;
55
+ /**
56
+ * Register `family` from an inlined base64 WOFF2 exactly once per session.
57
+ *
58
+ * Idempotent and environment-safe: it no-ops under SSR (no `document`), when
59
+ * the CSS Font Loading API is unavailable, or when the family is already
60
+ * present (duplicate bundles / HMR). A failed decode releases the family so a
61
+ * later explicit call can retry, and on any failure components simply keep the
62
+ * fallback stack — so this can never break rendering.
63
+ */
64
+ export declare function registerEmbeddedFont(family: string, woff2Base64: string, metrics?: EmbeddedFontMetrics): void;
@@ -0,0 +1,30 @@
1
+ //#region src/styles/register-embedded-font.ts
2
+ var e = /* @__PURE__ */ new Set(), t = {
3
+ ascentOverride: "75%",
4
+ descentOverride: "25%",
5
+ lineGapOverride: "0%"
6
+ };
7
+ function n(t, n, r) {
8
+ if (e.has(t) || typeof document > "u" || !("fonts" in document)) return;
9
+ for (let n of document.fonts) if (n.family === t) {
10
+ e.add(t);
11
+ return;
12
+ }
13
+ e.add(t);
14
+ let i = `url(data:font/woff2;base64,${n}) format('woff2')`;
15
+ try {
16
+ let n = new FontFace(t, i, {
17
+ style: "normal",
18
+ weight: "100 900",
19
+ display: "swap",
20
+ ...r
21
+ });
22
+ document.fonts.add(n), n.load().then(() => {}, () => {
23
+ document.fonts.delete(n), e.delete(t);
24
+ });
25
+ } catch {
26
+ e.delete(t);
27
+ }
28
+ }
29
+ //#endregion
30
+ export { t as PIXEL_GRID_METRICS, n as registerEmbeddedFont };
@@ -0,0 +1,131 @@
1
+ import { nothing } from 'lit';
2
+ import type { PropertyValues } from 'lit';
3
+ import { ScaleController } from './scale.js';
4
+ import { GridSnapController } from './grid-snap.js';
5
+ import { VfShadowRoleControl } from './form-control.js';
6
+ import { FocusRuleController } from './focus-modality.js';
7
+ /**
8
+ * Shared base for the kit's editable text fields — `vf-text-field`,
9
+ * `vf-text-area` and `vf-number-field`. Builds on
10
+ * {@link VfShadowRoleControl} (form association, the disabled-value guard and
11
+ * the name/description bridge every field needs, its role sitting on the inner
12
+ * native control) and adds the value/label scaffolding
13
+ * every field repeated: the reflected `name`, the `value`/`placeholder`/
14
+ * `readonly`/`label` props, the captured default restored on form reset, the
15
+ * `updated()` → {@link VfShadowRoleControl.syncFormValue} funnel, and the display-
16
+ * scaling controller. Pairs with the `vfField` css fragment (the white-well
17
+ * skin) in styles/base.ts.
18
+ *
19
+ * Subclasses render their own native control (`<input>`/`<textarea>` plus any
20
+ * adornments), tag it `class="vf-field"`, and may enrich the emitted event
21
+ * detail via {@link emitValue}.
22
+ */
23
+ export declare class VfTextControlBase extends VfShadowRoleControl {
24
+ /** Focus delegates into the inner native control. */
25
+ static shadowRootOptions: ShadowRootInit;
26
+ /** Current value. Synced on every keystroke and submitted with forms. */
27
+ value: string;
28
+ /** Placeholder text shown when the field is empty. */
29
+ placeholder: string;
30
+ /** Makes the field read-only (focusable, not editable). */
31
+ readonly: boolean;
32
+ /** Form field name used when submitting the associated form. */
33
+ name: string;
34
+ /**
35
+ * Accessible name, applied as `aria-label` on the inner native control (the
36
+ * element that receives focus and is announced by screen readers — a host
37
+ * attribute cannot reach it on its own). Left empty, the name falls back to
38
+ * whatever the host carries — `aria-labelledby`, `aria-label` or an
39
+ * associated `<label for>` — via {@link VfShadowRoleControl.hostLabel}, so the
40
+ * platform's own labeling vocabulary works on these controls too.
41
+ */
42
+ label: string;
43
+ /** A field is empty for `required`'s purposes when its value is `''`. */
44
+ protected get valueMissing(): boolean;
45
+ /**
46
+ * Host attributes forwarded verbatim onto the inner native control — the
47
+ * input-behavior vocabulary the platform only honors on the element that
48
+ * actually takes the input. `<vf-text-field autocomplete="email">` used to
49
+ * put the token on a custom element the browser ignores for autofill while
50
+ * the shadow `<input>` never received it; same story for the mobile-keyboard
51
+ * pair (`inputmode`, `enterkeyhint`) and the rest. These are deliberately
52
+ * *not* reactive properties: four of them are global attributes with IDL
53
+ * accessors already on `HTMLElement` (`spellcheck`, `autocapitalize`,
54
+ * `inputMode`, `enterKeyHint`), and a Lit `@property` would shadow the
55
+ * platform member — the kit's own `align`/`draggable` trap. Instead the
56
+ * attributes are observed (see `observedAttributes`) and read at render
57
+ * time via {@link forwardedAttr}. Subclasses may extend the list
58
+ * (`vf-text-field` adds `pattern`) or override a default in their template
59
+ * (`vf-number-field` keeps `inputmode="decimal"` unless told otherwise).
60
+ */
61
+ protected static readonly forwardedAttributes: readonly string[];
62
+ static get observedAttributes(): string[];
63
+ attributeChangedCallback(name: string, old: string | null, value: string | null): void;
64
+ /**
65
+ * The forwarded value of a host attribute for the inner control's template
66
+ * binding, or `nothing` (remove the attribute) while the host doesn't
67
+ * carry it.
68
+ */
69
+ protected forwardedAttr(name: string): string | typeof nothing;
70
+ /** Default-on display scaling (true 72dpi size); see src/scale.ts. */
71
+ protected readonly scale: ScaleController;
72
+ /** Device-pixel grid snapping (opt in with applyGridSnap()); see src/grid-snap.ts. */
73
+ protected readonly gridSnap: GridSnapController;
74
+ /**
75
+ * Whether the well wears the kit's dashed focus rule — keyboard focus only.
76
+ * See {@link FocusRuleController} for why a field cannot get this from
77
+ * `:focus-visible` the way most of the kit does. Typing does NOT reveal the
78
+ * rule after a click: the click already showed where the insertion point
79
+ * went, which is the whole reason a field marks keyboard focus and not every
80
+ * focus, so the field never calls the controller's `reveal()`.
81
+ */
82
+ protected readonly focusRule: FocusRuleController;
83
+ /**
84
+ * Classes for the `.vf-field-well` wrapper every field renders around its
85
+ * native control: the shared skin hook (`vfField`), the grid-snap target, and
86
+ * the focus rule's own gate. Assembled here so the three fields state the
87
+ * wrapper once and can't drift on what turns the rule on.
88
+ */
89
+ protected get wellClass(): string;
90
+ connectedCallback(): void;
91
+ /**
92
+ * Re-submit the value only when it (or the disabled state gating it) actually
93
+ * changed. Unrelated re-renders would otherwise re-run this — `vf-number-field`
94
+ * re-renders on every stepper press and release for its `pressed` state alone.
95
+ * On the first update `value` is in `changed` (class-field defaults are, with
96
+ * `undefined` as the old value), so the initial form value is still set.
97
+ */
98
+ protected updated(changed: PropertyValues<this>): void;
99
+ /** Restores the initial value when the associated form resets. */
100
+ formResetCallback(): void;
101
+ /**
102
+ * True for a plain Enter press (no modifiers, not IME-composing) — the key
103
+ * that triggers a form's implicit submission. The native control is
104
+ * shadow-encapsulated (null form owner), so single-line fields call
105
+ * `internals.form?.requestSubmit()` themselves; this centralises the guard.
106
+ */
107
+ protected isSubmitEnter(event: KeyboardEvent): boolean;
108
+ /**
109
+ * Emulates the form's implicit submission for a plain Enter press. HTML
110
+ * defines it as *activating the form's default button* — the first submit
111
+ * button in tree order — so the submission carries that button's submitter
112
+ * identity and name/value. A bare `requestSubmit()` carries neither
113
+ * (`event.submitter === null`), which made Enter and a click on the same
114
+ * form produce different payloads. `click()` is the activation: a native
115
+ * button becomes the real submitter, and a `vf-button` forwards it to its
116
+ * inner button, which runs the same transient-proxy path a pointer does. A
117
+ * disabled default button means no submission at all, as in HTML; a form
118
+ * with no submit button falls back to the bare call, also as in HTML.
119
+ */
120
+ protected requestImplicitSubmit(): void;
121
+ /**
122
+ * Dispatch a bubbling, composed value event (SPEC §4). Text fields use the
123
+ * default `{ value }` detail; `vf-number-field` passes an enriched detail that
124
+ * also carries `valueAsNumber`.
125
+ */
126
+ protected emitValue(type: 'vf-input' | 'vf-change', detail?: Record<string, unknown>): void;
127
+ /** Mirror the native control's value into `value` and announce a keystroke. */
128
+ protected handleInput(event: Event): void;
129
+ /** Mirror the native control's value into `value` and announce a commit. */
130
+ protected handleChange(event: Event): void;
131
+ }
@@ -0,0 +1,83 @@
1
+ import { ScaleController as e } from "./scale.js";
2
+ import t from "./_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js";
3
+ import { GridSnapController as n } from "./grid-snap.js";
4
+ import { emit as r, emitNative as i } from "./events.js";
5
+ import { VfShadowRoleControl as a } from "./form-control.js";
6
+ import { FocusRuleController as o } from "./focus-modality.js";
7
+ import { nothing as s } from "lit";
8
+ import { property as c } from "lit/decorators.js";
9
+ //#region src/text-control.ts
10
+ var l = class extends a {
11
+ constructor(...t) {
12
+ super(...t), this.value = "", this.placeholder = "", this.readonly = !1, this.name = "", this.label = "", this.scale = new e(this), this.gridSnap = new n(this), this.focusRule = new o(this);
13
+ }
14
+ static {
15
+ this.shadowRootOptions = {
16
+ ...a.shadowRootOptions,
17
+ delegatesFocus: !0
18
+ };
19
+ }
20
+ get valueMissing() {
21
+ return this.value === "";
22
+ }
23
+ static {
24
+ this.forwardedAttributes = [
25
+ "autocomplete",
26
+ "inputmode",
27
+ "enterkeyhint",
28
+ "maxlength",
29
+ "spellcheck",
30
+ "autocapitalize"
31
+ ];
32
+ }
33
+ static get observedAttributes() {
34
+ return [...super.observedAttributes, ...this.forwardedAttributes];
35
+ }
36
+ attributeChangedCallback(e, t, n) {
37
+ super.attributeChangedCallback(e, t, n), this.constructor.forwardedAttributes.includes(e) && this.requestUpdate();
38
+ }
39
+ forwardedAttr(e) {
40
+ return this.getAttribute(e) ?? s;
41
+ }
42
+ get wellClass() {
43
+ return `vf-field-well vf-snap${this.focusRule.marked ? " vf-focus-rule" : ""}`;
44
+ }
45
+ connectedCallback() {
46
+ super.connectedCallback(), this.latchFormDefault(this.value);
47
+ }
48
+ updated(e) {
49
+ (e.has("value") || this.disabledChanged(e)) && this.syncFormValue(this.value);
50
+ }
51
+ formResetCallback() {
52
+ this.value = this.formDefault("");
53
+ }
54
+ isSubmitEnter(e) {
55
+ return e.key === "Enter" && !e.isComposing && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey;
56
+ }
57
+ requestImplicitSubmit() {
58
+ let e = this.internals.form;
59
+ if (!e) return;
60
+ let t = [...e.elements].find((e) => e instanceof HTMLButtonElement && e.type === "submit" || e instanceof HTMLInputElement && (e.type === "submit" || e.type === "image") || e.localName === "vf-button" && e.type?.toLowerCase() === "submit");
61
+ if (t) {
62
+ if (t.matches(":disabled")) return;
63
+ t.click();
64
+ return;
65
+ }
66
+ e.requestSubmit();
67
+ }
68
+ emitValue(e, t = { value: this.value }) {
69
+ r(this, e, t);
70
+ }
71
+ handleInput(e) {
72
+ this.value = e.target.value, this.emitValue("vf-input");
73
+ }
74
+ handleChange(e) {
75
+ this.value = e.target.value, this.emitValue("vf-change"), i(this, "change");
76
+ }
77
+ };
78
+ t([c()], l.prototype, "value", void 0), t([c()], l.prototype, "placeholder", void 0), t([c({
79
+ type: Boolean,
80
+ reflect: !0
81
+ })], l.prototype, "readonly", void 0), t([c({ reflect: !0 })], l.prototype, "name", void 0), t([c()], l.prototype, "label", void 0);
82
+ //#endregion
83
+ export { l as VfTextControlBase };
@@ -0,0 +1,111 @@
1
+ import { type CSSResult, type TemplateResult } from 'lit';
2
+ import { type TileRect } from './styles/recipes/tile.js';
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).
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.
18
+ *
19
+ * Two rendering paths, chosen by whether the surface's pattern token is
20
+ * overridden ({@link patternOverride}):
21
+ *
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
43
+ * `left: calc(var(--vf-scale, 1) * kT px)`: one single-multiplication length
44
+ * per tile, quantized once, so each box paint-snaps onto the device grid
45
+ * 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
49
+ * consumer art additionally magnifies nearest-neighbor (the `vf-img` idiom).
50
+ *
51
+ * The trap that must not come back: NEVER lay tiles out with CSS grid,
52
+ * flexbox, inline-block flow, or nested row containers. Track and flow
53
+ * layouts accumulate quantized box sizes — the position of column `k` becomes
54
+ * a *sum* of `k` quantized lengths, error `k/64` CSS px, and the disease
55
+ * returns. Flat absolute placement with one multiplication per tile is the
56
+ * correctness, not a style choice.
57
+ *
58
+ * Wiring: the *container* — the layer the fill used to paint on, or a
59
+ * dedicated child filling it — takes the `vf-tile-grid` class (clip and
60
+ * 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`).
68
+ *
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.
74
+ */
75
+ export declare const vfTileGrid: CSSResult;
76
+ /**
77
+ * The flat set of tiles covering `cols × rows` boxes of `tile` system px each,
78
+ * to render inside a `vf-tile-grid` container — the consumer-pattern path.
79
+ * Each tile carries its own single-multiplication `calc()` position and size,
80
+ * live against `--vf-scale` — so a density or zoom change re-renders nothing,
81
+ * and the count changes only when the caller's *declared system-px size*
82
+ * does. Compute `cols`/`rows` from that declared size with `Math.ceil`; the
83
+ * clipped last row and column are absorbed by the container's clip.
84
+ */
85
+ export declare function tileGrid({ cols, rows, tile, }: {
86
+ cols: number;
87
+ rows: number;
88
+ tile: number;
89
+ }): TemplateResult[];
90
+ /**
91
+ * 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).
95
+ *
96
+ * Read with `getComputedStyle` at update time, not observed live: a pattern
97
+ * token swapped at runtime without touching the component wants a nudge (any
98
+ * property write, or `requestUpdate()`) — the same re-check-on-update
99
+ * contract the component's other measured inputs follow.
100
+ */
101
+ export declare function patternOverride(el: Element, token: string): string;
102
+ /**
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.
107
+ */
108
+ export declare class TileRasterCache {
109
+ #private;
110
+ for(motifWidth: number, motifHeight: number, rects: readonly TileRect[], width: number, height: number): string;
111
+ }
@@ -0,0 +1,53 @@
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";
4
+ //#region src/tile-grid.ts
5
+ var i = n`
6
+ .vf-tile-grid {
7
+ /* 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
+ overflow: hidden;
10
+ pointer-events: none;
11
+ }
12
+ .vf-tile,
13
+ .vf-tile-raster {
14
+ position: absolute;
15
+ /* The box IS the size — never the image's intrinsic size, and never an
16
+ intra-box repeat: a consumer tile smaller than its box repeating
17
+ inside it would re-import the accumulation this module removes. */
18
+ background-size: 100% 100%;
19
+ 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). */
23
+ image-rendering: pixelated;
24
+ }
25
+ .vf-tile {
26
+ background-image: var(--_vf-tile-image);
27
+ }
28
+ .vf-tile-raster {
29
+ top: 0;
30
+ left: 0;
31
+ }
32
+ `;
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
36
+ class="vf-tile"
37
+ style="left:${e(n * i)};top:${e(s * i)};width:${a};height:${a}"
38
+ ></span>`);
39
+ return o;
40
+ }
41
+ function o(e, t) {
42
+ return typeof getComputedStyle > "u" ? "" : getComputedStyle(e).getPropertyValue(t).trim();
43
+ }
44
+ var s = class {
45
+ #e = "";
46
+ #t = "";
47
+ for(e, n, r, i, a) {
48
+ let o = `${i}x${a}`;
49
+ return this.#e !== o && (this.#e = o, this.#t = t(e, n, r, i, a)), this.#t;
50
+ }
51
+ };
52
+ //#endregion
53
+ export { s as TileRasterCache, o as patternOverride, a as tileGrid, i as vfTileGrid };