@asteroidcms/core-utils 0.1.4 → 0.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/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  - **Provider-driven** - configure `cmsUrl`, `apiKey`, and Apollo behavior in one place
12
12
  - **API-key auth only** - sends `x-api-key` on every request, nothing else
13
13
  - **Typed hooks** - `useCmsContent` / `useCmsMutate` build GraphQL on the fly from a declarative selection
14
+ - **Server helpers** - `fetchCmsContent` / `cmsMutate` for Next.js Server Components, Route Handlers, and scripts
14
15
  - **Tree-shakeable** - ESM + CJS + types, `@apollo/client`/`react` as peer deps
15
16
 
16
17
  ---
@@ -34,7 +35,7 @@ npm install @apollo/client-integration-nextjs # for nextjs (optional)
34
35
  Wrap your app once:
35
36
 
36
37
  ```tsx
37
- import { AsteroidCMSProvider } from "@asteroidcms/core-utils";
38
+ import { AsteroidCMSProvider } from "@asteroidcms/core-utils/client";
38
39
 
39
40
  export function Root() {
40
41
  return (
@@ -51,7 +52,7 @@ export function Root() {
51
52
  Then use the hooks anywhere:
52
53
 
53
54
  ```tsx
54
- import { useCmsContent, useCmsImage } from "@asteroidcms/core-utils";
55
+ import { useCmsContent, useCmsImage } from "@asteroidcms/core-utils/client";
55
56
 
56
57
  function NewsList() {
57
58
  const cmsImage = useCmsImage();
@@ -197,9 +198,7 @@ const { data: topStories } = useCmsContent({
197
198
 
198
199
  ## `fetchCmsContent` (Next.js / RSC)
199
200
 
200
- Server-side counterpart to `useCmsContent`. Use it in Next.js Server Components, route handlers, or any other server context. Accepts a server-side Apollo client plus the same options object as `useCmsContent`, and returns the resolved data directly.
201
-
202
- Pass a `getClient` function that returns a server-side Apollo client. The shape matches what `registerApolloClient` from `@apollo/client-integration-nextjs` already returns, so you can hand it through directly.
201
+ Server-side counterpart to `useCmsContent`. Use it in Next.js Server Components, Route Handlers, or any other server context. Accepts a `getClient` function plus the same options object as `useCmsContent`, and returns the resolved data directly.
203
202
 
204
203
  ```ts
205
204
  // app/lib/cms-server.ts
@@ -242,28 +241,6 @@ const articles = await fetchCmsContent<Article[]>(getClient, {
242
241
 
243
242
  Outside Next.js you can pass any `() => ApolloClient` - e.g. `() => createApolloClient({ cmsUrl, apiKey })`.
244
243
 
245
- Add `import "server-only"` in the file that calls it if you want Next.js to fail the build when it leaks into a client component.
246
-
247
- ---
248
-
249
- ## `buildCmsQuery`
250
-
251
- Lower-level helper that turns a declarative selection into a GraphQL `DocumentNode` plus variables. Used internally by `useCmsContent` and `fetchCmsContent`; exported so you can drive your own Apollo calls (cache reads, prefetching, batching, etc.).
252
-
253
- ```ts
254
- import { buildCmsQuery } from "@asteroidcms/core-utils";
255
-
256
- const { query, variables, isSingle } = buildCmsQuery({
257
- schema_slug: "news",
258
- limit: 10,
259
- status: "PUBLISHED",
260
- select: ["title", "slug"],
261
- });
262
-
263
- const { data } = await apolloClient.query({ query, variables });
264
- const entries = isSingle ? data.entry : data.entries;
265
- ```
266
-
267
244
  ---
268
245
 
269
246
  ## `useCmsMutate`
@@ -310,6 +287,69 @@ removeComment();
310
287
 
311
288
  ---
312
289
 
290
+ ## `cmsMutate` (Next.js / RSC)
291
+
292
+ Server-side counterpart to `useCmsMutate`. Use it in Route Handlers, webhooks, cron jobs, or build scripts.
293
+
294
+ ```ts
295
+ import { cmsMutate } from "@asteroidcms/core-utils";
296
+ import { getClient } from "@/app/lib/cms-server";
297
+
298
+ // Create
299
+ const entry = await cmsMutate<{ id: string }>(getClient, {
300
+ schema_slug: "newsletter_subscribers",
301
+ mutationType: "create",
302
+ variables: { data: { email: "user@example.com", name: "Alice" } },
303
+ });
304
+
305
+ // Update
306
+ await cmsMutate(getClient, {
307
+ schema_slug: "news",
308
+ mutationType: "update",
309
+ entryId: "abc123",
310
+ variables: { data: { title: "Updated title" } },
311
+ });
312
+
313
+ // Delete
314
+ await cmsMutate(getClient, {
315
+ schema_slug: "comments",
316
+ mutationType: "delete",
317
+ entryId: "xyz789",
318
+ });
319
+ ```
320
+
321
+ ---
322
+
323
+ ## `buildCmsQuery` / `buildCmsMutation`
324
+
325
+ Lower-level helpers that turn a declarative selection into GraphQL `DocumentNode` plus variables. Used internally by the hooks and server helpers; exported so you can drive your own Apollo calls.
326
+
327
+ ```ts
328
+ import { buildCmsQuery, buildCmsMutation } from "@asteroidcms/core-utils";
329
+
330
+ // Query
331
+ const { query, variables, isSingle } = buildCmsQuery({
332
+ schema_slug: "news",
333
+ limit: 10,
334
+ status: "PUBLISHED",
335
+ select: ["title", "slug"],
336
+ });
337
+
338
+ const { data } = await apolloClient.query({ query, variables });
339
+ const entries = isSingle ? data.entry : data.entries;
340
+
341
+ // Mutation
342
+ const { mutation, variables: mutVars } = buildCmsMutation({
343
+ schema_slug: "news",
344
+ mutationType: "create",
345
+ variables: { data: { title: "Hello" } },
346
+ });
347
+
348
+ const { data: mutData } = await apolloClient.mutate({ mutation, variables: mutVars });
349
+ ```
350
+
351
+ ---
352
+
313
353
  ## `cmsImage` / `useCmsImage`
314
354
 
315
355
  Build a canonical media URL for an asset id.
@@ -356,19 +396,24 @@ getContentReadTime(article.body, {
356
396
 
357
397
  ## `<RichTextContent>`
358
398
 
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.
399
+ 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
400
 
361
401
  ```tsx
362
402
  import { RichTextContent } from "@asteroidcms/core-utils/client";
403
+ import { Info } from "lucide-react";
363
404
 
364
405
  <RichTextContent
365
406
  html={article.body}
366
407
  as="article"
367
408
  className="prose"
368
409
  classMap={{ p: "my-2 leading-relaxed", h2: "text-2xl font-bold" }}
410
+ onReady={(root) => console.log("hydrated", root.querySelectorAll("h2").length)}
411
+ calloutIcons={{ info: <Info size={14} strokeWidth={2.4} /> }}
369
412
  />;
370
413
  ```
371
414
 
415
+ Additional props: `contentRef` (forwards the wrapper element, useful for scroll observers or ToC hooks), `onReady` (fires once enhancements have run), and `calloutIcons` (per-variant icon override for `<aside data-callout data-icon>` blocks).
416
+
372
417
  Or use the parser directly (server-safe, no `highlight.js`):
373
418
 
374
419
  ```ts
@@ -383,56 +428,24 @@ const html = parseRichText(article.body, {
383
428
 
384
429
  ### Table of contents
385
430
 
386
- Build a live, scroll-tracked ToC from rendered content with `useTableOfContents`. Pair it with `RichTextContent`'s `contentRef` prop:
431
+ Build a static ToC from HTML with `extractHeadingsFromHtml`, or extract headings from live DOM with `extractHeadingsFromElement`:
387
432
 
388
- ```tsx
389
- import { useRef } from "react";
390
- import {
391
- RichTextContent,
392
- useTableOfContents,
393
- } from "@asteroidcms/core-utils/client";
394
-
395
- function Article({ slug, html }: { slug: string; html: string }) {
396
- const contentRef = useRef<HTMLElement | null>(null);
397
- const { items, activeId } = useTableOfContents(contentRef, {
398
- levels: [2, 3],
399
- contentKey: slug, // re-collect when content swaps
400
- activationOffset: 96, // px from viewport top
401
- });
433
+ ```ts
434
+ import { extractHeadingsFromHtml } from "@asteroidcms/core-utils";
402
435
 
403
- return (
404
- <div className="flex gap-8">
405
- <RichTextContent
406
- html={html}
407
- as="article"
408
- className="prose"
409
- contentRef={contentRef}
410
- />
411
- <nav>
412
- {items.map((it) => (
413
- <a
414
- key={it.id}
415
- href={`#${it.id}`}
416
- className={it.id === activeId ? "font-semibold" : ""}
417
- >
418
- {it.text}
419
- </a>
420
- ))}
421
- </nav>
422
- </div>
423
- );
424
- }
436
+ const toc = extractHeadingsFromHtml(article.body, { levels: [2, 3] });
437
+ // → [{ id: "intro", text: "Intro", level: 2 }, ...]
425
438
  ```
426
439
 
427
- For a static, server-side outline (RSC layouts, sitemaps, RSS), use `extractHeadingsFromHtml`:
428
-
429
440
  ```ts
430
- import { extractHeadingsFromHtml } from "@asteroidcms/core-utils";
441
+ import { extractHeadingsFromElement } from "@asteroidcms/core-utils/client";
431
442
 
432
- const toc = extractHeadingsFromHtml(article.body, { levels: [2, 3] });
443
+ const toc = extractHeadingsFromElement(contentRef.current, { levels: [2, 3] });
433
444
  ```
434
445
 
435
- See the [full rich-text docs](./docs/web-sdk-react/10-rich-text.md) for `classMap` variants, parser options, and `useTableOfContents` tuning.
446
+ Pair with `RichTextContent`'s `contentRef` prop and a scroll listener to build live active-heading tracking.
447
+
448
+ See the [full rich-text docs](./docs/web-sdk-react/10-rich-text.md) for `classMap` variants, parser options, and code block features.
436
449
 
437
450
  ---
438
451
 
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
@@ -1579,97 +1727,6 @@ function extractHeadingsFromElement(root, options = {}) {
1579
1727
  return out;
1580
1728
  }
1581
1729
 
1582
- // src/hooks/useTableOfContents.tsx
1583
- function useTableOfContents(ref, options = {}) {
1584
- const {
1585
- levels,
1586
- contentKey = null,
1587
- scrollMarginTop = 24,
1588
- activationOffset = 96
1589
- } = options;
1590
- const [items, setItems] = react.useState([]);
1591
- const [activeId, setActiveId] = react.useState("");
1592
- react.useEffect(() => {
1593
- const root = ref.current;
1594
- if (!root) {
1595
- setItems([]);
1596
- setActiveId("");
1597
- return;
1598
- }
1599
- let raf = 0;
1600
- const collect = () => {
1601
- const next = extractHeadingsFromElement(root, {
1602
- levels,
1603
- scrollMarginTop
1604
- });
1605
- setItems(next);
1606
- setActiveId(
1607
- (prev) => next.some((h) => h.id === prev) ? prev : next[0]?.id ?? ""
1608
- );
1609
- };
1610
- raf = requestAnimationFrame(collect);
1611
- const mo = new MutationObserver(() => {
1612
- if (raf) cancelAnimationFrame(raf);
1613
- raf = requestAnimationFrame(collect);
1614
- });
1615
- mo.observe(root, { childList: true, subtree: true, characterData: true });
1616
- return () => {
1617
- mo.disconnect();
1618
- if (raf) cancelAnimationFrame(raf);
1619
- };
1620
- }, [ref, contentKey, levels, scrollMarginTop]);
1621
- react.useEffect(() => {
1622
- if (items.length === 0) return;
1623
- const targets = items.map((it) => document.getElementById(it.id)).filter((el) => el !== null);
1624
- if (targets.length === 0) return;
1625
- let raf = 0;
1626
- const compute = () => {
1627
- raf = 0;
1628
- let activeIdx = 0;
1629
- for (let i = 0; i < items.length; i++) {
1630
- const el = document.getElementById(items[i].id);
1631
- if (!el) continue;
1632
- if (el.getBoundingClientRect().top - activationOffset <= 0) {
1633
- activeIdx = i;
1634
- } else {
1635
- break;
1636
- }
1637
- }
1638
- const scroller = document.scrollingElement || document.documentElement;
1639
- const scrollY = window.scrollY;
1640
- const viewportH = window.innerHeight;
1641
- const atBottom = scrollY + viewportH >= scroller.scrollHeight - 2;
1642
- if (atBottom) {
1643
- for (let i = items.length - 1; i > activeIdx; i--) {
1644
- const el = document.getElementById(items[i].id);
1645
- if (el && el.getBoundingClientRect().top < viewportH) {
1646
- activeIdx = i;
1647
- break;
1648
- }
1649
- }
1650
- }
1651
- setActiveId(items[activeIdx].id);
1652
- };
1653
- const schedule = () => {
1654
- if (raf) return;
1655
- raf = requestAnimationFrame(compute);
1656
- };
1657
- const io = new IntersectionObserver(schedule, {
1658
- rootMargin: `-${activationOffset}px 0px -${Math.max(0, window.innerHeight - activationOffset - 1)}px 0px`,
1659
- threshold: 0
1660
- });
1661
- targets.forEach((t) => io.observe(t));
1662
- window.addEventListener("resize", schedule, { passive: true });
1663
- compute();
1664
- return () => {
1665
- io.disconnect();
1666
- window.removeEventListener("resize", schedule);
1667
- if (raf) cancelAnimationFrame(raf);
1668
- };
1669
- }, [items, activationOffset]);
1670
- return { items, activeId };
1671
- }
1672
-
1673
1730
  exports.AsteroidCMSProvider = AsteroidCMSProvider;
1674
1731
  exports.RichTextContent = RichTextContent;
1675
1732
  exports.extractHeadingsFromElement = extractHeadingsFromElement;
@@ -1679,6 +1736,5 @@ exports.useAsteroidCMSConfig = useAsteroidCMSConfig;
1679
1736
  exports.useCmsContent = useCmsContent;
1680
1737
  exports.useCmsImage = useCmsImage;
1681
1738
  exports.useCmsMutate = useCmsMutate;
1682
- exports.useTableOfContents = useTableOfContents;
1683
1739
  //# sourceMappingURL=client.cjs.map
1684
1740
  //# sourceMappingURL=client.cjs.map