@webto-id/variant-check 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.
- package/dist/cli.js +91 -22
- package/dist/index.js +91 -22
- 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,
|
|
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)
|
|
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)
|
|
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
|
|
1570
|
-
else out += `${prelude}{${scopeBlock(body, root)}}`;
|
|
1576
|
+
else out += `${prelude}{${scopeBlock(body, root, parents)}}`;
|
|
1571
1577
|
continue;
|
|
1572
1578
|
}
|
|
1573
|
-
const
|
|
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
|
-
|
|
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}
|
|
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.
|
|
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"]);
|
|
@@ -1863,6 +1878,25 @@ async function compile2(source2, opts) {
|
|
|
1863
1878
|
lint.push({ level: "error", code: "tailwind", message: `Tailwind compile failed: ${e.message}` });
|
|
1864
1879
|
}
|
|
1865
1880
|
}
|
|
1881
|
+
if (!opts.skipTailwind && twCss) {
|
|
1882
|
+
const unknown = [];
|
|
1883
|
+
for (const t of classTokens) {
|
|
1884
|
+
if (!isCandidate(t)) continue;
|
|
1885
|
+
if (/^(group|peer)([/]|$)/.test(t)) continue;
|
|
1886
|
+
if (/^(js|wv)-/.test(t)) continue;
|
|
1887
|
+
if (userCss.includes(t) || script.includes(t)) continue;
|
|
1888
|
+
const cssName = "." + t.replace(/[^a-zA-Z0-9_-]/g, (ch) => "\\" + ch);
|
|
1889
|
+
const boundary = new RegExp(cssName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?![\\w-])");
|
|
1890
|
+
if (!boundary.test(twCss)) unknown.push(t);
|
|
1891
|
+
}
|
|
1892
|
+
if (unknown.length) {
|
|
1893
|
+
lint.push({
|
|
1894
|
+
level: "warning",
|
|
1895
|
+
code: "unknown-class",
|
|
1896
|
+
message: `class${unknown.length > 1 ? "es" : ""} '${unknown.join("', '")}' compile to no CSS: not a Tailwind utility, and neither this variant's <style> nor its <script> mentions ${unknown.length > 1 ? "them" : "it"}. Leftover from another CSS framework? Rewrite as Tailwind utilities/tokens or style it in <style>; a deliberate script hook belongs under a js- prefix`
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1866
1900
|
let css = "";
|
|
1867
1901
|
try {
|
|
1868
1902
|
css = scopeCss(twCss, ROOT_PLACEHOLDER) + (userCss.trim() ? "\n" + scopeCss(userCss, ROOT_PLACEHOLDER) : "");
|
|
@@ -2321,6 +2355,12 @@ function editUrlPillHtml(field, value, anchor = "below", extraClass = "") {
|
|
|
2321
2355
|
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
2356
|
}
|
|
2323
2357
|
__name(editUrlPillHtml, "editUrlPillHtml");
|
|
2358
|
+
function addImageButtonHtml(path, label = "Tambah Gambar", mode = "inline") {
|
|
2359
|
+
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");
|
|
2360
|
+
if (mode === "floating") return btn("webto-inline-add-btn absolute bottom-8 right-8 z-30");
|
|
2361
|
+
return `<div class="webto-add-row mt-8">${btn("webto-inline-add-btn")}</div>`;
|
|
2362
|
+
}
|
|
2363
|
+
__name(addImageButtonHtml, "addImageButtonHtml");
|
|
2324
2364
|
function renderComponent(n, scope, ctx, budget) {
|
|
2325
2365
|
const p = {};
|
|
2326
2366
|
for (const [k, v] of Object.entries(n.props)) p[k] = attrValue(v, scope, budget);
|
|
@@ -2335,6 +2375,13 @@ function renderComponent(n, scope, ctx, budget) {
|
|
|
2335
2375
|
}
|
|
2336
2376
|
case "EditUrlPill":
|
|
2337
2377
|
return editUrlPillHtml(stringify(p.field), stringify(p.value), stringify(p.anchor) || "below", flattenClassList(p.class));
|
|
2378
|
+
case "AddImageButton": {
|
|
2379
|
+
if (ctx.marks) ctx.marks.addImageButton = true;
|
|
2380
|
+
if (!ctx.editChrome || p.visible === false) return "";
|
|
2381
|
+
const path = stringify(p.path);
|
|
2382
|
+
if (!path) return "";
|
|
2383
|
+
return addImageButtonHtml(path, stringify(p.label) || "Tambah Gambar", p.mode === "floating" ? "floating" : "inline");
|
|
2384
|
+
}
|
|
2338
2385
|
case "ViewMoreLink": {
|
|
2339
2386
|
const url = stringify(p.url);
|
|
2340
2387
|
if (!url) return "";
|
|
@@ -2422,12 +2469,34 @@ function renderVariant(ir, content, ctx, opts = {}) {
|
|
|
2422
2469
|
scope[p.name] = v === void 0 && p.def ? evalExpr(p.def, scope, budget) : v;
|
|
2423
2470
|
}
|
|
2424
2471
|
for (const c of ir.consts) scope[c.name] = evalExpr(c.e, scope, budget);
|
|
2425
|
-
const
|
|
2472
|
+
const rctx = { ...ctx, marks: { addImageButton: false } };
|
|
2473
|
+
const html = withAddImageFallback(renderNodes(ir.root, scope, rctx, budget), content, rctx);
|
|
2426
2474
|
const css = ir.css ? layerize(ir.css.replace(/__WV_ROOT__/g, `[data-wv-inst="${uid}"]`)) : "";
|
|
2427
2475
|
const script = ir.script ? wrapScript(ir.script, uid) : "";
|
|
2428
2476
|
return { html, css, script, uid };
|
|
2429
2477
|
}
|
|
2430
2478
|
__name(renderVariant, "renderVariant");
|
|
2479
|
+
var EDIT_IMAGE_ITEM_RE = /data-edit-image="([A-Za-z_]\w*)\.(\d+)\.([A-Za-z_]\w*)"/g;
|
|
2480
|
+
function withAddImageFallback(html, content, ctx) {
|
|
2481
|
+
if (!ctx.editChrome || ctx.marks?.addImageButton || html.includes("data-edit-add-image=")) return html;
|
|
2482
|
+
const lists = /* @__PURE__ */ new Map();
|
|
2483
|
+
for (const m of html.matchAll(EDIT_IMAGE_ITEM_RE)) {
|
|
2484
|
+
const [, list, index, key] = m;
|
|
2485
|
+
const cur = lists.get(list) ?? { key, hits: 0, maxIndex: -1 };
|
|
2486
|
+
cur.hits += 1;
|
|
2487
|
+
cur.maxIndex = Math.max(cur.maxIndex, Number(index));
|
|
2488
|
+
lists.set(list, cur);
|
|
2489
|
+
}
|
|
2490
|
+
let best = null;
|
|
2491
|
+
for (const [list, v] of lists) if (!best || v.hits > best.hits) best = { list, ...v };
|
|
2492
|
+
if (!best) return html;
|
|
2493
|
+
const stored = content[best.list];
|
|
2494
|
+
const length = Array.isArray(stored) ? stored.length : best.maxIndex + 1;
|
|
2495
|
+
const pill = addImageButtonHtml(`${best.list}.${length}.${best.key}`);
|
|
2496
|
+
const tail = html.lastIndexOf("</section>");
|
|
2497
|
+
return tail >= 0 ? html.slice(0, tail) + pill + html.slice(tail) : html + pill;
|
|
2498
|
+
}
|
|
2499
|
+
__name(withAddImageFallback, "withAddImageFallback");
|
|
2431
2500
|
var STATEMENT_AT = /^\s*@[a-zA-Z-]+[^;{}]*;/;
|
|
2432
2501
|
function layerize(css) {
|
|
2433
2502
|
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,
|
|
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)
|
|
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)
|
|
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
|
|
1567
|
-
else out += `${prelude}{${scopeBlock(body, root)}}`;
|
|
1573
|
+
else out += `${prelude}{${scopeBlock(body, root, parents)}}`;
|
|
1568
1574
|
continue;
|
|
1569
1575
|
}
|
|
1570
|
-
const
|
|
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
|
-
|
|
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}
|
|
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.
|
|
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"]);
|
|
@@ -1860,6 +1875,25 @@ async function compile2(source, opts) {
|
|
|
1860
1875
|
lint.push({ level: "error", code: "tailwind", message: `Tailwind compile failed: ${e.message}` });
|
|
1861
1876
|
}
|
|
1862
1877
|
}
|
|
1878
|
+
if (!opts.skipTailwind && twCss) {
|
|
1879
|
+
const unknown = [];
|
|
1880
|
+
for (const t of classTokens) {
|
|
1881
|
+
if (!isCandidate(t)) continue;
|
|
1882
|
+
if (/^(group|peer)([/]|$)/.test(t)) continue;
|
|
1883
|
+
if (/^(js|wv)-/.test(t)) continue;
|
|
1884
|
+
if (userCss.includes(t) || script.includes(t)) continue;
|
|
1885
|
+
const cssName = "." + t.replace(/[^a-zA-Z0-9_-]/g, (ch) => "\\" + ch);
|
|
1886
|
+
const boundary = new RegExp(cssName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?![\\w-])");
|
|
1887
|
+
if (!boundary.test(twCss)) unknown.push(t);
|
|
1888
|
+
}
|
|
1889
|
+
if (unknown.length) {
|
|
1890
|
+
lint.push({
|
|
1891
|
+
level: "warning",
|
|
1892
|
+
code: "unknown-class",
|
|
1893
|
+
message: `class${unknown.length > 1 ? "es" : ""} '${unknown.join("', '")}' compile to no CSS: not a Tailwind utility, and neither this variant's <style> nor its <script> mentions ${unknown.length > 1 ? "them" : "it"}. Leftover from another CSS framework? Rewrite as Tailwind utilities/tokens or style it in <style>; a deliberate script hook belongs under a js- prefix`
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1863
1897
|
let css = "";
|
|
1864
1898
|
try {
|
|
1865
1899
|
css = scopeCss(twCss, ROOT_PLACEHOLDER) + (userCss.trim() ? "\n" + scopeCss(userCss, ROOT_PLACEHOLDER) : "");
|
|
@@ -2318,6 +2352,12 @@ function editUrlPillHtml(field, value, anchor = "below", extraClass = "") {
|
|
|
2318
2352
|
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
2353
|
}
|
|
2320
2354
|
__name(editUrlPillHtml, "editUrlPillHtml");
|
|
2355
|
+
function addImageButtonHtml(path, label = "Tambah Gambar", mode = "inline") {
|
|
2356
|
+
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");
|
|
2357
|
+
if (mode === "floating") return btn("webto-inline-add-btn absolute bottom-8 right-8 z-30");
|
|
2358
|
+
return `<div class="webto-add-row mt-8">${btn("webto-inline-add-btn")}</div>`;
|
|
2359
|
+
}
|
|
2360
|
+
__name(addImageButtonHtml, "addImageButtonHtml");
|
|
2321
2361
|
function renderComponent(n, scope, ctx, budget) {
|
|
2322
2362
|
const p = {};
|
|
2323
2363
|
for (const [k, v] of Object.entries(n.props)) p[k] = attrValue(v, scope, budget);
|
|
@@ -2332,6 +2372,13 @@ function renderComponent(n, scope, ctx, budget) {
|
|
|
2332
2372
|
}
|
|
2333
2373
|
case "EditUrlPill":
|
|
2334
2374
|
return editUrlPillHtml(stringify(p.field), stringify(p.value), stringify(p.anchor) || "below", flattenClassList(p.class));
|
|
2375
|
+
case "AddImageButton": {
|
|
2376
|
+
if (ctx.marks) ctx.marks.addImageButton = true;
|
|
2377
|
+
if (!ctx.editChrome || p.visible === false) return "";
|
|
2378
|
+
const path = stringify(p.path);
|
|
2379
|
+
if (!path) return "";
|
|
2380
|
+
return addImageButtonHtml(path, stringify(p.label) || "Tambah Gambar", p.mode === "floating" ? "floating" : "inline");
|
|
2381
|
+
}
|
|
2335
2382
|
case "ViewMoreLink": {
|
|
2336
2383
|
const url = stringify(p.url);
|
|
2337
2384
|
if (!url) return "";
|
|
@@ -2419,12 +2466,34 @@ function renderVariant(ir, content, ctx, opts = {}) {
|
|
|
2419
2466
|
scope[p.name] = v === void 0 && p.def ? evalExpr(p.def, scope, budget) : v;
|
|
2420
2467
|
}
|
|
2421
2468
|
for (const c of ir.consts) scope[c.name] = evalExpr(c.e, scope, budget);
|
|
2422
|
-
const
|
|
2469
|
+
const rctx = { ...ctx, marks: { addImageButton: false } };
|
|
2470
|
+
const html = withAddImageFallback(renderNodes(ir.root, scope, rctx, budget), content, rctx);
|
|
2423
2471
|
const css = ir.css ? layerize(ir.css.replace(/__WV_ROOT__/g, `[data-wv-inst="${uid}"]`)) : "";
|
|
2424
2472
|
const script = ir.script ? wrapScript(ir.script, uid) : "";
|
|
2425
2473
|
return { html, css, script, uid };
|
|
2426
2474
|
}
|
|
2427
2475
|
__name(renderVariant, "renderVariant");
|
|
2476
|
+
var EDIT_IMAGE_ITEM_RE = /data-edit-image="([A-Za-z_]\w*)\.(\d+)\.([A-Za-z_]\w*)"/g;
|
|
2477
|
+
function withAddImageFallback(html, content, ctx) {
|
|
2478
|
+
if (!ctx.editChrome || ctx.marks?.addImageButton || html.includes("data-edit-add-image=")) return html;
|
|
2479
|
+
const lists = /* @__PURE__ */ new Map();
|
|
2480
|
+
for (const m of html.matchAll(EDIT_IMAGE_ITEM_RE)) {
|
|
2481
|
+
const [, list, index, key] = m;
|
|
2482
|
+
const cur = lists.get(list) ?? { key, hits: 0, maxIndex: -1 };
|
|
2483
|
+
cur.hits += 1;
|
|
2484
|
+
cur.maxIndex = Math.max(cur.maxIndex, Number(index));
|
|
2485
|
+
lists.set(list, cur);
|
|
2486
|
+
}
|
|
2487
|
+
let best = null;
|
|
2488
|
+
for (const [list, v] of lists) if (!best || v.hits > best.hits) best = { list, ...v };
|
|
2489
|
+
if (!best) return html;
|
|
2490
|
+
const stored = content[best.list];
|
|
2491
|
+
const length = Array.isArray(stored) ? stored.length : best.maxIndex + 1;
|
|
2492
|
+
const pill = addImageButtonHtml(`${best.list}.${length}.${best.key}`);
|
|
2493
|
+
const tail = html.lastIndexOf("</section>");
|
|
2494
|
+
return tail >= 0 ? html.slice(0, tail) + pill + html.slice(tail) : html + pill;
|
|
2495
|
+
}
|
|
2496
|
+
__name(withAddImageFallback, "withAddImageFallback");
|
|
2428
2497
|
var STATEMENT_AT = /^\s*@[a-zA-Z-]+[^;{}]*;/;
|
|
2429
2498
|
function layerize(css) {
|
|
2430
2499
|
let rest = css;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webto-id/variant-check",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|