@tuwaio/docs-ui 0.3.2 → 0.4.0

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 CHANGED
@@ -12,8 +12,10 @@
12
12
  - **Nextra layout parts:** `Navbar` and `Footer` wrap the navbar and footer of `nextra-theme-docs` with the site logo and a row of external links (`baseNavLinks`, `baseFooterLinks` are the defaults: the npm and GitHub organizations of TUWA). The footer also links the policies in force on tuwa.io and the list of all policies on the docs hub.
13
13
  - **Logo:** `RemoteLogo` is an async React Server Component that fetches an SVG (by default the TUWA logo) and renders it as React elements, so it takes `className` and `currentColor`.
14
14
  - **Theme switch:** `ThemeSwitcher` is a light/dark switch for any theme state (for example `next-themes`).
15
+ - **Interface settings for 30 days:** `PreferenceExpiry` is an inline script for the `<head>` of a site's root layout. It forgets the Local Storage values of the given keys (by default `theme`) 30 days after they were written, as the [Cookie Policy](https://tuwa.io/cookie) states, and dates each later write, whatever library writes it (`next-themes`, the `Tabs` of Nextra).
15
16
  - **Decoration:** `Orb` (an animated sphere in the brand color of a TUWA project), `StarryBackground` (an animated star field on a canvas) and `NoSSR` (renders its children only on the client).
16
17
  - **Policy pages:** `policies` lists the TUWA policies (Terms, Privacy, Cookie, Refund, Security, Data Processing Addendum) with their paths and whether they are in force (`publishedPolicies`; footers use `footerPolicies`). `loadPolicy` loads the text of a policy, and `PolicyDocument` renders it with the date of the version, a Download PDF button, anchored sections and a list of sections. The texts and PDFs come from [`TuwaIO/workflows/docs`](https://github.com/TuwaIO/workflows/tree/main/docs). Code that needs only the list or `loadPolicy` (footers, sitemaps, route handlers) imports them from `@tuwaio/docs-ui/policies`, which has no React, Next.js or Nextra imports.
18
+ - **Search index:** the `tuwa-docs-search-index` command builds the [Pagefind](https://pagefind.app) index that the Nextra search loads from `/_pagefind`, from the pages `next build` prerendered. It reads both places Next.js keeps them: `.next/server/app` and, in builds with a deployment adapter (Next.js 16.3.8 and later on Vercel), `.next/server/route-cache`, where `pagefind --site .next/server/app` finds no pages.
17
19
  - **Stylesheet:** `dist/index.css` contains the `--tuwa-*` theme variables of [`@tuwaio/nova-core`](https://stories.tuwa.io/?path=/docs/packages-nova-core-overview--docs) (light and `.dark`), the styles of the Nextra parts and the Tailwind CSS v4 utilities used by the components, prefixed with `tuwadocs:`.
18
20
 
19
21
  ---
@@ -41,7 +43,8 @@ Import the stylesheet after the Nextra theme:
41
43
  The root layout of a TUWA documentation site:
42
44
 
43
45
  ```tsx
44
- import { baseNavLinks, Footer, Navbar, RemoteLogo } from '@tuwaio/docs-ui';
46
+ import { baseNavLinks, Footer, Navbar, PreferenceExpiry, RemoteLogo } from '@tuwaio/docs-ui';
47
+ import { Head } from 'nextra/components';
45
48
  import { getPageMap } from 'nextra/page-map';
46
49
  import { Layout } from 'nextra-theme-docs';
47
50
  import type { ReactNode } from 'react';
@@ -60,6 +63,9 @@ const navLinks = [
60
63
  export default async function RootLayout({ children }: { children: ReactNode }) {
61
64
  return (
62
65
  <html lang="en" suppressHydrationWarning>
66
+ <Head>
67
+ <PreferenceExpiry keys={['theme']} />
68
+ </Head>
63
69
  <body>
64
70
  <Layout
65
71
  navbar={<Navbar logo={logo} links={navLinks} />}
@@ -115,11 +121,29 @@ export function PolicyLinks() {
115
121
 
116
122
  Sections have stable anchors (`/refund#how-to-request-a-refund`), so hints and FAQ answers can link to a section.
117
123
 
124
+ ### Search index
125
+
126
+ Run the command after `next build`. It uses the `pagefind` package of the site, so add it to the site's dependencies:
127
+
128
+ ```json
129
+ {
130
+ "scripts": {
131
+ "build": "next build && pnpm postbuild",
132
+ "postbuild": "tuwa-docs-search-index"
133
+ },
134
+ "dependencies": {
135
+ "pagefind": "^1.5.2"
136
+ }
137
+ }
138
+ ```
139
+
140
+ It writes the index to `public/_pagefind` (keep that directory in `.gitignore`) with the same URLs as `pagefind --site .next/server/app`, and fails when the build has no prerendered pages.
141
+
118
142
  ---
119
143
 
120
144
  ## 🗄️ Browser Storage
121
145
 
122
- Nothing. `ThemeSwitcher` only calls `onToggle`; the site keeps the theme.
146
+ `ThemeSwitcher` stores nothing; it only calls `onToggle`, and the site keeps the theme. `PreferenceExpiry` stores `<key>:expires` (the time in epoch milliseconds) next to each key it is given, removes both once that time has passed, and wraps `Storage.prototype.setItem` on the page to date later writes of those keys.
123
147
 
124
148
  ## 🌐 External Services
125
149
 
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ // `tuwa-docs-search-index`: the postbuild step of the TUWA documentation sites, after `next build` (see README)
3
+ import { buildSearchIndex } from '../dist/searchIndex.js';
4
+
5
+ buildSearchIndex().then(
6
+ ({ urls }) => console.log(`Pagefind: indexed ${urls.length} pages into public/_pagefind`),
7
+ (error) => {
8
+ console.error(error instanceof Error ? error.message : error);
9
+ process.exit(1);
10
+ },
11
+ );
@@ -22,21 +22,21 @@ __export(Footer_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(Footer_exports);
24
24
  var import_jsx_runtime = require("react/jsx-runtime");
25
- var import_nova_core = require("@tuwaio/nova-core");
25
+ var import_utils = require("@tuwaio/nova-core/utils");
26
26
  var import_nextra_theme_docs = require("nextra-theme-docs");
27
27
  var import_policies = require("../policies");
28
- var import_utils = require("../utils");
28
+ var import_utils2 = require("../utils");
29
29
  function Footer({ links, logo }) {
30
30
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_nextra_theme_docs.Footer, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwadocs:flex tuwadocs:w-full tuwadocs:flex-col tuwadocs:items-center tuwadocs:sm:items-start tuwa-footer-border tuwadocs:pt-8", children: [
31
31
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwadocs:mb-6 tuwadocs:flex tuwadocs:items-center tuwadocs:gap-4 tuwadocs:w-full tuwadocs:justify-between", children: [
32
32
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tuwadocs:flex tuwadocs:items-center", children: logo }),
33
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tuwadocs:flex tuwadocs:items-center tuwadocs:gap-4 tuwadocs:text-sm", children: (links ?? import_utils.baseFooterLinks).map(({ title, href, image, className }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
33
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tuwadocs:flex tuwadocs:items-center tuwadocs:gap-4 tuwadocs:text-sm", children: (links ?? import_utils2.baseFooterLinks).map(({ title, href, image, className }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
34
34
  "a",
35
35
  {
36
36
  href,
37
37
  target: "_blank",
38
38
  rel: "noopener noreferrer",
39
- className: (0, import_nova_core.cn)("tuwa-footer-link tuwadocs:flex tuwadocs:items-center tuwadocs:gap-1", className),
39
+ className: (0, import_utils.cn)("tuwa-footer-link tuwadocs:flex tuwadocs:items-center tuwadocs:gap-1", className),
40
40
  children: [
41
41
  image,
42
42
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "tuwadocs:hidden tuwadocs:sm:inline", children: title })
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { cn } from "@tuwaio/nova-core";
2
+ import { cn } from "@tuwaio/nova-core/utils";
3
3
  import { Footer as NextraFooter } from "nextra-theme-docs";
4
4
  import { footerPolicies, getPolicyPageUrl, POLICIES_INDEX_URL } from "../policies";
5
5
  import { baseFooterLinks } from "../utils";
@@ -21,4 +21,5 @@ interface RemoteLogoProps extends React__default.SVGProps<SVGSVGElement> {
21
21
  */
22
22
  declare function RemoteLogo({ url, className, style, ...props }: RemoteLogoProps): Promise<string | React__default.JSX.Element>;
23
23
 
24
- export { RemoteLogo, type RemoteLogoProps };
24
+ export { RemoteLogo };
25
+ export type { RemoteLogoProps };
@@ -21,4 +21,5 @@ interface RemoteLogoProps extends React__default.SVGProps<SVGSVGElement> {
21
21
  */
22
22
  declare function RemoteLogo({ url, className, style, ...props }: RemoteLogoProps): Promise<string | React__default.JSX.Element>;
23
23
 
24
- export { RemoteLogo, type RemoteLogoProps };
24
+ export { RemoteLogo };
25
+ export type { RemoteLogoProps };
@@ -22,17 +22,17 @@ __export(NavBar_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(NavBar_exports);
24
24
  var import_jsx_runtime = require("react/jsx-runtime");
25
- var import_nova_core = require("@tuwaio/nova-core");
25
+ var import_utils = require("@tuwaio/nova-core/utils");
26
26
  var import_nextra_theme_docs = require("nextra-theme-docs");
27
- var import_utils = require("../utils");
27
+ var import_utils2 = require("../utils");
28
28
  function Navbar({ links, logo }) {
29
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_nextra_theme_docs.Navbar, { logo, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tuwadocs:flex tuwadocs:items-center tuwadocs:gap-3", children: (links ?? import_utils.baseNavLinks).map(({ title, href, image, className }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
29
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_nextra_theme_docs.Navbar, { logo, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tuwadocs:flex tuwadocs:items-center tuwadocs:gap-3", children: (links ?? import_utils2.baseNavLinks).map(({ title, href, image, className }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
30
30
  "a",
31
31
  {
32
32
  href,
33
33
  target: "_blank",
34
34
  rel: "noopener noreferrer",
35
- className: (0, import_nova_core.cn)(
35
+ className: (0, import_utils.cn)(
36
36
  "tuwadocs:hidden tuwadocs:sm:flex tuwadocs:items-center tuwadocs:gap-1 tuwadocs:px-3 tuwadocs:py-1.5 tuwadocs:text-sm tuwadocs:font-medium tuwadocs:transition-colors tuwadocs:text-[var(--tuwa-text-primary)]",
37
37
  className
38
38
  ),
@@ -32,4 +32,5 @@ interface NavProps {
32
32
  */
33
33
  declare function Navbar({ links, logo }: NavProps): React.JSX.Element;
34
34
 
35
- export { type NavProps, Navbar, type SocialLink };
35
+ export { Navbar };
36
+ export type { NavProps, SocialLink };
@@ -32,4 +32,5 @@ interface NavProps {
32
32
  */
33
33
  declare function Navbar({ links, logo }: NavProps): React.JSX.Element;
34
34
 
35
- export { type NavProps, Navbar, type SocialLink };
35
+ export { Navbar };
36
+ export type { NavProps, SocialLink };
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { cn } from "@tuwaio/nova-core";
2
+ import { cn } from "@tuwaio/nova-core/utils";
3
3
  import { Navbar as NextraNavbar } from "nextra-theme-docs";
4
4
  import { baseNavLinks } from "../utils";
5
5
  function Navbar({ links, logo }) {
@@ -33,7 +33,7 @@ __export(Orb_exports, {
33
33
  });
34
34
  module.exports = __toCommonJS(Orb_exports);
35
35
  var import_jsx_runtime = require("react/jsx-runtime");
36
- var import_nova_core = require("@tuwaio/nova-core");
36
+ var import_utils = require("@tuwaio/nova-core/utils");
37
37
  var import_react = __toESM(require("react"), 1);
38
38
  const packageColors = {
39
39
  siwx: "#10b981",
@@ -294,7 +294,7 @@ const Orb = import_react.default.forwardRef(
294
294
  "div",
295
295
  {
296
296
  ref,
297
- className: (0, import_nova_core.cn)(
297
+ className: (0, import_utils.cn)(
298
298
  "tuwadocs:relative tuwadocs:flex tuwadocs:items-center tuwadocs:justify-center tuwa-orb-float",
299
299
  className
300
300
  ),
@@ -26,4 +26,5 @@ interface OrbProps extends React__default.HTMLAttributes<HTMLDivElement> {
26
26
  */
27
27
  declare const Orb: React__default.ForwardRefExoticComponent<OrbProps & React__default.RefAttributes<HTMLDivElement>>;
28
28
 
29
- export { Orb, type OrbProps, type PackageType };
29
+ export { Orb };
30
+ export type { OrbProps, PackageType };
@@ -26,4 +26,5 @@ interface OrbProps extends React__default.HTMLAttributes<HTMLDivElement> {
26
26
  */
27
27
  declare const Orb: React__default.ForwardRefExoticComponent<OrbProps & React__default.RefAttributes<HTMLDivElement>>;
28
28
 
29
- export { Orb, type OrbProps, type PackageType };
29
+ export { Orb };
30
+ export type { OrbProps, PackageType };
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { cn } from "@tuwaio/nova-core";
3
+ import { cn } from "@tuwaio/nova-core/utils";
4
4
  import React from "react";
5
5
  const packageColors = {
6
6
  siwx: "#10b981",
@@ -33,7 +33,7 @@ __export(PolicyDocument_exports, {
33
33
  module.exports = __toCommonJS(PolicyDocument_exports);
34
34
  var import_jsx_runtime = require("react/jsx-runtime");
35
35
  var import_outline = require("@heroicons/react/24/outline");
36
- var import_nova_core = require("@tuwaio/nova-core");
36
+ var import_utils = require("@tuwaio/nova-core/utils");
37
37
  var import_html_react_parser = __toESM(require("html-react-parser"), 1);
38
38
  var import_policies = require("../policies");
39
39
  const BLOCKED_ELEMENTS = /* @__PURE__ */ new Set([
@@ -80,7 +80,7 @@ function PolicyDocument({
80
80
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
81
81
  "div",
82
82
  {
83
- className: (0, import_nova_core.cn)(
83
+ className: (0, import_utils.cn)(
84
84
  "tuwadocs:grid tuwadocs:grid-cols-1 tuwadocs:gap-10 tuwadocs:lg:grid-cols-[minmax(0,1fr)_14rem]",
85
85
  className
86
86
  ),
@@ -89,7 +89,7 @@ function PolicyDocument({
89
89
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
90
90
  "header",
91
91
  {
92
- className: (0, import_nova_core.cn)(
92
+ className: (0, import_utils.cn)(
93
93
  "tuwadocs:mb-8 tuwadocs:flex tuwadocs:flex-col tuwadocs:gap-4 tuwadocs:border-b tuwadocs:border-[var(--tuwa-border-primary)] tuwadocs:pb-6",
94
94
  classNames?.header
95
95
  ),
@@ -97,7 +97,7 @@ function PolicyDocument({
97
97
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
98
98
  "h1",
99
99
  {
100
- className: (0, import_nova_core.cn)(
100
+ className: (0, import_utils.cn)(
101
101
  "tuwadocs-policy-title tuwadocs:text-3xl tuwadocs:font-extrabold tuwadocs:tracking-tight tuwadocs:text-[var(--tuwa-text-primary)] tuwadocs:sm:text-4xl",
102
102
  classNames?.title
103
103
  ),
@@ -105,7 +105,7 @@ function PolicyDocument({
105
105
  }
106
106
  ),
107
107
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:justify-between tuwadocs:gap-3", children: [
108
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: (0, import_nova_core.cn)("tuwadocs:text-sm tuwadocs:text-[var(--tuwa-text-secondary)]", classNames?.updated), children: [
108
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: (0, import_utils.cn)("tuwadocs:text-sm tuwadocs:text-[var(--tuwa-text-secondary)]", classNames?.updated), children: [
109
109
  labels?.updated ?? "Last updated",
110
110
  " ",
111
111
  content.updated
@@ -116,7 +116,7 @@ function PolicyDocument({
116
116
  href: (0, import_policies.getPolicyFileUrl)(slug, "pdf", source),
117
117
  target: "_blank",
118
118
  rel: "noopener noreferrer",
119
- className: (0, import_nova_core.cn)(
119
+ className: (0, import_utils.cn)(
120
120
  "tuwadocs-policy-title tuwadocs:inline-flex tuwadocs:h-10 tuwadocs:no-underline tuwadocs:items-center tuwadocs:gap-2 tuwadocs:rounded-[var(--tuwa-rounded-corners)] tuwadocs:border tuwadocs:border-[var(--tuwa-border-primary)] tuwadocs:bg-[var(--tuwa-bg-primary)] tuwadocs:px-4 tuwadocs:text-sm tuwadocs:font-bold tuwadocs:text-[var(--tuwa-text-primary)] tuwadocs:transition-colors tuwadocs:hover:bg-[var(--tuwa-bg-muted)]",
121
121
  classNames?.download
122
122
  ),
@@ -130,13 +130,13 @@ function PolicyDocument({
130
130
  ]
131
131
  }
132
132
  ),
133
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("article", { className: (0, import_nova_core.cn)("tuwadocs-policy", classNames?.body), children: (0, import_html_react_parser.default)(content.html, { replace: sanitize }) })
133
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("article", { className: (0, import_utils.cn)("tuwadocs-policy", classNames?.body), children: (0, import_html_react_parser.default)(content.html, { replace: sanitize }) })
134
134
  ] }),
135
135
  content.sections.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
136
136
  "nav",
137
137
  {
138
138
  "aria-label": tocLabel,
139
- className: (0, import_nova_core.cn)(
139
+ className: (0, import_utils.cn)(
140
140
  "tuwadocs:hidden tuwadocs:lg:sticky tuwadocs:lg:top-24 tuwadocs:lg:block tuwadocs:lg:self-start",
141
141
  classNames?.toc
142
142
  ),
@@ -54,4 +54,5 @@ interface PolicyDocumentProps {
54
54
  */
55
55
  declare function PolicyDocument({ slug, content, source, labels, className, classNames, }: PolicyDocumentProps): React.JSX.Element;
56
56
 
57
- export { PolicyDocument, type PolicyDocumentProps };
57
+ export { PolicyDocument };
58
+ export type { PolicyDocumentProps };
@@ -54,4 +54,5 @@ interface PolicyDocumentProps {
54
54
  */
55
55
  declare function PolicyDocument({ slug, content, source, labels, className, classNames, }: PolicyDocumentProps): React.JSX.Element;
56
56
 
57
- export { PolicyDocument, type PolicyDocumentProps };
57
+ export { PolicyDocument };
58
+ export type { PolicyDocumentProps };
@@ -1,6 +1,6 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
3
- import { cn } from "@tuwaio/nova-core";
3
+ import { cn } from "@tuwaio/nova-core/utils";
4
4
  import parse, { Element } from "html-react-parser";
5
5
  import { getPolicyFileUrl, POLICY_SOURCE } from "../policies";
6
6
  const BLOCKED_ELEMENTS = /* @__PURE__ */ new Set([
@@ -24,7 +24,7 @@ __export(ThemeSwitcher_exports, {
24
24
  module.exports = __toCommonJS(ThemeSwitcher_exports);
25
25
  var import_jsx_runtime = require("react/jsx-runtime");
26
26
  var import_solid = require("@heroicons/react/24/solid");
27
- var import_nova_core = require("@tuwaio/nova-core");
27
+ var import_utils = require("@tuwaio/nova-core/utils");
28
28
  var import_react = require("react");
29
29
  function ThemeSwitcher({ theme, onToggle }) {
30
30
  const [mounted, setMounted] = (0, import_react.useState)(false);
@@ -36,7 +36,7 @@ function ThemeSwitcher({ theme, onToggle }) {
36
36
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
37
37
  "div",
38
38
  {
39
- className: (0, import_nova_core.cn)(
39
+ className: (0, import_utils.cn)(
40
40
  "tuwadocs:relative tuwadocs:flex tuwadocs:h-10 tuwadocs:w-20 tuwadocs:md:h-8 tuwadocs:md:w-16 tuwadocs:rounded-[var(--tuwa-rounded-corners)] tuwadocs:border tuwadocs:opacity-0",
41
41
  defaultStyles
42
42
  )
@@ -48,7 +48,7 @@ function ThemeSwitcher({ theme, onToggle }) {
48
48
  "button",
49
49
  {
50
50
  onClick: onToggle,
51
- className: (0, import_nova_core.cn)(
51
+ className: (0, import_utils.cn)(
52
52
  "tuwadocs:group tuwadocs:relative tuwadocs:flex tuwadocs:h-10 tuwadocs:w-20 tuwadocs:md:h-8 tuwadocs:md:w-16 tuwadocs:cursor-pointer tuwadocs:items-center tuwadocs:rounded-[var(--tuwa-rounded-corners)] tuwadocs:p-1 tuwadocs:transition-all tuwadocs:duration-300 tuwadocs:ease-in-out tuwadocs:backdrop-blur-sm tuwadocs:border",
53
53
  defaultStyles
54
54
  ),
@@ -57,7 +57,7 @@ function ThemeSwitcher({ theme, onToggle }) {
57
57
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
58
58
  "div",
59
59
  {
60
- className: (0, import_nova_core.cn)(
60
+ className: (0, import_utils.cn)(
61
61
  "tuwadocs:absolute tuwadocs:top-[1px] tuwadocs:left-[2px] tuwadocs:z-10 tuwadocs:h-9 tuwadocs:w-9 tuwadocs:rounded-[var(--tuwa-rounded-corners)] tuwadocs:bg-[var(--tuwa-bg-primary)] tuwadocs:shadow-md tuwadocs:md:hidden",
62
62
  isDark ? "tuwa-slide-dark" : "tuwa-slide-light"
63
63
  )
@@ -66,7 +66,7 @@ function ThemeSwitcher({ theme, onToggle }) {
66
66
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
67
67
  "div",
68
68
  {
69
- className: (0, import_nova_core.cn)(
69
+ className: (0, import_utils.cn)(
70
70
  "tuwadocs:absolute tuwadocs:top-[1px] tuwadocs:left-[2px] tuwadocs:z-10 tuwadocs:h-9 tuwadocs:w-9 tuwadocs:md:h-7 tuwadocs:md:w-7 tuwadocs:rounded-[var(--tuwa-rounded-corners)] tuwadocs:bg-[var(--tuwa-bg-primary)] tuwadocs:shadow-md tuwadocs:hidden tuwadocs:md:block",
71
71
  isDark ? "tuwa-slide-dark" : "tuwa-slide-light"
72
72
  )
@@ -19,4 +19,5 @@ interface ThemeSwitcherProps {
19
19
  */
20
20
  declare function ThemeSwitcher({ theme, onToggle }: ThemeSwitcherProps): React.JSX.Element;
21
21
 
22
- export { ThemeSwitcher, type ThemeSwitcherProps };
22
+ export { ThemeSwitcher };
23
+ export type { ThemeSwitcherProps };
@@ -19,4 +19,5 @@ interface ThemeSwitcherProps {
19
19
  */
20
20
  declare function ThemeSwitcher({ theme, onToggle }: ThemeSwitcherProps): React.JSX.Element;
21
21
 
22
- export { ThemeSwitcher, type ThemeSwitcherProps };
22
+ export { ThemeSwitcher };
23
+ export type { ThemeSwitcherProps };
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { MoonIcon, SunIcon } from "@heroicons/react/24/solid";
4
- import { cn } from "@tuwaio/nova-core";
4
+ import { cn } from "@tuwaio/nova-core/utils";
5
5
  import { useEffect, useState } from "react";
6
6
  function ThemeSwitcher({ theme, onToggle }) {
7
7
  const [mounted, setMounted] = useState(false);
package/dist/index.cjs CHANGED
@@ -42,6 +42,7 @@ __reExport(index_exports, require("./components/PolicyDocument"), module.exports
42
42
  var import_StarryBackground = __toESM(require("./components/StarryBackground"), 1);
43
43
  __reExport(index_exports, require("./components/ThemeSwitcher"), module.exports);
44
44
  __reExport(index_exports, require("./policies"), module.exports);
45
+ __reExport(index_exports, require("./preferences"), module.exports);
45
46
  __reExport(index_exports, require("./utils"), module.exports);
46
47
  // Annotate the CommonJS export names for ESM import in node:
47
48
  0 && (module.exports = {
@@ -54,5 +55,6 @@ __reExport(index_exports, require("./utils"), module.exports);
54
55
  ...require("./components/PolicyDocument"),
55
56
  ...require("./components/ThemeSwitcher"),
56
57
  ...require("./policies"),
58
+ ...require("./preferences"),
57
59
  ...require("./utils")
58
60
  });
package/dist/index.d.cts CHANGED
@@ -7,5 +7,6 @@ export { PolicyDocument, PolicyDocumentProps } from './components/PolicyDocument
7
7
  export { default as StarryBackground } from './components/StarryBackground.cjs';
8
8
  export { ThemeSwitcher, ThemeSwitcherProps } from './components/ThemeSwitcher.cjs';
9
9
  export { POLICIES_INDEX_URL, POLICY_SITE_URL, POLICY_SOURCE, Policy, PolicyContent, PolicySection, PolicySlug, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, publishedPolicies } from './policies.cjs';
10
+ export { PREFERENCE_DAYS, PreferenceExpiry, expireStoredPreferences } from './preferences.cjs';
10
11
  export { baseFooterLinks, baseNavLinks } from './utils.cjs';
11
12
  import 'react';
package/dist/index.d.ts CHANGED
@@ -7,5 +7,6 @@ export { PolicyDocument, PolicyDocumentProps } from './components/PolicyDocument
7
7
  export { default as StarryBackground } from './components/StarryBackground.js';
8
8
  export { ThemeSwitcher, ThemeSwitcherProps } from './components/ThemeSwitcher.js';
9
9
  export { POLICIES_INDEX_URL, POLICY_SITE_URL, POLICY_SOURCE, Policy, PolicyContent, PolicySection, PolicySlug, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, publishedPolicies } from './policies.js';
10
+ export { PREFERENCE_DAYS, PreferenceExpiry, expireStoredPreferences } from './preferences.js';
10
11
  export { baseFooterLinks, baseNavLinks } from './utils.js';
11
12
  import 'react';
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ export * from "./components/PolicyDocument";
7
7
  import { default as default3 } from "./components/StarryBackground";
8
8
  export * from "./components/ThemeSwitcher";
9
9
  export * from "./policies";
10
+ export * from "./preferences";
10
11
  export * from "./utils";
11
12
  export {
12
13
  default2 as NoSSR,
@@ -85,4 +85,5 @@ declare function loadPolicy(slug: PolicySlug, options?: {
85
85
  revalidate?: number | false;
86
86
  }): Promise<PolicyContent>;
87
87
 
88
- export { POLICIES_INDEX_URL, POLICY_SITE_URL, POLICY_SOURCE, type Policy, type PolicyContent, type PolicySection, type PolicySlug, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, publishedPolicies };
88
+ export { POLICIES_INDEX_URL, POLICY_SITE_URL, POLICY_SOURCE, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, publishedPolicies };
89
+ export type { Policy, PolicyContent, PolicySection, PolicySlug };
@@ -85,4 +85,5 @@ declare function loadPolicy(slug: PolicySlug, options?: {
85
85
  revalidate?: number | false;
86
86
  }): Promise<PolicyContent>;
87
87
 
88
- export { POLICIES_INDEX_URL, POLICY_SITE_URL, POLICY_SOURCE, type Policy, type PolicyContent, type PolicySection, type PolicySlug, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, publishedPolicies };
88
+ export { POLICIES_INDEX_URL, POLICY_SITE_URL, POLICY_SOURCE, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, publishedPolicies };
89
+ export type { Policy, PolicyContent, PolicySection, PolicySlug };
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var preferences_exports = {};
20
+ __export(preferences_exports, {
21
+ PREFERENCE_DAYS: () => PREFERENCE_DAYS,
22
+ PreferenceExpiry: () => PreferenceExpiry,
23
+ expireStoredPreferences: () => expireStoredPreferences
24
+ });
25
+ module.exports = __toCommonJS(preferences_exports);
26
+ var import_jsx_runtime = require("react/jsx-runtime");
27
+ const PREFERENCE_DAYS = 30;
28
+ function expireStoredPreferences(storage, proto, keys, ms, now) {
29
+ try {
30
+ for (const key of keys) {
31
+ const expiresAt = Number(storage.getItem(key + ":expires"));
32
+ if (!expiresAt || expiresAt <= now) {
33
+ storage.removeItem(key);
34
+ storage.removeItem(key + ":expires");
35
+ }
36
+ }
37
+ const setItem = proto.setItem;
38
+ proto.setItem = function(key, value) {
39
+ setItem.call(this, key, value);
40
+ if (this === storage && keys.indexOf(key) >= 0) setItem.call(this, key + ":expires", String(Date.now() + ms));
41
+ };
42
+ } catch {
43
+ }
44
+ }
45
+ function PreferenceExpiry({ keys = ["theme"] }) {
46
+ const ms = PREFERENCE_DAYS * 864e5;
47
+ const code = `(${expireStoredPreferences.toString()})(window.localStorage, Storage.prototype, ${JSON.stringify(keys)}, ${ms}, Date.now())`;
48
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("script", { dangerouslySetInnerHTML: { __html: code } });
49
+ }
50
+ // Annotate the CommonJS export names for ESM import in node:
51
+ 0 && (module.exports = {
52
+ PREFERENCE_DAYS,
53
+ PreferenceExpiry,
54
+ expireStoredPreferences
55
+ });
@@ -0,0 +1,28 @@
1
+ import * as React from 'react';
2
+
3
+ /** How long the TUWA sites keep an interface setting after it is chosen (Cookie Policy). */
4
+ declare const PREFERENCE_DAYS = 30;
5
+ /**
6
+ * Forgets the given Local Storage values once their 30 days have passed (or when they were saved before values had an
7
+ * expiry), then dates every later write of those keys: `<key>:expires` holds the time in epoch milliseconds. It runs
8
+ * inline in the page head, before next-themes or Nextra read the values, so it works whatever library writes them;
9
+ * it is serialised with `toString()`, so it uses only its arguments.
10
+ *
11
+ * @param storage - `window.localStorage`.
12
+ * @param proto - `Storage.prototype`, whose `setItem` is wrapped to date the writes of `keys`.
13
+ * @param keys - The keys of the interface settings, such as `theme`.
14
+ * @param ms - How long a value is kept after it is written.
15
+ * @param now - The current time.
16
+ */
17
+ declare function expireStoredPreferences(storage: Storage, proto: Pick<Storage, 'setItem'>, keys: string[], ms: number, now: number): void;
18
+ /**
19
+ * The inline script of {@link expireStoredPreferences}, for the `<head>` of a site's root layout, before the theme
20
+ * script of next-themes.
21
+ *
22
+ * @param props.keys - The Local Storage keys to keep for {@link PREFERENCE_DAYS} days; `theme` by default.
23
+ */
24
+ declare function PreferenceExpiry({ keys }: {
25
+ keys?: string[];
26
+ }): React.JSX.Element;
27
+
28
+ export { PREFERENCE_DAYS, PreferenceExpiry, expireStoredPreferences };
@@ -0,0 +1,28 @@
1
+ import * as React from 'react';
2
+
3
+ /** How long the TUWA sites keep an interface setting after it is chosen (Cookie Policy). */
4
+ declare const PREFERENCE_DAYS = 30;
5
+ /**
6
+ * Forgets the given Local Storage values once their 30 days have passed (or when they were saved before values had an
7
+ * expiry), then dates every later write of those keys: `<key>:expires` holds the time in epoch milliseconds. It runs
8
+ * inline in the page head, before next-themes or Nextra read the values, so it works whatever library writes them;
9
+ * it is serialised with `toString()`, so it uses only its arguments.
10
+ *
11
+ * @param storage - `window.localStorage`.
12
+ * @param proto - `Storage.prototype`, whose `setItem` is wrapped to date the writes of `keys`.
13
+ * @param keys - The keys of the interface settings, such as `theme`.
14
+ * @param ms - How long a value is kept after it is written.
15
+ * @param now - The current time.
16
+ */
17
+ declare function expireStoredPreferences(storage: Storage, proto: Pick<Storage, 'setItem'>, keys: string[], ms: number, now: number): void;
18
+ /**
19
+ * The inline script of {@link expireStoredPreferences}, for the `<head>` of a site's root layout, before the theme
20
+ * script of next-themes.
21
+ *
22
+ * @param props.keys - The Local Storage keys to keep for {@link PREFERENCE_DAYS} days; `theme` by default.
23
+ */
24
+ declare function PreferenceExpiry({ keys }: {
25
+ keys?: string[];
26
+ }): React.JSX.Element;
27
+
28
+ export { PREFERENCE_DAYS, PreferenceExpiry, expireStoredPreferences };
@@ -0,0 +1,29 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ const PREFERENCE_DAYS = 30;
3
+ function expireStoredPreferences(storage, proto, keys, ms, now) {
4
+ try {
5
+ for (const key of keys) {
6
+ const expiresAt = Number(storage.getItem(key + ":expires"));
7
+ if (!expiresAt || expiresAt <= now) {
8
+ storage.removeItem(key);
9
+ storage.removeItem(key + ":expires");
10
+ }
11
+ }
12
+ const setItem = proto.setItem;
13
+ proto.setItem = function(key, value) {
14
+ setItem.call(this, key, value);
15
+ if (this === storage && keys.indexOf(key) >= 0) setItem.call(this, key + ":expires", String(Date.now() + ms));
16
+ };
17
+ } catch {
18
+ }
19
+ }
20
+ function PreferenceExpiry({ keys = ["theme"] }) {
21
+ const ms = PREFERENCE_DAYS * 864e5;
22
+ const code = `(${expireStoredPreferences.toString()})(window.localStorage, Storage.prototype, ${JSON.stringify(keys)}, ${ms}, Date.now())`;
23
+ return /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: code } });
24
+ }
25
+ export {
26
+ PREFERENCE_DAYS,
27
+ PreferenceExpiry,
28
+ expireStoredPreferences
29
+ };
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var searchIndex_exports = {};
20
+ __export(searchIndex_exports, {
21
+ buildSearchIndex: () => buildSearchIndex,
22
+ findPrerenderedPages: () => findPrerenderedPages
23
+ });
24
+ module.exports = __toCommonJS(searchIndex_exports);
25
+ var import_node_fs = require("node:fs");
26
+ var import_node_path = require("node:path");
27
+ var import_node_url = require("node:url");
28
+ const directories = (dir) => (0, import_node_fs.existsSync)(dir) ? (0, import_node_fs.readdirSync)(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path.join)(dir, entry.name)) : [];
29
+ const htmlFiles = (root) => (0, import_node_fs.existsSync)(root) ? (0, import_node_fs.readdirSync)(root, { recursive: true, encoding: "utf8" }).filter((path) => path.endsWith(".html")).map((path) => ({ file: (0, import_node_path.join)(root, path), sourcePath: path.split(import_node_path.sep).join("/") })) : [];
30
+ function findPrerenderedPages(serverDir) {
31
+ const roots = [
32
+ (0, import_node_path.join)(serverDir, "app"),
33
+ ...directories((0, import_node_path.join)(serverDir, "route-cache")).flatMap(directories).map((route) => (0, import_node_path.join)(route, "$"))
34
+ ];
35
+ return roots.flatMap(htmlFiles).sort((a, b) => a.sourcePath < b.sourcePath ? -1 : 1);
36
+ }
37
+ async function loadPagefind(cwd) {
38
+ for (let dir = (0, import_node_path.resolve)(cwd); ; dir = (0, import_node_path.dirname)(dir)) {
39
+ const manifest = (0, import_node_path.join)(dir, "node_modules", "pagefind", "package.json");
40
+ if ((0, import_node_fs.existsSync)(manifest)) {
41
+ const pkg = JSON.parse((0, import_node_fs.readFileSync)(manifest, "utf8"));
42
+ const entry = pkg.exports?.["."]?.import ?? pkg.main ?? "lib/index.js";
43
+ return await import((0, import_node_url.pathToFileURL)((0, import_node_path.join)((0, import_node_path.dirname)(manifest), entry)).href);
44
+ }
45
+ if ((0, import_node_path.dirname)(dir) === dir) throw new Error("pagefind is not installed: add it to the dependencies of the site");
46
+ }
47
+ }
48
+ async function buildSearchIndex({
49
+ serverDir = ".next/server",
50
+ outputPath = "public/_pagefind",
51
+ cwd = process.cwd()
52
+ } = {}) {
53
+ const pages = findPrerenderedPages((0, import_node_path.resolve)(cwd, serverDir));
54
+ if (!pages.length) throw new Error(`Found no prerendered pages in ${serverDir}: run next build first`);
55
+ const pagefind = await loadPagefind(cwd);
56
+ try {
57
+ const { index, errors } = await pagefind.createIndex();
58
+ if (!index) throw new Error(`Pagefind: ${errors.join("; ")}`);
59
+ const urls = [];
60
+ for (const page of pages) {
61
+ const added = await index.addHTMLFile({ sourcePath: page.sourcePath, content: (0, import_node_fs.readFileSync)(page.file, "utf8") });
62
+ if (added.errors.length || !added.file)
63
+ throw new Error(`Pagefind, ${page.sourcePath}: ${added.errors.join("; ")}`);
64
+ urls.push(added.file.url);
65
+ }
66
+ const written = await index.writeFiles({ outputPath: (0, import_node_path.resolve)(cwd, outputPath) });
67
+ if (written.errors.length) throw new Error(`Pagefind: ${written.errors.join("; ")}`);
68
+ return { urls };
69
+ } finally {
70
+ await pagefind.close();
71
+ }
72
+ }
73
+ // Annotate the CommonJS export names for ESM import in node:
74
+ 0 && (module.exports = {
75
+ buildSearchIndex,
76
+ findPrerenderedPages
77
+ });
@@ -0,0 +1,40 @@
1
+ /** A page that `next build` prerendered: its HTML file and the path from which Pagefind derives its URL. */
2
+ type PrerenderedPage = {
3
+ /** The HTML file in `.next/server`. */
4
+ file: string;
5
+ /** The path of the page in the site, such as `packages/orbit-core.html` (URL `/packages/orbit-core.html`). */
6
+ sourcePath: string;
7
+ };
8
+ /**
9
+ * Lists the pages that `next build` prerendered as HTML. A build without an adapter keeps them in
10
+ * `<serverDir>/app/<path>.html`; a build with an adapter (Next 16.3.8 and later on Vercel) keeps them in
11
+ * `<serverDir>/route-cache/<kind>/<hash of the route>/$/<path>.html`. Both give the same paths.
12
+ *
13
+ * @param serverDir - The `.next/server` directory of the site.
14
+ * @returns The pages, sorted by path.
15
+ */
16
+ declare function findPrerenderedPages(serverDir: string): PrerenderedPage[];
17
+ /**
18
+ * Builds the Pagefind index of a documentation site from the pages that `next build` prerendered, in either layout
19
+ * of {@link findPrerenderedPages}, with the URLs `pagefind --site .next/server/app` gave them. Nextra's search loads it
20
+ * from `/_pagefind/pagefind.js`.
21
+ *
22
+ * Side effects: starts the Pagefind binary of the site's `pagefind` package and writes the index to `outputPath`.
23
+ *
24
+ * @param options - Where to read and write, relative to `cwd`.
25
+ * @param options.serverDir - The `.next/server` directory. Default `.next/server`.
26
+ * @param options.outputPath - Where the index is written. Default `public/_pagefind`.
27
+ * @param options.cwd - The directory of the site, where `pagefind` is installed. Default the current directory.
28
+ * @returns The URLs of the indexed pages.
29
+ * @throws When the build has no prerendered pages, or when Pagefind is missing or reports an error.
30
+ */
31
+ declare function buildSearchIndex({ serverDir, outputPath, cwd, }?: {
32
+ serverDir?: string;
33
+ outputPath?: string;
34
+ cwd?: string;
35
+ }): Promise<{
36
+ urls: string[];
37
+ }>;
38
+
39
+ export { buildSearchIndex, findPrerenderedPages };
40
+ export type { PrerenderedPage };
@@ -0,0 +1,40 @@
1
+ /** A page that `next build` prerendered: its HTML file and the path from which Pagefind derives its URL. */
2
+ type PrerenderedPage = {
3
+ /** The HTML file in `.next/server`. */
4
+ file: string;
5
+ /** The path of the page in the site, such as `packages/orbit-core.html` (URL `/packages/orbit-core.html`). */
6
+ sourcePath: string;
7
+ };
8
+ /**
9
+ * Lists the pages that `next build` prerendered as HTML. A build without an adapter keeps them in
10
+ * `<serverDir>/app/<path>.html`; a build with an adapter (Next 16.3.8 and later on Vercel) keeps them in
11
+ * `<serverDir>/route-cache/<kind>/<hash of the route>/$/<path>.html`. Both give the same paths.
12
+ *
13
+ * @param serverDir - The `.next/server` directory of the site.
14
+ * @returns The pages, sorted by path.
15
+ */
16
+ declare function findPrerenderedPages(serverDir: string): PrerenderedPage[];
17
+ /**
18
+ * Builds the Pagefind index of a documentation site from the pages that `next build` prerendered, in either layout
19
+ * of {@link findPrerenderedPages}, with the URLs `pagefind --site .next/server/app` gave them. Nextra's search loads it
20
+ * from `/_pagefind/pagefind.js`.
21
+ *
22
+ * Side effects: starts the Pagefind binary of the site's `pagefind` package and writes the index to `outputPath`.
23
+ *
24
+ * @param options - Where to read and write, relative to `cwd`.
25
+ * @param options.serverDir - The `.next/server` directory. Default `.next/server`.
26
+ * @param options.outputPath - Where the index is written. Default `public/_pagefind`.
27
+ * @param options.cwd - The directory of the site, where `pagefind` is installed. Default the current directory.
28
+ * @returns The URLs of the indexed pages.
29
+ * @throws When the build has no prerendered pages, or when Pagefind is missing or reports an error.
30
+ */
31
+ declare function buildSearchIndex({ serverDir, outputPath, cwd, }?: {
32
+ serverDir?: string;
33
+ outputPath?: string;
34
+ cwd?: string;
35
+ }): Promise<{
36
+ urls: string[];
37
+ }>;
38
+
39
+ export { buildSearchIndex, findPrerenderedPages };
40
+ export type { PrerenderedPage };
@@ -0,0 +1,52 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { dirname, join, resolve, sep } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ const directories = (dir) => existsSync(dir) ? readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join(dir, entry.name)) : [];
5
+ const htmlFiles = (root) => existsSync(root) ? readdirSync(root, { recursive: true, encoding: "utf8" }).filter((path) => path.endsWith(".html")).map((path) => ({ file: join(root, path), sourcePath: path.split(sep).join("/") })) : [];
6
+ function findPrerenderedPages(serverDir) {
7
+ const roots = [
8
+ join(serverDir, "app"),
9
+ ...directories(join(serverDir, "route-cache")).flatMap(directories).map((route) => join(route, "$"))
10
+ ];
11
+ return roots.flatMap(htmlFiles).sort((a, b) => a.sourcePath < b.sourcePath ? -1 : 1);
12
+ }
13
+ async function loadPagefind(cwd) {
14
+ for (let dir = resolve(cwd); ; dir = dirname(dir)) {
15
+ const manifest = join(dir, "node_modules", "pagefind", "package.json");
16
+ if (existsSync(manifest)) {
17
+ const pkg = JSON.parse(readFileSync(manifest, "utf8"));
18
+ const entry = pkg.exports?.["."]?.import ?? pkg.main ?? "lib/index.js";
19
+ return await import(pathToFileURL(join(dirname(manifest), entry)).href);
20
+ }
21
+ if (dirname(dir) === dir) throw new Error("pagefind is not installed: add it to the dependencies of the site");
22
+ }
23
+ }
24
+ async function buildSearchIndex({
25
+ serverDir = ".next/server",
26
+ outputPath = "public/_pagefind",
27
+ cwd = process.cwd()
28
+ } = {}) {
29
+ const pages = findPrerenderedPages(resolve(cwd, serverDir));
30
+ if (!pages.length) throw new Error(`Found no prerendered pages in ${serverDir}: run next build first`);
31
+ const pagefind = await loadPagefind(cwd);
32
+ try {
33
+ const { index, errors } = await pagefind.createIndex();
34
+ if (!index) throw new Error(`Pagefind: ${errors.join("; ")}`);
35
+ const urls = [];
36
+ for (const page of pages) {
37
+ const added = await index.addHTMLFile({ sourcePath: page.sourcePath, content: readFileSync(page.file, "utf8") });
38
+ if (added.errors.length || !added.file)
39
+ throw new Error(`Pagefind, ${page.sourcePath}: ${added.errors.join("; ")}`);
40
+ urls.push(added.file.url);
41
+ }
42
+ const written = await index.writeFiles({ outputPath: resolve(cwd, outputPath) });
43
+ if (written.errors.length) throw new Error(`Pagefind: ${written.errors.join("; ")}`);
44
+ return { urls };
45
+ } finally {
46
+ await pagefind.close();
47
+ }
48
+ }
49
+ export {
50
+ buildSearchIndex,
51
+ findPrerenderedPages
52
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuwaio/docs-ui",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "author": "Oleksandr Tkach",
6
6
  "license": "Apache-2.0",
@@ -8,12 +8,15 @@
8
8
  "main": "./dist/index.cjs",
9
9
  "module": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",
11
+ "bin": {
12
+ "tuwa-docs-search-index": "./bin/search-index.js"
13
+ },
11
14
  "type": "module",
12
15
  "exports": {
13
16
  ".": {
17
+ "types": "./dist/index.d.ts",
14
18
  "import": "./dist/index.js",
15
- "require": "./dist/index.cjs",
16
- "types": "./dist/index.d.ts"
19
+ "require": "./dist/index.cjs"
17
20
  },
18
21
  "./policies": {
19
22
  "types": "./dist/policies.d.ts",
@@ -27,6 +30,7 @@
27
30
  "access": "public"
28
31
  },
29
32
  "files": [
33
+ "bin",
30
34
  "dist",
31
35
  "socket.json"
32
36
  ],
@@ -57,7 +61,7 @@
57
61
  }
58
62
  ],
59
63
  "dependencies": {
60
- "@tuwaio/nova-core": "^0.9.1",
64
+ "@tuwaio/nova-core": "^0.9.3",
61
65
  "clsx": "^2.1.1",
62
66
  "html-react-parser": "^6.1.8",
63
67
  "tailwind-merge": "^3.7.0"
@@ -72,17 +76,17 @@
72
76
  "@heroicons/react": "^2.2.0",
73
77
  "@tailwindcss/postcss": "^4.3.3",
74
78
  "@tailwindcss/vite": "^4.3.3",
75
- "@types/node": "^26",
79
+ "@types/node": "^26.6.5",
76
80
  "@types/react": "^19.3.0",
77
81
  "autoprefixer": "^10.6.1",
78
- "next": "16.3.7",
82
+ "next": "16.4.1",
79
83
  "nextra": "^4.6.1",
80
84
  "nextra-theme-docs": "^4.6.1",
81
- "postcss": "^8.5.28",
85
+ "postcss": "^8.5.30",
82
86
  "postcss-cli": "^12.0.0",
83
- "react": "^19.3.0",
87
+ "react": "^19.3.1",
84
88
  "tailwindcss": "^4.3.3",
85
- "tsup": "^8.5.1",
89
+ "tsup": "^9.0.0",
86
90
  "typescript": "6.0.3"
87
91
  },
88
92
  "scripts": {