legal-terms 0.1.9
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/cli.mjs +158 -0
- package/package.json +82 -0
- package/readme.md +226 -0
- package/src/content/full.ts +516 -0
- package/src/content/summary.ts +431 -0
- package/src/index.ts +44 -0
- package/src/react/LegalTermsPrivacyPolicy.tsx +381 -0
- package/src/react/blocks.tsx +178 -0
- package/src/react/icons.tsx +139 -0
- package/src/react/index.ts +10 -0
- package/src/render/html.ts +155 -0
- package/src/render/markdown.ts +81 -0
- package/src/render/text.ts +74 -0
- package/src/resolve.ts +264 -0
- package/src/types.ts +247 -0
package/cli.mjs
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Print a configured Terms of Service + Privacy Policy as Markdown, HTML or
|
|
4
|
+
* plain text — for a repo's `TERMS.md`, a static page, or a diff of what a
|
|
5
|
+
* config change does to the policy text.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```sh
|
|
9
|
+
* npx legal-terms-privacy-policy --app-name Acme --variant full > TERMS.md
|
|
10
|
+
* npx legal-terms-privacy-policy --app-name Acme --format html --standalone \
|
|
11
|
+
* --no-part california --exclude social-features > terms.html
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { register } from 'node:module';
|
|
15
|
+
|
|
16
|
+
register('./ts-loader.mjs', import.meta.url);
|
|
17
|
+
|
|
18
|
+
const { renderMarkdown, renderHtml, renderText, resolveLegalDoc } = await import(
|
|
19
|
+
new URL('./src/index.ts', import.meta.url).href
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const USAGE = `
|
|
23
|
+
legal-terms-privacy-policy — print a configurable Terms of Service + Privacy Policy
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
legal-terms-privacy-policy [options]
|
|
27
|
+
|
|
28
|
+
Document:
|
|
29
|
+
--variant <summary|full> Which presentation to print (default: full)
|
|
30
|
+
--format <markdown|html|text|json>
|
|
31
|
+
Output format (default: markdown)
|
|
32
|
+
--standalone For --format html, emit a full page with styles
|
|
33
|
+
|
|
34
|
+
Values substituted into the text:
|
|
35
|
+
--app-name <name> Product name (default: Our Service)
|
|
36
|
+
--company-name <name> Legal entity (default: same as --app-name)
|
|
37
|
+
--contact-email <email> Address for legal and privacy requests
|
|
38
|
+
--home-url <url> Target of the "Back to Home" link
|
|
39
|
+
--last-revised <date> Revision date shown under the title
|
|
40
|
+
--effective-date <date> Effective date (default: same as --last-revised)
|
|
41
|
+
--jurisdiction <region> Region the Services are offered in
|
|
42
|
+
--minimum-age <n> Minimum age to hold an account
|
|
43
|
+
--children-age <n> COPPA age threshold
|
|
44
|
+
--deletion-days <n> Days to purge data after account deletion
|
|
45
|
+
|
|
46
|
+
Choosing what appears:
|
|
47
|
+
--no-part <id> Drop a named part; repeatable. One of:
|
|
48
|
+
ai, privacy, cookies, california, children,
|
|
49
|
+
security, thirdParty
|
|
50
|
+
--part <id> Force a part back on; repeatable
|
|
51
|
+
--include <id> Keep only these section ids; repeatable
|
|
52
|
+
--exclude <id> Drop these section ids; repeatable
|
|
53
|
+
--order <id,id,...> Put these sections first, in this order
|
|
54
|
+
--no-numbers Do not number the full-text sections
|
|
55
|
+
--title <text> Override the document title
|
|
56
|
+
|
|
57
|
+
--list-sections Print the section ids of the chosen variant
|
|
58
|
+
-h, --help Show this help
|
|
59
|
+
`.trim();
|
|
60
|
+
|
|
61
|
+
const PART_IDS = new Set([
|
|
62
|
+
'ai',
|
|
63
|
+
'privacy',
|
|
64
|
+
'cookies',
|
|
65
|
+
'california',
|
|
66
|
+
'children',
|
|
67
|
+
'security',
|
|
68
|
+
'thirdParty',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
function parseArgs(argv) {
|
|
72
|
+
const options = { parts: {}, include: [], exclude: [] };
|
|
73
|
+
let format = 'markdown';
|
|
74
|
+
let standalone = false;
|
|
75
|
+
let listSections = false;
|
|
76
|
+
|
|
77
|
+
/** Flags that take the next argv entry as their value. */
|
|
78
|
+
const valueFlags = {
|
|
79
|
+
'--variant': 'variant',
|
|
80
|
+
'--app-name': 'appName',
|
|
81
|
+
'--company-name': 'companyName',
|
|
82
|
+
'--contact-email': 'contactEmail',
|
|
83
|
+
'--home-url': 'homeUrl',
|
|
84
|
+
'--last-revised': 'lastRevisedDate',
|
|
85
|
+
'--effective-date': 'effectiveDate',
|
|
86
|
+
'--jurisdiction': 'jurisdiction',
|
|
87
|
+
'--minimum-age': 'minimumAge',
|
|
88
|
+
'--children-age': 'childrenAge',
|
|
89
|
+
'--deletion-days': 'dataDeletionDays',
|
|
90
|
+
'--title': 'title',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < argv.length; i++) {
|
|
94
|
+
const arg = argv[i];
|
|
95
|
+
if (arg === '-h' || arg === '--help') {
|
|
96
|
+
console.log(USAGE);
|
|
97
|
+
process.exit(0);
|
|
98
|
+
} else if (arg === '--list-sections') {
|
|
99
|
+
listSections = true;
|
|
100
|
+
} else if (arg === '--standalone') {
|
|
101
|
+
standalone = true;
|
|
102
|
+
} else if (arg === '--no-numbers') {
|
|
103
|
+
options.features = { ...options.features, numbered: false };
|
|
104
|
+
} else if (arg === '--format') {
|
|
105
|
+
format = argv[++i];
|
|
106
|
+
} else if (arg === '--no-part' || arg === '--part') {
|
|
107
|
+
const id = argv[++i];
|
|
108
|
+
if (!PART_IDS.has(id)) {
|
|
109
|
+
console.error(`Unknown part "${id}". Expected one of: ${[...PART_IDS].join(', ')}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
options.parts[id] = arg === '--part';
|
|
113
|
+
} else if (arg === '--include') {
|
|
114
|
+
options.include.push(argv[++i]);
|
|
115
|
+
} else if (arg === '--exclude') {
|
|
116
|
+
options.exclude.push(argv[++i]);
|
|
117
|
+
} else if (arg === '--order') {
|
|
118
|
+
options.order = argv[++i].split(',').map((s) => s.trim()).filter(Boolean);
|
|
119
|
+
} else if (valueFlags[arg]) {
|
|
120
|
+
options[valueFlags[arg]] = argv[++i];
|
|
121
|
+
} else {
|
|
122
|
+
console.error(`Unknown option "${arg}". Run with --help for usage.`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { options, format, standalone, listSections };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { options, format, standalone, listSections } = parseArgs(process.argv.slice(2));
|
|
130
|
+
|
|
131
|
+
if (listSections) {
|
|
132
|
+
const doc = resolveLegalDoc(options);
|
|
133
|
+
for (const section of doc.sections) {
|
|
134
|
+
console.log(section.id);
|
|
135
|
+
for (const sub of section.subsections ?? []) console.log(` ${sub.id}`);
|
|
136
|
+
}
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
switch (format) {
|
|
141
|
+
case 'markdown':
|
|
142
|
+
case 'md':
|
|
143
|
+
process.stdout.write(renderMarkdown(options));
|
|
144
|
+
break;
|
|
145
|
+
case 'html':
|
|
146
|
+
process.stdout.write(renderHtml(options, { standalone }) + '\n');
|
|
147
|
+
break;
|
|
148
|
+
case 'text':
|
|
149
|
+
case 'txt':
|
|
150
|
+
process.stdout.write(renderText(options));
|
|
151
|
+
break;
|
|
152
|
+
case 'json':
|
|
153
|
+
process.stdout.write(JSON.stringify(resolveLegalDoc(options), null, 2) + '\n');
|
|
154
|
+
break;
|
|
155
|
+
default:
|
|
156
|
+
console.error(`Unknown format "${format}". Expected markdown, html, text or json.`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "legal-terms",
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Configurable Terms of Service and Privacy Policy — a scannable summary and the full legal text, with every section addable, removable and reorderable. React page, plus Markdown/HTML/text renderers and a CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "vtempest",
|
|
7
|
+
"license": "rights.institute/prosper",
|
|
8
|
+
"main": "./src/index.ts",
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"module": "./src/index.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"import": "./src/index.ts",
|
|
15
|
+
"require": "./src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"./react": {
|
|
18
|
+
"types": "./src/react/index.ts",
|
|
19
|
+
"import": "./src/react/index.ts"
|
|
20
|
+
},
|
|
21
|
+
"./content/full": "./src/content/full.ts",
|
|
22
|
+
"./content/summary": "./src/content/summary.ts",
|
|
23
|
+
"./package.json": "./package.json",
|
|
24
|
+
"./*": "./src/*"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"legal-terms-privacy-policy": "./cli.mjs"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"src",
|
|
31
|
+
"cli.mjs",
|
|
32
|
+
"readme.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"coverage": "vitest run --coverage",
|
|
38
|
+
"test:ci": "vitest run --reporter=default --reporter=junit --outputFile=./junit.xml --coverage",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"demo": "node cli.mjs --app-name 'Example App' --variant full --format markdown",
|
|
41
|
+
"pub": "npm version patch && npm publish"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"terms-of-service",
|
|
45
|
+
"privacy-policy",
|
|
46
|
+
"legal",
|
|
47
|
+
"gdpr",
|
|
48
|
+
"ccpa",
|
|
49
|
+
"coppa",
|
|
50
|
+
"compliance",
|
|
51
|
+
"react",
|
|
52
|
+
"nextjs"
|
|
53
|
+
],
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/OpenSourceAGI/dev-tools-starter-agent.git",
|
|
57
|
+
"directory": "packages/legal-terms-privacy-policy"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://github.com/OpenSourceAGI/dev-tools-starter-agent/tree/master/packages/legal-terms-privacy-policy",
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"lucide-react": ">=0.4.0",
|
|
62
|
+
"react": ">=18"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"lucide-react": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"react": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/react": "^19.2.0",
|
|
74
|
+
"@types/react-dom": "^19.2.0",
|
|
75
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
76
|
+
"lucide-react": "^1.43.0",
|
|
77
|
+
"react": "^19.2.8",
|
|
78
|
+
"react-dom": "^19.2.8",
|
|
79
|
+
"typescript": "^5.9.3",
|
|
80
|
+
"vitest": "^4.1.0"
|
|
81
|
+
}
|
|
82
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<!-- template-git-repo:badges:start -->
|
|
2
|
+
<p align="center">
|
|
3
|
+
<a href="https://starterdocs.vtempest.workers.dev/docs/packages/legal-terms-privacy-policy"><img src="https://img.shields.io/badge/Docs-blue?logo=ReadTheDocs&logoColor=white" alt="Documentation" /></a>
|
|
4
|
+
<a href="https://stackblitz.com/github/OpenSourceAGI/dev-tools-starter-agent/tree/master/packages/legal-terms-privacy-policy"><img height="20px" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open in StackBlitz" /></a>
|
|
5
|
+
<br />
|
|
6
|
+
<a href="https://www.npmjs.com/package/legal-terms-privacy-policy"><img src="https://img.shields.io/npm/dm/legal-terms-privacy-policy.svg" alt="NPM Monthly Downloads" /></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/legal-terms-privacy-policy"><img src="https://img.shields.io/npm/v/legal-terms-privacy-policy.svg" alt="npm version" /></a>
|
|
8
|
+
<a href="https://www.npmjs.com/package/legal-terms-privacy-policy"><img src="https://img.shields.io/npm/dt/legal-terms-privacy-policy.svg" alt="NPM Total Downloads" /></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/legal-terms-privacy-policy"><img src="https://img.shields.io/npm/types/legal-terms-privacy-policy" alt="TypeScript types" /></a>
|
|
10
|
+
<a href="https://packagephobia.com/result?p=legal-terms-privacy-policy"><img src="https://packagephobia.com/badge?p=legal-terms-privacy-policy" alt="Install size" /></a>
|
|
11
|
+
</p>
|
|
12
|
+
<!-- template-git-repo:badges:end -->
|
|
13
|
+
|
|
14
|
+
<!-- skills:install:start -->
|
|
15
|
+
**🤖 Agent skill** — `npx skills@latest add https://github.com/OpenSourceAGI/dev-tools-starter-agent --skill legal-terms-privacy-policy` ([what it covers](../../skills/legal-terms-privacy-policy/SKILL.md))
|
|
16
|
+
<!-- skills:install:end -->
|
|
17
|
+
|
|
18
|
+
# legal-terms-privacy-policy
|
|
19
|
+
|
|
20
|
+
One combined **Terms of Service + Privacy Policy**, in two presentations of the same policy, with every section addable, removable and reorderable from config.
|
|
21
|
+
|
|
22
|
+
- **Summary** — the scannable card-and-icon layout published at [rights.institute/terms-privacy](https://rights.institute/terms-privacy).
|
|
23
|
+
- **Full text** — the long-form legal document published by QwkSearch, Debate AI and AI Broker.
|
|
24
|
+
|
|
25
|
+
Ship both and let readers switch between them, or pin a page to one. Nothing in the package needs editing to adopt it: the product name, contact address, dates, which clauses appear and in what order all come from props.
|
|
26
|
+
|
|
27
|
+
> Not legal advice. This is boilerplate to start from — have a lawyer review the text you publish.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm install legal-terms-privacy-policy
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The package ships TypeScript source rather than a build output, matching the other packages in this monorepo. In a Next.js app, add it to `transpilePackages`:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
// next.config.js
|
|
39
|
+
export default { transpilePackages: ['legal-terms-privacy-policy'] };
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`react` and `lucide-react` are optional peers — needed only for the React page, not for the Markdown/HTML/text renderers or the CLI.
|
|
43
|
+
|
|
44
|
+
## React page
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { LegalTermsPrivacyPolicy } from 'legal-terms-privacy-policy/react';
|
|
48
|
+
|
|
49
|
+
export default function TermsPage() {
|
|
50
|
+
return (
|
|
51
|
+
<LegalTermsPrivacyPolicy
|
|
52
|
+
appName="QwkSearch"
|
|
53
|
+
contactEmail="legal@qwksearch.com"
|
|
54
|
+
lastRevisedDate="March 1, 2026"
|
|
55
|
+
defaultVariant="full"
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The component renders the whole page — back link, title, badges, revision date, the summary ⇄ full-text switch, section navigation and the body. It is Tailwind-styled and works in light and dark.
|
|
62
|
+
|
|
63
|
+
### Controlling the variant
|
|
64
|
+
|
|
65
|
+
Uncontrolled, the switch keeps its own state starting from `defaultVariant`. Pass `variant` and `onVariantChange` to drive it from the URL instead:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
'use client';
|
|
69
|
+
const [variant, setVariant] = useState<Variant>('summary');
|
|
70
|
+
<LegalTermsPrivacyPolicy variant={variant} onVariantChange={setVariant} appName="Acme" />;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
To publish only one presentation, set `variant` and turn the switch off:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<LegalTermsPrivacyPolicy appName="Acme" variant="full" features={{ variantSwitch: false }} />
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
Every option below works the same way in the React component, the renderers and the CLI.
|
|
82
|
+
|
|
83
|
+
### Values substituted into the text
|
|
84
|
+
|
|
85
|
+
The legal text carries `{{token}}` placeholders. Unknown tokens are left visible rather than blanked, so a typo shows up on the page instead of silently deleting a clause.
|
|
86
|
+
|
|
87
|
+
| Option | Default | Appears in |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| `appName` | `"Our Service"` | Throughout |
|
|
90
|
+
| `companyName` | same as `appName` | Liability, contact, footer |
|
|
91
|
+
| `contactEmail` | `"legal@example.com"` | Accounts, retention, contact |
|
|
92
|
+
| `homeUrl` | `"/"` | "Back to Home" link |
|
|
93
|
+
| `lastRevisedDate` | `"January 1, 2025"` | Under the title |
|
|
94
|
+
| `effectiveDate` | same as `lastRevisedDate` | Under the title |
|
|
95
|
+
| `jurisdiction` | `"the United States"` | Introduction |
|
|
96
|
+
| `minimumAge` | `18` | Acceptance of Terms |
|
|
97
|
+
| `childrenAge` | `13` | Children's Privacy |
|
|
98
|
+
| `dataDeletionDays` | `30` | Data Security and Retention |
|
|
99
|
+
| `tokens` | `{}` | Extra placeholders for your own sections |
|
|
100
|
+
|
|
101
|
+
### Adding and removing parts
|
|
102
|
+
|
|
103
|
+
`parts` switches named groups of clauses on or off in one go:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
<LegalTermsPrivacyPolicy
|
|
107
|
+
appName="Grab URL"
|
|
108
|
+
parts={{ ai: false, california: false }} // no model in the loop, no CA notice
|
|
109
|
+
/>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| Part | Covers |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| `core` | Introduction, changes, accounts, use, materials, feedback, warranties, termination, contact. Always on — switching it off is ignored. |
|
|
115
|
+
| `ai` | The Artificial Intelligence Ethical Use Policy and AI-specific clauses |
|
|
116
|
+
| `privacy` | Collection, use, disclosure and retention of personal data |
|
|
117
|
+
| `cookies` | Cookies, tracking technologies and Do Not Track |
|
|
118
|
+
| `california` | The CCPA/CPRA resident notice |
|
|
119
|
+
| `children` | The COPPA under-13 notice |
|
|
120
|
+
| `security` | Security measures and data retention |
|
|
121
|
+
| `thirdParty` | Third-party links and social features |
|
|
122
|
+
|
|
123
|
+
For finer control, work by section id — `include` keeps only what you name, `exclude` drops it, and both reach subsections:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
exclude={['social-features', 'california-selling']}
|
|
127
|
+
include={['introduction', 'privacy-policy', 'contact']}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Naming a parent in `include` keeps its whole subtree (`['ai-ethics']` is the section and its four sub-policies); naming only a child keeps the parent as its heading (`['california-rights']` renders under "California Residents"). `exclude` still applies inside an included parent.
|
|
131
|
+
|
|
132
|
+
Run `npx legal-terms-privacy-policy --list-sections` (add `--variant summary`) to see every id.
|
|
133
|
+
|
|
134
|
+
### Rewriting and adding sections
|
|
135
|
+
|
|
136
|
+
`replace` patches a section by id, merging over the built-in one — pass only the fields you are changing:
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
replace={{
|
|
140
|
+
contact: { blocks: [{ type: 'p', text: 'Write to {{companyName}}, 1 Main St.' }] },
|
|
141
|
+
'california-selling': { title: 'We Do Not Sell Your Data' },
|
|
142
|
+
}}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`add` inserts your own sections, anchored to an existing one:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
add={[{
|
|
149
|
+
after: 'termination',
|
|
150
|
+
section: {
|
|
151
|
+
id: 'arbitration',
|
|
152
|
+
title: 'Arbitration and Governing Law',
|
|
153
|
+
icon: 'Scale',
|
|
154
|
+
accent: 'slate',
|
|
155
|
+
blocks: [
|
|
156
|
+
{ type: 'p', text: 'Disputes with {{companyName}} are resolved by binding arbitration.' },
|
|
157
|
+
{ type: 'ul', lead: 'Exceptions:', items: ['Small claims court', 'Injunctive relief'] },
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
}]}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Anchors are `after`, `before` or `at` (an index); with none of them, the section is appended. `order` puts named sections first, in the order given, and leaves the rest in place.
|
|
164
|
+
|
|
165
|
+
### Block types
|
|
166
|
+
|
|
167
|
+
Sections hold `blocks`, so caller-authored content renders in every format:
|
|
168
|
+
|
|
169
|
+
| Block | Shape |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| `p` | `{ type: 'p', text, strong? }` |
|
|
172
|
+
| `ol` / `ul` | `{ type: 'ol', lead?, items }` — items are strings or `{ text, items }` for one nested level |
|
|
173
|
+
| `cards` | `{ type: 'cards', columns?: 1-4, center?, items: [{ title, text?, items?, icon? }] }` |
|
|
174
|
+
| `note` | `{ type: 'note', title?, text?, items?, icon? }` |
|
|
175
|
+
|
|
176
|
+
Card and note blocks render as tinted tiles in the summary variant and flatten to headings and lists in the full-text variant, so you only write them once. `icon` names a [lucide](https://lucide.dev) icon; unknown names fall back to a document glyph, or pass your own components through the `icons` prop.
|
|
177
|
+
|
|
178
|
+
### Chrome
|
|
179
|
+
|
|
180
|
+
`features` toggles the page furniture: `backLink`, `sidebar`, `tableOfContents`, `copyButtons`, `badges`, `variantSwitch`, `footer`, `numbered`. `badges` (the array) sets the compliance pills — `['GDPR Compliant', 'CCPA Compliant', 'Cookie Policy']` by default; pass `[]` to drop them.
|
|
181
|
+
|
|
182
|
+
## Rendering without React
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { renderMarkdown, renderHtml, renderText, resolveLegalDoc } from 'legal-terms-privacy-policy';
|
|
186
|
+
|
|
187
|
+
const config = { appName: 'Acme', contactEmail: 'legal@acme.com', parts: { ai: false } };
|
|
188
|
+
|
|
189
|
+
renderMarkdown(config); // TERMS.md, MDX docs page
|
|
190
|
+
renderHtml(config, { standalone: true }); // complete styled page
|
|
191
|
+
renderText(config); // email, CLI, in-app agreement dialog
|
|
192
|
+
resolveLegalDoc(config).sections; // the resolved tree, to render yourself
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`LEGAL_CSS` is exported for pairing with the HTML fragment. Interpolated values are HTML-escaped, and emails and URLs in the text become links.
|
|
196
|
+
|
|
197
|
+
## CLI
|
|
198
|
+
|
|
199
|
+
```sh
|
|
200
|
+
npx legal-terms-privacy-policy --app-name Acme --variant full > TERMS.md
|
|
201
|
+
|
|
202
|
+
npx legal-terms-privacy-policy \
|
|
203
|
+
--app-name Acme --contact-email legal@acme.com \
|
|
204
|
+
--format html --standalone \
|
|
205
|
+
--no-part california --exclude social-features > terms.html
|
|
206
|
+
|
|
207
|
+
npx legal-terms-privacy-policy --list-sections --variant summary
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`--format` takes `markdown`, `html`, `text` or `json`. Run `--help` for the full list.
|
|
211
|
+
|
|
212
|
+
## Keeping the two variants honest
|
|
213
|
+
|
|
214
|
+
The summary is a plain-language restatement; the full text is what binds. If you edit one, edit the other — `src/content/summary.ts` and `src/content/full.ts` are the two files to keep in step. Publishing a summary whose claims the full text does not support is the failure mode this package is shaped to avoid, which is why the switch is on by default.
|
|
215
|
+
|
|
216
|
+
## Tests
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
npm test
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Covers token substitution, part and id filtering, patching, insertion, ordering, and each renderer — including that neither variant ships an unsubstituted `{{token}}` and that interpolated values cannot inject markup.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
[PROSPER 1.0.0](https://rights.institute/prosper)
|