@noirmd/previewer 2.1.4 → 2.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/NReditor.cjs CHANGED
@@ -565,6 +565,22 @@ function parseHtmlAttrs(attrsString) {
565
565
  }
566
566
 
567
567
  // core/parser.ts
568
+ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
569
+ "area",
570
+ "base",
571
+ "br",
572
+ "col",
573
+ "embed",
574
+ "hr",
575
+ "img",
576
+ "input",
577
+ "link",
578
+ "meta",
579
+ "param",
580
+ "source",
581
+ "track",
582
+ "wbr"
583
+ ]);
568
584
  function parseMarkdown(markdown2) {
569
585
  if (!markdown2) return [];
570
586
  const lines = markdown2.replace(/\r\n/g, "\n").replace(/\r/g, "").split("\n");
@@ -578,8 +594,19 @@ function parseMarkdown(markdown2) {
578
594
  if (match = trimmed.match(/^(#{1,6})\s+(.+)$/)) {
579
595
  const level = match[1].length;
580
596
  const rawText = match[2];
581
- const { text: text2, classes: classes2, id: customId } = extractAttributes(rawText);
582
- const baseId = customId || generateId(text2.replace(/->|<-/g, ""));
597
+ const { text: rawParsedText, classes: classes2, id: customId } = extractAttributes(rawText);
598
+ let text2 = rawParsedText;
599
+ let align;
600
+ const alignCenter = text2.match(/^->\s*(.+?)\s*<-\s*$/);
601
+ const alignRight = text2.match(/^->\s*(.+?)\s*->\s*$/);
602
+ if (alignCenter) {
603
+ text2 = alignCenter[1];
604
+ align = "center";
605
+ } else if (alignRight) {
606
+ text2 = alignRight[1];
607
+ align = "right";
608
+ }
609
+ const baseId = customId || generateId(text2);
583
610
  let id2 = baseId;
584
611
  let n = 1;
585
612
  while (usedIds.has(id2)) {
@@ -587,7 +614,7 @@ function parseMarkdown(markdown2) {
587
614
  id2 = `${baseId}-${n}`;
588
615
  }
589
616
  usedIds.add(id2);
590
- result.push({ type: "header", level, text: text2, id: id2, classes: classes2 || void 0 });
617
+ result.push({ type: "header", level, text: text2, id: id2, classes: classes2 || void 0, align });
591
618
  i++;
592
619
  continue;
593
620
  }
@@ -763,29 +790,13 @@ function parseMarkdown(markdown2) {
763
790
  let tagStartMatch = trimmed.match(/^<([a-zA-Z][\w-]*)/);
764
791
  if (tagStartMatch) {
765
792
  const tagName = tagStartMatch[1].toLowerCase();
766
- const voidElements = /* @__PURE__ */ new Set([
767
- "area",
768
- "base",
769
- "br",
770
- "col",
771
- "embed",
772
- "hr",
773
- "img",
774
- "input",
775
- "link",
776
- "meta",
777
- "param",
778
- "source",
779
- "track",
780
- "wbr"
781
- ]);
782
793
  const remainingText = lines.slice(i).join("\n");
783
794
  const openTagRegex = new RegExp(`^\\s*<${tagName}\\b([^>]*?)>`, "i");
784
795
  const openTagMatch = remainingText.match(openTagRegex);
785
796
  if (openTagMatch) {
786
797
  const fullOpenTag = openTagMatch[0];
787
798
  const attrs = openTagMatch[1].replace(/\s+/g, " ").trim();
788
- const isSelfClosing = fullOpenTag.endsWith("/>") || voidElements.has(tagName);
799
+ const isSelfClosing = fullOpenTag.endsWith("/>") || VOID_ELEMENTS.has(tagName);
789
800
  if (isSelfClosing) {
790
801
  const blockText = remainingText.substring(0, openTagMatch.index + fullOpenTag.length);
791
802
  const consumedLines = blockText.split("\n").length;
@@ -1088,6 +1099,7 @@ function createModal(title) {
1088
1099
  header.appendChild(titleEl);
1089
1100
  const closeBtn = document.createElement("button");
1090
1101
  closeBtn.className = "nr-modal__close";
1102
+ closeBtn.setAttribute("aria-label", "Close");
1091
1103
  closeBtn.appendChild(createIcon("close"));
1092
1104
  closeBtn.addEventListener("click", () => dialog.close());
1093
1105
  header.appendChild(closeBtn);
@@ -1308,11 +1320,89 @@ function parseInlinePart(part) {
1308
1320
  return document.createTextNode(part);
1309
1321
  }
1310
1322
 
1323
+ // vanilla/utils.ts
1324
+ var THEME_TOKENS = /* @__PURE__ */ new Set([
1325
+ "primary",
1326
+ "secondary",
1327
+ "accent",
1328
+ "neutral",
1329
+ "info",
1330
+ "success",
1331
+ "warning",
1332
+ "error"
1333
+ ]);
1334
+ function isThemeToken(color) {
1335
+ return !!color && THEME_TOKENS.has(color);
1336
+ }
1337
+ function isArbitraryColor(value) {
1338
+ if (THEME_TOKENS.has(value)) return false;
1339
+ if (/^(#|rgb|hsl|oklch|oklab|lab|lch|color\()/i.test(value)) return true;
1340
+ if (/^[a-zA-Z]+$/.test(value)) return true;
1341
+ return false;
1342
+ }
1343
+ function applyBaseProps(el, props) {
1344
+ if (props.class) {
1345
+ el.classList.add(...props.class.split(/\s+/).filter(Boolean));
1346
+ }
1347
+ if (props.style) {
1348
+ const styles = parseCssString(props.style);
1349
+ for (const [key, value] of Object.entries(styles)) {
1350
+ el.style.setProperty(key, String(value));
1351
+ }
1352
+ }
1353
+ }
1354
+ function applyFloatStyle(el, float, width) {
1355
+ if (!float) return;
1356
+ if (float === "left" || float === "right") {
1357
+ el.style.float = float;
1358
+ if (!width) el.style.maxWidth = "50%";
1359
+ el.style.marginInlineStart = float === "right" ? "1rem" : "";
1360
+ el.style.marginInlineEnd = float === "left" ? "1rem" : "";
1361
+ } else if (float === "center") {
1362
+ el.style.marginInline = "auto";
1363
+ }
1364
+ }
1365
+ function applyColor(el, color, classSuffix) {
1366
+ if (!color) return "";
1367
+ if (isThemeToken(color)) {
1368
+ return ` ${classSuffix}--${color}`;
1369
+ }
1370
+ if (isArbitraryColor(color)) {
1371
+ el.style.background = color;
1372
+ el.style.color = "white";
1373
+ }
1374
+ return "";
1375
+ }
1376
+ function openModal(dialog) {
1377
+ if (!dialog.open) {
1378
+ document.body.appendChild(dialog);
1379
+ dialog.showModal();
1380
+ dialog.addEventListener("close", () => dialog.remove(), { once: true });
1381
+ }
1382
+ }
1383
+ var IMG_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
1384
+ function applyAlignClass(el, baseClass, align) {
1385
+ if (align === "center") {
1386
+ el.classList.add(`${baseClass}--center`);
1387
+ } else if (align === "right") {
1388
+ el.classList.add(`${baseClass}--right`);
1389
+ }
1390
+ }
1391
+ function resolveIcon(value, fallback) {
1392
+ if (!value) return fallback;
1393
+ if (value === "none" || value === "off") return null;
1394
+ return value;
1395
+ }
1396
+ function parseIntProp(value, defaultValue) {
1397
+ if (!value) return defaultValue;
1398
+ const n = parseInt(value, 10);
1399
+ return Number.isNaN(n) ? defaultValue : n;
1400
+ }
1401
+
1311
1402
  // vanilla/directives/admonition.ts
1312
1403
  var admonitionDirective = ({ directiveType, props, renderSlot }) => {
1313
1404
  const el = createAdmonition(directiveType, props.title, props.icon);
1314
- if (props.class) el.classList.add(...props.class.split(/\s+/).filter(Boolean));
1315
- if (props.style) el.setAttribute("style", props.style);
1405
+ applyBaseProps(el, props);
1316
1406
  const body = el.querySelector(".nr-admonition__body");
1317
1407
  if (body) {
1318
1408
  body.appendChild(renderSlot("default"));
@@ -1328,8 +1418,7 @@ var detailsDirective = ({ props, renderSlot }) => {
1328
1418
  props.icon,
1329
1419
  props.defaultOpen === "true"
1330
1420
  );
1331
- if (props.class) el.classList.add(...props.class.split(/\s+/).filter(Boolean));
1332
- if (props.style) el.setAttribute("style", props.style);
1421
+ applyBaseProps(el, props);
1333
1422
  const body = el.querySelector(".nr-details__body");
1334
1423
  if (body) {
1335
1424
  body.appendChild(renderSlot("default"));
@@ -1342,14 +1431,17 @@ var details_default = detailsDirective;
1342
1431
  var modalDirective = ({ props, renderSlot }) => {
1343
1432
  const label = props.label || props.title || "Open";
1344
1433
  const modalTitle = props.title || "Modal";
1345
- const customClass = props.class || "";
1434
+ const align = props.align || "left";
1435
+ const iconName = resolveIcon(props.icon, "open_in_full");
1346
1436
  const wrapper = document.createElement("div");
1347
1437
  wrapper.className = "nr-modal-trigger";
1438
+ applyAlignClass(wrapper, "nr-modal-trigger", align);
1348
1439
  const btn = document.createElement("button");
1349
- btn.className = `nr-button nr-button--default`;
1350
- if (customClass) btn.classList.add(...customClass.split(/\s+/).filter(Boolean));
1351
- const icon = props.icon || "open_in_new";
1352
- if (icon) btn.appendChild(createIcon(icon));
1440
+ btn.className = "nr-button nr-button--default";
1441
+ btn.setAttribute("aria-haspopup", "dialog");
1442
+ applyColor(btn, props.color, "nr-button");
1443
+ applyBaseProps(btn, props);
1444
+ if (iconName) btn.appendChild(createIcon(iconName));
1353
1445
  btn.appendChild(document.createTextNode(label));
1354
1446
  const dialog = createModal(modalTitle);
1355
1447
  const body = dialog.querySelector(".nr-modal__body");
@@ -1359,15 +1451,7 @@ var modalDirective = ({ props, renderSlot }) => {
1359
1451
  prose.appendChild(renderSlot("default"));
1360
1452
  body.appendChild(prose);
1361
1453
  }
1362
- btn.addEventListener("click", () => {
1363
- if (!dialog.open) {
1364
- document.body.appendChild(dialog);
1365
- dialog.showModal();
1366
- dialog.addEventListener("close", () => {
1367
- dialog.remove();
1368
- }, { once: true });
1369
- }
1370
- });
1454
+ btn.addEventListener("click", () => openModal(dialog));
1371
1455
  wrapper.appendChild(btn);
1372
1456
  wrapper.appendChild(dialog);
1373
1457
  return wrapper;
@@ -1377,20 +1461,22 @@ var modal_default = modalDirective;
1377
1461
  // vanilla/directives/button.ts
1378
1462
  var buttonDirective = ({ props, renderSlot }) => {
1379
1463
  const url = props.url || props.href || "#";
1380
- const label = props.label;
1381
- const icon = props.icon || "near_me";
1464
+ const label = props.label || props.title;
1465
+ const iconName = resolveIcon(props.icon, "touch_app");
1382
1466
  const target = props.target || "_blank";
1383
- const customClass = props.class || "";
1467
+ const align = props.align || "left";
1384
1468
  const wrapper = document.createElement("div");
1385
1469
  wrapper.className = "nr-button-wrap";
1470
+ applyAlignClass(wrapper, "nr-button-wrap", align);
1386
1471
  if (label) {
1387
1472
  const a = document.createElement("a");
1388
1473
  a.href = url;
1389
1474
  a.target = target;
1390
- a.rel = "noopener noreferrer";
1475
+ if (target === "_blank") a.rel = "noopener noreferrer";
1391
1476
  a.className = "nr-button nr-button--default";
1392
- if (customClass) a.classList.add(...customClass.split(/\s+/).filter(Boolean));
1393
- a.appendChild(createIcon(icon));
1477
+ applyColor(a, props.color, "nr-button");
1478
+ applyBaseProps(a, props);
1479
+ if (iconName) a.appendChild(createIcon(iconName));
1394
1480
  a.appendChild(document.createTextNode(label));
1395
1481
  wrapper.appendChild(a);
1396
1482
  return wrapper;
@@ -1400,17 +1486,20 @@ var buttonDirective = ({ props, renderSlot }) => {
1400
1486
  if (links.length > 0) {
1401
1487
  links.forEach((link) => {
1402
1488
  link.classList.add("nr-button", "nr-button--default");
1403
- if (customClass) link.classList.add(...customClass.split(/\s+/).filter(Boolean));
1489
+ applyColor(link, props.color, "nr-button");
1490
+ if (iconName) link.prepend(createIcon(iconName));
1491
+ applyBaseProps(link, props);
1404
1492
  });
1405
1493
  wrapper.appendChild(slotContent);
1406
1494
  } else {
1407
1495
  const a = document.createElement("a");
1408
1496
  a.href = url;
1409
1497
  a.target = target;
1410
- a.rel = "noopener noreferrer";
1498
+ if (target === "_blank") a.rel = "noopener noreferrer";
1411
1499
  a.className = "nr-button nr-button--default";
1412
- if (customClass) a.classList.add(...customClass.split(/\s+/).filter(Boolean));
1413
- a.appendChild(createIcon(icon));
1500
+ applyColor(a, props.color, "nr-button");
1501
+ applyBaseProps(a, props);
1502
+ if (iconName) a.appendChild(createIcon(iconName));
1414
1503
  a.appendChild(slotContent);
1415
1504
  wrapper.appendChild(a);
1416
1505
  }
@@ -1432,12 +1521,9 @@ var cardDirective = ({
1432
1521
  const { isSingleCard } = options || {};
1433
1522
  const isModal = directiveType === "card-m";
1434
1523
  const isLink = directiveType === "card-b";
1435
- const inlineStyles = props.style ? parseCssString(props.style) : {};
1436
1524
  const card = document.createElement("div");
1437
1525
  card.className = `nr-card${isModal || isLink ? " nr-card--interactive" : ""} ${customClass}`.trim();
1438
- for (const [key, value] of Object.entries(inlineStyles)) {
1439
- card.style.setProperty(key, String(value));
1440
- }
1526
+ applyBaseProps(card, props);
1441
1527
  if (image) {
1442
1528
  const imgWrap = document.createElement("div");
1443
1529
  imgWrap.className = `nr-card__image${isSingleCard ? " nr-card__image--tall" : ""}`;
@@ -1516,13 +1602,7 @@ var cardDirective = ({
1516
1602
  prose.appendChild(renderSlot("content") || renderSlot("default"));
1517
1603
  modalBody.appendChild(prose);
1518
1604
  }
1519
- card.addEventListener("click", () => {
1520
- if (!dialog.open) {
1521
- document.body.appendChild(dialog);
1522
- dialog.showModal();
1523
- dialog.addEventListener("close", () => dialog.remove(), { once: true });
1524
- }
1525
- });
1605
+ card.addEventListener("click", () => openModal(dialog));
1526
1606
  const frag = document.createDocumentFragment();
1527
1607
  frag.appendChild(card);
1528
1608
  frag.appendChild(dialog);
@@ -1570,8 +1650,8 @@ var slideDirective = ({
1570
1650
  if (lines.length === 0) {
1571
1651
  return document.createDocumentFragment();
1572
1652
  }
1573
- const interval = parseInt(props.interval || "3000", 10);
1574
- const speed = parseInt(props.speed || "500", 10);
1653
+ const interval = parseIntProp(props.interval, 3e3);
1654
+ const speed = parseIntProp(props.speed, 500);
1575
1655
  const rawClass = props.class || "";
1576
1656
  const inlineStyle = props.style ? parseCssString(props.style) : {};
1577
1657
  const scopeClass = `sld-${++slideCounter}`;
@@ -1622,10 +1702,11 @@ var slideDirective = ({
1622
1702
  }
1623
1703
  });
1624
1704
  if (lines.length > 1) {
1625
- setInterval(() => {
1705
+ const id = setInterval(() => {
1626
1706
  current = (current + 1) % lines.length;
1627
1707
  track.style.transform = `translateY(${-current * maxH}px)`;
1628
1708
  }, interval);
1709
+ container.dataset.nrIntervalId = String(id);
1629
1710
  }
1630
1711
  return container;
1631
1712
  };
@@ -1635,8 +1716,7 @@ var slide_default = slideDirective;
1635
1716
  var keysDirective = ({ props, slots }) => {
1636
1717
  const wrap = document.createElement("div");
1637
1718
  wrap.className = "nr-keys";
1638
- if (props.class) wrap.classList.add(...props.class.split(/\s+/).filter(Boolean));
1639
- if (props.style) wrap.setAttribute("style", props.style);
1719
+ applyBaseProps(wrap, props);
1640
1720
  const sizeClass = props.size ? ` nr-kbd--${props.size}` : "";
1641
1721
  const parts = (slots.default || "").split("+").map((p) => p.trim()).filter(Boolean);
1642
1722
  parts.forEach((part, i) => {
@@ -1660,8 +1740,7 @@ var accordionCounter = 0;
1660
1740
  var accordionItemDirective = ({ props, renderSlot }) => {
1661
1741
  const item = document.createElement("div");
1662
1742
  item.className = "nr-accordion__item";
1663
- if (props.class) item.classList.add(...props.class.split(/\s+/).filter(Boolean));
1664
- if (props.style) item.setAttribute("style", props.style);
1743
+ applyBaseProps(item, props);
1665
1744
  const input = document.createElement("input");
1666
1745
  input.type = "radio";
1667
1746
  input.className = "nr-accordion__input";
@@ -1680,8 +1759,7 @@ var accordionItemDirective = ({ props, renderSlot }) => {
1680
1759
  var accordionDirective = ({ props, renderSlot }) => {
1681
1760
  const wrap = document.createElement("div");
1682
1761
  wrap.className = "nr-accordion";
1683
- if (props.class) wrap.classList.add(...props.class.split(/\s+/).filter(Boolean));
1684
- if (props.style) wrap.setAttribute("style", props.style);
1762
+ applyBaseProps(wrap, props);
1685
1763
  wrap.appendChild(renderSlot("default"));
1686
1764
  const mode = props.mode === "checkbox" ? "checkbox" : "radio";
1687
1765
  const group = `nr-acc-${++accordionCounter}`;
@@ -1694,7 +1772,6 @@ var accordionDirective = ({ props, renderSlot }) => {
1694
1772
  var accordion_default = accordionDirective;
1695
1773
 
1696
1774
  // vanilla/directives/carousel.ts
1697
- var IMG_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
1698
1775
  var carouselDirective = ({ props, slots }) => {
1699
1776
  const images = [];
1700
1777
  const raw = slots.default || "";
@@ -1710,19 +1787,9 @@ var carouselDirective = ({ props, slots }) => {
1710
1787
  wrap.className = "nr-carousel";
1711
1788
  wrap.tabIndex = 0;
1712
1789
  wrap.setAttribute("aria-label", "Image carousel");
1713
- if (props.class) wrap.classList.add(...props.class.split(/\s+/).filter(Boolean));
1714
- if (props.style) wrap.setAttribute("style", props.style);
1790
+ applyBaseProps(wrap, props);
1715
1791
  if (props.width) wrap.style.width = props.width;
1716
- if (props.float) {
1717
- if (props.float === "left" || props.float === "right") {
1718
- wrap.style.float = props.float;
1719
- if (!props.width) wrap.style.maxWidth = "50%";
1720
- wrap.style.marginInlineStart = props.float === "right" ? "1rem" : "";
1721
- wrap.style.marginInlineEnd = props.float === "left" ? "1rem" : "";
1722
- } else if (props.float === "center") {
1723
- wrap.style.marginInline = "auto";
1724
- }
1725
- }
1792
+ applyFloatStyle(wrap, props.float, props.width);
1726
1793
  const viewport = document.createElement("div");
1727
1794
  viewport.className = "nr-carousel__viewport";
1728
1795
  if (props.height) viewport.style.height = props.height;
@@ -1792,11 +1859,10 @@ var DEFAULT_LABELS = ["days", "hours", "min", "sec"];
1792
1859
  var countdownDirective = ({ props }) => {
1793
1860
  const wrap = document.createElement("div");
1794
1861
  wrap.className = "nr-countdown";
1795
- if (props.class) wrap.classList.add(...props.class.split(/\s+/).filter(Boolean));
1796
- if (props.style) wrap.setAttribute("style", props.style);
1862
+ applyBaseProps(wrap, props);
1797
1863
  const labelParts = (props.labels || "").split("|").map((s) => s.trim());
1798
1864
  const labels = DEFAULT_LABELS.map((label, i) => labelParts[i] || label);
1799
- const digits = parseInt(props.digits || "2", 10);
1865
+ const digits = parseIntProp(props.digits, 2);
1800
1866
  const targetTime = props.target ? new Date(props.target).getTime() : NaN;
1801
1867
  const hasTarget = !Number.isNaN(targetTime);
1802
1868
  const blocks = [];
@@ -1843,13 +1909,15 @@ var countdownDirective = ({ props }) => {
1843
1909
  blocks.push({ value });
1844
1910
  });
1845
1911
  render();
1846
- if (hasTarget) setInterval(render, 1e3);
1912
+ if (hasTarget) {
1913
+ const id = setInterval(render, 1e3);
1914
+ wrap.dataset.nrIntervalId = String(id);
1915
+ }
1847
1916
  return wrap;
1848
1917
  };
1849
1918
  var countdown_default = countdownDirective;
1850
1919
 
1851
1920
  // vanilla/directives/diff.ts
1852
- var IMG_RE2 = /!\[([^\]]*)\]\(([^)]+)\)/g;
1853
1921
  var diffDirective = ({ props, slots }) => {
1854
1922
  let before = (props.before || "").split("#")[0].trim();
1855
1923
  let after = (props.after || "").split("#")[0].trim();
@@ -1857,8 +1925,8 @@ var diffDirective = ({ props, slots }) => {
1857
1925
  const urls = [];
1858
1926
  const raw = slots.default || "";
1859
1927
  let m;
1860
- IMG_RE2.lastIndex = 0;
1861
- while ((m = IMG_RE2.exec(raw)) !== null) {
1928
+ IMG_RE.lastIndex = 0;
1929
+ while ((m = IMG_RE.exec(raw)) !== null) {
1862
1930
  urls.push(m[2].split("#")[0].trim());
1863
1931
  }
1864
1932
  if (!before && urls.length > 0) before = urls[0];
@@ -1871,21 +1939,11 @@ var diffDirective = ({ props, slots }) => {
1871
1939
  figure.className = "nr-diff";
1872
1940
  figure.tabIndex = 0;
1873
1941
  figure.setAttribute("aria-label", "Image comparison slider");
1874
- if (props.class) figure.classList.add(...props.class.split(/\s+/).filter(Boolean));
1875
- if (props.style) figure.setAttribute("style", props.style);
1942
+ applyBaseProps(figure, props);
1876
1943
  if (props.aspect) figure.style.aspectRatio = props.aspect;
1877
1944
  if (props.height) figure.style.height = props.height;
1878
1945
  if (props.width) figure.style.width = props.width;
1879
- if (props.float) {
1880
- if (props.float === "left" || props.float === "right") {
1881
- figure.style.float = props.float;
1882
- if (!props.width) figure.style.maxWidth = "50%";
1883
- figure.style.marginInlineStart = props.float === "right" ? "1rem" : "";
1884
- figure.style.marginInlineEnd = props.float === "left" ? "1rem" : "";
1885
- } else if (props.float === "center") {
1886
- figure.style.marginInline = "auto";
1887
- }
1888
- }
1946
+ applyFloatStyle(figure, props.float, props.width);
1889
1947
  const beforeItem = document.createElement("div");
1890
1948
  beforeItem.className = "nr-diff__item nr-diff__item--before";
1891
1949
  beforeItem.setAttribute("role", "img");
@@ -1948,8 +2006,7 @@ var diff_default = diffDirective;
1948
2006
  var hover3dDirective = ({ props, renderSlot }) => {
1949
2007
  const container = document.createElement("div");
1950
2008
  container.className = "nr-hover-3d";
1951
- if (props.class) container.classList.add(...props.class.split(/\s+/).filter(Boolean));
1952
- if (props.style) container.setAttribute("style", props.style);
2009
+ applyBaseProps(container, props);
1953
2010
  const stage = document.createElement("div");
1954
2011
  stage.className = "nr-hover-3d__stage";
1955
2012
  stage.appendChild(renderSlot("default"));
@@ -1962,14 +2019,13 @@ var hover3dDirective = ({ props, renderSlot }) => {
1962
2019
  var hover3d_default = hover3dDirective;
1963
2020
 
1964
2021
  // vanilla/directives/hovergallery.ts
1965
- var IMG_RE3 = /!\[([^\]]*)\]\(([^)]+)\)/g;
1966
2022
  var MAX_IMAGES = 10;
1967
2023
  var hovergalleryDirective = ({ props, slots }) => {
1968
2024
  const images = [];
1969
2025
  const raw = slots.default || "";
1970
2026
  let m;
1971
- IMG_RE3.lastIndex = 0;
1972
- while ((m = IMG_RE3.exec(raw)) !== null) {
2027
+ IMG_RE.lastIndex = 0;
2028
+ while ((m = IMG_RE.exec(raw)) !== null) {
1973
2029
  images.push({ src: m[2].split("#")[0].trim(), alt: m[1].trim() || "gallery image" });
1974
2030
  }
1975
2031
  if (images.length === 0) {
@@ -1978,9 +2034,8 @@ var hovergalleryDirective = ({ props, slots }) => {
1978
2034
  const count = Math.min(images.length, MAX_IMAGES);
1979
2035
  const figure = document.createElement("figure");
1980
2036
  figure.className = "nr-hover-gallery";
2037
+ applyBaseProps(figure, props);
1981
2038
  if (props.aspect) figure.style.aspectRatio = props.aspect;
1982
- if (props.class) figure.classList.add(...props.class.split(/\s+/).filter(Boolean));
1983
- if (props.style) figure.setAttribute("style", (figure.getAttribute("style") || "") + ";" + props.style);
1984
2039
  const imgEls = [];
1985
2040
  for (let i = 0; i < count; i++) {
1986
2041
  const el = document.createElement("img");
@@ -2030,28 +2085,11 @@ var hovergalleryDirective = ({ props, slots }) => {
2030
2085
  var hovergallery_default = hovergalleryDirective;
2031
2086
 
2032
2087
  // vanilla/directives/chat.ts
2033
- var CHAT_THEME_TOKENS = /* @__PURE__ */ new Set([
2034
- "primary",
2035
- "secondary",
2036
- "accent",
2037
- "neutral",
2038
- "info",
2039
- "success",
2040
- "warning",
2041
- "error"
2042
- ]);
2043
- function isArbitraryColor(value) {
2044
- if (CHAT_THEME_TOKENS.has(value)) return false;
2045
- if (/^(#|rgb|hsl|oklch|oklab|lab|lch|color\()/i.test(value)) return true;
2046
- if (/^[a-zA-Z]+$/.test(value)) return true;
2047
- return false;
2048
- }
2049
2088
  var chatItemDirective = ({ props, renderSlot }) => {
2050
2089
  const side = props.side === "end" ? "end" : "start";
2051
2090
  const wrap = document.createElement("div");
2052
2091
  wrap.className = `nr-chat nr-chat--${side}`;
2053
- if (props.class) wrap.classList.add(...props.class.split(/\s+/).filter(Boolean));
2054
- if (props.style) wrap.setAttribute("style", props.style);
2092
+ applyBaseProps(wrap, props);
2055
2093
  const header = document.createElement("div");
2056
2094
  header.className = "nr-chat__header";
2057
2095
  if (props.name) {
@@ -2076,14 +2114,8 @@ var chatItemDirective = ({ props, renderSlot }) => {
2076
2114
  avatar.appendChild(img);
2077
2115
  wrap.appendChild(avatar);
2078
2116
  }
2079
- const isThemeToken = CHAT_THEME_TOKENS.has(props.color || "");
2080
- const colorClass = isThemeToken ? ` nr-chat__bubble--${props.color}` : "";
2081
2117
  const bubble = document.createElement("div");
2082
- bubble.className = `nr-chat__bubble${colorClass}`;
2083
- if (props.color && isArbitraryColor(props.color) && !isThemeToken) {
2084
- bubble.style.background = props.color;
2085
- bubble.style.color = "white";
2086
- }
2118
+ bubble.className = `nr-chat__bubble${applyColor(bubble, props.color, "nr-chat__bubble")}`;
2087
2119
  bubble.appendChild(renderSlot("default"));
2088
2120
  wrap.appendChild(bubble);
2089
2121
  if (props.footer) {
@@ -2097,8 +2129,7 @@ var chatItemDirective = ({ props, renderSlot }) => {
2097
2129
  var chatDirective = ({ props, renderSlot }) => {
2098
2130
  const wrap = document.createElement("div");
2099
2131
  wrap.className = "nr-chat";
2100
- if (props.class) wrap.classList.add(...props.class.split(/\s+/).filter(Boolean));
2101
- if (props.style) wrap.setAttribute("style", props.style);
2132
+ applyBaseProps(wrap, props);
2102
2133
  wrap.appendChild(renderSlot("default"));
2103
2134
  return wrap;
2104
2135
  };
@@ -2134,8 +2165,7 @@ function bindEventProp(el, eventProp) {
2134
2165
  var richlistItemDirective = ({ props, renderSlot }) => {
2135
2166
  const li = document.createElement("li");
2136
2167
  li.className = "nr-richlist__item";
2137
- if (props.class) li.classList.add(...props.class.split(/\s+/).filter(Boolean));
2138
- if (props.style) li.setAttribute("style", props.style);
2168
+ applyBaseProps(li, props);
2139
2169
  if (props.image) {
2140
2170
  const thumb = document.createElement("div");
2141
2171
  thumb.className = "nr-richlist__thumb";
@@ -2197,36 +2227,20 @@ var richlistItemDirective = ({ props, renderSlot }) => {
2197
2227
  var richlistDirective = ({ props, renderSlot }) => {
2198
2228
  const ul = document.createElement("ul");
2199
2229
  ul.className = "nr-richlist";
2200
- if (props.class) ul.classList.add(...props.class.split(/\s+/).filter(Boolean));
2201
- if (props.style) ul.setAttribute("style", props.style);
2230
+ applyBaseProps(ul, props);
2202
2231
  ul.appendChild(renderSlot("default"));
2203
2232
  return ul;
2204
2233
  };
2205
2234
  var richlist_default = richlistDirective;
2206
2235
 
2207
2236
  // vanilla/directives/stat.ts
2208
- var STAT_THEME_TOKENS = /* @__PURE__ */ new Set([
2209
- "primary",
2210
- "secondary",
2211
- "info",
2212
- "success",
2213
- "warning",
2214
- "error"
2215
- ]);
2216
- function isArbitraryColor2(value) {
2217
- if (STAT_THEME_TOKENS.has(value)) return false;
2218
- if (/^(#|rgb|hsl|oklch|oklab|lab|lch|color\()/i.test(value)) return true;
2219
- if (/^[a-zA-Z]+$/.test(value)) return true;
2220
- return false;
2221
- }
2222
2237
  var statDirective = ({ props }) => {
2223
- const isThemeToken = STAT_THEME_TOKENS.has(props.color || "");
2224
- const colorClass = isThemeToken ? ` nr-stat--${props.color}` : "";
2238
+ const statIsThemeToken = isThemeToken(props.color);
2239
+ const colorClass = statIsThemeToken ? ` nr-stat--${props.color}` : "";
2225
2240
  const stat = document.createElement("div");
2226
2241
  stat.className = `nr-stat${colorClass}`;
2227
- if (props.class) stat.classList.add(...props.class.split(/\s+/).filter(Boolean));
2228
- if (props.style) stat.setAttribute("style", props.style);
2229
- const useInlineColor = props.color && isArbitraryColor2(props.color) && !isThemeToken;
2242
+ applyBaseProps(stat, props);
2243
+ const useInlineColor = props.color && isArbitraryColor(props.color) && !statIsThemeToken;
2230
2244
  if (props.icon) {
2231
2245
  const figure = document.createElement("div");
2232
2246
  figure.className = "nr-stat__figure";
@@ -2319,9 +2333,12 @@ function renderHtmlString(html) {
2319
2333
  processedContent = processedContent.replace(
2320
2334
  /<style(?:\s+[^>]*)?>([\s\S]*?)<\/style>/gi,
2321
2335
  (_match, cssContent) => {
2336
+ const trimmed = cssContent.trim();
2337
+ const existing = document.head.querySelector("style[data-nr-global]");
2338
+ if (existing && existing.textContent === trimmed) return "";
2322
2339
  const styleEl = document.createElement("style");
2323
2340
  styleEl.setAttribute("data-nr-global", "");
2324
- styleEl.textContent = cssContent;
2341
+ styleEl.textContent = trimmed;
2325
2342
  document.head.appendChild(styleEl);
2326
2343
  return "";
2327
2344
  }
@@ -2391,19 +2408,14 @@ function renderElement(element, ctx, allElements) {
2391
2408
  switch (element.type) {
2392
2409
  case "header": {
2393
2410
  const tag = `h${element.level}`;
2394
- let text = element.text;
2395
- const alignCenter = text.match(/^->\s*(.+?)\s*<-\s*$/);
2396
- const alignRight = text.match(/^->\s*(.+?)\s*->\s*$/);
2397
- if (alignCenter) text = alignCenter[1];
2398
- else if (alignRight) text = alignRight[1];
2399
2411
  const h = document.createElement(tag);
2400
2412
  h.id = element.id;
2401
2413
  let cls = `md-h${element.level}`;
2402
- if (alignCenter) cls += " text-center";
2403
- if (alignRight) cls += " text-right";
2414
+ if (element.align === "center") cls += " text-center";
2415
+ else if (element.align === "right") cls += " text-right";
2404
2416
  if (element.classes) cls += ` ${element.classes}`;
2405
2417
  h.className = cls;
2406
- h.appendChild(renderInline(text));
2418
+ h.appendChild(renderInline(element.text));
2407
2419
  return h;
2408
2420
  }
2409
2421
  case "paragraph": {
@@ -2555,7 +2567,7 @@ var guideData = [
2555
2567
  "title": "Introducci\xF3n",
2556
2568
  "icon": "menu_book",
2557
2569
  "order": 1,
2558
- "md": '# Introducci\xF3n a NoirMD\n\n**NoirMD** es un editor y motor de renderizado Markdown con extensiones propias: **admoniciones**, **componentes**, **directivas interactivas** y **markdown enriquecido**.\n\nEsta gu\xEDa est\xE1 escrita con el propio motor: cada directiva que ves aqu\xED es una muestra **viva** y funcional, no una captura.\n\n## C\xF3mo usar el editor\n\n| Elemento | Descripci\xF3n |\n| --- | --- |\n| Toolbar superior | Modo (editor / split / preview), guardar, copiar, imprimir, tema, gu\xEDa y configurar |\n| Panel izquierdo | Editor de c\xF3digo con resaltado de sintaxis |\n| Panel derecho | Preview en vivo (en modo split o preview) |\n| Atajo | `Ctrl+S` guarda el contenido |\n\n## Sintaxis de una directiva\n\nLas directivas se escriben con tres dos puntos `:::` y un nombre, opcionalmente con atributos entre llaves:\n\n```\n:::card {title="Mi tarjeta" icon="star"}\n\nContenido **markdown** aqu\xED dentro.\n\n:::\n```\n\nTodo lo que est\xE1 entre la apertura y el cierre `:::` se renderiza con el mismo motor, as\xED que puedes **anidar** directivas.\n\n## Cheatsheet r\xE1pido\n\n| Sintaxis | Resultado |\n| --- | --- |\n| `# T\xEDtulo` \u2192 `###### T\xEDtulo` | Encabezados |\n| `**negrita**` \xB7 `*cursiva*` \xB7 `~~tachado~~` | \xC9nfasis |\n| `` `c\xF3digo` `` | C\xF3digo inline |\n| `` ```js `` | Bloque de c\xF3digo con resaltado |\n| `[texto](url)` | Enlace |\n| `![alt](url)` | Imagen |\n| `![alt](url#left)` | Imagen flotante a la izquierda |\n| `==resaltado==` | Resaltado |\n| `%color%texto%%` | Texto de color |\n| `->centrado<-` | Texto centrado |\n| `!>spoiler<!` | Spoiler oculto |\n| `|[[icono]]|` | Icono Material |\n| `[TOC]` | \xCDndice de contenidos |\n| `:::note` `:::warning` `:::danger` `:::info` `:::greentext` | Admoniciones |\n| `:::card` `:::accordion` `:::carousel` `:::diff` `:::chat` `:::stat` `:::countdown` `:::keys` `:::hover-3d` `:::hover-gallery` `:::richlist` | Componentes |\n| `:::details` `:::modal` `:::button` `:::slide` | Interactivos |\n| `<style>` HTML inline | Bloques HTML (CSS global, HTML crudo) |\n\n## Organizaci\xF3n de la gu\xEDa\n\n- **Markdown** \u2014 sintaxis base y enriquecida (t\xEDtulos, \xE9nfasis, tablas, c\xF3digo, im\xE1genes, inline).\n- **Admoniciones** \u2014 cajas de aviso: nota, warning, danger, info y greentext.\n- **Componentes** \u2014 los 10 componentes de tarjeta, teclas, acorde\xF3n, carrusel, etc.\n- **Interactivos** \u2014 details, modal, botones y slides.\n- **Layout** \u2014 bloques HTML crudo: CSS global con `<style>` e HTML inline.\n\nCada p\xE1gina incluye: la sintaxis exacta, la tabla de props, un ejemplo en vivo y el c\xF3digo fuente para copiar.'
2570
+ "md": '# Introducci\xF3n a NoirMD\n\n**NoirMD** es un editor y motor de renderizado Markdown con extensiones propias: **admoniciones**, **componentes**, **directivas interactivas** y **markdown enriquecido**.\n\nEsta gu\xEDa est\xE1 escrita con el propio motor: cada directiva que ves aqu\xED es una muestra **viva** y funcional, no una captura.\n\n## C\xF3mo usar el editor\n\n| Elemento | Descripci\xF3n |\n| --- | --- |\n| Toolbar superior | Modo (editor / split / preview), guardar, copiar, imprimir, tema, gu\xEDa y configurar |\n| Panel izquierdo | Editor de c\xF3digo con resaltado de sintaxis |\n| Panel derecho | Preview en vivo (en modo split o preview) |\n| Atajo | `Ctrl+S` guarda el contenido |\n\n## Sintaxis de una directiva\n\nLas directivas se escriben con tres dos puntos `:::` y un nombre, opcionalmente con atributos entre llaves:\n\n```\n:::card {title="Mi tarjeta" icon="star"}\n\nContenido **markdown** aqu\xED dentro.\n\n:::\n```\n\nTodo lo que est\xE1 entre la apertura y el cierre `:::` se renderiza con el mismo motor, as\xED que puedes **anidar** directivas.\n\n## Cheatsheet r\xE1pido\n\n| Sintaxis | Resultado |\n| --- | --- |\n| `# T\xEDtulo` \u2192 `###### T\xEDtulo` | Encabezados |\n| `**negrita**` \xB7 `_cursiva_` \xB7 `~~tachado~~` | \xC9nfasis |\n| `` `c\xF3digo` `` | C\xF3digo inline |\n| `` ```js `` | Bloque de c\xF3digo con resaltado |\n| `[texto](url)` | Enlace |\n| `![alt](url)` | Imagen |\n| `![alt](url#left)` | Imagen flotante a la izquierda |\n| `==resaltado==` | Resaltado |\n| `%color%texto%%` | Texto de color |\n| `->centrado<-` | Texto centrado |\n| `!>spoiler<!` | Spoiler oculto |\n| `|[[icono]]|` | Icono Material |\n| `[TOC]` | \xCDndice de contenidos |\n| `:::note` `:::warning` `:::danger` `:::info` `:::greentext` | Admoniciones |\n| `:::card` `:::accordion` `:::carousel` `:::diff` `:::chat` `:::stat` `:::countdown` `:::keys` `:::hover-3d` `:::hover-gallery` `:::richlist` | Componentes |\n| `:::details` `:::modal` `:::button` `:::slide` | Interactivos |\n| `<style>` HTML inline | Bloques HTML (CSS global, HTML crudo) |\n\n## Organizaci\xF3n de la gu\xEDa\n\n- **Markdown** \u2014 sintaxis base y enriquecida (t\xEDtulos, \xE9nfasis, tablas, c\xF3digo, im\xE1genes, inline).\n- **Admoniciones** \u2014 cajas de aviso: nota, warning, danger, info y greentext.\n- **Componentes** \u2014 los 10 componentes de tarjeta, teclas, acorde\xF3n, carrusel, etc.\n- **Interactivos** \u2014 details, modal, botones y slides.\n- **Layout** \u2014 bloques HTML crudo: CSS global con `<style>` e HTML inline.\n\nCada p\xE1gina incluye: la sintaxis exacta, la tabla de props, un ejemplo en vivo y el c\xF3digo fuente para copiar.'
2559
2571
  },
2560
2572
  {
2561
2573
  "id": "titulos",
@@ -2771,7 +2783,7 @@ var guideData = [
2771
2783
  "title": "Modal",
2772
2784
  "icon": "open_in_full",
2773
2785
  "order": 2,
2774
- "md": '# Modal\n\nLa directiva `:::modal` crea un **di\xE1logo modal** con su bot\xF3n de apertura.\n\n## Sintaxis\n\n```md\n:::modal {title="Confirmar borrado" label="Abrir modal" icon="delete"}\n\xBFSeguro que quieres borrar este documento? Esta acci\xF3n no se puede deshacer.\n\n| Acci\xF3n | Efecto |\n| --- | --- |\n| Aceptar | Borra el documento |\n| Cancelar | No hace nada |\n:::\n```\n\n:::modal {title="Confirmar borrado" label="Abrir modal" icon="delete"}\n\xBFSeguro que quieres borrar este documento? Esta acci\xF3n no se puede deshacer.\n\n| Acci\xF3n | Efecto |\n| --- | --- |\n| Aceptar | Borra el documento |\n| Cancelar | No hace nada |\n:::\n\n## Contenido enriquecido\n\n```md\n:::modal {title="Notas de la versi\xF3n" label="Ver novedades" icon="new_releases"}\n**v2.0** \u2014 cambios principales:\n\n- Nuevo componente `:::diff`\n- Gu\xEDa integrada en el editor\n- Especificidad CSS corregida en im\xE1genes\n:::\n```\n\n:::modal {title="Notas de la versi\xF3n" label="Ver novedades" icon="new_releases"}\n**v2.0** \u2014 cambios principales:\n\n- Nuevo componente `:::diff`\n- Gu\xEDa integrada en el editor\n- Especificidad CSS corregida en im\xE1genes\n:::\n\n## Props\n\n| Prop | Tipo | Descripci\xF3n |\n| --- | --- | --- |\n| `title` | texto | T\xEDtulo del modal |\n| `label` | texto | Texto del bot\xF3n de apertura (default: `title` o \xABOpen\xBB) |\n| `icon` | nombre Material | Icono del bot\xF3n (default `open_in_new`) |\n| `class` | texto | Clases CSS adicionales |\n| `style` | CSS | Estilos inline |\n\n## Interacci\xF3n\n\n- **Bot\xF3n**: abre el modal (focus se mueve dentro).\n- **Overlay** o bot\xF3n **\xD7**: cierra.\n- **Esc**: cierra (en desktop).\n- `dialog` nativo \u2192 accesible por defecto, focus trapped y `inert` al fondo.'
2786
+ "md": '# Modal\n\nLa directiva `:::modal` crea un **di\xE1logo modal** con su bot\xF3n de apertura.\n\n## Sintaxis\n\n```md\n:::modal {title="Confirmar borrado" label="Abrir modal" icon="delete"}\n\xBFSeguro que quieres borrar este documento? Esta acci\xF3n no se puede deshacer.\n\n| Acci\xF3n | Efecto |\n| --- | --- |\n| Aceptar | Borra el documento |\n| Cancelar | No hace nada |\n:::\n```\n\n:::modal {title="Confirmar borrado" label="Abrir modal" icon="delete"}\n\xBFSeguro que quieres borrar este documento? Esta acci\xF3n no se puede deshacer.\n\n| Acci\xF3n | Efecto |\n| --- | --- |\n| Aceptar | Borra el documento |\n| Cancelar | No hace nada |\n:::\n\n## Contenido enriquecido\n\n```md\n:::modal {title="Notas de la versi\xF3n" label="Ver novedades" icon="new_releases"}\n**v2.0** \u2014 cambios principales:\n\n- Nuevo componente `:::diff`\n- Gu\xEDa integrada en el editor\n- Especificidad CSS corregida en im\xE1genes\n:::\n```\n\n:::modal {title="Notas de la versi\xF3n" label="Ver novedades" icon="new_releases"}\n**v2.0** \u2014 cambios principales:\n\n- Nuevo componente `:::diff`\n- Gu\xEDa integrada en el editor\n- Especificidad CSS corregida en im\xE1genes\n:::\n\n## Sin icono\n\nPara ocultar el icono del bot\xF3n de apertura, usa `icon="none"`:\n\n```md\n:::modal {title="Sin icono" label="Abrir" icon="none"}\nContenido del modal.\n:::\n```\n\n:::modal {title="Sin icono" label="Abrir" icon="none"}\nContenido del modal.\n:::\n\n## Color\n\nEl bot\xF3n de apertura acepta tokens de tema (`primary`, `secondary`, `info`, `success`, `warning`, `error`) o colores CSS arbitrarios:\n\n```md\n:::modal {title="\xC9xito" label="Abrir" icon="check_circle" color="success"}\nAcci\xF3n completada correctamente.\n:::\n```\n\n:::modal {title="\xC9xito" label="Abrir" icon="check_circle" color="success"}\nAcci\xF3n completada correctamente.\n:::\n\n## Alineaci\xF3n del bot\xF3n\n\nEl bot\xF3n de apertura se alinea a la izquierda por defecto. Usa el prop `align` para cambiar la alineaci\xF3n:\n\n### Centrado\n\n```md\n:::modal {title="Centrado" label="Abrir" icon="open_in_full" align="center"}\nContenido del modal centrado.\n:::\n```\n\n:::modal {title="Centrado" label="Abrir" icon="open_in_full" align="center"}\nContenido del modal centrado.\n:::\n\n### Alineado a la derecha\n\n```md\n:::modal {title="Derecha" label="Abrir" icon="open_in_full" align="right"}\nContenido del modal alineado a la derecha.\n:::\n```\n\n:::modal {title="Derecha" label="Abrir" icon="open_in_full" align="right"}\nContenido del modal alineado a la derecha.\n:::\n\n## Props\n\n| Prop | Tipo | Descripci\xF3n |\n| --- | --- | --- |\n| `title` | texto | T\xEDtulo del modal |\n| `label` (o `title`) | texto | Texto del bot\xF3n de apertura (`title` funciona como alias, default: \xABOpen\xBB) |\n| `icon` | nombre Material | Icono del bot\xF3n (default `open_in_full`). Usa `icon="none"` para ocultar |\n| `color` | token de tema o CSS | Color del bot\xF3n de apertura |\n| `align` | `left` / `center` / `right` | Alineaci\xF3n del bot\xF3n de apertura (default `left`) |\n| `class` | texto | Clases CSS adicionales (se aplican al bot\xF3n de apertura) |\n| `style` | CSS | Estilos inline (se aplican al bot\xF3n de apertura) |\n\n## Interacci\xF3n\n\n- **Bot\xF3n**: abre el modal (focus se mueve dentro).\n- **Overlay** o bot\xF3n **\xD7**: cierra.\n- **Esc**: cierra (en desktop).\n- `dialog` nativo \u2192 accesible por defecto, focus trapped y `inert` al fondo.'
2775
2787
  },
2776
2788
  {
2777
2789
  "id": "button",
@@ -2779,7 +2791,7 @@ var guideData = [
2779
2791
  "title": "Button",
2780
2792
  "icon": "touch_app",
2781
2793
  "order": 3,
2782
- "md": '# Button\n\nLa directiva `:::button` crea un **bot\xF3n con enlace** (se abre en pesta\xF1a nueva por defecto).\n\n## Sintaxis\n\n```md\n:::button {label="Documentaci\xF3n" url="https://example.com" icon="menu_book"}\n:::\n```\n\n:::button {label="Documentaci\xF3n" url="https://example.com" icon="menu_book"}\n:::\n\n## Variante con enlace interno\n\n```md\n:::button {label="Ir a la p\xE1gina de notas" url="#admonici\xF3n-nota" icon="sticky_note_2" target="_self"}\n:::\n```\n\n:::button {label="Ir a la p\xE1gina de notas" url="#admonici\xF3n-nota" icon="sticky_note_2" target="_self"}\n:::\n\n## Con contenido markdown\n\nSi el bloque contiene texto/enlaces, se renderizan dentro del bot\xF3n:\n\n```md\n:::button {label="Descargar" url="https://example.com/download" icon="download"}\nDescarga el **manual** en PDF\n:::\n```\n\n:::button {label="Descargar" url="https://example.com/download" icon="download"}\nDescarga el **manual** en PDF\n:::\n\n## Props\n\n| Prop | Tipo | Descripci\xF3n |\n| --- | --- | --- |\n| `label` | texto | Texto del bot\xF3n |\n| `url` (o `href`) | URL | Destino del enlace (default `#`) |\n| `icon` | nombre Material | Icono (default `near_me`) |\n| `target` | `_blank` / `_self` / ... | Destino del enlace (default `_blank`) |\n| `class` | texto | Clases CSS adicionales |'
2794
+ "md": '# Button\n\nLa directiva `:::button` crea un **bot\xF3n con enlace** (se abre en pesta\xF1a nueva por defecto).\n\n## Sintaxis\n\n```md\n:::button {label="Documentaci\xF3n" url="https://example.com" icon="menu_book"}\n:::\n```\n\n:::button {label="Documentaci\xF3n" url="https://example.com" icon="menu_book"}\n:::\n\n## Variante con enlace interno\n\n```md\n:::button {label="Ir a la p\xE1gina de notas" url="#admonici\xF3n-nota" icon="sticky_note_2" target="_self"}\n:::\n```\n\n:::button {label="Ir a la p\xE1gina de notas" url="#admonici\xF3n-nota" icon="sticky_note_2" target="_self"}\n:::\n\n## Con contenido markdown\n\nSi el bloque contiene texto/enlaces, se renderizan dentro del bot\xF3n. El icono se a\xF1ade autom\xE1ticamente a cada enlace del contenido:\n\n```md\n:::button {label="Descargar" url="https://example.com/download" icon="download"}\nDescarga el **manual** en PDF\n:::\n```\n\n:::button {label="Descargar" url="https://example.com/download" icon="download"}\nDescarga el **manual** en PDF\n:::\n\n## Con enlace en el contenido\n\nSi el contenido es un enlace markdown, el bot\xF3n usa el enlace del contenido y el icono se a\xF1ade al principio:\n\n```md\n:::button {icon="star"}\n[Descarga con FDM](https://example.com/download)\n:::\n```\n\n:::button {icon="star"}\n[Descarga con FDM](https://example.com/download)\n:::\n\n## Sin icono\n\nPara ocultar el icono, usa `icon="none"`:\n\n```md\n:::button {label="Sin icono" url="https://example.com" icon="none"}\n:::\n```\n\n:::button {label="Sin icono" url="https://example.com" icon="none"}\n:::\n\n## Color\n\nLos botones aceptan tokens de tema (`primary`, `secondary`, `info`, `success`, `warning`, `error`) o colores CSS arbitrarios (`red`, `#ff0000`, `rgb(255,0,0)`):\n\n```md\n:::button {label="\xC9xito" url="https://example.com" icon="check_circle" color="success"}\n:::\n```\n\n:::button {label="\xC9xito" url="https://example.com" icon="check_circle" color="success"}\n:::\n\n```md\n:::button {label="Rojo" url="https://example.com" icon="error" color="#e11d48"}\n:::\n```\n\n:::button {label="Rojo" url="https://example.com" icon="error" color="#e11d48"}\n:::\n\n## Alineaci\xF3n\n\nLos botones se alinean a la izquierda por defecto. Usa el prop `align` para cambiar la alineaci\xF3n:\n\n### Centrado\n\n```md\n:::button {label="Centrado" url="https://example.com" icon="center_focus_strong" align="center"}\n:::\n```\n\n:::button {label="Centrado" url="https://example.com" icon="center_focus_strong" align="center"}\n:::\n\n### Alineado a la derecha\n\n```md\n:::button {label="Derecha" url="https://example.com" icon="arrow_forward" align="right"}\n:::\n```\n\n:::button {label="Derecha" url="https://example.com" icon="arrow_forward" align="right"}\n:::\n\n## Props\n\n| Prop | Tipo | Descripci\xF3n |\n| --- | --- | --- |\n| `label` (o `title`) | texto | Texto del bot\xF3n (`title` funciona como alias por compatibilidad) |\n| `url` (o `href`) | URL | Destino del enlace (default `#`) |\n| `icon` | nombre Material | Icono (default `touch_app`). Usa `icon="none"` para ocultar |\n| `target` | `_blank` / `_self` / ... | Destino del enlace (default `_blank`) |\n| `color` | token de tema o CSS | Color del bot\xF3n (ver colores soportados arriba) |\n| `align` | `left` / `center` / `right` | Alineaci\xF3n del bot\xF3n (default `left`) |\n| `class` | texto | Clases CSS adicionales |\n| `style` | CSS | Estilos inline (ej: `style="font-size:1rem"`) |'
2783
2795
  },
2784
2796
  {
2785
2797
  "id": "slide",
@@ -2796,6 +2808,14 @@ var guideData = [
2796
2808
  "icon": "code_off",
2797
2809
  "order": 1,
2798
2810
  "md": '# Bloques HTML\r\n\r\nNoirMD permite escribir **HTML crudo** directamente en el markdown. Seg\xFAn la etiqueta, el comportamiento var\xEDa.\r\n\r\n## CSS global con `<style>`\r\n\r\nEscribe un bloque `<style>` para inyectar CSS en el documento. El contenido se extrae autom\xE1ticamente y se inserta en el `<head>` del DOM.\r\n\r\n```md\r\n<style>\r\n.mi-clase {\r\n background: #f1f5f9;\r\n border-radius: 10px;\r\n padding: 1rem;\r\n}\r\n</style>\r\n```\r\n\r\n<style>\r\n.nr-demo-box {\r\n display: grid;\r\n grid-template-columns: 1fr 1fr;\r\n gap: 1rem;\r\n padding: 1rem;\r\n border-radius: 12px;\r\n background: color-mix(in srgb, var(--color-accent-primary, #0ea5e9) 10%, transparent);\r\n}\r\n.nr-demo-box > div {\r\n padding: 1rem;\r\n border-radius: 8px;\r\n background: var(--color-background-secondary-solid, #1e293b);\r\n}\r\n</style>\r\n\r\n<div class="nr-demo-box">\r\n<div>**A**</div>\r\n<div>**B**</div>\r\n</div>\r\n\r\n> El CSS se aplica al **documento renderizado completo**, no solo al bloque. Define clases una vez y \xFAsalas despu\xE9s en cualquier etiqueta HTML.\r\n\r\n## HTML inline\r\n\r\nCualquier etiqueta HTML escrita directamente en el markdown se renderiza sin procesar como markdown. El contenido dentro se preserva tal cual.\r\n\r\n```md\r\n<div style="text-align: center; padding: 1rem; border: 1px solid #334155; border-radius: 10px;">\r\n HTML escrito a mano funciona tal cual.\r\n</div>\r\n```\r\n\r\n<div style="text-align: center; padding: 1rem; border: 1px solid #334155; border-radius: 10px;">\r\n HTML escrito a mano funciona tal cual.\r\n</div>\r\n\r\n## Elementos interactivos nativos\r\n\r\n```md\r\n<details class="nr-details">\r\n <summary>Detalle nativo con <b>HTML</b></summary>\r\n <p>Los atributos, estilos y eventos se conservan intactos.</p>\r\n</details>\r\n```\r\n\r\n<details class="nr-details">\r\n <summary>Detalle nativo con <b>HTML</b></summary>\r\n <p>Los atributos, estilos y eventos se conservan intactos.</p>\r\n</details>\r\n\r\n## Scripts\r\n\r\nLos bloques `<script>` se ejecutan autom\xE1ticamente al renderizar:\r\n\r\n```md\r\n<script>\r\n console.log(\'Este script se ejecuta al renderizar\');\r\n</script>\r\n```\r\n\r\n> \u26A0\uFE0F Al ser HTML y scripts sin filtrar, \xFAsalos solo con contenido de confianza.\r\n\r\n## Cu\xE1ndo usar bloques HTML\r\n\r\n- Insertar embeds (`iframe`, `video`, widgets externos).\r\n- Estructuras que el markdown no cubre (layouts complejos, formularios nativos).\r\n- Inyectar CSS reutilizable con `<style>`.\r\n- Prototipar HTML antes de convertirlo a directiva.'
2811
+ },
2812
+ {
2813
+ "id": "wrapper-directives",
2814
+ "category": "Layout",
2815
+ "title": "Wrapper Directives",
2816
+ "icon": "crop_free",
2817
+ "order": 1,
2818
+ "md": '# Wrapper Directives\r\n\r\nLas directivas `:::div`, `:::style`, `:::custom` y `:::raw` son wrappers gen\xE9ricos para **envolver contenido** con clases, estilos y atributos personalizados.\r\n\r\n## `:::div` \u2014 Div gen\xE9rico\r\n\r\nEnvuelve contenido en un `<div>` con clases, id, estilos y atributos `data-*`.\r\n\r\n```md\r\n:::div {class="mi-clase" id="seccion" style="padding: 2rem; background: #f0f0f0"}\r\n\r\nContenido **markdown** aqu\xED.\r\n\r\n:::\r\n```\r\n\r\n:::div {class="mi-clase" id="seccion" style="padding: 2rem; background: #f0f0f0"}\r\n\r\nContenido **markdown** aqu\xED.\r\n\r\n:::\r\n\r\n## `:::style` \u2014 Inyectar CSS\r\n\r\nInyecta un bloque `<style>` global. \xDAtil para estilos que afectan m\xFAltiples componentes.\r\n\r\n```md\r\n:::style\r\n.nr-mi-clase { color: red; }\r\n:::\r\n```\r\n\r\n## `:::custom` \u2014 Elemento personalizado\r\n\r\nSimilar a `div`, pero permite crear cualquier elemento HTML.\r\n\r\n## `:::raw` \u2014 HTML crudo\r\n\r\nRenderiza contenido HTML sin procesar.\r\n\r\n## Props\r\n\r\n| Prop | Tipo | Descripci\xF3n |\r\n| --- | --- | --- |\r\n| `class` | texto | Clases CSS (soporta `.shorthand` tambi\xE9n) |\r\n| `id` | texto | ID del elemento |\r\n| `style` | CSS inline | Estilos inline (se aplica con `setProperty`, no sobreescribe otros estilos) |\r\n| `data-*` | texto | Cualquier atributo `data-*` se aplica al elemento |\r\n\r\n## Shorthands\r\n\r\n```md\r\n:::div {.mi-clase #mi-id}\r\n\r\nIgual que usar `class="mi-clase" id="mi-id"`.\r\n\r\n:::\r\n```'
2799
2819
  }
2800
2820
  ];
2801
2821