@webto-id/variant-check 0.1.6 → 0.1.7

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 CHANGED
@@ -1198,7 +1198,7 @@ function lintAttr(name, v, tag, ctx) {
1198
1198
  const s = staticValue(v);
1199
1199
  if (s !== null) {
1200
1200
  if (/url\s*\(|expression\s*\(|@import|behavior\s*:/i.test(s)) push(ctx, "error", "style-url", `inline style on <${tag}> must not use url()/expression()`);
1201
- if (/position\s*:\s*fixed/i.test(s) && ctx.sectionType !== "navbar" && ctx.sectionType !== "banner") push(ctx, "error", "fixed", `position: fixed is only allowed in navbar/banner variants`);
1201
+ if (/position\s*:\s*fixed/i.test(s)) push(ctx, "error", "fixed", `position: fixed is not allowed \u2014 the platform makes the section wrapper sticky (navbar) instead; keep the root in normal flow`);
1202
1202
  if (/#[0-9a-f]{3,8}\b/i.test(s) || /\b(rgb|hsl)a?\(/i.test(s)) push(ctx, "warning", "hardcoded-color", `inline style on <${tag}> hardcodes a color \u2014 use var(--color-*)`);
1203
1203
  if (/border-radius\s*:\s*\d/i.test(s)) push(ctx, "warning", "hardcoded-radius", `inline style on <${tag}> hardcodes border-radius \u2014 use var(--radius)`);
1204
1204
  if (/font-family\s*:(?![^;]*var\(--font-)/i.test(s)) push(ctx, "warning", "hardcoded-font", `inline style on <${tag}> hardcodes font-family \u2014 use var(--font-heading|--font-body)`);
@@ -1225,7 +1225,7 @@ __name(lintHtml, "lintHtml");
1225
1225
 
1226
1226
  // ../variant-compiler/src/lint/css.ts
1227
1227
  var ALLOWED_URL_HOSTS = ["images.unsplash.com", "images.pexels.com"];
1228
- function lintCss(css, sectionType2, attrs = {}) {
1228
+ function lintCss(css, _sectionType, attrs = {}) {
1229
1229
  const out = [];
1230
1230
  const err = /* @__PURE__ */ __name((code, message) => out.push({ level: "error", code, message }), "err");
1231
1231
  const warn = /* @__PURE__ */ __name((code, message) => out.push({ level: "warning", code, message }), "warn");
@@ -1238,7 +1238,7 @@ function lintCss(css, sectionType2, attrs = {}) {
1238
1238
  if (/@font-face\b/i.test(stripped)) err("css-font-face", "@font-face is not allowed \u2014 fonts come from the site theme (var(--font-heading|--font-body))");
1239
1239
  if (/expression\s*\(/i.test(stripped)) err("css-expression", "expression() is not allowed");
1240
1240
  if (/behavior\s*:|-moz-binding\s*:/i.test(stripped)) err("css-behavior", "behavior/-moz-binding are not allowed");
1241
- if (/\bposition\s*:\s*fixed\b/i.test(stripped) && sectionType2 !== "navbar" && sectionType2 !== "banner") err("fixed", "position: fixed is only allowed in navbar/banner variants");
1241
+ if (/\bposition\s*:\s*fixed\b/i.test(stripped)) err("fixed", "position: fixed is not allowed \u2014 the platform makes the section wrapper sticky (navbar) instead; keep the root in normal flow");
1242
1242
  for (const m of stripped.matchAll(/url\s*\(\s*(['"]?)([^'")]+)\1\s*\)/gi)) {
1243
1243
  const u = m[2].trim();
1244
1244
  if (/^data:image\/svg\+xml/i.test(u)) continue;
@@ -1739,6 +1739,19 @@ async function compileTailwind(candidates) {
1739
1739
  __name(compileTailwind, "compileTailwind");
1740
1740
 
1741
1741
  // ../variant-compiler/src/schema.ts
1742
+ var CHROME_CONTEXT_PROPS = /* @__PURE__ */ new Set([
1743
+ "pages",
1744
+ "currentSlug",
1745
+ "linkPrefix",
1746
+ "colorMode",
1747
+ "footerPages",
1748
+ "imageCredits"
1749
+ ]);
1750
+ var CHROME_SECTION_TYPES = /* @__PURE__ */ new Set(["navbar", "banner", "footer"]);
1751
+ var NEVER_HIDDEN = {
1752
+ products: ["source"],
1753
+ blog: ["source"]
1754
+ };
1742
1755
  function typeOf(s) {
1743
1756
  if (!s) return "unknown";
1744
1757
  const t = s.type;
@@ -1751,13 +1764,16 @@ function typeOf(s) {
1751
1764
  return "unknown";
1752
1765
  }
1753
1766
  __name(typeOf, "typeOf");
1754
- function deriveSchema(fields, props, base2) {
1767
+ function deriveSchema(fields, props, base2, sectionType2) {
1755
1768
  const lint = [];
1769
+ const isChrome = sectionType2 !== void 0 && CHROME_SECTION_TYPES.has(sectionType2);
1770
+ const isContextProp = /* @__PURE__ */ __name((key) => isChrome && CHROME_CONTEXT_PROPS.has(key), "isContextProp");
1756
1771
  const readKeys = new Set(props.map((p) => p.key));
1757
1772
  const declared = new Map(fields.map((f) => [f.key, f]));
1758
1773
  const extProps = {};
1759
1774
  const usedFields = [];
1760
1775
  for (const f of fields) {
1776
+ if (isContextProp(f.key)) continue;
1761
1777
  if (!f.optional) lint.push({ level: "warning", code: "props-optional", message: `prop '${f.key}' should be optional (\`${f.key}?:\`) \u2014 content may omit it` });
1762
1778
  const baseField = base2?.properties[f.key];
1763
1779
  if (baseField) {
@@ -1770,11 +1786,13 @@ function deriveSchema(fields, props, base2) {
1770
1786
  extProps[f.key] = { ...f.schema, ...f.description ? { description: f.description } : {} };
1771
1787
  }
1772
1788
  for (const k of readKeys) {
1789
+ if (isContextProp(k)) continue;
1773
1790
  if (!declared.has(k)) lint.push({ level: "warning", code: "props-undeclared", message: `'${k}' is read from Astro.props but not declared in interface Props` });
1774
1791
  else if (!base2?.properties[k] && !(k in extProps)) extProps[k] = { type: "string" };
1775
1792
  if (base2?.properties[k] && !usedFields.includes(k)) usedFields.push(k);
1776
1793
  }
1777
- const hiddenFields = base2 ? Object.keys(base2.properties).filter((k) => !usedFields.includes(k)) : [];
1794
+ const keepVisible = new Set(sectionType2 !== void 0 ? NEVER_HIDDEN[sectionType2] ?? [] : []);
1795
+ const hiddenFields = base2 ? Object.keys(base2.properties).filter((k) => !usedFields.includes(k) && !keepVisible.has(k)) : [];
1778
1796
  const extension = { type: "object", properties: extProps, additionalProperties: true };
1779
1797
  return { extension, hiddenFields, usedFields, lint };
1780
1798
  }
@@ -1904,7 +1922,7 @@ async function compile2(source2, opts) {
1904
1922
  lint.push({ level: "error", code: "css-parse", message: `could not scope CSS: ${e.message}` });
1905
1923
  }
1906
1924
  if (css.length > CSS_MAX_BYTES) lint.push({ level: "error", code: "css-size", message: `compiled CSS exceeds ${CSS_MAX_BYTES / 1024} KB` });
1907
- const sch = deriveSchema(front.fields, front.props, opts.base ?? null);
1925
+ const sch = deriveSchema(front.fields, front.props, opts.base ?? null, opts.sectionType);
1908
1926
  lint.push(...sch.lint);
1909
1927
  if (opts.strict) {
1910
1928
  for (const m of lint) if (m.level === "warning" && STRICT_CODES.has(m.code)) m.level = "error";
@@ -2470,7 +2488,12 @@ function renderVariant(ir, content, ctx, opts = {}) {
2470
2488
  }
2471
2489
  for (const c of ir.consts) scope[c.name] = evalExpr(c.e, scope, budget);
2472
2490
  const rctx = { ...ctx, marks: { addImageButton: false } };
2473
- const html = withAddImageFallback(renderNodes(ir.root, scope, rctx, budget), content, rctx);
2491
+ const isDbMode = (ir.sectionType === "products" || ir.sectionType === "blog") && content.source === "database";
2492
+ let rendered = renderNodes(ir.root, scope, rctx, budget);
2493
+ if (isDbMode) {
2494
+ rendered = rendered.replace(/<div class="webto-add-row[^"]*">\s*<button[^>]*data-edit-add-image="(?:products|posts)\.[^"]*"[\s\S]*?<\/button>\s*<\/div>/g, "").replace(/<button[^>]*data-edit-add-image="(?:products|posts)\.[^"]*"[\s\S]*?<\/button>/g, "").replace(/\s(?:data-edit-(?:field|image|add-image))="(?:products|posts)\.[^"]*"/g, "");
2495
+ }
2496
+ const html = withAddImageFallback(rendered, content, rctx);
2474
2497
  const css = ir.css ? layerize(ir.css.replace(/__WV_ROOT__/g, `[data-wv-inst="${uid}"]`)) : "";
2475
2498
  const script = ir.script ? wrapScript(ir.script, uid) : "";
2476
2499
  return { html, css, script, uid };
@@ -3729,6 +3752,114 @@ var base_schemas_default = {
3729
3752
  type: "string"
3730
3753
  }
3731
3754
  }
3755
+ },
3756
+ navbar: {
3757
+ properties: {
3758
+ siteName: {
3759
+ description: "Nama yang ditampilkan di navigasi",
3760
+ type: "string"
3761
+ },
3762
+ siteNameSize: {
3763
+ default: 18,
3764
+ description: "Ukuran font nama (px)",
3765
+ type: "number",
3766
+ minimum: 12,
3767
+ maximum: 48
3768
+ },
3769
+ logoUrl: {
3770
+ description: "URL logo (image or vectorized SVG)",
3771
+ type: "string"
3772
+ },
3773
+ logoUrlDark: {
3774
+ description: "Optional logo shown when the site is in dark mode \u2014 falls back to the main logo",
3775
+ type: "string"
3776
+ },
3777
+ showSiteNameWithLogo: {
3778
+ description: "Also show the site name text when a logo image is set",
3779
+ type: "boolean"
3780
+ },
3781
+ ctaText: {
3782
+ description: "Teks tombol CTA",
3783
+ type: "string"
3784
+ },
3785
+ ctaUrl: {
3786
+ description: "URL tombol CTA",
3787
+ type: "string"
3788
+ },
3789
+ links: {
3790
+ description: "Custom navbar links (shown alongside page links)",
3791
+ type: "array",
3792
+ items: {
3793
+ type: "object",
3794
+ properties: {
3795
+ label: {
3796
+ type: "string",
3797
+ description: "Link text"
3798
+ },
3799
+ url: {
3800
+ type: "string",
3801
+ description: "Link URL (e.g. /#pricing, /about, https://...)"
3802
+ }
3803
+ },
3804
+ required: [
3805
+ "label",
3806
+ "url"
3807
+ ],
3808
+ additionalProperties: false
3809
+ }
3810
+ }
3811
+ }
3812
+ },
3813
+ banner: {
3814
+ properties: {
3815
+ text: {
3816
+ type: "string",
3817
+ description: "Banner announcement text"
3818
+ },
3819
+ ctaText: {
3820
+ description: "Call-to-action button text",
3821
+ type: "string"
3822
+ },
3823
+ ctaUrl: {
3824
+ description: "Call-to-action button URL",
3825
+ type: "string"
3826
+ },
3827
+ icon: {
3828
+ description: "Icon emoji or name",
3829
+ type: "string"
3830
+ }
3831
+ }
3832
+ },
3833
+ footer: {
3834
+ properties: {
3835
+ text: {
3836
+ default: "\xA9 2025 All rights reserved.",
3837
+ description: "Footer copyright text",
3838
+ type: "string"
3839
+ },
3840
+ links: {
3841
+ description: "Footer navigation links",
3842
+ type: "array",
3843
+ items: {
3844
+ type: "object",
3845
+ properties: {
3846
+ label: {
3847
+ type: "string",
3848
+ description: "Link text"
3849
+ },
3850
+ url: {
3851
+ type: "string",
3852
+ description: "Link URL"
3853
+ }
3854
+ },
3855
+ required: [
3856
+ "label",
3857
+ "url"
3858
+ ],
3859
+ additionalProperties: false
3860
+ }
3861
+ }
3862
+ }
3732
3863
  }
3733
3864
  };
3734
3865
 
package/dist/index.js CHANGED
@@ -1195,7 +1195,7 @@ function lintAttr(name, v, tag, ctx) {
1195
1195
  const s = staticValue(v);
1196
1196
  if (s !== null) {
1197
1197
  if (/url\s*\(|expression\s*\(|@import|behavior\s*:/i.test(s)) push(ctx, "error", "style-url", `inline style on <${tag}> must not use url()/expression()`);
1198
- if (/position\s*:\s*fixed/i.test(s) && ctx.sectionType !== "navbar" && ctx.sectionType !== "banner") push(ctx, "error", "fixed", `position: fixed is only allowed in navbar/banner variants`);
1198
+ if (/position\s*:\s*fixed/i.test(s)) push(ctx, "error", "fixed", `position: fixed is not allowed \u2014 the platform makes the section wrapper sticky (navbar) instead; keep the root in normal flow`);
1199
1199
  if (/#[0-9a-f]{3,8}\b/i.test(s) || /\b(rgb|hsl)a?\(/i.test(s)) push(ctx, "warning", "hardcoded-color", `inline style on <${tag}> hardcodes a color \u2014 use var(--color-*)`);
1200
1200
  if (/border-radius\s*:\s*\d/i.test(s)) push(ctx, "warning", "hardcoded-radius", `inline style on <${tag}> hardcodes border-radius \u2014 use var(--radius)`);
1201
1201
  if (/font-family\s*:(?![^;]*var\(--font-)/i.test(s)) push(ctx, "warning", "hardcoded-font", `inline style on <${tag}> hardcodes font-family \u2014 use var(--font-heading|--font-body)`);
@@ -1222,7 +1222,7 @@ __name(lintHtml, "lintHtml");
1222
1222
 
1223
1223
  // ../variant-compiler/src/lint/css.ts
1224
1224
  var ALLOWED_URL_HOSTS = ["images.unsplash.com", "images.pexels.com"];
1225
- function lintCss(css, sectionType, attrs = {}) {
1225
+ function lintCss(css, _sectionType, attrs = {}) {
1226
1226
  const out = [];
1227
1227
  const err = /* @__PURE__ */ __name((code, message) => out.push({ level: "error", code, message }), "err");
1228
1228
  const warn = /* @__PURE__ */ __name((code, message) => out.push({ level: "warning", code, message }), "warn");
@@ -1235,7 +1235,7 @@ function lintCss(css, sectionType, attrs = {}) {
1235
1235
  if (/@font-face\b/i.test(stripped)) err("css-font-face", "@font-face is not allowed \u2014 fonts come from the site theme (var(--font-heading|--font-body))");
1236
1236
  if (/expression\s*\(/i.test(stripped)) err("css-expression", "expression() is not allowed");
1237
1237
  if (/behavior\s*:|-moz-binding\s*:/i.test(stripped)) err("css-behavior", "behavior/-moz-binding are not allowed");
1238
- if (/\bposition\s*:\s*fixed\b/i.test(stripped) && sectionType !== "navbar" && sectionType !== "banner") err("fixed", "position: fixed is only allowed in navbar/banner variants");
1238
+ if (/\bposition\s*:\s*fixed\b/i.test(stripped)) err("fixed", "position: fixed is not allowed \u2014 the platform makes the section wrapper sticky (navbar) instead; keep the root in normal flow");
1239
1239
  for (const m of stripped.matchAll(/url\s*\(\s*(['"]?)([^'")]+)\1\s*\)/gi)) {
1240
1240
  const u = m[2].trim();
1241
1241
  if (/^data:image\/svg\+xml/i.test(u)) continue;
@@ -1736,6 +1736,19 @@ async function compileTailwind(candidates) {
1736
1736
  __name(compileTailwind, "compileTailwind");
1737
1737
 
1738
1738
  // ../variant-compiler/src/schema.ts
1739
+ var CHROME_CONTEXT_PROPS = /* @__PURE__ */ new Set([
1740
+ "pages",
1741
+ "currentSlug",
1742
+ "linkPrefix",
1743
+ "colorMode",
1744
+ "footerPages",
1745
+ "imageCredits"
1746
+ ]);
1747
+ var CHROME_SECTION_TYPES = /* @__PURE__ */ new Set(["navbar", "banner", "footer"]);
1748
+ var NEVER_HIDDEN = {
1749
+ products: ["source"],
1750
+ blog: ["source"]
1751
+ };
1739
1752
  function typeOf(s) {
1740
1753
  if (!s) return "unknown";
1741
1754
  const t = s.type;
@@ -1748,13 +1761,16 @@ function typeOf(s) {
1748
1761
  return "unknown";
1749
1762
  }
1750
1763
  __name(typeOf, "typeOf");
1751
- function deriveSchema(fields, props, base) {
1764
+ function deriveSchema(fields, props, base, sectionType) {
1752
1765
  const lint = [];
1766
+ const isChrome = sectionType !== void 0 && CHROME_SECTION_TYPES.has(sectionType);
1767
+ const isContextProp = /* @__PURE__ */ __name((key) => isChrome && CHROME_CONTEXT_PROPS.has(key), "isContextProp");
1753
1768
  const readKeys = new Set(props.map((p) => p.key));
1754
1769
  const declared = new Map(fields.map((f) => [f.key, f]));
1755
1770
  const extProps = {};
1756
1771
  const usedFields = [];
1757
1772
  for (const f of fields) {
1773
+ if (isContextProp(f.key)) continue;
1758
1774
  if (!f.optional) lint.push({ level: "warning", code: "props-optional", message: `prop '${f.key}' should be optional (\`${f.key}?:\`) \u2014 content may omit it` });
1759
1775
  const baseField = base?.properties[f.key];
1760
1776
  if (baseField) {
@@ -1767,11 +1783,13 @@ function deriveSchema(fields, props, base) {
1767
1783
  extProps[f.key] = { ...f.schema, ...f.description ? { description: f.description } : {} };
1768
1784
  }
1769
1785
  for (const k of readKeys) {
1786
+ if (isContextProp(k)) continue;
1770
1787
  if (!declared.has(k)) lint.push({ level: "warning", code: "props-undeclared", message: `'${k}' is read from Astro.props but not declared in interface Props` });
1771
1788
  else if (!base?.properties[k] && !(k in extProps)) extProps[k] = { type: "string" };
1772
1789
  if (base?.properties[k] && !usedFields.includes(k)) usedFields.push(k);
1773
1790
  }
1774
- const hiddenFields = base ? Object.keys(base.properties).filter((k) => !usedFields.includes(k)) : [];
1791
+ const keepVisible = new Set(sectionType !== void 0 ? NEVER_HIDDEN[sectionType] ?? [] : []);
1792
+ const hiddenFields = base ? Object.keys(base.properties).filter((k) => !usedFields.includes(k) && !keepVisible.has(k)) : [];
1775
1793
  const extension = { type: "object", properties: extProps, additionalProperties: true };
1776
1794
  return { extension, hiddenFields, usedFields, lint };
1777
1795
  }
@@ -1901,7 +1919,7 @@ async function compile2(source, opts) {
1901
1919
  lint.push({ level: "error", code: "css-parse", message: `could not scope CSS: ${e.message}` });
1902
1920
  }
1903
1921
  if (css.length > CSS_MAX_BYTES) lint.push({ level: "error", code: "css-size", message: `compiled CSS exceeds ${CSS_MAX_BYTES / 1024} KB` });
1904
- const sch = deriveSchema(front.fields, front.props, opts.base ?? null);
1922
+ const sch = deriveSchema(front.fields, front.props, opts.base ?? null, opts.sectionType);
1905
1923
  lint.push(...sch.lint);
1906
1924
  if (opts.strict) {
1907
1925
  for (const m of lint) if (m.level === "warning" && STRICT_CODES.has(m.code)) m.level = "error";
@@ -2467,7 +2485,12 @@ function renderVariant(ir, content, ctx, opts = {}) {
2467
2485
  }
2468
2486
  for (const c of ir.consts) scope[c.name] = evalExpr(c.e, scope, budget);
2469
2487
  const rctx = { ...ctx, marks: { addImageButton: false } };
2470
- const html = withAddImageFallback(renderNodes(ir.root, scope, rctx, budget), content, rctx);
2488
+ const isDbMode = (ir.sectionType === "products" || ir.sectionType === "blog") && content.source === "database";
2489
+ let rendered = renderNodes(ir.root, scope, rctx, budget);
2490
+ if (isDbMode) {
2491
+ rendered = rendered.replace(/<div class="webto-add-row[^"]*">\s*<button[^>]*data-edit-add-image="(?:products|posts)\.[^"]*"[\s\S]*?<\/button>\s*<\/div>/g, "").replace(/<button[^>]*data-edit-add-image="(?:products|posts)\.[^"]*"[\s\S]*?<\/button>/g, "").replace(/\s(?:data-edit-(?:field|image|add-image))="(?:products|posts)\.[^"]*"/g, "");
2492
+ }
2493
+ const html = withAddImageFallback(rendered, content, rctx);
2471
2494
  const css = ir.css ? layerize(ir.css.replace(/__WV_ROOT__/g, `[data-wv-inst="${uid}"]`)) : "";
2472
2495
  const script = ir.script ? wrapScript(ir.script, uid) : "";
2473
2496
  return { html, css, script, uid };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webto-id/variant-check",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",