legal-terms
v0.1.40
Published
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.
Maintainers
Readme
🤖 Agent skill — npx skills@latest add https://github.com/OpenSourceAGI/dev-tools-starter-agent --skill legal-terms-privacy-policy (what it covers)
legal-terms-privacy-policy
One combined Terms of Service + Privacy Policy, in two presentations of the same policy, with every section addable, removable and reorderable from config.
- Summary — the scannable card-and-icon layout published at rights.institute/terms-privacy.
- Full text — the long-form legal document published by QwkSearch, Debate AI and AI Broker.
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.
Not legal advice. This is boilerplate to start from — have a lawyer review the text you publish.
Install
npm install legal-terms-privacy-policyThe 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:
// next.config.js
export default { transpilePackages: ['legal-terms-privacy-policy'] };react and lucide-react are optional peers — needed only for the React page, not for the Markdown/HTML/text renderers or the CLI.
React page
import { LegalTermsPrivacyPolicy } from 'legal-terms-privacy-policy/react';
export default function TermsPage() {
return (
<LegalTermsPrivacyPolicy
appName="QwkSearch"
contactEmail="[email protected]"
lastRevisedDate="March 1, 2026"
defaultVariant="full"
/>
);
}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.
Controlling the variant
Uncontrolled, the switch keeps its own state starting from defaultVariant. Pass variant and onVariantChange to drive it from the URL instead:
'use client';
const [variant, setVariant] = useState<Variant>('summary');
<LegalTermsPrivacyPolicy variant={variant} onVariantChange={setVariant} appName="Acme" />;To publish only one presentation, set variant and turn the switch off:
<LegalTermsPrivacyPolicy appName="Acme" variant="full" features={{ variantSwitch: false }} />Configuration
Every option below works the same way in the React component, the renderers and the CLI.
Values substituted into the text
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.
| Option | Default | Appears in |
| --- | --- | --- |
| appName | "Our Service" | Throughout |
| companyName | same as appName | Liability, contact, footer |
| contactEmail | "[email protected]" | Accounts, retention, contact |
| homeUrl | "/" | "Back to Home" link |
| lastRevisedDate | "January 1, 2025" | Under the title |
| effectiveDate | same as lastRevisedDate | Under the title |
| jurisdiction | "the United States" | Introduction |
| minimumAge | 18 | Acceptance of Terms |
| childrenAge | 13 | Children's Privacy |
| dataDeletionDays | 30 | Data Security and Retention |
| tokens | {} | Extra placeholders for your own sections |
Adding and removing parts
parts switches named groups of clauses on or off in one go:
<LegalTermsPrivacyPolicy
appName="Grab URL"
parts={{ ai: false, california: false }} // no model in the loop, no CA notice
/>| Part | Covers |
| --- | --- |
| core | Introduction, changes, accounts, use, materials, feedback, warranties, termination, contact. Always on — switching it off is ignored. |
| ai | The Artificial Intelligence Ethical Use Policy and AI-specific clauses |
| privacy | Collection, use, disclosure and retention of personal data |
| cookies | Cookies, tracking technologies and Do Not Track |
| california | The CCPA/CPRA resident notice |
| children | The COPPA under-13 notice |
| security | Security measures and data retention |
| thirdParty | Third-party links and social features |
For finer control, work by section id — include keeps only what you name, exclude drops it, and both reach subsections:
exclude={['social-features', 'california-selling']}
include={['introduction', 'privacy-policy', 'contact']}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.
Run npx legal-terms-privacy-policy --list-sections (add --variant summary) to see every id.
Rewriting and adding sections
replace patches a section by id, merging over the built-in one — pass only the fields you are changing:
replace={{
contact: { blocks: [{ type: 'p', text: 'Write to {{companyName}}, 1 Main St.' }] },
'california-selling': { title: 'We Do Not Sell Your Data' },
}}add inserts your own sections, anchored to an existing one:
add={[{
after: 'termination',
section: {
id: 'arbitration',
title: 'Arbitration and Governing Law',
icon: 'Scale',
accent: 'slate',
blocks: [
{ type: 'p', text: 'Disputes with {{companyName}} are resolved by binding arbitration.' },
{ type: 'ul', lead: 'Exceptions:', items: ['Small claims court', 'Injunctive relief'] },
],
},
}]}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.
Block types
Sections hold blocks, so caller-authored content renders in every format:
| Block | Shape |
| --- | --- |
| p | { type: 'p', text, strong? } |
| ol / ul | { type: 'ol', lead?, items } — items are strings or { text, items } for one nested level |
| cards | { type: 'cards', columns?: 1-4, center?, items: [{ title, text?, items?, icon? }] } |
| note | { type: 'note', title?, text?, items?, icon? } |
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 icon; unknown names fall back to a document glyph, or pass your own components through the icons prop.
Chrome
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.
Rendering without React
import { renderMarkdown, renderHtml, renderText, resolveLegalDoc } from 'legal-terms-privacy-policy';
const config = { appName: 'Acme', contactEmail: '[email protected]', parts: { ai: false } };
renderMarkdown(config); // TERMS.md, MDX docs page
renderHtml(config, { standalone: true }); // complete styled page
renderText(config); // email, CLI, in-app agreement dialog
resolveLegalDoc(config).sections; // the resolved tree, to render yourselfLEGAL_CSS is exported for pairing with the HTML fragment. Interpolated values are HTML-escaped, and emails and URLs in the text become links.
CLI
npx legal-terms-privacy-policy --app-name Acme --variant full > TERMS.md
npx legal-terms-privacy-policy \
--app-name Acme --contact-email [email protected] \
--format html --standalone \
--no-part california --exclude social-features > terms.html
npx legal-terms-privacy-policy --list-sections --variant summary--format takes markdown, html, text or json. Run --help for the full list.
Keeping the two variants honest
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.
Tests
npm testCovers 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.
