@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/dist/client.d.cts CHANGED
@@ -1,6 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import * as react from 'react';
3
- import { PropsWithChildren, RefObject } from 'react';
2
+ import { PropsWithChildren, ReactNode } from 'react';
4
3
  import * as _apollo_client from '@apollo/client';
5
4
  import { InMemoryCacheConfig, ApolloClientOptions, ApolloClient } from '@apollo/client';
6
5
  import { useMutation } from '@apollo/client/react';
@@ -205,7 +204,7 @@ declare function useCmsImage(): (id?: string) => string;
205
204
  *
206
205
  * Idempotent: parseRichText(parseRichText(x, opts), opts) === parseRichText(x, opts).
207
206
  */
208
- type RichTextClassKey = "p" | "br" | "hr" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol" | "li" | "blockquote" | "pre" | "code" | "inlineCode" | "a" | "strong" | "em" | "u" | "s" | "kbd" | "table" | "tableWrapper" | "thead" | "tbody" | "tr" | "th" | "td" | "figure" | "figcaption" | "img" | "span" | "callout" | "calloutTitle";
207
+ type RichTextClassKey = "p" | "br" | "hr" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol" | "li" | "blockquote" | "pre" | "code" | "inlineCode" | "a" | "strong" | "em" | "u" | "s" | "kbd" | "table" | "tableWrapper" | "thead" | "tbody" | "tr" | "th" | "td" | "figure" | "figcaption" | "img" | "span" | "callout" | "calloutTitle" | "collapsible" | "collapsibleTitle";
209
208
  type RichTextClassMap = Partial<Record<RichTextClassKey, string>> & {
210
209
  /** Variant overrides keyed as `${matchKey}:${variant}`, e.g. "callout:warning". */
211
210
  variants?: Record<string, string>;
@@ -225,18 +224,37 @@ interface RichTextContentProps {
225
224
  */
226
225
  onReady?: (root: HTMLElement) => void;
227
226
  /**
228
- * Optional ref to the wrapper element. Useful for hooks like
229
- * `useTableOfContents` that need a stable reference to the rendered tree.
227
+ * Optional ref to the wrapper element. Useful for consumers that need a
228
+ * stable reference to the rendered tree (e.g. scroll observers, ToC hooks).
230
229
  */
231
230
  contentRef?: React.MutableRefObject<HTMLElement | null>;
231
+ /**
232
+ * Per-variant icon override for callouts that opt into icons via
233
+ * `data-icon`. Provide a React element for any variant key — these
234
+ * elements are rendered into the chip via a portal, so they live in
235
+ * React land (refs, event handlers, theme context all work). Variants
236
+ * you don't provide fall back to the built-in SVG glyph.
237
+ *
238
+ * Common variants: `"info" | "warning" | "success" | "danger" | "default"`.
239
+ * Custom variant names work too — anything you set via
240
+ * `data-variant` on an `<aside data-callout>`.
241
+ *
242
+ * Example:
243
+ * ```tsx
244
+ * import { Info, AlertTriangle } from "lucide-react";
245
+ *
246
+ * <RichTextContent
247
+ * html={article.doc}
248
+ * calloutIcons={{
249
+ * info: <Info size={14} strokeWidth={2.4} />,
250
+ * warning: <AlertTriangle size={14} strokeWidth={2.4} />,
251
+ * }}
252
+ * />
253
+ * ```
254
+ */
255
+ calloutIcons?: Partial<Record<string, ReactNode>>;
232
256
  }
233
- declare function RichTextContent({ html, classMap, as, className, onReady, contentRef, }: RichTextContentProps): react.DOMElement<{
234
- ref: react.MutableRefObject<HTMLElement | null>;
235
- className: string | undefined;
236
- dangerouslySetInnerHTML: {
237
- __html: string;
238
- };
239
- }, HTMLElement>;
257
+ declare function RichTextContent({ html, classMap, as, className, onReady, contentRef, calloutIcons, }: RichTextContentProps): react_jsx_runtime.JSX.Element;
240
258
 
241
259
  /**
242
260
  * Heading extraction helpers used to build tables of contents (ToC) from
@@ -275,42 +293,4 @@ declare function extractHeadingsFromElement(root: HTMLElement, options?: Extract
275
293
  scrollMarginTop?: number;
276
294
  }): ExtractedHeading[];
277
295
 
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 };
296
+ export { AsteroidCMSProvider, type AsteroidCMSProviderProps, type ExtractHeadingsOptions, type ExtractedHeading, type HeadingLevel, RichTextContent, type UseCmsContentOptions, type UseCmsMutateOptions, extractHeadingsFromElement, extractHeadingsFromHtml, slugify, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate };
package/dist/client.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import * as react from 'react';
3
- import { PropsWithChildren, RefObject } from 'react';
2
+ import { PropsWithChildren, ReactNode } from 'react';
4
3
  import * as _apollo_client from '@apollo/client';
5
4
  import { InMemoryCacheConfig, ApolloClientOptions, ApolloClient } from '@apollo/client';
6
5
  import { useMutation } from '@apollo/client/react';
@@ -205,7 +204,7 @@ declare function useCmsImage(): (id?: string) => string;
205
204
  *
206
205
  * Idempotent: parseRichText(parseRichText(x, opts), opts) === parseRichText(x, opts).
207
206
  */
208
- type RichTextClassKey = "p" | "br" | "hr" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol" | "li" | "blockquote" | "pre" | "code" | "inlineCode" | "a" | "strong" | "em" | "u" | "s" | "kbd" | "table" | "tableWrapper" | "thead" | "tbody" | "tr" | "th" | "td" | "figure" | "figcaption" | "img" | "span" | "callout" | "calloutTitle";
207
+ type RichTextClassKey = "p" | "br" | "hr" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol" | "li" | "blockquote" | "pre" | "code" | "inlineCode" | "a" | "strong" | "em" | "u" | "s" | "kbd" | "table" | "tableWrapper" | "thead" | "tbody" | "tr" | "th" | "td" | "figure" | "figcaption" | "img" | "span" | "callout" | "calloutTitle" | "collapsible" | "collapsibleTitle";
209
208
  type RichTextClassMap = Partial<Record<RichTextClassKey, string>> & {
210
209
  /** Variant overrides keyed as `${matchKey}:${variant}`, e.g. "callout:warning". */
211
210
  variants?: Record<string, string>;
@@ -225,18 +224,37 @@ interface RichTextContentProps {
225
224
  */
226
225
  onReady?: (root: HTMLElement) => void;
227
226
  /**
228
- * Optional ref to the wrapper element. Useful for hooks like
229
- * `useTableOfContents` that need a stable reference to the rendered tree.
227
+ * Optional ref to the wrapper element. Useful for consumers that need a
228
+ * stable reference to the rendered tree (e.g. scroll observers, ToC hooks).
230
229
  */
231
230
  contentRef?: React.MutableRefObject<HTMLElement | null>;
231
+ /**
232
+ * Per-variant icon override for callouts that opt into icons via
233
+ * `data-icon`. Provide a React element for any variant key — these
234
+ * elements are rendered into the chip via a portal, so they live in
235
+ * React land (refs, event handlers, theme context all work). Variants
236
+ * you don't provide fall back to the built-in SVG glyph.
237
+ *
238
+ * Common variants: `"info" | "warning" | "success" | "danger" | "default"`.
239
+ * Custom variant names work too — anything you set via
240
+ * `data-variant` on an `<aside data-callout>`.
241
+ *
242
+ * Example:
243
+ * ```tsx
244
+ * import { Info, AlertTriangle } from "lucide-react";
245
+ *
246
+ * <RichTextContent
247
+ * html={article.doc}
248
+ * calloutIcons={{
249
+ * info: <Info size={14} strokeWidth={2.4} />,
250
+ * warning: <AlertTriangle size={14} strokeWidth={2.4} />,
251
+ * }}
252
+ * />
253
+ * ```
254
+ */
255
+ calloutIcons?: Partial<Record<string, ReactNode>>;
232
256
  }
233
- declare function RichTextContent({ html, classMap, as, className, onReady, contentRef, }: RichTextContentProps): react.DOMElement<{
234
- ref: react.MutableRefObject<HTMLElement | null>;
235
- className: string | undefined;
236
- dangerouslySetInnerHTML: {
237
- __html: string;
238
- };
239
- }, HTMLElement>;
257
+ declare function RichTextContent({ html, classMap, as, className, onReady, contentRef, calloutIcons, }: RichTextContentProps): react_jsx_runtime.JSX.Element;
240
258
 
241
259
  /**
242
260
  * Heading extraction helpers used to build tables of contents (ToC) from
@@ -275,42 +293,4 @@ declare function extractHeadingsFromElement(root: HTMLElement, options?: Extract
275
293
  scrollMarginTop?: number;
276
294
  }): ExtractedHeading[];
277
295
 
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 };
296
+ export { AsteroidCMSProvider, type AsteroidCMSProviderProps, type ExtractHeadingsOptions, type ExtractedHeading, type HeadingLevel, RichTextContent, type UseCmsContentOptions, type UseCmsMutateOptions, extractHeadingsFromElement, extractHeadingsFromHtml, slugify, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate };
package/dist/client.js CHANGED
@@ -1,12 +1,12 @@
1
1
  "use client";
2
- import { createContext, useContext, useMemo, useRef, useEffect, createElement, useState } 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 = {}) {
@@ -823,6 +827,8 @@ function classKeyForTag(tag, attrs, openStack) {
823
827
  if (tag === "p" && "data-callout-title" in attrs) return "calloutTitle";
824
828
  if (tag === "code" && openStack[openStack.length - 1] !== "pre")
825
829
  return "inlineCode";
830
+ if (tag === "details") return "collapsible";
831
+ if (tag === "summary") return "collapsibleTitle";
826
832
  const known = [
827
833
  "p",
828
834
  "br",
@@ -989,8 +995,6 @@ function sanitizeAndStyle(html, options) {
989
995
  }
990
996
  return out.join("");
991
997
  }
992
-
993
- // src/components/RichTextContent.tsx
994
998
  var DEFAULT_CLASS_MAP = {
995
999
  variants: {
996
1000
  "figure:pullquote": "relative my-8",
@@ -1025,6 +1029,46 @@ function findQuoteBody(bq) {
1025
1029
  if (lastIdx < 0) return { first: null, last: null };
1026
1030
  return { first: children[0], last: children[lastIdx] };
1027
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
+ }
1028
1072
  function enhanceBlockquotes(root) {
1029
1073
  const quotes = root.querySelectorAll("blockquote");
1030
1074
  quotes.forEach((bq) => {
@@ -1254,6 +1298,28 @@ function enhanceCodeBlocks(root) {
1254
1298
  highlightCodeBlock(pre);
1255
1299
  });
1256
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
+ `;
1257
1323
  var CODEBLOCK_STYLE = `
1258
1324
  .rt-codeblock { position: relative; }
1259
1325
  .rt-codeblock[data-filename]:not([data-filename=""]) { padding-top: 2rem; }
@@ -1305,6 +1371,64 @@ var CODEBLOCK_STYLE = `
1305
1371
  }
1306
1372
  .rt-quote-open { margin-right: 0.15em; }
1307
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
+ }
1308
1432
  /* highlight.js theme handles .hljs-* color classes; we only override the
1309
1433
  default .hljs background so the per-block chrome (dark bg, terminal,
1310
1434
  diff red/green rows) wins. */
@@ -1446,17 +1570,29 @@ function ensureCodeBlockStyles() {
1446
1570
  }
1447
1571
  const tag = document.createElement("style");
1448
1572
  tag.id = "rt-codeblock-style";
1449
- tag.textContent = CODEBLOCK_STYLE;
1573
+ tag.textContent = HLJS_THEME + "\n" + CODEBLOCK_STYLE;
1450
1574
  document.head.appendChild(tag);
1451
1575
  styleInjected = true;
1452
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
+ }
1453
1588
  function RichTextContent({
1454
1589
  html,
1455
1590
  classMap,
1456
1591
  as = "div",
1457
1592
  className,
1458
1593
  onReady,
1459
- contentRef
1594
+ contentRef,
1595
+ calloutIcons
1460
1596
  }) {
1461
1597
  const merged = useMemo(
1462
1598
  () => mergeClassMap(DEFAULT_CLASS_MAP, classMap),
@@ -1467,6 +1603,7 @@ function RichTextContent({
1467
1603
  [html, merged]
1468
1604
  );
1469
1605
  const ref = useRef(null);
1606
+ const [chips, setChips] = useState([]);
1470
1607
  useEffect(() => {
1471
1608
  ensureCodeBlockStyles();
1472
1609
  const root = ref.current;
@@ -1476,6 +1613,8 @@ function RichTextContent({
1476
1613
  mo.disconnect();
1477
1614
  enhanceCodeBlocks(root);
1478
1615
  enhanceBlockquotes(root);
1616
+ const nextChips = enhanceCallouts(root);
1617
+ setChips((prev) => calloutChipsEqual(prev, nextChips) ? prev : nextChips);
1479
1618
  onReady?.(root);
1480
1619
  mo.observe(root, { childList: true, subtree: true });
1481
1620
  };
@@ -1493,11 +1632,20 @@ function RichTextContent({
1493
1632
  if (raf) cancelAnimationFrame(raf);
1494
1633
  };
1495
1634
  }, [safe, onReady, contentRef]);
1496
- return createElement(as, {
1497
- ref,
1498
- className,
1499
- dangerouslySetInnerHTML: { __html: safe }
1500
- });
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
+ ] });
1501
1649
  }
1502
1650
 
1503
1651
  // src/utils/extractHeadings.ts
@@ -1573,97 +1721,6 @@ function extractHeadingsFromElement(root, options = {}) {
1573
1721
  return out;
1574
1722
  }
1575
1723
 
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 };
1724
+ export { AsteroidCMSProvider, RichTextContent, extractHeadingsFromElement, extractHeadingsFromHtml, slugify, useAsteroidCMSConfig, useCmsContent, useCmsImage, useCmsMutate };
1668
1725
  //# sourceMappingURL=client.js.map
1669
1726
  //# sourceMappingURL=client.js.map