@webto-id/variant-check 0.1.4 → 0.1.5

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 (3) hide show
  1. package/dist/cli.js +72 -22
  2. package/dist/index.js +72 -22
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
6
6
  import { readFileSync, writeFileSync } from "fs";
7
7
 
8
8
  // ../variant-compiler/src/ir.ts
9
- var COMPONENT_NAMES = ["Container", "Button", "EditUrlPill", "ViewMoreLink"];
9
+ var COMPONENT_NAMES = ["Container", "Button", "EditUrlPill", "ViewMoreLink", "AddImageButton"];
10
10
 
11
11
  // ../variant-compiler/src/parse/parser.ts
12
12
  var ParseError = class extends Error {
@@ -603,9 +603,10 @@ var HELPER_BY_IMPORT = {
603
603
  Container: "Container",
604
604
  Button: "Button",
605
605
  EditUrlPill: "EditUrlPill",
606
- ViewMoreLink: "ViewMoreLink"
606
+ ViewMoreLink: "ViewMoreLink",
607
+ AddImageButton: "AddImageButton"
607
608
  };
608
- var ALLOWED_SOURCE = /^(webto\/variant|(\.{1,2}\/)+(lib\/(sanitize|image|section-i18n)|(components\/)?(v2\/)?(ui\/)?(Container|Button|ViewMoreLink)\.astro|(components\/)?EditUrlPill\.astro|ui\/(Container|Button)\.astro|ViewMoreLink\.astro))$/;
609
+ var ALLOWED_SOURCE = /^(webto\/variant|(\.{1,2}\/)+(lib\/(sanitize|image|section-i18n)|(components\/)?(v2\/)?(ui\/)?(Container|Button|ViewMoreLink|AddImageButton)\.astro|(components\/)?EditUrlPill\.astro|ui\/(Container|Button)\.astro|ViewMoreLink\.astro))$/;
609
610
  function stripComments(src) {
610
611
  return src.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")).replace(/(^|[^:\\])\/\/[^\n]*/g, (m, pre) => pre + " ".repeat(m.length - pre.length));
611
612
  }
@@ -744,7 +745,7 @@ function parseFrontmatter(raw, lineOffset = 0) {
744
745
  const [, isType, def, named, source2] = m;
745
746
  i += m[0].length;
746
747
  if (isType) continue;
747
- if (!ALLOWED_SOURCE.test(source2)) throw new ParseError(`import from '${source2}' is not allowed \u2014 only "webto/variant" helpers (t, img, url, Container, Button, EditUrlPill, ViewMoreLink)`, line, "import");
748
+ if (!ALLOWED_SOURCE.test(source2)) throw new ParseError(`import from '${source2}' is not allowed \u2014 only "webto/variant" helpers (t, img, url, Container, Button, EditUrlPill, ViewMoreLink, AddImageButton)`, line, "import");
748
749
  const names = [];
749
750
  if (def) names.push({ local: def, imported: def });
750
751
  if (named) {
@@ -1158,7 +1159,7 @@ function lintNodes(nodes, ctx, ariaHidden = false) {
1158
1159
  const style = staticValue(n.attrs.style);
1159
1160
  const pinned = typeof style === "string" && /font-size\s*:/i.test(style);
1160
1161
  const inert = typeof cls === "string" && !pinned ? cls.split(/\s+/).filter((c) => HEADING_SIZE_CLASS.test(c)) : [];
1161
- if (inert.length) push(ctx, "warning", "heading-size-inert", `${inert.join(" ")} on <${tag}> has no effect: h1-h4 sizes follow the site theme (Style tab). Keep it as intent, or force a fixed size with !${inert[0]}, an inline style, or a non-heading tag`);
1162
+ if (inert.length) push(ctx, "warning", "heading-size-inert", `${inert.join(" ")} on <${tag}> has no effect: h1-h4 sizes follow the site theme (Style tab). Keep it as intent, pin the size with !${inert[0]} (honored everywhere, never rescaled by theme or per-section settings) or an inline style, or use a non-heading tag to follow the body scale`);
1162
1163
  }
1163
1164
  if (tag === "a") {
1164
1165
  const target = staticValue(n.attrs.target);
@@ -1520,18 +1521,18 @@ __name(hasBreak, "hasBreak");
1520
1521
 
1521
1522
  // ../variant-compiler/src/css/scope.ts
1522
1523
  var ROOT_PLACEHOLDER = "__WV_ROOT__";
1523
- var RECURSE_AT = /^@(media|supports|container|layer|scope|document)\b/i;
1524
1524
  var VERBATIM_AT = /^@(keyframes|-webkit-keyframes|property|font-face|page|counter-style|font-feature-values|namespace|charset)\b/i;
1525
1525
  function scopeCss(css, root = ROOT_PLACEHOLDER) {
1526
- return scopeBlock(stripComments2(css), root);
1526
+ return scopeBlock(stripComments2(css), root, null);
1527
1527
  }
1528
1528
  __name(scopeCss, "scopeCss");
1529
1529
  function stripComments2(s) {
1530
1530
  return s.replace(/\/\*[\s\S]*?\*\//g, "");
1531
1531
  }
1532
1532
  __name(stripComments2, "stripComments");
1533
- function scopeBlock(src, root) {
1533
+ function scopeBlock(src, root, parents) {
1534
1534
  let out = "";
1535
+ let decls = "";
1535
1536
  let i = 0;
1536
1537
  while (i < src.length) {
1537
1538
  let j = i;
@@ -1548,7 +1549,10 @@ function scopeBlock(src, root) {
1548
1549
  }
1549
1550
  const prelude = src.slice(i, j).trim();
1550
1551
  if (j >= src.length) {
1551
- if (prelude) out += prelude;
1552
+ if (prelude) {
1553
+ if (parents) decls += `${prelude};`;
1554
+ else out += prelude;
1555
+ }
1552
1556
  break;
1553
1557
  }
1554
1558
  if (src[j] === "}") {
@@ -1556,7 +1560,10 @@ function scopeBlock(src, root) {
1556
1560
  continue;
1557
1561
  }
1558
1562
  if (src[j] === ";") {
1559
- if (prelude) out += `${prelude};`;
1563
+ if (prelude) {
1564
+ if (parents && !prelude.startsWith("@")) decls += `${prelude};`;
1565
+ else out += `${prelude};`;
1566
+ }
1560
1567
  i = j + 1;
1561
1568
  continue;
1562
1569
  }
@@ -1566,21 +1573,28 @@ function scopeBlock(src, root) {
1566
1573
  if (!prelude) continue;
1567
1574
  if (prelude.startsWith("@")) {
1568
1575
  if (VERBATIM_AT.test(prelude)) out += `${prelude}{${body}}`;
1569
- else if (RECURSE_AT.test(prelude)) out += `${prelude}{${scopeBlock(body, root)}}`;
1570
- else out += `${prelude}{${scopeBlock(body, root)}}`;
1576
+ else out += `${prelude}{${scopeBlock(body, root, parents)}}`;
1571
1577
  continue;
1572
1578
  }
1573
- const selectors = splitSelectors(prelude).map((s) => scopeSelector(s.trim(), root)).filter(Boolean);
1579
+ const raw = splitSelectors(prelude).map((s) => s.trim()).filter(Boolean);
1580
+ const selectors = parents ? resolveNested(raw, parents) : raw.map((s) => scopeSelector(s, root)).filter(Boolean);
1574
1581
  if (!selectors.length) continue;
1575
- if (body.includes("{")) {
1576
- out += `${selectors.join(",")}{${scopeBlock(body, root)}}`;
1577
- } else {
1578
- out += `${selectors.join(",")}{${body.trim()}}`;
1579
- }
1582
+ out += scopeBlock(body, root, selectors);
1580
1583
  }
1584
+ if (parents && decls) out = `${parents.join(",")}{${decls}}` + out;
1581
1585
  return out;
1582
1586
  }
1583
1587
  __name(scopeBlock, "scopeBlock");
1588
+ function resolveNested(selectors, parents) {
1589
+ const parentRef = parents.length === 1 ? parents[0] : `:is(${parents.join(", ")})`;
1590
+ const out = [];
1591
+ for (const sel of selectors) {
1592
+ if (sel.includes("&")) out.push(sel.replace(/&/g, parentRef));
1593
+ else for (const p of parents) out.push(`${p} ${sel}`);
1594
+ }
1595
+ return out;
1596
+ }
1597
+ __name(resolveNested, "resolveNested");
1584
1598
  function matchBrace2(src, open) {
1585
1599
  let depth = 0;
1586
1600
  let inStr = null;
@@ -1625,7 +1639,7 @@ function splitSelectors(s) {
1625
1639
  __name(splitSelectors, "splitSelectors");
1626
1640
  function scopeSelector(sel, root) {
1627
1641
  if (!sel) return "";
1628
- if (sel.startsWith("&")) return `${root} ${sel.slice(1).trim()}`.trim();
1642
+ if (sel.startsWith("&")) return `${root}${sel.slice(1)}`.trim();
1629
1643
  if (sel.includes(ROOT_PLACEHOLDER)) return sel;
1630
1644
  const m = /^(:root|html|body)(?![\w-])/i.exec(sel);
1631
1645
  if (m) {
@@ -1701,7 +1715,8 @@ var MACRO_CLASSES = [
1701
1715
  "text-foreground hover:bg-muted",
1702
1716
  "px-4 py-2 text-sm px-6 py-3 text-base px-8 py-4 text-lg",
1703
1717
  "gap-1.5 px-2.5 py-1 rounded-md border-dashed text-xs whitespace-nowrap font-mono break-all w-3.5 h-3.5 shrink-0",
1704
- "mt-10 flex justify-start justify-center justify-end hover:border-primary/30 text-primary hover:underline w-4 h-4 relative"
1718
+ "mt-10 flex justify-start justify-center justify-end hover:border-primary/30 text-primary hover:underline w-4 h-4 relative",
1719
+ "mt-8 absolute bottom-8 right-8 z-30"
1705
1720
  ];
1706
1721
  function pickCandidates(tokens) {
1707
1722
  const set = /* @__PURE__ */ new Set();
@@ -1766,7 +1781,7 @@ function deriveSchema(fields, props, base2) {
1766
1781
  __name(deriveSchema, "deriveSchema");
1767
1782
 
1768
1783
  // ../variant-compiler/src/index.ts
1769
- var COMPILER_VERSION = "0.1.4";
1784
+ var COMPILER_VERSION = "0.1.5";
1770
1785
  var IR_MAX_BYTES = 256 * 1024;
1771
1786
  var CSS_MAX_BYTES = 64 * 1024;
1772
1787
  var STRICT_CODES = /* @__PURE__ */ new Set(["dead-text", "hardcoded-color", "hardcoded-radius", "hardcoded-font", "hardcoded-image", "img-alt", "props-optional", "tailwind-skipped"]);
@@ -2321,6 +2336,12 @@ function editUrlPillHtml(field, value, anchor = "below", extraClass = "") {
2321
2336
  return `<div data-edit-reveal data-anchor="${escAttr(anchor)}" class="edit-url-pill${extraClass ? " " + escAttr(extraClass) : ""}" style="display:none;position:absolute;z-index:10;pointer-events:auto;${pos}"><div class="edit-url-pill__inner inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md border border-dashed text-xs whitespace-nowrap shadow-sm" style="color:var(--color-background,#fff);border-color:color-mix(in srgb,var(--color-foreground,#000) 40%,transparent);background-color:var(--color-foreground,#000);text-transform:none;letter-spacing:normal;"><svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /></svg><span data-edit-field="${escAttr(field)}" class="font-mono break-all">${escText(value)}</span></div></div>`;
2322
2337
  }
2323
2338
  __name(editUrlPillHtml, "editUrlPillHtml");
2339
+ function addImageButtonHtml(path, label = "Tambah Gambar", mode = "inline") {
2340
+ const btn = /* @__PURE__ */ __name((cls) => `<button type="button" class="${escAttr(cls)}" data-edit-add-image="${escAttr(path)}" aria-label="${escAttr(label)}"><span class="webto-inline-add-icon">+</span>${escText(label)}</button>`, "btn");
2341
+ if (mode === "floating") return btn("webto-inline-add-btn absolute bottom-8 right-8 z-30");
2342
+ return `<div class="webto-add-row mt-8">${btn("webto-inline-add-btn")}</div>`;
2343
+ }
2344
+ __name(addImageButtonHtml, "addImageButtonHtml");
2324
2345
  function renderComponent(n, scope, ctx, budget) {
2325
2346
  const p = {};
2326
2347
  for (const [k, v] of Object.entries(n.props)) p[k] = attrValue(v, scope, budget);
@@ -2335,6 +2356,13 @@ function renderComponent(n, scope, ctx, budget) {
2335
2356
  }
2336
2357
  case "EditUrlPill":
2337
2358
  return editUrlPillHtml(stringify(p.field), stringify(p.value), stringify(p.anchor) || "below", flattenClassList(p.class));
2359
+ case "AddImageButton": {
2360
+ if (ctx.marks) ctx.marks.addImageButton = true;
2361
+ if (!ctx.editChrome || p.visible === false) return "";
2362
+ const path = stringify(p.path);
2363
+ if (!path) return "";
2364
+ return addImageButtonHtml(path, stringify(p.label) || "Tambah Gambar", p.mode === "floating" ? "floating" : "inline");
2365
+ }
2338
2366
  case "ViewMoreLink": {
2339
2367
  const url = stringify(p.url);
2340
2368
  if (!url) return "";
@@ -2422,12 +2450,34 @@ function renderVariant(ir, content, ctx, opts = {}) {
2422
2450
  scope[p.name] = v === void 0 && p.def ? evalExpr(p.def, scope, budget) : v;
2423
2451
  }
2424
2452
  for (const c of ir.consts) scope[c.name] = evalExpr(c.e, scope, budget);
2425
- const html = renderNodes(ir.root, scope, ctx, budget);
2453
+ const rctx = { ...ctx, marks: { addImageButton: false } };
2454
+ const html = withAddImageFallback(renderNodes(ir.root, scope, rctx, budget), content, rctx);
2426
2455
  const css = ir.css ? layerize(ir.css.replace(/__WV_ROOT__/g, `[data-wv-inst="${uid}"]`)) : "";
2427
2456
  const script = ir.script ? wrapScript(ir.script, uid) : "";
2428
2457
  return { html, css, script, uid };
2429
2458
  }
2430
2459
  __name(renderVariant, "renderVariant");
2460
+ var EDIT_IMAGE_ITEM_RE = /data-edit-image="([A-Za-z_]\w*)\.(\d+)\.([A-Za-z_]\w*)"/g;
2461
+ function withAddImageFallback(html, content, ctx) {
2462
+ if (!ctx.editChrome || ctx.marks?.addImageButton || html.includes("data-edit-add-image=")) return html;
2463
+ const lists = /* @__PURE__ */ new Map();
2464
+ for (const m of html.matchAll(EDIT_IMAGE_ITEM_RE)) {
2465
+ const [, list, index, key] = m;
2466
+ const cur = lists.get(list) ?? { key, hits: 0, maxIndex: -1 };
2467
+ cur.hits += 1;
2468
+ cur.maxIndex = Math.max(cur.maxIndex, Number(index));
2469
+ lists.set(list, cur);
2470
+ }
2471
+ let best = null;
2472
+ for (const [list, v] of lists) if (!best || v.hits > best.hits) best = { list, ...v };
2473
+ if (!best) return html;
2474
+ const stored = content[best.list];
2475
+ const length = Array.isArray(stored) ? stored.length : best.maxIndex + 1;
2476
+ const pill = addImageButtonHtml(`${best.list}.${length}.${best.key}`);
2477
+ const tail = html.lastIndexOf("</section>");
2478
+ return tail >= 0 ? html.slice(0, tail) + pill + html.slice(tail) : html + pill;
2479
+ }
2480
+ __name(withAddImageFallback, "withAddImageFallback");
2431
2481
  var STATEMENT_AT = /^\s*@[a-zA-Z-]+[^;{}]*;/;
2432
2482
  function layerize(css) {
2433
2483
  let rest = css;
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ var __defProp = Object.defineProperty;
3
3
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
4
 
5
5
  // ../variant-compiler/src/ir.ts
6
- var COMPONENT_NAMES = ["Container", "Button", "EditUrlPill", "ViewMoreLink"];
6
+ var COMPONENT_NAMES = ["Container", "Button", "EditUrlPill", "ViewMoreLink", "AddImageButton"];
7
7
 
8
8
  // ../variant-compiler/src/parse/parser.ts
9
9
  var ParseError = class extends Error {
@@ -600,9 +600,10 @@ var HELPER_BY_IMPORT = {
600
600
  Container: "Container",
601
601
  Button: "Button",
602
602
  EditUrlPill: "EditUrlPill",
603
- ViewMoreLink: "ViewMoreLink"
603
+ ViewMoreLink: "ViewMoreLink",
604
+ AddImageButton: "AddImageButton"
604
605
  };
605
- var ALLOWED_SOURCE = /^(webto\/variant|(\.{1,2}\/)+(lib\/(sanitize|image|section-i18n)|(components\/)?(v2\/)?(ui\/)?(Container|Button|ViewMoreLink)\.astro|(components\/)?EditUrlPill\.astro|ui\/(Container|Button)\.astro|ViewMoreLink\.astro))$/;
606
+ var ALLOWED_SOURCE = /^(webto\/variant|(\.{1,2}\/)+(lib\/(sanitize|image|section-i18n)|(components\/)?(v2\/)?(ui\/)?(Container|Button|ViewMoreLink|AddImageButton)\.astro|(components\/)?EditUrlPill\.astro|ui\/(Container|Button)\.astro|ViewMoreLink\.astro))$/;
606
607
  function stripComments(src) {
607
608
  return src.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")).replace(/(^|[^:\\])\/\/[^\n]*/g, (m, pre) => pre + " ".repeat(m.length - pre.length));
608
609
  }
@@ -741,7 +742,7 @@ function parseFrontmatter(raw, lineOffset = 0) {
741
742
  const [, isType, def, named, source] = m;
742
743
  i += m[0].length;
743
744
  if (isType) continue;
744
- if (!ALLOWED_SOURCE.test(source)) throw new ParseError(`import from '${source}' is not allowed \u2014 only "webto/variant" helpers (t, img, url, Container, Button, EditUrlPill, ViewMoreLink)`, line, "import");
745
+ if (!ALLOWED_SOURCE.test(source)) throw new ParseError(`import from '${source}' is not allowed \u2014 only "webto/variant" helpers (t, img, url, Container, Button, EditUrlPill, ViewMoreLink, AddImageButton)`, line, "import");
745
746
  const names = [];
746
747
  if (def) names.push({ local: def, imported: def });
747
748
  if (named) {
@@ -1155,7 +1156,7 @@ function lintNodes(nodes, ctx, ariaHidden = false) {
1155
1156
  const style = staticValue(n.attrs.style);
1156
1157
  const pinned = typeof style === "string" && /font-size\s*:/i.test(style);
1157
1158
  const inert = typeof cls === "string" && !pinned ? cls.split(/\s+/).filter((c) => HEADING_SIZE_CLASS.test(c)) : [];
1158
- if (inert.length) push(ctx, "warning", "heading-size-inert", `${inert.join(" ")} on <${tag}> has no effect: h1-h4 sizes follow the site theme (Style tab). Keep it as intent, or force a fixed size with !${inert[0]}, an inline style, or a non-heading tag`);
1159
+ if (inert.length) push(ctx, "warning", "heading-size-inert", `${inert.join(" ")} on <${tag}> has no effect: h1-h4 sizes follow the site theme (Style tab). Keep it as intent, pin the size with !${inert[0]} (honored everywhere, never rescaled by theme or per-section settings) or an inline style, or use a non-heading tag to follow the body scale`);
1159
1160
  }
1160
1161
  if (tag === "a") {
1161
1162
  const target = staticValue(n.attrs.target);
@@ -1517,18 +1518,18 @@ __name(hasBreak, "hasBreak");
1517
1518
 
1518
1519
  // ../variant-compiler/src/css/scope.ts
1519
1520
  var ROOT_PLACEHOLDER = "__WV_ROOT__";
1520
- var RECURSE_AT = /^@(media|supports|container|layer|scope|document)\b/i;
1521
1521
  var VERBATIM_AT = /^@(keyframes|-webkit-keyframes|property|font-face|page|counter-style|font-feature-values|namespace|charset)\b/i;
1522
1522
  function scopeCss(css, root = ROOT_PLACEHOLDER) {
1523
- return scopeBlock(stripComments2(css), root);
1523
+ return scopeBlock(stripComments2(css), root, null);
1524
1524
  }
1525
1525
  __name(scopeCss, "scopeCss");
1526
1526
  function stripComments2(s) {
1527
1527
  return s.replace(/\/\*[\s\S]*?\*\//g, "");
1528
1528
  }
1529
1529
  __name(stripComments2, "stripComments");
1530
- function scopeBlock(src, root) {
1530
+ function scopeBlock(src, root, parents) {
1531
1531
  let out = "";
1532
+ let decls = "";
1532
1533
  let i = 0;
1533
1534
  while (i < src.length) {
1534
1535
  let j = i;
@@ -1545,7 +1546,10 @@ function scopeBlock(src, root) {
1545
1546
  }
1546
1547
  const prelude = src.slice(i, j).trim();
1547
1548
  if (j >= src.length) {
1548
- if (prelude) out += prelude;
1549
+ if (prelude) {
1550
+ if (parents) decls += `${prelude};`;
1551
+ else out += prelude;
1552
+ }
1549
1553
  break;
1550
1554
  }
1551
1555
  if (src[j] === "}") {
@@ -1553,7 +1557,10 @@ function scopeBlock(src, root) {
1553
1557
  continue;
1554
1558
  }
1555
1559
  if (src[j] === ";") {
1556
- if (prelude) out += `${prelude};`;
1560
+ if (prelude) {
1561
+ if (parents && !prelude.startsWith("@")) decls += `${prelude};`;
1562
+ else out += `${prelude};`;
1563
+ }
1557
1564
  i = j + 1;
1558
1565
  continue;
1559
1566
  }
@@ -1563,21 +1570,28 @@ function scopeBlock(src, root) {
1563
1570
  if (!prelude) continue;
1564
1571
  if (prelude.startsWith("@")) {
1565
1572
  if (VERBATIM_AT.test(prelude)) out += `${prelude}{${body}}`;
1566
- else if (RECURSE_AT.test(prelude)) out += `${prelude}{${scopeBlock(body, root)}}`;
1567
- else out += `${prelude}{${scopeBlock(body, root)}}`;
1573
+ else out += `${prelude}{${scopeBlock(body, root, parents)}}`;
1568
1574
  continue;
1569
1575
  }
1570
- const selectors = splitSelectors(prelude).map((s) => scopeSelector(s.trim(), root)).filter(Boolean);
1576
+ const raw = splitSelectors(prelude).map((s) => s.trim()).filter(Boolean);
1577
+ const selectors = parents ? resolveNested(raw, parents) : raw.map((s) => scopeSelector(s, root)).filter(Boolean);
1571
1578
  if (!selectors.length) continue;
1572
- if (body.includes("{")) {
1573
- out += `${selectors.join(",")}{${scopeBlock(body, root)}}`;
1574
- } else {
1575
- out += `${selectors.join(",")}{${body.trim()}}`;
1576
- }
1579
+ out += scopeBlock(body, root, selectors);
1577
1580
  }
1581
+ if (parents && decls) out = `${parents.join(",")}{${decls}}` + out;
1578
1582
  return out;
1579
1583
  }
1580
1584
  __name(scopeBlock, "scopeBlock");
1585
+ function resolveNested(selectors, parents) {
1586
+ const parentRef = parents.length === 1 ? parents[0] : `:is(${parents.join(", ")})`;
1587
+ const out = [];
1588
+ for (const sel of selectors) {
1589
+ if (sel.includes("&")) out.push(sel.replace(/&/g, parentRef));
1590
+ else for (const p of parents) out.push(`${p} ${sel}`);
1591
+ }
1592
+ return out;
1593
+ }
1594
+ __name(resolveNested, "resolveNested");
1581
1595
  function matchBrace2(src, open) {
1582
1596
  let depth = 0;
1583
1597
  let inStr = null;
@@ -1622,7 +1636,7 @@ function splitSelectors(s) {
1622
1636
  __name(splitSelectors, "splitSelectors");
1623
1637
  function scopeSelector(sel, root) {
1624
1638
  if (!sel) return "";
1625
- if (sel.startsWith("&")) return `${root} ${sel.slice(1).trim()}`.trim();
1639
+ if (sel.startsWith("&")) return `${root}${sel.slice(1)}`.trim();
1626
1640
  if (sel.includes(ROOT_PLACEHOLDER)) return sel;
1627
1641
  const m = /^(:root|html|body)(?![\w-])/i.exec(sel);
1628
1642
  if (m) {
@@ -1698,7 +1712,8 @@ var MACRO_CLASSES = [
1698
1712
  "text-foreground hover:bg-muted",
1699
1713
  "px-4 py-2 text-sm px-6 py-3 text-base px-8 py-4 text-lg",
1700
1714
  "gap-1.5 px-2.5 py-1 rounded-md border-dashed text-xs whitespace-nowrap font-mono break-all w-3.5 h-3.5 shrink-0",
1701
- "mt-10 flex justify-start justify-center justify-end hover:border-primary/30 text-primary hover:underline w-4 h-4 relative"
1715
+ "mt-10 flex justify-start justify-center justify-end hover:border-primary/30 text-primary hover:underline w-4 h-4 relative",
1716
+ "mt-8 absolute bottom-8 right-8 z-30"
1702
1717
  ];
1703
1718
  function pickCandidates(tokens) {
1704
1719
  const set = /* @__PURE__ */ new Set();
@@ -1763,7 +1778,7 @@ function deriveSchema(fields, props, base) {
1763
1778
  __name(deriveSchema, "deriveSchema");
1764
1779
 
1765
1780
  // ../variant-compiler/src/index.ts
1766
- var COMPILER_VERSION = "0.1.4";
1781
+ var COMPILER_VERSION = "0.1.5";
1767
1782
  var IR_MAX_BYTES = 256 * 1024;
1768
1783
  var CSS_MAX_BYTES = 64 * 1024;
1769
1784
  var STRICT_CODES = /* @__PURE__ */ new Set(["dead-text", "hardcoded-color", "hardcoded-radius", "hardcoded-font", "hardcoded-image", "img-alt", "props-optional", "tailwind-skipped"]);
@@ -2318,6 +2333,12 @@ function editUrlPillHtml(field, value, anchor = "below", extraClass = "") {
2318
2333
  return `<div data-edit-reveal data-anchor="${escAttr(anchor)}" class="edit-url-pill${extraClass ? " " + escAttr(extraClass) : ""}" style="display:none;position:absolute;z-index:10;pointer-events:auto;${pos}"><div class="edit-url-pill__inner inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md border border-dashed text-xs whitespace-nowrap shadow-sm" style="color:var(--color-background,#fff);border-color:color-mix(in srgb,var(--color-foreground,#000) 40%,transparent);background-color:var(--color-foreground,#000);text-transform:none;letter-spacing:normal;"><svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /></svg><span data-edit-field="${escAttr(field)}" class="font-mono break-all">${escText(value)}</span></div></div>`;
2319
2334
  }
2320
2335
  __name(editUrlPillHtml, "editUrlPillHtml");
2336
+ function addImageButtonHtml(path, label = "Tambah Gambar", mode = "inline") {
2337
+ const btn = /* @__PURE__ */ __name((cls) => `<button type="button" class="${escAttr(cls)}" data-edit-add-image="${escAttr(path)}" aria-label="${escAttr(label)}"><span class="webto-inline-add-icon">+</span>${escText(label)}</button>`, "btn");
2338
+ if (mode === "floating") return btn("webto-inline-add-btn absolute bottom-8 right-8 z-30");
2339
+ return `<div class="webto-add-row mt-8">${btn("webto-inline-add-btn")}</div>`;
2340
+ }
2341
+ __name(addImageButtonHtml, "addImageButtonHtml");
2321
2342
  function renderComponent(n, scope, ctx, budget) {
2322
2343
  const p = {};
2323
2344
  for (const [k, v] of Object.entries(n.props)) p[k] = attrValue(v, scope, budget);
@@ -2332,6 +2353,13 @@ function renderComponent(n, scope, ctx, budget) {
2332
2353
  }
2333
2354
  case "EditUrlPill":
2334
2355
  return editUrlPillHtml(stringify(p.field), stringify(p.value), stringify(p.anchor) || "below", flattenClassList(p.class));
2356
+ case "AddImageButton": {
2357
+ if (ctx.marks) ctx.marks.addImageButton = true;
2358
+ if (!ctx.editChrome || p.visible === false) return "";
2359
+ const path = stringify(p.path);
2360
+ if (!path) return "";
2361
+ return addImageButtonHtml(path, stringify(p.label) || "Tambah Gambar", p.mode === "floating" ? "floating" : "inline");
2362
+ }
2335
2363
  case "ViewMoreLink": {
2336
2364
  const url = stringify(p.url);
2337
2365
  if (!url) return "";
@@ -2419,12 +2447,34 @@ function renderVariant(ir, content, ctx, opts = {}) {
2419
2447
  scope[p.name] = v === void 0 && p.def ? evalExpr(p.def, scope, budget) : v;
2420
2448
  }
2421
2449
  for (const c of ir.consts) scope[c.name] = evalExpr(c.e, scope, budget);
2422
- const html = renderNodes(ir.root, scope, ctx, budget);
2450
+ const rctx = { ...ctx, marks: { addImageButton: false } };
2451
+ const html = withAddImageFallback(renderNodes(ir.root, scope, rctx, budget), content, rctx);
2423
2452
  const css = ir.css ? layerize(ir.css.replace(/__WV_ROOT__/g, `[data-wv-inst="${uid}"]`)) : "";
2424
2453
  const script = ir.script ? wrapScript(ir.script, uid) : "";
2425
2454
  return { html, css, script, uid };
2426
2455
  }
2427
2456
  __name(renderVariant, "renderVariant");
2457
+ var EDIT_IMAGE_ITEM_RE = /data-edit-image="([A-Za-z_]\w*)\.(\d+)\.([A-Za-z_]\w*)"/g;
2458
+ function withAddImageFallback(html, content, ctx) {
2459
+ if (!ctx.editChrome || ctx.marks?.addImageButton || html.includes("data-edit-add-image=")) return html;
2460
+ const lists = /* @__PURE__ */ new Map();
2461
+ for (const m of html.matchAll(EDIT_IMAGE_ITEM_RE)) {
2462
+ const [, list, index, key] = m;
2463
+ const cur = lists.get(list) ?? { key, hits: 0, maxIndex: -1 };
2464
+ cur.hits += 1;
2465
+ cur.maxIndex = Math.max(cur.maxIndex, Number(index));
2466
+ lists.set(list, cur);
2467
+ }
2468
+ let best = null;
2469
+ for (const [list, v] of lists) if (!best || v.hits > best.hits) best = { list, ...v };
2470
+ if (!best) return html;
2471
+ const stored = content[best.list];
2472
+ const length = Array.isArray(stored) ? stored.length : best.maxIndex + 1;
2473
+ const pill = addImageButtonHtml(`${best.list}.${length}.${best.key}`);
2474
+ const tail = html.lastIndexOf("</section>");
2475
+ return tail >= 0 ? html.slice(0, tail) + pill + html.slice(tail) : html + pill;
2476
+ }
2477
+ __name(withAddImageFallback, "withAddImageFallback");
2428
2478
  var STATEMENT_AT = /^\s*@[a-zA-Z-]+[^;{}]*;/;
2429
2479
  function layerize(css) {
2430
2480
  let rest = css;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webto-id/variant-check",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Lint, compile and preview a Webto Variant Format (.astro subset) section variant with the exact compiler the webto.id marketplace uses.",
5
5
  "license": "MIT",
6
6
  "type": "module",