@tuwaio/docs-ui 0.3.3 → 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 +26 -2
- package/bin/search-index.js +11 -0
- package/dist/components/Logo.d.cts +2 -1
- package/dist/components/Logo.d.ts +2 -1
- package/dist/components/NavBar.d.cts +2 -1
- package/dist/components/NavBar.d.ts +2 -1
- package/dist/components/Orb.d.cts +2 -1
- package/dist/components/Orb.d.ts +2 -1
- package/dist/components/PolicyDocument.d.cts +2 -1
- package/dist/components/PolicyDocument.d.ts +2 -1
- package/dist/components/ThemeSwitcher.d.cts +2 -1
- package/dist/components/ThemeSwitcher.d.ts +2 -1
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/policies.d.cts +2 -1
- package/dist/policies.d.ts +2 -1
- package/dist/preferences.cjs +55 -0
- package/dist/preferences.d.cts +28 -0
- package/dist/preferences.d.ts +28 -0
- package/dist/preferences.js +29 -0
- package/dist/searchIndex.cjs +77 -0
- package/dist/searchIndex.d.cts +40 -0
- package/dist/searchIndex.d.ts +40 -0
- package/dist/searchIndex.js +52 -0
- package/package.json +12 -8
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
|
-
|
|
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
|
+
);
|
|
@@ -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
|
|
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
|
|
24
|
+
export { RemoteLogo };
|
|
25
|
+
export type { RemoteLogoProps };
|
|
@@ -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
|
|
29
|
+
export { Orb };
|
|
30
|
+
export type { OrbProps, PackageType };
|
package/dist/components/Orb.d.ts
CHANGED
|
@@ -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
|
|
29
|
+
export { Orb };
|
|
30
|
+
export type { OrbProps, PackageType };
|
|
@@ -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
|
|
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
|
|
57
|
+
export { PolicyDocument };
|
|
58
|
+
export type { PolicyDocumentProps };
|
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,
|
package/dist/policies.d.cts
CHANGED
|
@@ -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,
|
|
88
|
+
export { POLICIES_INDEX_URL, POLICY_SITE_URL, POLICY_SOURCE, footerPolicies, getPolicyFileUrl, getPolicyPageUrl, loadPolicy, policies, publishedPolicies };
|
|
89
|
+
export type { Policy, PolicyContent, PolicySection, PolicySlug };
|
package/dist/policies.d.ts
CHANGED
|
@@ -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,
|
|
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
|
+
"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
|
],
|
|
@@ -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.
|
|
82
|
+
"next": "16.4.1",
|
|
79
83
|
"nextra": "^4.6.1",
|
|
80
84
|
"nextra-theme-docs": "^4.6.1",
|
|
81
|
-
"postcss": "^8.5.
|
|
85
|
+
"postcss": "^8.5.30",
|
|
82
86
|
"postcss-cli": "^12.0.0",
|
|
83
|
-
"react": "^19.3.
|
|
87
|
+
"react": "^19.3.1",
|
|
84
88
|
"tailwindcss": "^4.3.3",
|
|
85
|
-
"tsup": "^
|
|
89
|
+
"tsup": "^9.0.0",
|
|
86
90
|
"typescript": "6.0.3"
|
|
87
91
|
},
|
|
88
92
|
"scripts": {
|