uivisor 0.1.7 → 0.1.9

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 (2) hide show
  1. package/dist/overlay/index.js +167 -41
  2. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  // src/overlay/breakpoint.ts
2
2
  var TAILWIND = {
3
3
  name: "tailwind",
4
+ dir: "min",
4
5
  breakpoints: [
5
6
  { name: "sm", minWidth: 640 },
6
7
  { name: "md", minWidth: 768 },
@@ -9,13 +10,44 @@ var TAILWIND = {
9
10
  { name: "2xl", minWidth: 1536 }
10
11
  ]
11
12
  };
13
+ function appliesAt(dir, threshold, width) {
14
+ return dir === "min" ? width >= threshold : width <= threshold;
15
+ }
16
+ function priority(dir, threshold) {
17
+ return dir === "min" ? threshold : -threshold;
18
+ }
19
+ function baseThreshold(dir) {
20
+ return dir === "min" ? 0 : Infinity;
21
+ }
12
22
  function activeBreakpoint(width, system = TAILWIND) {
13
- let active = { name: "base", minWidth: 0 };
23
+ let active = { name: "base", minWidth: baseThreshold(system.dir) };
24
+ let best = priority(system.dir, active.minWidth);
14
25
  for (const bp of system.breakpoints) {
15
- if (width >= bp.minWidth) active = { name: bp.name, minWidth: bp.minWidth };
26
+ if (!appliesAt(system.dir, bp.minWidth, width)) continue;
27
+ const p = priority(system.dir, bp.minWidth);
28
+ if (p > best) {
29
+ best = p;
30
+ active = { name: bp.name, minWidth: bp.minWidth };
31
+ }
16
32
  }
17
33
  return active;
18
34
  }
35
+ function effectiveBreakpoint(editedNames, width, system = TAILWIND) {
36
+ const stops = { base: baseThreshold(system.dir) };
37
+ for (const bp of system.breakpoints) stops[bp.name] = bp.minWidth;
38
+ let winner = null;
39
+ let best = -Infinity;
40
+ for (const name of editedNames) {
41
+ const threshold = stops[name];
42
+ if (threshold == null || !appliesAt(system.dir, threshold, width)) continue;
43
+ const p = priority(system.dir, threshold);
44
+ if (winner === null || p > best) {
45
+ best = p;
46
+ winner = name;
47
+ }
48
+ }
49
+ return winner;
50
+ }
19
51
  function currentBreakpoint(system = TAILWIND) {
20
52
  const width = typeof window !== "undefined" ? window.innerWidth : 0;
21
53
  return activeBreakpoint(width, system);
@@ -32,18 +64,23 @@ function nameForWidth(px2) {
32
64
  }
33
65
  function detectBreakpoints() {
34
66
  if (typeof document === "undefined") return TAILWIND;
35
- const widths = /* @__PURE__ */ new Set();
67
+ const mins = /* @__PURE__ */ new Set();
68
+ const maxes = /* @__PURE__ */ new Set();
69
+ const grab = (text, re, into) => {
70
+ const m = re.exec(text);
71
+ if (m) {
72
+ const val = parseFloat(m[1]);
73
+ const px2 = m[2].toLowerCase() === "px" ? val : val * 16;
74
+ if (px2 >= 240 && px2 <= 4096) into.add(Math.round(px2));
75
+ }
76
+ };
36
77
  const visit = (rules) => {
37
78
  if (!rules) return;
38
79
  for (let i = 0; i < rules.length; i++) {
39
80
  const rule = rules[i];
40
81
  if (rule.type === 4 && rule.media) {
41
- const m = /min-width:\s*(\d*\.?\d+)(px|rem|em)/i.exec(rule.media.mediaText);
42
- if (m) {
43
- const val = parseFloat(m[1]);
44
- const px2 = m[2].toLowerCase() === "px" ? val : val * 16;
45
- if (px2 >= 240 && px2 <= 4096) widths.add(Math.round(px2));
46
- }
82
+ grab(rule.media.mediaText, /min-width:\s*(\d*\.?\d+)(px|rem|em)/i, mins);
83
+ grab(rule.media.mediaText, /max-width:\s*(\d*\.?\d+)(px|rem|em)/i, maxes);
47
84
  visit(rule.cssRules);
48
85
  } else if (rule.cssRules) {
49
86
  visit(rule.cssRules);
@@ -56,10 +93,13 @@ function detectBreakpoints() {
56
93
  } catch {
57
94
  }
58
95
  }
96
+ const useMax = maxes.size > mins.size;
97
+ const widths = useMax ? maxes : mins;
59
98
  const sorted = [...widths].sort((a, b) => a - b);
60
99
  if (!sorted.length) return TAILWIND;
61
100
  return {
62
101
  name: "detected",
102
+ dir: useMax ? "max" : "min",
63
103
  breakpoints: sorted.map((w) => ({ name: nameForWidth(w), minWidth: w }))
64
104
  };
65
105
  }
@@ -366,7 +406,12 @@ var ICONS = {
366
406
  layout: sv('<rect x="2" y="2" width="12" height="12" rx="1.5"/><path d="M6 2.5 V13.5 M6 6 H13.5"/>'),
367
407
  width: sv('<path d="M1.5 8 H14.5 M4 5 L1.5 8 L4 11 M12 5 L14.5 8 L12 11"/>'),
368
408
  height: sv('<path d="M8 1.5 V14.5 M5 4 L8 1.5 L11 4 M5 12 L8 14.5 L11 12"/>'),
369
- chevron: sv('<path d="M6 4 L10 8 L6 12"/>')
409
+ chevron: sv('<path d="M6 4 L10 8 L6 12"/>'),
410
+ phone: sv('<rect x="5" y="1.8" width="6" height="12.4" rx="1.6"/><path d="M7 12.4h2"/>'),
411
+ tablet: sv('<rect x="3.3" y="2.2" width="9.4" height="11.6" rx="1.6"/><path d="M7 11.7h2"/>'),
412
+ desktop: sv('<rect x="1.8" y="2.8" width="12.4" height="8" rx="1"/><path d="M5.8 14h4.4 M8 10.8v3.2"/>'),
413
+ live: sv('<circle cx="8" cy="8" r="2"/><path d="M4.6 4.6a4.8 4.8 0 0 0 0 6.8 M11.4 4.6a4.8 4.8 0 0 1 0 6.8"/>'),
414
+ all: sv('<path d="M8 2.5v11 M3.2 5.2l9.6 5.6 M12.8 5.2l-9.6 5.6"/>')
370
415
  };
371
416
  var SECTIONS = [
372
417
  {
@@ -1082,12 +1127,14 @@ var CSS = (
1082
1127
  }
1083
1128
  .uiv-framewrap.show { display: flex; }
1084
1129
  .uiv-framebar {
1085
- display: flex; align-items: center; justify-content: center; gap: 14px;
1086
- height: 38px; color: #e4e4e7; font-size: 12px; flex: 0 0 auto;
1130
+ display: flex; align-items: center; gap: 12px;
1131
+ height: 46px; padding: 0 14px; color: #e4e4e7; font-size: 12px; flex: 0 0 auto;
1087
1132
  border-bottom: 1px solid #27272a;
1088
1133
  }
1089
- .uiv-framew { font-family: ui-monospace, monospace; color: #c7d2fe; font-weight: 600; }
1090
- .uiv-framex { cursor: pointer; color: #a1a1aa; }
1134
+ .uiv-framechips { display: flex; gap: 6px; flex: 1; justify-content: center; flex-wrap: wrap; }
1135
+ .uiv-framew { font-family: ui-monospace, monospace; color: #c7d2fe; font-weight: 600;
1136
+ white-space: nowrap; flex: 0 0 auto; }
1137
+ .uiv-framex { cursor: pointer; color: #a1a1aa; font-size: 14px; flex: 0 0 auto; }
1091
1138
  .uiv-framex:hover { color: #fff; }
1092
1139
  .uiv-framestage {
1093
1140
  flex: 1; display: flex; align-items: stretch; justify-content: center;
@@ -1151,7 +1198,10 @@ var CSS = (
1151
1198
  border-radius: 6px; padding: 3px 8px; font-size: 11px; font-weight: 600;
1152
1199
  font-family: ui-monospace, monospace;
1153
1200
  }
1201
+ .uiv-chip { display: inline-flex; align-items: center; gap: 4px; }
1202
+ .uiv-chip svg { width: 13px; height: 13px; opacity: .85; }
1154
1203
  .uiv-chip:hover, .uiv-clschip:hover { color: #fff; background: #3f3f46; }
1204
+ .uiv-chip.on svg { opacity: 1; }
1155
1205
  .uiv-chip.win { border-color: #52525b; color: #d4d4d8; }
1156
1206
  .uiv-chip.on, .uiv-clschip.on { background: #4f46e5; border-color: #6366f1; color: #fff; }
1157
1207
  .uiv-bphint { margin-top: 7px; font-size: 10px; color: #71717a; line-height: 1.4; }
@@ -1196,11 +1246,15 @@ var CSS = (
1196
1246
  .uiv-ctl.st-file > .clabel { color: #e4e4e7; }
1197
1247
  .uiv-ctl.st-edited > .clabel { color: #4ade80; }
1198
1248
  .uiv-ctl.st-auto > .clabel { color: #6b7280; }
1249
+ .uiv-ctl.st-inherit > .clabel { color: #38bdf8; } /* value cascaded from another bp */
1250
+ .uiv-inh { font-size: 9px; font-weight: 700; color: #38bdf8; font-family: ui-monospace, monospace;
1251
+ background: #0c4a6e55; border: 1px solid #0369a1; border-radius: 4px; padding: 0 3px; margin-left: 2px; }
1199
1252
  .uiv-leg { display: flex; gap: 12px; padding: 8px 12px 2px; font-size: 9px;
1200
1253
  text-transform: uppercase; letter-spacing: .4px; }
1201
1254
  .uiv-lg { color: #e4e4e7; display: flex; align-items: center; gap: 4px; } /* file = white */
1202
1255
  .uiv-lg::before { content: ''; width: 7px; height: 7px; border-radius: 2px; background: currentColor; }
1203
1256
  .uiv-lg.edit { color: #4ade80; }
1257
+ .uiv-lg.inh { color: #38bdf8; }
1204
1258
  .uiv-lg.auto { color: #6b7280; }
1205
1259
  .uiv-sw { display: inline-block; width: 11px; height: 11px; border-radius: 3px;
1206
1260
  border: 1px solid rgba(255,255,255,0.2); flex: 0 0 auto; }
@@ -1458,7 +1512,7 @@ var Uivisor = class {
1458
1512
  this.root.innerHTML = `
1459
1513
  <style>${CSS}</style>
1460
1514
  <div class="uiv-framewrap">
1461
- <div class="uiv-framebar"><span class="uiv-framew">768px</span><span class="uiv-framex" title="Exit responsive">\u2715 exit</span></div>
1515
+ <div class="uiv-framebar"><div class="uiv-framechips uiv-chips"></div><span class="uiv-framew">768px</span><span class="uiv-framex" title="Exit responsive">\u2715</span></div>
1462
1516
  <div class="uiv-framestage">
1463
1517
  <div class="uiv-framehost">
1464
1518
  <iframe class="uiv-frame" data-uiv-frame="1"></iframe>
@@ -1643,7 +1697,7 @@ var Uivisor = class {
1643
1697
  const host = this.q(".uiv-framehost");
1644
1698
  host.style.width = `${this.frameWidth}px`;
1645
1699
  const bp = activeBreakpoint(this.frameWidth, this.bpSystem()).name;
1646
- this.q(".uiv-framew").textContent = `${this.frameWidth}px \xB7 ${bp}`;
1700
+ this.q(".uiv-framew").textContent = `${this.frameWidth}px \xB7 ${this.bpLabel(bp)}`;
1647
1701
  this.updateBp();
1648
1702
  this.reposition();
1649
1703
  }
@@ -1724,8 +1778,7 @@ var Uivisor = class {
1724
1778
  const el = this.selected;
1725
1779
  const st = this.st();
1726
1780
  if (!el || !st) return "";
1727
- const scope = this.activeScope();
1728
- const ch = st.record.changes.find((c) => c.property === css && c.breakpoint === scope.name);
1781
+ const ch = this.effectiveChange(css);
1729
1782
  if (ch) {
1730
1783
  const v = ch.live ?? ch.after.computed;
1731
1784
  return v.includes("var(") ? this.computedVal(css) || v : v;
@@ -1810,16 +1863,25 @@ var Uivisor = class {
1810
1863
  * inline override leaking across all of them.
1811
1864
  */
1812
1865
  reapplyScope() {
1813
- const scope = this.activeScope();
1866
+ const width = this.activeWidth();
1867
+ const sys = this.bpSystem();
1814
1868
  for (const [el, st] of this.states) {
1815
1869
  const sibs = this.siblingsOf(el);
1816
1870
  const targets = st.record.target === "all" ? sibs : [el];
1817
1871
  for (const css of st.applied) for (const e of sibs) removeOverride(e, css);
1818
1872
  st.applied = /* @__PURE__ */ new Set();
1873
+ const byProp = /* @__PURE__ */ new Map();
1819
1874
  for (const c of st.record.changes) {
1820
- if (c.breakpoint !== scope.name) continue;
1821
- for (const e of targets) applyOverride(e, c.property, c.live ?? c.after.computed);
1822
- st.applied.add(c.property);
1875
+ const arr = byProp.get(c.property) ?? [];
1876
+ arr.push(c);
1877
+ byProp.set(c.property, arr);
1878
+ }
1879
+ for (const [prop, changes] of byProp) {
1880
+ const eff = effectiveBreakpoint(changes.map((c2) => c2.breakpoint), width, sys);
1881
+ if (!eff) continue;
1882
+ const c = changes.find((x) => x.breakpoint === eff);
1883
+ for (const e of targets) applyOverride(e, prop, c.live ?? c.after.computed);
1884
+ st.applied.add(prop);
1823
1885
  }
1824
1886
  }
1825
1887
  this.reposition();
@@ -1832,6 +1894,34 @@ var Uivisor = class {
1832
1894
  if (this.responsive) return activeBreakpoint(this.frameWidth, sys);
1833
1895
  return currentBreakpoint(sys);
1834
1896
  }
1897
+ /** The width the inspector is scoped to (virtual screen, else real window). */
1898
+ activeWidth() {
1899
+ if (this.responsive) return this.frameWidth;
1900
+ return typeof window !== "undefined" ? window.innerWidth : 0;
1901
+ }
1902
+ /** The recorded change that wins the breakpoint cascade for `css` at the active
1903
+ * width — i.e. the value effective here, set on this breakpoint or inherited. */
1904
+ effectiveChange(css) {
1905
+ const st = this.st();
1906
+ if (!st) return null;
1907
+ const changes = st.record.changes.filter((c) => c.property === css);
1908
+ if (!changes.length) return null;
1909
+ const eff = effectiveBreakpoint(
1910
+ changes.map((c) => c.breakpoint),
1911
+ this.activeWidth(),
1912
+ this.bpSystem()
1913
+ );
1914
+ return eff ? changes.find((c) => c.breakpoint === eff) ?? null : null;
1915
+ }
1916
+ /** If `css`'s effective value is INHERITED from another breakpoint, its name. */
1917
+ inheritedFrom(props) {
1918
+ const scope = this.activeScope();
1919
+ for (const p of props) {
1920
+ const e = this.effectiveChange(p);
1921
+ if (e && e.breakpoint !== scope.name) return e.breakpoint;
1922
+ }
1923
+ return null;
1924
+ }
1835
1925
  recordProps(cssList) {
1836
1926
  const el = this.selected;
1837
1927
  const st = this.st();
@@ -1935,6 +2025,7 @@ var Uivisor = class {
1935
2025
  <div class="uiv-hint">Alt+U toggles \xB7 Esc deselects \xB7 \u2318/Ctrl+Z undo, \u21E7 to redo. Tweaks stay in the browser \u2014 nothing is written to your code.</div>
1936
2026
  ${this.journalHtml()}
1937
2027
  `;
2028
+ if (this.responsive) this.renderFrameBar();
1938
2029
  this.bindControls();
1939
2030
  return;
1940
2031
  }
@@ -1954,6 +2045,7 @@ var Uivisor = class {
1954
2045
  ${this.controlsHtml(this.context(this.selected))}
1955
2046
  ${this.journalHtml()}
1956
2047
  `;
2048
+ if (this.responsive) this.renderFrameBar();
1957
2049
  this.bindControls();
1958
2050
  }
1959
2051
  /** Small indicator: how many design tokens were detected (or a hint if none). */
@@ -2013,12 +2105,20 @@ var Uivisor = class {
2013
2105
  return out;
2014
2106
  }
2015
2107
  /** State class for an editable control's row: edited (green, at this breakpoint)
2016
- * · file (white, authored in CSS) · auto (grey, browser-computed/default). */
2108
+ * · inherit (a value cascaded from another breakpoint) · file (white, authored
2109
+ * in CSS) · auto (grey, browser-computed/default). */
2017
2110
  controlStateClass(props) {
2018
2111
  if (this.isChanged(props)) return " st-edited";
2112
+ if (this.inheritedFrom(props)) return " st-inherit";
2019
2113
  const inherit = props.some((p) => INHERITED_PROPS.has(p));
2020
2114
  return this.isAuthored(props, inherit) ? " st-file" : " st-auto";
2021
2115
  }
2116
+ /** A control's label, with an "inherited from {bp}" badge when the value cascaded. */
2117
+ ctlLabel(label, props) {
2118
+ const from = this.inheritedFrom(props);
2119
+ const badge = from ? ` <span class="uiv-inh" title="inherited from ${escapeAttr(this.bpLabel(from))} \u2014 not set at this breakpoint">\u2923${escapeHtml(this.bpLabel(from))}</span>` : "";
2120
+ return `<span class="clabel">${label}${badge}</span>`;
2121
+ }
2022
2122
  /** Is any of `props` authored in the project CSS? For inherited properties we
2023
2123
  * also walk ancestors (a body/parent font rule still counts as "from the file"). */
2024
2124
  isAuthored(props, inherit) {
@@ -2151,23 +2251,49 @@ var Uivisor = class {
2151
2251
  const items = collapsed ? "" : rows.map((r) => `<div class="uiv-rrow"><span class="uiv-rk">${r.k}</span><span class="uiv-rv">${r.v}</span></div>`).join("");
2152
2252
  return `<div class="uiv-sec">${this.accordionTitle("Current styles")}<div class="uiv-readout">${items}</div></div>`;
2153
2253
  }
2154
- /** Breakpoint scope switcher: shows the PROJECT's breakpoints + the live window one. */
2155
- breakpointBarHtml() {
2254
+ /** Display label for a breakpoint name the unprefixed "base" scope reads "all"
2255
+ * (applies to every size by default); internal key stays "base". */
2256
+ bpLabel(name) {
2257
+ return name === "base" ? "all" : name;
2258
+ }
2259
+ /** A device icon for a breakpoint chip, by its threshold (size proxy). */
2260
+ bpIcon(name) {
2261
+ if (name === "live") return ICONS.live;
2262
+ if (name === "base") return ICONS.all;
2263
+ const px2 = this.bpSystem().breakpoints.find((b) => b.name === name)?.minWidth ?? 0;
2264
+ if (px2 < 768) return ICONS.phone;
2265
+ if (px2 < 1024) return ICONS.tablet;
2266
+ return ICONS.desktop;
2267
+ }
2268
+ /** Breakpoint chips with icons. Order: "all" (base, the default) → Live → each
2269
+ * project breakpoint. Reused by the panel (Live) and the over-frame bar. */
2270
+ breakpointChipsHtml() {
2156
2271
  const sys = this.bpSystem();
2157
- const bps = sys.breakpoints;
2158
- const names = ["base", ...bps.map((b) => b.name)];
2159
2272
  const frameBp = this.responsive ? activeBreakpoint(this.frameWidth, sys).name : null;
2160
2273
  const winBp = currentBreakpoint(sys).name;
2274
+ const isActive = (n) => this.responsive ? n === frameBp : n === winBp;
2275
+ const chip = (n, on, title) => `<button class="uiv-chip${on ? " on" : ""}" data-bp="${n}" title="${escapeAttr(title)}">${this.bpIcon(n)}<span>${n === "live" ? "Live" : this.bpLabel(n)}</span></button>`;
2276
+ const all = chip("base", isActive("base"), "No breakpoint \u2014 applies to every size by default");
2277
+ const live = chip("live", !this.responsive, "Follow your real browser window");
2278
+ const rest = sys.breakpoints.map((b) => chip(b.name, isActive(b.name), sys.dir === "min" ? `\u2265 ${b.minWidth}px` : `\u2264 ${b.minWidth}px`)).join("");
2279
+ return all + live + rest;
2280
+ }
2281
+ /** Panel breakpoint bar — shown only in Live mode (in responsive mode the bar
2282
+ * lives over the virtual screen instead). */
2283
+ breakpointBarHtml() {
2284
+ if (this.responsive) return "";
2285
+ const sys = this.bpSystem();
2286
+ const winBp = currentBreakpoint(sys).name;
2161
2287
  const liveW = typeof window !== "undefined" ? window.innerWidth : 0;
2162
- const liveChip = `<button class="uiv-chip${!this.responsive ? " on" : ""}" data-bp="live" title="Follow your real browser window">Live</button>`;
2163
- const chips = names.map((n) => {
2164
- const active = this.responsive ? n === frameBp : n === winBp;
2165
- const px2 = n === "base" ? 0 : bps.find((b) => b.name === n).minWidth;
2166
- return `<button class="uiv-chip${active ? " on" : ""}" data-bp="${n}" title="Preview at \u2265${px2}px">${n}</button>`;
2167
- }).join("");
2168
2288
  const detected = sys.name === "detected" ? "" : " (defaults)";
2169
- const hint = this.responsive ? `Virtual screen at <b>${this.frameWidth}px</b> (${frameBp}). Edits scoped to <b>${frameBp}:</b>. Drag the frame edge to fine-tune.` : `Live \u2014 your window is <b>${liveW}px</b> = <b>${winBp}</b> range, edits scoped to <b>${winBp}:</b>. Click another size to shrink the screen to it.`;
2170
- return `<div class="uiv-sec"><div class="uiv-sectitle">Screen / breakpoint${detected}</div><div class="uiv-chips">${liveChip}${chips}</div><div class="uiv-bphint">${hint}</div></div>`;
2289
+ const cascade = sys.dir === "min" ? `Mobile-first: an edit applies to this breakpoint and <b>wider</b>.` : `Desktop-first: an edit applies to this breakpoint and <b>narrower</b>.`;
2290
+ const hint = `Live \u2014 window <b>${liveW}px</b> = <b>${this.bpLabel(winBp)}</b>. ${cascade} Click a size to shrink the screen.`;
2291
+ return `<div class="uiv-sec"><div class="uiv-sectitle">Screen / breakpoint${detected}</div><div class="uiv-chips">${this.breakpointChipsHtml()}</div><div class="uiv-bphint">${hint}</div></div>`;
2292
+ }
2293
+ /** Populate the bar over the virtual screen (responsive mode) with the chips. */
2294
+ renderFrameBar() {
2295
+ const host = this.root.querySelector(".uiv-framechips");
2296
+ if (host) host.innerHTML = this.breakpointChipsHtml();
2171
2297
  }
2172
2298
  /** "Apply changes to": this element, an existing shared class, or a NEW class. */
2173
2299
  targetHtml(st) {
@@ -2202,7 +2328,7 @@ var Uivisor = class {
2202
2328
  return true;
2203
2329
  }
2204
2330
  controlsHtml(ctx) {
2205
- const legend = `<div class="uiv-leg"><span class="uiv-lg">file</span><span class="uiv-lg edit">edited</span><span class="uiv-lg auto">auto</span></div>`;
2331
+ const legend = `<div class="uiv-leg"><span class="uiv-lg">file</span><span class="uiv-lg edit">edited</span><span class="uiv-lg inh">inherited</span><span class="uiv-lg auto">auto</span></div>`;
2206
2332
  const secs = SECTIONS.map((sec) => {
2207
2333
  const controls = sec.controls.filter((c) => this.relevant(c, ctx));
2208
2334
  if (!controls.length) return "";
@@ -2284,7 +2410,7 @@ var Uivisor = class {
2284
2410
  const d = this.dimDisplay(c);
2285
2411
  const changed = this.isChanged([c.css]);
2286
2412
  const units = c.units.map((u) => `<option value="${u}"${u === d.unit ? " selected" : ""}>${UNIT_LABELS[u] ?? u}</option>`).join("");
2287
- return `<div class="uiv-ctl${this.controlStateClass([c.css])}"><span class="clabel">${c.label}</span><div class="cfield"><div class="uiv-num uiv-dim${changed ? " changed" : ""}" data-css="${c.css}"><span class="uiv-scrub" title="Drag to change">${c.icon}</span><input type="number" step="any" value="${escapeAttr(d.num)}" placeholder="${escapeAttr(d.placeholder)}"><select class="uiv-unit" title="Unit">${units}</select></div></div><span></span></div>`;
2413
+ return `<div class="uiv-ctl${this.controlStateClass([c.css])}">${this.ctlLabel(c.label, [c.css])}<div class="cfield"><div class="uiv-num uiv-dim${changed ? " changed" : ""}" data-css="${c.css}"><span class="uiv-scrub" title="Drag to change">${c.icon}</span><input type="number" step="any" value="${escapeAttr(d.num)}" placeholder="${escapeAttr(d.placeholder)}"><select class="uiv-unit" title="Unit">${units}</select></div></div><span></span></div>`;
2288
2414
  }
2289
2415
  /** A design-token picker row for a property, shown only when the project exposes
2290
2416
  * tokens for that category. Picking a token applies its value + tags the prompt. */
@@ -2318,7 +2444,7 @@ var Uivisor = class {
2318
2444
  const info = this.numInfo(cssList);
2319
2445
  const changed = this.isChanged(cssList);
2320
2446
  const open = this.expanded.has(c.key);
2321
- let html = `<div class="uiv-ctl${this.controlStateClass(cssList)}"><span class="clabel">${c.label}</span><div class="cfield">${this.numField(cssList.join(","), info.mixed ? "" : info.value, c.icon, changed, false, info.mixed ? "Mixed" : "\u2014")}</div><button class="uiv-expand${open ? " on" : ""}" data-key="${c.key}" title="Edit each side individually">${open ? ICONS.collapse : ICONS.expand}</button></div>`;
2447
+ let html = `<div class="uiv-ctl${this.controlStateClass(cssList)}">` + this.ctlLabel(c.label, cssList) + `<div class="cfield">${this.numField(cssList.join(","), info.mixed ? "" : info.value, c.icon, changed, false, info.mixed ? "Mixed" : "\u2014")}</div><button class="uiv-expand${open ? " on" : ""}" data-key="${c.key}" title="Edit each side individually">${open ? ICONS.collapse : ICONS.expand}</button></div>`;
2322
2448
  if (open) {
2323
2449
  html += `<div class="uiv-sides">` + c.sides.map((s) => {
2324
2450
  const v = this.liveNum(s.css);
@@ -2336,7 +2462,7 @@ var Uivisor = class {
2336
2462
  }
2337
2463
  if (c.kind === "len") {
2338
2464
  const v = this.liveNum(c.css);
2339
- return `<div class="uiv-ctl${this.controlStateClass([c.css])}"><span class="clabel">${c.label}</span><div class="cfield">${this.numField(c.css, v == null ? "" : String(round2(v)), c.icon, this.isChanged([c.css]), false, "\u2014")}</div><span></span></div>` + this.tokenRowHtml(c.css, "Token");
2465
+ return `<div class="uiv-ctl${this.controlStateClass([c.css])}">${this.ctlLabel(c.label, [c.css])}<div class="cfield">${this.numField(c.css, v == null ? "" : String(round2(v)), c.icon, this.isChanged([c.css]), false, "\u2014")}</div><span></span></div>` + this.tokenRowHtml(c.css, "Token");
2340
2466
  }
2341
2467
  if (c.kind === "dim") {
2342
2468
  return this.dimField(c);
@@ -2345,10 +2471,10 @@ var Uivisor = class {
2345
2471
  const cur = this.selectCurrent(c.css);
2346
2472
  const optList = cur && !c.options.includes(cur) ? [cur, ...c.options] : c.options;
2347
2473
  const opts = optList.map((o) => `<option value="${o}"${o === cur ? " selected" : ""}>${o}</option>`).join("");
2348
- return `<div class="uiv-ctl${this.controlStateClass([c.css])}"><span class="clabel">${c.label}</span><div class="cfield"><select class="uiv-sel${this.isChanged([c.css]) ? " changed" : ""}" data-css="${c.css}">${opts}</select></div><span></span></div>`;
2474
+ return `<div class="uiv-ctl${this.controlStateClass([c.css])}">${this.ctlLabel(c.label, [c.css])}<div class="cfield"><select class="uiv-sel${this.isChanged([c.css]) ? " changed" : ""}" data-css="${c.css}">${opts}</select></div><span></span></div>`;
2349
2475
  }
2350
2476
  const val = toHexInput(this.liveVal(c.css));
2351
- return `<div class="uiv-ctl${this.controlStateClass([c.css])}"><span class="clabel">${c.label}</span><div class="cfield"><input type="color" class="uiv-color${this.isChanged([c.css]) ? " changed" : ""}" data-css="${c.css}" value="${val}"></div><span></span></div>` + this.tokenRowHtml(c.css, "Token");
2477
+ return `<div class="uiv-ctl${this.controlStateClass([c.css])}">${this.ctlLabel(c.label, [c.css])}<div class="cfield"><input type="color" class="uiv-color${this.isChanged([c.css]) ? " changed" : ""}" data-css="${c.css}" value="${val}"></div><span></span></div>` + this.tokenRowHtml(c.css, "Token");
2352
2478
  }
2353
2479
  bindControls() {
2354
2480
  const root = this.root;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uivisor",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "Dev-only visual UI tweaker that turns mouse edits into a precise, breakpoint-aware prompt for your AI coding agent — without touching your source.",
6
6
  "license": "MIT",