@aranova/tracking-next 0.20.0 → 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 +62 -0
- 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.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.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
package/README.md
CHANGED
|
@@ -425,6 +425,67 @@ The hook returns the effective `state` (`"granted" | "denied"`), its `source` (`
|
|
|
425
425
|
|
|
426
426
|
**Migrating from the banner flow:** delete `<ConsentBanner />`, add a footer control built on `useCookiePreferences`, and mention the tracking + opt-out in your privacy policy. Existing visitors' stored grants stay granted; stored declines stay denied for 90 days from their first visit after the upgrade.
|
|
427
427
|
|
|
428
|
+
## Blog Rendering
|
|
429
|
+
|
|
430
|
+
Render Aranova-published blog posts straight from the CDN — no API calls, no database. Three subpaths:
|
|
431
|
+
|
|
432
|
+
- `@aranova/tracking-next/blog` — **React-free**: `createBlogClient`, theme presets + `createBlogFontTheme`, SEO builders (`buildBlogPostMetadata`, `buildBlogPostingJsonLd`, `buildBlogSitemapEntries`), TOC/reading-time helpers. Safe in metadata, sitemaps, client bundles.
|
|
433
|
+
- `@aranova/tracking-next/blog-server` — the RSC renderers `<BlogPost/>` / `<BlogPostView/>` + article chrome (Server Components only).
|
|
434
|
+
- `@aranova/tracking-next/blog-client` — animated islands: `TocPanel` (scroll-spy TOC), `ReadingProgress` (progress bar with a headless `onProgress` mode).
|
|
435
|
+
|
|
436
|
+
```tsx
|
|
437
|
+
// lib/blog.ts
|
|
438
|
+
import { createBlogClient } from "@aranova/tracking-next/blog";
|
|
439
|
+
export const blogClient = createBlogClient({
|
|
440
|
+
apiKey: process.env.NEXT_PUBLIC_ARANOVA_TRACKING_API_KEY!,
|
|
441
|
+
cdnBaseUrl: process.env.NEXT_PUBLIC_ARANOVA_BLOG_CDN_URL!,
|
|
442
|
+
namespace: process.env.NEXT_PUBLIC_ARANOVA_BLOG_NAMESPACE!, // Aranova provides it
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// lib/blog-theme.ts — start from a preset, wire your next/font vars
|
|
446
|
+
import { BLOG_THEME_PRESETS, createBlogFontTheme } from "@aranova/tracking-next/blog";
|
|
447
|
+
export const BLOG_THEME = createBlogFontTheme(BLOG_THEME_PRESETS.modern, {
|
|
448
|
+
heading: "var(--font-display)",
|
|
449
|
+
body: "var(--font-sans)",
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// app/blog/[slug]/page.tsx (App Router)
|
|
453
|
+
import { BlogPost } from "@aranova/tracking-next/blog-server";
|
|
454
|
+
|
|
455
|
+
export const revalidate = 300; // re-publishes appear within 5 minutes
|
|
456
|
+
export const dynamicParams = true;
|
|
457
|
+
export async function generateStaticParams() {
|
|
458
|
+
return blogClient.getStaticParams();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
|
462
|
+
const { slug } = await params;
|
|
463
|
+
return (
|
|
464
|
+
<BlogPost
|
|
465
|
+
slug={slug}
|
|
466
|
+
client={blogClient}
|
|
467
|
+
defaultTheme={BLOG_THEME}
|
|
468
|
+
showMeta
|
|
469
|
+
showToc
|
|
470
|
+
tocSide="right" // or "left"
|
|
471
|
+
tocCollapse="vertical" // or "horizontal" (collapses to a chevron chip)
|
|
472
|
+
showProgress
|
|
473
|
+
showBreadcrumbs
|
|
474
|
+
breadcrumbs={[
|
|
475
|
+
{ label: "Home", href: "/" },
|
|
476
|
+
{ label: "Blog", href: "/blog" },
|
|
477
|
+
]}
|
|
478
|
+
/>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Index pages: `blogClient.listPosts()` returns manifest entries with `title`, `description`, `published_at`, `reading_minutes`, and `hero_image` — everything an image-card grid needs without fetching content objects. `buildBlogPostMetadata` / `buildBlogPostingJsonLd` / `buildBlogSitemapEntries` cover `generateMetadata`, JSON-LD, and `sitemap.ts`.
|
|
484
|
+
|
|
485
|
+
Customization, cheapest first: the **theme** (`defaultTheme`, deep-partial: palette incl. optional dark, responsive type scale, spacing, radius, `caption_align`); **chrome props** (above); **component replacement** — `components={{ Cta: MyCtaSection, ... }}` swaps the renderer for any MDX component (`Cta`/`Callout`/`BlogImage`/`FaqItem`/`KeyTakeaways`, keep `data-aranova-cta={label}` on custom CTAs so cta_click auto-capture keeps firing); **per-surface classes** — `classNames={{ article, cta, toc, meta, ... }}` merged onto the stable `.aranova-blog-*` classes. The islands are importable directly for fully custom compositions — e.g. `<ReadingProgress showBar={false} onProgress={(f, pct) => ...} />` to drive your own indicator.
|
|
486
|
+
|
|
487
|
+
Reads are best-effort: network/parse failures degrade to `null`/empty — a blog page never crashes the host site.
|
|
488
|
+
|
|
428
489
|
## Exports
|
|
429
490
|
|
|
430
491
|
- Root package: `createTracking`, `TrackingProvider`, `useTracking`, `AdPlatformTracking`,
|
|
@@ -439,4 +500,5 @@ The hook returns the effective `state` (`"granted" | "denied"`), its `source` (`
|
|
|
439
500
|
- `@aranova/tracking-next/middleware`: `createTrackingMiddleware()`
|
|
440
501
|
- `@aranova/tracking-next/server`: `getTrackingParamsServer()`
|
|
441
502
|
- `@aranova/tracking-next/phone`: isomorphic phone utils (no React)
|
|
503
|
+
- `@aranova/tracking-next/blog` / `/blog-server` / `/blog-client`: CDN blog rendering (see Blog Rendering above)
|
|
442
504
|
- Codegen: [`@aranova/tracking-cli`](https://www.npmjs.com/package/@aranova/tracking-cli) — `gen` typed service unions (devDependency)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { B as BlogHeading } from './toc-C_OvH3ES.mjs';
|
|
3
|
+
|
|
4
|
+
interface TocPanelProps {
|
|
5
|
+
/** The post outline — see `extractBlogHeadings`. Empty array renders nothing. */
|
|
6
|
+
headings: readonly BlogHeading[];
|
|
7
|
+
/** Panel header label. Default `"On this page"`. */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Whether the panel body starts expanded. Default `true`. */
|
|
10
|
+
defaultOpen?: boolean;
|
|
11
|
+
/** Site-supplied class merged onto the fixed `.aranova-blog-toc-panel` class. */
|
|
12
|
+
className?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Collapse axis. `"vertical"` (default) folds the list up under the title;
|
|
15
|
+
* `"horizontal"` folds it sideways toward `side`. Stamped as `data-collapse`
|
|
16
|
+
* for the base CSS.
|
|
17
|
+
*/
|
|
18
|
+
collapse?: "vertical" | "horizontal";
|
|
19
|
+
/**
|
|
20
|
+
* Which edge a `"horizontal"` collapse recedes toward — match this to the
|
|
21
|
+
* rail's side so it folds into the page margin. Stamped as `data-side`.
|
|
22
|
+
* Default `"right"`.
|
|
23
|
+
*/
|
|
24
|
+
side?: "left" | "right";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Collapsible, scroll-spying table of contents. Rendered into BlogPostView's
|
|
28
|
+
* `aside` slot by the enhanced `BlogPost` wrapper in `@aranova/tracking-next`
|
|
29
|
+
* (imported there via the `@aranova/tracking-next/blog-client` self-specifier
|
|
30
|
+
* so this module's `"use client"` boundary survives bundling).
|
|
31
|
+
*/
|
|
32
|
+
declare function TocPanel({ headings, title, defaultOpen, className, collapse, side, }: TocPanelProps): ReactNode;
|
|
33
|
+
|
|
34
|
+
interface ReadingProgressProps {
|
|
35
|
+
/** CSS selector for the element whose read-through is measured. Default `".aranova-blog-post"`. */
|
|
36
|
+
target?: string;
|
|
37
|
+
/** Bar thickness in px. Default `3`. */
|
|
38
|
+
height?: number;
|
|
39
|
+
/** Site-supplied class merged onto the fixed `.aranova-blog-progress` class. */
|
|
40
|
+
className?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Called on every scroll/resize frame with the read-through fraction (0–1)
|
|
43
|
+
* and its rounded 0–100 percent. Lets a site drive its OWN progress UI (a
|
|
44
|
+
* header meter, a percent readout) off the same measurement. Fires once on
|
|
45
|
+
* mount with the initial position. Latest callback is always used — a new
|
|
46
|
+
* function identity each render does not re-subscribe the listeners.
|
|
47
|
+
*/
|
|
48
|
+
onProgress?: (fraction: number, percent: number) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Render the built-in bar. Default `true`. Set `false` for a headless meter:
|
|
51
|
+
* the island still measures scroll and fires `onProgress`, but paints no bar
|
|
52
|
+
* (the site owns the visuals and their accessibility).
|
|
53
|
+
*/
|
|
54
|
+
showBar?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Fixed top-of-viewport reading progress bar (`role="progressbar"`, valuenow
|
|
58
|
+
* 0–100). Rendered into BlogPostView's `header` slot by the enhanced
|
|
59
|
+
* `BlogPost` wrapper in `@aranova/tracking-next` when `showProgress` is set.
|
|
60
|
+
*
|
|
61
|
+
* For a site-owned indicator, render this island directly (it is already a
|
|
62
|
+
* client component) with `onProgress` — optionally `showBar={false}` to drop
|
|
63
|
+
* the default bar and paint your own UI from the callback. The `BlogPost`
|
|
64
|
+
* server wrapper cannot forward a function across the RSC boundary, so the
|
|
65
|
+
* callback lives here, not on `showProgress`.
|
|
66
|
+
*/
|
|
67
|
+
declare function ReadingProgress({ target, height, className, onProgress, showBar, }: ReadingProgressProps): ReactNode;
|
|
68
|
+
|
|
69
|
+
export { BlogHeading, ReadingProgress, type ReadingProgressProps, TocPanel, type TocPanelProps };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { B as BlogHeading } from './toc-C_OvH3ES.js';
|
|
3
|
+
|
|
4
|
+
interface TocPanelProps {
|
|
5
|
+
/** The post outline — see `extractBlogHeadings`. Empty array renders nothing. */
|
|
6
|
+
headings: readonly BlogHeading[];
|
|
7
|
+
/** Panel header label. Default `"On this page"`. */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** Whether the panel body starts expanded. Default `true`. */
|
|
10
|
+
defaultOpen?: boolean;
|
|
11
|
+
/** Site-supplied class merged onto the fixed `.aranova-blog-toc-panel` class. */
|
|
12
|
+
className?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Collapse axis. `"vertical"` (default) folds the list up under the title;
|
|
15
|
+
* `"horizontal"` folds it sideways toward `side`. Stamped as `data-collapse`
|
|
16
|
+
* for the base CSS.
|
|
17
|
+
*/
|
|
18
|
+
collapse?: "vertical" | "horizontal";
|
|
19
|
+
/**
|
|
20
|
+
* Which edge a `"horizontal"` collapse recedes toward — match this to the
|
|
21
|
+
* rail's side so it folds into the page margin. Stamped as `data-side`.
|
|
22
|
+
* Default `"right"`.
|
|
23
|
+
*/
|
|
24
|
+
side?: "left" | "right";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Collapsible, scroll-spying table of contents. Rendered into BlogPostView's
|
|
28
|
+
* `aside` slot by the enhanced `BlogPost` wrapper in `@aranova/tracking-next`
|
|
29
|
+
* (imported there via the `@aranova/tracking-next/blog-client` self-specifier
|
|
30
|
+
* so this module's `"use client"` boundary survives bundling).
|
|
31
|
+
*/
|
|
32
|
+
declare function TocPanel({ headings, title, defaultOpen, className, collapse, side, }: TocPanelProps): ReactNode;
|
|
33
|
+
|
|
34
|
+
interface ReadingProgressProps {
|
|
35
|
+
/** CSS selector for the element whose read-through is measured. Default `".aranova-blog-post"`. */
|
|
36
|
+
target?: string;
|
|
37
|
+
/** Bar thickness in px. Default `3`. */
|
|
38
|
+
height?: number;
|
|
39
|
+
/** Site-supplied class merged onto the fixed `.aranova-blog-progress` class. */
|
|
40
|
+
className?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Called on every scroll/resize frame with the read-through fraction (0–1)
|
|
43
|
+
* and its rounded 0–100 percent. Lets a site drive its OWN progress UI (a
|
|
44
|
+
* header meter, a percent readout) off the same measurement. Fires once on
|
|
45
|
+
* mount with the initial position. Latest callback is always used — a new
|
|
46
|
+
* function identity each render does not re-subscribe the listeners.
|
|
47
|
+
*/
|
|
48
|
+
onProgress?: (fraction: number, percent: number) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Render the built-in bar. Default `true`. Set `false` for a headless meter:
|
|
51
|
+
* the island still measures scroll and fires `onProgress`, but paints no bar
|
|
52
|
+
* (the site owns the visuals and their accessibility).
|
|
53
|
+
*/
|
|
54
|
+
showBar?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Fixed top-of-viewport reading progress bar (`role="progressbar"`, valuenow
|
|
58
|
+
* 0–100). Rendered into BlogPostView's `header` slot by the enhanced
|
|
59
|
+
* `BlogPost` wrapper in `@aranova/tracking-next` when `showProgress` is set.
|
|
60
|
+
*
|
|
61
|
+
* For a site-owned indicator, render this island directly (it is already a
|
|
62
|
+
* client component) with `onProgress` — optionally `showBar={false}` to drop
|
|
63
|
+
* the default bar and paint your own UI from the callback. The `BlogPost`
|
|
64
|
+
* server wrapper cannot forward a function across the RSC boundary, so the
|
|
65
|
+
* callback lives here, not on `showProgress`.
|
|
66
|
+
*/
|
|
67
|
+
declare function ReadingProgress({ target, height, className, onProgress, showBar, }: ReadingProgressProps): ReactNode;
|
|
68
|
+
|
|
69
|
+
export { BlogHeading, ReadingProgress, type ReadingProgressProps, TocPanel, type TocPanelProps };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/blog-client.ts
|
|
22
|
+
var blog_client_exports = {};
|
|
23
|
+
__export(blog_client_exports, {
|
|
24
|
+
ReadingProgress: () => ReadingProgress,
|
|
25
|
+
TocPanel: () => TocPanel
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(blog_client_exports);
|
|
28
|
+
|
|
29
|
+
// ../tracking-core/src/resources/blog/view/client/toc-panel.tsx
|
|
30
|
+
var import_react = require("react");
|
|
31
|
+
|
|
32
|
+
// ../tracking-core/src/resources/blog/view/class-names.ts
|
|
33
|
+
function joinClassNames(base, extra) {
|
|
34
|
+
return extra ? `${base} ${extra}` : base;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ../tracking-core/src/resources/blog/view/client/toc-panel.tsx
|
|
38
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
39
|
+
var SPY_ROOT_MARGIN = "-80px 0px -70% 0px";
|
|
40
|
+
function prefersReducedMotion() {
|
|
41
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
42
|
+
}
|
|
43
|
+
function TocPanel({
|
|
44
|
+
headings,
|
|
45
|
+
title = "On this page",
|
|
46
|
+
defaultOpen = true,
|
|
47
|
+
className,
|
|
48
|
+
collapse = "vertical",
|
|
49
|
+
side = "right"
|
|
50
|
+
}) {
|
|
51
|
+
const [open, setOpen] = (0, import_react.useState)(defaultOpen);
|
|
52
|
+
const [activeId, setActiveId] = (0, import_react.useState)(null);
|
|
53
|
+
const bodyId = (0, import_react.useId)();
|
|
54
|
+
const clickedAt = (0, import_react.useRef)(0);
|
|
55
|
+
const userToggled = (0, import_react.useRef)(false);
|
|
56
|
+
(0, import_react.useEffect)(() => {
|
|
57
|
+
if (!defaultOpen || userToggled.current) return;
|
|
58
|
+
if (typeof window.matchMedia === "function" && window.matchMedia("(max-width: 1099px)").matches) {
|
|
59
|
+
setOpen(false);
|
|
60
|
+
}
|
|
61
|
+
}, [defaultOpen]);
|
|
62
|
+
(0, import_react.useEffect)(() => {
|
|
63
|
+
const targets = headings.map((h) => document.getElementById(h.id)).filter((el) => el !== null);
|
|
64
|
+
if (targets.length === 0) return;
|
|
65
|
+
const visible = /* @__PURE__ */ new Set();
|
|
66
|
+
const observer = new IntersectionObserver(
|
|
67
|
+
(entries) => {
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (entry.isIntersecting) visible.add(entry.target.id);
|
|
70
|
+
else visible.delete(entry.target.id);
|
|
71
|
+
}
|
|
72
|
+
if (Date.now() - clickedAt.current < 700) return;
|
|
73
|
+
const next = headings.find((h) => visible.has(h.id));
|
|
74
|
+
if (next) setActiveId(next.id);
|
|
75
|
+
},
|
|
76
|
+
{ rootMargin: SPY_ROOT_MARGIN }
|
|
77
|
+
);
|
|
78
|
+
for (const el of targets) observer.observe(el);
|
|
79
|
+
return () => observer.disconnect();
|
|
80
|
+
}, [headings]);
|
|
81
|
+
const onLinkClick = (0, import_react.useCallback)((event, id) => {
|
|
82
|
+
const el = document.getElementById(id);
|
|
83
|
+
if (!el) return;
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
clickedAt.current = Date.now();
|
|
86
|
+
setActiveId(id);
|
|
87
|
+
el.scrollIntoView({ behavior: prefersReducedMotion() ? "auto" : "smooth" });
|
|
88
|
+
history.replaceState(null, "", `#${id}`);
|
|
89
|
+
}, []);
|
|
90
|
+
if (headings.length === 0) return null;
|
|
91
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
92
|
+
"div",
|
|
93
|
+
{
|
|
94
|
+
className: joinClassNames("aranova-blog-toc-panel", className),
|
|
95
|
+
"data-open": open ? "true" : "false",
|
|
96
|
+
"data-collapse": collapse,
|
|
97
|
+
"data-side": side,
|
|
98
|
+
children: [
|
|
99
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
100
|
+
"button",
|
|
101
|
+
{
|
|
102
|
+
type: "button",
|
|
103
|
+
className: "aranova-blog-toc-panel-title",
|
|
104
|
+
"aria-expanded": open,
|
|
105
|
+
"aria-controls": bodyId,
|
|
106
|
+
onClick: () => {
|
|
107
|
+
userToggled.current = true;
|
|
108
|
+
setOpen((o) => !o);
|
|
109
|
+
},
|
|
110
|
+
children: title
|
|
111
|
+
}
|
|
112
|
+
),
|
|
113
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { id: bodyId, className: "aranova-blog-toc-panel-body", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "aranova-blog-toc-panel-clip", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", { "aria-label": "Table of contents", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { children: headings.map((heading) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { "data-depth": heading.depth, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
114
|
+
"a",
|
|
115
|
+
{
|
|
116
|
+
href: `#${heading.id}`,
|
|
117
|
+
title: heading.text,
|
|
118
|
+
className: heading.id === activeId ? "aranova-blog-toc-panel-link is-active" : "aranova-blog-toc-panel-link",
|
|
119
|
+
"data-active": heading.id === activeId ? "true" : void 0,
|
|
120
|
+
"aria-current": heading.id === activeId ? "location" : void 0,
|
|
121
|
+
onClick: (event) => onLinkClick(event, heading.id),
|
|
122
|
+
children: heading.text
|
|
123
|
+
}
|
|
124
|
+
) }, heading.id)) }) }) }) })
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ../tracking-core/src/resources/blog/view/client/reading-progress.tsx
|
|
131
|
+
var import_react2 = require("react");
|
|
132
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
133
|
+
function ReadingProgress({
|
|
134
|
+
target = ".aranova-blog-post",
|
|
135
|
+
height = 3,
|
|
136
|
+
className,
|
|
137
|
+
onProgress,
|
|
138
|
+
showBar = true
|
|
139
|
+
}) {
|
|
140
|
+
const barRef = (0, import_react2.useRef)(null);
|
|
141
|
+
const onProgressRef = (0, import_react2.useRef)(onProgress);
|
|
142
|
+
onProgressRef.current = onProgress;
|
|
143
|
+
(0, import_react2.useEffect)(() => {
|
|
144
|
+
const article = document.querySelector(target);
|
|
145
|
+
if (!(article instanceof HTMLElement)) return;
|
|
146
|
+
let frame = 0;
|
|
147
|
+
const update = () => {
|
|
148
|
+
frame = 0;
|
|
149
|
+
const rect = article.getBoundingClientRect();
|
|
150
|
+
const total = rect.height - window.innerHeight;
|
|
151
|
+
const fraction = total <= 0 ? 1 : Math.min(1, Math.max(0, -rect.top / total));
|
|
152
|
+
const percent = Math.round(fraction * 100);
|
|
153
|
+
const bar = barRef.current;
|
|
154
|
+
if (bar) {
|
|
155
|
+
bar.style.transform = `scaleX(${fraction})`;
|
|
156
|
+
bar.setAttribute("aria-valuenow", String(percent));
|
|
157
|
+
}
|
|
158
|
+
onProgressRef.current?.(fraction, percent);
|
|
159
|
+
};
|
|
160
|
+
const schedule = () => {
|
|
161
|
+
if (frame === 0) frame = requestAnimationFrame(update);
|
|
162
|
+
};
|
|
163
|
+
update();
|
|
164
|
+
window.addEventListener("scroll", schedule, { passive: true });
|
|
165
|
+
window.addEventListener("resize", schedule);
|
|
166
|
+
return () => {
|
|
167
|
+
window.removeEventListener("scroll", schedule);
|
|
168
|
+
window.removeEventListener("resize", schedule);
|
|
169
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
170
|
+
};
|
|
171
|
+
}, [target]);
|
|
172
|
+
if (!showBar) return null;
|
|
173
|
+
const style = { height: `${height}px` };
|
|
174
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
175
|
+
"div",
|
|
176
|
+
{
|
|
177
|
+
ref: barRef,
|
|
178
|
+
className: joinClassNames("aranova-blog-progress", className),
|
|
179
|
+
role: "progressbar",
|
|
180
|
+
"aria-label": "Reading progress",
|
|
181
|
+
"aria-valuemin": 0,
|
|
182
|
+
"aria-valuemax": 100,
|
|
183
|
+
"aria-valuenow": 0,
|
|
184
|
+
style
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
189
|
+
0 && (module.exports = {
|
|
190
|
+
ReadingProgress,
|
|
191
|
+
TocPanel
|
|
192
|
+
});
|
|
193
|
+
//# sourceMappingURL=blog-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/blog-client.ts","../../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// Blog client-island entry — `@aranova/tracking-next/blog-client`. Authored\n// once in tracking-core (see blog-client-public.ts); this file exists as the\n// per-package tsup build entry, exactly like ./phone-react's `phone.ts`.\n//\n// The 'use client' directive must stay on line 1: tsup/esbuild preserves an\n// entry file's leading directive at the top of the emitted bundle, and Next\n// relies on it to treat the whole entry as a client-module boundary. Server\n// code (the enhanced BlogPost in blog-post.tsx) imports these islands ONLY via\n// the `@aranova/tracking-next/blog-client` self-specifier (external in tsup) —\n// a relative import would inline them into dist/blog-server.* under\n// `splitting: false` and silently drop this directive.\nexport * from \"../../tracking-core/src/blog-client-public\";\n","\"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":["import_react","import_jsx_runtime"]}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ../tracking-core/src/resources/blog/view/client/toc-panel.tsx
|
|
4
|
+
import {
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useId,
|
|
8
|
+
useRef,
|
|
9
|
+
useState
|
|
10
|
+
} from "react";
|
|
11
|
+
|
|
12
|
+
// ../tracking-core/src/resources/blog/view/class-names.ts
|
|
13
|
+
function joinClassNames(base, extra) {
|
|
14
|
+
return extra ? `${base} ${extra}` : base;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ../tracking-core/src/resources/blog/view/client/toc-panel.tsx
|
|
18
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
19
|
+
var SPY_ROOT_MARGIN = "-80px 0px -70% 0px";
|
|
20
|
+
function prefersReducedMotion() {
|
|
21
|
+
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
22
|
+
}
|
|
23
|
+
function TocPanel({
|
|
24
|
+
headings,
|
|
25
|
+
title = "On this page",
|
|
26
|
+
defaultOpen = true,
|
|
27
|
+
className,
|
|
28
|
+
collapse = "vertical",
|
|
29
|
+
side = "right"
|
|
30
|
+
}) {
|
|
31
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
32
|
+
const [activeId, setActiveId] = useState(null);
|
|
33
|
+
const bodyId = useId();
|
|
34
|
+
const clickedAt = useRef(0);
|
|
35
|
+
const userToggled = useRef(false);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!defaultOpen || userToggled.current) return;
|
|
38
|
+
if (typeof window.matchMedia === "function" && window.matchMedia("(max-width: 1099px)").matches) {
|
|
39
|
+
setOpen(false);
|
|
40
|
+
}
|
|
41
|
+
}, [defaultOpen]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const targets = headings.map((h) => document.getElementById(h.id)).filter((el) => el !== null);
|
|
44
|
+
if (targets.length === 0) return;
|
|
45
|
+
const visible = /* @__PURE__ */ new Set();
|
|
46
|
+
const observer = new IntersectionObserver(
|
|
47
|
+
(entries) => {
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (entry.isIntersecting) visible.add(entry.target.id);
|
|
50
|
+
else visible.delete(entry.target.id);
|
|
51
|
+
}
|
|
52
|
+
if (Date.now() - clickedAt.current < 700) return;
|
|
53
|
+
const next = headings.find((h) => visible.has(h.id));
|
|
54
|
+
if (next) setActiveId(next.id);
|
|
55
|
+
},
|
|
56
|
+
{ rootMargin: SPY_ROOT_MARGIN }
|
|
57
|
+
);
|
|
58
|
+
for (const el of targets) observer.observe(el);
|
|
59
|
+
return () => observer.disconnect();
|
|
60
|
+
}, [headings]);
|
|
61
|
+
const onLinkClick = useCallback((event, id) => {
|
|
62
|
+
const el = document.getElementById(id);
|
|
63
|
+
if (!el) return;
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
clickedAt.current = Date.now();
|
|
66
|
+
setActiveId(id);
|
|
67
|
+
el.scrollIntoView({ behavior: prefersReducedMotion() ? "auto" : "smooth" });
|
|
68
|
+
history.replaceState(null, "", `#${id}`);
|
|
69
|
+
}, []);
|
|
70
|
+
if (headings.length === 0) return null;
|
|
71
|
+
return /* @__PURE__ */ jsxs(
|
|
72
|
+
"div",
|
|
73
|
+
{
|
|
74
|
+
className: joinClassNames("aranova-blog-toc-panel", className),
|
|
75
|
+
"data-open": open ? "true" : "false",
|
|
76
|
+
"data-collapse": collapse,
|
|
77
|
+
"data-side": side,
|
|
78
|
+
children: [
|
|
79
|
+
/* @__PURE__ */ jsx(
|
|
80
|
+
"button",
|
|
81
|
+
{
|
|
82
|
+
type: "button",
|
|
83
|
+
className: "aranova-blog-toc-panel-title",
|
|
84
|
+
"aria-expanded": open,
|
|
85
|
+
"aria-controls": bodyId,
|
|
86
|
+
onClick: () => {
|
|
87
|
+
userToggled.current = true;
|
|
88
|
+
setOpen((o) => !o);
|
|
89
|
+
},
|
|
90
|
+
children: title
|
|
91
|
+
}
|
|
92
|
+
),
|
|
93
|
+
/* @__PURE__ */ jsx("div", { id: bodyId, className: "aranova-blog-toc-panel-body", children: /* @__PURE__ */ jsx("div", { className: "aranova-blog-toc-panel-clip", children: /* @__PURE__ */ jsx("nav", { "aria-label": "Table of contents", children: /* @__PURE__ */ jsx("ol", { children: headings.map((heading) => /* @__PURE__ */ jsx("li", { "data-depth": heading.depth, children: /* @__PURE__ */ jsx(
|
|
94
|
+
"a",
|
|
95
|
+
{
|
|
96
|
+
href: `#${heading.id}`,
|
|
97
|
+
title: heading.text,
|
|
98
|
+
className: heading.id === activeId ? "aranova-blog-toc-panel-link is-active" : "aranova-blog-toc-panel-link",
|
|
99
|
+
"data-active": heading.id === activeId ? "true" : void 0,
|
|
100
|
+
"aria-current": heading.id === activeId ? "location" : void 0,
|
|
101
|
+
onClick: (event) => onLinkClick(event, heading.id),
|
|
102
|
+
children: heading.text
|
|
103
|
+
}
|
|
104
|
+
) }, heading.id)) }) }) }) })
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ../tracking-core/src/resources/blog/view/client/reading-progress.tsx
|
|
111
|
+
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
112
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
113
|
+
function ReadingProgress({
|
|
114
|
+
target = ".aranova-blog-post",
|
|
115
|
+
height = 3,
|
|
116
|
+
className,
|
|
117
|
+
onProgress,
|
|
118
|
+
showBar = true
|
|
119
|
+
}) {
|
|
120
|
+
const barRef = useRef2(null);
|
|
121
|
+
const onProgressRef = useRef2(onProgress);
|
|
122
|
+
onProgressRef.current = onProgress;
|
|
123
|
+
useEffect2(() => {
|
|
124
|
+
const article = document.querySelector(target);
|
|
125
|
+
if (!(article instanceof HTMLElement)) return;
|
|
126
|
+
let frame = 0;
|
|
127
|
+
const update = () => {
|
|
128
|
+
frame = 0;
|
|
129
|
+
const rect = article.getBoundingClientRect();
|
|
130
|
+
const total = rect.height - window.innerHeight;
|
|
131
|
+
const fraction = total <= 0 ? 1 : Math.min(1, Math.max(0, -rect.top / total));
|
|
132
|
+
const percent = Math.round(fraction * 100);
|
|
133
|
+
const bar = barRef.current;
|
|
134
|
+
if (bar) {
|
|
135
|
+
bar.style.transform = `scaleX(${fraction})`;
|
|
136
|
+
bar.setAttribute("aria-valuenow", String(percent));
|
|
137
|
+
}
|
|
138
|
+
onProgressRef.current?.(fraction, percent);
|
|
139
|
+
};
|
|
140
|
+
const schedule = () => {
|
|
141
|
+
if (frame === 0) frame = requestAnimationFrame(update);
|
|
142
|
+
};
|
|
143
|
+
update();
|
|
144
|
+
window.addEventListener("scroll", schedule, { passive: true });
|
|
145
|
+
window.addEventListener("resize", schedule);
|
|
146
|
+
return () => {
|
|
147
|
+
window.removeEventListener("scroll", schedule);
|
|
148
|
+
window.removeEventListener("resize", schedule);
|
|
149
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
150
|
+
};
|
|
151
|
+
}, [target]);
|
|
152
|
+
if (!showBar) return null;
|
|
153
|
+
const style = { height: `${height}px` };
|
|
154
|
+
return /* @__PURE__ */ jsx2(
|
|
155
|
+
"div",
|
|
156
|
+
{
|
|
157
|
+
ref: barRef,
|
|
158
|
+
className: joinClassNames("aranova-blog-progress", className),
|
|
159
|
+
role: "progressbar",
|
|
160
|
+
"aria-label": "Reading progress",
|
|
161
|
+
"aria-valuemin": 0,
|
|
162
|
+
"aria-valuemax": 100,
|
|
163
|
+
"aria-valuenow": 0,
|
|
164
|
+
style
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
ReadingProgress,
|
|
170
|
+
TocPanel
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=blog-client.mjs.map
|