@asteroidcms/core-utils 0.1.4 → 0.1.5

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/README.md CHANGED
@@ -356,19 +356,24 @@ getContentReadTime(article.body, {
356
356
 
357
357
  ## `<RichTextContent>`
358
358
 
359
- Render Asteroid CMS rich-text HTML with syntax-highlighted code blocks (via `highlight.js`), self-healing enhancements (copy buttons, blockquote decorations), and slugified heading IDs out of the box.
359
+ Render Asteroid CMS rich-text HTML with syntax-highlighted code blocks (via `highlight.js`), self-healing enhancements (copy buttons, blockquote decorations, callout chips), terminal/diff code-block variants, and slugified heading IDs out of the box.
360
360
 
361
361
  ```tsx
362
362
  import { RichTextContent } from "@asteroidcms/core-utils/client";
363
+ import { Info } from "lucide-react";
363
364
 
364
365
  <RichTextContent
365
366
  html={article.body}
366
367
  as="article"
367
368
  className="prose"
368
369
  classMap={{ p: "my-2 leading-relaxed", h2: "text-2xl font-bold" }}
370
+ onReady={(root) => console.log("hydrated", root.querySelectorAll("h2").length)}
371
+ calloutIcons={{ info: <Info size={14} strokeWidth={2.4} /> }}
369
372
  />;
370
373
  ```
371
374
 
375
+ Additional props: `contentRef` (forwards the wrapper element, useful for `useTableOfContents`), `onReady` (fires once enhancements have run), and `calloutIcons` (per-variant icon override for `<aside data-callout data-icon>` blocks).
376
+
372
377
  Or use the parser directly (server-safe, no `highlight.js`):
373
378
 
374
379
  ```ts
package/dist/client.cjs CHANGED
@@ -7,8 +7,8 @@ var client = require('@apollo/client');
7
7
  var context = require('@apollo/client/link/context');
8
8
  var error = require('@apollo/client/link/error');
9
9
  var jsxRuntime = require('react/jsx-runtime');
10
+ var reactDom = require('react-dom');
10
11
  var hljs = require('highlight.js/lib/common');
11
- require('highlight.js/styles/tokyo-night-dark.css');
12
12
 
13
13
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
14
14
 
@@ -371,7 +371,9 @@ var DEFAULT_ALLOWLIST = [
371
371
  "section",
372
372
  "article",
373
373
  "div",
374
- "span"
374
+ "span",
375
+ "details",
376
+ "summary"
375
377
  ];
376
378
  var ALLOWED_ATTRS = {
377
379
  a: ["href", "title", "target", "rel"],
@@ -381,7 +383,8 @@ var ALLOWED_ATTRS = {
381
383
  col: ["span", "width"],
382
384
  colgroup: ["span"],
383
385
  table: ["border", "cellpadding", "cellspacing"],
384
- span: ["style"]
386
+ span: ["style"],
387
+ details: ["open"]
385
388
  };
386
389
  var ALLOWED_STYLE_PROPS = {
387
390
  span: ["font-size"]
@@ -415,7 +418,8 @@ var GLOBAL_ALLOWED_ATTRS = [
415
418
  "data-title",
416
419
  "data-callout-title",
417
420
  "data-language",
418
- "data-filename"
421
+ "data-filename",
422
+ "data-icon"
419
423
  ];
420
424
  var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src"]);
421
425
  function parseRichText(html, options = {}) {
@@ -829,6 +833,8 @@ function classKeyForTag(tag, attrs, openStack) {
829
833
  if (tag === "p" && "data-callout-title" in attrs) return "calloutTitle";
830
834
  if (tag === "code" && openStack[openStack.length - 1] !== "pre")
831
835
  return "inlineCode";
836
+ if (tag === "details") return "collapsible";
837
+ if (tag === "summary") return "collapsibleTitle";
832
838
  const known = [
833
839
  "p",
834
840
  "br",
@@ -995,8 +1001,6 @@ function sanitizeAndStyle(html, options) {
995
1001
  }
996
1002
  return out.join("");
997
1003
  }
998
-
999
- // src/components/RichTextContent.tsx
1000
1004
  var DEFAULT_CLASS_MAP = {
1001
1005
  variants: {
1002
1006
  "figure:pullquote": "relative my-8",
@@ -1031,6 +1035,46 @@ function findQuoteBody(bq) {
1031
1035
  if (lastIdx < 0) return { first: null, last: null };
1032
1036
  return { first: children[0], last: children[lastIdx] };
1033
1037
  }
1038
+ var CALLOUT_ICON_SVG = {
1039
+ info: `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 16v-5"/><path d="M12 8h.01"/></svg>`,
1040
+ warning: `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>`,
1041
+ success: `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>`,
1042
+ danger: `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`,
1043
+ default: `<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M12 2.5l1.6 5.4a4 4 0 0 0 2.5 2.5L21.5 12l-5.4 1.6a4 4 0 0 0-2.5 2.5L12 21.5l-1.6-5.4a4 4 0 0 0-2.5-2.5L2.5 12l5.4-1.6a4 4 0 0 0 2.5-2.5z"/></svg>`
1044
+ };
1045
+ function calloutVariantOf(el) {
1046
+ return el.getAttribute("data-variant") ?? "default";
1047
+ }
1048
+ function isIconEnabled(el) {
1049
+ const v = el.getAttribute("data-icon");
1050
+ if (v === null) return false;
1051
+ return v !== "false" && v !== "0";
1052
+ }
1053
+ function enhanceCallouts(root) {
1054
+ const callouts = root.querySelectorAll("aside[data-callout]");
1055
+ callouts.forEach((el) => {
1056
+ if (el.dataset.rtCalloutEnhanced === "1") return;
1057
+ el.dataset.rtCalloutEnhanced = "1";
1058
+ if (!isIconEnabled(el)) return;
1059
+ if (el.querySelector(":scope > .rt-callout-icon")) return;
1060
+ const variant = calloutVariantOf(el);
1061
+ const icon = document.createElement("span");
1062
+ icon.className = "rt-callout-icon";
1063
+ icon.dataset.variant = variant;
1064
+ icon.setAttribute("aria-hidden", "true");
1065
+ el.prepend(icon);
1066
+ });
1067
+ const chips = [];
1068
+ root.querySelectorAll(
1069
+ "aside[data-callout] > .rt-callout-icon"
1070
+ ).forEach((chip) => {
1071
+ chips.push({
1072
+ el: chip,
1073
+ variant: chip.dataset.variant || (chip.parentElement?.getAttribute("data-variant") ?? "default")
1074
+ });
1075
+ });
1076
+ return chips;
1077
+ }
1034
1078
  function enhanceBlockquotes(root) {
1035
1079
  const quotes = root.querySelectorAll("blockquote");
1036
1080
  quotes.forEach((bq) => {
@@ -1260,6 +1304,28 @@ function enhanceCodeBlocks(root) {
1260
1304
  highlightCodeBlock(pre);
1261
1305
  });
1262
1306
  }
1307
+ var HLJS_THEME = `
1308
+ pre code.hljs { display: block; overflow-x: auto; padding: 1em; }
1309
+ code.hljs { padding: 3px 5px; }
1310
+ .hljs-meta, .hljs-comment { color: #565f89; }
1311
+ .hljs-tag, .hljs-doctag, .hljs-selector-id, .hljs-selector-class,
1312
+ .hljs-regexp, .hljs-template-tag, .hljs-selector-pseudo,
1313
+ .hljs-selector-attr, .hljs-variable.language_, .hljs-deletion { color: #f7768e; }
1314
+ .hljs-variable, .hljs-template-variable, .hljs-number, .hljs-literal,
1315
+ .hljs-type, .hljs-params, .hljs-link { color: #ff9e64; }
1316
+ .hljs-built_in, .hljs-attribute { color: #e0af68; }
1317
+ .hljs-selector-tag { color: #73daca; }
1318
+ .hljs-keyword, .hljs-title.function_, .hljs-title, .hljs-title.class_,
1319
+ .hljs-title.class_.inherited__, .hljs-subst, .hljs-property { color: #7dcfff; }
1320
+ .hljs-quote, .hljs-string, .hljs-symbol, .hljs-bullet,
1321
+ .hljs-addition { color: #9ece6a; }
1322
+ .hljs-code, .hljs-formula, .hljs-section { color: #7aa2f7; }
1323
+ .hljs-name, .hljs-operator, .hljs-char.escape_, .hljs-attr { color: #bb9af7; }
1324
+ .hljs-punctuation { color: #c0caf5; }
1325
+ .hljs { background: #1a1b26; color: #9aa5ce; }
1326
+ .hljs-emphasis { font-style: italic; }
1327
+ .hljs-strong { font-weight: bold; }
1328
+ `;
1263
1329
  var CODEBLOCK_STYLE = `
1264
1330
  .rt-codeblock { position: relative; }
1265
1331
  .rt-codeblock[data-filename]:not([data-filename=""]) { padding-top: 2rem; }
@@ -1311,6 +1377,64 @@ var CODEBLOCK_STYLE = `
1311
1377
  }
1312
1378
  .rt-quote-open { margin-right: 0.15em; }
1313
1379
  .rt-quote-close { margin-left: 0.15em; }
1380
+
1381
+ /* Callout chip ----------------------------------------------------------- */
1382
+ /* Layout: when a callout opts into an icon, use a 2-column grid so the chip
1383
+ sits left of the title + body. Consumers can override via classMap. */
1384
+ aside[data-callout][data-icon]:not([data-icon="false"]):not([data-icon="0"]) {
1385
+ display: grid;
1386
+ grid-template-columns: auto 1fr;
1387
+ column-gap: 0.85rem;
1388
+ align-items: start;
1389
+ }
1390
+ aside[data-callout][data-icon]:not([data-icon="false"]):not([data-icon="0"]) > .rt-callout-icon {
1391
+ grid-row: 1 / span 99;
1392
+ margin-top: 0.05rem;
1393
+ }
1394
+ .rt-callout-icon {
1395
+ display: inline-flex;
1396
+ align-items: center;
1397
+ justify-content: center;
1398
+ width: 1.6rem;
1399
+ height: 1.6rem;
1400
+ border-radius: 0.4rem;
1401
+ color: #fff;
1402
+ background: #475569;
1403
+ flex-shrink: 0;
1404
+ }
1405
+ .rt-callout-icon[data-variant="info"] { background: #2563eb; }
1406
+ .rt-callout-icon[data-variant="warning"] { background: #d97706; }
1407
+ .rt-callout-icon[data-variant="success"] { background: #16a34a; }
1408
+ .rt-callout-icon[data-variant="danger"] { background: #dc2626; }
1409
+ .rt-callout-icon[data-variant="default"] { background: #475569; }
1410
+
1411
+ /* Collapsible (FAQ accordion) ------------------------------------------ */
1412
+ /* Native <details>/<summary> with a rotating chevron. Only structural
1413
+ styling lives here \u2014 colors, padding, and typography belong to
1414
+ consumers via the \`collapsible\` and \`collapsibleTitle\` classMap keys. */
1415
+ details[data-collapsible] > summary {
1416
+ cursor: pointer;
1417
+ list-style: none;
1418
+ display: flex;
1419
+ align-items: center;
1420
+ justify-content: space-between;
1421
+ gap: 0.75rem;
1422
+ }
1423
+ details[data-collapsible] > summary::-webkit-details-marker { display: none; }
1424
+ details[data-collapsible] > summary::after {
1425
+ content: "";
1426
+ width: 0.55rem;
1427
+ height: 0.55rem;
1428
+ border-right: 1.5px solid currentColor;
1429
+ border-bottom: 1.5px solid currentColor;
1430
+ transform: rotate(-45deg) translate(-0.1rem, 0.05rem);
1431
+ transition: transform 0.18s ease;
1432
+ opacity: 0.55;
1433
+ flex-shrink: 0;
1434
+ }
1435
+ details[data-collapsible][open] > summary::after {
1436
+ transform: rotate(45deg) translate(-0.05rem, -0.05rem);
1437
+ }
1314
1438
  /* highlight.js theme handles .hljs-* color classes; we only override the
1315
1439
  default .hljs background so the per-block chrome (dark bg, terminal,
1316
1440
  diff red/green rows) wins. */
@@ -1452,17 +1576,29 @@ function ensureCodeBlockStyles() {
1452
1576
  }
1453
1577
  const tag = document.createElement("style");
1454
1578
  tag.id = "rt-codeblock-style";
1455
- tag.textContent = CODEBLOCK_STYLE;
1579
+ tag.textContent = HLJS_THEME + "\n" + CODEBLOCK_STYLE;
1456
1580
  document.head.appendChild(tag);
1457
1581
  styleInjected = true;
1458
1582
  }
1583
+ function calloutChipsEqual(a, b) {
1584
+ if (a.length !== b.length) return false;
1585
+ for (let i = 0; i < a.length; i++) {
1586
+ if (a[i].el !== b[i].el || a[i].variant !== b[i].variant) return false;
1587
+ }
1588
+ return true;
1589
+ }
1590
+ function BuiltinCalloutIcon({ variant }) {
1591
+ const html = CALLOUT_ICON_SVG[variant] ?? CALLOUT_ICON_SVG.default;
1592
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { dangerouslySetInnerHTML: { __html: html } });
1593
+ }
1459
1594
  function RichTextContent({
1460
1595
  html,
1461
1596
  classMap,
1462
1597
  as = "div",
1463
1598
  className,
1464
1599
  onReady,
1465
- contentRef
1600
+ contentRef,
1601
+ calloutIcons
1466
1602
  }) {
1467
1603
  const merged = react.useMemo(
1468
1604
  () => mergeClassMap(DEFAULT_CLASS_MAP, classMap),
@@ -1473,6 +1609,7 @@ function RichTextContent({
1473
1609
  [html, merged]
1474
1610
  );
1475
1611
  const ref = react.useRef(null);
1612
+ const [chips, setChips] = react.useState([]);
1476
1613
  react.useEffect(() => {
1477
1614
  ensureCodeBlockStyles();
1478
1615
  const root = ref.current;
@@ -1482,6 +1619,8 @@ function RichTextContent({
1482
1619
  mo.disconnect();
1483
1620
  enhanceCodeBlocks(root);
1484
1621
  enhanceBlockquotes(root);
1622
+ const nextChips = enhanceCallouts(root);
1623
+ setChips((prev) => calloutChipsEqual(prev, nextChips) ? prev : nextChips);
1485
1624
  onReady?.(root);
1486
1625
  mo.observe(root, { childList: true, subtree: true });
1487
1626
  };
@@ -1499,11 +1638,20 @@ function RichTextContent({
1499
1638
  if (raf) cancelAnimationFrame(raf);
1500
1639
  };
1501
1640
  }, [safe, onReady, contentRef]);
1502
- return react.createElement(as, {
1503
- ref,
1504
- className,
1505
- dangerouslySetInnerHTML: { __html: safe }
1506
- });
1641
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
1642
+ react.createElement(as, {
1643
+ ref,
1644
+ className,
1645
+ dangerouslySetInnerHTML: { __html: safe }
1646
+ }),
1647
+ chips.map(
1648
+ (chip, i) => reactDom.createPortal(
1649
+ calloutIcons && chip.variant in calloutIcons ? calloutIcons[chip.variant] : /* @__PURE__ */ jsxRuntime.jsx(BuiltinCalloutIcon, { variant: chip.variant }),
1650
+ chip.el,
1651
+ `${chip.variant}:${i}`
1652
+ )
1653
+ )
1654
+ ] });
1507
1655
  }
1508
1656
 
1509
1657
  // src/utils/extractHeadings.ts