uivisor 0.1.10 → 0.2.1

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 +255 -48
  2. package/package.json +1 -1
@@ -411,7 +411,8 @@ var ICONS = {
411
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
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
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"/>')
414
+ all: sv('<path d="M8 2.5v11 M3.2 5.2l9.6 5.6 M12.8 5.2l-9.6 5.6"/>'),
415
+ grip: '<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor"><circle cx="6" cy="4" r="1.05"/><circle cx="6" cy="8" r="1.05"/><circle cx="6" cy="12" r="1.05"/><circle cx="10" cy="4" r="1.05"/><circle cx="10" cy="8" r="1.05"/><circle cx="10" cy="12" r="1.05"/></svg>'
415
416
  };
416
417
  var SECTIONS = [
417
418
  {
@@ -942,8 +943,9 @@ function renderPrompt(records) {
942
943
  }
943
944
  if (c.after.designToken || c.after.token && /^(text|bg|border|rounded|shadow)-(?!\[)/.test(c.after.token))
944
945
  anyDesignToken = true;
946
+ const from = (c.before.computed ?? "").trim();
945
947
  lines.push(
946
- ` - ${c.property}: ${c.before.computed} \u2192 ${c.after.computed}${suggestion}`
948
+ from ? ` - ${c.property}: ${from} \u2192 ${c.after.computed}${suggestion}` : ` - ${c.property}: set to ${c.after.computed}${suggestion}`
947
949
  );
948
950
  }
949
951
  }
@@ -1189,24 +1191,34 @@ var CSS = (
1189
1191
 
1190
1192
  .uiv-panel {
1191
1193
  position: fixed; right: 16px; bottom: 72px; z-index: 2147483647;
1192
- width: 328px; max-height: 80vh; overflow: auto;
1193
- background: #18181b; color: #e4e4e7; border: 1px solid #3f3f46;
1194
- border-radius: 12px; box-shadow: 0 12px 40px rgba(0,0,0,0.45);
1194
+ width: 360px; max-height: 84vh; overflow: auto;
1195
+ background: #141416; color: #e4e4e7; border: 1px solid #2a2a2e;
1196
+ border-radius: 14px; box-shadow: 0 16px 48px rgba(0,0,0,0.5);
1195
1197
  font-size: 12px; display: none;
1196
1198
  }
1197
1199
  .uiv-panel.show { display: block; }
1198
1200
 
1199
1201
  .uiv-head {
1200
- display: flex; align-items: center; gap: 8px; padding: 10px 12px;
1201
- border-bottom: 1px solid #27272a; position: sticky; top: 0; background: #18181b;
1202
+ display: flex; align-items: center; gap: 8px; padding: 11px 13px;
1203
+ border-bottom: 1px solid #242428; position: sticky; top: 0; z-index: 5; background: #141416;
1202
1204
  }
1203
- .uiv-head b { font-size: 13px; color: #fff; letter-spacing: .3px; }
1204
- .uiv-bp { margin-left: auto; font-size: 10px; padding: 2px 7px; border-radius: 999px;
1205
+ .uiv-head b { font-size: 13px; color: #fafafa; letter-spacing: .2px; }
1206
+ .uiv-bp { margin-left: auto; font-size: 10px; padding: 2px 8px; border-radius: 999px;
1205
1207
  background: #312e81; color: #c7d2fe; font-weight: 600; }
1206
1208
  .uiv-x { cursor: pointer; color: #71717a; padding: 2px 4px; }
1207
1209
  .uiv-x:hover { color: #fff; }
1208
1210
 
1209
- .uiv-sec { padding: 10px 12px; border-bottom: 1px solid #27272a; }
1211
+ .uiv-sec { padding: 11px 13px; border-bottom: 1px solid #1f1f22; }
1212
+
1213
+ /* ---- Framer-style top alignment toolbar ---- */
1214
+ .uiv-toolbar { display: flex; align-items: center; gap: 8px; padding: 8px 13px;
1215
+ border-bottom: 1px solid #1f1f22; }
1216
+ .uiv-tgroup { display: flex; gap: 2px; }
1217
+ .uiv-tbtn { display: flex; align-items: center; justify-content: center; width: 26px; height: 22px;
1218
+ border-radius: 5px; background: transparent; border: 1px solid transparent; color: #8b8b94; cursor: pointer; }
1219
+ .uiv-tbtn:hover { background: #1f1f23; color: #e4e4e7; }
1220
+ .uiv-tbtn.on { background: #312e81; border-color: #4f46e5; color: #fff; }
1221
+ .uiv-tsep { width: 1px; height: 16px; background: #2a2a2e; }
1210
1222
  .uiv-empty { color: #71717a; padding: 18px 12px; text-align: center; }
1211
1223
 
1212
1224
  .uiv-meta { line-height: 1.5; }
@@ -1318,8 +1330,8 @@ var CSS = (
1318
1330
  .uiv-ctl > .cfield { min-width: 0; }
1319
1331
 
1320
1332
  /* numeric field with a scrub handle on the left */
1321
- .uiv-num { display: flex; align-items: stretch; background: #27272a;
1322
- border: 1px solid #3f3f46; border-radius: 7px; overflow: hidden; }
1333
+ .uiv-num { display: flex; align-items: stretch; background: #1c1c20;
1334
+ border: 1px solid #313138; border-radius: 7px; overflow: hidden; }
1323
1335
  .uiv-num.changed { border-color: #4ade80; }
1324
1336
  .uiv-num.changed input { color: #4ade80; } /* uivisor-edited value \u2192 green */
1325
1337
  .uiv-sel.changed, .uiv-color.changed { border-color: #4ade80; }
@@ -1344,7 +1356,7 @@ var CSS = (
1344
1356
 
1345
1357
  .uiv-expand { display: flex; align-items: center; justify-content: center;
1346
1358
  width: 26px; height: 28px; border-radius: 7px; cursor: pointer;
1347
- background: #27272a; border: 1px solid #3f3f46; color: #8b8b94; }
1359
+ background: #1c1c20; border: 1px solid #313138; color: #8b8b94; }
1348
1360
  .uiv-expand:hover { color: #fff; background: #3f3f46; }
1349
1361
  .uiv-expand.on { color: #c7d2fe; border-color: #4f46e5; background: #312e81; }
1350
1362
 
@@ -1353,13 +1365,13 @@ var CSS = (
1353
1365
 
1354
1366
  /* Weight dropdown only \u2014 must NOT match the unit <select> inside dim fields. */
1355
1367
  .uiv-ctl select.uiv-sel {
1356
- width: 100%; background: #27272a; border: 1px solid #3f3f46; color: #fff;
1368
+ width: 100%; background: #1c1c20; border: 1px solid #313138; color: #fff;
1357
1369
  border-radius: 7px; padding: 6px 7px; font-size: 12px; outline: none;
1358
1370
  }
1359
1371
  .uiv-ctl select.uiv-sel:focus { border-color: #6366f1; }
1360
1372
  .uiv-ctl input[type=color] { width: 100%; height: 28px; padding: 2px; cursor: pointer;
1361
- background: #27272a; border: 1px solid #3f3f46; border-radius: 7px; }
1362
- .uiv-ctl input.uiv-text { width: 100%; background: #27272a; border: 1px solid #3f3f46;
1373
+ background: #1c1c20; border: 1px solid #313138; border-radius: 7px; }
1374
+ .uiv-ctl input.uiv-text { width: 100%; background: #1c1c20; border: 1px solid #313138;
1363
1375
  color: #fff; border-radius: 7px; padding: 6px 7px; font-size: 12px; outline: none; }
1364
1376
  .uiv-ctl input.uiv-text:focus { border-color: #6366f1; }
1365
1377
  .uiv-ctl input.uiv-text.changed { border-color: #4ade80; color: #4ade80; }
@@ -1383,7 +1395,7 @@ var CSS = (
1383
1395
  .uiv-btn.primary:hover { background: #4338ca; }
1384
1396
  .uiv-btn.ghost { flex: 0 0 auto; }
1385
1397
  /* floating read-only "all styles" block, docked bottom-right, left of the panel */
1386
- .uiv-info { position: fixed; right: 352px; bottom: 16px; z-index: 2147483646;
1398
+ .uiv-info { position: fixed; right: 384px; bottom: 16px; z-index: 2147483646;
1387
1399
  width: 216px; max-height: 52vh; overflow: auto; display: none;
1388
1400
  background: rgba(24,24,27,0.86); color: #e4e4e7;
1389
1401
  border: 1px solid #3f3f46; border-radius: 10px; padding: 8px 10px;
@@ -1392,6 +1404,47 @@ var CSS = (
1392
1404
  .uiv-info-h { font-size: 10px; text-transform: uppercase; letter-spacing: .4px;
1393
1405
  color: #8b8b94; font-weight: 600; margin-bottom: 6px; }
1394
1406
  .uiv-info-sub { color: #52525b; }
1407
+ /* ---- box-model widget (nested margin / padding, Figma/Framer style) ----
1408
+ Bands are sized so the side inputs NEVER overlap the inner content box:
1409
+ vertical band 26px (> input 18px), horizontal band 46px (> input 36px). */
1410
+ .uiv-bm { position: relative; height: 148px; margin: 2px 0 9px;
1411
+ background: #1b1b1f; border: 1px solid #2f2f35; border-radius: 9px; }
1412
+ .uiv-bm-pad { position: absolute; top: 26px; bottom: 26px; left: 46px; right: 46px;
1413
+ background: #26262c; border: 1px solid #3a3a42; border-radius: 7px; }
1414
+ .uiv-bm-content { position: absolute; z-index: 0; top: 26px; bottom: 26px; left: 46px; right: 46px;
1415
+ background: #34343c; border-radius: 5px; }
1416
+ .uiv-bm-tag { position: absolute; z-index: 1; top: 4px; left: 8px; font-size: 7.5px; font-weight: 700;
1417
+ letter-spacing: .5px; color: #6b6b73; pointer-events: none; }
1418
+ .uiv-bm-i { position: absolute; z-index: 3; width: 36px; height: 18px; padding: 0; box-sizing: border-box;
1419
+ background: #0e0e11; border: 1px solid #34343c; border-radius: 5px;
1420
+ color: #d4d4d8; text-align: center; text-align-last: center; line-height: 16px; font-size: 10px;
1421
+ outline: none; cursor: ew-resize; font-family: ui-monospace, monospace; -moz-appearance: textfield; }
1422
+ .uiv-bm-i::-webkit-outer-spin-button, .uiv-bm-i::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
1423
+ .uiv-bm-i:hover { border-color: #52525b; }
1424
+ .uiv-bm-i:focus { cursor: text; border-color: #6366f1; }
1425
+ .uiv-bm-i.bm-top { top: 4px; left: 50%; transform: translateX(-50%); }
1426
+ .uiv-bm-i.bm-bottom { bottom: 4px; left: 50%; transform: translateX(-50%); }
1427
+ .uiv-bm-i.bm-left { left: 5px; top: 50%; transform: translateY(-50%); }
1428
+ .uiv-bm-i.bm-right { right: 5px; top: 50%; transform: translateY(-50%); }
1429
+ .uiv-bm-i.st-file { color: #e4e4e7; }
1430
+ .uiv-bm-i.st-edited { color: #4ade80; }
1431
+ .uiv-bm-i.st-inherit { color: #38bdf8; }
1432
+ .uiv-bm-i.st-auto { color: #6b7280; }
1433
+ /* a side bound to a design token \u2014 shown by name, accent-coloured, no spin */
1434
+ .uiv-bm-i.uiv-bm-tok { color: #a5b4fc; font-size: 9px; letter-spacing: -0.2px;
1435
+ border-color: #4338ca; background: #1e1b4b40; text-overflow: ellipsis; }
1436
+ .uiv-bm-i.uiv-bm-tok.st-edited { color: #818cf8; }
1437
+ /* spacing-token dropdown \u2014 opens when a side value is focused/clicked */
1438
+ .uiv-bm-pop { display: flex; flex-wrap: wrap; gap: 5px; align-items: center;
1439
+ margin: -3px 0 8px; padding: 8px; background: #1c1c20; border: 1px solid #34343c;
1440
+ border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.4); }
1441
+ .uiv-bm-pop[hidden] { display: none; }
1442
+ .uiv-bm-poplabel { width: 100%; font-size: 9px; font-weight: 700; text-transform: uppercase;
1443
+ letter-spacing: .4px; color: #6b6b73; font-family: ui-monospace, monospace; }
1444
+ .uiv-bm-chip { background: #1e1b4b55; border: 1px solid #4338ca; color: #c7d2fe; border-radius: 6px;
1445
+ padding: 3px 8px; font-size: 10px; cursor: pointer; font-family: ui-monospace, monospace; }
1446
+ .uiv-bm-chip:hover { background: #4f46e5; border-color: #6366f1; color: #fff; }
1447
+
1395
1448
  .uiv-toast { position: fixed; right: 16px; bottom: 128px; z-index: 2147483647;
1396
1449
  background: #22c55e; color: #052e16; padding: 8px 12px; border-radius: 8px;
1397
1450
  font-size: 12px; font-weight: 600; display: none; }
@@ -1422,6 +1475,8 @@ var Uivisor = class {
1422
1475
  this.collapsedSecs = /* @__PURE__ */ new Set();
1423
1476
  /** Controls manually revealed via "+" (hideWhenAuto controls that are auto). */
1424
1477
  this.revealedCtls = /* @__PURE__ */ new Set();
1478
+ /** Box-model widget: the last-focused side the token dropdown targets. */
1479
+ this.lastBmSide = "padding-top";
1425
1480
  /** Undo / redo stacks of full edit-state snapshots. */
1426
1481
  this.undoStack = [];
1427
1482
  this.redoStack = [];
@@ -1439,6 +1494,11 @@ var Uivisor = class {
1439
1494
  // responsive (virtual screen) mode
1440
1495
  this.responsive = false;
1441
1496
  this.frameWidth = 768;
1497
+ /** The breakpoint new edits are scoped to — chosen explicitly via the chips,
1498
+ * NOT derived from the frame's pixel width. Defaults to 'base' so a casual tweak
1499
+ * applies to EVERY size (and never "disappears" when you view another size).
1500
+ * Picking a specific breakpoint chip narrows the scope to that breakpoint. */
1501
+ this.pickedBp = "base";
1442
1502
  // ---- pointer handling ----
1443
1503
  this.onMove = (e) => {
1444
1504
  if (!this.enabled || this.isOurs(e)) {
@@ -1651,6 +1711,7 @@ var Uivisor = class {
1651
1711
  } else {
1652
1712
  this.scheduleBpRefresh();
1653
1713
  if (!this.responsive) {
1714
+ this.pickedBp = "base";
1654
1715
  this.frameWidth = this.defaultFrameWidth();
1655
1716
  this.toggleResponsive(true);
1656
1717
  }
@@ -1660,12 +1721,6 @@ var Uivisor = class {
1660
1721
  defaultFrameWidth() {
1661
1722
  return typeof window !== "undefined" ? window.innerWidth : 1280;
1662
1723
  }
1663
- /** Frame width for the "all"/base chip: a phone-ish width in the base range. */
1664
- baseFrameWidth() {
1665
- const bps = this.bpSystem().breakpoints;
1666
- const firstBp = bps.length ? bps[0].minWidth : 640;
1667
- return this.bpSystem().dir === "min" ? Math.min(390, firstBp - 1) : 390;
1668
- }
1669
1724
  /** Stylesheets (esp. JIT/CDN Tailwind) load async — re-detect breakpoints a few
1670
1725
  * times after enabling and re-render only if the set actually changed. */
1671
1726
  scheduleBpRefresh() {
@@ -1760,8 +1815,7 @@ var Uivisor = class {
1760
1815
  this.frameWidth = Math.max(280, Math.min(2400, Math.round(w)));
1761
1816
  const host = this.q(".uiv-framehost");
1762
1817
  host.style.width = `${this.frameWidth}px`;
1763
- const bp = activeBreakpoint(this.frameWidth, this.bpSystem()).name;
1764
- this.q(".uiv-framew").textContent = `${this.frameWidth}px \xB7 ${this.bpLabel(bp)}`;
1818
+ this.q(".uiv-framew").textContent = `${this.frameWidth}px \xB7 editing ${this.bpLabel(this.scopeName())}`;
1765
1819
  this.updateBp();
1766
1820
  this.reposition();
1767
1821
  }
@@ -1810,7 +1864,9 @@ var Uivisor = class {
1810
1864
  };
1811
1865
  this.states.set(el, {
1812
1866
  record,
1813
- original: snapshot(el, ALL_CSS),
1867
+ // Snapshot every registry property (not just the curated set) so a generic
1868
+ // edit's "before" value is known — the All-CSS inspector edits anything.
1869
+ original: snapshot(el, this.snapshotProps()),
1814
1870
  applied: /* @__PURE__ */ new Set(),
1815
1871
  dimUnit: {}
1816
1872
  });
@@ -1819,6 +1875,10 @@ var Uivisor = class {
1819
1875
  this.reposition();
1820
1876
  this.renderBody();
1821
1877
  }
1878
+ /** Properties to snapshot on selection — the curated set the controls can edit. */
1879
+ snapshotProps() {
1880
+ return ALL_CSS;
1881
+ }
1822
1882
  // ---- value helpers ----
1823
1883
  st() {
1824
1884
  return this.selected ? this.states.get(this.selected) ?? null : null;
@@ -1851,6 +1911,14 @@ var Uivisor = class {
1851
1911
  if (inline && !inline.includes("var(")) return inline;
1852
1912
  return this.computedVal(css) || st.original[css] || "";
1853
1913
  }
1914
+ /** If a design token is the effective value for `css`, its short name (e.g. "lg"
1915
+ * for --space-lg) — so the field shows the token by name, not its resolved px. */
1916
+ tokenNameFor(css) {
1917
+ const ch = this.effectiveChange(css);
1918
+ if (!ch?.after.designToken) return null;
1919
+ const t = this.designSystem().tokens.find((x) => x.cssVar === ch.after.designToken);
1920
+ return t ? t.name : ch.after.designToken;
1921
+ }
1854
1922
  liveNum(css) {
1855
1923
  const v = this.liveVal(css).trim();
1856
1924
  const m = /^(-?\d*\.?\d+)px$/.exec(v);
@@ -1876,8 +1944,10 @@ var Uivisor = class {
1876
1944
  }
1877
1945
  selectCurrent(css) {
1878
1946
  let v = this.liveVal(css).trim();
1879
- if (v === "normal") v = "400";
1880
- if (v === "bold") v = "700";
1947
+ if (css === "font-weight") {
1948
+ if (v === "normal") v = "400";
1949
+ if (v === "bold") v = "700";
1950
+ }
1881
1951
  return v;
1882
1952
  }
1883
1953
  // ---- apply / record / revert ----
@@ -1950,18 +2020,37 @@ var Uivisor = class {
1950
2020
  }
1951
2021
  this.reposition();
1952
2022
  }
1953
- /** The breakpoint recorded edits are scoped to: manual override, else window. */
1954
- /** The breakpoint edits are scoped to: the virtual screen's width when in
1955
- * responsive mode, otherwise the real window. */
2023
+ /** The breakpoint new edits are scoped to. In responsive mode it's the EXPLICITLY
2024
+ * picked breakpoint (default 'base' = every size), decoupled from the frame's
2025
+ * pixel width so editing at a desktop-sized frame still defaults to "all sizes"
2026
+ * instead of silently scoping to the top breakpoint and vanishing elsewhere. */
1956
2027
  activeScope() {
1957
2028
  const sys = this.bpSystem();
1958
- if (this.responsive) return activeBreakpoint(this.frameWidth, sys);
1959
- return currentBreakpoint(sys);
1960
- }
1961
- /** The width the inspector is scoped to (virtual screen, else real window). */
2029
+ if (!this.responsive) return currentBreakpoint(sys);
2030
+ const name = this.scopeName();
2031
+ const bp = sys.breakpoints.find((b) => b.name === name);
2032
+ return { name, minWidth: bp ? bp.minWidth : 0 };
2033
+ }
2034
+ /** The picked breakpoint, validated against the current system (falls back to
2035
+ * 'base' if a previously-picked breakpoint no longer exists after re-detection). */
2036
+ scopeName() {
2037
+ if (this.pickedBp === "base") return "base";
2038
+ return this.bpSystem().breakpoints.some((b) => b.name === this.pickedBp) ? this.pickedBp : "base";
2039
+ }
2040
+ /** The width the cascade is evaluated at — the representative width of the PICKED
2041
+ * scope, not the frame's visual width. base → a width where only base applies (so
2042
+ * base edits win and show everywhere); a named breakpoint → its threshold. */
1962
2043
  activeWidth() {
1963
- if (this.responsive) return this.frameWidth;
1964
- return typeof window !== "undefined" ? window.innerWidth : 0;
2044
+ if (!this.responsive) return typeof window !== "undefined" ? window.innerWidth : 0;
2045
+ return this.scopeWidth(this.scopeName());
2046
+ }
2047
+ scopeWidth(name) {
2048
+ const sys = this.bpSystem();
2049
+ if (name !== "base") {
2050
+ const bp = sys.breakpoints.find((b) => b.name === name);
2051
+ if (bp) return bp.minWidth;
2052
+ }
2053
+ return sys.dir === "min" ? 0 : 1e6;
1965
2054
  }
1966
2055
  /** The recorded change that wins the breakpoint cascade for `css` at the active
1967
2056
  * width — i.e. the value effective here, set on this breakpoint or inherited. */
@@ -2021,13 +2110,13 @@ var Uivisor = class {
2021
2110
  const el = this.selected;
2022
2111
  const st = this.st();
2023
2112
  if (!el || !st) return;
2024
- const sibs = this.siblingsOf(el);
2113
+ const scope = this.activeScope();
2025
2114
  for (const css of cssList) {
2026
- for (const e of sibs) removeOverride(e, css);
2027
- st.applied.delete(css);
2028
- st.record.changes = st.record.changes.filter((c) => c.property !== css);
2115
+ st.record.changes = st.record.changes.filter(
2116
+ (c) => !(c.property === css && c.breakpoint === scope.name)
2117
+ );
2029
2118
  }
2030
- this.reposition();
2119
+ this.reapplyScope();
2031
2120
  this.renderBody();
2032
2121
  }
2033
2122
  commitNumeric(cssList, raw) {
@@ -2103,6 +2192,7 @@ var Uivisor = class {
2103
2192
  <div class="uiv-src">${escapeHtml(src)}</div>
2104
2193
  <span class="uiv-mech">${st.record.styling.primaryMechanism}</span>
2105
2194
  </div>
2195
+ ${this.alignToolbarHtml()}
2106
2196
  ${this.dsIndicatorHtml()}
2107
2197
  ${this.breakpointBarHtml()}
2108
2198
  ${this.targetHtml(st)}
@@ -2346,9 +2436,8 @@ var Uivisor = class {
2346
2436
  * project breakpoint. Reused by the panel (Live) and the over-frame bar. */
2347
2437
  breakpointChipsHtml() {
2348
2438
  const sys = this.bpSystem();
2349
- const frameBp = this.responsive ? activeBreakpoint(this.frameWidth, sys).name : null;
2350
2439
  const winBp = currentBreakpoint(sys).name;
2351
- const isActive = (n) => this.responsive ? n === frameBp : n === winBp;
2440
+ const isActive = (n) => this.responsive ? n === this.scopeName() : n === winBp;
2352
2441
  const chip = (n, on, title) => `<button class="uiv-chip${on ? " on" : ""}" data-bp="${n}" title="${escapeAttr(title)}">${this.bpIcon(n)}<span>${this.bpLabel(n)}</span></button>`;
2353
2442
  const all = chip("base", isActive("base"), "No breakpoint \u2014 applies to every size by default");
2354
2443
  const rest = sys.breakpoints.map((b) => chip(b.name, isActive(b.name), sys.dir === "min" ? `\u2265 ${b.minWidth}px` : `\u2264 ${b.minWidth}px`)).join("");
@@ -2403,15 +2492,60 @@ var Uivisor = class {
2403
2492
  if (req === "flexgrid") return ctx.flexGrid;
2404
2493
  return true;
2405
2494
  }
2495
+ /** Framer-style top alignment toolbar — justify (horizontal) + align (vertical)
2496
+ * icon buttons. Shown for flex/grid containers (where they apply). */
2497
+ alignToolbarHtml() {
2498
+ const el = this.selected;
2499
+ if (!el || !this.context(el).flexGrid) return "";
2500
+ const j = this.liveVal("justify-content").trim();
2501
+ const a = this.liveVal("align-items").trim();
2502
+ const g = (r) => `<svg viewBox="0 0 14 14" width="13" height="13" fill="currentColor">${r}</svg>`;
2503
+ const JI = {
2504
+ "flex-start": g('<rect x="1" y="3" width="2" height="8"/><rect x="4" y="3" width="2" height="8"/>'),
2505
+ center: g('<rect x="4" y="3" width="2" height="8"/><rect x="8" y="3" width="2" height="8"/>'),
2506
+ "flex-end": g('<rect x="8" y="3" width="2" height="8"/><rect x="11" y="3" width="2" height="8"/>'),
2507
+ "space-between": g('<rect x="1" y="3" width="2" height="8"/><rect x="11" y="3" width="2" height="8"/>')
2508
+ };
2509
+ const AI = {
2510
+ "flex-start": g('<rect x="3" y="1" width="8" height="2"/><rect x="3" y="4" width="8" height="2"/>'),
2511
+ center: g('<rect x="3" y="4" width="8" height="2"/><rect x="3" y="8" width="8" height="2"/>'),
2512
+ "flex-end": g('<rect x="3" y="8" width="8" height="2"/><rect x="3" y="11" width="8" height="2"/>'),
2513
+ stretch: g('<rect x="3" y="1" width="8" height="12"/>')
2514
+ };
2515
+ const btn = (prop, val, icon, cur) => `<button class="uiv-tbtn${cur === val ? " on" : ""}" data-prop="${prop}" data-val="${val}" title="${prop}: ${val}">${icon}</button>`;
2516
+ const jb = Object.entries(JI).map(([v, ic]) => btn("justify-content", v, ic, j)).join("");
2517
+ const ab = Object.entries(AI).map(([v, ic]) => btn("align-items", v, ic, a)).join("");
2518
+ return `<div class="uiv-toolbar"><div class="uiv-tgroup">${jb}</div><div class="uiv-tsep"></div><div class="uiv-tgroup">${ab}</div></div>`;
2519
+ }
2520
+ /** Figma/Framer-style nested box-model widget: MARGIN ring around a PADDING ring,
2521
+ * with an editable number on each of the 8 sides. Commits via the engine. */
2522
+ boxModelHtml() {
2523
+ const num = (css) => {
2524
+ const n = this.liveNum(css);
2525
+ return n == null ? "0" : String(round2(n));
2526
+ };
2527
+ const side = (css, pos) => {
2528
+ const tok = this.tokenNameFor(css);
2529
+ const val = tok ?? num(css);
2530
+ const title = tok ? `${css}: ${tok}` : css;
2531
+ return `<input class="uiv-bm-i ${pos}${this.controlStateClass([css])}${tok ? " uiv-bm-tok" : ""}" data-css="${css}" value="${escapeAttr(val)}" title="${escapeAttr(title)}" inputmode="decimal" spellcheck="false">`;
2532
+ };
2533
+ const spaceTokens = this.designSystem().byCategory["spacing"] ?? [];
2534
+ const pop = spaceTokens.length ? `<div class="uiv-bm-pop" hidden><span class="uiv-bm-poplabel">Token</span>` + spaceTokens.map((t) => `<button class="uiv-bm-chip" data-var="${escapeAttr(t.cssVar)}" title="${escapeAttr(t.value)}">${escapeHtml(t.name)} \xB7 ${escapeHtml(t.value)}</button>`).join("") + `</div>` : "";
2535
+ return `<div class="uiv-bm"><span class="uiv-bm-tag">MARGIN</span>` + side("margin-top", "bm-top") + side("margin-right", "bm-right") + side("margin-bottom", "bm-bottom") + side("margin-left", "bm-left") + `<div class="uiv-bm-pad"><span class="uiv-bm-tag">PADDING</span>` + side("padding-top", "bm-top") + side("padding-right", "bm-right") + side("padding-bottom", "bm-bottom") + side("padding-left", "bm-left") + `<div class="uiv-bm-content"></div></div></div>` + pop;
2536
+ }
2406
2537
  controlsHtml(ctx) {
2407
2538
  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>`;
2408
2539
  const secs = SECTIONS.map((sec) => {
2409
2540
  const controls = sec.controls.filter((c) => this.relevant(c, ctx));
2410
2541
  if (!controls.length) return "";
2411
2542
  if (this.collapsedSecs.has(sec.title)) return `<div class="uiv-sec">${this.accordionTitle(sec.title)}</div>`;
2543
+ const isSpacing = sec.title === "Spacing";
2544
+ const widget = isSpacing ? this.boxModelHtml() : "";
2412
2545
  const rows = [];
2413
2546
  const adds = [];
2414
2547
  for (const c of controls) {
2548
+ if (isSpacing && c.kind === "box" && (c.key === "padding" || c.key === "margin")) continue;
2415
2549
  const css = c.css;
2416
2550
  if (c.kind === "len" && c.hideWhenAuto && css && !this.revealedCtls.has(css) && this.controlState([css]) === "auto") {
2417
2551
  adds.push(`<button class="uiv-addctl" data-css="${css}">+ ${escapeHtml(c.label)}</button>`);
@@ -2420,7 +2554,7 @@ var Uivisor = class {
2420
2554
  }
2421
2555
  }
2422
2556
  const addRow = adds.length ? `<div class="uiv-adds">${adds.join("")}</div>` : "";
2423
- return `<div class="uiv-sec">${this.accordionTitle(sec.title)}${rows.join("")}${addRow}</div>`;
2557
+ return `<div class="uiv-sec">${this.accordionTitle(sec.title)}${widget}${rows.join("")}${addRow}</div>`;
2424
2558
  }).join("");
2425
2559
  return legend + secs;
2426
2560
  }
@@ -2568,6 +2702,78 @@ var Uivisor = class {
2568
2702
  }
2569
2703
  bindControls() {
2570
2704
  const root = this.root;
2705
+ root.querySelectorAll(".uiv-tbtn").forEach((node) => {
2706
+ const btn = node;
2707
+ const prop = btn.getAttribute("data-prop");
2708
+ const val = btn.getAttribute("data-val");
2709
+ btn.addEventListener("click", () => {
2710
+ this.pushHistory();
2711
+ this.commitValue([prop], val);
2712
+ });
2713
+ });
2714
+ const bmPop = root.querySelector(".uiv-bm-pop");
2715
+ const bmPopLabel = bmPop?.querySelector(".uiv-bm-poplabel");
2716
+ root.querySelectorAll(".uiv-bm-i").forEach((node) => {
2717
+ const inp = node;
2718
+ const css = inp.getAttribute("data-css");
2719
+ inp.addEventListener("focus", () => {
2720
+ this.lastBmSide = css;
2721
+ if (bmPop) {
2722
+ bmPop.hidden = false;
2723
+ if (bmPopLabel) bmPopLabel.textContent = `${css} \u2192`;
2724
+ }
2725
+ });
2726
+ inp.addEventListener("blur", () => {
2727
+ window.setTimeout(() => {
2728
+ if (bmPop) bmPop.hidden = true;
2729
+ }, 140);
2730
+ });
2731
+ inp.addEventListener("change", () => {
2732
+ this.pushHistory();
2733
+ this.commitNumeric([css], inp.value);
2734
+ });
2735
+ inp.addEventListener("keydown", (e) => {
2736
+ if (e.key === "Enter") inp.blur();
2737
+ });
2738
+ inp.addEventListener("pointerdown", (e) => {
2739
+ const startX = e.clientX;
2740
+ const startVal = parseFloat(inp.value) || 0;
2741
+ let moved = false;
2742
+ let pushed = false;
2743
+ const move = (ev) => {
2744
+ if (!moved && Math.abs(ev.clientX - startX) < 3) return;
2745
+ if (!moved) {
2746
+ moved = true;
2747
+ inp.blur();
2748
+ }
2749
+ if (!pushed) {
2750
+ this.pushHistory();
2751
+ pushed = true;
2752
+ }
2753
+ let nv = startVal + Math.round(ev.clientX - startX);
2754
+ if (ev.shiftKey) nv = Math.round(nv / 10) * 10;
2755
+ nv = Math.max(0, nv);
2756
+ inp.value = String(nv);
2757
+ this.liveSet([css], `${nv}px`);
2758
+ };
2759
+ const up = () => {
2760
+ window.removeEventListener("pointermove", move);
2761
+ window.removeEventListener("pointerup", up);
2762
+ if (moved) this.recordProps([css]);
2763
+ };
2764
+ window.addEventListener("pointermove", move);
2765
+ window.addEventListener("pointerup", up);
2766
+ });
2767
+ });
2768
+ root.querySelectorAll(".uiv-bm-chip").forEach((node) => {
2769
+ const btn = node;
2770
+ const cssVar = btn.getAttribute("data-var");
2771
+ btn.addEventListener("mousedown", (e) => {
2772
+ e.preventDefault();
2773
+ const token = this.designSystem().tokens.find((t) => t.cssVar === cssVar);
2774
+ if (token) this.applyToken(this.lastBmSide, token);
2775
+ });
2776
+ });
2571
2777
  root.querySelectorAll(".uiv-num:not(.uiv-dim)").forEach((node) => {
2572
2778
  const box = node;
2573
2779
  const cssList = (box.getAttribute("data-css") || "").split(",").filter(Boolean);
@@ -2602,6 +2808,7 @@ var Uivisor = class {
2602
2808
  const input = node;
2603
2809
  const css = input.getAttribute("data-css");
2604
2810
  input.addEventListener("change", () => {
2811
+ if (input.value.toLowerCase() === toHexInput(this.liveVal(css)).toLowerCase()) return;
2605
2812
  this.pushHistory();
2606
2813
  this.commitValue([css], input.value);
2607
2814
  });
@@ -2662,8 +2869,8 @@ var Uivisor = class {
2662
2869
  const btn = node;
2663
2870
  const bp = btn.getAttribute("data-bp");
2664
2871
  btn.addEventListener("click", () => {
2665
- const w = bp === "base" ? this.baseFrameWidth() : this.bpSystem().breakpoints.find((b) => b.name === bp)?.minWidth ?? 768;
2666
- this.setFrameWidth(w);
2872
+ this.pickedBp = bp;
2873
+ this.setFrameWidth(bp === "base" ? this.defaultFrameWidth() : this.scopeWidth(bp));
2667
2874
  this.reapplyScope();
2668
2875
  this.renderBody();
2669
2876
  });
@@ -2955,7 +3162,7 @@ function escapeHtml(s) {
2955
3162
  return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
2956
3163
  }
2957
3164
  function escapeAttr(s) {
2958
- return s.replace(/"/g, "&quot;");
3165
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
2959
3166
  }
2960
3167
  function cssAttrEscape(s) {
2961
3168
  return s.replace(/["\\]/g, "\\$&");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uivisor",
3
- "version": "0.1.10",
3
+ "version": "0.2.1",
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",