@kubuild/renderer 0.0.1 → 0.1.0

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/index.js CHANGED
@@ -215,7 +215,7 @@ function dispatchAction(options) {
215
215
 
216
216
  // src/renderer.tsx
217
217
  import { resolveBinding as resolveBinding3, sanitizeUrl, sanitizeHtml } from "@kubuild/core";
218
- import { icons as lucideIcons } from "lucide-react";
218
+ import { icons as lucideIcons, Package, Puzzle, AlertTriangle as AlertTriangle2 } from "lucide-react";
219
219
 
220
220
  // src/styles.ts
221
221
  function resolveNodeStyles(styles, viewport = "desktop") {
@@ -230,17 +230,18 @@ function escapeCssValue(value) {
230
230
  function escapeCssIdent(value) {
231
231
  return value.replace(/["\\\]]/g, "\\$&");
232
232
  }
233
- function styleDefinitionToCssDeclarations(styleDefinition) {
233
+ function styleDefinitionToCssDeclarations(styleDefinition, options) {
234
234
  if (!styleDefinition || typeof styleDefinition !== "object") return "";
235
235
  const declarations = [];
236
+ const suffix = options?.important ? " !important" : "";
236
237
  for (const [key, value] of Object.entries(styleDefinition)) {
237
238
  if (value === null || value === void 0 || value === "") continue;
238
239
  const property = key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
239
- declarations.push(`${property}: ${escapeCssValue(value)};`);
240
+ declarations.push(`${property}: ${escapeCssValue(value)}${suffix};`);
240
241
  }
241
242
  return declarations.join(" ");
242
243
  }
243
- function collectStateStylesCss(document) {
244
+ function collectStateStylesCss(document, options = { important: true }) {
244
245
  if (!document?.document) return "";
245
246
  const rules = [];
246
247
  const walk = (node) => {
@@ -248,7 +249,8 @@ function collectStateStylesCss(document) {
248
249
  if (states && typeof states === "object") {
249
250
  for (const [state, styleDefinition] of Object.entries(states)) {
250
251
  const declarations = styleDefinitionToCssDeclarations(
251
- styleDefinition
252
+ styleDefinition,
253
+ { important: options.important !== false }
252
254
  );
253
255
  if (!declarations) continue;
254
256
  const safeState = /^::?[a-zA-Z-]+$/.test(state) ? state : null;
@@ -289,8 +291,208 @@ p, h1, h2, h3, h4, h5, h6 {
289
291
  }
290
292
  `.trim();
291
293
 
294
+ // src/animation.ts
295
+ var ANIMATION_KEYFRAMES_CSS = `
296
+ /* Entrance / AOS Keyframes */
297
+ @keyframes kb-anim-fade {
298
+ from { opacity: 0; }
299
+ to { opacity: 1; }
300
+ }
301
+ @keyframes kb-anim-fade-up {
302
+ from { opacity: 0; transform: translateY(24px); }
303
+ to { opacity: 1; transform: translateY(0); }
304
+ }
305
+ @keyframes kb-anim-fade-down {
306
+ from { opacity: 0; transform: translateY(-24px); }
307
+ to { opacity: 1; transform: translateY(0); }
308
+ }
309
+ @keyframes kb-anim-fade-left {
310
+ from { opacity: 0; transform: translateX(24px); }
311
+ to { opacity: 1; transform: translateX(0); }
312
+ }
313
+ @keyframes kb-anim-fade-right {
314
+ from { opacity: 0; transform: translateX(-24px); }
315
+ to { opacity: 1; transform: translateX(0); }
316
+ }
317
+ @keyframes kb-anim-zoom-in {
318
+ from { opacity: 0; transform: scale(0.92); }
319
+ to { opacity: 1; transform: scale(1); }
320
+ }
321
+ @keyframes kb-anim-zoom-out {
322
+ from { opacity: 0; transform: scale(1.08); }
323
+ to { opacity: 1; transform: scale(1); }
324
+ }
325
+ @keyframes kb-anim-slide-up {
326
+ from { transform: translateY(100%); }
327
+ to { transform: translateY(0); }
328
+ }
329
+ @keyframes kb-anim-slide-down {
330
+ from { transform: translateY(-100%); }
331
+ to { transform: translateY(0); }
332
+ }
333
+ @keyframes kb-anim-slide-left {
334
+ from { transform: translateX(100%); }
335
+ to { transform: translateX(0); }
336
+ }
337
+ @keyframes kb-anim-slide-right {
338
+ from { transform: translateX(-100%); }
339
+ to { transform: translateX(0); }
340
+ }
341
+ @keyframes kb-anim-flip-up {
342
+ from { opacity: 0; transform: perspective(600px) rotateX(45deg); }
343
+ to { opacity: 1; transform: perspective(600px) rotateX(0deg); }
344
+ }
345
+ @keyframes kb-anim-flip-down {
346
+ from { opacity: 0; transform: perspective(600px) rotateX(-45deg); }
347
+ to { opacity: 1; transform: perspective(600px) rotateX(0deg); }
348
+ }
349
+
350
+ /* Continuous Loop Keyframes */
351
+ @keyframes kb-loop-pulse {
352
+ 0%, 100% { transform: scale(1); opacity: 1; }
353
+ 50% { transform: scale(1.05); opacity: 0.9; }
354
+ }
355
+ @keyframes kb-loop-bounce {
356
+ 0%, 100% { transform: translateY(0); }
357
+ 50% { transform: translateY(-8px); }
358
+ }
359
+ @keyframes kb-loop-spin {
360
+ from { transform: rotate(0deg); }
361
+ to { transform: rotate(360deg); }
362
+ }
363
+ @keyframes kb-loop-float {
364
+ 0%, 100% { transform: translateY(0); }
365
+ 50% { transform: translateY(-6px); }
366
+ }
367
+ @keyframes kb-loop-shimmer {
368
+ 0%, 100% { opacity: 0.75; }
369
+ 50% { opacity: 1; }
370
+ }
371
+ `.trim();
372
+ function escapeCssIdent2(value) {
373
+ return value.replace(/["\\\]]/g, "\\$&");
374
+ }
375
+ function getHoverEffectCss(nodeId, hoverEffect) {
376
+ const safeId = escapeCssIdent2(nodeId);
377
+ const rules = [];
378
+ switch (hoverEffect) {
379
+ case "lift":
380
+ rules.push(
381
+ `[data-kubuild-node="${safeId}"] { transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s ease !important; will-change: transform; }`,
382
+ `[data-kubuild-node="${safeId}"]:hover { transform: translateY(-4px) !important; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1) !important; }`
383
+ );
384
+ break;
385
+ case "scale":
386
+ rules.push(
387
+ `[data-kubuild-node="${safeId}"] { transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important; will-change: transform; }`,
388
+ `[data-kubuild-node="${safeId}"]:hover { transform: scale(1.04) !important; }`
389
+ );
390
+ break;
391
+ case "glow":
392
+ rules.push(
393
+ `[data-kubuild-node="${safeId}"] { transition: box-shadow 0.25s ease !important; }`,
394
+ `[data-kubuild-node="${safeId}"]:hover { box-shadow: 0 0 20px 2px rgba(59, 130, 246, 0.5) !important; }`
395
+ );
396
+ break;
397
+ case "tilt":
398
+ rules.push(
399
+ `[data-kubuild-node="${safeId}"] { transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important; will-change: transform; }`,
400
+ `[data-kubuild-node="${safeId}"]:hover { transform: rotate(2deg) scale(1.02) !important; }`
401
+ );
402
+ break;
403
+ default:
404
+ break;
405
+ }
406
+ return rules;
407
+ }
408
+ function getLoopEffectCss(nodeId, loopEffect) {
409
+ const safeId = escapeCssIdent2(nodeId);
410
+ const rules = [];
411
+ switch (loopEffect) {
412
+ case "pulse":
413
+ rules.push(
414
+ `[data-kubuild-node="${safeId}"] { animation: kb-loop-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important; }`
415
+ );
416
+ break;
417
+ case "bounce":
418
+ rules.push(
419
+ `[data-kubuild-node="${safeId}"] { animation: kb-loop-bounce 1.5s ease-in-out infinite !important; }`
420
+ );
421
+ break;
422
+ case "spin":
423
+ rules.push(
424
+ `[data-kubuild-node="${safeId}"] { animation: kb-loop-spin 3s linear infinite !important; }`
425
+ );
426
+ break;
427
+ case "float":
428
+ rules.push(
429
+ `[data-kubuild-node="${safeId}"] { animation: kb-loop-float 3s ease-in-out infinite !important; }`
430
+ );
431
+ break;
432
+ case "shimmer":
433
+ rules.push(
434
+ `[data-kubuild-node="${safeId}"] { animation: kb-loop-shimmer 2s ease-in-out infinite !important; }`
435
+ );
436
+ break;
437
+ default:
438
+ break;
439
+ }
440
+ return rules;
441
+ }
442
+ function getEntranceAnimationCss(nodeId, anim) {
443
+ if (!anim.type || anim.type === "none") return [];
444
+ const safeId = escapeCssIdent2(nodeId);
445
+ const duration = typeof anim.duration === "number" ? anim.duration : 600;
446
+ const delay = typeof anim.delay === "number" ? anim.delay : 0;
447
+ const easing = anim.easing || "ease-out";
448
+ return [
449
+ `[data-kubuild-node="${safeId}"] { animation-name: kb-anim-${anim.type} !important; animation-duration: ${duration}ms !important; animation-delay: ${delay}ms !important; animation-timing-function: ${easing} !important; animation-fill-mode: both !important; }`
450
+ ];
451
+ }
452
+ function collectAnimationStylesCss(document) {
453
+ if (!document?.document) return "";
454
+ const rules = [];
455
+ let hasAnyAnimation = false;
456
+ const walk = (node) => {
457
+ const anim = node.animation;
458
+ if (anim) {
459
+ if (anim.hoverEffect && anim.hoverEffect !== "none") {
460
+ rules.push(...getHoverEffectCss(node.id, anim.hoverEffect));
461
+ hasAnyAnimation = true;
462
+ }
463
+ if (anim.loopEffect && anim.loopEffect !== "none") {
464
+ rules.push(...getLoopEffectCss(node.id, anim.loopEffect));
465
+ hasAnyAnimation = true;
466
+ }
467
+ if (anim.type && anim.type !== "none") {
468
+ rules.push(...getEntranceAnimationCss(node.id, anim));
469
+ hasAnyAnimation = true;
470
+ }
471
+ }
472
+ node.children?.forEach(walk);
473
+ };
474
+ walk(document.document);
475
+ if (!hasAnyAnimation) return "";
476
+ return `${ANIMATION_KEYFRAMES_CSS}
477
+
478
+ ${rules.join("\n")}`;
479
+ }
480
+ function replayNodeAnimation(nodeId, rootElement) {
481
+ const root = rootElement || (typeof window !== "undefined" ? window.document : null);
482
+ if (!root) return false;
483
+ const el = root.querySelector(`[data-kubuild-node="${escapeCssIdent2(nodeId)}"]`);
484
+ if (!el) return false;
485
+ const currentAnimation = el.style.animation;
486
+ el.style.animation = "none";
487
+ void el.offsetWidth;
488
+ el.style.animation = currentAnimation;
489
+ el.dispatchEvent(new CustomEvent("kubuild:replay-animation", { bubbles: true, detail: { nodeId } }));
490
+ return true;
491
+ }
492
+
292
493
  // src/error-boundary.tsx
293
494
  import { Component } from "react";
495
+ import { AlertTriangle } from "lucide-react";
294
496
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
295
497
  var ComponentErrorBoundary = class extends Component {
296
498
  constructor(props) {
@@ -327,10 +529,13 @@ var ComponentErrorBoundary = class extends Component {
327
529
  lineHeight: "1.4"
328
530
  },
329
531
  children: [
330
- /* @__PURE__ */ jsxs("div", { style: { fontWeight: 600, marginBottom: "4px" }, children: [
331
- "\u26A0\uFE0F Component Render Error: <",
332
- componentType,
333
- ">"
532
+ /* @__PURE__ */ jsxs("div", { style: { fontWeight: 600, marginBottom: "4px", display: "flex", alignItems: "center", gap: "6px" }, children: [
533
+ /* @__PURE__ */ jsx2(AlertTriangle, { size: 14, "aria-hidden": "true" }),
534
+ /* @__PURE__ */ jsxs("span", { children: [
535
+ "Component Render Error: <",
536
+ componentType,
537
+ ">"
538
+ ] })
334
539
  ] }),
335
540
  /* @__PURE__ */ jsxs("div", { style: { fontSize: "11px", color: "#7f1d1d", wordBreak: "break-all" }, children: [
336
541
  "Node ID: ",
@@ -1589,9 +1794,12 @@ function NodeRenderer({
1589
1794
  fontFamily: "system-ui, -apple-system, sans-serif"
1590
1795
  },
1591
1796
  onClick: handleClick,
1592
- children: /* @__PURE__ */ jsxs2("div", { style: { fontWeight: 600, fontSize: "13px" }, children: [
1593
- "\u{1F4E6} Collection: expected an array at ",
1594
- /* @__PURE__ */ jsx3("code", { children: sourceKey ?? "(missing sourceKey)" })
1797
+ children: /* @__PURE__ */ jsxs2("div", { style: { fontWeight: 600, fontSize: "13px", display: "flex", alignItems: "center", gap: "6px" }, children: [
1798
+ /* @__PURE__ */ jsx3(Package, { size: 14, "aria-hidden": "true" }),
1799
+ /* @__PURE__ */ jsxs2("span", { children: [
1800
+ "Collection: expected an array at ",
1801
+ /* @__PURE__ */ jsx3("code", { children: sourceKey ?? "(missing sourceKey)" })
1802
+ ] })
1595
1803
  ] })
1596
1804
  }
1597
1805
  );
@@ -1642,9 +1850,12 @@ function NodeRenderer({
1642
1850
  },
1643
1851
  onClick: handleClick,
1644
1852
  children: [
1645
- /* @__PURE__ */ jsxs2("div", { style: { fontWeight: 600, fontSize: "13px", marginBottom: "4px" }, children: [
1646
- "\u{1F9E9} Unknown Component: ",
1647
- /* @__PURE__ */ jsx3("code", { children: node.type })
1853
+ /* @__PURE__ */ jsxs2("div", { style: { fontWeight: 600, fontSize: "13px", marginBottom: "4px", display: "flex", alignItems: "center", gap: "6px" }, children: [
1854
+ /* @__PURE__ */ jsx3(Puzzle, { size: 14, "aria-hidden": "true" }),
1855
+ /* @__PURE__ */ jsxs2("span", { children: [
1856
+ "Unknown Component: ",
1857
+ /* @__PURE__ */ jsx3("code", { children: node.type })
1858
+ ] })
1648
1859
  ] }),
1649
1860
  /* @__PURE__ */ jsxs2("div", { style: { fontSize: "11px", color: "#b45309", marginBottom: childrenElements ? "8px" : 0 }, children: [
1650
1861
  "Node ID: ",
@@ -1690,10 +1901,13 @@ function NodeRenderer({
1690
1901
  lineHeight: "1.4"
1691
1902
  },
1692
1903
  children: [
1693
- /* @__PURE__ */ jsxs2("div", { style: { fontWeight: 600, marginBottom: "4px" }, children: [
1694
- "\u26A0\uFE0F Component Render Error: <",
1695
- node.type,
1696
- ">"
1904
+ /* @__PURE__ */ jsxs2("div", { style: { fontWeight: 600, marginBottom: "4px", display: "flex", alignItems: "center", gap: "6px" }, children: [
1905
+ /* @__PURE__ */ jsx3(AlertTriangle2, { size: 14, "aria-hidden": "true" }),
1906
+ /* @__PURE__ */ jsxs2("span", { children: [
1907
+ "Component Render Error: <",
1908
+ node.type,
1909
+ ">"
1910
+ ] })
1697
1911
  ] }),
1698
1912
  /* @__PURE__ */ jsxs2("div", { style: { fontSize: "11px", color: "#7f1d1d", wordBreak: "break-all" }, children: [
1699
1913
  "Node ID: ",
@@ -1738,6 +1952,10 @@ var KubuildRenderer = ({
1738
1952
  const css = collectStateStylesCss(document);
1739
1953
  return css ? /* @__PURE__ */ jsx3("style", { "data-kubuild-state-styles": true, children: css }) : null;
1740
1954
  })(),
1955
+ (() => {
1956
+ const animCss = collectAnimationStylesCss(document);
1957
+ return animCss ? /* @__PURE__ */ jsx3("style", { "data-kubuild-animation-styles": true, children: animCss }) : null;
1958
+ })(),
1741
1959
  /* @__PURE__ */ jsx3(
1742
1960
  NodeRenderer,
1743
1961
  {
@@ -1997,7 +2215,503 @@ var PreviewViewportAdapter = ({
1997
2215
  );
1998
2216
  };
1999
2217
  var KubuildPreviewViewport = PreviewViewportAdapter;
2218
+
2219
+ // src/code-generator.ts
2220
+ function escapeHtml(str) {
2221
+ if (str === null || str === void 0) return "";
2222
+ return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2223
+ }
2224
+ function escapeAttr(str) {
2225
+ return escapeHtml(str);
2226
+ }
2227
+ function getYouTubeId2(url) {
2228
+ if (!url || typeof url !== "string") return null;
2229
+ const match = url.match(
2230
+ /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i
2231
+ );
2232
+ return match ? match[1] : null;
2233
+ }
2234
+ function getVimeoId2(url) {
2235
+ if (!url || typeof url !== "string") return null;
2236
+ const match = url.match(
2237
+ /(?:vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|album\/(\d+)\/video\/|video\/|))(\d+)/i
2238
+ );
2239
+ return match ? match[3] : null;
2240
+ }
2241
+ function renderNodeToHtml(node, indentLevel, options) {
2242
+ const indent = " ".repeat(indentLevel * (options.indentSize ?? 2));
2243
+ const innerIndent = " ".repeat((indentLevel + 1) * (options.indentSize ?? 2));
2244
+ const props = node.props || {};
2245
+ const children = node.children || [];
2246
+ const includeNodeClass = options.includeNodeClasses !== false;
2247
+ const nodeClass = includeNodeClass ? `kb-node-${node.id}` : "";
2248
+ const buildClass = (...classes) => {
2249
+ return classes.filter(Boolean).join(" ");
2250
+ };
2251
+ const idAttr = props.id ? ` id="${escapeAttr(props.id)}"` : "";
2252
+ switch (node.type) {
2253
+ case "page": {
2254
+ const tag = options.rootTag || "main";
2255
+ const cls = buildClass("kb-page", nodeClass);
2256
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2257
+ if (!childHtml) {
2258
+ return `${indent}<${tag} class="${cls}"${idAttr}></${tag}>`;
2259
+ }
2260
+ return `${indent}<${tag} class="${cls}"${idAttr}>
2261
+ ${childHtml}
2262
+ ${indent}</${tag}>`;
2263
+ }
2264
+ case "section": {
2265
+ const ariaLabel = props.ariaLabel ? ` aria-label="${escapeAttr(props.ariaLabel)}"` : "";
2266
+ const cls = buildClass("kb-section", nodeClass);
2267
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2268
+ if (!childHtml) {
2269
+ return `${indent}<section class="${cls}"${idAttr}${ariaLabel}></section>`;
2270
+ }
2271
+ return `${indent}<section class="${cls}"${idAttr}${ariaLabel}>
2272
+ ${childHtml}
2273
+ ${indent}</section>`;
2274
+ }
2275
+ case "container": {
2276
+ const cls = buildClass("kb-container", nodeClass);
2277
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2278
+ if (!childHtml) {
2279
+ return `${indent}<div class="${cls}"${idAttr}></div>`;
2280
+ }
2281
+ return `${indent}<div class="${cls}"${idAttr}>
2282
+ ${childHtml}
2283
+ ${indent}</div>`;
2284
+ }
2285
+ case "columns": {
2286
+ const cls = buildClass("kb-columns", nodeClass);
2287
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2288
+ if (!childHtml) {
2289
+ return `${indent}<div class="${cls}"${idAttr}></div>`;
2290
+ }
2291
+ return `${indent}<div class="${cls}"${idAttr}>
2292
+ ${childHtml}
2293
+ ${indent}</div>`;
2294
+ }
2295
+ case "heading": {
2296
+ let level = "h2";
2297
+ if (typeof props.level === "string" && /^h[1-6]$/i.test(props.level)) {
2298
+ level = props.level.toLowerCase();
2299
+ } else if (typeof props.level === "number" && props.level >= 1 && props.level <= 6) {
2300
+ level = `h${props.level}`;
2301
+ } else if (typeof props.tag === "string" && /^h[1-6]$/i.test(props.tag)) {
2302
+ level = props.tag.toLowerCase();
2303
+ }
2304
+ const text = props.text ?? props.value ?? props.content ?? "Heading";
2305
+ const cls = buildClass("kb-heading", nodeClass);
2306
+ return `${indent}<${level} class="${cls}"${idAttr}>${escapeHtml(text)}</${level}>`;
2307
+ }
2308
+ case "paragraph": {
2309
+ const text = props.text ?? props.value ?? props.content ?? "";
2310
+ const cls = buildClass("kb-paragraph", nodeClass);
2311
+ return `${indent}<p class="${cls}"${idAttr}>${escapeHtml(text)}</p>`;
2312
+ }
2313
+ case "text": {
2314
+ const tag = props.as || "p";
2315
+ const text = props.text ?? props.value ?? props.content ?? "";
2316
+ const cls = buildClass("kb-text", nodeClass);
2317
+ return `${indent}<${tag} class="${cls}"${idAttr}>${escapeHtml(text)}</${tag}>`;
2318
+ }
2319
+ case "link": {
2320
+ const href = props.href ? ` href="${escapeAttr(props.href)}"` : ' href="#"';
2321
+ const target = props.target ? ` target="${escapeAttr(props.target)}"` : "";
2322
+ const rel = props.rel ? ` rel="${escapeAttr(props.rel)}"` : target.includes("_blank") ? ' rel="noopener noreferrer"' : "";
2323
+ const text = props.text ?? props.label ?? props.value;
2324
+ const cls = buildClass("kb-link", nodeClass);
2325
+ if (children.length > 0) {
2326
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2327
+ return `${indent}<a class="${cls}"${idAttr}${href}${target}${rel}>
2328
+ ${childHtml}
2329
+ ${indent}</a>`;
2330
+ }
2331
+ return `${indent}<a class="${cls}"${idAttr}${href}${target}${rel}>${escapeHtml(text ?? "Link")}</a>`;
2332
+ }
2333
+ case "blockquote": {
2334
+ const cite = props.cite ? ` cite="${escapeAttr(props.cite)}"` : "";
2335
+ const quote = props.quote ?? props.text ?? props.value;
2336
+ const author = props.author ?? props.citeAuthor;
2337
+ const cls = buildClass("kb-blockquote", nodeClass);
2338
+ if (quote || author) {
2339
+ const quoteHtml = quote ? `${innerIndent}<p>${escapeHtml(quote)}</p>` : "";
2340
+ const authorHtml = author ? `${innerIndent}<cite>${escapeHtml(author)}</cite>` : "";
2341
+ const parts = [quoteHtml, authorHtml].filter(Boolean).join("\n");
2342
+ return `${indent}<blockquote class="${cls}"${idAttr}${cite}>
2343
+ ${parts}
2344
+ ${indent}</blockquote>`;
2345
+ }
2346
+ if (children.length > 0) {
2347
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2348
+ return `${indent}<blockquote class="${cls}"${idAttr}${cite}>
2349
+ ${childHtml}
2350
+ ${indent}</blockquote>`;
2351
+ }
2352
+ return `${indent}<blockquote class="${cls}"${idAttr}${cite}></blockquote>`;
2353
+ }
2354
+ case "badge": {
2355
+ const text = props.text ?? props.label ?? props.value ?? "Badge";
2356
+ const cls = buildClass("kb-badge", nodeClass);
2357
+ return `${indent}<span class="${cls}"${idAttr}>${escapeHtml(text)}</span>`;
2358
+ }
2359
+ case "code-block": {
2360
+ const code = props.code ?? props.text ?? props.value ?? "";
2361
+ const lang = props.language || props.lang;
2362
+ const langClass = lang ? ` class="language-${escapeAttr(lang)}"` : "";
2363
+ const cls = buildClass("kb-code-block", nodeClass);
2364
+ return `${indent}<pre class="${cls}"${idAttr}><code${langClass}>${escapeHtml(code)}</code></pre>`;
2365
+ }
2366
+ case "divider": {
2367
+ const cls = buildClass("kb-divider", nodeClass);
2368
+ const text = props.text ?? props.label;
2369
+ if (text) {
2370
+ return `${indent}<div class="${cls}"${idAttr} role="separator"><span>${escapeHtml(text)}</span></div>`;
2371
+ }
2372
+ return `${indent}<hr class="${cls}"${idAttr} />`;
2373
+ }
2374
+ case "spacer": {
2375
+ const cls = buildClass("kb-spacer", nodeClass);
2376
+ return `${indent}<div class="${cls}"${idAttr} aria-hidden="true"></div>`;
2377
+ }
2378
+ case "image": {
2379
+ const src = props.src ? ` src="${escapeAttr(props.src)}"` : ' src=""';
2380
+ const alt = props.alt ? ` alt="${escapeAttr(props.alt)}"` : ' alt=""';
2381
+ const loading = props.loading ? ` loading="${escapeAttr(props.loading)}"` : ' loading="lazy"';
2382
+ const cls = buildClass("kb-image", nodeClass);
2383
+ return `${indent}<img class="${cls}"${idAttr}${src}${alt}${loading} />`;
2384
+ }
2385
+ case "video": {
2386
+ const src = props.src || "";
2387
+ const ytId = getYouTubeId2(src);
2388
+ const vmId = getVimeoId2(src);
2389
+ const cls = buildClass("kb-video", nodeClass);
2390
+ if (ytId) {
2391
+ return `${indent}<div class="kb-video-wrapper ${nodeClass}"${idAttr}>
2392
+ ${innerIndent}<iframe src="https://www.youtube-nocookie.com/embed/${escapeAttr(ytId)}" title="${escapeAttr(props.title || "Video player")}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
2393
+ ${indent}</div>`;
2394
+ }
2395
+ if (vmId) {
2396
+ return `${indent}<div class="kb-video-wrapper ${nodeClass}"${idAttr}>
2397
+ ${innerIndent}<iframe src="https://player.vimeo.com/video/${escapeAttr(vmId)}" title="${escapeAttr(props.title || "Video player")}" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>
2398
+ ${indent}</div>`;
2399
+ }
2400
+ const poster = props.poster ? ` poster="${escapeAttr(props.poster)}"` : "";
2401
+ const controls = props.controls !== false ? " controls" : "";
2402
+ const autoplay = props.autoplay ? " autoplay" : "";
2403
+ const loop = props.loop ? " loop" : "";
2404
+ const muted = props.muted ? " muted" : "";
2405
+ const videoSrc = src ? ` src="${escapeAttr(src)}"` : "";
2406
+ return `${indent}<video class="${cls}"${idAttr}${videoSrc}${poster}${controls}${autoplay}${loop}${muted}></video>`;
2407
+ }
2408
+ case "icon": {
2409
+ const name = props.name || props.icon || "star";
2410
+ const ariaLabel = props.ariaLabel ? ` aria-label="${escapeAttr(props.ariaLabel)}"` : ' aria-hidden="true"';
2411
+ const cls = buildClass("kb-icon", nodeClass);
2412
+ return `${indent}<span class="${cls}"${idAttr}${ariaLabel} data-icon="${escapeAttr(name)}"></span>`;
2413
+ }
2414
+ case "html-embed": {
2415
+ const rawHtml = props.html ?? props.content ?? "";
2416
+ const cls = buildClass("kb-html-embed", nodeClass);
2417
+ if (!rawHtml) {
2418
+ return `${indent}<div class="${cls}"${idAttr}></div>`;
2419
+ }
2420
+ return `${indent}<div class="${cls}"${idAttr}>
2421
+ ${innerIndent}${rawHtml}
2422
+ ${indent}</div>`;
2423
+ }
2424
+ case "button": {
2425
+ const label = props.label ?? props.text ?? props.value ?? "Button";
2426
+ const type = props.type ? ` type="${escapeAttr(props.type)}"` : ' type="button"';
2427
+ const disabled = props.disabled ? " disabled" : "";
2428
+ const cls = buildClass("kb-button", nodeClass);
2429
+ if (props.href) {
2430
+ const href = ` href="${escapeAttr(props.href)}"`;
2431
+ const target = props.target ? ` target="${escapeAttr(props.target)}"` : "";
2432
+ return `${indent}<a class="${cls}"${idAttr}${href}${target}>${escapeHtml(label)}</a>`;
2433
+ }
2434
+ return `${indent}<button class="${cls}"${idAttr}${type}${disabled}>${escapeHtml(label)}</button>`;
2435
+ }
2436
+ case "form": {
2437
+ const action = props.action ? ` action="${escapeAttr(props.action)}"` : "";
2438
+ const method = props.method ? ` method="${escapeAttr(props.method)}"` : ' method="POST"';
2439
+ const cls = buildClass("kb-form", nodeClass);
2440
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2441
+ if (!childHtml) {
2442
+ return `${indent}<form class="${cls}"${idAttr}${action}${method}></form>`;
2443
+ }
2444
+ return `${indent}<form class="${cls}"${idAttr}${action}${method}>
2445
+ ${childHtml}
2446
+ ${indent}</form>`;
2447
+ }
2448
+ case "input": {
2449
+ const type = props.type ? ` type="${escapeAttr(props.type)}"` : ' type="text"';
2450
+ const name = props.name ? ` name="${escapeAttr(props.name)}"` : "";
2451
+ const placeholder = props.placeholder ? ` placeholder="${escapeAttr(props.placeholder)}"` : "";
2452
+ const value = props.value !== void 0 ? ` value="${escapeAttr(props.value)}"` : "";
2453
+ const required = props.required ? " required" : "";
2454
+ const disabled = props.disabled ? " disabled" : "";
2455
+ const cls = buildClass("kb-input", nodeClass);
2456
+ return `${indent}<input class="${cls}"${idAttr}${type}${name}${placeholder}${value}${required}${disabled} />`;
2457
+ }
2458
+ case "textarea": {
2459
+ const name = props.name ? ` name="${escapeAttr(props.name)}"` : "";
2460
+ const placeholder = props.placeholder ? ` placeholder="${escapeAttr(props.placeholder)}"` : "";
2461
+ const rows = props.rows ? ` rows="${escapeAttr(props.rows)}"` : ' rows="4"';
2462
+ const value = props.value ?? props.defaultValue ?? "";
2463
+ const required = props.required ? " required" : "";
2464
+ const disabled = props.disabled ? " disabled" : "";
2465
+ const cls = buildClass("kb-textarea", nodeClass);
2466
+ return `${indent}<textarea class="${cls}"${idAttr}${name}${placeholder}${rows}${required}${disabled}>${escapeHtml(value)}</textarea>`;
2467
+ }
2468
+ case "select": {
2469
+ const name = props.name ? ` name="${escapeAttr(props.name)}"` : "";
2470
+ const required = props.required ? " required" : "";
2471
+ const disabled = props.disabled ? " disabled" : "";
2472
+ const cls = buildClass("kb-select", nodeClass);
2473
+ const optionsList = Array.isArray(props.options) ? props.options : [];
2474
+ const optionIndent = " ".repeat((indentLevel + 1) * (options.indentSize ?? 2));
2475
+ const optionsHtml = optionsList.map((opt) => {
2476
+ const optVal = typeof opt === "object" ? opt.value : opt;
2477
+ const optLabel = typeof opt === "object" ? opt.label : opt;
2478
+ const selected = props.value === optVal || props.defaultValue === optVal ? " selected" : "";
2479
+ return `${optionIndent}<option value="${escapeAttr(optVal)}"${selected}>${escapeHtml(optLabel)}</option>`;
2480
+ }).join("\n");
2481
+ if (!optionsHtml) {
2482
+ return `${indent}<select class="${cls}"${idAttr}${name}${required}${disabled}></select>`;
2483
+ }
2484
+ return `${indent}<select class="${cls}"${idAttr}${name}${required}${disabled}>
2485
+ ${optionsHtml}
2486
+ ${indent}</select>`;
2487
+ }
2488
+ case "checkbox": {
2489
+ const name = props.name ? ` name="${escapeAttr(props.name)}"` : "";
2490
+ const checked = props.checked || props.defaultChecked ? " checked" : "";
2491
+ const label = props.label ?? props.text ?? "";
2492
+ const cls = buildClass("kb-checkbox-label", nodeClass);
2493
+ return `${indent}<label class="${cls}"${idAttr}><input type="checkbox"${name}${checked} /><span>${escapeHtml(label)}</span></label>`;
2494
+ }
2495
+ case "radio": {
2496
+ const name = props.name ? ` name="${escapeAttr(props.name)}"` : "";
2497
+ const value = props.value ? ` value="${escapeAttr(props.value)}"` : "";
2498
+ const checked = props.checked || props.defaultChecked ? " checked" : "";
2499
+ const label = props.label ?? props.text ?? "";
2500
+ const cls = buildClass("kb-radio-label", nodeClass);
2501
+ return `${indent}<label class="${cls}"${idAttr}><input type="radio"${name}${value}${checked} /><span>${escapeHtml(label)}</span></label>`;
2502
+ }
2503
+ case "list": {
2504
+ const tag = props.tag === "ol" || props.type === "ol" || props.ordered ? "ol" : "ul";
2505
+ const cls = buildClass("kb-list", nodeClass);
2506
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2507
+ if (!childHtml) {
2508
+ return `${indent}<${tag} class="${cls}"${idAttr}></${tag}>`;
2509
+ }
2510
+ return `${indent}<${tag} class="${cls}"${idAttr}>
2511
+ ${childHtml}
2512
+ ${indent}</${tag}>`;
2513
+ }
2514
+ case "list-item": {
2515
+ const text = props.text ?? props.value;
2516
+ const cls = buildClass("kb-list-item", nodeClass);
2517
+ if (children.length > 0) {
2518
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2519
+ return `${indent}<li class="${cls}"${idAttr}>
2520
+ ${childHtml}
2521
+ ${indent}</li>`;
2522
+ }
2523
+ return `${indent}<li class="${cls}"${idAttr}>${escapeHtml(text ?? "List item")}</li>`;
2524
+ }
2525
+ case "table": {
2526
+ const cls = buildClass("kb-table", nodeClass);
2527
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2528
+ if (!childHtml) {
2529
+ return `${indent}<table class="${cls}"${idAttr}></table>`;
2530
+ }
2531
+ return `${indent}<table class="${cls}"${idAttr}>
2532
+ ${childHtml}
2533
+ ${indent}</table>`;
2534
+ }
2535
+ case "table-row": {
2536
+ const cls = buildClass("kb-table-row", nodeClass);
2537
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2538
+ if (!childHtml) {
2539
+ return `${indent}<tr class="${cls}"${idAttr}></tr>`;
2540
+ }
2541
+ return `${indent}<tr class="${cls}"${idAttr}>
2542
+ ${childHtml}
2543
+ ${indent}</tr>`;
2544
+ }
2545
+ case "table-cell": {
2546
+ const isHeader = props.isHeader || props.type === "header" || props.tag === "th";
2547
+ const tag = isHeader ? "th" : "td";
2548
+ const colSpan = props.colSpan && Number(props.colSpan) > 1 ? ` colspan="${escapeAttr(props.colSpan)}"` : "";
2549
+ const rowSpan = props.rowSpan && Number(props.rowSpan) > 1 ? ` rowspan="${escapeAttr(props.rowSpan)}"` : "";
2550
+ const text = props.text ?? props.value ?? "";
2551
+ const cls = buildClass("kb-table-cell", nodeClass);
2552
+ if (children.length > 0) {
2553
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2554
+ return `${indent}<${tag} class="${cls}"${idAttr}${colSpan}${rowSpan}>
2555
+ ${childHtml}
2556
+ ${indent}</${tag}>`;
2557
+ }
2558
+ return `${indent}<${tag} class="${cls}"${idAttr}${colSpan}${rowSpan}>${escapeHtml(text)}</${tag}>`;
2559
+ }
2560
+ case "collection": {
2561
+ const cls = buildClass("kb-collection", nodeClass);
2562
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2563
+ if (!childHtml) {
2564
+ return `${indent}<div class="${cls}"${idAttr}></div>`;
2565
+ }
2566
+ return `${indent}<div class="${cls}"${idAttr}>
2567
+ ${childHtml}
2568
+ ${indent}</div>`;
2569
+ }
2570
+ default: {
2571
+ const cls = buildClass(`kb-${node.type}`, nodeClass);
2572
+ const childHtml = children.map((child) => renderNodeToHtml(child, indentLevel + 1, options)).join("\n");
2573
+ if (!childHtml) {
2574
+ return `${indent}<div class="${cls}"${idAttr}></div>`;
2575
+ }
2576
+ return `${indent}<div class="${cls}"${idAttr}>
2577
+ ${childHtml}
2578
+ ${indent}</div>`;
2579
+ }
2580
+ }
2581
+ }
2582
+ function generateSemanticHtml(docOrNode, options = {}) {
2583
+ const rootNode = "document" in docOrNode ? docOrNode.document : docOrNode;
2584
+ if (!rootNode) return "";
2585
+ return renderNodeToHtml(rootNode, 0, options);
2586
+ }
2587
+ function formatCssRule(selector, declarationsStr, indent = "") {
2588
+ if (!declarationsStr.trim()) return "";
2589
+ const rules = declarationsStr.split(";").map((r) => r.trim()).filter(Boolean);
2590
+ if (rules.length === 0) return "";
2591
+ const indentedRules = rules.map((r) => `${indent} ${r};`).join("\n");
2592
+ return `${indent}${selector} {
2593
+ ${indentedRules}
2594
+ ${indent}}`;
2595
+ }
2596
+ function generateDocumentCss(docOrNode, options = {}) {
2597
+ const rootNode = "document" in docOrNode ? docOrNode.document : docOrNode;
2598
+ if (!rootNode) return "";
2599
+ const classPrefix = options.classPrefix || "kb-node-";
2600
+ const baseRules = [];
2601
+ const tabletRules = [];
2602
+ const mobileRules = [];
2603
+ const stateRules = [];
2604
+ const walk = (node) => {
2605
+ const selector = `.${classPrefix}${node.id}`;
2606
+ if (node.styles) {
2607
+ const baseStyles = {
2608
+ ...node.styles.base || {},
2609
+ ...node.styles.desktop || {}
2610
+ };
2611
+ const baseDecls = styleDefinitionToCssDeclarations(baseStyles);
2612
+ if (baseDecls) {
2613
+ baseRules.push(formatCssRule(selector, baseDecls));
2614
+ }
2615
+ if (node.styles.tablet) {
2616
+ const tabletDecls = styleDefinitionToCssDeclarations(node.styles.tablet);
2617
+ if (tabletDecls) {
2618
+ tabletRules.push(formatCssRule(selector, tabletDecls, " "));
2619
+ }
2620
+ }
2621
+ if (node.styles.mobile) {
2622
+ const mobileDecls = styleDefinitionToCssDeclarations(node.styles.mobile);
2623
+ if (mobileDecls) {
2624
+ mobileRules.push(formatCssRule(selector, mobileDecls, " "));
2625
+ }
2626
+ }
2627
+ if (node.styles.states && typeof node.styles.states === "object") {
2628
+ for (const [state, styleDef] of Object.entries(node.styles.states)) {
2629
+ if (!styleDef) continue;
2630
+ const safeState = /^::?[a-zA-Z-]+$/.test(state) ? state : null;
2631
+ if (!safeState) continue;
2632
+ const stateDecls = styleDefinitionToCssDeclarations(styleDef);
2633
+ if (stateDecls) {
2634
+ stateRules.push(formatCssRule(`${selector}${safeState}`, stateDecls));
2635
+ }
2636
+ }
2637
+ }
2638
+ }
2639
+ node.children?.forEach(walk);
2640
+ };
2641
+ walk(rootNode);
2642
+ const sections = [];
2643
+ if (options.includeReset !== false) {
2644
+ sections.push(`/* ==========================================================================
2645
+ Baseline Reset & Typography Standards
2646
+ ========================================================================== */
2647
+ ${DEFAULT_CSS_RESET}`);
2648
+ }
2649
+ if (baseRules.length > 0) {
2650
+ sections.push(`/* ==========================================================================
2651
+ Component Styles
2652
+ ========================================================================== */
2653
+ ${baseRules.join("\n\n")}`);
2654
+ }
2655
+ if (stateRules.length > 0) {
2656
+ sections.push(`/* ==========================================================================
2657
+ Interactive & Hover States
2658
+ ========================================================================== */
2659
+ ${stateRules.join("\n\n")}`);
2660
+ }
2661
+ if (tabletRules.length > 0) {
2662
+ sections.push(`/* ==========================================================================
2663
+ Tablet Breakpoint (max-width: 1024px)
2664
+ ========================================================================== */
2665
+ @media (max-width: 1024px) {
2666
+ ${tabletRules.join("\n\n")}
2667
+ }`);
2668
+ }
2669
+ if (mobileRules.length > 0) {
2670
+ sections.push(`/* ==========================================================================
2671
+ Mobile Breakpoint (max-width: 640px)
2672
+ ========================================================================== */
2673
+ @media (max-width: 640px) {
2674
+ ${mobileRules.join("\n\n")}
2675
+ }`);
2676
+ }
2677
+ const pageDoc = "document" in docOrNode ? docOrNode : { schema: "stora.page", version: "1.0.0", document: rootNode };
2678
+ const animStyles = collectAnimationStylesCss(pageDoc);
2679
+ if (animStyles) {
2680
+ sections.push(`/* ==========================================================================
2681
+ Animations & Motion Keyframes
2682
+ ========================================================================== */
2683
+ ${animStyles}`);
2684
+ }
2685
+ return sections.join("\n\n");
2686
+ }
2687
+ function generateStandaloneHtml(doc, options = {}) {
2688
+ const title = escapeHtml(doc.metadata?.title || "KUBUILD Page");
2689
+ const description = doc.metadata?.description ? escapeAttr(doc.metadata.description) : "";
2690
+ const author = doc.metadata?.author ? escapeAttr(doc.metadata.author) : "";
2691
+ const lang = options.lang || "en";
2692
+ const css = generateDocumentCss(doc, options.cssOptions);
2693
+ const html = generateSemanticHtml(doc, options.htmlOptions);
2694
+ const metaDescription = description ? ` <meta name="description" content="${description}">
2695
+ ` : "";
2696
+ const metaAuthor = author ? ` <meta name="author" content="${author}">
2697
+ ` : "";
2698
+ return `<!DOCTYPE html>
2699
+ <html lang="${lang}">
2700
+ <head>
2701
+ <meta charset="UTF-8">
2702
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2703
+ <title>${title}</title>
2704
+ ${metaDescription}${metaAuthor} <style>
2705
+ ${css}
2706
+ </style>
2707
+ </head>
2708
+ <body>
2709
+ ${html}
2710
+ </body>
2711
+ </html>`;
2712
+ }
2000
2713
  export {
2714
+ ANIMATION_KEYFRAMES_CSS,
2001
2715
  ComponentErrorBoundary,
2002
2716
  DEFAULT_BREAKPOINTS,
2003
2717
  DEFAULT_CSS_RESET,
@@ -2010,11 +2724,19 @@ export {
2010
2724
  NodeRenderer,
2011
2725
  PreviewViewportAdapter,
2012
2726
  RenderContextProvider,
2727
+ collectAnimationStylesCss,
2013
2728
  collectStateStylesCss,
2014
2729
  createMinimalRenderContext,
2015
2730
  createRenderContext,
2016
2731
  dispatchAction,
2732
+ generateDocumentCss,
2733
+ generateSemanticHtml,
2734
+ generateStandaloneHtml,
2735
+ getEntranceAnimationCss,
2736
+ getHoverEffectCss,
2737
+ getLoopEffectCss,
2017
2738
  isActionRegistered,
2739
+ replayNodeAnimation,
2018
2740
  resolveActionPayload,
2019
2741
  resolveActionPayloadDetailed,
2020
2742
  resolveAssetSync,