uivisor 0.1.7 → 0.1.8

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 +160 -38
  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,11 @@ 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"/>')
370
414
  };
371
415
  var SECTIONS = [
372
416
  {
@@ -1082,12 +1126,14 @@ var CSS = (
1082
1126
  }
1083
1127
  .uiv-framewrap.show { display: flex; }
1084
1128
  .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;
1129
+ display: flex; align-items: center; gap: 12px;
1130
+ height: 46px; padding: 0 14px; color: #e4e4e7; font-size: 12px; flex: 0 0 auto;
1087
1131
  border-bottom: 1px solid #27272a;
1088
1132
  }
1089
- .uiv-framew { font-family: ui-monospace, monospace; color: #c7d2fe; font-weight: 600; }
1090
- .uiv-framex { cursor: pointer; color: #a1a1aa; }
1133
+ .uiv-framechips { display: flex; gap: 6px; flex: 1; justify-content: center; flex-wrap: wrap; }
1134
+ .uiv-framew { font-family: ui-monospace, monospace; color: #c7d2fe; font-weight: 600;
1135
+ white-space: nowrap; flex: 0 0 auto; }
1136
+ .uiv-framex { cursor: pointer; color: #a1a1aa; font-size: 14px; flex: 0 0 auto; }
1091
1137
  .uiv-framex:hover { color: #fff; }
1092
1138
  .uiv-framestage {
1093
1139
  flex: 1; display: flex; align-items: stretch; justify-content: center;
@@ -1151,7 +1197,10 @@ var CSS = (
1151
1197
  border-radius: 6px; padding: 3px 8px; font-size: 11px; font-weight: 600;
1152
1198
  font-family: ui-monospace, monospace;
1153
1199
  }
1200
+ .uiv-chip { display: inline-flex; align-items: center; gap: 4px; }
1201
+ .uiv-chip svg { width: 13px; height: 13px; opacity: .85; }
1154
1202
  .uiv-chip:hover, .uiv-clschip:hover { color: #fff; background: #3f3f46; }
1203
+ .uiv-chip.on svg { opacity: 1; }
1155
1204
  .uiv-chip.win { border-color: #52525b; color: #d4d4d8; }
1156
1205
  .uiv-chip.on, .uiv-clschip.on { background: #4f46e5; border-color: #6366f1; color: #fff; }
1157
1206
  .uiv-bphint { margin-top: 7px; font-size: 10px; color: #71717a; line-height: 1.4; }
@@ -1196,11 +1245,15 @@ var CSS = (
1196
1245
  .uiv-ctl.st-file > .clabel { color: #e4e4e7; }
1197
1246
  .uiv-ctl.st-edited > .clabel { color: #4ade80; }
1198
1247
  .uiv-ctl.st-auto > .clabel { color: #6b7280; }
1248
+ .uiv-ctl.st-inherit > .clabel { color: #38bdf8; } /* value cascaded from another bp */
1249
+ .uiv-inh { font-size: 9px; font-weight: 700; color: #38bdf8; font-family: ui-monospace, monospace;
1250
+ background: #0c4a6e55; border: 1px solid #0369a1; border-radius: 4px; padding: 0 3px; margin-left: 2px; }
1199
1251
  .uiv-leg { display: flex; gap: 12px; padding: 8px 12px 2px; font-size: 9px;
1200
1252
  text-transform: uppercase; letter-spacing: .4px; }
1201
1253
  .uiv-lg { color: #e4e4e7; display: flex; align-items: center; gap: 4px; } /* file = white */
1202
1254
  .uiv-lg::before { content: ''; width: 7px; height: 7px; border-radius: 2px; background: currentColor; }
1203
1255
  .uiv-lg.edit { color: #4ade80; }
1256
+ .uiv-lg.inh { color: #38bdf8; }
1204
1257
  .uiv-lg.auto { color: #6b7280; }
1205
1258
  .uiv-sw { display: inline-block; width: 11px; height: 11px; border-radius: 3px;
1206
1259
  border: 1px solid rgba(255,255,255,0.2); flex: 0 0 auto; }
@@ -1458,7 +1511,7 @@ var Uivisor = class {
1458
1511
  this.root.innerHTML = `
1459
1512
  <style>${CSS}</style>
1460
1513
  <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>
1514
+ <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
1515
  <div class="uiv-framestage">
1463
1516
  <div class="uiv-framehost">
1464
1517
  <iframe class="uiv-frame" data-uiv-frame="1"></iframe>
@@ -1724,8 +1777,7 @@ var Uivisor = class {
1724
1777
  const el = this.selected;
1725
1778
  const st = this.st();
1726
1779
  if (!el || !st) return "";
1727
- const scope = this.activeScope();
1728
- const ch = st.record.changes.find((c) => c.property === css && c.breakpoint === scope.name);
1780
+ const ch = this.effectiveChange(css);
1729
1781
  if (ch) {
1730
1782
  const v = ch.live ?? ch.after.computed;
1731
1783
  return v.includes("var(") ? this.computedVal(css) || v : v;
@@ -1810,16 +1862,25 @@ var Uivisor = class {
1810
1862
  * inline override leaking across all of them.
1811
1863
  */
1812
1864
  reapplyScope() {
1813
- const scope = this.activeScope();
1865
+ const width = this.activeWidth();
1866
+ const sys = this.bpSystem();
1814
1867
  for (const [el, st] of this.states) {
1815
1868
  const sibs = this.siblingsOf(el);
1816
1869
  const targets = st.record.target === "all" ? sibs : [el];
1817
1870
  for (const css of st.applied) for (const e of sibs) removeOverride(e, css);
1818
1871
  st.applied = /* @__PURE__ */ new Set();
1872
+ const byProp = /* @__PURE__ */ new Map();
1819
1873
  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);
1874
+ const arr = byProp.get(c.property) ?? [];
1875
+ arr.push(c);
1876
+ byProp.set(c.property, arr);
1877
+ }
1878
+ for (const [prop, changes] of byProp) {
1879
+ const eff = effectiveBreakpoint(changes.map((c2) => c2.breakpoint), width, sys);
1880
+ if (!eff) continue;
1881
+ const c = changes.find((x) => x.breakpoint === eff);
1882
+ for (const e of targets) applyOverride(e, prop, c.live ?? c.after.computed);
1883
+ st.applied.add(prop);
1823
1884
  }
1824
1885
  }
1825
1886
  this.reposition();
@@ -1832,6 +1893,34 @@ var Uivisor = class {
1832
1893
  if (this.responsive) return activeBreakpoint(this.frameWidth, sys);
1833
1894
  return currentBreakpoint(sys);
1834
1895
  }
1896
+ /** The width the inspector is scoped to (virtual screen, else real window). */
1897
+ activeWidth() {
1898
+ if (this.responsive) return this.frameWidth;
1899
+ return typeof window !== "undefined" ? window.innerWidth : 0;
1900
+ }
1901
+ /** The recorded change that wins the breakpoint cascade for `css` at the active
1902
+ * width — i.e. the value effective here, set on this breakpoint or inherited. */
1903
+ effectiveChange(css) {
1904
+ const st = this.st();
1905
+ if (!st) return null;
1906
+ const changes = st.record.changes.filter((c) => c.property === css);
1907
+ if (!changes.length) return null;
1908
+ const eff = effectiveBreakpoint(
1909
+ changes.map((c) => c.breakpoint),
1910
+ this.activeWidth(),
1911
+ this.bpSystem()
1912
+ );
1913
+ return eff ? changes.find((c) => c.breakpoint === eff) ?? null : null;
1914
+ }
1915
+ /** If `css`'s effective value is INHERITED from another breakpoint, its name. */
1916
+ inheritedFrom(props) {
1917
+ const scope = this.activeScope();
1918
+ for (const p of props) {
1919
+ const e = this.effectiveChange(p);
1920
+ if (e && e.breakpoint !== scope.name) return e.breakpoint;
1921
+ }
1922
+ return null;
1923
+ }
1835
1924
  recordProps(cssList) {
1836
1925
  const el = this.selected;
1837
1926
  const st = this.st();
@@ -1935,6 +2024,7 @@ var Uivisor = class {
1935
2024
  <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
2025
  ${this.journalHtml()}
1937
2026
  `;
2027
+ if (this.responsive) this.renderFrameBar();
1938
2028
  this.bindControls();
1939
2029
  return;
1940
2030
  }
@@ -1954,6 +2044,7 @@ var Uivisor = class {
1954
2044
  ${this.controlsHtml(this.context(this.selected))}
1955
2045
  ${this.journalHtml()}
1956
2046
  `;
2047
+ if (this.responsive) this.renderFrameBar();
1957
2048
  this.bindControls();
1958
2049
  }
1959
2050
  /** Small indicator: how many design tokens were detected (or a hint if none). */
@@ -2013,12 +2104,20 @@ var Uivisor = class {
2013
2104
  return out;
2014
2105
  }
2015
2106
  /** State class for an editable control's row: edited (green, at this breakpoint)
2016
- * · file (white, authored in CSS) · auto (grey, browser-computed/default). */
2107
+ * · inherit (a value cascaded from another breakpoint) · file (white, authored
2108
+ * in CSS) · auto (grey, browser-computed/default). */
2017
2109
  controlStateClass(props) {
2018
2110
  if (this.isChanged(props)) return " st-edited";
2111
+ if (this.inheritedFrom(props)) return " st-inherit";
2019
2112
  const inherit = props.some((p) => INHERITED_PROPS.has(p));
2020
2113
  return this.isAuthored(props, inherit) ? " st-file" : " st-auto";
2021
2114
  }
2115
+ /** A control's label, with an "inherited from {bp}" badge when the value cascaded. */
2116
+ ctlLabel(label, props) {
2117
+ const from = this.inheritedFrom(props);
2118
+ const badge = from ? ` <span class="uiv-inh" title="inherited from ${escapeAttr(from)} \u2014 not set at this breakpoint">\u2923${escapeHtml(from)}</span>` : "";
2119
+ return `<span class="clabel">${label}${badge}</span>`;
2120
+ }
2022
2121
  /** Is any of `props` authored in the project CSS? For inherited properties we
2023
2122
  * also walk ancestors (a body/parent font rule still counts as "from the file"). */
2024
2123
  isAuthored(props, inherit) {
@@ -2151,23 +2250,46 @@ var Uivisor = class {
2151
2250
  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
2251
  return `<div class="uiv-sec">${this.accordionTitle("Current styles")}<div class="uiv-readout">${items}</div></div>`;
2153
2252
  }
2154
- /** Breakpoint scope switcher: shows the PROJECT's breakpoints + the live window one. */
2155
- breakpointBarHtml() {
2253
+ /** A device icon for a breakpoint chip, by its threshold (size proxy). */
2254
+ bpIcon(name) {
2255
+ if (name === "live") return ICONS.live;
2256
+ const px2 = name === "base" ? 0 : this.bpSystem().breakpoints.find((b) => b.name === name)?.minWidth ?? 0;
2257
+ if (px2 < 768) return ICONS.phone;
2258
+ if (px2 < 1024) return ICONS.tablet;
2259
+ return ICONS.desktop;
2260
+ }
2261
+ /** Breakpoint chips (Live + each project breakpoint) with device icons. Reused by
2262
+ * the panel (Live mode) and the bar over the virtual screen (responsive mode). */
2263
+ breakpointChipsHtml() {
2156
2264
  const sys = this.bpSystem();
2157
- const bps = sys.breakpoints;
2158
- const names = ["base", ...bps.map((b) => b.name)];
2265
+ const names = ["base", ...sys.breakpoints.map((b) => b.name)];
2159
2266
  const frameBp = this.responsive ? activeBreakpoint(this.frameWidth, sys).name : null;
2160
2267
  const winBp = currentBreakpoint(sys).name;
2161
- 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>`;
2268
+ const chip = (n, on, title) => `<button class="uiv-chip${on ? " on" : ""}" data-bp="${n}" title="${escapeAttr(title)}">${this.bpIcon(n)}<span>${n === "live" ? "Live" : n}</span></button>`;
2269
+ const live = chip("live", !this.responsive, "Follow your real browser window");
2163
2270
  const chips = names.map((n) => {
2164
2271
  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>`;
2272
+ const px2 = n === "base" ? 0 : sys.breakpoints.find((b) => b.name === n).minWidth;
2273
+ return chip(n, active, sys.dir === "min" ? `\u2265 ${px2}px` : `\u2264 ${px2}px`);
2167
2274
  }).join("");
2275
+ return live + chips;
2276
+ }
2277
+ /** Panel breakpoint bar — shown only in Live mode (in responsive mode the bar
2278
+ * lives over the virtual screen instead). */
2279
+ breakpointBarHtml() {
2280
+ if (this.responsive) return "";
2281
+ const sys = this.bpSystem();
2282
+ const winBp = currentBreakpoint(sys).name;
2283
+ const liveW = typeof window !== "undefined" ? window.innerWidth : 0;
2168
2284
  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>`;
2285
+ 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>.`;
2286
+ const hint = `Live \u2014 window <b>${liveW}px</b> = <b>${winBp}</b>. ${cascade} Click a size to shrink the screen.`;
2287
+ 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>`;
2288
+ }
2289
+ /** Populate the bar over the virtual screen (responsive mode) with the chips. */
2290
+ renderFrameBar() {
2291
+ const host = this.root.querySelector(".uiv-framechips");
2292
+ if (host) host.innerHTML = this.breakpointChipsHtml();
2171
2293
  }
2172
2294
  /** "Apply changes to": this element, an existing shared class, or a NEW class. */
2173
2295
  targetHtml(st) {
@@ -2202,7 +2324,7 @@ var Uivisor = class {
2202
2324
  return true;
2203
2325
  }
2204
2326
  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>`;
2327
+ 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
2328
  const secs = SECTIONS.map((sec) => {
2207
2329
  const controls = sec.controls.filter((c) => this.relevant(c, ctx));
2208
2330
  if (!controls.length) return "";
@@ -2284,7 +2406,7 @@ var Uivisor = class {
2284
2406
  const d = this.dimDisplay(c);
2285
2407
  const changed = this.isChanged([c.css]);
2286
2408
  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>`;
2409
+ 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
2410
  }
2289
2411
  /** A design-token picker row for a property, shown only when the project exposes
2290
2412
  * tokens for that category. Picking a token applies its value + tags the prompt. */
@@ -2318,7 +2440,7 @@ var Uivisor = class {
2318
2440
  const info = this.numInfo(cssList);
2319
2441
  const changed = this.isChanged(cssList);
2320
2442
  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>`;
2443
+ 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
2444
  if (open) {
2323
2445
  html += `<div class="uiv-sides">` + c.sides.map((s) => {
2324
2446
  const v = this.liveNum(s.css);
@@ -2336,7 +2458,7 @@ var Uivisor = class {
2336
2458
  }
2337
2459
  if (c.kind === "len") {
2338
2460
  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");
2461
+ 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
2462
  }
2341
2463
  if (c.kind === "dim") {
2342
2464
  return this.dimField(c);
@@ -2345,10 +2467,10 @@ var Uivisor = class {
2345
2467
  const cur = this.selectCurrent(c.css);
2346
2468
  const optList = cur && !c.options.includes(cur) ? [cur, ...c.options] : c.options;
2347
2469
  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>`;
2470
+ 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
2471
  }
2350
2472
  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");
2473
+ 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
2474
  }
2353
2475
  bindControls() {
2354
2476
  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.8",
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",