@aranova/tracking-next 0.19.2 → 0.20.1
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 +66 -1
- package/dist/blog-client.d.mts +69 -0
- package/dist/blog-client.d.ts +69 -0
- package/dist/blog-client.js +193 -0
- package/dist/blog-client.js.map +1 -0
- package/dist/blog-client.mjs +172 -0
- package/dist/blog-client.mjs.map +1 -0
- package/dist/blog-server.d.mts +248 -0
- package/dist/blog-server.d.ts +248 -0
- package/dist/blog-server.js +2303 -0
- package/dist/blog-server.js.map +1 -0
- package/dist/blog-server.mjs +2219 -0
- package/dist/blog-server.mjs.map +1 -0
- package/dist/blog.d.mts +2842 -0
- package/dist/blog.d.ts +2842 -0
- package/dist/blog.js +1043 -0
- package/dist/blog.js.map +1 -0
- package/dist/blog.mjs +965 -0
- package/dist/blog.mjs.map +1 -0
- package/dist/index.d.mts +29 -24
- package/dist/index.d.ts +29 -24
- package/dist/index.js +8 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8 -13
- package/dist/index.mjs.map +1 -1
- package/dist/middleware.js +6 -11
- package/dist/middleware.js.map +1 -1
- package/dist/middleware.mjs +6 -11
- package/dist/middleware.mjs.map +1 -1
- package/dist/phone.js +6 -11
- package/dist/phone.js.map +1 -1
- package/dist/phone.mjs +6 -11
- package/dist/phone.mjs.map +1 -1
- package/dist/server.js +6 -11
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +6 -11
- package/dist/server.mjs.map +1 -1
- package/dist/toc-C_OvH3ES.d.mts +23 -0
- package/dist/toc-C_OvH3ES.d.ts +23 -0
- package/package.json +37 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../tracking-core/src/resources/blog/view/client/toc-panel.tsx","../../tracking-core/src/resources/blog/view/class-names.ts","../../tracking-core/src/resources/blog/view/client/reading-progress.tsx"],"sourcesContent":["\"use client\";\n\n// Client-island table of contents — the animated companion to the zero-JS\n// `BlogTocShell`. Same anchor-id contract (`extractBlogHeadings` ↔ rehype-slug\n// parity), same theme vars, but with a spring-free collapsible body (the\n// grid-template-rows 0fr↔1fr trick) and an IntersectionObserver scroll-spy.\n//\n// SSR-safe: no window/document access at module scope or during render — the\n// observer attaches in an effect, so the island server-renders the FULL anchor\n// list (hydration-identical markup) and degrades to a plain in-page nav until\n// JS lands. Styled by the `.aranova-blog-toc-panel` section of the base CSS;\n// the sheet's global `prefers-reduced-motion: reduce` block disables the\n// open/close transition, and smooth scrolling checks the media query directly.\n\nimport {\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n type MouseEvent,\n type ReactNode,\n} from \"react\";\n\nimport type { BlogHeading } from \"../../toc\";\nimport { joinClassNames } from \"../class-names\";\n\nexport interface TocPanelProps {\n /** The post outline — see `extractBlogHeadings`. Empty array renders nothing. */\n headings: readonly BlogHeading[];\n /** Panel header label. Default `\"On this page\"`. */\n title?: string;\n /** Whether the panel body starts expanded. Default `true`. */\n defaultOpen?: boolean;\n /** Site-supplied class merged onto the fixed `.aranova-blog-toc-panel` class. */\n className?: string;\n /**\n * Collapse axis. `\"vertical\"` (default) folds the list up under the title;\n * `\"horizontal\"` folds it sideways toward `side`. Stamped as `data-collapse`\n * for the base CSS.\n */\n collapse?: \"vertical\" | \"horizontal\";\n /**\n * Which edge a `\"horizontal\"` collapse recedes toward — match this to the\n * rail's side so it folds into the page margin. Stamped as `data-side`.\n * Default `\"right\"`.\n */\n side?: \"left\" | \"right\";\n}\n\n/** Matches the scroll-spy's idea of \"current section\" to the sticky-header offset. */\nconst SPY_ROOT_MARGIN = \"-80px 0px -70% 0px\";\n\nfunction prefersReducedMotion(): boolean {\n // Defensive typeof: only ever called from event handlers (so window exists),\n // but matchMedia is missing in some embedded webviews and in jsdom.\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n\n/**\n * Collapsible, scroll-spying table of contents. Rendered into BlogPostView's\n * `aside` slot by the enhanced `BlogPost` wrapper in `@aranova/tracking-next`\n * (imported there via the `@aranova/tracking-next/blog-client` self-specifier\n * so this module's `\"use client\"` boundary survives bundling).\n */\nexport function TocPanel({\n headings,\n title = \"On this page\",\n defaultOpen = true,\n className,\n collapse = \"vertical\",\n side = \"right\",\n}: TocPanelProps): ReactNode {\n const [open, setOpen] = useState(defaultOpen);\n const [activeId, setActiveId] = useState<string | null>(null);\n const bodyId = useId();\n // Clicking a link sets the active id immediately; suppress the observer for\n // a beat so intermediate sections don't flicker through during the scroll.\n const clickedAt = useRef(0);\n // The user's explicit toggle always wins over the mobile auto-collapse below.\n const userToggled = useRef(false);\n\n // On NARROW viewports (below the desktop rail breakpoint) a long outline\n // rendered open pushes the article a full screen down — start collapsed\n // there. Runs in an effect (not render) so SSR/hydration markup stays\n // identical; guarded matchMedia for jsdom/old webviews.\n useEffect(() => {\n if (!defaultOpen || userToggled.current) return;\n if (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(max-width: 1099px)\").matches\n ) {\n setOpen(false);\n }\n }, [defaultOpen]);\n\n useEffect(() => {\n const targets = headings\n // oxlint-disable-next-line unicorn/prefer-query-selector -- slug ids need CSS-escaping for querySelector, and CSS.escape is missing in some webviews\n .map((h) => document.getElementById(h.id))\n .filter((el): el is HTMLElement => el !== null);\n if (targets.length === 0) return;\n\n const visible = new Set<string>();\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) visible.add(entry.target.id);\n else visible.delete(entry.target.id);\n }\n if (Date.now() - clickedAt.current < 700) return;\n // Active = the FIRST outline entry currently in the spy band; when the\n // band is empty (between sections), keep the previous active id.\n const next = headings.find((h) => visible.has(h.id));\n if (next) setActiveId(next.id);\n },\n { rootMargin: SPY_ROOT_MARGIN },\n );\n for (const el of targets) observer.observe(el);\n return () => observer.disconnect();\n }, [headings]);\n\n const onLinkClick = useCallback((event: MouseEvent<HTMLAnchorElement>, id: string) => {\n // oxlint-disable-next-line unicorn/prefer-query-selector -- see above: getElementById needs no CSS-escaping\n const el = document.getElementById(id);\n if (!el) return; // fall through to the browser's default anchor jump\n event.preventDefault();\n clickedAt.current = Date.now();\n setActiveId(id);\n el.scrollIntoView({ behavior: prefersReducedMotion() ? \"auto\" : \"smooth\" });\n history.replaceState(null, \"\", `#${id}`);\n }, []);\n\n if (headings.length === 0) return null;\n\n return (\n <div\n className={joinClassNames(\"aranova-blog-toc-panel\", className)}\n data-open={open ? \"true\" : \"false\"}\n data-collapse={collapse}\n data-side={side}\n >\n <button\n type=\"button\"\n className=\"aranova-blog-toc-panel-title\"\n aria-expanded={open}\n aria-controls={bodyId}\n onClick={() => {\n userToggled.current = true;\n setOpen((o) => !o);\n }}\n >\n {title}\n </button>\n {/* 0fr↔1fr grid wrapper: the inner div clips, the row track animates. */}\n <div id={bodyId} className=\"aranova-blog-toc-panel-body\">\n <div className=\"aranova-blog-toc-panel-clip\">\n <nav aria-label=\"Table of contents\">\n <ol>\n {headings.map((heading) => (\n <li key={heading.id} data-depth={heading.depth}>\n <a\n href={`#${heading.id}`}\n title={heading.text}\n className={\n heading.id === activeId\n ? \"aranova-blog-toc-panel-link is-active\"\n : \"aranova-blog-toc-panel-link\"\n }\n data-active={heading.id === activeId ? \"true\" : undefined}\n aria-current={heading.id === activeId ? \"location\" : undefined}\n onClick={(event) => onLinkClick(event, heading.id)}\n >\n {heading.text}\n </a>\n </li>\n ))}\n </ol>\n </nav>\n </div>\n </div>\n </div>\n );\n}\n","// Client-side className overrides for the blog view — the escape hatch for\n// sites that want to hook a Tailwind/utility class onto a specific surface\n// without replacing the whole component (see `components` for that) or\n// writing global CSS against the fixed `.aranova-blog-*` selectors (still the\n// simplest option and requires no API — these classes are stable and public).\n//\n// `joinClassNames` is a plain append, not a Tailwind-merge: every base class\n// here is an SDK-internal selector (`aranova-blog-callout`, …), never a\n// utility class, so there is nothing for an override to conflict with —\n// dropping a `tailwind-merge` dependency into a framework-free package for a\n// non-conflict is unnecessary weight.\nexport function joinClassNames(base: string, extra?: string): string {\n return extra ? `${base} ${extra}` : base;\n}\n\n/**\n * One optional override per styleable surface. Every key merges onto its\n * fixed base class (never replaces it), so the `.aranova-blog-*` selector\n * still applies — this is additive, not a restyle mechanism.\n */\nexport interface BlogClassNameOverrides {\n /** The `<article>` root. */\n article?: string;\n /** The content/aside grid wrapper. */\n layout?: string;\n /** The content column. */\n content?: string;\n /** The aside rail (table of contents). */\n aside?: string;\n /** `<Callout>` MDX component. */\n callout?: string;\n /** `<Cta>` MDX component. */\n cta?: string;\n /** `<BlogImage>` MDX component's `<figure>` wrapper. */\n figure?: string;\n /** `<FaqItem>` MDX component's `<details>` wrapper. */\n faqItem?: string;\n /** `<FaqItem>`'s `<summary>`. */\n faqQuestion?: string;\n /** `<FaqItem>`'s answer `<div>`. */\n faqAnswer?: string;\n /** `<KeyTakeaways>` MDX component. */\n keyTakeaways?: string;\n /** The `<table>` overflow wrapper `<div>`. */\n tableWrap?: string;\n /** `BlogPostMeta` byline row. */\n meta?: string;\n /** `BlogBreadcrumbs` nav. */\n breadcrumbs?: string;\n /** `BlogTocShell` / the animated `TocPanel` island. */\n toc?: string;\n /** The `ReadingProgress` island. */\n progress?: string;\n}\n","\"use client\";\n\n// Client-island reading-progress bar: a fixed strip along the top of the\n// viewport that fills (scaleX) with the scrolled fraction of the article.\n//\n// NOT hidden under prefers-reduced-motion — the bar communicates position, it\n// is not decorative motion — but it carries no transitions: the fill tracks\n// the scroll position directly via rAF-throttled listeners (the sheet's\n// reduced-motion block has nothing to disable here). SSR-safe: renders at 0%\n// and only touches window/document inside the effect. Styled by the\n// `.aranova-blog-progress` section of the base CSS (primary color var on a\n// transparent track).\n\nimport { useEffect, useRef, type CSSProperties, type ReactNode } from \"react\";\n\nimport { joinClassNames } from \"../class-names\";\n\nexport interface ReadingProgressProps {\n /** CSS selector for the element whose read-through is measured. Default `\".aranova-blog-post\"`. */\n target?: string;\n /** Bar thickness in px. Default `3`. */\n height?: number;\n /** Site-supplied class merged onto the fixed `.aranova-blog-progress` class. */\n className?: string;\n /**\n * Called on every scroll/resize frame with the read-through fraction (0–1)\n * and its rounded 0–100 percent. Lets a site drive its OWN progress UI (a\n * header meter, a percent readout) off the same measurement. Fires once on\n * mount with the initial position. Latest callback is always used — a new\n * function identity each render does not re-subscribe the listeners.\n */\n onProgress?: (fraction: number, percent: number) => void;\n /**\n * Render the built-in bar. Default `true`. Set `false` for a headless meter:\n * the island still measures scroll and fires `onProgress`, but paints no bar\n * (the site owns the visuals and their accessibility).\n */\n showBar?: boolean;\n}\n\n/**\n * Fixed top-of-viewport reading progress bar (`role=\"progressbar\"`, valuenow\n * 0–100). Rendered into BlogPostView's `header` slot by the enhanced\n * `BlogPost` wrapper in `@aranova/tracking-next` when `showProgress` is set.\n *\n * For a site-owned indicator, render this island directly (it is already a\n * client component) with `onProgress` — optionally `showBar={false}` to drop\n * the default bar and paint your own UI from the callback. The `BlogPost`\n * server wrapper cannot forward a function across the RSC boundary, so the\n * callback lives here, not on `showProgress`.\n */\nexport function ReadingProgress({\n target = \".aranova-blog-post\",\n height = 3,\n className,\n onProgress,\n showBar = true,\n}: ReadingProgressProps): ReactNode {\n const barRef = useRef<HTMLDivElement | null>(null);\n // Hold the latest callback so a changing function identity does not tear down\n // and re-attach the scroll listeners every render.\n const onProgressRef = useRef(onProgress);\n onProgressRef.current = onProgress;\n\n useEffect(() => {\n const article = document.querySelector(target);\n if (!(article instanceof HTMLElement)) return;\n\n let frame = 0;\n const update = (): void => {\n frame = 0;\n const rect = article.getBoundingClientRect();\n // Fraction of the article scrolled past the viewport: 0 while its top is\n // on-screen, 1 once its bottom clears the viewport. Articles shorter\n // than the viewport read as fully-read the moment they are visible.\n const total = rect.height - window.innerHeight;\n const fraction = total <= 0 ? 1 : Math.min(1, Math.max(0, -rect.top / total));\n const percent = Math.round(fraction * 100);\n const bar = barRef.current;\n if (bar) {\n bar.style.transform = `scaleX(${fraction})`;\n bar.setAttribute(\"aria-valuenow\", String(percent));\n }\n onProgressRef.current?.(fraction, percent);\n };\n const schedule = (): void => {\n if (frame === 0) frame = requestAnimationFrame(update);\n };\n\n update();\n window.addEventListener(\"scroll\", schedule, { passive: true });\n window.addEventListener(\"resize\", schedule);\n return () => {\n window.removeEventListener(\"scroll\", schedule);\n window.removeEventListener(\"resize\", schedule);\n if (frame !== 0) cancelAnimationFrame(frame);\n };\n }, [target]);\n\n if (!showBar) return null;\n\n const style: CSSProperties = { height: `${height}px` };\n return (\n <div\n ref={barRef}\n className={joinClassNames(\"aranova-blog-progress\", className)}\n role=\"progressbar\"\n aria-label=\"Reading progress\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={0}\n style={style}\n />\n );\n}\n"],"mappings":"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","names":["useEffect","useRef","jsx","useRef","useEffect"]}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { BlogImage, BlogClassNameOverrides, BlogTheme, BlogClient, BlogThemeOverride } from './blog.mjs';
|
|
2
|
+
export { BASE_BLOG_THEME, BLOG_BREAKPOINTS, BLOG_BREAKPOINT_MIN_WIDTHS_PX, BLOG_MDX_ALLOWED_COMPONENTS, BLOG_MDX_COMPONENT_GRAMMAR, BLOG_MDX_MAX_DEPTH, BLOG_MDX_MAX_NODES, BLOG_MDX_MAX_SIZE, BLOG_THEME_PRESETS, BlogBreakpoint, BlogBreakpointMap, BlogClientConfig, BlogFaqPair, BlogFontSources, BlogHeroImage, BlogJsonLdOptions, BlogManifest, BlogManifestEntry, BlogMdxComponentGrammarEntry, BlogMdxComponentName, BlogMdxValidateOptions, BlogMdxValidationResult, BlogMdxViolation, BlogPostContent, BlogPostMetadata, BlogPostMetadataImage, BlogSeo, BlogSeoOptions, BlogSitemapEntry, BlogThemeColors, BlogThemePresetName, BlogThemeSpacing, BlogThemeTypography, NextFetchInit, ResponsiveValue, blogHeroImageSchema, blogImageSchema, blogManifestEntrySchema, blogManifestSchema, blogManifestWireSchema, blogPostContentSchema, blogPostContentWireSchema, blogSeoSchema, blogThemeColorsSchema, blogThemeOverrideSchema, blogThemeSchema, blogThemeSpacingSchema, blogThemeToCssVars, blogThemeTypographySchema, buildBlogPostMetadata, buildBlogPostingJsonLd, buildBlogSitemapEntries, buildFaqJsonLd, createBlogClient, createBlogFontTheme, extractFaqPairs, joinClassNames, nullishField, parseBlogManifest, parseBlogPostContent, resolveBlogTheme, responsive, sanitizeBlogTheme, serializeJsonLd, toBlogBreakpointMap, validateBlogMdx } from './blog.mjs';
|
|
3
|
+
import { B as BlogHeading } from './toc-C_OvH3ES.mjs';
|
|
4
|
+
export { e as estimateReadingTime, a as extractBlogHeadings } from './toc-C_OvH3ES.mjs';
|
|
5
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
6
|
+
import 'zod';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The CLOSED component interface the MDX renderer accepts. Generated MDX may
|
|
10
|
+
* only reference these elements/components; anything else fails MDX rendering
|
|
11
|
+
* (caught by the view's fallback). Callers may override any subset via the
|
|
12
|
+
* `components` prop on `BlogPost` / `BlogPostView`.
|
|
13
|
+
*/
|
|
14
|
+
interface BlogComponents {
|
|
15
|
+
h1: (props: ComponentPropsWithoutRef<"h1">) => ReactNode;
|
|
16
|
+
h2: (props: ComponentPropsWithoutRef<"h2">) => ReactNode;
|
|
17
|
+
h3: (props: ComponentPropsWithoutRef<"h3">) => ReactNode;
|
|
18
|
+
h4: (props: ComponentPropsWithoutRef<"h4">) => ReactNode;
|
|
19
|
+
h5: (props: ComponentPropsWithoutRef<"h5">) => ReactNode;
|
|
20
|
+
h6: (props: ComponentPropsWithoutRef<"h6">) => ReactNode;
|
|
21
|
+
p: (props: ComponentPropsWithoutRef<"p">) => ReactNode;
|
|
22
|
+
a: (props: ComponentPropsWithoutRef<"a">) => ReactNode;
|
|
23
|
+
img: (props: ComponentPropsWithoutRef<"img">) => ReactNode;
|
|
24
|
+
pre: (props: ComponentPropsWithoutRef<"pre">) => ReactNode;
|
|
25
|
+
code: (props: ComponentPropsWithoutRef<"code">) => ReactNode;
|
|
26
|
+
blockquote: (props: ComponentPropsWithoutRef<"blockquote">) => ReactNode;
|
|
27
|
+
ul: (props: ComponentPropsWithoutRef<"ul">) => ReactNode;
|
|
28
|
+
ol: (props: ComponentPropsWithoutRef<"ol">) => ReactNode;
|
|
29
|
+
li: (props: ComponentPropsWithoutRef<"li">) => ReactNode;
|
|
30
|
+
table: (props: ComponentPropsWithoutRef<"table">) => ReactNode;
|
|
31
|
+
hr: (props: ComponentPropsWithoutRef<"hr">) => ReactNode;
|
|
32
|
+
/** Styled aside for tips/notes: `<Callout>…</Callout>` in MDX. */
|
|
33
|
+
Callout: (props: {
|
|
34
|
+
children?: ReactNode;
|
|
35
|
+
}) => ReactNode;
|
|
36
|
+
/** Primary-colored button-styled anchor: `<Cta href label/>` in MDX. */
|
|
37
|
+
Cta: (props: {
|
|
38
|
+
href: string;
|
|
39
|
+
label: string;
|
|
40
|
+
}) => ReactNode;
|
|
41
|
+
/** Renders an image from the post's image records by id: `<BlogImage id/>`. */
|
|
42
|
+
BlogImage: (props: {
|
|
43
|
+
id: string;
|
|
44
|
+
}) => ReactNode;
|
|
45
|
+
/** One FAQ entry (question heading + prose answer children): `<FaqItem question>…</FaqItem>`. */
|
|
46
|
+
FaqItem: (props: {
|
|
47
|
+
question: string;
|
|
48
|
+
children?: ReactNode;
|
|
49
|
+
}) => ReactNode;
|
|
50
|
+
/** Styled TL;DR box holding a markdown bullet list: `<KeyTakeaways>…</KeyTakeaways>`. */
|
|
51
|
+
KeyTakeaways: (props: {
|
|
52
|
+
children?: ReactNode;
|
|
53
|
+
}) => ReactNode;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Default renderers. `images` is closed over so `<BlogImage id/>` in MDX can
|
|
57
|
+
* resolve the post's image records without threading props through MDX.
|
|
58
|
+
*
|
|
59
|
+
* Images render as plain `<img>` (with intrinsic width/height + lazy loading),
|
|
60
|
+
* NOT `next/image` — tracking-core is framework-free, and the CDN host would
|
|
61
|
+
* otherwise need to be whitelisted in every consumer's `next.config`. A Next
|
|
62
|
+
* site wanting `next/image` can override `img`/`BlogImage` via `components`.
|
|
63
|
+
*
|
|
64
|
+
* `classNames` merges a site-supplied class onto the fixed base class of each
|
|
65
|
+
* styleable component (see {@link BlogClassNameOverrides}) — additive, never
|
|
66
|
+
* a replacement of the `.aranova-blog-*` selector.
|
|
67
|
+
*/
|
|
68
|
+
declare function createDefaultComponents(images: BlogImage[], classNames?: BlogClassNameOverrides): BlogComponents;
|
|
69
|
+
|
|
70
|
+
interface BlogPostViewProps {
|
|
71
|
+
/** Raw MDX source (the `mdx` field of the post content JSON). */
|
|
72
|
+
mdx: string;
|
|
73
|
+
/** A fully-RESOLVED theme (see `resolveBlogTheme`). Default: `BASE_BLOG_THEME`. */
|
|
74
|
+
theme?: BlogTheme;
|
|
75
|
+
/** Overrides merged over the default component set. */
|
|
76
|
+
components?: Partial<BlogComponents>;
|
|
77
|
+
/** Article chrome rendered above the content/aside layout (breadcrumbs, meta, …). */
|
|
78
|
+
header?: ReactNode;
|
|
79
|
+
/** Sidebar chrome (e.g. `BlogTocShell`); omitted entirely when not provided. */
|
|
80
|
+
aside?: ReactNode;
|
|
81
|
+
/** Entrance-motion opt-in, stamped as `data-motion` for the base CSS. Default `"none"`. */
|
|
82
|
+
motion?: "none" | "subtle";
|
|
83
|
+
/** Site-supplied classes merged onto the fixed base classes (additive, per surface). */
|
|
84
|
+
classNames?: BlogClassNameOverrides;
|
|
85
|
+
/**
|
|
86
|
+
* Reject content before the H1 (frontmatter, a stray metadata block, prose)
|
|
87
|
+
* — only meaningful for a whole post. Default `true`; direct-fragment
|
|
88
|
+
* callers (component-grammar tests) pass `false`.
|
|
89
|
+
*/
|
|
90
|
+
requireLeadingH1?: boolean;
|
|
91
|
+
/** Which side the aside rail sits on at desktop width. Default `"right"`. */
|
|
92
|
+
tocSide?: "left" | "right";
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Renders MDX inside a themed `<article class="aranova-blog-post">`. The theme
|
|
96
|
+
* flows in as sanitized `--ara-blog-*` CSS variables on the article element;
|
|
97
|
+
* `BLOG_BASE_CSS` (a static constant — nothing interpolated) consumes them.
|
|
98
|
+
*
|
|
99
|
+
* Structure: `{header}` chrome first, then a `.aranova-blog-layout` holding the
|
|
100
|
+
* `.aranova-blog-content` column and (when provided) an `.aranova-blog-aside`
|
|
101
|
+
* rail — the base CSS turns the layout into a sticky-aside grid on wide
|
|
102
|
+
* viewports and keeps it a plain block on mobile.
|
|
103
|
+
*
|
|
104
|
+
* MDX is compiled via `compileMDX` — the awaitable form of `<MDXRemote/>` from
|
|
105
|
+
* `next-mdx-remote/rsc` — because an element-level `<MDXRemote/>` compiles
|
|
106
|
+
* during React's render, where a malformed document would throw past any
|
|
107
|
+
* try/catch here. `rehype-slug` stamps GitHub-style anchor ids on every
|
|
108
|
+
* heading (slug parity with `extractBlogHeadings` in ../toc.ts). On compile
|
|
109
|
+
* failure a typed fallback article renders instead; this component never
|
|
110
|
+
* throws into the host page.
|
|
111
|
+
*/
|
|
112
|
+
declare function BlogPostView({ mdx, theme, components, header, aside, motion, classNames, requireLeadingH1, tocSide, }: BlogPostViewProps): Promise<ReactNode>;
|
|
113
|
+
|
|
114
|
+
interface BlogBreadcrumbItem {
|
|
115
|
+
label: string;
|
|
116
|
+
href: string;
|
|
117
|
+
}
|
|
118
|
+
interface BlogBreadcrumbsProps {
|
|
119
|
+
/** Ancestor links, outermost first (e.g. Home → Blog). */
|
|
120
|
+
items: readonly BlogBreadcrumbItem[];
|
|
121
|
+
/** The current page (usually the post title) — rendered as text, not a link. */
|
|
122
|
+
currentLabel: string;
|
|
123
|
+
/** Site-supplied class merged onto the fixed `.aranova-blog-breadcrumbs` class. */
|
|
124
|
+
className?: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Accessible breadcrumb trail: ancestor links separated by a decorative `/`,
|
|
128
|
+
* ending in the current page as `<span aria-current="page">`. Styled by the
|
|
129
|
+
* `.aranova-blog-breadcrumbs` section of the base CSS.
|
|
130
|
+
*/
|
|
131
|
+
declare function BlogBreadcrumbs({ items, currentLabel, className, }: BlogBreadcrumbsProps): ReactNode;
|
|
132
|
+
|
|
133
|
+
interface BlogPostProps$1 {
|
|
134
|
+
slug: string;
|
|
135
|
+
client: BlogClient;
|
|
136
|
+
/** Client-registered default theme layer (sits between the base theme and per-post overrides). */
|
|
137
|
+
defaultTheme?: BlogThemeOverride;
|
|
138
|
+
/** Overrides merged over the default component set. */
|
|
139
|
+
components?: Partial<BlogComponents>;
|
|
140
|
+
/** Rendered when the slug is unknown/unfetchable. Default: a minimal not-found article. */
|
|
141
|
+
notFound?: ReactNode;
|
|
142
|
+
/** Render the byline row (published/updated dates + reading time). Default `false`. */
|
|
143
|
+
showMeta?: boolean;
|
|
144
|
+
/** Render the sticky table-of-contents rail (omitted when the post has no h2/h3). Default `false`. */
|
|
145
|
+
showToc?: boolean;
|
|
146
|
+
/** Which side the ToC rail sits on at desktop width. Default `"right"`. */
|
|
147
|
+
tocSide?: "left" | "right";
|
|
148
|
+
/** Render the breadcrumb trail (requires `breadcrumbs`). Default `false`. */
|
|
149
|
+
showBreadcrumbs?: boolean;
|
|
150
|
+
/** Ancestor links for the breadcrumb trail, outermost first (e.g. Home → Blog). */
|
|
151
|
+
breadcrumbs?: readonly BlogBreadcrumbItem[];
|
|
152
|
+
/** BCP-47 locale for the byline dates. Default `"en-CA"`. */
|
|
153
|
+
locale?: string;
|
|
154
|
+
/**
|
|
155
|
+
* Component-injection slot for the table of contents: when provided (and
|
|
156
|
+
* `showToc` finds an outline), it renders INSTEAD of the zero-JS
|
|
157
|
+
* `BlogTocShell` default. This is how `@aranova/tracking-next`'s enhanced
|
|
158
|
+
* `BlogPost` wires in the animated `TocPanel` client island — tracking-core
|
|
159
|
+
* cannot import it back from tracking-next (core is the dependency), and the
|
|
160
|
+
* island must reach server bundles only via the
|
|
161
|
+
* `@aranova/tracking-next/blog-client` self-specifier so its `"use client"`
|
|
162
|
+
* boundary survives `splitting: false` bundling.
|
|
163
|
+
*/
|
|
164
|
+
tocSlot?: (headings: readonly BlogHeading[]) => ReactNode;
|
|
165
|
+
/**
|
|
166
|
+
* Component-injection slot rendered at the very top of the header chrome
|
|
167
|
+
* (before breadcrumbs/byline). Same rationale as `tocSlot` — the enhanced
|
|
168
|
+
* wrapper injects the `ReadingProgress` island here under `showProgress`.
|
|
169
|
+
*/
|
|
170
|
+
progressSlot?: () => ReactNode;
|
|
171
|
+
/**
|
|
172
|
+
* Site-supplied classes merged onto the fixed base class of each styleable
|
|
173
|
+
* surface (article/layout/MDX components/chrome) — additive, never a
|
|
174
|
+
* replacement of the `.aranova-blog-*` selector. For a full component
|
|
175
|
+
* rewrite instead, use `components`.
|
|
176
|
+
*/
|
|
177
|
+
classNames?: BlogClassNameOverrides;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Static, author-controlled stylesheet for the blog post view. Scoped under
|
|
182
|
+
* `.aranova-blog-post` and driven entirely by the `--ara-blog-*` CSS custom
|
|
183
|
+
* properties emitted by `blogThemeToCssVars`.
|
|
184
|
+
*
|
|
185
|
+
* SECURITY: this string is a compile-time constant — NOTHING from post content
|
|
186
|
+
* or theme data is ever interpolated into it. All per-post variation flows
|
|
187
|
+
* through the sanitized CSS variables on the `<article>` element.
|
|
188
|
+
*/
|
|
189
|
+
declare const BLOG_BASE_CSS = "/* Aranova blog post base stylesheet \u2014 authored source of truth.\n *\n * Scoped under `.aranova-blog-post` and driven entirely by the sanitized\n * `--ara-blog-*` CSS custom properties emitted by `blogThemeToCssVars` on the\n * article element. Host sites usually run Tailwind preflight, so:\n *\n * - element rules use `.aranova-blog-post :where(el)` \u2014 one class of\n * specificity (0,1,0), enough to beat preflight's bare element resets\n * while every rule in this sheet stays flat and order-resolvable;\n * - component classes (`.aranova-blog-callout`, \u2026) keep their natural\n * class-on-class specificity.\n *\n * Responsive-capable tokens (h1/h2/h3/h4/body/small sizes, body line-height,\n * content max-width, block gap, section gap, radius) are NEVER consumed via their raw\n * `--ara-blog-*` vars here \u2014 the generated block below re-points internal\n * `--_*` vars per breakpoint, and all rules consume those.\n *\n * Rhythm rules near the end target BOTH `.aranova-blog-post` (content is a\n * direct child of the article today) and `.aranova-blog-content` (a later\n * phase wraps content in that div) so that phase needs no CSS changes.\n *\n * No raw colors anywhere: every color flows through the theme vars, which\n * already carry `light-dark(...)` values when a dark palette is present.\n */\n\n/* Responsive token plumbing \u2014 GENERATED by build-styles.ts from theme.ts\n (BLOG_BREAKPOINT_MIN_WIDTHS_PX + the responsive() token set). Mobile-first:\n each breakpoint falls back through every narrower key down to the base var.\n Authored rules consume ONLY the --_* internal vars for these tokens. */\n.aranova-blog-post {\n --_h1-size: var(--ara-blog-type-h1-size);\n --_h2-size: var(--ara-blog-type-h2-size);\n --_h3-size: var(--ara-blog-type-h3-size);\n --_h4-size: var(--ara-blog-type-h4-size);\n --_body-size: var(--ara-blog-type-body-size);\n --_body-line-height: var(--ara-blog-type-body-line-height);\n --_small-size: var(--ara-blog-type-small-size);\n --_content-max-width: var(--ara-blog-space-content-max-width);\n --_block-gap: var(--ara-blog-space-block-gap);\n --_section-gap: var(--ara-blog-space-section-gap);\n --_radius: var(--ara-blog-radius);\n}\n@media (min-width: 640px) {\n .aranova-blog-post {\n --_h1-size: var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size));\n --_h2-size: var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size));\n --_h3-size: var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size));\n --_h4-size: var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size));\n --_body-size: var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size));\n --_body-line-height: var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height));\n --_small-size: var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size));\n --_content-max-width: var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width));\n --_block-gap: var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap));\n --_section-gap: var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap));\n --_radius: var(--ara-blog-radius-sm, var(--ara-blog-radius));\n }\n}\n@media (min-width: 768px) {\n .aranova-blog-post {\n --_h1-size: var(--ara-blog-type-h1-size-md, var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size)));\n --_h2-size: var(--ara-blog-type-h2-size-md, var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size)));\n --_h3-size: var(--ara-blog-type-h3-size-md, var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size)));\n --_h4-size: var(--ara-blog-type-h4-size-md, var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size)));\n --_body-size: var(--ara-blog-type-body-size-md, var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size)));\n --_body-line-height: var(--ara-blog-type-body-line-height-md, var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height)));\n --_small-size: var(--ara-blog-type-small-size-md, var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size)));\n --_content-max-width: var(--ara-blog-space-content-max-width-md, var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width)));\n --_block-gap: var(--ara-blog-space-block-gap-md, var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap)));\n --_section-gap: var(--ara-blog-space-section-gap-md, var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap)));\n --_radius: var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius)));\n }\n}\n@media (min-width: 1024px) {\n .aranova-blog-post {\n --_h1-size: var(--ara-blog-type-h1-size-lg, var(--ara-blog-type-h1-size-md, var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size))));\n --_h2-size: var(--ara-blog-type-h2-size-lg, var(--ara-blog-type-h2-size-md, var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size))));\n --_h3-size: var(--ara-blog-type-h3-size-lg, var(--ara-blog-type-h3-size-md, var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size))));\n --_h4-size: var(--ara-blog-type-h4-size-lg, var(--ara-blog-type-h4-size-md, var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size))));\n --_body-size: var(--ara-blog-type-body-size-lg, var(--ara-blog-type-body-size-md, var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size))));\n --_body-line-height: var(--ara-blog-type-body-line-height-lg, var(--ara-blog-type-body-line-height-md, var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height))));\n --_small-size: var(--ara-blog-type-small-size-lg, var(--ara-blog-type-small-size-md, var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size))));\n --_content-max-width: var(--ara-blog-space-content-max-width-lg, var(--ara-blog-space-content-max-width-md, var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width))));\n --_block-gap: var(--ara-blog-space-block-gap-lg, var(--ara-blog-space-block-gap-md, var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap))));\n --_section-gap: var(--ara-blog-space-section-gap-lg, var(--ara-blog-space-section-gap-md, var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap))));\n --_radius: var(--ara-blog-radius-lg, var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius))));\n }\n}\n@media (min-width: 1280px) {\n .aranova-blog-post {\n --_h1-size: var(--ara-blog-type-h1-size-xl, var(--ara-blog-type-h1-size-lg, var(--ara-blog-type-h1-size-md, var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size)))));\n --_h2-size: var(--ara-blog-type-h2-size-xl, var(--ara-blog-type-h2-size-lg, var(--ara-blog-type-h2-size-md, var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size)))));\n --_h3-size: var(--ara-blog-type-h3-size-xl, var(--ara-blog-type-h3-size-lg, var(--ara-blog-type-h3-size-md, var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size)))));\n --_h4-size: var(--ara-blog-type-h4-size-xl, var(--ara-blog-type-h4-size-lg, var(--ara-blog-type-h4-size-md, var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size)))));\n --_body-size: var(--ara-blog-type-body-size-xl, var(--ara-blog-type-body-size-lg, var(--ara-blog-type-body-size-md, var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size)))));\n --_body-line-height: var(--ara-blog-type-body-line-height-xl, var(--ara-blog-type-body-line-height-lg, var(--ara-blog-type-body-line-height-md, var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height)))));\n --_small-size: var(--ara-blog-type-small-size-xl, var(--ara-blog-type-small-size-lg, var(--ara-blog-type-small-size-md, var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size)))));\n --_content-max-width: var(--ara-blog-space-content-max-width-xl, var(--ara-blog-space-content-max-width-lg, var(--ara-blog-space-content-max-width-md, var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width)))));\n --_block-gap: var(--ara-blog-space-block-gap-xl, var(--ara-blog-space-block-gap-lg, var(--ara-blog-space-block-gap-md, var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap)))));\n --_section-gap: var(--ara-blog-space-section-gap-xl, var(--ara-blog-space-section-gap-lg, var(--ara-blog-space-section-gap-md, var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap)))));\n --_radius: var(--ara-blog-radius-xl, var(--ara-blog-radius-lg, var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius)))));\n }\n}\n@media (min-width: 1536px) {\n .aranova-blog-post {\n --_h1-size: var(--ara-blog-type-h1-size-2xl, var(--ara-blog-type-h1-size-xl, var(--ara-blog-type-h1-size-lg, var(--ara-blog-type-h1-size-md, var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size))))));\n --_h2-size: var(--ara-blog-type-h2-size-2xl, var(--ara-blog-type-h2-size-xl, var(--ara-blog-type-h2-size-lg, var(--ara-blog-type-h2-size-md, var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size))))));\n --_h3-size: var(--ara-blog-type-h3-size-2xl, var(--ara-blog-type-h3-size-xl, var(--ara-blog-type-h3-size-lg, var(--ara-blog-type-h3-size-md, var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size))))));\n --_h4-size: var(--ara-blog-type-h4-size-2xl, var(--ara-blog-type-h4-size-xl, var(--ara-blog-type-h4-size-lg, var(--ara-blog-type-h4-size-md, var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size))))));\n --_body-size: var(--ara-blog-type-body-size-2xl, var(--ara-blog-type-body-size-xl, var(--ara-blog-type-body-size-lg, var(--ara-blog-type-body-size-md, var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size))))));\n --_body-line-height: var(--ara-blog-type-body-line-height-2xl, var(--ara-blog-type-body-line-height-xl, var(--ara-blog-type-body-line-height-lg, var(--ara-blog-type-body-line-height-md, var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height))))));\n --_small-size: var(--ara-blog-type-small-size-2xl, var(--ara-blog-type-small-size-xl, var(--ara-blog-type-small-size-lg, var(--ara-blog-type-small-size-md, var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size))))));\n --_content-max-width: var(--ara-blog-space-content-max-width-2xl, var(--ara-blog-space-content-max-width-xl, var(--ara-blog-space-content-max-width-lg, var(--ara-blog-space-content-max-width-md, var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width))))));\n --_block-gap: var(--ara-blog-space-block-gap-2xl, var(--ara-blog-space-block-gap-xl, var(--ara-blog-space-block-gap-lg, var(--ara-blog-space-block-gap-md, var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap))))));\n --_section-gap: var(--ara-blog-space-section-gap-2xl, var(--ara-blog-space-section-gap-xl, var(--ara-blog-space-section-gap-lg, var(--ara-blog-space-section-gap-md, var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap))))));\n --_radius: var(--ara-blog-radius-2xl, var(--ara-blog-radius-xl, var(--ara-blog-radius-lg, var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius))))));\n }\n}\n@media (min-width: 1920px) {\n .aranova-blog-post {\n --_h1-size: var(--ara-blog-type-h1-size-3xl, var(--ara-blog-type-h1-size-2xl, var(--ara-blog-type-h1-size-xl, var(--ara-blog-type-h1-size-lg, var(--ara-blog-type-h1-size-md, var(--ara-blog-type-h1-size-sm, var(--ara-blog-type-h1-size)))))));\n --_h2-size: var(--ara-blog-type-h2-size-3xl, var(--ara-blog-type-h2-size-2xl, var(--ara-blog-type-h2-size-xl, var(--ara-blog-type-h2-size-lg, var(--ara-blog-type-h2-size-md, var(--ara-blog-type-h2-size-sm, var(--ara-blog-type-h2-size)))))));\n --_h3-size: var(--ara-blog-type-h3-size-3xl, var(--ara-blog-type-h3-size-2xl, var(--ara-blog-type-h3-size-xl, var(--ara-blog-type-h3-size-lg, var(--ara-blog-type-h3-size-md, var(--ara-blog-type-h3-size-sm, var(--ara-blog-type-h3-size)))))));\n --_h4-size: var(--ara-blog-type-h4-size-3xl, var(--ara-blog-type-h4-size-2xl, var(--ara-blog-type-h4-size-xl, var(--ara-blog-type-h4-size-lg, var(--ara-blog-type-h4-size-md, var(--ara-blog-type-h4-size-sm, var(--ara-blog-type-h4-size)))))));\n --_body-size: var(--ara-blog-type-body-size-3xl, var(--ara-blog-type-body-size-2xl, var(--ara-blog-type-body-size-xl, var(--ara-blog-type-body-size-lg, var(--ara-blog-type-body-size-md, var(--ara-blog-type-body-size-sm, var(--ara-blog-type-body-size)))))));\n --_body-line-height: var(--ara-blog-type-body-line-height-3xl, var(--ara-blog-type-body-line-height-2xl, var(--ara-blog-type-body-line-height-xl, var(--ara-blog-type-body-line-height-lg, var(--ara-blog-type-body-line-height-md, var(--ara-blog-type-body-line-height-sm, var(--ara-blog-type-body-line-height)))))));\n --_small-size: var(--ara-blog-type-small-size-3xl, var(--ara-blog-type-small-size-2xl, var(--ara-blog-type-small-size-xl, var(--ara-blog-type-small-size-lg, var(--ara-blog-type-small-size-md, var(--ara-blog-type-small-size-sm, var(--ara-blog-type-small-size)))))));\n --_content-max-width: var(--ara-blog-space-content-max-width-3xl, var(--ara-blog-space-content-max-width-2xl, var(--ara-blog-space-content-max-width-xl, var(--ara-blog-space-content-max-width-lg, var(--ara-blog-space-content-max-width-md, var(--ara-blog-space-content-max-width-sm, var(--ara-blog-space-content-max-width)))))));\n --_block-gap: var(--ara-blog-space-block-gap-3xl, var(--ara-blog-space-block-gap-2xl, var(--ara-blog-space-block-gap-xl, var(--ara-blog-space-block-gap-lg, var(--ara-blog-space-block-gap-md, var(--ara-blog-space-block-gap-sm, var(--ara-blog-space-block-gap)))))));\n --_section-gap: var(--ara-blog-space-section-gap-3xl, var(--ara-blog-space-section-gap-2xl, var(--ara-blog-space-section-gap-xl, var(--ara-blog-space-section-gap-lg, var(--ara-blog-space-section-gap-md, var(--ara-blog-space-section-gap-sm, var(--ara-blog-space-section-gap)))))));\n --_radius: var(--ara-blog-radius-3xl, var(--ara-blog-radius-2xl, var(--ara-blog-radius-xl, var(--ara-blog-radius-lg, var(--ara-blog-radius-md, var(--ara-blog-radius-sm, var(--ara-blog-radius)))))));\n }\n}\n\n.aranova-blog-post {\n /* Internal spacing scale, derived from the responsive block gap. */\n --_flow: var(--_block-gap);\n --_flow-sm: calc(var(--_block-gap) * 0.5);\n --_flow-lg: calc(var(--_block-gap) * 1.75);\n --_flow-section: var(--_section-gap);\n /* Optional heading color token \u2014 themes that don't set it fall back to text. */\n --_heading-color: var(--ara-blog-color-heading, var(--ara-blog-color-text));\n\n max-width: var(--_content-max-width);\n margin-inline: auto;\n background: var(--ara-blog-color-background);\n color: var(--ara-blog-color-text);\n font-family: var(--ara-blog-type-body-family);\n font-weight: var(--ara-blog-type-body-weight);\n font-size: var(--_body-size);\n line-height: var(--_body-line-height);\n overflow-wrap: break-word;\n}\n@media (max-width: 639.99px) {\n .aranova-blog-post {\n padding-inline: 1.25rem;\n }\n}\n\n/* --- Headings ------------------------------------------------------------ */\n.aranova-blog-post :where(h1, h2, h3, h4, h5, h6) {\n font-family: var(--ara-blog-type-heading-family);\n font-weight: var(--ara-blog-type-heading-weight);\n line-height: 1.25;\n margin-block: 0;\n color: var(--_heading-color);\n scroll-margin-top: 5rem;\n}\n.aranova-blog-post :where(h1, h2, h3) {\n /* `pretty` fills each line greedily (only the LAST line is massaged to avoid\n * an orphan word) \u2014 so a heading uses the full measure. `balance` was leaving\n * short first lines. */\n text-wrap: pretty;\n}\n.aranova-blog-post :where(h1) {\n font-size: var(--_h1-size);\n letter-spacing: var(--ara-blog-type-heading-letter-spacing);\n}\n.aranova-blog-post :where(h2) {\n font-size: var(--_h2-size);\n letter-spacing: var(--ara-blog-type-heading-letter-spacing);\n}\n.aranova-blog-post :where(h3) {\n font-size: var(--_h3-size);\n letter-spacing: calc(var(--ara-blog-type-heading-letter-spacing) * 0.5);\n}\n.aranova-blog-post :where(h4) {\n font-size: var(--_h4-size);\n font-weight: 600;\n letter-spacing: calc(var(--ara-blog-type-heading-letter-spacing) * 0.5);\n}\n.aranova-blog-post :where(h5) {\n font-size: var(--_body-size);\n font-weight: 600;\n}\n.aranova-blog-post :where(h6) {\n font-size: var(--_small-size);\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--ara-blog-color-text-muted);\n}\n\n/* --- Text ---------------------------------------------------------------- */\n.aranova-blog-post :where(p) {\n margin-block: 0;\n}\n/* Lead paragraph: the one directly after the post title. */\n.aranova-blog-post :where(h1) + :where(p) {\n font-size: 1.15em;\n color: color-mix(in srgb, var(--ara-blog-color-text) 72%, var(--ara-blog-color-text-muted));\n}\n.aranova-blog-post :where(a) {\n color: var(--ara-blog-color-link);\n text-decoration: underline;\n text-underline-offset: 0.15em;\n}\n.aranova-blog-post :where(a):focus-visible {\n outline: 2px solid var(--ara-blog-color-link);\n outline-offset: 2px;\n border-radius: 2px;\n}\n\n/* --- Lists ---------------------------------------------------------------- */\n.aranova-blog-post :where(ul, ol) {\n margin-block: 0;\n padding-inline-start: 1.5em;\n}\n.aranova-blog-post :where(ul) {\n list-style-type: disc;\n}\n.aranova-blog-post :where(ul ul) {\n list-style-type: circle;\n}\n.aranova-blog-post :where(ul ul ul) {\n list-style-type: square;\n}\n.aranova-blog-post :where(ol) {\n list-style-type: decimal;\n}\n.aranova-blog-post :where(ol ol) {\n list-style-type: lower-alpha;\n}\n.aranova-blog-post :where(ul) > li::marker {\n color: var(--ara-blog-color-text-muted);\n}\n.aranova-blog-post :where(ol) > li::marker {\n font-weight: 600;\n color: var(--_heading-color);\n}\n.aranova-blog-post :where(li + li) {\n margin-block-start: 0.35em;\n}\n.aranova-blog-post :where(li > ul, li > ol) {\n margin-block-start: 0.35em;\n}\n\n/* --- Blockquote ------------------------------------------------------------ */\n.aranova-blog-post :where(blockquote) {\n margin: 0;\n padding-block: 0.25em;\n padding-inline: 1.25em 0.5em;\n border-inline-start: 3px solid var(--ara-blog-color-primary);\n color: var(--ara-blog-color-text-muted);\n font-style: italic;\n}\n\n/* --- Code ------------------------------------------------------------------ */\n.aranova-blog-post :where(code) {\n background: var(--ara-blog-color-code-background);\n border-radius: calc(var(--_radius) / 2);\n padding: 0.15em 0.35em;\n font-size: 0.9em;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n}\n.aranova-blog-post :where(pre) {\n margin: 0;\n background: var(--ara-blog-color-code-background);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n padding: 1em;\n overflow-x: auto;\n line-height: 1.5;\n}\n.aranova-blog-post :where(pre code) {\n background: none;\n border-radius: 0;\n padding: 0;\n font-size: var(--_small-size);\n}\n\n/* --- Media ------------------------------------------------------------------ */\n.aranova-blog-post :where(img) {\n max-width: 100%;\n height: auto;\n display: block;\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n}\n.aranova-blog-post :where(figure) {\n margin: 0;\n}\n.aranova-blog-post :where(figcaption) {\n margin-block-start: 0.5em;\n font-size: var(--_small-size);\n color: var(--ara-blog-color-text-muted);\n text-align: var(--ara-blog-type-caption-align, center);\n}\n\n/* --- Tables ------------------------------------------------------------------ */\n.aranova-blog-post .aranova-blog-table-wrap {\n overflow-x: auto;\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n}\n.aranova-blog-post :where(table) {\n border-collapse: collapse;\n width: 100%;\n font-size: var(--_small-size);\n}\n.aranova-blog-post :where(th, td) {\n border: 0;\n border-block-end: 1px solid var(--ara-blog-color-border);\n padding: 0.6em 0.85em;\n text-align: left;\n}\n.aranova-blog-post :where(th) {\n background: var(--ara-blog-color-surface);\n border-block-end: 2px solid var(--ara-blog-color-border);\n font-weight: 600;\n}\n.aranova-blog-post :where(tbody tr:nth-child(even) td) {\n background: color-mix(\n in srgb,\n var(--ara-blog-color-surface) 55%,\n var(--ara-blog-color-background)\n );\n}\n.aranova-blog-post :where(tbody tr:last-child) :where(th, td) {\n border-block-end: 0;\n}\n\n/* --- Section break ------------------------------------------------------------ */\n.aranova-blog-post :where(hr) {\n border: none;\n border-top: 2px solid var(--ara-blog-color-border);\n width: 4rem;\n margin-inline: auto;\n}\n\n/* --- Callout ------------------------------------------------------------------ */\n.aranova-blog-post .aranova-blog-callout {\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-inline-start: 4px solid var(--ara-blog-color-primary);\n border-radius: var(--_radius);\n padding: 0.85em 1.1em;\n}\n.aranova-blog-post .aranova-blog-callout > * + * {\n margin-block-start: var(--_flow-sm);\n}\n\n/* --- CTA ------------------------------------------------------------------ */\n.aranova-blog-post .aranova-blog-cta {\n display: inline-block;\n background: var(--ara-blog-color-primary);\n color: var(--ara-blog-color-primary-foreground);\n border-radius: var(--_radius);\n padding: 0.6em 1.2em;\n font-weight: 600;\n text-decoration: none;\n transition:\n filter 150ms ease,\n transform 150ms ease;\n}\n.aranova-blog-post .aranova-blog-cta:hover {\n filter: brightness(1.08);\n transform: translateY(-1px);\n}\n.aranova-blog-post .aranova-blog-cta:active {\n filter: none;\n transform: none;\n}\n.aranova-blog-post .aranova-blog-cta:focus-visible {\n outline: 2px solid var(--ara-blog-color-primary);\n outline-offset: 2px;\n}\n\n/* --- FAQ (details/summary accordion) ------------------------------------------ */\n.aranova-blog-post .aranova-blog-faq-item {\n border-block-start: 1px solid var(--ara-blog-color-border);\n padding-block: calc(var(--_flow) * 0.75);\n}\n.aranova-blog-post .aranova-blog-faq-question {\n cursor: pointer;\n list-style: none;\n position: relative;\n padding-inline-end: 1.75em;\n}\n.aranova-blog-post .aranova-blog-faq-question::-webkit-details-marker {\n display: none;\n}\n/* Rotating chevron (border trick \u2014 colors via theme vars only). */\n.aranova-blog-post .aranova-blog-faq-question::after {\n content: \"\";\n position: absolute;\n inset-block-start: 50%;\n inset-inline-end: 0.35em;\n width: 0.45em;\n height: 0.45em;\n border-inline-end: 2px solid var(--ara-blog-color-text-muted);\n border-block-end: 2px solid var(--ara-blog-color-text-muted);\n transform: translateY(-70%) rotate(45deg);\n transition: transform 150ms ease;\n}\n.aranova-blog-post .aranova-blog-faq-item[open] > .aranova-blog-faq-question::after {\n transform: translateY(-30%) rotate(225deg);\n}\n.aranova-blog-post .aranova-blog-faq-question:focus-visible {\n outline: 2px solid var(--ara-blog-color-link);\n outline-offset: 2px;\n border-radius: 2px;\n}\n.aranova-blog-post .aranova-blog-faq-answer {\n padding-block-start: var(--_flow-sm);\n}\n.aranova-blog-post .aranova-blog-faq-answer > * + * {\n margin-block-start: var(--_flow-sm);\n}\n/* Progressive open/close animation where the platform supports it. */\n@supports (interpolate-size: allow-keywords) {\n .aranova-blog-post .aranova-blog-faq-item {\n interpolate-size: allow-keywords;\n }\n .aranova-blog-post .aranova-blog-faq-item::details-content {\n block-size: 0;\n overflow-y: clip;\n transition:\n block-size 200ms ease,\n content-visibility 200ms allow-discrete;\n }\n .aranova-blog-post .aranova-blog-faq-item[open]::details-content {\n block-size: auto;\n }\n}\n\n/* --- Key takeaways ------------------------------------------------------------ */\n.aranova-blog-post .aranova-blog-key-takeaways {\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n padding: 1em 1.25em;\n}\n.aranova-blog-post .aranova-blog-key-takeaways :where(ul) {\n margin-block: 0;\n}\n.aranova-blog-post .aranova-blog-key-takeaways :where(ul) > li::marker {\n color: var(--ara-blog-color-primary);\n}\n\n/* --- Article layout (content column + optional aside rail) -----------------------\n * `.aranova-blog-layout` is a plain block on narrow viewports (the aside, when\n * present, stacks after the content). On wide viewports a grid opens ONLY when\n * an aside exists, and the article widens by exactly the rail + gap so the\n * content column keeps its `content_max_width`. */\n.aranova-blog-post .aranova-blog-layout {\n /* Flex column (not block) so the aside can be VISUALLY hoisted above the\n * content on narrow viewports while the content stays first in DOM\n * (reading order / SEO). */\n display: flex;\n flex-direction: column;\n}\n.aranova-blog-post .aranova-blog-layout > .aranova-blog-aside {\n order: -1;\n margin-block-end: var(--_flow-lg);\n}\n@media (min-width: 1100px) {\n .aranova-blog-post:has(.aranova-blog-aside) {\n max-width: calc(var(--_content-max-width) + 240px + 3rem);\n }\n .aranova-blog-post .aranova-blog-layout:has(.aranova-blog-aside) {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 240px;\n column-gap: 3rem;\n align-items: start;\n }\n .aranova-blog-post .aranova-blog-layout:has(.aranova-blog-aside) > .aranova-blog-aside {\n /* Reset the mobile hoist: grid auto-placement honors `order`, so -1 would\n * drop the aside into the FIRST column. */\n order: 0;\n margin-block-end: 0;\n position: sticky;\n top: 5rem;\n align-self: start;\n max-height: calc(100vh - 6rem);\n overflow: auto;\n }\n /* Rail on the LEFT: flip the column order (content DOM stays first for\n * reading order / SEO; only the visual columns swap). */\n .aranova-blog-post .aranova-blog-layout[data-toc-side=\"left\"]:has(.aranova-blog-aside) {\n grid-template-columns: 240px minmax(0, 1fr);\n }\n /* Horizontal collapse, CLOSED: the rail column shrinks to the compact toggle\n * (auto) and the article reflows into the reclaimed space. */\n .aranova-blog-post\n .aranova-blog-layout:has(\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n ) {\n grid-template-columns: minmax(0, 1fr) auto;\n }\n .aranova-blog-post\n .aranova-blog-layout[data-toc-side=\"left\"]:has(\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n ) {\n grid-template-columns: auto minmax(0, 1fr);\n }\n .aranova-blog-post\n .aranova-blog-layout[data-toc-side=\"left\"]:has(.aranova-blog-aside)\n > .aranova-blog-content {\n order: 2;\n }\n .aranova-blog-post\n .aranova-blog-layout[data-toc-side=\"left\"]:has(.aranova-blog-aside)\n > .aranova-blog-aside {\n order: 1;\n }\n}\n\n/* --- Byline / meta row (chrome) --------------------------------------------------- */\n.aranova-blog-post .aranova-blog-meta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.35em 0.5em;\n font-size: var(--_small-size);\n color: var(--ara-blog-color-text-muted);\n padding-block-end: var(--_flow);\n border-block-end: 1px solid var(--ara-blog-color-border);\n}\n.aranova-blog-post .aranova-blog-meta-item {\n display: inline-flex;\n align-items: baseline;\n gap: 0.5em;\n white-space: nowrap;\n}\n.aranova-blog-post .aranova-blog-meta-tags {\n display: inline-flex;\n flex-wrap: wrap;\n gap: 0.35em;\n margin-inline-start: auto;\n}\n.aranova-blog-post .aranova-blog-meta-tag {\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: 999px;\n padding: 0.1em 0.7em;\n white-space: nowrap;\n}\n\n/* --- Breadcrumbs (chrome) ---------------------------------------------------------- */\n.aranova-blog-post .aranova-blog-breadcrumbs {\n font-size: var(--_small-size);\n color: var(--ara-blog-color-text-muted);\n}\n.aranova-blog-post .aranova-blog-breadcrumbs ol {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 0.2em 0.5em;\n}\n.aranova-blog-post .aranova-blog-breadcrumbs li {\n display: inline-flex;\n align-items: baseline;\n gap: 0.5em;\n}\n.aranova-blog-post .aranova-blog-breadcrumbs a {\n color: var(--ara-blog-color-text-muted);\n text-decoration: none;\n}\n.aranova-blog-post .aranova-blog-breadcrumbs a:hover {\n color: var(--ara-blog-color-link);\n text-decoration: underline;\n text-underline-offset: 0.15em;\n}\n.aranova-blog-post .aranova-blog-breadcrumbs [aria-current=\"page\"] {\n color: var(--ara-blog-color-text);\n}\n\n/* --- Table of contents (chrome) -----------------------------------------------------\n * Zero-JS shell: a native details/summary disclosure. Chevron follows the FAQ\n * accordion's border-trick pattern (theme vars only, no raw colors). */\n.aranova-blog-post .aranova-blog-toc {\n font-size: var(--_small-size);\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n padding: 0.85em 1.1em;\n}\n.aranova-blog-post .aranova-blog-toc-title {\n cursor: pointer;\n list-style: none;\n position: relative;\n padding-inline-end: 1.75em;\n font-weight: 600;\n color: var(--_heading-color);\n}\n.aranova-blog-post .aranova-blog-toc-title::-webkit-details-marker {\n display: none;\n}\n.aranova-blog-post .aranova-blog-toc-title::after {\n content: \"\";\n position: absolute;\n inset-block-start: 50%;\n inset-inline-end: 0.35em;\n width: 0.45em;\n height: 0.45em;\n border-inline-end: 2px solid var(--ara-blog-color-text-muted);\n border-block-end: 2px solid var(--ara-blog-color-text-muted);\n transform: translateY(-70%) rotate(45deg);\n transition: transform 150ms ease;\n}\n.aranova-blog-post .aranova-blog-toc[open] > .aranova-blog-toc-title::after {\n transform: translateY(-30%) rotate(225deg);\n}\n.aranova-blog-post .aranova-blog-toc-title:focus-visible {\n outline: 2px solid var(--ara-blog-color-link);\n outline-offset: 2px;\n border-radius: 2px;\n}\n.aranova-blog-post .aranova-blog-toc ol {\n list-style: none;\n margin: 0.75em 0 0;\n padding: 0;\n}\n.aranova-blog-post .aranova-blog-toc li {\n margin: 0;\n}\n.aranova-blog-post .aranova-blog-toc li + li {\n margin-block-start: 0.3em;\n}\n.aranova-blog-post .aranova-blog-toc li[data-depth=\"3\"] {\n padding-inline-start: 0.9em;\n font-size: 0.94em;\n}\n.aranova-blog-post .aranova-blog-toc a {\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n line-height: 1.35;\n color: var(--ara-blog-color-text-muted);\n text-decoration: none;\n}\n.aranova-blog-post .aranova-blog-toc a:hover {\n color: var(--ara-blog-color-link);\n text-decoration: underline;\n text-underline-offset: 0.15em;\n}\n\n/* --- Table of contents (client island) ------------------------------------------\n * Animated companion to the zero-JS shell above: same box, but a real <button>\n * header (aria-expanded) and a grid-template-rows 0fr\u21941fr collapse. The global\n * reduced-motion block at the end of this sheet disables the transition. */\n.aranova-blog-post .aranova-blog-toc-panel {\n font-size: var(--_small-size);\n background: var(--ara-blog-color-surface);\n border: 1px solid var(--ara-blog-color-border);\n border-radius: var(--_radius);\n padding: 0.85em 1.1em;\n}\n.aranova-blog-post .aranova-blog-toc-panel-title {\n /* Button reset \u2014 host sites may or may not run Tailwind preflight. */\n appearance: none;\n background: none;\n border: 0;\n margin: 0;\n padding: 0;\n padding-inline-end: 1.75em;\n width: 100%;\n text-align: start;\n font: inherit;\n cursor: pointer;\n position: relative;\n font-weight: 600;\n color: var(--_heading-color);\n}\n/* Rotating chevron (border trick \u2014 same pattern as the FAQ / zero-JS TOC). */\n.aranova-blog-post .aranova-blog-toc-panel-title::after {\n content: \"\";\n position: absolute;\n inset-block-start: 50%;\n inset-inline-end: 0.35em;\n width: 0.45em;\n height: 0.45em;\n border-inline-end: 2px solid var(--ara-blog-color-text-muted);\n border-block-end: 2px solid var(--ara-blog-color-text-muted);\n transform: translateY(-70%) rotate(45deg);\n transition: transform 250ms ease;\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-open=\"true\"]\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-30%) rotate(225deg);\n}\n.aranova-blog-post .aranova-blog-toc-panel-title:focus-visible {\n outline: 2px solid var(--ara-blog-color-link);\n outline-offset: 2px;\n border-radius: 2px;\n}\n/* Collapsible body: the grid row animates 0fr\u21941fr; the clip wrapper hides overflow. */\n.aranova-blog-post .aranova-blog-toc-panel-body {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 250ms ease;\n}\n.aranova-blog-post .aranova-blog-toc-panel[data-open=\"true\"] > .aranova-blog-toc-panel-body {\n grid-template-rows: 1fr;\n}\n/* Horizontal collapse: a clean binary toggle along the inline axis. Open = the\n * normal rail; closed = the whole panel is a compact chevron chip and the rail\n * grid column reclaims the space (via `:has()` on the layout, below). No width\n * animation: grid `auto` tracks can't interpolate, and a sliding clip either\n * overflows the sticky aside (horizontal scrollbar) or reflows the entries \u2014\n * instant + correct beats janky. */\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]\n > .aranova-blog-toc-panel-body {\n display: block;\n grid-template-rows: none;\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-body {\n display: none;\n}\n/* Closed panel compacts to a small rounded chevron chip: the label is clipped\n * out of a square toggle box and the panel hugs it (the rail's `auto` grid\n * column then shrinks to the chip). */\n.aranova-blog-post .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"]) {\n width: fit-content;\n padding: 0.6em;\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-title {\n display: block;\n width: 1.4em;\n height: 1.4em;\n overflow: hidden;\n text-indent: 150%;\n white-space: nowrap;\n padding-inline-end: 0;\n}\n/* Horizontal chevron points along the fold axis (\u2039/\u203A), flipping on open/close\n * per side, instead of the vertical \u2304/\u2303. */\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]\n > .aranova-blog-toc-panel-title::after {\n inset-inline: auto 0.5em;\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-title::after {\n inset-inline: 0;\n margin-inline: auto;\n}\n/* Left rail: open folds left (\u2039 = rotate 135deg), closed expands right (\u203A = -45deg). */\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"][data-side=\"left\"][data-open=\"true\"]\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-30%) rotate(135deg);\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"][data-side=\"left\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-70%) rotate(-45deg);\n}\n/* Right rail: mirror \u2014 open folds right (\u203A), closed expands left (\u2039). */\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"][data-side=\"right\"][data-open=\"true\"]\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-70%) rotate(-45deg);\n}\n.aranova-blog-post\n .aranova-blog-toc-panel[data-collapse=\"horizontal\"][data-side=\"right\"]:not([data-open=\"true\"])\n > .aranova-blog-toc-panel-title::after {\n transform: translateY(-30%) rotate(135deg);\n}\n.aranova-blog-post .aranova-blog-toc-panel-clip {\n overflow: hidden;\n min-height: 0;\n}\n.aranova-blog-post .aranova-blog-toc-panel ol {\n list-style: none;\n margin: 0.75em 0 0;\n padding: 0;\n}\n.aranova-blog-post .aranova-blog-toc-panel li {\n margin: 0;\n}\n.aranova-blog-post .aranova-blog-toc-panel li + li {\n margin-block-start: 0.3em;\n}\n.aranova-blog-post .aranova-blog-toc-panel li[data-depth=\"3\"] {\n padding-inline-start: 0.9em;\n font-size: 0.94em;\n}\n.aranova-blog-post .aranova-blog-toc-panel-link {\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n line-height: 1.35;\n color: var(--ara-blog-color-text-muted);\n text-decoration: none;\n border-inline-start: 2px solid transparent;\n padding-inline-start: 0.6em;\n margin-inline-start: -0.75em;\n transition: color 150ms ease;\n}\n.aranova-blog-post .aranova-blog-toc-panel-link:hover {\n color: var(--ara-blog-color-link);\n text-decoration: underline;\n text-underline-offset: 0.15em;\n}\n/* Scroll-spy active entry: primary color + inline-start accent. */\n.aranova-blog-post .aranova-blog-toc-panel-link[data-active=\"true\"] {\n color: var(--ara-blog-color-primary);\n border-inline-start-color: var(--ara-blog-color-primary);\n font-weight: 600;\n}\n\n/* --- Reading progress (client island) --------------------------------------------\n * Fixed strip along the top of the viewport; fill = scaleX(scrolled fraction),\n * driven directly from rAF (no transition \u2014 it tracks position, so the\n * reduced-motion block has nothing to disable and the bar stays useful). */\n.aranova-blog-post .aranova-blog-progress {\n position: fixed;\n inset-block-start: 0;\n inset-inline: 0;\n z-index: 60;\n background: var(--ara-blog-color-primary);\n transform: scaleX(0);\n transform-origin: 0 50%;\n pointer-events: none;\n}\n\n/* --- Vertical rhythm ------------------------------------------------------------\n * Kept AFTER the element rules: the owl selector and the :where()-wrapped\n * element rules all sit at (0,1,0), so source order lets rhythm win over the\n * elements' margin resets, and the per-heading overrides below win over the\n * owl. `:is(...)` targets both today's direct-child layout and the later\n * `.aranova-blog-content` wrapper at class specificity. */\n:is(.aranova-blog-post, .aranova-blog-content) > * + * {\n margin-block-start: var(--_flow);\n}\n:is(.aranova-blog-post, .aranova-blog-content) > :where(h2) {\n margin-block-start: var(--_flow-section);\n}\n:is(.aranova-blog-post, .aranova-blog-content) > :where(h3, h4) {\n margin-block-start: var(--_flow-lg);\n}\n:is(.aranova-blog-post, .aranova-blog-content) > :where(figure) {\n margin-block: var(--_flow-lg);\n}\n:is(.aranova-blog-post, .aranova-blog-content) > :where(hr) {\n margin-block: var(--_flow-section);\n}\n\n/* --- Motion ------------------------------------------------------------------\n * Opt-in: a later phase stamps data-motion=\"subtle\" on the article; without\n * the attribute nothing animates. */\n@media (prefers-reduced-motion: no-preference) {\n .aranova-blog-post[data-motion=\"subtle\"] {\n animation: aranova-blog-fade-up 300ms ease-out both;\n }\n}\n@keyframes aranova-blog-fade-up {\n from {\n opacity: 0;\n transform: translateY(8px);\n }\n to {\n opacity: 1;\n transform: none;\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .aranova-blog-post,\n .aranova-blog-post *,\n .aranova-blog-post *::before,\n .aranova-blog-post *::after {\n animation: none !important;\n transition: none !important;\n }\n}\n";
|
|
190
|
+
|
|
191
|
+
/** Fallback locale for date formatting (the agency's home market). */
|
|
192
|
+
declare const DEFAULT_BLOG_LOCALE = "en-CA";
|
|
193
|
+
interface BlogPostMetaProps {
|
|
194
|
+
/** ISO date(-time) string; invalid/absent values render nothing. */
|
|
195
|
+
publishedAt?: string;
|
|
196
|
+
/** ISO date(-time) string; only shown when it differs from `publishedAt`'s day. */
|
|
197
|
+
updatedAt?: string | null;
|
|
198
|
+
/** Whole minutes (see `estimateReadingTime`); non-positive values render nothing. */
|
|
199
|
+
readingTimeMinutes?: number;
|
|
200
|
+
tags?: readonly string[];
|
|
201
|
+
/** BCP-47 locale for date formatting. Default {@link DEFAULT_BLOG_LOCALE}. */
|
|
202
|
+
locale?: string;
|
|
203
|
+
/** Site-supplied class merged onto the fixed `.aranova-blog-meta` class. */
|
|
204
|
+
className?: string;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Post byline row: published date (+ "Updated …" when meaningfully different),
|
|
208
|
+
* "· N min read", and tag pills. Renders `null` when there is nothing to show.
|
|
209
|
+
* Styled by the `.aranova-blog-meta` section of the base CSS (muted small row
|
|
210
|
+
* with a bottom divider).
|
|
211
|
+
*/
|
|
212
|
+
declare function BlogPostMeta({ publishedAt, updatedAt, readingTimeMinutes, tags, locale, className, }: BlogPostMetaProps): ReactNode;
|
|
213
|
+
|
|
214
|
+
interface BlogTocShellProps {
|
|
215
|
+
/** The post outline — see `extractBlogHeadings`. Empty array renders nothing. */
|
|
216
|
+
headings: readonly BlogHeading[];
|
|
217
|
+
/** Disclosure summary label. Default `"On this page"`. */
|
|
218
|
+
title?: string;
|
|
219
|
+
/** Site-supplied class merged onto the fixed `.aranova-blog-toc` class. */
|
|
220
|
+
className?: string;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* No-JS table of contents: a native `<details open>` disclosure holding an
|
|
224
|
+
* anchor list (h3 entries indented under their h2). Styled by the
|
|
225
|
+
* `.aranova-blog-toc` section of the base CSS (chevron consistent with the
|
|
226
|
+
* FAQ disclosure pattern).
|
|
227
|
+
*/
|
|
228
|
+
declare function BlogTocShell({ headings, title, className, }: BlogTocShellProps): ReactNode;
|
|
229
|
+
|
|
230
|
+
interface BlogPostProps extends Omit<BlogPostProps$1, "tocSlot" | "progressSlot"> {
|
|
231
|
+
/** Render the fixed top-of-viewport reading progress bar. Default `false`. */
|
|
232
|
+
showProgress?: boolean;
|
|
233
|
+
/**
|
|
234
|
+
* ToC collapse axis: `"vertical"` (default) folds up under the title,
|
|
235
|
+
* `"horizontal"` folds sideways toward `tocSide`. Only affects the animated
|
|
236
|
+
* `TocPanel` island rendered here.
|
|
237
|
+
*/
|
|
238
|
+
tocCollapse?: "vertical" | "horizontal";
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* `BlogPost` with the animated islands wired in: `showToc` renders the
|
|
242
|
+
* collapsible scroll-spy `TocPanel` (instead of core's zero-JS shell) and
|
|
243
|
+
* `showProgress` adds the `ReadingProgress` bar to the header chrome. All
|
|
244
|
+
* other props pass straight through to tracking-core's `BlogPost`.
|
|
245
|
+
*/
|
|
246
|
+
declare function BlogPost({ showProgress, tocCollapse, ...props }: BlogPostProps): ReactNode;
|
|
247
|
+
|
|
248
|
+
export { BLOG_BASE_CSS, type BlogBreadcrumbItem, BlogBreadcrumbs, type BlogBreadcrumbsProps, BlogClassNameOverrides, BlogClient, type BlogComponents, BlogHeading, BlogImage, BlogPost, BlogPostMeta, type BlogPostMetaProps, type BlogPostProps, BlogPostView, type BlogPostViewProps, BlogTheme, BlogThemeOverride, BlogTocShell, type BlogTocShellProps, DEFAULT_BLOG_LOCALE, createDefaultComponents };
|