@asteroidcms/core-utils 0.1.3 → 0.1.4

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.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
- import { PropsWithChildren } from 'react';
3
+ import { PropsWithChildren, RefObject } from 'react';
4
4
  import * as _apollo_client from '@apollo/client';
5
5
  import { InMemoryCacheConfig, ApolloClientOptions, ApolloClient } from '@apollo/client';
6
6
  import { useMutation } from '@apollo/client/react';
@@ -217,8 +217,20 @@ interface RichTextContentProps {
217
217
  /** Wrapper element. Defaults to `div`. Use `article` for blog content. */
218
218
  as?: keyof React.JSX.IntrinsicElements;
219
219
  className?: string;
220
+ /**
221
+ * Fires after the parsed HTML is in the DOM and post-render enhancements
222
+ * (syntax highlighting, copy buttons, blockquote decorations) have run.
223
+ * The wrapper element is passed back so callers can read headings, attach
224
+ * a ToC observer, or do other DOM work without re-querying.
225
+ */
226
+ onReady?: (root: HTMLElement) => void;
227
+ /**
228
+ * Optional ref to the wrapper element. Useful for hooks like
229
+ * `useTableOfContents` that need a stable reference to the rendered tree.
230
+ */
231
+ contentRef?: React.MutableRefObject<HTMLElement | null>;
220
232
  }
221
- declare function RichTextContent({ html, classMap, as, className, }: RichTextContentProps): react.DOMElement<{
233
+ declare function RichTextContent({ html, classMap, as, className, onReady, contentRef, }: RichTextContentProps): react.DOMElement<{
222
234
  ref: react.MutableRefObject<HTMLElement | null>;
223
235
  className: string | undefined;
224
236
  dangerouslySetInnerHTML: {
@@ -226,4 +238,79 @@ declare function RichTextContent({ html, classMap, as, className, }: RichTextCon
226
238
  };
227
239
  }, HTMLElement>;
228
240
 
229
- export { AsteroidCMSProvider, type AsteroidCMSProviderProps, RichTextContent, type UseCmsContentOptions, type UseCmsMutateOptions, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate };
241
+ /**
242
+ * Heading extraction helpers used to build tables of contents (ToC) from
243
+ * rich-text HTML. Server-safe — no React, no DOM dependency in the HTML
244
+ * variant. The DOM variant assigns missing `id`s in-place so anchor links
245
+ * resolve immediately.
246
+ */
247
+ type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
248
+ interface ExtractedHeading {
249
+ id: string;
250
+ text: string;
251
+ level: HeadingLevel;
252
+ }
253
+ interface ExtractHeadingsOptions {
254
+ /** Levels to include. Defaults to `[2, 3]` — typical doc page outline. */
255
+ levels?: ReadonlyArray<HeadingLevel>;
256
+ /** Custom slug function. Defaults to a lowercase/kebab/diacritic-safe slug. */
257
+ slugify?: (text: string, index: number) => string;
258
+ }
259
+ declare function slugify(text: string): string;
260
+ /**
261
+ * Parse headings out of a raw HTML string. Returns headings in document
262
+ * order with stable, de-duplicated IDs.
263
+ *
264
+ * If a heading already has an `id` attribute, it's preserved verbatim
265
+ * (and reserved so later slugs don't collide with it).
266
+ */
267
+ declare function extractHeadingsFromHtml(html: string, options?: ExtractHeadingsOptions): ExtractedHeading[];
268
+ /**
269
+ * Walk a rendered DOM subtree, collect headings, and assign missing `id`s
270
+ * in-place so anchor links resolve immediately. Also sets `scrollMarginTop`
271
+ * on each heading when `scrollMarginTop` is provided so navigation lands
272
+ * cleanly below a sticky header.
273
+ */
274
+ declare function extractHeadingsFromElement(root: HTMLElement, options?: ExtractHeadingsOptions & {
275
+ scrollMarginTop?: number;
276
+ }): ExtractedHeading[];
277
+
278
+ interface UseTableOfContentsOptions {
279
+ /** Heading levels to include. Defaults to `[2, 3]`. */
280
+ levels?: ReadonlyArray<HeadingLevel>;
281
+ /**
282
+ * Re-collect headings whenever this value changes. Pass the article slug
283
+ * (or any stable identifier) so swapping content rebuilds the ToC.
284
+ */
285
+ contentKey?: string | number | null;
286
+ /** Pixels to subtract from heading scroll-into-view target. Default 24. */
287
+ scrollMarginTop?: number;
288
+ /**
289
+ * Distance from the top of the viewport (in px) at which a heading becomes
290
+ * "active". A heading is considered active once its top edge has scrolled
291
+ * past this line. Default `96` — works well with a sticky header that
292
+ * stands ~60–80px tall.
293
+ */
294
+ activationOffset?: number;
295
+ }
296
+ interface UseTableOfContentsResult {
297
+ items: ExtractedHeading[];
298
+ activeId: string;
299
+ }
300
+ /**
301
+ * Build a table of contents from a rendered element and track which
302
+ * heading is currently in view via IntersectionObserver. Resilient to
303
+ * content swaps when `contentKey` is provided.
304
+ *
305
+ * Usage:
306
+ * ```tsx
307
+ * const ref = useRef<HTMLDivElement>(null);
308
+ * const { items, activeId } = useTableOfContents(ref, {
309
+ * contentKey: article.slug,
310
+ * levels: [2, 3],
311
+ * });
312
+ * ```
313
+ */
314
+ declare function useTableOfContents(ref: RefObject<HTMLElement | null>, options?: UseTableOfContentsOptions): UseTableOfContentsResult;
315
+
316
+ export { AsteroidCMSProvider, type AsteroidCMSProviderProps, type ExtractHeadingsOptions, type ExtractedHeading, type HeadingLevel, RichTextContent, type UseCmsContentOptions, type UseCmsMutateOptions, type UseTableOfContentsOptions, type UseTableOfContentsResult, extractHeadingsFromElement, extractHeadingsFromHtml, slugify, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate, useTableOfContents };
package/dist/client.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
- import { PropsWithChildren } from 'react';
3
+ import { PropsWithChildren, RefObject } from 'react';
4
4
  import * as _apollo_client from '@apollo/client';
5
5
  import { InMemoryCacheConfig, ApolloClientOptions, ApolloClient } from '@apollo/client';
6
6
  import { useMutation } from '@apollo/client/react';
@@ -217,8 +217,20 @@ interface RichTextContentProps {
217
217
  /** Wrapper element. Defaults to `div`. Use `article` for blog content. */
218
218
  as?: keyof React.JSX.IntrinsicElements;
219
219
  className?: string;
220
+ /**
221
+ * Fires after the parsed HTML is in the DOM and post-render enhancements
222
+ * (syntax highlighting, copy buttons, blockquote decorations) have run.
223
+ * The wrapper element is passed back so callers can read headings, attach
224
+ * a ToC observer, or do other DOM work without re-querying.
225
+ */
226
+ onReady?: (root: HTMLElement) => void;
227
+ /**
228
+ * Optional ref to the wrapper element. Useful for hooks like
229
+ * `useTableOfContents` that need a stable reference to the rendered tree.
230
+ */
231
+ contentRef?: React.MutableRefObject<HTMLElement | null>;
220
232
  }
221
- declare function RichTextContent({ html, classMap, as, className, }: RichTextContentProps): react.DOMElement<{
233
+ declare function RichTextContent({ html, classMap, as, className, onReady, contentRef, }: RichTextContentProps): react.DOMElement<{
222
234
  ref: react.MutableRefObject<HTMLElement | null>;
223
235
  className: string | undefined;
224
236
  dangerouslySetInnerHTML: {
@@ -226,4 +238,79 @@ declare function RichTextContent({ html, classMap, as, className, }: RichTextCon
226
238
  };
227
239
  }, HTMLElement>;
228
240
 
229
- export { AsteroidCMSProvider, type AsteroidCMSProviderProps, RichTextContent, type UseCmsContentOptions, type UseCmsMutateOptions, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate };
241
+ /**
242
+ * Heading extraction helpers used to build tables of contents (ToC) from
243
+ * rich-text HTML. Server-safe — no React, no DOM dependency in the HTML
244
+ * variant. The DOM variant assigns missing `id`s in-place so anchor links
245
+ * resolve immediately.
246
+ */
247
+ type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
248
+ interface ExtractedHeading {
249
+ id: string;
250
+ text: string;
251
+ level: HeadingLevel;
252
+ }
253
+ interface ExtractHeadingsOptions {
254
+ /** Levels to include. Defaults to `[2, 3]` — typical doc page outline. */
255
+ levels?: ReadonlyArray<HeadingLevel>;
256
+ /** Custom slug function. Defaults to a lowercase/kebab/diacritic-safe slug. */
257
+ slugify?: (text: string, index: number) => string;
258
+ }
259
+ declare function slugify(text: string): string;
260
+ /**
261
+ * Parse headings out of a raw HTML string. Returns headings in document
262
+ * order with stable, de-duplicated IDs.
263
+ *
264
+ * If a heading already has an `id` attribute, it's preserved verbatim
265
+ * (and reserved so later slugs don't collide with it).
266
+ */
267
+ declare function extractHeadingsFromHtml(html: string, options?: ExtractHeadingsOptions): ExtractedHeading[];
268
+ /**
269
+ * Walk a rendered DOM subtree, collect headings, and assign missing `id`s
270
+ * in-place so anchor links resolve immediately. Also sets `scrollMarginTop`
271
+ * on each heading when `scrollMarginTop` is provided so navigation lands
272
+ * cleanly below a sticky header.
273
+ */
274
+ declare function extractHeadingsFromElement(root: HTMLElement, options?: ExtractHeadingsOptions & {
275
+ scrollMarginTop?: number;
276
+ }): ExtractedHeading[];
277
+
278
+ interface UseTableOfContentsOptions {
279
+ /** Heading levels to include. Defaults to `[2, 3]`. */
280
+ levels?: ReadonlyArray<HeadingLevel>;
281
+ /**
282
+ * Re-collect headings whenever this value changes. Pass the article slug
283
+ * (or any stable identifier) so swapping content rebuilds the ToC.
284
+ */
285
+ contentKey?: string | number | null;
286
+ /** Pixels to subtract from heading scroll-into-view target. Default 24. */
287
+ scrollMarginTop?: number;
288
+ /**
289
+ * Distance from the top of the viewport (in px) at which a heading becomes
290
+ * "active". A heading is considered active once its top edge has scrolled
291
+ * past this line. Default `96` — works well with a sticky header that
292
+ * stands ~60–80px tall.
293
+ */
294
+ activationOffset?: number;
295
+ }
296
+ interface UseTableOfContentsResult {
297
+ items: ExtractedHeading[];
298
+ activeId: string;
299
+ }
300
+ /**
301
+ * Build a table of contents from a rendered element and track which
302
+ * heading is currently in view via IntersectionObserver. Resilient to
303
+ * content swaps when `contentKey` is provided.
304
+ *
305
+ * Usage:
306
+ * ```tsx
307
+ * const ref = useRef<HTMLDivElement>(null);
308
+ * const { items, activeId } = useTableOfContents(ref, {
309
+ * contentKey: article.slug,
310
+ * levels: [2, 3],
311
+ * });
312
+ * ```
313
+ */
314
+ declare function useTableOfContents(ref: RefObject<HTMLElement | null>, options?: UseTableOfContentsOptions): UseTableOfContentsResult;
315
+
316
+ export { AsteroidCMSProvider, type AsteroidCMSProviderProps, type ExtractHeadingsOptions, type ExtractedHeading, type HeadingLevel, RichTextContent, type UseCmsContentOptions, type UseCmsMutateOptions, type UseTableOfContentsOptions, type UseTableOfContentsResult, extractHeadingsFromElement, extractHeadingsFromHtml, slugify, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate, useTableOfContents };
package/dist/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { createContext, useContext, useMemo, useRef, useEffect, createElement } from 'react';
2
+ import { createContext, useContext, useMemo, useRef, useEffect, createElement, useState } 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';
@@ -419,7 +419,36 @@ function parseRichText(html, options = {}) {
419
419
  working = upgradeStandaloneImages(working);
420
420
  working = upgradeAuthoredBlockquotes(working);
421
421
  working = flattenTableCellParagraphs(working);
422
- return sanitizeAndStyle(working, options);
422
+ working = sanitizeAndStyle(working, options);
423
+ if (options.autoHeadingIds !== false) {
424
+ working = injectHeadingIds(working);
425
+ }
426
+ return working;
427
+ }
428
+ function injectHeadingIds(html) {
429
+ const used = /* @__PURE__ */ new Map();
430
+ const existingRe = /<h[1-6]\b[^>]*\bid\s*=\s*("([^"]*)"|'([^']*)'|(\S+))/gi;
431
+ let em;
432
+ while ((em = existingRe.exec(html)) !== null) {
433
+ const id = em[2] ?? em[3] ?? em[4] ?? "";
434
+ if (id) used.set(id, (used.get(id) ?? 0) + 1);
435
+ }
436
+ return html.replace(
437
+ /<(h[1-6])\b([^>]*)>([\s\S]*?)<\/\1>/gi,
438
+ (full, tag, attrs, inner) => {
439
+ if (/\bid\s*=/i.test(attrs)) return full;
440
+ 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();
441
+ if (!text) return full;
442
+ const base = slugifyHeading(text) || tag;
443
+ const n = used.get(base) ?? 0;
444
+ used.set(base, n + 1);
445
+ const id = n === 0 ? base : `${base}-${n}`;
446
+ return `<${tag}${attrs} id="${id}">${inner}</${tag}>`;
447
+ }
448
+ );
449
+ }
450
+ function slugifyHeading(text) {
451
+ return text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
423
452
  }
424
453
  function flattenTableCellParagraphs(html) {
425
454
  return html.replace(
@@ -1425,7 +1454,9 @@ function RichTextContent({
1425
1454
  html,
1426
1455
  classMap,
1427
1456
  as = "div",
1428
- className
1457
+ className,
1458
+ onReady,
1459
+ contentRef
1429
1460
  }) {
1430
1461
  const merged = useMemo(
1431
1462
  () => mergeClassMap(DEFAULT_CLASS_MAP, classMap),
@@ -1438,11 +1469,30 @@ function RichTextContent({
1438
1469
  const ref = useRef(null);
1439
1470
  useEffect(() => {
1440
1471
  ensureCodeBlockStyles();
1441
- if (ref.current) {
1442
- enhanceCodeBlocks(ref.current);
1443
- enhanceBlockquotes(ref.current);
1444
- }
1445
- }, [safe]);
1472
+ const root = ref.current;
1473
+ if (!root) return;
1474
+ if (contentRef) contentRef.current = root;
1475
+ const apply = () => {
1476
+ mo.disconnect();
1477
+ enhanceCodeBlocks(root);
1478
+ enhanceBlockquotes(root);
1479
+ onReady?.(root);
1480
+ mo.observe(root, { childList: true, subtree: true });
1481
+ };
1482
+ let raf = 0;
1483
+ const mo = new MutationObserver(() => {
1484
+ if (raf) return;
1485
+ raf = requestAnimationFrame(() => {
1486
+ raf = 0;
1487
+ apply();
1488
+ });
1489
+ });
1490
+ apply();
1491
+ return () => {
1492
+ mo.disconnect();
1493
+ if (raf) cancelAnimationFrame(raf);
1494
+ };
1495
+ }, [safe, onReady, contentRef]);
1446
1496
  return createElement(as, {
1447
1497
  ref,
1448
1498
  className,
@@ -1450,6 +1500,170 @@ function RichTextContent({
1450
1500
  });
1451
1501
  }
1452
1502
 
1453
- export { AsteroidCMSProvider, RichTextContent, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate };
1503
+ // src/utils/extractHeadings.ts
1504
+ var DEFAULT_LEVELS = [2, 3];
1505
+ function slugify(text) {
1506
+ return text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1507
+ }
1508
+ function decodeBasicEntities(s) {
1509
+ return s.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
1510
+ }
1511
+ function stripTags(s) {
1512
+ return decodeBasicEntities(s.replace(/<[^>]+>/g, " ")).replace(/\s+/g, " ").trim();
1513
+ }
1514
+ function uniqueId(base, used) {
1515
+ const seed = base || "section";
1516
+ const n = used.get(seed) ?? 0;
1517
+ used.set(seed, n + 1);
1518
+ return n === 0 ? seed : `${seed}-${n}`;
1519
+ }
1520
+ function extractHeadingsFromHtml(html, options = {}) {
1521
+ if (!html) return [];
1522
+ const levels = options.levels ?? DEFAULT_LEVELS;
1523
+ const slug = options.slugify ?? slugify;
1524
+ const used = /* @__PURE__ */ new Map();
1525
+ const out = [];
1526
+ const re = /<h([1-6])\b([^>]*)>([\s\S]*?)<\/h\1>/gi;
1527
+ let m;
1528
+ let i = 0;
1529
+ while ((m = re.exec(html)) !== null) {
1530
+ const level = Number(m[1]);
1531
+ if (!levels.includes(level)) continue;
1532
+ const attrs = m[2] ?? "";
1533
+ const inner = m[3] ?? "";
1534
+ const text = stripTags(inner);
1535
+ if (!text) continue;
1536
+ const explicitIdMatch = attrs.match(/\bid\s*=\s*("([^"]*)"|'([^']*)'|(\S+))/i);
1537
+ let id;
1538
+ if (explicitIdMatch) {
1539
+ id = explicitIdMatch[2] ?? explicitIdMatch[3] ?? explicitIdMatch[4] ?? "";
1540
+ if (id) used.set(id, (used.get(id) ?? 0) + 1);
1541
+ } else {
1542
+ id = uniqueId(slug(text, i), used);
1543
+ }
1544
+ out.push({ id, text, level });
1545
+ i++;
1546
+ }
1547
+ return out;
1548
+ }
1549
+ function extractHeadingsFromElement(root, options = {}) {
1550
+ const levels = options.levels ?? DEFAULT_LEVELS;
1551
+ const slug = options.slugify ?? slugify;
1552
+ const selector = levels.map((l) => `h${l}`).join(",");
1553
+ const nodes = root.querySelectorAll(selector);
1554
+ const used = /* @__PURE__ */ new Map();
1555
+ nodes.forEach((n) => {
1556
+ if (n.id) used.set(n.id, (used.get(n.id) ?? 0) + 1);
1557
+ });
1558
+ const out = [];
1559
+ let i = 0;
1560
+ nodes.forEach((node) => {
1561
+ const level = Number(node.tagName.slice(1));
1562
+ const text = (node.textContent ?? "").replace(/\s+/g, " ").trim();
1563
+ if (!text) return;
1564
+ if (!node.id) {
1565
+ node.id = uniqueId(slug(text, i), used);
1566
+ }
1567
+ if (options.scrollMarginTop != null) {
1568
+ node.style.scrollMarginTop = `${options.scrollMarginTop}px`;
1569
+ }
1570
+ out.push({ id: node.id, text, level });
1571
+ i++;
1572
+ });
1573
+ return out;
1574
+ }
1575
+
1576
+ // src/hooks/useTableOfContents.tsx
1577
+ function useTableOfContents(ref, options = {}) {
1578
+ const {
1579
+ levels,
1580
+ contentKey = null,
1581
+ scrollMarginTop = 24,
1582
+ activationOffset = 96
1583
+ } = options;
1584
+ const [items, setItems] = useState([]);
1585
+ const [activeId, setActiveId] = useState("");
1586
+ useEffect(() => {
1587
+ const root = ref.current;
1588
+ if (!root) {
1589
+ setItems([]);
1590
+ setActiveId("");
1591
+ return;
1592
+ }
1593
+ let raf = 0;
1594
+ const collect = () => {
1595
+ const next = extractHeadingsFromElement(root, {
1596
+ levels,
1597
+ scrollMarginTop
1598
+ });
1599
+ setItems(next);
1600
+ setActiveId(
1601
+ (prev) => next.some((h) => h.id === prev) ? prev : next[0]?.id ?? ""
1602
+ );
1603
+ };
1604
+ raf = requestAnimationFrame(collect);
1605
+ const mo = new MutationObserver(() => {
1606
+ if (raf) cancelAnimationFrame(raf);
1607
+ raf = requestAnimationFrame(collect);
1608
+ });
1609
+ mo.observe(root, { childList: true, subtree: true, characterData: true });
1610
+ return () => {
1611
+ mo.disconnect();
1612
+ if (raf) cancelAnimationFrame(raf);
1613
+ };
1614
+ }, [ref, contentKey, levels, scrollMarginTop]);
1615
+ useEffect(() => {
1616
+ if (items.length === 0) return;
1617
+ const targets = items.map((it) => document.getElementById(it.id)).filter((el) => el !== null);
1618
+ if (targets.length === 0) return;
1619
+ let raf = 0;
1620
+ const compute = () => {
1621
+ raf = 0;
1622
+ let activeIdx = 0;
1623
+ for (let i = 0; i < items.length; i++) {
1624
+ const el = document.getElementById(items[i].id);
1625
+ if (!el) continue;
1626
+ if (el.getBoundingClientRect().top - activationOffset <= 0) {
1627
+ activeIdx = i;
1628
+ } else {
1629
+ break;
1630
+ }
1631
+ }
1632
+ const scroller = document.scrollingElement || document.documentElement;
1633
+ const scrollY = window.scrollY;
1634
+ const viewportH = window.innerHeight;
1635
+ const atBottom = scrollY + viewportH >= scroller.scrollHeight - 2;
1636
+ if (atBottom) {
1637
+ for (let i = items.length - 1; i > activeIdx; i--) {
1638
+ const el = document.getElementById(items[i].id);
1639
+ if (el && el.getBoundingClientRect().top < viewportH) {
1640
+ activeIdx = i;
1641
+ break;
1642
+ }
1643
+ }
1644
+ }
1645
+ setActiveId(items[activeIdx].id);
1646
+ };
1647
+ const schedule = () => {
1648
+ if (raf) return;
1649
+ raf = requestAnimationFrame(compute);
1650
+ };
1651
+ const io = new IntersectionObserver(schedule, {
1652
+ rootMargin: `-${activationOffset}px 0px -${Math.max(0, window.innerHeight - activationOffset - 1)}px 0px`,
1653
+ threshold: 0
1654
+ });
1655
+ targets.forEach((t) => io.observe(t));
1656
+ window.addEventListener("resize", schedule, { passive: true });
1657
+ compute();
1658
+ return () => {
1659
+ io.disconnect();
1660
+ window.removeEventListener("resize", schedule);
1661
+ if (raf) cancelAnimationFrame(raf);
1662
+ };
1663
+ }, [items, activationOffset]);
1664
+ return { items, activeId };
1665
+ }
1666
+
1667
+ export { AsteroidCMSProvider, RichTextContent, extractHeadingsFromElement, extractHeadingsFromHtml, slugify, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate, useTableOfContents };
1454
1668
  //# sourceMappingURL=client.js.map
1455
1669
  //# sourceMappingURL=client.js.map