@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 +83 -70
- package/dist/client.cjs +161 -105
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +31 -51
- package/dist/client.d.ts +31 -51
- package/dist/client.js +164 -107
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +99 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -7
- package/dist/index.d.ts +41 -7
- package/dist/index.js +98 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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
|
|
431
|
+
Build a static ToC from HTML with `extractHeadingsFromHtml`, or extract headings from live DOM with `extractHeadingsFromElement`:
|
|
387
432
|
|
|
388
|
-
```
|
|
389
|
-
import {
|
|
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
|
-
|
|
404
|
-
|
|
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 {
|
|
441
|
+
import { extractHeadingsFromElement } from "@asteroidcms/core-utils/client";
|
|
431
442
|
|
|
432
|
-
const toc =
|
|
443
|
+
const toc = extractHeadingsFromElement(contentRef.current, { levels: [2, 3] });
|
|
433
444
|
```
|
|
434
445
|
|
|
435
|
-
|
|
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.
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
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
|