uivisor 0.1.6 → 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 +219 -61
  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; }
@@ -1191,12 +1240,20 @@ var CSS = (
1191
1240
  .uiv-rk { color: #71717a; }
1192
1241
  .uiv-rv { color: #fff; word-break: break-all; display: flex; align-items: center; gap: 6px; }
1193
1242
  .uiv-rv.changed { color: #4ade80; } /* edited in uivisor \u2192 green */
1194
- .uiv-rv.auto { color: #6b7280; } /* browser-computed / default \u2192 grey */
1195
- .uiv-leg { display: flex; gap: 10px; padding: 1px 0 6px; font-size: 9px;
1243
+
1244
+ /* control-row state: file (authored) \xB7 edited (this breakpoint) \xB7 auto (computed) */
1245
+ .uiv-ctl.st-file > .clabel { color: #e4e4e7; }
1246
+ .uiv-ctl.st-edited > .clabel { color: #4ade80; }
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; }
1251
+ .uiv-leg { display: flex; gap: 12px; padding: 8px 12px 2px; font-size: 9px;
1196
1252
  text-transform: uppercase; letter-spacing: .4px; }
1197
1253
  .uiv-lg { color: #e4e4e7; display: flex; align-items: center; gap: 4px; } /* file = white */
1198
1254
  .uiv-lg::before { content: ''; width: 7px; height: 7px; border-radius: 2px; background: currentColor; }
1199
1255
  .uiv-lg.edit { color: #4ade80; }
1256
+ .uiv-lg.inh { color: #38bdf8; }
1200
1257
  .uiv-lg.auto { color: #6b7280; }
1201
1258
  .uiv-sw { display: inline-block; width: 11px; height: 11px; border-radius: 3px;
1202
1259
  border: 1px solid rgba(255,255,255,0.2); flex: 0 0 auto; }
@@ -1299,6 +1356,15 @@ var CSS = (
1299
1356
  // src/overlay/index.ts
1300
1357
  var counter = 0;
1301
1358
  var round2 = (n) => Math.round(n * 100) / 100;
1359
+ var INHERITED_PROPS = /* @__PURE__ */ new Set([
1360
+ "font-size",
1361
+ "font-weight",
1362
+ "line-height",
1363
+ "letter-spacing",
1364
+ "color",
1365
+ "text-align",
1366
+ "font-family"
1367
+ ]);
1302
1368
  var Uivisor = class {
1303
1369
  constructor() {
1304
1370
  this.enabled = false;
@@ -1379,7 +1445,10 @@ var Uivisor = class {
1379
1445
  const winBp = currentBreakpoint(this.bpSystem()).name;
1380
1446
  if (winBp !== this.lastWinBp) {
1381
1447
  this.lastWinBp = winBp;
1382
- if (this.enabled && this.selected) this.renderBody();
1448
+ if (this.enabled && this.selected) {
1449
+ this.reapplyScope();
1450
+ this.renderBody();
1451
+ }
1383
1452
  }
1384
1453
  };
1385
1454
  this.reposition = () => {
@@ -1442,7 +1511,7 @@ var Uivisor = class {
1442
1511
  this.root.innerHTML = `
1443
1512
  <style>${CSS}</style>
1444
1513
  <div class="uiv-framewrap">
1445
- <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>
1446
1515
  <div class="uiv-framestage">
1447
1516
  <div class="uiv-framehost">
1448
1517
  <iframe class="uiv-frame" data-uiv-frame="1"></iframe>
@@ -1645,6 +1714,7 @@ var Uivisor = class {
1645
1714
  const up = () => {
1646
1715
  handle.removeEventListener("pointermove", move);
1647
1716
  handle.removeEventListener("pointerup", up);
1717
+ this.reapplyScope();
1648
1718
  this.renderBody();
1649
1719
  };
1650
1720
  handle.addEventListener("pointermove", move);
@@ -1680,6 +1750,7 @@ var Uivisor = class {
1680
1750
  dimUnit: {}
1681
1751
  });
1682
1752
  }
1753
+ if (el) this.reapplyScope();
1683
1754
  this.reposition();
1684
1755
  this.renderBody();
1685
1756
  }
@@ -1706,6 +1777,11 @@ var Uivisor = class {
1706
1777
  const el = this.selected;
1707
1778
  const st = this.st();
1708
1779
  if (!el || !st) return "";
1780
+ const ch = this.effectiveChange(css);
1781
+ if (ch) {
1782
+ const v = ch.live ?? ch.after.computed;
1783
+ return v.includes("var(") ? this.computedVal(css) || v : v;
1784
+ }
1709
1785
  const inline = el.style.getPropertyValue(css);
1710
1786
  if (inline && !inline.includes("var(")) return inline;
1711
1787
  return this.computedVal(css) || st.original[css] || "";
@@ -1728,7 +1804,10 @@ var Uivisor = class {
1728
1804
  isChanged(cssList) {
1729
1805
  const st = this.st();
1730
1806
  if (!st) return false;
1731
- return cssList.some((c) => st.record.changes.some((ch) => ch.property === c));
1807
+ const scope = this.activeScope();
1808
+ return cssList.some(
1809
+ (c) => st.record.changes.some((ch) => ch.property === c && ch.breakpoint === scope.name)
1810
+ );
1732
1811
  }
1733
1812
  selectCurrent(css) {
1734
1813
  let v = this.liveVal(css).trim();
@@ -1773,16 +1852,36 @@ var Uivisor = class {
1773
1852
  }
1774
1853
  /** Re-apply recorded overrides after the target (all ↔ one) changes. */
1775
1854
  reapplyForTarget() {
1776
- const el = this.selected;
1777
- const st = this.st();
1778
- if (!el || !st) return;
1779
- const sibs = this.siblingsOf(el);
1780
- const targets = this.targetEls();
1781
- const props = new Set(st.record.changes.map((c) => c.property));
1782
- for (const css of props) {
1783
- for (const e of sibs) removeOverride(e, css);
1784
- const c = st.record.changes.find((ch) => ch.property === css);
1785
- if (c) for (const e of targets) applyOverride(e, css, c.live ?? c.after.computed);
1855
+ this.reapplyScope();
1856
+ }
1857
+ /**
1858
+ * Project the live preview for the ACTIVE breakpoint: strip every override we
1859
+ * applied, then re-apply ONLY the changes recorded for the current scope. This
1860
+ * is what makes per-breakpoint edits behave — set padding 20 at xl, 10 at md,
1861
+ * and each breakpoint shows (and previews) its own value instead of one global
1862
+ * inline override leaking across all of them.
1863
+ */
1864
+ reapplyScope() {
1865
+ const width = this.activeWidth();
1866
+ const sys = this.bpSystem();
1867
+ for (const [el, st] of this.states) {
1868
+ const sibs = this.siblingsOf(el);
1869
+ const targets = st.record.target === "all" ? sibs : [el];
1870
+ for (const css of st.applied) for (const e of sibs) removeOverride(e, css);
1871
+ st.applied = /* @__PURE__ */ new Set();
1872
+ const byProp = /* @__PURE__ */ new Map();
1873
+ for (const c of st.record.changes) {
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);
1884
+ }
1786
1885
  }
1787
1886
  this.reposition();
1788
1887
  }
@@ -1794,6 +1893,34 @@ var Uivisor = class {
1794
1893
  if (this.responsive) return activeBreakpoint(this.frameWidth, sys);
1795
1894
  return currentBreakpoint(sys);
1796
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
+ }
1797
1924
  recordProps(cssList) {
1798
1925
  const el = this.selected;
1799
1926
  const st = this.st();
@@ -1897,6 +2024,7 @@ var Uivisor = class {
1897
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>
1898
2025
  ${this.journalHtml()}
1899
2026
  `;
2027
+ if (this.responsive) this.renderFrameBar();
1900
2028
  this.bindControls();
1901
2029
  return;
1902
2030
  }
@@ -1916,6 +2044,7 @@ var Uivisor = class {
1916
2044
  ${this.controlsHtml(this.context(this.selected))}
1917
2045
  ${this.journalHtml()}
1918
2046
  `;
2047
+ if (this.responsive) this.renderFrameBar();
1919
2048
  this.bindControls();
1920
2049
  }
1921
2050
  /** Small indicator: how many design tokens were detected (or a hint if none). */
@@ -1974,6 +2103,21 @@ var Uivisor = class {
1974
2103
  cache.set(el, out);
1975
2104
  return out;
1976
2105
  }
2106
+ /** State class for an editable control's row: edited (green, at this breakpoint)
2107
+ * · inherit (a value cascaded from another breakpoint) · file (white, authored
2108
+ * in CSS) · auto (grey, browser-computed/default). */
2109
+ controlStateClass(props) {
2110
+ if (this.isChanged(props)) return " st-edited";
2111
+ if (this.inheritedFrom(props)) return " st-inherit";
2112
+ const inherit = props.some((p) => INHERITED_PROPS.has(p));
2113
+ return this.isAuthored(props, inherit) ? " st-file" : " st-auto";
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
+ }
1977
2121
  /** Is any of `props` authored in the project CSS? For inherited properties we
1978
2122
  * also walk ancestors (a body/parent font rule still counts as "from the file"). */
1979
2123
  isAuthored(props, inherit) {
@@ -2014,14 +2158,9 @@ var Uivisor = class {
2014
2158
  };
2015
2159
  const px4 = (pre, suf = "") => [`${pre}-top${suf}`, `${pre}-right${suf}`, `${pre}-bottom${suf}`, `${pre}-left${suf}`];
2016
2160
  const clip = (s, n = 30) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
2017
- const changedSet = new Set(this.st()?.record.changes.map((c) => c.property) ?? []);
2018
- const stateCls = (props, inherit = false) => {
2019
- if (props.some((p) => changedSet.has(p))) return " changed";
2020
- return this.isAuthored(props, inherit) ? "" : " auto";
2021
- };
2022
2161
  const rows = [];
2023
- const add = (k, v, props, inherit = false) => {
2024
- if (v !== "" && v != null) rows.push({ k, v, cls: stateCls(props, inherit) });
2162
+ const add = (k, v, _props = [], _inherit = false) => {
2163
+ if (v !== "" && v != null) rows.push({ k, v });
2025
2164
  };
2026
2165
  const disp = g("display");
2027
2166
  add("display", disp, ["display"]);
@@ -2108,29 +2247,49 @@ var Uivisor = class {
2108
2247
  const tr = g("transform");
2109
2248
  if (tr && tr !== "none") add("transform", clip(tr, 22), ["transform"]);
2110
2249
  const collapsed = this.collapsedSecs.has("Current styles");
2111
- 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>`;
2112
- const items = collapsed ? "" : legend + rows.map(
2113
- (r) => `<div class="uiv-rrow"><span class="uiv-rk">${r.k}</span><span class="uiv-rv${r.cls}">${r.v}</span></div>`
2114
- ).join("");
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("");
2115
2251
  return `<div class="uiv-sec">${this.accordionTitle("Current styles")}<div class="uiv-readout">${items}</div></div>`;
2116
2252
  }
2117
- /** Breakpoint scope switcher: shows the PROJECT's breakpoints + the live window one. */
2118
- 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() {
2119
2264
  const sys = this.bpSystem();
2120
- const bps = sys.breakpoints;
2121
- const names = ["base", ...bps.map((b) => b.name)];
2265
+ const names = ["base", ...sys.breakpoints.map((b) => b.name)];
2122
2266
  const frameBp = this.responsive ? activeBreakpoint(this.frameWidth, sys).name : null;
2123
2267
  const winBp = currentBreakpoint(sys).name;
2124
- const liveW = typeof window !== "undefined" ? window.innerWidth : 0;
2125
- 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");
2126
2270
  const chips = names.map((n) => {
2127
2271
  const active = this.responsive ? n === frameBp : n === winBp;
2128
- const px2 = n === "base" ? 0 : bps.find((b) => b.name === n).minWidth;
2129
- 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`);
2130
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;
2131
2284
  const detected = sys.name === "detected" ? "" : " (defaults)";
2132
- 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.`;
2133
- 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();
2134
2293
  }
2135
2294
  /** "Apply changes to": this element, an existing shared class, or a NEW class. */
2136
2295
  targetHtml(st) {
@@ -2165,12 +2324,14 @@ var Uivisor = class {
2165
2324
  return true;
2166
2325
  }
2167
2326
  controlsHtml(ctx) {
2168
- return SECTIONS.map((sec) => {
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>`;
2328
+ const secs = SECTIONS.map((sec) => {
2169
2329
  const controls = sec.controls.filter((c) => this.relevant(c, ctx));
2170
2330
  if (!controls.length) return "";
2171
2331
  const rows = this.collapsedSecs.has(sec.title) ? "" : controls.map((c) => this.controlRow(c)).join("");
2172
2332
  return `<div class="uiv-sec">${this.accordionTitle(sec.title)}${rows}</div>`;
2173
2333
  }).join("");
2334
+ return legend + secs;
2174
2335
  }
2175
2336
  /** A collapsible section header. Clicking it hides/shows the section's controls. */
2176
2337
  accordionTitle(title) {
@@ -2245,7 +2406,7 @@ var Uivisor = class {
2245
2406
  const d = this.dimDisplay(c);
2246
2407
  const changed = this.isChanged([c.css]);
2247
2408
  const units = c.units.map((u) => `<option value="${u}"${u === d.unit ? " selected" : ""}>${UNIT_LABELS[u] ?? u}</option>`).join("");
2248
- return `<div class="uiv-ctl"><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>`;
2249
2410
  }
2250
2411
  /** A design-token picker row for a property, shown only when the project exposes
2251
2412
  * tokens for that category. Picking a token applies its value + tags the prompt. */
@@ -2279,7 +2440,7 @@ var Uivisor = class {
2279
2440
  const info = this.numInfo(cssList);
2280
2441
  const changed = this.isChanged(cssList);
2281
2442
  const open = this.expanded.has(c.key);
2282
- let html = `<div class="uiv-ctl"><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>`;
2283
2444
  if (open) {
2284
2445
  html += `<div class="uiv-sides">` + c.sides.map((s) => {
2285
2446
  const v = this.liveNum(s.css);
@@ -2297,7 +2458,7 @@ var Uivisor = class {
2297
2458
  }
2298
2459
  if (c.kind === "len") {
2299
2460
  const v = this.liveNum(c.css);
2300
- return `<div class="uiv-ctl"><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");
2301
2462
  }
2302
2463
  if (c.kind === "dim") {
2303
2464
  return this.dimField(c);
@@ -2306,10 +2467,10 @@ var Uivisor = class {
2306
2467
  const cur = this.selectCurrent(c.css);
2307
2468
  const optList = cur && !c.options.includes(cur) ? [cur, ...c.options] : c.options;
2308
2469
  const opts = optList.map((o) => `<option value="${o}"${o === cur ? " selected" : ""}>${o}</option>`).join("");
2309
- return `<div class="uiv-ctl"><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>`;
2310
2471
  }
2311
2472
  const val = toHexInput(this.liveVal(c.css));
2312
- return `<div class="uiv-ctl"><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");
2313
2474
  }
2314
2475
  bindControls() {
2315
2476
  const root = this.root;
@@ -2410,6 +2571,7 @@ var Uivisor = class {
2410
2571
  this.toggleResponsive(true);
2411
2572
  } else {
2412
2573
  this.setFrameWidth(w);
2574
+ this.reapplyScope();
2413
2575
  this.renderBody();
2414
2576
  }
2415
2577
  });
@@ -2607,13 +2769,9 @@ var Uivisor = class {
2607
2769
  for (const ent of snap.entries) {
2608
2770
  const st = { record: clone(ent.record), original: { ...ent.original }, applied: /* @__PURE__ */ new Set(), dimUnit: { ...ent.dimUnit } };
2609
2771
  this.states.set(ent.el, st);
2610
- const targets = st.record.target === "all" ? this.siblingsOf(ent.el) : [ent.el];
2611
- for (const c of st.record.changes) {
2612
- for (const e of targets) applyOverride(e, c.property, c.live ?? c.after.computed);
2613
- st.applied.add(c.property);
2614
- }
2615
2772
  }
2616
2773
  this.selected = snap.selected && this.states.has(snap.selected) ? snap.selected : null;
2774
+ this.reapplyScope();
2617
2775
  this.reposition();
2618
2776
  this.renderBody();
2619
2777
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uivisor",
3
- "version": "0.1.6",
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",