@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.js CHANGED
@@ -531,6 +531,22 @@ function parseHtmlAttrs(attrsString) {
531
531
  }
532
532
 
533
533
  // core/parser.ts
534
+ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
535
+ "area",
536
+ "base",
537
+ "br",
538
+ "col",
539
+ "embed",
540
+ "hr",
541
+ "img",
542
+ "input",
543
+ "link",
544
+ "meta",
545
+ "param",
546
+ "source",
547
+ "track",
548
+ "wbr"
549
+ ]);
534
550
  function parseMarkdown(markdown2) {
535
551
  if (!markdown2) return [];
536
552
  const lines = markdown2.replace(/\r\n/g, "\n").replace(/\r/g, "").split("\n");
@@ -544,8 +560,19 @@ function parseMarkdown(markdown2) {
544
560
  if (match = trimmed.match(/^(#{1,6})\s+(.+)$/)) {
545
561
  const level = match[1].length;
546
562
  const rawText = match[2];
547
- const { text: text2, classes: classes2, id: customId } = extractAttributes(rawText);
548
- const baseId = customId || generateId(text2.replace(/->|<-/g, ""));
563
+ const { text: rawParsedText, classes: classes2, id: customId } = extractAttributes(rawText);
564
+ let text2 = rawParsedText;
565
+ let align;
566
+ const alignCenter = text2.match(/^->\s*(.+?)\s*<-\s*$/);
567
+ const alignRight = text2.match(/^->\s*(.+?)\s*->\s*$/);
568
+ if (alignCenter) {
569
+ text2 = alignCenter[1];
570
+ align = "center";
571
+ } else if (alignRight) {
572
+ text2 = alignRight[1];
573
+ align = "right";
574
+ }
575
+ const baseId = customId || generateId(text2);
549
576
  let id2 = baseId;
550
577
  let n = 1;
551
578
  while (usedIds.has(id2)) {
@@ -553,7 +580,7 @@ function parseMarkdown(markdown2) {
553
580
  id2 = `${baseId}-${n}`;
554
581
  }
555
582
  usedIds.add(id2);
556
- result.push({ type: "header", level, text: text2, id: id2, classes: classes2 || void 0 });
583
+ result.push({ type: "header", level, text: text2, id: id2, classes: classes2 || void 0, align });
557
584
  i++;
558
585
  continue;
559
586
  }
@@ -729,29 +756,13 @@ function parseMarkdown(markdown2) {
729
756
  let tagStartMatch = trimmed.match(/^<([a-zA-Z][\w-]*)/);
730
757
  if (tagStartMatch) {
731
758
  const tagName = tagStartMatch[1].toLowerCase();
732
- const voidElements = /* @__PURE__ */ new Set([
733
- "area",
734
- "base",
735
- "br",
736
- "col",
737
- "embed",
738
- "hr",
739
- "img",
740
- "input",
741
- "link",
742
- "meta",
743
- "param",
744
- "source",
745
- "track",
746
- "wbr"
747
- ]);
748
759
  const remainingText = lines.slice(i).join("\n");
749
760
  const openTagRegex = new RegExp(`^\\s*<${tagName}\\b([^>]*?)>`, "i");
750
761
  const openTagMatch = remainingText.match(openTagRegex);
751
762
  if (openTagMatch) {
752
763
  const fullOpenTag = openTagMatch[0];
753
764
  const attrs = openTagMatch[1].replace(/\s+/g, " ").trim();
754
- const isSelfClosing = fullOpenTag.endsWith("/>") || voidElements.has(tagName);
765
+ const isSelfClosing = fullOpenTag.endsWith("/>") || VOID_ELEMENTS.has(tagName);
755
766
  if (isSelfClosing) {
756
767
  const blockText = remainingText.substring(0, openTagMatch.index + fullOpenTag.length);
757
768
  const consumedLines = blockText.split("\n").length;
@@ -1054,6 +1065,7 @@ function createModal(title) {
1054
1065
  header.appendChild(titleEl);
1055
1066
  const closeBtn = document.createElement("button");
1056
1067
  closeBtn.className = "nr-modal__close";
1068
+ closeBtn.setAttribute("aria-label", "Close");
1057
1069
  closeBtn.appendChild(createIcon("close"));
1058
1070
  closeBtn.addEventListener("click", () => dialog.close());
1059
1071
  header.appendChild(closeBtn);
@@ -1274,11 +1286,89 @@ function parseInlinePart(part) {
1274
1286
  return document.createTextNode(part);
1275
1287
  }
1276
1288
 
1289
+ // vanilla/utils.ts
1290
+ var THEME_TOKENS = /* @__PURE__ */ new Set([
1291
+ "primary",
1292
+ "secondary",
1293
+ "accent",
1294
+ "neutral",
1295
+ "info",
1296
+ "success",
1297
+ "warning",
1298
+ "error"
1299
+ ]);
1300
+ function isThemeToken(color) {
1301
+ return !!color && THEME_TOKENS.has(color);
1302
+ }
1303
+ function isArbitraryColor(value) {
1304
+ if (THEME_TOKENS.has(value)) return false;
1305
+ if (/^(#|rgb|hsl|oklch|oklab|lab|lch|color\()/i.test(value)) return true;
1306
+ if (/^[a-zA-Z]+$/.test(value)) return true;
1307
+ return false;
1308
+ }
1309
+ function applyBaseProps(el, props) {
1310
+ if (props.class) {
1311
+ el.classList.add(...props.class.split(/\s+/).filter(Boolean));
1312
+ }
1313
+ if (props.style) {
1314
+ const styles = parseCssString(props.style);
1315
+ for (const [key, value] of Object.entries(styles)) {
1316
+ el.style.setProperty(key, String(value));
1317
+ }
1318
+ }
1319
+ }
1320
+ function applyFloatStyle(el, float, width) {
1321
+ if (!float) return;
1322
+ if (float === "left" || float === "right") {
1323
+ el.style.float = float;
1324
+ if (!width) el.style.maxWidth = "50%";
1325
+ el.style.marginInlineStart = float === "right" ? "1rem" : "";
1326
+ el.style.marginInlineEnd = float === "left" ? "1rem" : "";
1327
+ } else if (float === "center") {
1328
+ el.style.marginInline = "auto";
1329
+ }
1330
+ }
1331
+ function applyColor(el, color, classSuffix) {
1332
+ if (!color) return "";
1333
+ if (isThemeToken(color)) {
1334
+ return ` ${classSuffix}--${color}`;
1335
+ }
1336
+ if (isArbitraryColor(color)) {
1337
+ el.style.background = color;
1338
+ el.style.color = "white";
1339
+ }
1340
+ return "";
1341
+ }
1342
+ function openModal(dialog) {
1343
+ if (!dialog.open) {
1344
+ document.body.appendChild(dialog);
1345
+ dialog.showModal();
1346
+ dialog.addEventListener("close", () => dialog.remove(), { once: true });
1347
+ }
1348
+ }
1349
+ var IMG_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
1350
+ function applyAlignClass(el, baseClass, align) {
1351
+ if (align === "center") {
1352
+ el.classList.add(`${baseClass}--center`);
1353
+ } else if (align === "right") {
1354
+ el.classList.add(`${baseClass}--right`);
1355
+ }
1356
+ }
1357
+ function resolveIcon(value, fallback) {
1358
+ if (!value) return fallback;
1359
+ if (value === "none" || value === "off") return null;
1360
+ return value;
1361
+ }
1362
+ function parseIntProp(value, defaultValue) {
1363
+ if (!value) return defaultValue;
1364
+ const n = parseInt(value, 10);
1365
+ return Number.isNaN(n) ? defaultValue : n;
1366
+ }
1367
+
1277
1368
  // vanilla/directives/admonition.ts
1278
1369
  var admonitionDirective = ({ directiveType, props, renderSlot }) => {
1279
1370
  const el = createAdmonition(directiveType, props.title, props.icon);
1280
- if (props.class) el.classList.add(...props.class.split(/\s+/).filter(Boolean));
1281
- if (props.style) el.setAttribute("style", props.style);
1371
+ applyBaseProps(el, props);
1282
1372
  const body = el.querySelector(".nr-admonition__body");
1283
1373
  if (body) {
1284
1374
  body.appendChild(renderSlot("default"));
@@ -1294,8 +1384,7 @@ var detailsDirective = ({ props, renderSlot }) => {
1294
1384
  props.icon,
1295
1385
  props.defaultOpen === "true"
1296
1386
  );
1297
- if (props.class) el.classList.add(...props.class.split(/\s+/).filter(Boolean));
1298
- if (props.style) el.setAttribute("style", props.style);
1387
+ applyBaseProps(el, props);
1299
1388
  const body = el.querySelector(".nr-details__body");
1300
1389
  if (body) {
1301
1390
  body.appendChild(renderSlot("default"));
@@ -1308,14 +1397,17 @@ var details_default = detailsDirective;
1308
1397
  var modalDirective = ({ props, renderSlot }) => {
1309
1398
  const label = props.label || props.title || "Open";
1310
1399
  const modalTitle = props.title || "Modal";
1311
- const customClass = props.class || "";
1400
+ const align = props.align || "left";
1401
+ const iconName = resolveIcon(props.icon, "open_in_full");
1312
1402
  const wrapper = document.createElement("div");
1313
1403
  wrapper.className = "nr-modal-trigger";
1404
+ applyAlignClass(wrapper, "nr-modal-trigger", align);
1314
1405
  const btn = document.createElement("button");
1315
- btn.className = `nr-button nr-button--default`;
1316
- if (customClass) btn.classList.add(...customClass.split(/\s+/).filter(Boolean));
1317
- const icon = props.icon || "open_in_new";
1318
- if (icon) btn.appendChild(createIcon(icon));
1406
+ btn.className = "nr-button nr-button--default";
1407
+ btn.setAttribute("aria-haspopup", "dialog");
1408
+ applyColor(btn, props.color, "nr-button");
1409
+ applyBaseProps(btn, props);
1410
+ if (iconName) btn.appendChild(createIcon(iconName));
1319
1411
  btn.appendChild(document.createTextNode(label));
1320
1412
  const dialog = createModal(modalTitle);
1321
1413
  const body = dialog.querySelector(".nr-modal__body");
@@ -1325,15 +1417,7 @@ var modalDirective = ({ props, renderSlot }) => {
1325
1417
  prose.appendChild(renderSlot("default"));
1326
1418
  body.appendChild(prose);
1327
1419
  }
1328
- btn.addEventListener("click", () => {
1329
- if (!dialog.open) {
1330
- document.body.appendChild(dialog);
1331
- dialog.showModal();
1332
- dialog.addEventListener("close", () => {
1333
- dialog.remove();
1334
- }, { once: true });
1335
- }
1336
- });
1420
+ btn.addEventListener("click", () => openModal(dialog));
1337
1421
  wrapper.appendChild(btn);
1338
1422
  wrapper.appendChild(dialog);
1339
1423
  return wrapper;
@@ -1343,20 +1427,22 @@ var modal_default = modalDirective;
1343
1427
  // vanilla/directives/button.ts
1344
1428
  var buttonDirective = ({ props, renderSlot }) => {
1345
1429
  const url = props.url || props.href || "#";
1346
- const label = props.label;
1347
- const icon = props.icon || "near_me";
1430
+ const label = props.label || props.title;
1431
+ const iconName = resolveIcon(props.icon, "touch_app");
1348
1432
  const target = props.target || "_blank";
1349
- const customClass = props.class || "";
1433
+ const align = props.align || "left";
1350
1434
  const wrapper = document.createElement("div");
1351
1435
  wrapper.className = "nr-button-wrap";
1436
+ applyAlignClass(wrapper, "nr-button-wrap", align);
1352
1437
  if (label) {
1353
1438
  const a = document.createElement("a");
1354
1439
  a.href = url;
1355
1440
  a.target = target;
1356
- a.rel = "noopener noreferrer";
1441
+ if (target === "_blank") a.rel = "noopener noreferrer";
1357
1442
  a.className = "nr-button nr-button--default";
1358
- if (customClass) a.classList.add(...customClass.split(/\s+/).filter(Boolean));
1359
- a.appendChild(createIcon(icon));
1443
+ applyColor(a, props.color, "nr-button");
1444
+ applyBaseProps(a, props);
1445
+ if (iconName) a.appendChild(createIcon(iconName));
1360
1446
  a.appendChild(document.createTextNode(label));
1361
1447
  wrapper.appendChild(a);
1362
1448
  return wrapper;
@@ -1366,17 +1452,20 @@ var buttonDirective = ({ props, renderSlot }) => {
1366
1452
  if (links.length > 0) {
1367
1453
  links.forEach((link) => {
1368
1454
  link.classList.add("nr-button", "nr-button--default");
1369
- if (customClass) link.classList.add(...customClass.split(/\s+/).filter(Boolean));
1455
+ applyColor(link, props.color, "nr-button");
1456
+ if (iconName) link.prepend(createIcon(iconName));
1457
+ applyBaseProps(link, props);
1370
1458
  });
1371
1459
  wrapper.appendChild(slotContent);
1372
1460
  } else {
1373
1461
  const a = document.createElement("a");
1374
1462
  a.href = url;
1375
1463
  a.target = target;
1376
- a.rel = "noopener noreferrer";
1464
+ if (target === "_blank") a.rel = "noopener noreferrer";
1377
1465
  a.className = "nr-button nr-button--default";
1378
- if (customClass) a.classList.add(...customClass.split(/\s+/).filter(Boolean));
1379
- a.appendChild(createIcon(icon));
1466
+ applyColor(a, props.color, "nr-button");
1467
+ applyBaseProps(a, props);
1468
+ if (iconName) a.appendChild(createIcon(iconName));
1380
1469
  a.appendChild(slotContent);
1381
1470
  wrapper.appendChild(a);
1382
1471
  }
@@ -1398,12 +1487,9 @@ var cardDirective = ({
1398
1487
  const { isSingleCard } = options || {};
1399
1488
  const isModal = directiveType === "card-m";
1400
1489
  const isLink = directiveType === "card-b";
1401
- const inlineStyles = props.style ? parseCssString(props.style) : {};
1402
1490
  const card = document.createElement("div");
1403
1491
  card.className = `nr-card${isModal || isLink ? " nr-card--interactive" : ""} ${customClass}`.trim();
1404
- for (const [key, value] of Object.entries(inlineStyles)) {
1405
- card.style.setProperty(key, String(value));
1406
- }
1492
+ applyBaseProps(card, props);
1407
1493
  if (image) {
1408
1494
  const imgWrap = document.createElement("div");
1409
1495
  imgWrap.className = `nr-card__image${isSingleCard ? " nr-card__image--tall" : ""}`;
@@ -1482,13 +1568,7 @@ var cardDirective = ({
1482
1568
  prose.appendChild(renderSlot("content") || renderSlot("default"));
1483
1569
  modalBody.appendChild(prose);
1484
1570
  }
1485
- card.addEventListener("click", () => {
1486
- if (!dialog.open) {
1487
- document.body.appendChild(dialog);
1488
- dialog.showModal();
1489
- dialog.addEventListener("close", () => dialog.remove(), { once: true });
1490
- }
1491
- });
1571
+ card.addEventListener("click", () => openModal(dialog));
1492
1572
  const frag = document.createDocumentFragment();
1493
1573
  frag.appendChild(card);
1494
1574
  frag.appendChild(dialog);
@@ -1536,8 +1616,8 @@ var slideDirective = ({
1536
1616
  if (lines.length === 0) {
1537
1617
  return document.createDocumentFragment();
1538
1618
  }
1539
- const interval = parseInt(props.interval || "3000", 10);
1540
- const speed = parseInt(props.speed || "500", 10);
1619
+ const interval = parseIntProp(props.interval, 3e3);
1620
+ const speed = parseIntProp(props.speed, 500);
1541
1621
  const rawClass = props.class || "";
1542
1622
  const inlineStyle = props.style ? parseCssString(props.style) : {};
1543
1623
  const scopeClass = `sld-${++slideCounter}`;
@@ -1588,10 +1668,11 @@ var slideDirective = ({
1588
1668
  }
1589
1669
  });
1590
1670
  if (lines.length > 1) {
1591
- setInterval(() => {
1671
+ const id = setInterval(() => {
1592
1672
  current = (current + 1) % lines.length;
1593
1673
  track.style.transform = `translateY(${-current * maxH}px)`;
1594
1674
  }, interval);
1675
+ container.dataset.nrIntervalId = String(id);
1595
1676
  }
1596
1677
  return container;
1597
1678
  };
@@ -1601,8 +1682,7 @@ var slide_default = slideDirective;
1601
1682
  var keysDirective = ({ props, slots }) => {
1602
1683
  const wrap = document.createElement("div");
1603
1684
  wrap.className = "nr-keys";
1604
- if (props.class) wrap.classList.add(...props.class.split(/\s+/).filter(Boolean));
1605
- if (props.style) wrap.setAttribute("style", props.style);
1685
+ applyBaseProps(wrap, props);
1606
1686
  const sizeClass = props.size ? ` nr-kbd--${props.size}` : "";
1607
1687
  const parts = (slots.default || "").split("+").map((p) => p.trim()).filter(Boolean);
1608
1688
  parts.forEach((part, i) => {
@@ -1626,8 +1706,7 @@ var accordionCounter = 0;
1626
1706
  var accordionItemDirective = ({ props, renderSlot }) => {
1627
1707
  const item = document.createElement("div");
1628
1708
  item.className = "nr-accordion__item";
1629
- if (props.class) item.classList.add(...props.class.split(/\s+/).filter(Boolean));
1630
- if (props.style) item.setAttribute("style", props.style);
1709
+ applyBaseProps(item, props);
1631
1710
  const input = document.createElement("input");
1632
1711
  input.type = "radio";
1633
1712
  input.className = "nr-accordion__input";
@@ -1646,8 +1725,7 @@ var accordionItemDirective = ({ props, renderSlot }) => {
1646
1725
  var accordionDirective = ({ props, renderSlot }) => {
1647
1726
  const wrap = document.createElement("div");
1648
1727
  wrap.className = "nr-accordion";
1649
- if (props.class) wrap.classList.add(...props.class.split(/\s+/).filter(Boolean));
1650
- if (props.style) wrap.setAttribute("style", props.style);
1728
+ applyBaseProps(wrap, props);
1651
1729
  wrap.appendChild(renderSlot("default"));
1652
1730
  const mode = props.mode === "checkbox" ? "checkbox" : "radio";
1653
1731
  const group = `nr-acc-${++accordionCounter}`;
@@ -1660,7 +1738,6 @@ var accordionDirective = ({ props, renderSlot }) => {
1660
1738
  var accordion_default = accordionDirective;
1661
1739
 
1662
1740
  // vanilla/directives/carousel.ts
1663
- var IMG_RE = /!\[([^\]]*)\]\(([^)]+)\)/g;
1664
1741
  var carouselDirective = ({ props, slots }) => {
1665
1742
  const images = [];
1666
1743
  const raw = slots.default || "";
@@ -1676,19 +1753,9 @@ var carouselDirective = ({ props, slots }) => {
1676
1753
  wrap.className = "nr-carousel";
1677
1754
  wrap.tabIndex = 0;
1678
1755
  wrap.setAttribute("aria-label", "Image carousel");
1679
- if (props.class) wrap.classList.add(...props.class.split(/\s+/).filter(Boolean));
1680
- if (props.style) wrap.setAttribute("style", props.style);
1756
+ applyBaseProps(wrap, props);
1681
1757
  if (props.width) wrap.style.width = props.width;
1682
- if (props.float) {
1683
- if (props.float === "left" || props.float === "right") {
1684
- wrap.style.float = props.float;
1685
- if (!props.width) wrap.style.maxWidth = "50%";
1686
- wrap.style.marginInlineStart = props.float === "right" ? "1rem" : "";
1687
- wrap.style.marginInlineEnd = props.float === "left" ? "1rem" : "";
1688
- } else if (props.float === "center") {
1689
- wrap.style.marginInline = "auto";
1690
- }
1691
- }
1758
+ applyFloatStyle(wrap, props.float, props.width);
1692
1759
  const viewport = document.createElement("div");
1693
1760
  viewport.className = "nr-carousel__viewport";
1694
1761
  if (props.height) viewport.style.height = props.height;
@@ -1758,11 +1825,10 @@ var DEFAULT_LABELS = ["days", "hours", "min", "sec"];
1758
1825
  var countdownDirective = ({ props }) => {
1759
1826
  const wrap = document.createElement("div");
1760
1827
  wrap.className = "nr-countdown";
1761
- if (props.class) wrap.classList.add(...props.class.split(/\s+/).filter(Boolean));
1762
- if (props.style) wrap.setAttribute("style", props.style);
1828
+ applyBaseProps(wrap, props);
1763
1829
  const labelParts = (props.labels || "").split("|").map((s) => s.trim());
1764
1830
  const labels = DEFAULT_LABELS.map((label, i) => labelParts[i] || label);
1765
- const digits = parseInt(props.digits || "2", 10);
1831
+ const digits = parseIntProp(props.digits, 2);
1766
1832
  const targetTime = props.target ? new Date(props.target).getTime() : NaN;
1767
1833
  const hasTarget = !Number.isNaN(targetTime);
1768
1834
  const blocks = [];
@@ -1809,13 +1875,15 @@ var countdownDirective = ({ props }) => {
1809
1875
  blocks.push({ value });
1810
1876
  });
1811
1877
  render();
1812
- if (hasTarget) setInterval(render, 1e3);
1878
+ if (hasTarget) {
1879
+ const id = setInterval(render, 1e3);
1880
+ wrap.dataset.nrIntervalId = String(id);
1881
+ }
1813
1882
  return wrap;
1814
1883
  };
1815
1884
  var countdown_default = countdownDirective;
1816
1885
 
1817
1886
  // vanilla/directives/diff.ts
1818
- var IMG_RE2 = /!\[([^\]]*)\]\(([^)]+)\)/g;
1819
1887
  var diffDirective = ({ props, slots }) => {
1820
1888
  let before = (props.before || "").split("#")[0].trim();
1821
1889
  let after = (props.after || "").split("#")[0].trim();
@@ -1823,8 +1891,8 @@ var diffDirective = ({ props, slots }) => {
1823
1891
  const urls = [];
1824
1892
  const raw = slots.default || "";
1825
1893
  let m;
1826
- IMG_RE2.lastIndex = 0;
1827
- while ((m = IMG_RE2.exec(raw)) !== null) {
1894
+ IMG_RE.lastIndex = 0;
1895
+ while ((m = IMG_RE.exec(raw)) !== null) {
1828
1896
  urls.push(m[2].split("#")[0].trim());
1829
1897
  }
1830
1898
  if (!before && urls.length > 0) before = urls[0];
@@ -1837,21 +1905,11 @@ var diffDirective = ({ props, slots }) => {
1837
1905
  figure.className = "nr-diff";
1838
1906
  figure.tabIndex = 0;
1839
1907
  figure.setAttribute("aria-label", "Image comparison slider");
1840
- if (props.class) figure.classList.add(...props.class.split(/\s+/).filter(Boolean));
1841
- if (props.style) figure.setAttribute("style", props.style);
1908
+ applyBaseProps(figure, props);
1842
1909
  if (props.aspect) figure.style.aspectRatio = props.aspect;
1843
1910
  if (props.height) figure.style.height = props.height;
1844
1911
  if (props.width) figure.style.width = props.width;
1845
- if (props.float) {
1846
- if (props.float === "left" || props.float === "right") {
1847
- figure.style.float = props.float;
1848
- if (!props.width) figure.style.maxWidth = "50%";
1849
- figure.style.marginInlineStart = props.float === "right" ? "1rem" : "";
1850
- figure.style.marginInlineEnd = props.float === "left" ? "1rem" : "";
1851
- } else if (props.float === "center") {
1852
- figure.style.marginInline = "auto";
1853
- }
1854
- }
1912
+ applyFloatStyle(figure, props.float, props.width);
1855
1913
  const beforeItem = document.createElement("div");
1856
1914
  beforeItem.className = "nr-diff__item nr-diff__item--before";
1857
1915
  beforeItem.setAttribute("role", "img");
@@ -1914,8 +1972,7 @@ var diff_default = diffDirective;
1914
1972
  var hover3dDirective = ({ props, renderSlot }) => {
1915
1973
  const container = document.createElement("div");
1916
1974
  container.className = "nr-hover-3d";
1917
- if (props.class) container.classList.add(...props.class.split(/\s+/).filter(Boolean));
1918
- if (props.style) container.setAttribute("style", props.style);
1975
+ applyBaseProps(container, props);
1919
1976
  const stage = document.createElement("div");
1920
1977
  stage.className = "nr-hover-3d__stage";
1921
1978
  stage.appendChild(renderSlot("default"));
@@ -1928,14 +1985,13 @@ var hover3dDirective = ({ props, renderSlot }) => {
1928
1985
  var hover3d_default = hover3dDirective;
1929
1986
 
1930
1987
  // vanilla/directives/hovergallery.ts
1931
- var IMG_RE3 = /!\[([^\]]*)\]\(([^)]+)\)/g;
1932
1988
  var MAX_IMAGES = 10;
1933
1989
  var hovergalleryDirective = ({ props, slots }) => {
1934
1990
  const images = [];
1935
1991
  const raw = slots.default || "";
1936
1992
  let m;
1937
- IMG_RE3.lastIndex = 0;
1938
- while ((m = IMG_RE3.exec(raw)) !== null) {
1993
+ IMG_RE.lastIndex = 0;
1994
+ while ((m = IMG_RE.exec(raw)) !== null) {
1939
1995
  images.push({ src: m[2].split("#")[0].trim(), alt: m[1].trim() || "gallery image" });
1940
1996
  }
1941
1997
  if (images.length === 0) {
@@ -1944,9 +2000,8 @@ var hovergalleryDirective = ({ props, slots }) => {
1944
2000
  const count = Math.min(images.length, MAX_IMAGES);
1945
2001
  const figure = document.createElement("figure");
1946
2002
  figure.className = "nr-hover-gallery";
2003
+ applyBaseProps(figure, props);
1947
2004
  if (props.aspect) figure.style.aspectRatio = props.aspect;
1948
- if (props.class) figure.classList.add(...props.class.split(/\s+/).filter(Boolean));
1949
- if (props.style) figure.setAttribute("style", (figure.getAttribute("style") || "") + ";" + props.style);
1950
2005
  const imgEls = [];
1951
2006
  for (let i = 0; i < count; i++) {
1952
2007
  const el = document.createElement("img");
@@ -1996,28 +2051,11 @@ var hovergalleryDirective = ({ props, slots }) => {
1996
2051
  var hovergallery_default = hovergalleryDirective;
1997
2052
 
1998
2053
  // vanilla/directives/chat.ts
1999
- var CHAT_THEME_TOKENS = /* @__PURE__ */ new Set([
2000
- "primary",
2001
- "secondary",
2002
- "accent",
2003
- "neutral",
2004
- "info",
2005
- "success",
2006
- "warning",
2007
- "error"
2008
- ]);
2009
- function isArbitraryColor(value) {
2010
- if (CHAT_THEME_TOKENS.has(value)) return false;
2011
- if (/^(#|rgb|hsl|oklch|oklab|lab|lch|color\()/i.test(value)) return true;
2012
- if (/^[a-zA-Z]+$/.test(value)) return true;
2013
- return false;
2014
- }
2015
2054
  var chatItemDirective = ({ props, renderSlot }) => {
2016
2055
  const side = props.side === "end" ? "end" : "start";
2017
2056
  const wrap = document.createElement("div");
2018
2057
  wrap.className = `nr-chat nr-chat--${side}`;
2019
- if (props.class) wrap.classList.add(...props.class.split(/\s+/).filter(Boolean));
2020
- if (props.style) wrap.setAttribute("style", props.style);
2058
+ applyBaseProps(wrap, props);
2021
2059
  const header = document.createElement("div");
2022
2060
  header.className = "nr-chat__header";
2023
2061
  if (props.name) {
@@ -2042,14 +2080,8 @@ var chatItemDirective = ({ props, renderSlot }) => {
2042
2080
  avatar.appendChild(img);
2043
2081
  wrap.appendChild(avatar);
2044
2082
  }
2045
- const isThemeToken = CHAT_THEME_TOKENS.has(props.color || "");
2046
- const colorClass = isThemeToken ? ` nr-chat__bubble--${props.color}` : "";
2047
2083
  const bubble = document.createElement("div");
2048
- bubble.className = `nr-chat__bubble${colorClass}`;
2049
- if (props.color && isArbitraryColor(props.color) && !isThemeToken) {
2050
- bubble.style.background = props.color;
2051
- bubble.style.color = "white";
2052
- }
2084
+ bubble.className = `nr-chat__bubble${applyColor(bubble, props.color, "nr-chat__bubble")}`;
2053
2085
  bubble.appendChild(renderSlot("default"));
2054
2086
  wrap.appendChild(bubble);
2055
2087
  if (props.footer) {
@@ -2063,8 +2095,7 @@ var chatItemDirective = ({ props, renderSlot }) => {
2063
2095
  var chatDirective = ({ props, renderSlot }) => {
2064
2096
  const wrap = document.createElement("div");
2065
2097
  wrap.className = "nr-chat";
2066
- if (props.class) wrap.classList.add(...props.class.split(/\s+/).filter(Boolean));
2067
- if (props.style) wrap.setAttribute("style", props.style);
2098
+ applyBaseProps(wrap, props);
2068
2099
  wrap.appendChild(renderSlot("default"));
2069
2100
  return wrap;
2070
2101
  };
@@ -2100,8 +2131,7 @@ function bindEventProp(el, eventProp) {
2100
2131
  var richlistItemDirective = ({ props, renderSlot }) => {
2101
2132
  const li = document.createElement("li");
2102
2133
  li.className = "nr-richlist__item";
2103
- if (props.class) li.classList.add(...props.class.split(/\s+/).filter(Boolean));
2104
- if (props.style) li.setAttribute("style", props.style);
2134
+ applyBaseProps(li, props);
2105
2135
  if (props.image) {
2106
2136
  const thumb = document.createElement("div");
2107
2137
  thumb.className = "nr-richlist__thumb";
@@ -2163,36 +2193,20 @@ var richlistItemDirective = ({ props, renderSlot }) => {
2163
2193
  var richlistDirective = ({ props, renderSlot }) => {
2164
2194
  const ul = document.createElement("ul");
2165
2195
  ul.className = "nr-richlist";
2166
- if (props.class) ul.classList.add(...props.class.split(/\s+/).filter(Boolean));
2167
- if (props.style) ul.setAttribute("style", props.style);
2196
+ applyBaseProps(ul, props);
2168
2197
  ul.appendChild(renderSlot("default"));
2169
2198
  return ul;
2170
2199
  };
2171
2200
  var richlist_default = richlistDirective;
2172
2201
 
2173
2202
  // vanilla/directives/stat.ts
2174
- var STAT_THEME_TOKENS = /* @__PURE__ */ new Set([
2175
- "primary",
2176
- "secondary",
2177
- "info",
2178
- "success",
2179
- "warning",
2180
- "error"
2181
- ]);
2182
- function isArbitraryColor2(value) {
2183
- if (STAT_THEME_TOKENS.has(value)) return false;
2184
- if (/^(#|rgb|hsl|oklch|oklab|lab|lch|color\()/i.test(value)) return true;
2185
- if (/^[a-zA-Z]+$/.test(value)) return true;
2186
- return false;
2187
- }
2188
2203
  var statDirective = ({ props }) => {
2189
- const isThemeToken = STAT_THEME_TOKENS.has(props.color || "");
2190
- const colorClass = isThemeToken ? ` nr-stat--${props.color}` : "";
2204
+ const statIsThemeToken = isThemeToken(props.color);
2205
+ const colorClass = statIsThemeToken ? ` nr-stat--${props.color}` : "";
2191
2206
  const stat = document.createElement("div");
2192
2207
  stat.className = `nr-stat${colorClass}`;
2193
- if (props.class) stat.classList.add(...props.class.split(/\s+/).filter(Boolean));
2194
- if (props.style) stat.setAttribute("style", props.style);
2195
- const useInlineColor = props.color && isArbitraryColor2(props.color) && !isThemeToken;
2208
+ applyBaseProps(stat, props);
2209
+ const useInlineColor = props.color && isArbitraryColor(props.color) && !statIsThemeToken;
2196
2210
  if (props.icon) {
2197
2211
  const figure = document.createElement("div");
2198
2212
  figure.className = "nr-stat__figure";
@@ -2285,9 +2299,12 @@ function renderHtmlString(html) {
2285
2299
  processedContent = processedContent.replace(
2286
2300
  /<style(?:\s+[^>]*)?>([\s\S]*?)<\/style>/gi,
2287
2301
  (_match, cssContent) => {
2302
+ const trimmed = cssContent.trim();
2303
+ const existing = document.head.querySelector("style[data-nr-global]");
2304
+ if (existing && existing.textContent === trimmed) return "";
2288
2305
  const styleEl = document.createElement("style");
2289
2306
  styleEl.setAttribute("data-nr-global", "");
2290
- styleEl.textContent = cssContent;
2307
+ styleEl.textContent = trimmed;
2291
2308
  document.head.appendChild(styleEl);
2292
2309
  return "";
2293
2310
  }
@@ -2357,19 +2374,14 @@ function renderElement(element, ctx, allElements) {
2357
2374
  switch (element.type) {
2358
2375
  case "header": {
2359
2376
  const tag = `h${element.level}`;
2360
- let text = element.text;
2361
- const alignCenter = text.match(/^->\s*(.+?)\s*<-\s*$/);
2362
- const alignRight = text.match(/^->\s*(.+?)\s*->\s*$/);
2363
- if (alignCenter) text = alignCenter[1];
2364
- else if (alignRight) text = alignRight[1];
2365
2377
  const h = document.createElement(tag);
2366
2378
  h.id = element.id;
2367
2379
  let cls = `md-h${element.level}`;
2368
- if (alignCenter) cls += " text-center";
2369
- if (alignRight) cls += " text-right";
2380
+ if (element.align === "center") cls += " text-center";
2381
+ else if (element.align === "right") cls += " text-right";
2370
2382
  if (element.classes) cls += ` ${element.classes}`;
2371
2383
  h.className = cls;
2372
- h.appendChild(renderInline(text));
2384
+ h.appendChild(renderInline(element.text));
2373
2385
  return h;
2374
2386
  }
2375
2387
  case "paragraph": {
@@ -2521,7 +2533,7 @@ var guideData = [
2521
2533
  "title": "Introducci\xF3n",
2522
2534
  "icon": "menu_book",
2523
2535
  "order": 1,
2524
- "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.'
2536
+ "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.'
2525
2537
  },
2526
2538
  {
2527
2539
  "id": "titulos",
@@ -2737,7 +2749,7 @@ var guideData = [
2737
2749
  "title": "Modal",
2738
2750
  "icon": "open_in_full",
2739
2751
  "order": 2,
2740
- "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.'
2752
+ "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.'
2741
2753
  },
2742
2754
  {
2743
2755
  "id": "button",
@@ -2745,7 +2757,7 @@ var guideData = [
2745
2757
  "title": "Button",
2746
2758
  "icon": "touch_app",
2747
2759
  "order": 3,
2748
- "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 |'
2760
+ "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"`) |'
2749
2761
  },
2750
2762
  {
2751
2763
  "id": "slide",
@@ -2762,6 +2774,14 @@ var guideData = [
2762
2774
  "icon": "code_off",
2763
2775
  "order": 1,
2764
2776
  "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.'
2777
+ },
2778
+ {
2779
+ "id": "wrapper-directives",
2780
+ "category": "Layout",
2781
+ "title": "Wrapper Directives",
2782
+ "icon": "crop_free",
2783
+ "order": 1,
2784
+ "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```'
2765
2785
  }
2766
2786
  ];
2767
2787