feat: Add estimated reading time to DocsHeader and DocsLayout

This commit is contained in:
Gauthier Daniels 2025-04-11 12:39:57 +02:00
parent d0dca7d956
commit b7ad77d73b
17 changed files with 125 additions and 208 deletions

View File

@ -1,19 +1,32 @@
import { usePageContext } from "vike-react/usePageContext"; import { usePageContext } from "vike-react/usePageContext";
import { ClockIcon } from "@heroicons/react/24/outline";
import { navigation } from "@/lib/navigation"; import { navigation } from "@/lib/navigation";
export function DocsHeader({ title }: { title?: string }) { type DocsHeaderProps = {
title?: string;
estimatedReadingTime?: string;
};
export function DocsHeader(props: DocsHeaderProps) {
const { urlPathname } = usePageContext(); const { urlPathname } = usePageContext();
const section = navigation.find((section) => section.links.find((link) => link.href === urlPathname)); const section = navigation.find((section) => section.links.find((link) => link.href === urlPathname));
if (!title && !section) { if (!props.title && !section) {
return null; return null;
} }
return ( return (
<header className="mb-9 space-y-1"> <header className="mb-9 space-y-1">
{section && <p className="font-display text-sm font-medium text-violet-500">{section.title}</p>} {section && <p className="font-display text-sm font-medium text-violet-500">{section.title}</p>}
{title && <h1 className="font-display text-3xl tracking-tight text-slate-900 dark:text-white">{title}</h1>} {props.title && (
<h1 className="font-display text-3xl tracking-tight text-slate-900 dark:text-white">{props.title}</h1>
)}
{props.estimatedReadingTime && (
<p className="text-sm text-slate-500 dark:text-slate-400 inline-flex items-center gap-1">
<ClockIcon className="w-4" /> {props.estimatedReadingTime}
</p>
)}
</header> </header>
); );
} }

View File

@ -9,10 +9,12 @@ import { Prose } from "@syntax/Prose";
export function DocsLayout({ export function DocsLayout({
children, children,
frontmatter: { title }, frontmatter: { title },
estimatedReadingTime,
nodes, nodes,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
frontmatter: { title?: string }; frontmatter: { title?: string };
estimatedReadingTime?: string;
nodes: Array<Node>; nodes: Array<Node>;
}) { }) {
let tableOfContents = collectSections(nodes); let tableOfContents = collectSections(nodes);
@ -21,7 +23,7 @@ export function DocsLayout({
<> <>
<div className="max-w-2xl min-w-0 flex-auto px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16"> <div className="max-w-2xl min-w-0 flex-auto px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16">
<article> <article>
<DocsHeader title={title} /> <DocsHeader title={title} estimatedReadingTime={estimatedReadingTime} />
<Prose>{children}</Prose> <Prose>{children}</Prose>
</article> </article>
<PrevNextLinks /> <PrevNextLinks />

View File

@ -18,6 +18,7 @@ const nodes = {
this.render, this.render,
{ {
frontmatter: yaml.load(node.attributes.frontmatter), frontmatter: yaml.load(node.attributes.frontmatter),
estimatedReadingTime: config?.variables?.estimatedReadingTime,
nodes: node.children, nodes: node.children,
}, },
node.transformChildren(config), node.transformChildren(config),

View File

@ -12,6 +12,7 @@
"@fontsource-variable/inter": "^5.2.5", "@fontsource-variable/inter": "^5.2.5",
"@fontsource-variable/lexend": "^5.2.5", "@fontsource-variable/lexend": "^5.2.5",
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.0",
"@heroicons/react": "^2.2.0",
"@markdoc/markdoc": "^0.5.1", "@markdoc/markdoc": "^0.5.1",
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
@ -28,6 +29,7 @@
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-highlight-words": "^0.21.0", "react-highlight-words": "^0.21.0",
"reading-time-estimator": "^1.12.0",
"simple-functional-loader": "^1.2.1", "simple-functional-loader": "^1.2.1",
"telefunc": "^0.1.87", "telefunc": "^0.1.87",
"unplugin-fonts": "^1.3.1", "unplugin-fonts": "^1.3.1",

View File

@ -7,10 +7,10 @@ import tags from "@/markdoc/tags";
import React from "react"; import React from "react";
export default function Page() { export default function Page() {
const { doc } = useData<Data>(); const { doc, estimatedReadingTime } = useData<Data>();
const parsedDoc = Markdoc.parse(doc.content); const parsedDoc = Markdoc.parse(doc.content);
const transformedDoc = Markdoc.transform(parsedDoc, { nodes, tags, variables: {} }); const transformedDoc = Markdoc.transform(parsedDoc, { nodes, tags, variables: { estimatedReadingTime } });
return Markdoc.renderers.react(transformedDoc, React); return Markdoc.renderers.react(transformedDoc, React);
} }

View File

@ -1,6 +1,7 @@
import type { PageContext } from "vike/types"; import type { PageContext } from "vike/types";
import { docsService } from "@/services/DocsService"; import { docsService } from "@/services/DocsService";
import { readingTime } from "reading-time-estimator";
import { useConfig } from "vike-react/useConfig"; import { useConfig } from "vike-react/useConfig";
import buildTitle from "@/pages/buildTitle"; import buildTitle from "@/pages/buildTitle";
import { render } from "vike/abort"; import { render } from "vike/abort";
@ -18,7 +19,7 @@ export async function data(pageContext: PageContext) {
throw render(404); throw render(404);
} }
console.log({ doc }); const readingTimeObject = readingTime(doc.content, 300, "fr");
config({ config({
title: buildTitle(doc.title), title: buildTitle(doc.title),
@ -27,5 +28,5 @@ export async function data(pageContext: PageContext) {
docsService.transform(doc); docsService.transform(doc);
return { doc }; return { doc, estimatedReadingTime: readingTimeObject.text };
} }

View File

@ -1,17 +0,0 @@
import React from "react";
import { useData } from "vike-react/useData";
import type { Data } from "./+data.js";
export default function Page() {
const movie = useData<Data>();
return (
<>
<h1>{movie.title}</h1>
Release Date: {movie.release_date}
<br />
Director: {movie.director}
<br />
Producer: {movie.producer}
</>
);
}

View File

@ -1,32 +0,0 @@
// https://vike.dev/data
import type { PageContextServer } from "vike/types";
import type { MovieDetails } from "../types.js";
import { useConfig } from "vike-react/useConfig";
export type Data = Awaited<ReturnType<typeof data>>;
export const data = async (pageContext: PageContextServer) => {
// https://vike.dev/useConfig
const config = useConfig();
const response = await fetch(`https://brillout.github.io/star-wars/api/films/${pageContext.routeParams.id}.json`);
let movie = (await response.json()) as MovieDetails;
config({
// Set <title>
title: movie.title,
});
// We remove data we don't need because the data is passed to
// the client; we should minimize what is sent over the network.
movie = minimize(movie);
return movie;
};
function minimize(movie: MovieDetails): MovieDetails {
const { id, title, release_date, director, producer } = movie;
const minimizedMovie = { id, title, release_date, director, producer };
return minimizedMovie;
}

View File

@ -1,22 +0,0 @@
import React from "react";
import { useData } from "vike-react/useData";
import type { Data } from "./+data.js";
export default function Page() {
const movies = useData<Data>();
return (
<>
<h1>Star Wars Movies</h1>
<ol>
{movies.map(({ id, title, release_date }) => (
<li key={id}>
<a href={`/star-wars/${id}`}>{title}</a> ({release_date})
</li>
))}
</ol>
<p>
Source: <a href="https://brillout.github.io/star-wars">brillout.github.io/star-wars</a>.
</p>
</>
);
}

View File

@ -1,32 +0,0 @@
// https://vike.dev/data
import type { Movie, MovieDetails } from "../types.js";
import { useConfig } from "vike-react/useConfig";
export type Data = Awaited<ReturnType<typeof data>>;
export const data = async () => {
// https://vike.dev/useConfig
const config = useConfig();
const response = await fetch("https://brillout.github.io/star-wars/api/films.json");
const moviesData = (await response.json()) as MovieDetails[];
config({
// Set <title>
title: `${moviesData.length} Star Wars Movies`,
});
// We remove data we don't need because the data is passed to the client; we should
// minimize what is sent over the network.
const movies = minimize(moviesData);
return movies;
};
function minimize(movies: MovieDetails[]): Movie[] {
return movies.map((movie) => {
const { title, release_date, id } = movie;
return { title, release_date, id };
});
}

View File

@ -1,10 +0,0 @@
export type Movie = {
id: string;
title: string;
release_date: string;
};
export type MovieDetails = Movie & {
director: string;
producer: string;
};

View File

@ -1,14 +0,0 @@
import type { Data } from "./+data";
import React from "react";
import { useData } from "vike-react/useData";
import { TodoList } from "./TodoList.js";
export default function Page() {
const data = useData<Data>();
return (
<>
<h1>To-do List</h1>
<TodoList initialTodoItems={data.todo} />
</>
);
}

View File

@ -1,3 +0,0 @@
export const config = {
prerender: false,
};

View File

@ -1,11 +0,0 @@
// https://vike.dev/data
import { todos } from "../../database/todoItems";
import type { PageContextServer } from "vike/types";
export type Data = {
todo: { text: string }[];
};
export default async function data(_pageContext: PageContextServer): Promise<Data> {
return { todo: todos };
}

View File

@ -1,7 +0,0 @@
// We use Telefunc (https://telefunc.com) for data mutations. Being able to use Telefunc for fetching initial data is work-in-progress (https://vike.dev/data-fetching#tools).
import { todos } from "../../database/todoItems";
export async function onNewTodo({ text }: { text: string }) {
todos.push({ text });
}

View File

@ -1,52 +0,0 @@
import { onNewTodo } from "./TodoList.telefunc";
import React, { useState } from "react";
export function TodoList({ initialTodoItems }: { initialTodoItems: { text: string }[] }) {
const [todoItems, setTodoItems] = useState(initialTodoItems);
const [newTodo, setNewTodo] = useState("");
return (
<>
<ul>
{todoItems.map((todoItem, index) => (
// biome-ignore lint:
<li key={index}>{todoItem.text}</li>
))}
</ul>
<div>
<form
onSubmit={async (ev) => {
ev.preventDefault();
// Optimistic UI update
setTodoItems((prev) => [...prev, { text: newTodo }]);
try {
await onNewTodo({ text: newTodo });
setNewTodo("");
} catch (e) {
console.error(e);
// rollback
setTodoItems((prev) => prev.slice(0, -1));
}
}}
>
<input
type="text"
onChange={(ev) => setNewTodo(ev.target.value)}
value={newTodo}
className={
"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-purple-500 focus:border-purple-500 w-full sm:w-auto p-2 mr-1 mb-1"
}
/>
<button
type="submit"
className={
"text-white bg-purple-700 hover:bg-purple-800 focus:ring-2 focus:outline-hidden focus:ring-purple-300 font-medium rounded-lg text-sm w-full sm:w-auto p-2"
}
>
Add to-do
</button>
</form>
</div>
</>
);
}

98
app/pnpm-lock.yaml generated
View File

@ -26,6 +26,9 @@ importers:
'@headlessui/react': '@headlessui/react':
specifier: ^2.2.0 specifier: ^2.2.0
version: 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) version: 2.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@heroicons/react':
specifier: ^2.2.0
version: 2.2.0(react@19.0.0)
'@markdoc/markdoc': '@markdoc/markdoc':
specifier: ^0.5.1 specifier: ^0.5.1
version: 0.5.1(@types/react@19.0.10)(react@19.0.0) version: 0.5.1(@types/react@19.0.10)(react@19.0.0)
@ -74,6 +77,9 @@ importers:
react-highlight-words: react-highlight-words:
specifier: ^0.21.0 specifier: ^0.21.0
version: 0.21.0(react@19.0.0) version: 0.21.0(react@19.0.0)
reading-time-estimator:
specifier: ^1.12.0
version: 1.12.0
simple-functional-loader: simple-functional-loader:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
@ -719,6 +725,11 @@ packages:
react: ^18 || ^19 || ^19.0.0-rc react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc
'@heroicons/react@2.2.0':
resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==}
peerDependencies:
react: '>= 16 || ^19.0.0-rc'
'@humanfs/core@0.19.1': '@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'} engines: {node: '>=18.18.0'}
@ -1401,6 +1412,10 @@ packages:
deep-is@0.1.4: deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
define-data-property@1.1.4: define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1425,6 +1440,19 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
dunder-proto@1.0.1: dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1449,6 +1477,10 @@ packages:
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
es-abstract@1.23.9: es-abstract@1.23.9:
resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1772,6 +1804,9 @@ packages:
highlight-words-core@1.2.3: highlight-words-core@1.2.3:
resolution: {integrity: sha512-m1O9HW3/GNHxzSIXWw1wCNXXsgLlxrP0OI6+ycGUhiUHkikqW3OrwVHz+lxeNBe5yqLESdIcj8PowHQ2zLvUvQ==} resolution: {integrity: sha512-m1O9HW3/GNHxzSIXWw1wCNXXsgLlxrP0OI6+ycGUhiUHkikqW3OrwVHz+lxeNBe5yqLESdIcj8PowHQ2zLvUvQ==}
htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
http-errors@2.0.0: http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -1867,6 +1902,10 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'} engines: {node: '>=0.12.0'}
is-plain-object@5.0.0:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
is-regex@1.2.1: is-regex@1.2.1:
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2180,6 +2219,9 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'} engines: {node: '>=6'}
parse-srcset@1.0.2:
resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==}
path-browserify@1.0.1: path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
@ -2310,6 +2352,9 @@ packages:
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
reading-time-estimator@1.12.0:
resolution: {integrity: sha512-A17wehSIG6bxen84mf5m+MP4bDDZytcfMOfJWDp1DT8StGRgljtD58zC6E+fSnQZvU5mfsjiJZ7BLhr9kHYoQQ==}
real-require@0.2.0: real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'} engines: {node: '>= 12.13.0'}
@ -2385,6 +2430,9 @@ packages:
safer-buffer@2.1.2: safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
sanitize-html@2.15.0:
resolution: {integrity: sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==}
scheduler@0.25.0: scheduler@0.25.0:
resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==}
@ -3298,6 +3346,10 @@ snapshots:
react: 19.0.0 react: 19.0.0
react-dom: 19.0.0(react@19.0.0) react-dom: 19.0.0(react@19.0.0)
'@heroicons/react@2.2.0(react@19.0.0)':
dependencies:
react: 19.0.0
'@humanfs/core@0.19.1': {} '@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6': '@humanfs/node@0.16.6':
@ -4007,6 +4059,8 @@ snapshots:
deep-is@0.1.4: {} deep-is@0.1.4: {}
deepmerge@4.3.1: {}
define-data-property@1.1.4: define-data-property@1.1.4:
dependencies: dependencies:
es-define-property: 1.0.1 es-define-property: 1.0.1
@ -4029,6 +4083,24 @@ snapshots:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
domelementtype@2.3.0: {}
domhandler@5.0.3:
dependencies:
domelementtype: 2.3.0
domutils@3.2.2:
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dunder-proto@1.0.1: dunder-proto@1.0.1:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@ -4050,6 +4122,8 @@ snapshots:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
tapable: 2.2.1 tapable: 2.2.1
entities@4.5.0: {}
es-abstract@1.23.9: es-abstract@1.23.9:
dependencies: dependencies:
array-buffer-byte-length: 1.0.2 array-buffer-byte-length: 1.0.2
@ -4535,6 +4609,13 @@ snapshots:
highlight-words-core@1.2.3: {} highlight-words-core@1.2.3: {}
htmlparser2@8.0.2:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.2.2
entities: 4.5.0
http-errors@2.0.0: http-errors@2.0.0:
dependencies: dependencies:
depd: 2.0.0 depd: 2.0.0
@ -4634,6 +4715,8 @@ snapshots:
is-number@7.0.0: {} is-number@7.0.0: {}
is-plain-object@5.0.0: {}
is-regex@1.2.1: is-regex@1.2.1:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4
@ -4918,6 +5001,8 @@ snapshots:
dependencies: dependencies:
callsites: 3.1.0 callsites: 3.1.0
parse-srcset@1.0.2: {}
path-browserify@1.0.1: {} path-browserify@1.0.1: {}
path-exists@4.0.0: {} path-exists@4.0.0: {}
@ -5043,6 +5128,10 @@ snapshots:
react@19.0.0: {} react@19.0.0: {}
reading-time-estimator@1.12.0:
dependencies:
sanitize-html: 2.15.0
real-require@0.2.0: {} real-require@0.2.0: {}
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
@ -5143,6 +5232,15 @@ snapshots:
safer-buffer@2.1.2: {} safer-buffer@2.1.2: {}
sanitize-html@2.15.0:
dependencies:
deepmerge: 4.3.1
escape-string-regexp: 4.0.0
htmlparser2: 8.0.2
is-plain-object: 5.0.0
parse-srcset: 1.0.2
postcss: 8.5.3
scheduler@0.25.0: {} scheduler@0.25.0: {}
search-insights@2.17.3: {} search-insights@2.17.3: {}