@tuwaio/docs-ui 0.2.0 → 0.3.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
@@ -3,16 +3,17 @@
3
3
  [![NPM Version](https://img.shields.io/npm/v/@tuwaio/docs-ui.svg)](https://www.npmjs.com/package/@tuwaio/docs-ui)
4
4
  [![License](https://img.shields.io/npm/l/@tuwaio/docs-ui.svg)](https://github.com/TuwaIO/docs/blob/main/packages/docs-ui/LICENSE)
5
5
 
6
- `@tuwaio/docs-ui` is the shared UI of the TUWA documentation sites: the navbar, footer, logo and stylesheet of every `*.docs.tuwa.io` site and of the [docs hub](https://docs.tuwa.io), plus the decorative parts of the hub and the TUWA landing page. The sites are built with Next.js (App Router) and Nextra 4; one package keeps them visually consistent.
6
+ `@tuwaio/docs-ui` is the shared UI of the TUWA documentation sites: the navbar, footer, logo and stylesheet of every `*.docs.tuwa.io` site and of the [docs hub](https://docs.tuwa.io), plus the decorative parts of the hub and the TUWA landing page, and the policy pages of the TUWA website and the Quasar dashboard. The sites are built with Next.js (App Router) and Nextra 4; one package keeps them visually consistent.
7
7
 
8
8
  ---
9
9
 
10
10
  ## 🏛️ Core Capabilities
11
11
 
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).
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
15
  - **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
+ - **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).
16
17
  - **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:`.
17
18
 
18
19
  ---
@@ -76,13 +77,53 @@ export default async function RootLayout({ children }: { children: ReactNode })
76
77
 
77
78
  ---
78
79
 
80
+ ### Policy pages
81
+
82
+ A policy page in the Next.js App Router (a Server Component; the content is cached for an hour):
83
+
84
+ ```tsx
85
+ import { loadPolicy, PolicyDocument } from '@tuwaio/docs-ui';
86
+
87
+ export default async function RefundPage() {
88
+ const content = await loadPolicy('refund');
89
+
90
+ return (
91
+ <main className="mx-auto max-w-6xl px-4 py-12">
92
+ <PolicyDocument slug="refund" content={content} />
93
+ </main>
94
+ );
95
+ }
96
+ ```
97
+
98
+ Footers list the policies in force:
99
+
100
+ ```tsx
101
+ import { footerPolicies } from '@tuwaio/docs-ui';
102
+
103
+ export function PolicyLinks() {
104
+ return (
105
+ <nav aria-label="Policies" className="flex flex-wrap gap-x-4 gap-y-2 text-xs">
106
+ {footerPolicies.map((policy) => (
107
+ <a key={policy.slug} href={policy.href}>
108
+ {policy.title}
109
+ </a>
110
+ ))}
111
+ </nav>
112
+ );
113
+ }
114
+ ```
115
+
116
+ Sections have stable anchors (`/refund#how-to-request-a-refund`), so hints and FAQ answers can link to a section.
117
+
118
+ ---
119
+
79
120
  ## 🗄️ Browser Storage
80
121
 
81
122
  Nothing. `ThemeSwitcher` only calls `onToggle`; the site keeps the theme.
82
123
 
83
124
  ## 🌐 External Services
84
125
 
85
- `RemoteLogo` requests its `url` on the server (by default `cdn.jsdelivr.net/gh/TuwaIO/workflows@main/preview/logo_v2.svg`), cached by Next.js for 24 hours. The other components send no requests.
126
+ `RemoteLogo` requests its `url` on the server (by default `cdn.jsdelivr.net/gh/TuwaIO/workflows@main/preview/logo_v2.svg`), cached by Next.js for 24 hours. `loadPolicy` requests `raw.githubusercontent.com/TuwaIO/workflows/main/docs/<slug>.json` on the server, cached by Next.js for an hour by default. The Download PDF button of `PolicyDocument` links to the PDF on the same host; the browser requests it only when it is clicked. The other components send no requests.
86
127
 
87
128
  ---
88
129
 
@@ -24,6 +24,7 @@ module.exports = __toCommonJS(Footer_exports);
24
24
  var import_jsx_runtime = require("react/jsx-runtime");
25
25
  var import_nova_core = require("@tuwaio/nova-core");
26
26
  var import_nextra_theme_docs = require("nextra-theme-docs");
27
+ var import_policies = require("../policies");
27
28
  var import_utils = require("../utils");
28
29
  function Footer({ links, logo }) {
29
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: [
@@ -45,18 +46,47 @@ function Footer({ links, logo }) {
45
46
  )) })
46
47
  ] }),
47
48
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwadocs:w-full tuwa-footer-border tuwadocs:pt-4", children: [
48
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwadocs:flex tuwadocs:flex-col tuwadocs:sm:flex-row tuwadocs:justify-between tuwadocs:items-start tuwadocs:sm:items-center tuwadocs:gap-4", children: [
49
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwadocs:flex-1", children: [
49
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwadocs:flex tuwadocs:flex-col tuwadocs:sm:flex-row tuwadocs:justify-between tuwadocs:items-center tuwadocs:sm:items-start tuwadocs:gap-4", children: [
50
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwadocs:flex-1 tuwadocs:text-center tuwadocs:sm:text-left", children: [
50
51
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "tuwa-footer-description", children: "The modular, headless-first Web3 infrastructure. Build self-custodial applications on open-source, portable building blocks." }),
51
52
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "tuwa-footer-license", children: "Licensed under Apache 2.0. Open source and free to use." })
52
53
  ] }),
53
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwa-footer-copyright", children: [
54
- "\xA9 2025 - ",
55
- (/* @__PURE__ */ new Date()).getFullYear(),
56
- " TUWA. All rights reserved."
57
- ] })
54
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
55
+ "nav",
56
+ {
57
+ "aria-label": "Policies",
58
+ className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:justify-center tuwadocs:sm:justify-end tuwadocs:gap-x-4 tuwadocs:gap-y-2 tuwadocs:text-xs",
59
+ children: [
60
+ import_policies.footerPolicies.map((policy) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
61
+ "a",
62
+ {
63
+ href: (0, import_policies.getPolicyPageUrl)(policy.slug),
64
+ target: "_blank",
65
+ rel: "noopener noreferrer",
66
+ className: "tuwa-footer-link",
67
+ children: policy.title
68
+ },
69
+ policy.slug
70
+ )),
71
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
72
+ "a",
73
+ {
74
+ href: import_policies.POLICIES_INDEX_URL,
75
+ target: "_blank",
76
+ rel: "noopener noreferrer",
77
+ className: "tuwa-footer-link tuwadocs:font-semibold",
78
+ children: "All policies"
79
+ }
80
+ )
81
+ ]
82
+ }
83
+ )
58
84
  ] }),
59
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:gap-3 tuwadocs:mt-4 tuwadocs:pt-4 tuwa-footer-border" })
85
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:justify-center tuwadocs:gap-3 tuwadocs:mt-4 tuwadocs:pt-4 tuwa-footer-border", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwa-footer-copyright", children: [
86
+ "\xA9 2025 - ",
87
+ (/* @__PURE__ */ new Date()).getFullYear(),
88
+ " TUWA. All rights reserved."
89
+ ] }) })
60
90
  ] })
61
91
  ] }) });
62
92
  }
@@ -3,8 +3,9 @@ import { NavProps } from './NavBar.cjs';
3
3
 
4
4
  /**
5
5
  * The footer of a TUWA documentation site: the `Footer` of `nextra-theme-docs` with the logo, external links, the
6
- * TUWA description, the license and the copyright line (the current year is read on render). Render it in the `footer`
7
- * prop of the Nextra `Layout`.
6
+ * TUWA description and the license on the left, links to the policies in force on tuwa.io (`footerPolicies`) and to
7
+ * the list of all policies on the docs hub on the right, and the copyright line (the current year is read on render)
8
+ * centered below; on narrow screens everything is centered. Render it in the `footer` prop of the Nextra `Layout`.
8
9
  *
9
10
  * @param props - See {@link NavProps}.
10
11
  * @returns The footer.
@@ -3,8 +3,9 @@ import { NavProps } from './NavBar.js';
3
3
 
4
4
  /**
5
5
  * The footer of a TUWA documentation site: the `Footer` of `nextra-theme-docs` with the logo, external links, the
6
- * TUWA description, the license and the copyright line (the current year is read on render). Render it in the `footer`
7
- * prop of the Nextra `Layout`.
6
+ * TUWA description and the license on the left, links to the policies in force on tuwa.io (`footerPolicies`) and to
7
+ * the list of all policies on the docs hub on the right, and the copyright line (the current year is read on render)
8
+ * centered below; on narrow screens everything is centered. Render it in the `footer` prop of the Nextra `Layout`.
8
9
  *
9
10
  * @param props - See {@link NavProps}.
10
11
  * @returns The footer.
@@ -1,6 +1,7 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "@tuwaio/nova-core";
3
3
  import { Footer as NextraFooter } from "nextra-theme-docs";
4
+ import { footerPolicies, getPolicyPageUrl, POLICIES_INDEX_URL } from "../policies";
4
5
  import { baseFooterLinks } from "../utils";
5
6
  function Footer({ links, logo }) {
6
7
  return /* @__PURE__ */ jsx(NextraFooter, { children: /* @__PURE__ */ 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: [
@@ -22,18 +23,47 @@ function Footer({ links, logo }) {
22
23
  )) })
23
24
  ] }),
24
25
  /* @__PURE__ */ jsxs("div", { className: "tuwadocs:w-full tuwa-footer-border tuwadocs:pt-4", children: [
25
- /* @__PURE__ */ jsxs("div", { className: "tuwadocs:flex tuwadocs:flex-col tuwadocs:sm:flex-row tuwadocs:justify-between tuwadocs:items-start tuwadocs:sm:items-center tuwadocs:gap-4", children: [
26
- /* @__PURE__ */ jsxs("div", { className: "tuwadocs:flex-1", children: [
26
+ /* @__PURE__ */ jsxs("div", { className: "tuwadocs:flex tuwadocs:flex-col tuwadocs:sm:flex-row tuwadocs:justify-between tuwadocs:items-center tuwadocs:sm:items-start tuwadocs:gap-4", children: [
27
+ /* @__PURE__ */ jsxs("div", { className: "tuwadocs:flex-1 tuwadocs:text-center tuwadocs:sm:text-left", children: [
27
28
  /* @__PURE__ */ jsx("p", { className: "tuwa-footer-description", children: "The modular, headless-first Web3 infrastructure. Build self-custodial applications on open-source, portable building blocks." }),
28
29
  /* @__PURE__ */ jsx("p", { className: "tuwa-footer-license", children: "Licensed under Apache 2.0. Open source and free to use." })
29
30
  ] }),
30
- /* @__PURE__ */ jsxs("div", { className: "tuwa-footer-copyright", children: [
31
- "\xA9 2025 - ",
32
- (/* @__PURE__ */ new Date()).getFullYear(),
33
- " TUWA. All rights reserved."
34
- ] })
31
+ /* @__PURE__ */ jsxs(
32
+ "nav",
33
+ {
34
+ "aria-label": "Policies",
35
+ className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:justify-center tuwadocs:sm:justify-end tuwadocs:gap-x-4 tuwadocs:gap-y-2 tuwadocs:text-xs",
36
+ children: [
37
+ footerPolicies.map((policy) => /* @__PURE__ */ jsx(
38
+ "a",
39
+ {
40
+ href: getPolicyPageUrl(policy.slug),
41
+ target: "_blank",
42
+ rel: "noopener noreferrer",
43
+ className: "tuwa-footer-link",
44
+ children: policy.title
45
+ },
46
+ policy.slug
47
+ )),
48
+ /* @__PURE__ */ jsx(
49
+ "a",
50
+ {
51
+ href: POLICIES_INDEX_URL,
52
+ target: "_blank",
53
+ rel: "noopener noreferrer",
54
+ className: "tuwa-footer-link tuwadocs:font-semibold",
55
+ children: "All policies"
56
+ }
57
+ )
58
+ ]
59
+ }
60
+ )
35
61
  ] }),
36
- /* @__PURE__ */ jsx("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:gap-3 tuwadocs:mt-4 tuwadocs:pt-4 tuwa-footer-border" })
62
+ /* @__PURE__ */ jsx("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:justify-center tuwadocs:gap-3 tuwadocs:mt-4 tuwadocs:pt-4 tuwa-footer-border", children: /* @__PURE__ */ jsxs("div", { className: "tuwa-footer-copyright", children: [
63
+ "\xA9 2025 - ",
64
+ (/* @__PURE__ */ new Date()).getFullYear(),
65
+ " TUWA. All rights reserved."
66
+ ] }) })
37
67
  ] })
38
68
  ] }) });
39
69
  }
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var PolicyDocument_exports = {};
30
+ __export(PolicyDocument_exports, {
31
+ PolicyDocument: () => PolicyDocument
32
+ });
33
+ module.exports = __toCommonJS(PolicyDocument_exports);
34
+ var import_jsx_runtime = require("react/jsx-runtime");
35
+ var import_outline = require("@heroicons/react/24/outline");
36
+ var import_nova_core = require("@tuwaio/nova-core");
37
+ var import_html_react_parser = __toESM(require("html-react-parser"), 1);
38
+ var import_policies = require("../policies");
39
+ const BLOCKED_ELEMENTS = /* @__PURE__ */ new Set([
40
+ "script",
41
+ "style",
42
+ "iframe",
43
+ "object",
44
+ "embed",
45
+ "form",
46
+ "input",
47
+ "button",
48
+ "textarea",
49
+ "select",
50
+ "link",
51
+ "meta",
52
+ "base"
53
+ ]);
54
+ function sanitize(node) {
55
+ if (!(node instanceof import_html_react_parser.Element)) return void 0;
56
+ if (BLOCKED_ELEMENTS.has(node.name)) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, {});
57
+ for (const name of Object.keys(node.attribs)) {
58
+ if (name.toLowerCase().startsWith("on")) delete node.attribs[name];
59
+ }
60
+ if (node.name === "a") {
61
+ const href = node.attribs.href ?? "";
62
+ if (/^\s*javascript:/i.test(href)) {
63
+ delete node.attribs.href;
64
+ } else if (/^https?:\/\//i.test(href)) {
65
+ node.attribs.target = "_blank";
66
+ node.attribs.rel = "noopener noreferrer";
67
+ }
68
+ }
69
+ return void 0;
70
+ }
71
+ function PolicyDocument({
72
+ slug,
73
+ content,
74
+ source = import_policies.POLICY_SOURCE,
75
+ labels,
76
+ className,
77
+ classNames
78
+ }) {
79
+ const tocLabel = labels?.toc ?? "On this page";
80
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
81
+ "div",
82
+ {
83
+ className: (0, import_nova_core.cn)(
84
+ "tuwadocs:grid tuwadocs:grid-cols-1 tuwadocs:gap-10 tuwadocs:lg:grid-cols-[minmax(0,1fr)_14rem]",
85
+ className
86
+ ),
87
+ children: [
88
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "tuwadocs:min-w-0", children: [
89
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
90
+ "header",
91
+ {
92
+ className: (0, import_nova_core.cn)(
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
+ classNames?.header
95
+ ),
96
+ children: [
97
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
98
+ "h1",
99
+ {
100
+ className: (0, import_nova_core.cn)(
101
+ "tuwadocs-policy-title tuwadocs:text-3xl tuwadocs:font-extrabold tuwadocs:tracking-tight tuwadocs:text-[var(--tuwa-text-primary)] tuwadocs:sm:text-4xl",
102
+ classNames?.title
103
+ ),
104
+ children: content.title
105
+ }
106
+ ),
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: [
109
+ labels?.updated ?? "Last updated",
110
+ " ",
111
+ content.updated
112
+ ] }),
113
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
114
+ "a",
115
+ {
116
+ href: (0, import_policies.getPolicyFileUrl)(slug, "pdf", source),
117
+ target: "_blank",
118
+ rel: "noopener noreferrer",
119
+ className: (0, import_nova_core.cn)(
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
+ classNames?.download
122
+ ),
123
+ children: [
124
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_outline.ArrowDownTrayIcon, { className: "tuwadocs:h-4 tuwadocs:w-4", "aria-hidden": "true" }),
125
+ labels?.download ?? "Download PDF"
126
+ ]
127
+ }
128
+ )
129
+ ] })
130
+ ]
131
+ }
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 }) })
134
+ ] }),
135
+ content.sections.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
136
+ "nav",
137
+ {
138
+ "aria-label": tocLabel,
139
+ className: (0, import_nova_core.cn)(
140
+ "tuwadocs:hidden tuwadocs:lg:sticky tuwadocs:lg:top-24 tuwadocs:lg:block tuwadocs:lg:self-start",
141
+ classNames?.toc
142
+ ),
143
+ children: [
144
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "tuwadocs:mb-3 tuwadocs:text-xs tuwadocs:font-bold tuwadocs:uppercase tuwadocs:tracking-widest tuwadocs:text-[var(--tuwa-text-tertiary)]", children: tocLabel }),
145
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { className: "tuwadocs:flex tuwadocs:list-none tuwadocs:flex-col tuwadocs:gap-2 tuwadocs:border-l tuwadocs:border-[var(--tuwa-border-primary)] tuwadocs:pl-4", children: content.sections.map((section) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
146
+ "a",
147
+ {
148
+ href: `#${section.id}`,
149
+ className: "tuwadocs:text-sm tuwadocs:text-[var(--tuwa-text-secondary)] tuwadocs:no-underline tuwadocs:transition-colors tuwadocs:hover:text-[var(--tuwa-text-primary)]",
150
+ children: section.title
151
+ }
152
+ ) }, section.id)) })
153
+ ]
154
+ }
155
+ )
156
+ ]
157
+ }
158
+ );
159
+ }
160
+ // Annotate the CommonJS export names for ESM import in node:
161
+ 0 && (module.exports = {
162
+ PolicyDocument
163
+ });
@@ -0,0 +1,57 @@
1
+ import * as React from 'react';
2
+ import { PolicySlug, PolicyContent } from '../policies.cjs';
3
+
4
+ /**
5
+ * Props of {@link PolicyDocument}.
6
+ */
7
+ interface PolicyDocumentProps {
8
+ /** Slug of the policy; sets the link of the Download PDF button. */
9
+ slug: PolicySlug;
10
+ /** Content of the policy, from `loadPolicy`. */
11
+ content: PolicyContent;
12
+ /** Base URL of the policy files for the PDF link (default: `POLICY_SOURCE`). */
13
+ source?: string;
14
+ /** Texts of the page. */
15
+ labels?: {
16
+ /** Text before the date of the version (default: `Last updated`). */
17
+ updated?: string;
18
+ /** Text of the button that opens the PDF (default: `Download PDF`). */
19
+ download?: string;
20
+ /** Heading and accessible name of the list of sections (default: `On this page`). */
21
+ toc?: string;
22
+ };
23
+ /** Classes of the root element. */
24
+ className?: string;
25
+ /** Classes of the parts of the page. */
26
+ classNames?: {
27
+ /** The header with the title, the date and the button. */
28
+ header?: string;
29
+ /** The title. */
30
+ title?: string;
31
+ /** The date of the version. */
32
+ updated?: string;
33
+ /** The Download PDF button. */
34
+ download?: string;
35
+ /** The `article` with the body. */
36
+ body?: string;
37
+ /** The list of sections (shown from the `lg` breakpoint). */
38
+ toc?: string;
39
+ };
40
+ }
41
+ /**
42
+ * A TUWA policy page (Terms, Privacy, Cookie, Refund, Security): the title, the date of the version, a Download PDF
43
+ * button with the official document, the text with anchored sections and, from the `lg` breakpoint, a sticky list of
44
+ * the sections. The text is selectable and readable on phones, by search engines and by screen readers; the PDF is
45
+ * generated from the same source.
46
+ *
47
+ * Load the content on the server with `loadPolicy` and render the page inside the layout of the site. The body is
48
+ * HTML from `TuwaIO/workflows`: scripts, styles, frames, forms and event attributes are dropped, and external links
49
+ * open in a new tab. Headings scroll below a sticky header (`scroll-margin-top`), so `/refund#how-to-request-a-refund`
50
+ * opens at its section.
51
+ *
52
+ * @param props - See {@link PolicyDocumentProps}.
53
+ * @returns The policy page.
54
+ */
55
+ declare function PolicyDocument({ slug, content, source, labels, className, classNames, }: PolicyDocumentProps): React.JSX.Element;
56
+
57
+ export { PolicyDocument, type PolicyDocumentProps };
@@ -0,0 +1,57 @@
1
+ import * as React from 'react';
2
+ import { PolicySlug, PolicyContent } from '../policies.js';
3
+
4
+ /**
5
+ * Props of {@link PolicyDocument}.
6
+ */
7
+ interface PolicyDocumentProps {
8
+ /** Slug of the policy; sets the link of the Download PDF button. */
9
+ slug: PolicySlug;
10
+ /** Content of the policy, from `loadPolicy`. */
11
+ content: PolicyContent;
12
+ /** Base URL of the policy files for the PDF link (default: `POLICY_SOURCE`). */
13
+ source?: string;
14
+ /** Texts of the page. */
15
+ labels?: {
16
+ /** Text before the date of the version (default: `Last updated`). */
17
+ updated?: string;
18
+ /** Text of the button that opens the PDF (default: `Download PDF`). */
19
+ download?: string;
20
+ /** Heading and accessible name of the list of sections (default: `On this page`). */
21
+ toc?: string;
22
+ };
23
+ /** Classes of the root element. */
24
+ className?: string;
25
+ /** Classes of the parts of the page. */
26
+ classNames?: {
27
+ /** The header with the title, the date and the button. */
28
+ header?: string;
29
+ /** The title. */
30
+ title?: string;
31
+ /** The date of the version. */
32
+ updated?: string;
33
+ /** The Download PDF button. */
34
+ download?: string;
35
+ /** The `article` with the body. */
36
+ body?: string;
37
+ /** The list of sections (shown from the `lg` breakpoint). */
38
+ toc?: string;
39
+ };
40
+ }
41
+ /**
42
+ * A TUWA policy page (Terms, Privacy, Cookie, Refund, Security): the title, the date of the version, a Download PDF
43
+ * button with the official document, the text with anchored sections and, from the `lg` breakpoint, a sticky list of
44
+ * the sections. The text is selectable and readable on phones, by search engines and by screen readers; the PDF is
45
+ * generated from the same source.
46
+ *
47
+ * Load the content on the server with `loadPolicy` and render the page inside the layout of the site. The body is
48
+ * HTML from `TuwaIO/workflows`: scripts, styles, frames, forms and event attributes are dropped, and external links
49
+ * open in a new tab. Headings scroll below a sticky header (`scroll-margin-top`), so `/refund#how-to-request-a-refund`
50
+ * opens at its section.
51
+ *
52
+ * @param props - See {@link PolicyDocumentProps}.
53
+ * @returns The policy page.
54
+ */
55
+ declare function PolicyDocument({ slug, content, source, labels, className, classNames, }: PolicyDocumentProps): React.JSX.Element;
56
+
57
+ export { PolicyDocument, type PolicyDocumentProps };
@@ -0,0 +1,129 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
3
+ import { cn } from "@tuwaio/nova-core";
4
+ import parse, { Element } from "html-react-parser";
5
+ import { getPolicyFileUrl, POLICY_SOURCE } from "../policies";
6
+ const BLOCKED_ELEMENTS = /* @__PURE__ */ new Set([
7
+ "script",
8
+ "style",
9
+ "iframe",
10
+ "object",
11
+ "embed",
12
+ "form",
13
+ "input",
14
+ "button",
15
+ "textarea",
16
+ "select",
17
+ "link",
18
+ "meta",
19
+ "base"
20
+ ]);
21
+ function sanitize(node) {
22
+ if (!(node instanceof Element)) return void 0;
23
+ if (BLOCKED_ELEMENTS.has(node.name)) return /* @__PURE__ */ jsx(Fragment, {});
24
+ for (const name of Object.keys(node.attribs)) {
25
+ if (name.toLowerCase().startsWith("on")) delete node.attribs[name];
26
+ }
27
+ if (node.name === "a") {
28
+ const href = node.attribs.href ?? "";
29
+ if (/^\s*javascript:/i.test(href)) {
30
+ delete node.attribs.href;
31
+ } else if (/^https?:\/\//i.test(href)) {
32
+ node.attribs.target = "_blank";
33
+ node.attribs.rel = "noopener noreferrer";
34
+ }
35
+ }
36
+ return void 0;
37
+ }
38
+ function PolicyDocument({
39
+ slug,
40
+ content,
41
+ source = POLICY_SOURCE,
42
+ labels,
43
+ className,
44
+ classNames
45
+ }) {
46
+ const tocLabel = labels?.toc ?? "On this page";
47
+ return /* @__PURE__ */ jsxs(
48
+ "div",
49
+ {
50
+ className: cn(
51
+ "tuwadocs:grid tuwadocs:grid-cols-1 tuwadocs:gap-10 tuwadocs:lg:grid-cols-[minmax(0,1fr)_14rem]",
52
+ className
53
+ ),
54
+ children: [
55
+ /* @__PURE__ */ jsxs("div", { className: "tuwadocs:min-w-0", children: [
56
+ /* @__PURE__ */ jsxs(
57
+ "header",
58
+ {
59
+ className: cn(
60
+ "tuwadocs:mb-8 tuwadocs:flex tuwadocs:flex-col tuwadocs:gap-4 tuwadocs:border-b tuwadocs:border-[var(--tuwa-border-primary)] tuwadocs:pb-6",
61
+ classNames?.header
62
+ ),
63
+ children: [
64
+ /* @__PURE__ */ jsx(
65
+ "h1",
66
+ {
67
+ className: cn(
68
+ "tuwadocs-policy-title tuwadocs:text-3xl tuwadocs:font-extrabold tuwadocs:tracking-tight tuwadocs:text-[var(--tuwa-text-primary)] tuwadocs:sm:text-4xl",
69
+ classNames?.title
70
+ ),
71
+ children: content.title
72
+ }
73
+ ),
74
+ /* @__PURE__ */ jsxs("div", { className: "tuwadocs:flex tuwadocs:flex-wrap tuwadocs:items-center tuwadocs:justify-between tuwadocs:gap-3", children: [
75
+ /* @__PURE__ */ jsxs("p", { className: cn("tuwadocs:text-sm tuwadocs:text-[var(--tuwa-text-secondary)]", classNames?.updated), children: [
76
+ labels?.updated ?? "Last updated",
77
+ " ",
78
+ content.updated
79
+ ] }),
80
+ /* @__PURE__ */ jsxs(
81
+ "a",
82
+ {
83
+ href: getPolicyFileUrl(slug, "pdf", source),
84
+ target: "_blank",
85
+ rel: "noopener noreferrer",
86
+ className: cn(
87
+ "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)]",
88
+ classNames?.download
89
+ ),
90
+ children: [
91
+ /* @__PURE__ */ jsx(ArrowDownTrayIcon, { className: "tuwadocs:h-4 tuwadocs:w-4", "aria-hidden": "true" }),
92
+ labels?.download ?? "Download PDF"
93
+ ]
94
+ }
95
+ )
96
+ ] })
97
+ ]
98
+ }
99
+ ),
100
+ /* @__PURE__ */ jsx("article", { className: cn("tuwadocs-policy", classNames?.body), children: parse(content.html, { replace: sanitize }) })
101
+ ] }),
102
+ content.sections.length > 0 && /* @__PURE__ */ jsxs(
103
+ "nav",
104
+ {
105
+ "aria-label": tocLabel,
106
+ className: cn(
107
+ "tuwadocs:hidden tuwadocs:lg:sticky tuwadocs:lg:top-24 tuwadocs:lg:block tuwadocs:lg:self-start",
108
+ classNames?.toc
109
+ ),
110
+ children: [
111
+ /* @__PURE__ */ jsx("p", { className: "tuwadocs:mb-3 tuwadocs:text-xs tuwadocs:font-bold tuwadocs:uppercase tuwadocs:tracking-widest tuwadocs:text-[var(--tuwa-text-tertiary)]", children: tocLabel }),
112
+ /* @__PURE__ */ jsx("ol", { className: "tuwadocs:flex tuwadocs:list-none tuwadocs:flex-col tuwadocs:gap-2 tuwadocs:border-l tuwadocs:border-[var(--tuwa-border-primary)] tuwadocs:pl-4", children: content.sections.map((section) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
113
+ "a",
114
+ {
115
+ href: `#${section.id}`,
116
+ className: "tuwadocs:text-sm tuwadocs:text-[var(--tuwa-text-secondary)] tuwadocs:no-underline tuwadocs:transition-colors tuwadocs:hover:text-[var(--tuwa-text-primary)]",
117
+ children: section.title
118
+ }
119
+ ) }, section.id)) })
120
+ ]
121
+ }
122
+ )
123
+ ]
124
+ }
125
+ );
126
+ }
127
+ export {
128
+ PolicyDocument
129
+ };