uivisor 0.1.4 → 0.1.6

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 +472 -44
  2. package/package.json +1 -1
@@ -542,6 +542,155 @@ var SHORTHAND_SETS = [
542
542
  }
543
543
  ];
544
544
 
545
+ // src/overlay/designtokens.ts
546
+ var COLOR_RE = /^(#[0-9a-f]{3,8}|rgba?\(|hsla?\()/i;
547
+ var LEN_RE = /^-?\d*\.?\d+(px|rem|em)$/i;
548
+ var SHADOW_RE = /\d+px.*(rgba?\(|hsla?\(|#[0-9a-f]{3,8})/i;
549
+ var FONT_STACK_RE = /(sans-serif|serif|monospace|system-ui|,)/i;
550
+ var isColor = (v) => COLOR_RE.test(v.trim());
551
+ var isLen = (v) => LEN_RE.test(v.trim());
552
+ function lenToPx(value, rootPx = 16) {
553
+ const m = /^(-?\d*\.?\d+)(px|rem|em)$/i.exec(value.trim());
554
+ if (!m) return null;
555
+ const n = parseFloat(m[1]);
556
+ const unit = m[2].toLowerCase();
557
+ if (unit === "px") return n;
558
+ return n * rootPx;
559
+ }
560
+ var PREFIXES = [
561
+ { re: /^--font-size-/, cat: "font-size" },
562
+ { re: /^--fs-/, cat: "font-size" },
563
+ { re: /^--text-/, cat: "font-size" },
564
+ // only when value is a length (handled below)
565
+ { re: /^--leading-/, cat: "line-height" },
566
+ { re: /^--line-height-/, cat: "line-height" },
567
+ { re: /^--tracking-/, cat: "letter-spacing" },
568
+ { re: /^--letter-spacing-/, cat: "letter-spacing" },
569
+ { re: /^--radius-/, cat: "radius" },
570
+ { re: /^--rounded-/, cat: "radius" },
571
+ { re: /^--shadow-/, cat: "shadow" },
572
+ { re: /^--space-/, cat: "spacing" },
573
+ { re: /^--spacing-/, cat: "spacing" },
574
+ { re: /^--gap-/, cat: "spacing" },
575
+ { re: /^--airy-/, cat: "spacing" },
576
+ { re: /^--color-/, cat: "color" },
577
+ { re: /^--bg-/, cat: "color" },
578
+ { re: /^--border-/, cat: "color" },
579
+ { re: /^--font-/, cat: "font-family" }
580
+ ];
581
+ var FONTSIZE_HINT = /(^|[-_])(fs|font-size|text|headline|title|body|label|display|caption|heading)([-_]|$)/i;
582
+ var SPACING_HINT = /(^|[-_])(space|spacing|gap|airy|inset)([-_]|$)/i;
583
+ function classifyVar(name, value) {
584
+ const v = value.trim();
585
+ const n = name.toLowerCase();
586
+ if (!v) return null;
587
+ if (n.includes("shadow") || SHADOW_RE.test(v)) return "shadow";
588
+ if (isColor(v)) return "color";
589
+ if (FONT_STACK_RE.test(v) && !isLen(v)) {
590
+ if (n.includes("family") || /^--font(-|$)/.test(n) || FONT_STACK_RE.test(v)) return "font-family";
591
+ }
592
+ if (isLen(v)) {
593
+ if (n.includes("radius") || n.includes("rounded")) return "radius";
594
+ if (n.includes("leading") || n.includes("line-height")) return "line-height";
595
+ if (n.includes("tracking") || n.includes("letter")) return "letter-spacing";
596
+ if (FONTSIZE_HINT.test(n)) return "font-size";
597
+ if (SPACING_HINT.test(n)) return "spacing";
598
+ return null;
599
+ }
600
+ return null;
601
+ }
602
+ function tokenName(cssVar, _category) {
603
+ for (const p of PREFIXES) {
604
+ if (p.re.test(cssVar)) return cssVar.replace(p.re, "");
605
+ }
606
+ return cssVar.replace(/^--/, "");
607
+ }
608
+ function buildDesignSystem(vars, rootPx = 16) {
609
+ const tokens = [];
610
+ for (const [cssVar, value] of Object.entries(vars)) {
611
+ const category = classifyVar(cssVar, value);
612
+ if (!category) continue;
613
+ const scalar = category === "font-size" || category === "radius" || category === "spacing";
614
+ tokens.push({
615
+ cssVar,
616
+ name: tokenName(cssVar, category),
617
+ category,
618
+ value: value.trim(),
619
+ px: scalar ? lenToPx(value, rootPx) : null
620
+ });
621
+ }
622
+ const byCategory = {};
623
+ for (const t of tokens) (byCategory[t.category] ||= []).push(t);
624
+ for (const cat of Object.keys(byCategory)) {
625
+ byCategory[cat].sort(
626
+ (a, b) => a.px != null && b.px != null ? a.px - b.px : a.name.localeCompare(b.name)
627
+ );
628
+ }
629
+ return { tokens, byCategory, source: tokens.length ? "css-vars" : "none" };
630
+ }
631
+ function nearestToken(ds, category, target) {
632
+ const list = ds.byCategory[category];
633
+ if (!list || !list.length) return null;
634
+ if (target.value != null) {
635
+ const norm = (s) => {
636
+ const t = s.trim();
637
+ return (rgbToHex(t) || t).replace(/\s+/g, "").toLowerCase();
638
+ };
639
+ const want = norm(target.value);
640
+ const hit = list.find((t) => norm(t.value) === want);
641
+ if (hit) return { token: hit, exact: true };
642
+ }
643
+ if (target.px != null && list.some((t) => t.px != null)) {
644
+ let best = list[0];
645
+ let bestD = Infinity;
646
+ for (const t of list) {
647
+ if (t.px == null) continue;
648
+ const d = Math.abs(t.px - target.px);
649
+ if (d < bestD) {
650
+ bestD = d;
651
+ best = t;
652
+ }
653
+ }
654
+ return { token: best, exact: bestD < 0.5 };
655
+ }
656
+ return null;
657
+ }
658
+ var SCALAR_VAR_SELECTOR = /(^|[\s,>])(:root|html|\[data-theme)/i;
659
+ function detectDesignSystem() {
660
+ if (typeof document === "undefined") return { tokens: [], byCategory: {}, source: "none" };
661
+ const names = /* @__PURE__ */ new Set();
662
+ for (const sheet of Array.from(document.styleSheets)) {
663
+ let rules;
664
+ try {
665
+ rules = sheet.cssRules;
666
+ } catch {
667
+ continue;
668
+ }
669
+ for (const rule of Array.from(rules)) collectVarNames(rule, names);
670
+ }
671
+ const root = document.documentElement;
672
+ const cs = getComputedStyle(root);
673
+ const rootPx = parseFloat(cs.fontSize) || 16;
674
+ const vars = {};
675
+ for (const name of names) {
676
+ const resolved = cs.getPropertyValue(name).trim();
677
+ if (resolved) vars[name] = resolved;
678
+ }
679
+ return buildDesignSystem(vars, rootPx);
680
+ }
681
+ function collectVarNames(rule, out) {
682
+ const style = rule.style;
683
+ const selector = rule.selectorText;
684
+ if (style && selector && SCALAR_VAR_SELECTOR.test(selector)) {
685
+ for (let i = 0; i < style.length; i++) {
686
+ const prop = style[i];
687
+ if (prop.startsWith("--")) out.add(prop);
688
+ }
689
+ }
690
+ const inner = rule.cssRules;
691
+ if (inner) for (const r of Array.from(inner)) collectVarNames(r, out);
692
+ }
693
+
545
694
  // src/overlay/mechanism.ts
546
695
  var TW_UTILITY = /^-?(?:p|m|px|py|pt|pr|pb|pl|mx|my|mt|mr|mb|ml|gap|w|h|min-w|max-w|min-h|max-h|text|bg|border|rounded|flex|grid|gap-x|gap-y|items|justify|self|place|font|leading|tracking|space|inline|block|inline-block|hidden|table|absolute|relative|fixed|sticky|static|top|bottom|left|right|inset|z|shadow|opacity|transition|duration|ease|scale|rotate|translate|cursor|overflow|object|aspect|order|col|row|basis|grow|shrink|divide|ring|outline)(?:-|$)/;
547
696
  var TW_VARIANT = /^(?:sm|md|lg|xl|2xl|hover|focus|focus-visible|focus-within|active|visited|disabled|dark|group|group-hover|peer|first|last|odd|even|motion-safe|motion-reduce|print|rtl|ltr)::?|^(?:sm|md|lg|xl|2xl|hover|focus|active|disabled|dark|group-hover):/;
@@ -667,6 +816,7 @@ function renderPrompt(records) {
667
816
  lines.push("");
668
817
  let anyClassTarget = false;
669
818
  let anyNewClass = false;
819
+ let anyDesignToken = false;
670
820
  active.forEach((r, i) => {
671
821
  const id = r.identity;
672
822
  const newClass = r.target?.startsWith("new:") ? r.target.slice(4) : null;
@@ -718,7 +868,15 @@ function renderPrompt(records) {
718
868
  const scope = bp === "base" ? "all sizes (base)" : `${bp} breakpoint (\u2265${byBp[bp][0].breakpointPx}px)`;
719
869
  lines.push(`- At ${scope}:`);
720
870
  for (const c of byBp[bp]) {
721
- const suggestion = c.after.token ? ` \u2192 \`${variant(bp, c.after.token)}\`` : "";
871
+ let suggestion = "";
872
+ if (c.after.token) {
873
+ suggestion = ` \u2192 \`${variant(bp, c.after.token)}\``;
874
+ } else if (c.after.designToken) {
875
+ const at = bp === "base" ? "" : ` (scope to ${bp})`;
876
+ suggestion = ` \u2192 use design token \`${c.after.designToken}\` (\`var(${c.after.designToken})\`)${at}`;
877
+ }
878
+ if (c.after.designToken || c.after.token && /^(text|bg|border|rounded|shadow)-(?!\[)/.test(c.after.token))
879
+ anyDesignToken = true;
722
880
  lines.push(
723
881
  ` - ${c.property}: ${c.before.computed} \u2192 ${c.after.computed}${suggestion}`
724
882
  );
@@ -747,6 +905,11 @@ function renderPrompt(records) {
747
905
  "- When creating a new class, leave the current classes untouched and add the new class alongside them; pick the styling mechanism the project already uses."
748
906
  );
749
907
  }
908
+ if (anyDesignToken) {
909
+ lines.push(
910
+ "- Prefer the project DESIGN TOKEN shown for each change (a Tailwind utility like `text-headline-l`, or the CSS variable `var(--\u2026)`) over a hardcoded px/hex value \u2014 these come from the project\u2019s own design system."
911
+ );
912
+ }
750
913
  return lines.join("\n");
751
914
  }
752
915
  function renderSpec(records, meta) {
@@ -1002,6 +1165,25 @@ var CSS = (
1002
1165
  .uiv-newclass:focus { border-style: solid; border-color: #6366f1; color: #fff; }
1003
1166
  .uiv-newclass.on { border-style: solid; border-color: #22d3ee; color: #fff; }
1004
1167
 
1168
+ /* ---- design-system indicator + token pickers ---- */
1169
+ .uiv-dsbar { padding: 7px 12px; border-bottom: 1px solid #27272a;
1170
+ font-size: 10px; font-weight: 600; letter-spacing: .3px; color: #a5b4fc;
1171
+ background: #1e1b4b33; display: flex; align-items: center; gap: 6px; }
1172
+ .uiv-tlabel { color: #818cf8 !important; font-size: 10px; }
1173
+ .uiv-ctl select.uiv-tokensel {
1174
+ width: 100%; background: #1e1b4b55; border: 1px solid #4338ca; color: #c7d2fe;
1175
+ border-radius: 7px; padding: 6px 7px; font-size: 11px; outline: none;
1176
+ font-family: ui-monospace, monospace; }
1177
+ .uiv-ctl select.uiv-tokensel:hover { border-color: #6366f1; color: #fff; }
1178
+ .uiv-ctl select.uiv-tokensel:focus { border-color: #818cf8; }
1179
+ .uiv-ctl select.uiv-tokensel.changed { border-color: #4ade80; color: #86efac; }
1180
+ .uiv-swatches { display: flex; flex-wrap: wrap; gap: 5px; }
1181
+ .uiv-swatch { width: 20px; height: 20px; border-radius: 5px; cursor: pointer;
1182
+ border: 1px solid rgba(255,255,255,0.18); padding: 0; outline: none;
1183
+ box-shadow: inset 0 0 0 1px rgba(0,0,0,0.15); }
1184
+ .uiv-swatch:hover { transform: scale(1.12); border-color: #fff; }
1185
+ .uiv-swatch.on { box-shadow: 0 0 0 2px #18181b, 0 0 0 3.5px #4ade80; border-color: #4ade80; }
1186
+
1005
1187
  /* ---- current-styles readout ---- */
1006
1188
  .uiv-readout { display: flex; flex-direction: column; gap: 3px; }
1007
1189
  .uiv-rrow { display: grid; grid-template-columns: 70px 1fr; gap: 8px; align-items: center;
@@ -1009,6 +1191,13 @@ var CSS = (
1009
1191
  .uiv-rk { color: #71717a; }
1010
1192
  .uiv-rv { color: #fff; word-break: break-all; display: flex; align-items: center; gap: 6px; }
1011
1193
  .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;
1196
+ text-transform: uppercase; letter-spacing: .4px; }
1197
+ .uiv-lg { color: #e4e4e7; display: flex; align-items: center; gap: 4px; } /* file = white */
1198
+ .uiv-lg::before { content: ''; width: 7px; height: 7px; border-radius: 2px; background: currentColor; }
1199
+ .uiv-lg.edit { color: #4ade80; }
1200
+ .uiv-lg.auto { color: #6b7280; }
1012
1201
  .uiv-sw { display: inline-block; width: 11px; height: 11px; border-radius: 3px;
1013
1202
  border: 1px solid rgba(255,255,255,0.2); flex: 0 0 auto; }
1014
1203
 
@@ -1121,11 +1310,17 @@ var Uivisor = class {
1121
1310
  /** Undo / redo stacks of full edit-state snapshots. */
1122
1311
  this.undoStack = [];
1123
1312
  this.redoStack = [];
1124
- /** Cached live computed-style for the selected element (invalidated on reselect). */
1313
+ /** Cached live computed-style for the selected element (invalidated each render). */
1125
1314
  this._cs = null;
1126
1315
  this._csEl = null;
1316
+ /** Per-render memo of authored longhands per element (for the styles readout). */
1317
+ this._matched = null;
1127
1318
  /** Cached project breakpoint system (detected from CSS), refreshed until found. */
1128
1319
  this._bp = null;
1320
+ /** Cached project design system (detected from CSS variables), refreshed until found. */
1321
+ this._ds = null;
1322
+ /** Memoised Tailwind-utility probes: candidate class → resolved class or null. */
1323
+ this.utilCache = /* @__PURE__ */ new Map();
1129
1324
  // responsive (virtual screen) mode
1130
1325
  this.responsive = false;
1131
1326
  this.frameWidth = 768;
@@ -1201,6 +1396,43 @@ var Uivisor = class {
1201
1396
  if (!this._bp || this._bp.name !== "detected") this._bp = detectBreakpoints();
1202
1397
  return this._bp;
1203
1398
  }
1399
+ /** Project design tokens — re-detect until the CSS variables resolve. */
1400
+ designSystem() {
1401
+ if (!this._ds || this._ds.source === "none") this._ds = detectDesignSystem();
1402
+ return this._ds;
1403
+ }
1404
+ /** Which token category (if any) a CSS property picks tokens from. */
1405
+ dsCategoryFor(css) {
1406
+ if (css === "font-size") return "font-size";
1407
+ if (css === "color" || css === "background-color") return "color";
1408
+ return null;
1409
+ }
1410
+ /** Does a Tailwind utility for this token actually exist in the project CSS?
1411
+ * Probe a hidden element in the page document and compare the resolved value. */
1412
+ dsUtility(css, token) {
1413
+ const prefix = css === "background-color" ? "bg" : "text";
1414
+ const cls = `${prefix}-${token.name}`;
1415
+ if (this.utilCache.has(cls)) return this.utilCache.get(cls) ?? null;
1416
+ let res = null;
1417
+ try {
1418
+ const probe = document.createElement("span");
1419
+ probe.className = cls;
1420
+ probe.style.cssText = "position:absolute;left:-99999px;top:0;visibility:hidden";
1421
+ document.body.appendChild(probe);
1422
+ const got = getComputedStyle(probe).getPropertyValue(css).trim();
1423
+ probe.remove();
1424
+ if (token.category === "color") {
1425
+ const a = (rgbToHex(got) || got).toLowerCase();
1426
+ const b = (rgbToHex(token.value) || token.value).toLowerCase();
1427
+ if (a && a === b) res = cls;
1428
+ } else if (token.px != null && Math.abs(parseFloat(got) - token.px) < 0.5) {
1429
+ res = cls;
1430
+ }
1431
+ } catch {
1432
+ }
1433
+ this.utilCache.set(cls, res);
1434
+ return res;
1435
+ }
1204
1436
  mount() {
1205
1437
  this.host = document.createElement("div");
1206
1438
  this.host.id = "uivisor-root";
@@ -1309,7 +1541,10 @@ var Uivisor = class {
1309
1541
  const prev = this._bp;
1310
1542
  this._bp = null;
1311
1543
  const next = this.bpSystem();
1312
- if (!prev || key(prev) !== key(next)) this.renderBody();
1544
+ const prevDsN = this._ds?.tokens.length ?? -1;
1545
+ this._ds = null;
1546
+ const nextDs = this.designSystem();
1547
+ if (!prev || key(prev) !== key(next) || prevDsN !== nextDs.tokens.length) this.renderBody();
1313
1548
  };
1314
1549
  for (const d of [250, 900, 2200]) window.setTimeout(refresh, d);
1315
1550
  }
@@ -1471,7 +1706,9 @@ var Uivisor = class {
1471
1706
  const el = this.selected;
1472
1707
  const st = this.st();
1473
1708
  if (!el || !st) return "";
1474
- return el.style.getPropertyValue(css) || this.computedVal(css) || st.original[css] || "";
1709
+ const inline = el.style.getPropertyValue(css);
1710
+ if (inline && !inline.includes("var(")) return inline;
1711
+ return this.computedVal(css) || st.original[css] || "";
1475
1712
  }
1476
1713
  liveNum(css) {
1477
1714
  const v = this.liveVal(css).trim();
@@ -1545,7 +1782,7 @@ var Uivisor = class {
1545
1782
  for (const css of props) {
1546
1783
  for (const e of sibs) removeOverride(e, css);
1547
1784
  const c = st.record.changes.find((ch) => ch.property === css);
1548
- if (c) for (const e of targets) applyOverride(e, css, c.after.computed);
1785
+ if (c) for (const e of targets) applyOverride(e, css, c.live ?? c.after.computed);
1549
1786
  }
1550
1787
  this.reposition();
1551
1788
  }
@@ -1570,6 +1807,24 @@ var Uivisor = class {
1570
1807
  }
1571
1808
  this.renderBody();
1572
1809
  }
1810
+ /** Apply a design-system token to a property. The LIVE override is `var(--token)`
1811
+ * so a responsive token keeps adapting as you move across breakpoints; the
1812
+ * recorded value shows the token's value, tagged so the prompt asks for the token. */
1813
+ applyToken(css, token) {
1814
+ this.pushHistory();
1815
+ const live = `var(${token.cssVar})`;
1816
+ this.liveSet([css], live);
1817
+ const st = this.st();
1818
+ if (!st) return;
1819
+ const scope = this.activeScope();
1820
+ st.applied.add(css);
1821
+ const change = buildChange(css, st.original[css], token.value, scope);
1822
+ change.live = live;
1823
+ change.after.designToken = token.cssVar;
1824
+ change.after.token = st.record.styling.primaryMechanism === "tailwind" ? this.dsUtility(css, token) : null;
1825
+ this.setChange(st.record, change);
1826
+ this.renderBody();
1827
+ }
1573
1828
  revertProps(cssList) {
1574
1829
  const el = this.selected;
1575
1830
  const st = this.st();
@@ -1631,6 +1886,9 @@ var Uivisor = class {
1631
1886
  }
1632
1887
  // ---- rendering ----
1633
1888
  renderBody() {
1889
+ this._cs = null;
1890
+ this._csEl = null;
1891
+ this._matched = null;
1634
1892
  const body = this.q(".uiv-body");
1635
1893
  if (!this.selected) {
1636
1894
  body.innerHTML = `
@@ -1651,6 +1909,7 @@ var Uivisor = class {
1651
1909
  <div class="uiv-src">${escapeHtml(src)}</div>
1652
1910
  <span class="uiv-mech">${st.record.styling.primaryMechanism}</span>
1653
1911
  </div>
1912
+ ${this.dsIndicatorHtml()}
1654
1913
  ${this.currentStylesHtml()}
1655
1914
  ${this.breakpointBarHtml()}
1656
1915
  ${this.targetHtml(st)}
@@ -1659,7 +1918,79 @@ var Uivisor = class {
1659
1918
  `;
1660
1919
  this.bindControls();
1661
1920
  }
1662
- /** Read-only readout of the element's actual current styles so you don't guess. */
1921
+ /** Small indicator: how many design tokens were detected (or a hint if none). */
1922
+ dsIndicatorHtml() {
1923
+ const ds = this.designSystem();
1924
+ if (ds.source === "none") return "";
1925
+ const cats = Object.keys(ds.byCategory).map((c) => `${ds.byCategory[c].length} ${c}`).join(" \xB7 ");
1926
+ return `<div class="uiv-dsbar" title="${escapeAttr(cats)}">\u25C6 Design system \xB7 ${ds.tokens.length} tokens detected</div>`;
1927
+ }
1928
+ /** Longhand properties an authoring rule (or non-uivisor inline) sets on `el`.
1929
+ * Reimplements getMatchedCSSRules over same-origin sheets (incl. matching
1930
+ * @media), so we can tell "set in the project's CSS" from "browser default". */
1931
+ matchedProps(el) {
1932
+ const cache = this._matched ||= /* @__PURE__ */ new Map();
1933
+ const cached = cache.get(el);
1934
+ if (cached) return cached;
1935
+ const out = /* @__PURE__ */ new Set();
1936
+ const he = el;
1937
+ const applied = el === this.selected ? this.st()?.applied ?? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set();
1938
+ if (he.style) for (let i = 0; i < he.style.length; i++) {
1939
+ const p = he.style[i];
1940
+ if (!applied.has(p)) out.add(p);
1941
+ }
1942
+ const win = el.ownerDocument.defaultView || window;
1943
+ const walk = (rules) => {
1944
+ for (let i = 0; i < rules.length; i++) {
1945
+ const r = rules[i];
1946
+ if (r.selectorText && r.style) {
1947
+ let m = false;
1948
+ try {
1949
+ m = el.matches(r.selectorText);
1950
+ } catch {
1951
+ m = false;
1952
+ }
1953
+ if (m) for (let j = 0; j < r.style.length; j++) out.add(r.style[j]);
1954
+ } else if (r.media && r.cssRules) {
1955
+ let ok = true;
1956
+ try {
1957
+ ok = win.matchMedia(r.media.mediaText).matches;
1958
+ } catch {
1959
+ ok = true;
1960
+ }
1961
+ if (ok) walk(r.cssRules);
1962
+ } else if (r.cssRules) {
1963
+ walk(r.cssRules);
1964
+ }
1965
+ }
1966
+ };
1967
+ const sheets = el.ownerDocument.styleSheets;
1968
+ for (let i = 0; i < sheets.length; i++) {
1969
+ try {
1970
+ walk(sheets[i].cssRules);
1971
+ } catch {
1972
+ }
1973
+ }
1974
+ cache.set(el, out);
1975
+ return out;
1976
+ }
1977
+ /** Is any of `props` authored in the project CSS? For inherited properties we
1978
+ * also walk ancestors (a body/parent font rule still counts as "from the file"). */
1979
+ isAuthored(props, inherit) {
1980
+ let node = this.selected;
1981
+ while (node) {
1982
+ const set = this.matchedProps(node);
1983
+ if (props.some((p) => set.has(p))) return true;
1984
+ node = inherit ? node.parentElement : null;
1985
+ }
1986
+ return false;
1987
+ }
1988
+ /**
1989
+ * Read-only readout of the element's ACTUAL current styles, three-state coloured:
1990
+ * white = authored in the project CSS · green = edited in uivisor ·
1991
+ * grey = browser-computed/auto (e.g. a width a flex item derived, a default).
1992
+ * Comprehensive + context-aware (flex/grid/position rows only when relevant).
1993
+ */
1663
1994
  currentStylesHtml() {
1664
1995
  const el = this.selected;
1665
1996
  if (!el) return "";
@@ -1675,47 +2006,75 @@ var Uivisor = class {
1675
2006
  const h = hex(v);
1676
2007
  return /^#|rgb/.test(h) ? `<span class="uiv-sw" style="background:${h}"></span>${h}` : h;
1677
2008
  };
1678
- const box = (prefix) => {
1679
- const t = g(`${prefix}-top`);
1680
- const r = g(`${prefix}-right`);
1681
- const b = g(`${prefix}-bottom`);
1682
- const l = g(`${prefix}-left`);
2009
+ const sides = (parts) => {
2010
+ const [t, r, b, l] = parts.map(g);
1683
2011
  if (t === r && r === b && b === l) return t;
1684
2012
  if (t === b && r === l) return `${t} ${r}`;
1685
2013
  return `${t} ${r} ${b} ${l}`;
1686
2014
  };
2015
+ const px4 = (pre, suf = "") => [`${pre}-top${suf}`, `${pre}-right${suf}`, `${pre}-bottom${suf}`, `${pre}-left${suf}`];
2016
+ const clip = (s, n = 30) => s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
1687
2017
  const changedSet = new Set(this.st()?.record.changes.map((c) => c.property) ?? []);
1688
- const changed = (props) => props.some((p) => changedSet.has(p));
1689
- const px4 = (pre) => [`${pre}-top`, `${pre}-right`, `${pre}-bottom`, `${pre}-left`];
2018
+ const stateCls = (props, inherit = false) => {
2019
+ if (props.some((p) => changedSet.has(p))) return " changed";
2020
+ return this.isAuthored(props, inherit) ? "" : " auto";
2021
+ };
1690
2022
  const rows = [];
1691
- const add = (k, v, props = []) => {
1692
- if (v) rows.push({ k, v, edited: changed(props) });
2023
+ const add = (k, v, props, inherit = false) => {
2024
+ if (v !== "" && v != null) rows.push({ k, v, cls: stateCls(props, inherit) });
1693
2025
  };
1694
- add("display", g("display"));
1695
- add("size", `${Math.round(parseFloat(g("width")) || 0)} \xD7 ${Math.round(parseFloat(g("height")) || 0)}`);
1696
- const pad = box("padding");
1697
- if (pad && pad !== "0px") add("padding", pad, px4("padding"));
1698
- const mar = box("margin");
1699
- if (mar && mar !== "0px") add("margin", mar, px4("margin"));
1700
- if (/flex|grid/.test(g("display"))) {
2026
+ const disp = g("display");
2027
+ add("display", disp, ["display"]);
2028
+ const pos = g("position");
2029
+ if (pos && pos !== "static") {
2030
+ add("position", pos, ["position"]);
2031
+ const inset = ["top", "right", "bottom", "left"].filter((s) => g(s) !== "auto").map((s) => `${s[0]} ${g(s)}`).join(" ");
2032
+ if (inset) add("inset", inset, ["top", "right", "bottom", "left"]);
2033
+ }
2034
+ add("width", `${Math.round(parseFloat(g("width")) || 0)}px`, ["width"]);
2035
+ add("height", `${Math.round(parseFloat(g("height")) || 0)}px`, ["height"]);
2036
+ const maxw = g("max-width");
2037
+ if (maxw && maxw !== "none") add("max-w", maxw, ["max-width"]);
2038
+ const minh = g("min-height");
2039
+ if (minh && minh !== "0px" && minh !== "auto") add("min-h", minh, ["min-height"]);
2040
+ if (/flex|grid/.test(disp)) {
2041
+ if (disp.includes("flex")) add("direction", g("flex-direction"), ["flex-direction"]);
2042
+ add("justify", g("justify-content"), ["justify-content"]);
2043
+ add("align", g("align-items"), ["align-items"]);
2044
+ if (disp.includes("flex")) {
2045
+ const w = g("flex-wrap");
2046
+ if (w && w !== "nowrap") add("wrap", w, ["flex-wrap"]);
2047
+ }
2048
+ if (disp.includes("grid")) {
2049
+ const c = g("grid-template-columns");
2050
+ if (c && c !== "none") add("grid-cols", clip(c), ["grid-template-columns"]);
2051
+ }
1701
2052
  const gap = g("gap");
1702
- if (gap && gap !== "normal" && gap !== "0px") add("gap", gap, ["gap"]);
2053
+ if (gap && gap !== "normal") add("gap", gap, ["gap", "row-gap", "column-gap"]);
1703
2054
  }
1704
- add("font", `${g("font-size")} \xB7 ${g("font-weight")} \xB7 lh ${g("line-height")}`, [
1705
- "font-size",
1706
- "font-weight",
1707
- "line-height"
1708
- ]);
1709
- const ls = g("letter-spacing");
1710
- if (ls && ls !== "normal") add("tracking", ls, ["letter-spacing"]);
1711
- add("color", swatch(g("color")), ["color"]);
1712
- const bg = g("background-color");
1713
- if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent")
1714
- add("background", swatch(bg), ["background-color"]);
2055
+ let parentDisp = "";
2056
+ try {
2057
+ if (el.parentElement) parentDisp = getComputedStyle(el.parentElement).display;
2058
+ } catch {
2059
+ }
2060
+ if (/flex|grid/.test(parentDisp)) {
2061
+ add("flex", `${g("flex-grow")} ${g("flex-shrink")} ${g("flex-basis")}`, ["flex-grow", "flex-shrink", "flex-basis"]);
2062
+ const as = g("align-self");
2063
+ if (as && as !== "auto" && as !== "normal") add("self", as, ["align-self"]);
2064
+ }
2065
+ const pad = sides(px4("padding"));
2066
+ if (pad && pad !== "0px") add("padding", pad, px4("padding"));
2067
+ const mar = sides(px4("margin"));
2068
+ if (mar && mar !== "0px") add("margin", mar, px4("margin"));
1715
2069
  const bw = g("border-top-width");
1716
2070
  if (bw && parseFloat(bw) > 0)
1717
- add("border", `${bw} ${g("border-top-style")} ${hex(g("border-top-color"))}`, px4("border").map((p) => `${p}-width`));
1718
- const br = g("border-radius");
2071
+ add("border", `${bw} ${g("border-top-style")} ${swatch(g("border-top-color"))}`, px4("border", "-width"));
2072
+ const br = sides([
2073
+ "border-top-left-radius",
2074
+ "border-top-right-radius",
2075
+ "border-bottom-right-radius",
2076
+ "border-bottom-left-radius"
2077
+ ]);
1719
2078
  if (br && br !== "0px")
1720
2079
  add("radius", br, [
1721
2080
  "border-radius",
@@ -1724,11 +2083,35 @@ var Uivisor = class {
1724
2083
  "border-bottom-right-radius",
1725
2084
  "border-bottom-left-radius"
1726
2085
  ]);
1727
- if (g("box-shadow") !== "none" && g("box-shadow")) add("shadow", "yes");
2086
+ if (this.context(el).hasText) {
2087
+ add("font-size", g("font-size"), ["font-size"], true);
2088
+ add("weight", g("font-weight"), ["font-weight"], true);
2089
+ add("line", g("line-height"), ["line-height"], true);
2090
+ const ls = g("letter-spacing");
2091
+ if (ls && ls !== "normal") add("tracking", ls, ["letter-spacing"], true);
2092
+ add("color", swatch(g("color")), ["color"], true);
2093
+ const ta = g("text-align");
2094
+ if (ta && ta !== "start" && ta !== "left") add("text-align", ta, ["text-align"], true);
2095
+ }
2096
+ const bg = g("background-color");
2097
+ if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") add("background", swatch(bg), ["background-color"]);
2098
+ const bgi = g("background-image");
2099
+ if (bgi && bgi !== "none") add("bg-image", clip(bgi, 26), ["background-image"]);
2100
+ const sh = g("box-shadow");
2101
+ if (sh && sh !== "none") add("shadow", clip(sh, 26), ["box-shadow"]);
1728
2102
  const op = g("opacity");
1729
- if (op && parseFloat(op) < 1) add("opacity", op);
2103
+ if (op && parseFloat(op) < 1) add("opacity", op, ["opacity"]);
2104
+ const ov = g("overflow");
2105
+ if (ov && ov !== "visible") add("overflow", ov, ["overflow", "overflow-x", "overflow-y"]);
2106
+ const z = g("z-index");
2107
+ if (z && z !== "auto") add("z-index", z, ["z-index"]);
2108
+ const tr = g("transform");
2109
+ if (tr && tr !== "none") add("transform", clip(tr, 22), ["transform"]);
1730
2110
  const collapsed = this.collapsedSecs.has("Current styles");
1731
- const items = collapsed ? "" : rows.map((r) => `<div class="uiv-rrow"><span class="uiv-rk">${r.k}</span><span class="uiv-rv${r.edited ? " changed" : ""}">${r.v}</span></div>`).join("");
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("");
1732
2115
  return `<div class="uiv-sec">${this.accordionTitle("Current styles")}<div class="uiv-readout">${items}</div></div>`;
1733
2116
  }
1734
2117
  /** Breakpoint scope switcher: shows the PROJECT's breakpoints + the live window one. */
@@ -1864,6 +2247,32 @@ var Uivisor = class {
1864
2247
  const units = c.units.map((u) => `<option value="${u}"${u === d.unit ? " selected" : ""}>${UNIT_LABELS[u] ?? u}</option>`).join("");
1865
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>`;
1866
2249
  }
2250
+ /** A design-token picker row for a property, shown only when the project exposes
2251
+ * tokens for that category. Picking a token applies its value + tags the prompt. */
2252
+ tokenRowHtml(css, label) {
2253
+ const cat = this.dsCategoryFor(css);
2254
+ if (!cat) return "";
2255
+ const ds = this.designSystem();
2256
+ const list = ds.byCategory[cat];
2257
+ if (!list || !list.length) return "";
2258
+ const target = cat === "font-size" ? { px: this.currentPx(css) ?? void 0 } : { value: this.computedVal(css) };
2259
+ const near = nearestToken(ds, cat, target);
2260
+ const edited = this.st()?.record.changes.some(
2261
+ (c) => c.property === css && c.after.designToken
2262
+ );
2263
+ if (cat === "color") {
2264
+ const swatches = list.map((t) => {
2265
+ const on = near?.exact && near.token.cssVar === t.cssVar;
2266
+ return `<button class="uiv-swatch${on ? " on" : ""}" data-css="${css}" data-var="${escapeAttr(t.cssVar)}" title="${escapeAttr(`${t.name} \xB7 ${t.value}`)}" style="background:${escapeAttr(t.value)}"></button>`;
2267
+ }).join("");
2268
+ return `<div class="uiv-ctl"><span class="clabel uiv-tlabel">${label}</span><div class="cfield uiv-swatches">${swatches}</div><span></span></div>`;
2269
+ }
2270
+ const head = `<option value="">${near && !near.exact ? `\u2248 ${escapeHtml(near.token.name)} \xB7 pick token` : "\u2014 pick token \u2014"}</option>`;
2271
+ const opts = list.map(
2272
+ (t) => `<option value="${escapeAttr(t.cssVar)}"${near?.exact && near.token.cssVar === t.cssVar ? " selected" : ""}>${escapeHtml(`${t.name} \xB7 ${t.value}`)}</option>`
2273
+ ).join("");
2274
+ return `<div class="uiv-ctl"><span class="clabel uiv-tlabel">${label}</span><div class="cfield"><select class="uiv-sel uiv-tokensel${edited ? " changed" : ""}" data-css="${css}">${head}${opts}</select></div><span></span></div>`;
2275
+ }
1867
2276
  controlRow(c) {
1868
2277
  if (c.kind === "box") {
1869
2278
  const cssList = c.sides.map((s) => s.css);
@@ -1888,7 +2297,7 @@ var Uivisor = class {
1888
2297
  }
1889
2298
  if (c.kind === "len") {
1890
2299
  const v = this.liveNum(c.css);
1891
- 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>`;
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");
1892
2301
  }
1893
2302
  if (c.kind === "dim") {
1894
2303
  return this.dimField(c);
@@ -1900,7 +2309,7 @@ var Uivisor = class {
1900
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>`;
1901
2310
  }
1902
2311
  const val = toHexInput(this.liveVal(c.css));
1903
- 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>`;
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");
1904
2313
  }
1905
2314
  bindControls() {
1906
2315
  const root = this.root;
@@ -1942,7 +2351,7 @@ var Uivisor = class {
1942
2351
  this.commitValue([css], input.value);
1943
2352
  });
1944
2353
  });
1945
- root.querySelectorAll(".uiv-sel").forEach((node) => {
2354
+ root.querySelectorAll(".uiv-sel:not(.uiv-tokensel)").forEach((node) => {
1946
2355
  const sel = node;
1947
2356
  const css = sel.getAttribute("data-css");
1948
2357
  sel.addEventListener("change", () => {
@@ -1950,6 +2359,24 @@ var Uivisor = class {
1950
2359
  this.commitValue([css], sel.value);
1951
2360
  });
1952
2361
  });
2362
+ root.querySelectorAll(".uiv-tokensel").forEach((node) => {
2363
+ const sel = node;
2364
+ const css = sel.getAttribute("data-css");
2365
+ sel.addEventListener("change", () => {
2366
+ if (!sel.value) return;
2367
+ const token = this.designSystem().tokens.find((t) => t.cssVar === sel.value);
2368
+ if (token) this.applyToken(css, token);
2369
+ });
2370
+ });
2371
+ root.querySelectorAll(".uiv-swatch").forEach((node) => {
2372
+ const btn = node;
2373
+ const css = btn.getAttribute("data-css");
2374
+ const cssVar = btn.getAttribute("data-var");
2375
+ btn.addEventListener("click", () => {
2376
+ const token = this.designSystem().tokens.find((t) => t.cssVar === cssVar);
2377
+ if (token) this.applyToken(css, token);
2378
+ });
2379
+ });
1953
2380
  root.querySelectorAll(".uiv-expand").forEach((node) => {
1954
2381
  const btn = node;
1955
2382
  const key = btn.getAttribute("data-key");
@@ -2096,7 +2523,8 @@ var Uivisor = class {
2096
2523
  const loc = id.source.confidence !== "none" ? `${id.source.file}:${id.source.line}` : id.componentName || "";
2097
2524
  const chgs = collapseChanges(r.changes).map((c) => {
2098
2525
  const bp = c.breakpoint === "base" ? "" : `<span class="bp">${c.breakpoint}:</span> `;
2099
- const tok = c.after.token ? ` <span class="tok">${escapeHtml(c.after.token)}</span>` : "";
2526
+ const tokLabel = c.after.token || (c.after.designToken ? `var(${c.after.designToken})` : "");
2527
+ const tok = tokLabel ? ` <span class="tok">${escapeHtml(tokLabel)}</span>` : "";
2100
2528
  return `<div class="uiv-jchg">${bp}${c.property}: ${c.before.computed} \u2192 ${c.after.computed}${tok}</div>`;
2101
2529
  }).join("");
2102
2530
  return `<div class="uiv-jitem"><div class="jhead"><span class="jel">&lt;${id.tagName}&gt;</span><span class="jloc">${escapeHtml(loc)}</span></div>${chgs}</div>`;
@@ -2181,7 +2609,7 @@ var Uivisor = class {
2181
2609
  this.states.set(ent.el, st);
2182
2610
  const targets = st.record.target === "all" ? this.siblingsOf(ent.el) : [ent.el];
2183
2611
  for (const c of st.record.changes) {
2184
- for (const e of targets) applyOverride(e, c.property, c.after.computed);
2612
+ for (const e of targets) applyOverride(e, c.property, c.live ?? c.after.computed);
2185
2613
  st.applied.add(c.property);
2186
2614
  }
2187
2615
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uivisor",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",