@asteroidcms/core-utils 0.1.3 → 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/dist/client.js CHANGED
@@ -1,12 +1,12 @@
1
1
  "use client";
2
- import { createContext, useContext, useMemo, useRef, useEffect, createElement } from 'react';
2
+ import { createContext, useContext, useMemo, useRef, useState, useEffect, Fragment, createElement } from 'react';
3
3
  import { ApolloProvider, useQuery, useMutation } from '@apollo/client/react';
4
4
  import { gql, HttpLink, ApolloClient, InMemoryCache, ApolloLink, CombinedGraphQLErrors, CombinedProtocolErrors } from '@apollo/client';
5
5
  import { SetContextLink } from '@apollo/client/link/context';
6
6
  import { ErrorLink } from '@apollo/client/link/error';
7
- import { jsx } from 'react/jsx-runtime';
7
+ import { jsx, jsxs } from 'react/jsx-runtime';
8
+ import { createPortal } from 'react-dom';
8
9
  import hljs from 'highlight.js/lib/common';
9
- import 'highlight.js/styles/tokyo-night-dark.css';
10
10
 
11
11
  var AsteroidCMSContext = createContext(null);
12
12
  function useAsteroidCMSConfig() {
@@ -365,7 +365,9 @@ var DEFAULT_ALLOWLIST = [
365
365
  "section",
366
366
  "article",
367
367
  "div",
368
- "span"
368
+ "span",
369
+ "details",
370
+ "summary"
369
371
  ];
370
372
  var ALLOWED_ATTRS = {
371
373
  a: ["href", "title", "target", "rel"],
@@ -375,7 +377,8 @@ var ALLOWED_ATTRS = {
375
377
  col: ["span", "width"],
376
378
  colgroup: ["span"],
377
379
  table: ["border", "cellpadding", "cellspacing"],
378
- span: ["style"]
380
+ span: ["style"],
381
+ details: ["open"]
379
382
  };
380
383
  var ALLOWED_STYLE_PROPS = {
381
384
  span: ["font-size"]
@@ -409,7 +412,8 @@ var GLOBAL_ALLOWED_ATTRS = [
409
412
  "data-title",
410
413
  "data-callout-title",
411
414
  "data-language",
412
- "data-filename"
415
+ "data-filename",
416
+ "data-icon"
413
417
  ];
414
418
  var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src"]);
415
419
  function parseRichText(html, options = {}) {
@@ -419,7 +423,36 @@ function parseRichText(html, options = {}) {
419
423
  working = upgradeStandaloneImages(working);
420
424
  working = upgradeAuthoredBlockquotes(working);
421
425
  working = flattenTableCellParagraphs(working);
422
- return sanitizeAndStyle(working, options);
426
+ working = sanitizeAndStyle(working, options);
427
+ if (options.autoHeadingIds !== false) {
428
+ working = injectHeadingIds(working);
429
+ }
430
+ return working;
431
+ }
432
+ function injectHeadingIds(html) {
433
+ const used = /* @__PURE__ */ new Map();
434
+ const existingRe = /<h[1-6]\b[^>]*\bid\s*=\s*("([^"]*)"|'([^']*)'|(\S+))/gi;
435
+ let em;
436
+ while ((em = existingRe.exec(html)) !== null) {
437
+ const id = em[2] ?? em[3] ?? em[4] ?? "";
438
+ if (id) used.set(id, (used.get(id) ?? 0) + 1);
439
+ }
440
+ return html.replace(
441
+ /<(h[1-6])\b([^>]*)>([\s\S]*?)<\/\1>/gi,
442
+ (full, tag, attrs, inner) => {
443
+ if (/\bid\s*=/i.test(attrs)) return full;
444
+ const text = inner.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\s+/g, " ").trim();
445
+ if (!text) return full;
446
+ const base = slugifyHeading(text) || tag;
447
+ const n = used.get(base) ?? 0;
448
+ used.set(base, n + 1);
449
+ const id = n === 0 ? base : `${base}-${n}`;
450
+ return `<${tag}${attrs} id="${id}">${inner}</${tag}>`;
451
+ }
452
+ );
453
+ }
454
+ function slugifyHeading(text) {
455
+ return text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
423
456
  }
424
457
  function flattenTableCellParagraphs(html) {
425
458
  return html.replace(
@@ -794,6 +827,8 @@ function classKeyForTag(tag, attrs, openStack) {
794
827
  if (tag === "p" && "data-callout-title" in attrs) return "calloutTitle";
795
828
  if (tag === "code" && openStack[openStack.length - 1] !== "pre")
796
829
  return "inlineCode";
830
+ if (tag === "details") return "collapsible";
831
+ if (tag === "summary") return "collapsibleTitle";
797
832
  const known = [
798
833
  "p",
799
834
  "br",
@@ -960,8 +995,6 @@ function sanitizeAndStyle(html, options) {
960
995
  }
961
996
  return out.join("");
962
997
  }
963
-
964
- // src/components/RichTextContent.tsx
965
998
  var DEFAULT_CLASS_MAP = {
966
999
  variants: {
967
1000
  "figure:pullquote": "relative my-8",
@@ -996,6 +1029,46 @@ function findQuoteBody(bq) {
996
1029
  if (lastIdx < 0) return { first: null, last: null };
997
1030
  return { first: children[0], last: children[lastIdx] };
998
1031
  }
1032
+ var CALLOUT_ICON_SVG = {
1033
+ 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>`,
1034
+ 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>`,
1035
+ 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>`,
1036
+ 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>`,
1037
+ 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>`
1038
+ };
1039
+ function calloutVariantOf(el) {
1040
+ return el.getAttribute("data-variant") ?? "default";
1041
+ }
1042
+ function isIconEnabled(el) {
1043
+ const v = el.getAttribute("data-icon");
1044
+ if (v === null) return false;
1045
+ return v !== "false" && v !== "0";
1046
+ }
1047
+ function enhanceCallouts(root) {
1048
+ const callouts = root.querySelectorAll("aside[data-callout]");
1049
+ callouts.forEach((el) => {
1050
+ if (el.dataset.rtCalloutEnhanced === "1") return;
1051
+ el.dataset.rtCalloutEnhanced = "1";
1052
+ if (!isIconEnabled(el)) return;
1053
+ if (el.querySelector(":scope > .rt-callout-icon")) return;
1054
+ const variant = calloutVariantOf(el);
1055
+ const icon = document.createElement("span");
1056
+ icon.className = "rt-callout-icon";
1057
+ icon.dataset.variant = variant;
1058
+ icon.setAttribute("aria-hidden", "true");
1059
+ el.prepend(icon);
1060
+ });
1061
+ const chips = [];
1062
+ root.querySelectorAll(
1063
+ "aside[data-callout] > .rt-callout-icon"
1064
+ ).forEach((chip) => {
1065
+ chips.push({
1066
+ el: chip,
1067
+ variant: chip.dataset.variant || (chip.parentElement?.getAttribute("data-variant") ?? "default")
1068
+ });
1069
+ });
1070
+ return chips;
1071
+ }
999
1072
  function enhanceBlockquotes(root) {
1000
1073
  const quotes = root.querySelectorAll("blockquote");
1001
1074
  quotes.forEach((bq) => {
@@ -1225,6 +1298,28 @@ function enhanceCodeBlocks(root) {
1225
1298
  highlightCodeBlock(pre);
1226
1299
  });
1227
1300
  }
1301
+ var HLJS_THEME = `
1302
+ pre code.hljs { display: block; overflow-x: auto; padding: 1em; }
1303
+ code.hljs { padding: 3px 5px; }
1304
+ .hljs-meta, .hljs-comment { color: #565f89; }
1305
+ .hljs-tag, .hljs-doctag, .hljs-selector-id, .hljs-selector-class,
1306
+ .hljs-regexp, .hljs-template-tag, .hljs-selector-pseudo,
1307
+ .hljs-selector-attr, .hljs-variable.language_, .hljs-deletion { color: #f7768e; }
1308
+ .hljs-variable, .hljs-template-variable, .hljs-number, .hljs-literal,
1309
+ .hljs-type, .hljs-params, .hljs-link { color: #ff9e64; }
1310
+ .hljs-built_in, .hljs-attribute { color: #e0af68; }
1311
+ .hljs-selector-tag { color: #73daca; }
1312
+ .hljs-keyword, .hljs-title.function_, .hljs-title, .hljs-title.class_,
1313
+ .hljs-title.class_.inherited__, .hljs-subst, .hljs-property { color: #7dcfff; }
1314
+ .hljs-quote, .hljs-string, .hljs-symbol, .hljs-bullet,
1315
+ .hljs-addition { color: #9ece6a; }
1316
+ .hljs-code, .hljs-formula, .hljs-section { color: #7aa2f7; }
1317
+ .hljs-name, .hljs-operator, .hljs-char.escape_, .hljs-attr { color: #bb9af7; }
1318
+ .hljs-punctuation { color: #c0caf5; }
1319
+ .hljs { background: #1a1b26; color: #9aa5ce; }
1320
+ .hljs-emphasis { font-style: italic; }
1321
+ .hljs-strong { font-weight: bold; }
1322
+ `;
1228
1323
  var CODEBLOCK_STYLE = `
1229
1324
  .rt-codeblock { position: relative; }
1230
1325
  .rt-codeblock[data-filename]:not([data-filename=""]) { padding-top: 2rem; }
@@ -1276,6 +1371,64 @@ var CODEBLOCK_STYLE = `
1276
1371
  }
1277
1372
  .rt-quote-open { margin-right: 0.15em; }
1278
1373
  .rt-quote-close { margin-left: 0.15em; }
1374
+
1375
+ /* Callout chip ----------------------------------------------------------- */
1376
+ /* Layout: when a callout opts into an icon, use a 2-column grid so the chip
1377
+ sits left of the title + body. Consumers can override via classMap. */
1378
+ aside[data-callout][data-icon]:not([data-icon="false"]):not([data-icon="0"]) {
1379
+ display: grid;
1380
+ grid-template-columns: auto 1fr;
1381
+ column-gap: 0.85rem;
1382
+ align-items: start;
1383
+ }
1384
+ aside[data-callout][data-icon]:not([data-icon="false"]):not([data-icon="0"]) > .rt-callout-icon {
1385
+ grid-row: 1 / span 99;
1386
+ margin-top: 0.05rem;
1387
+ }
1388
+ .rt-callout-icon {
1389
+ display: inline-flex;
1390
+ align-items: center;
1391
+ justify-content: center;
1392
+ width: 1.6rem;
1393
+ height: 1.6rem;
1394
+ border-radius: 0.4rem;
1395
+ color: #fff;
1396
+ background: #475569;
1397
+ flex-shrink: 0;
1398
+ }
1399
+ .rt-callout-icon[data-variant="info"] { background: #2563eb; }
1400
+ .rt-callout-icon[data-variant="warning"] { background: #d97706; }
1401
+ .rt-callout-icon[data-variant="success"] { background: #16a34a; }
1402
+ .rt-callout-icon[data-variant="danger"] { background: #dc2626; }
1403
+ .rt-callout-icon[data-variant="default"] { background: #475569; }
1404
+
1405
+ /* Collapsible (FAQ accordion) ------------------------------------------ */
1406
+ /* Native <details>/<summary> with a rotating chevron. Only structural
1407
+ styling lives here \u2014 colors, padding, and typography belong to
1408
+ consumers via the \`collapsible\` and \`collapsibleTitle\` classMap keys. */
1409
+ details[data-collapsible] > summary {
1410
+ cursor: pointer;
1411
+ list-style: none;
1412
+ display: flex;
1413
+ align-items: center;
1414
+ justify-content: space-between;
1415
+ gap: 0.75rem;
1416
+ }
1417
+ details[data-collapsible] > summary::-webkit-details-marker { display: none; }
1418
+ details[data-collapsible] > summary::after {
1419
+ content: "";
1420
+ width: 0.55rem;
1421
+ height: 0.55rem;
1422
+ border-right: 1.5px solid currentColor;
1423
+ border-bottom: 1.5px solid currentColor;
1424
+ transform: rotate(-45deg) translate(-0.1rem, 0.05rem);
1425
+ transition: transform 0.18s ease;
1426
+ opacity: 0.55;
1427
+ flex-shrink: 0;
1428
+ }
1429
+ details[data-collapsible][open] > summary::after {
1430
+ transform: rotate(45deg) translate(-0.05rem, -0.05rem);
1431
+ }
1279
1432
  /* highlight.js theme handles .hljs-* color classes; we only override the
1280
1433
  default .hljs background so the per-block chrome (dark bg, terminal,
1281
1434
  diff red/green rows) wins. */
@@ -1417,15 +1570,29 @@ function ensureCodeBlockStyles() {
1417
1570
  }
1418
1571
  const tag = document.createElement("style");
1419
1572
  tag.id = "rt-codeblock-style";
1420
- tag.textContent = CODEBLOCK_STYLE;
1573
+ tag.textContent = HLJS_THEME + "\n" + CODEBLOCK_STYLE;
1421
1574
  document.head.appendChild(tag);
1422
1575
  styleInjected = true;
1423
1576
  }
1577
+ function calloutChipsEqual(a, b) {
1578
+ if (a.length !== b.length) return false;
1579
+ for (let i = 0; i < a.length; i++) {
1580
+ if (a[i].el !== b[i].el || a[i].variant !== b[i].variant) return false;
1581
+ }
1582
+ return true;
1583
+ }
1584
+ function BuiltinCalloutIcon({ variant }) {
1585
+ const html = CALLOUT_ICON_SVG[variant] ?? CALLOUT_ICON_SVG.default;
1586
+ return /* @__PURE__ */ jsx("span", { dangerouslySetInnerHTML: { __html: html } });
1587
+ }
1424
1588
  function RichTextContent({
1425
1589
  html,
1426
1590
  classMap,
1427
1591
  as = "div",
1428
- className
1592
+ className,
1593
+ onReady,
1594
+ contentRef,
1595
+ calloutIcons
1429
1596
  }) {
1430
1597
  const merged = useMemo(
1431
1598
  () => mergeClassMap(DEFAULT_CLASS_MAP, classMap),
@@ -1436,20 +1603,215 @@ function RichTextContent({
1436
1603
  [html, merged]
1437
1604
  );
1438
1605
  const ref = useRef(null);
1606
+ const [chips, setChips] = useState([]);
1439
1607
  useEffect(() => {
1440
1608
  ensureCodeBlockStyles();
1441
- if (ref.current) {
1442
- enhanceCodeBlocks(ref.current);
1443
- enhanceBlockquotes(ref.current);
1609
+ const root = ref.current;
1610
+ if (!root) return;
1611
+ if (contentRef) contentRef.current = root;
1612
+ const apply = () => {
1613
+ mo.disconnect();
1614
+ enhanceCodeBlocks(root);
1615
+ enhanceBlockquotes(root);
1616
+ const nextChips = enhanceCallouts(root);
1617
+ setChips((prev) => calloutChipsEqual(prev, nextChips) ? prev : nextChips);
1618
+ onReady?.(root);
1619
+ mo.observe(root, { childList: true, subtree: true });
1620
+ };
1621
+ let raf = 0;
1622
+ const mo = new MutationObserver(() => {
1623
+ if (raf) return;
1624
+ raf = requestAnimationFrame(() => {
1625
+ raf = 0;
1626
+ apply();
1627
+ });
1628
+ });
1629
+ apply();
1630
+ return () => {
1631
+ mo.disconnect();
1632
+ if (raf) cancelAnimationFrame(raf);
1633
+ };
1634
+ }, [safe, onReady, contentRef]);
1635
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1636
+ createElement(as, {
1637
+ ref,
1638
+ className,
1639
+ dangerouslySetInnerHTML: { __html: safe }
1640
+ }),
1641
+ chips.map(
1642
+ (chip, i) => createPortal(
1643
+ calloutIcons && chip.variant in calloutIcons ? calloutIcons[chip.variant] : /* @__PURE__ */ jsx(BuiltinCalloutIcon, { variant: chip.variant }),
1644
+ chip.el,
1645
+ `${chip.variant}:${i}`
1646
+ )
1647
+ )
1648
+ ] });
1649
+ }
1650
+
1651
+ // src/utils/extractHeadings.ts
1652
+ var DEFAULT_LEVELS = [2, 3];
1653
+ function slugify(text) {
1654
+ return text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1655
+ }
1656
+ function decodeBasicEntities(s) {
1657
+ return s.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
1658
+ }
1659
+ function stripTags(s) {
1660
+ return decodeBasicEntities(s.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
1661
+ }
1662
+ function uniqueId(base, used) {
1663
+ const seed = base || "section";
1664
+ const n = used.get(seed) ?? 0;
1665
+ used.set(seed, n + 1);
1666
+ return n === 0 ? seed : `${seed}-${n}`;
1667
+ }
1668
+ function extractHeadingsFromHtml(html, options = {}) {
1669
+ if (!html) return [];
1670
+ const levels = options.levels ?? DEFAULT_LEVELS;
1671
+ const slug = options.slugify ?? slugify;
1672
+ const used = /* @__PURE__ */ new Map();
1673
+ const out = [];
1674
+ const re = /<h([1-6])\b([^>]*)>([\s\S]*?)<\/h\1>/gi;
1675
+ let m;
1676
+ let i = 0;
1677
+ while ((m = re.exec(html)) !== null) {
1678
+ const level = Number(m[1]);
1679
+ if (!levels.includes(level)) continue;
1680
+ const attrs = m[2] ?? "";
1681
+ const inner = m[3] ?? "";
1682
+ const text = stripTags(inner);
1683
+ if (!text) continue;
1684
+ const explicitIdMatch = attrs.match(/\bid\s*=\s*("([^"]*)"|'([^']*)'|(\S+))/i);
1685
+ let id;
1686
+ if (explicitIdMatch) {
1687
+ id = explicitIdMatch[2] ?? explicitIdMatch[3] ?? explicitIdMatch[4] ?? "";
1688
+ if (id) used.set(id, (used.get(id) ?? 0) + 1);
1689
+ } else {
1690
+ id = uniqueId(slug(text, i), used);
1444
1691
  }
1445
- }, [safe]);
1446
- return createElement(as, {
1447
- ref,
1448
- className,
1449
- dangerouslySetInnerHTML: { __html: safe }
1692
+ out.push({ id, text, level });
1693
+ i++;
1694
+ }
1695
+ return out;
1696
+ }
1697
+ function extractHeadingsFromElement(root, options = {}) {
1698
+ const levels = options.levels ?? DEFAULT_LEVELS;
1699
+ const slug = options.slugify ?? slugify;
1700
+ const selector = levels.map((l) => `h${l}`).join(",");
1701
+ const nodes = root.querySelectorAll(selector);
1702
+ const used = /* @__PURE__ */ new Map();
1703
+ nodes.forEach((n) => {
1704
+ if (n.id) used.set(n.id, (used.get(n.id) ?? 0) + 1);
1450
1705
  });
1706
+ const out = [];
1707
+ let i = 0;
1708
+ nodes.forEach((node) => {
1709
+ const level = Number(node.tagName.slice(1));
1710
+ const text = (node.textContent ?? "").replace(/\s+/g, " ").trim();
1711
+ if (!text) return;
1712
+ if (!node.id) {
1713
+ node.id = uniqueId(slug(text, i), used);
1714
+ }
1715
+ if (options.scrollMarginTop != null) {
1716
+ node.style.scrollMarginTop = `${options.scrollMarginTop}px`;
1717
+ }
1718
+ out.push({ id: node.id, text, level });
1719
+ i++;
1720
+ });
1721
+ return out;
1722
+ }
1723
+
1724
+ // src/hooks/useTableOfContents.tsx
1725
+ function useTableOfContents(ref, options = {}) {
1726
+ const {
1727
+ levels,
1728
+ contentKey = null,
1729
+ scrollMarginTop = 24,
1730
+ activationOffset = 96
1731
+ } = options;
1732
+ const [items, setItems] = useState([]);
1733
+ const [activeId, setActiveId] = useState("");
1734
+ useEffect(() => {
1735
+ const root = ref.current;
1736
+ if (!root) {
1737
+ setItems([]);
1738
+ setActiveId("");
1739
+ return;
1740
+ }
1741
+ let raf = 0;
1742
+ const collect = () => {
1743
+ const next = extractHeadingsFromElement(root, {
1744
+ levels,
1745
+ scrollMarginTop
1746
+ });
1747
+ setItems(next);
1748
+ setActiveId(
1749
+ (prev) => next.some((h) => h.id === prev) ? prev : next[0]?.id ?? ""
1750
+ );
1751
+ };
1752
+ raf = requestAnimationFrame(collect);
1753
+ const mo = new MutationObserver(() => {
1754
+ if (raf) cancelAnimationFrame(raf);
1755
+ raf = requestAnimationFrame(collect);
1756
+ });
1757
+ mo.observe(root, { childList: true, subtree: true, characterData: true });
1758
+ return () => {
1759
+ mo.disconnect();
1760
+ if (raf) cancelAnimationFrame(raf);
1761
+ };
1762
+ }, [ref, contentKey, levels, scrollMarginTop]);
1763
+ useEffect(() => {
1764
+ if (items.length === 0) return;
1765
+ const targets = items.map((it) => document.getElementById(it.id)).filter((el) => el !== null);
1766
+ if (targets.length === 0) return;
1767
+ let raf = 0;
1768
+ const compute = () => {
1769
+ raf = 0;
1770
+ let activeIdx = 0;
1771
+ for (let i = 0; i < items.length; i++) {
1772
+ const el = document.getElementById(items[i].id);
1773
+ if (!el) continue;
1774
+ if (el.getBoundingClientRect().top - activationOffset <= 0) {
1775
+ activeIdx = i;
1776
+ } else {
1777
+ break;
1778
+ }
1779
+ }
1780
+ const scroller = document.scrollingElement || document.documentElement;
1781
+ const scrollY = window.scrollY;
1782
+ const viewportH = window.innerHeight;
1783
+ const atBottom = scrollY + viewportH >= scroller.scrollHeight - 2;
1784
+ if (atBottom) {
1785
+ for (let i = items.length - 1; i > activeIdx; i--) {
1786
+ const el = document.getElementById(items[i].id);
1787
+ if (el && el.getBoundingClientRect().top < viewportH) {
1788
+ activeIdx = i;
1789
+ break;
1790
+ }
1791
+ }
1792
+ }
1793
+ setActiveId(items[activeIdx].id);
1794
+ };
1795
+ const schedule = () => {
1796
+ if (raf) return;
1797
+ raf = requestAnimationFrame(compute);
1798
+ };
1799
+ const io = new IntersectionObserver(schedule, {
1800
+ rootMargin: `-${activationOffset}px 0px -${Math.max(0, window.innerHeight - activationOffset - 1)}px 0px`,
1801
+ threshold: 0
1802
+ });
1803
+ targets.forEach((t) => io.observe(t));
1804
+ window.addEventListener("resize", schedule, { passive: true });
1805
+ compute();
1806
+ return () => {
1807
+ io.disconnect();
1808
+ window.removeEventListener("resize", schedule);
1809
+ if (raf) cancelAnimationFrame(raf);
1810
+ };
1811
+ }, [items, activationOffset]);
1812
+ return { items, activeId };
1451
1813
  }
1452
1814
 
1453
- export { AsteroidCMSProvider, RichTextContent, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate };
1815
+ export { AsteroidCMSProvider, RichTextContent, extractHeadingsFromElement, extractHeadingsFromHtml, slugify, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate, useTableOfContents };
1454
1816
  //# sourceMappingURL=client.js.map
1455
1817
  //# sourceMappingURL=client.js.map