chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
export { LegalBlockGroup } from './legal-block-group'
|
||||
export { LegalSectionView } from './legal-section'
|
||||
export { ProseHero } from './prose-hero'
|
||||
export { ProseLink } from './prose-link'
|
||||
export { ProseShell } from './prose-shell'
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { LegalBlockView } from './legal-block'
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { LegalBlockView } from './legal-block'
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { cn } from '@sim/emcn'
|
||||
import { PROSE_SPACING, PROSE_TYPE } from '@/app/(landing)/components/prose-page/constants'
|
||||
import type { LegalBlock } from '@/app/(landing)/components/prose-page/types'
|
||||
|
||||
/**
|
||||
* Renders a single {@link LegalBlock} into its canonical chrome. The block's
|
||||
* `kind` discriminant selects the element (paragraph / subheading `<h3>` /
|
||||
* bulleted list / callout box); all sizing and color come from `PROSE_TYPE`, so
|
||||
* Terms and Privacy share one visual treatment for every block type. Content
|
||||
* only - no layout knob. Server Component.
|
||||
*/
|
||||
|
||||
interface LegalBlockViewProps {
|
||||
block: LegalBlock
|
||||
}
|
||||
|
||||
export function LegalBlockView({ block }: LegalBlockViewProps) {
|
||||
switch (block.kind) {
|
||||
case 'paragraph':
|
||||
return <p className={PROSE_TYPE.body}>{block.content}</p>
|
||||
case 'subheading':
|
||||
return <h3 className={PROSE_TYPE.h3}>{block.text}</h3>
|
||||
case 'list':
|
||||
return (
|
||||
<ul className={cn('list-disc', PROSE_SPACING.listIndent, PROSE_SPACING.listStack)}>
|
||||
{block.items.map((item, index) => {
|
||||
const itemKey = `item-${index}`
|
||||
return (
|
||||
<li key={itemKey} className={PROSE_TYPE.list}>
|
||||
{item}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
case 'callout':
|
||||
return <div className={PROSE_TYPE.callout}>{block.content}</div>
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { LegalBlockGroup } from './legal-block-group'
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { cn } from '@sim/emcn'
|
||||
import { LegalBlockView } from '@/app/(landing)/components/prose-page/components/legal-block-group/components'
|
||||
import { PROSE_SPACING } from '@/app/(landing)/components/prose-page/constants'
|
||||
import type { LegalBlock } from '@/app/(landing)/components/prose-page/types'
|
||||
|
||||
/**
|
||||
* Renders an ordered run of {@link LegalBlock}s as a vertically-stacked group at
|
||||
* the shared block rhythm (`PROSE_SPACING.blockStack`). This is the single source
|
||||
* of the block-group markup - consumed both by the page intro ({@link ProsePage})
|
||||
* and by every {@link LegalSectionView} - so the intro and the sections can never
|
||||
* drift in spacing. Server Component.
|
||||
*/
|
||||
|
||||
interface LegalBlockGroupProps {
|
||||
blocks: LegalBlock[]
|
||||
}
|
||||
|
||||
export function LegalBlockGroup({ blocks }: LegalBlockGroupProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col', PROSE_SPACING.blockStack)}>
|
||||
{blocks.map((block, index) => (
|
||||
<LegalBlockView key={`${block.kind}-${index}`} block={block} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { LegalSectionView } from './legal-section'
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { cn } from '@sim/emcn'
|
||||
import { LegalBlockGroup } from '@/app/(landing)/components/prose-page/components/legal-block-group'
|
||||
import { PROSE_SPACING, PROSE_TYPE } from '@/app/(landing)/components/prose-page/constants'
|
||||
import type { LegalSection } from '@/app/(landing)/components/prose-page/types'
|
||||
|
||||
/**
|
||||
* Renders one {@link LegalSection} as a `<section>` landmark: an `<h2>` wired to
|
||||
* `aria-labelledby` followed by the section's ordered blocks. Spacing is owned
|
||||
* by `PROSE_SPACING` (heading → blocks, and block → block), so every section in
|
||||
* Terms and Privacy keeps the same rhythm. Server Component.
|
||||
*/
|
||||
|
||||
interface LegalSectionViewProps {
|
||||
section: LegalSection
|
||||
}
|
||||
|
||||
export function LegalSectionView({ section }: LegalSectionViewProps) {
|
||||
const headingId = `${section.id}-heading`
|
||||
|
||||
return (
|
||||
<section
|
||||
id={section.id}
|
||||
aria-labelledby={headingId}
|
||||
className={cn('flex flex-col', PROSE_SPACING.sectionStack)}
|
||||
>
|
||||
<h2 id={headingId} className={PROSE_TYPE.h2}>
|
||||
{section.heading}
|
||||
</h2>
|
||||
<LegalBlockGroup blocks={section.blocks} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ProseHero } from './prose-hero'
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@sim/emcn'
|
||||
import { PROSE_SPACING, PROSE_TYPE } from '@/app/(landing)/components/prose-page/constants'
|
||||
|
||||
/**
|
||||
* The shared prose hero - the only `<h1>` on a prose page. Renders the title,
|
||||
* an optional meta line (e.g. "Last updated: …"), an optional lead paragraph,
|
||||
* and an optional actions slot (e.g. the changelog's GitHub / Docs / RSS chips).
|
||||
*
|
||||
* Reused by Terms, Privacy, and the Changelog so the headline rhythm is
|
||||
* identical across all three. Spacing comes from `PROSE_SPACING.heroStack`; the
|
||||
* navbar-clearing top padding is owned by {@link ProseShell}. Server Component -
|
||||
* any interactive `actions` are passed as an already-rendered client island.
|
||||
*/
|
||||
|
||||
interface ProseHeroProps {
|
||||
/** The page's single `<h1>` text. */
|
||||
title: string
|
||||
/** Optional muted meta line beneath the title (e.g. a "Last updated" date). */
|
||||
meta?: string
|
||||
/** Optional lead paragraph in the body color. */
|
||||
lead?: string
|
||||
/** Optional actions row (a client island of links/chips). */
|
||||
actions?: ReactNode
|
||||
}
|
||||
|
||||
export function ProseHero({ title, meta, lead, actions }: ProseHeroProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col', PROSE_SPACING.heroStack)}>
|
||||
<h1 className={PROSE_TYPE.h1}>{title}</h1>
|
||||
{meta ? <p className={PROSE_TYPE.meta}>{meta}</p> : null}
|
||||
{lead ? <p className={PROSE_TYPE.lead}>{lead}</p> : null}
|
||||
{actions}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ProseLink } from './prose-link'
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { PROSE_TYPE } from '@/app/(landing)/components/prose-page/constants'
|
||||
|
||||
/**
|
||||
* The one inline link used inside legal prose - a single source of truth for
|
||||
* link chrome (`PROSE_TYPE.link`) so every mailto/internal/external link in the
|
||||
* configs reads identically. Configs pass only `href` + children; never a
|
||||
* className.
|
||||
*
|
||||
* Routing/safety is derived from the href: an absolute `http(s)://` URL renders
|
||||
* as a hardened external anchor (`target='_blank' rel='noopener noreferrer'`), a
|
||||
* `mailto:` href as a plain anchor, and anything else as a crawlable Next
|
||||
* `<Link>`. Server Component.
|
||||
*/
|
||||
|
||||
interface ProseLinkProps {
|
||||
/** Destination - `http(s)://`, `mailto:`, or an internal path. */
|
||||
href: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function isHttp(href: string): boolean {
|
||||
return /^https?:\/\//.test(href)
|
||||
}
|
||||
|
||||
export function ProseLink({ href, children }: ProseLinkProps) {
|
||||
if (isHttp(href)) {
|
||||
return (
|
||||
<a href={href} target='_blank' rel='noopener noreferrer' className={PROSE_TYPE.link}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
if (href.startsWith('mailto:')) {
|
||||
return (
|
||||
<a href={href} className={PROSE_TYPE.link}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={href} className={PROSE_TYPE.link}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ProseShell } from './prose-shell'
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@sim/emcn'
|
||||
import { PROSE_SPACING } from '@/app/(landing)/components/prose-page/constants'
|
||||
|
||||
/**
|
||||
* The prose page frame - owns the `<main id='main-content'>` landmark, the one
|
||||
* horizontal gutter (matching the navbar and footer so content aligns with the
|
||||
* wordmark), the outer content cap, the navbar-clearing top padding, and the
|
||||
* vertical rhythm of the full-width left-aligned content column.
|
||||
*
|
||||
* Every prose page (Terms, Privacy, Changelog) renders inside this shell, so the
|
||||
* page frame is described once and can never drift. Consumers pass only the
|
||||
* content nodes; all spacing comes from `PROSE_SPACING`. Server Component.
|
||||
*/
|
||||
|
||||
interface ProseShellProps {
|
||||
/** The content column - a {@link ProseHero} followed by sections. */
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function ProseShell({ children }: ProseShellProps) {
|
||||
return (
|
||||
<main
|
||||
id='main-content'
|
||||
className={cn(PROSE_SPACING.outerCap, PROSE_SPACING.gutter, PROSE_SPACING.heroTopPadding)}
|
||||
>
|
||||
<div className={cn('flex flex-col', PROSE_SPACING.bodyRhythm)}>{children}</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Prose-layout spacing and type - the single source of truth for every gutter,
|
||||
* gap, reading-column width, and text token used across the legal pages
|
||||
* (Terms, Privacy) and the Changelog. Modeled on the platform page's
|
||||
* `PLATFORM_SPACING`: the padding fortress lives here, so a reviewer changes
|
||||
* spacing in exactly one place and Terms can never drift from Privacy.
|
||||
*
|
||||
* No prose component hard-codes a spacing value inline and no consumer page can
|
||||
* reach these knobs - a page passes only content (strings + `ReactNode`), and
|
||||
* the layout decides where and with how much space.
|
||||
*
|
||||
* All values are Tailwind class fragments (not raw numbers) so they compose
|
||||
* directly into `className` strings and stay legible.
|
||||
*/
|
||||
export const PROSE_SPACING = {
|
||||
/**
|
||||
* The one horizontal gutter, matching the navbar and footer so content starts
|
||||
* on the wordmark's vertical line at every width.
|
||||
*/
|
||||
gutter: 'px-20 max-lg:px-8 max-sm:px-5',
|
||||
/** Outer content cap, matching navbar/footer (`mx-auto w-full max-w-[1460px]`). */
|
||||
outerCap: 'mx-auto w-full max-w-[1460px]',
|
||||
/** Top padding that clears the sticky navbar, matching the platform hero. */
|
||||
heroTopPadding: 'pt-[112px] max-sm:pt-20',
|
||||
/** Vertical rhythm of the content column - hero → body and section → section. */
|
||||
bodyRhythm: 'gap-16 max-sm:gap-12',
|
||||
/** Hero header sub-stack (title → meta → lead → actions). */
|
||||
heroStack: 'gap-5',
|
||||
/** Section sub-stack (heading → block group). */
|
||||
sectionStack: 'gap-4',
|
||||
/** Gap between blocks (paragraphs, lists, subheadings, callouts) within a group. */
|
||||
blockStack: 'gap-4',
|
||||
/** List-item vertical spacing. */
|
||||
listStack: 'space-y-2',
|
||||
/** List left indent for the disc marker. */
|
||||
listIndent: 'pl-6',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Prose type tokens - the single source of truth for every heading size, body
|
||||
* color, list, callout, and inline-link treatment. Centralized alongside the
|
||||
* spacing fortress so chrome is described once and never re-derived per page.
|
||||
* Uses the platform light tokens exclusively (no hex, no `--landing-*`).
|
||||
*/
|
||||
export const PROSE_TYPE = {
|
||||
h1: 'text-balance text-[40px] text-[var(--text-primary)] leading-[1.1] max-sm:text-[32px]',
|
||||
meta: 'text-[14px] text-[var(--text-muted)]',
|
||||
lead: 'max-w-[640px] text-[20px] text-[var(--text-body)] leading-[1.5] max-sm:text-[17px]',
|
||||
h2: 'text-[24px] text-[var(--text-primary)] leading-[1.25] max-sm:text-[21px]',
|
||||
h3: 'text-[17px] text-[var(--text-primary)] leading-[1.35]',
|
||||
body: 'text-[15px] text-[var(--text-body)] leading-[1.65]',
|
||||
list: 'text-[15px] text-[var(--text-body)] leading-[1.6] marker:text-[var(--text-muted)]',
|
||||
callout:
|
||||
'rounded-lg border border-[var(--border)] bg-[var(--surface-2)] px-4 py-3 text-[14px] text-[var(--text-body)] leading-[1.6]',
|
||||
link: 'text-[var(--text-primary)] underline underline-offset-2 transition-colors hover:text-[var(--text-body)]',
|
||||
} as const
|
||||
@@ -0,0 +1,6 @@
|
||||
export { ProseHero } from './components/prose-hero'
|
||||
export { ProseLink } from './components/prose-link'
|
||||
export { ProseShell } from './components/prose-shell'
|
||||
export { PROSE_SPACING, PROSE_TYPE } from './constants'
|
||||
export { ProsePage } from './prose-page'
|
||||
export type { LegalBlock, LegalPageConfig, LegalSection } from './types'
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
LegalBlockGroup,
|
||||
LegalSectionView,
|
||||
ProseHero,
|
||||
ProseShell,
|
||||
} from '@/app/(landing)/components/prose-page/components'
|
||||
import type { LegalPageConfig } from '@/app/(landing)/components/prose-page/types'
|
||||
|
||||
/**
|
||||
* The reusable legal-page content stack - the single component both Terms and
|
||||
* Privacy consume. A legal route renders the shared {@link LandingShell} and
|
||||
* drops in one `<ProsePage config={…} />`, so the two documents share one hero,
|
||||
* one full-width left-aligned column, one rhythm, and one set of block
|
||||
* treatments - they cannot drift from each other.
|
||||
*
|
||||
* Order is fixed: hero (the page's only `<h1>`, carrying the title, the
|
||||
* "Last updated" meta, and the lead) → an optional intro block group under the
|
||||
* `<h1>` → the configured legal sections in array order. The heading outline is
|
||||
* strict H1 → H2 (per section) → H3 (per subheading block), never skipped.
|
||||
* Server Component - the whole page is static.
|
||||
*/
|
||||
|
||||
interface ProsePageProps {
|
||||
/** The complete legal-page content - hero copy, intro, and ordered sections. */
|
||||
config: LegalPageConfig
|
||||
}
|
||||
|
||||
export function ProsePage({ config }: ProsePageProps) {
|
||||
return (
|
||||
<ProseShell>
|
||||
<ProseHero
|
||||
title={config.title}
|
||||
meta={`Last updated: ${config.lastUpdated}`}
|
||||
lead={config.description}
|
||||
/>
|
||||
|
||||
{config.intro.length > 0 ? <LegalBlockGroup blocks={config.intro} /> : null}
|
||||
|
||||
{config.sections.map((section) => (
|
||||
<LegalSectionView key={section.id} section={section} />
|
||||
))}
|
||||
</ProseShell>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* Legal-page content contract - the entire content a legal route passes to
|
||||
* {@link ProsePage}. Every field is content-only: copy strings plus `ReactNode`
|
||||
* for inline rich text (bold terms, mailto links via {@link ProseLink}). There
|
||||
* is deliberately no `className`, `style`, or layout knob anywhere in this tree;
|
||||
* spacing and chrome live entirely in `PROSE_SPACING`/`PROSE_TYPE`. A page
|
||||
* describes WHAT to show; the layout decides WHERE and with how much space.
|
||||
*
|
||||
* Both Terms and Privacy are rendered by feeding this one primitive a config, so
|
||||
* the two documents share a single layout and can never visually drift.
|
||||
*/
|
||||
|
||||
/** A single rendered block within a legal section (or the page intro). */
|
||||
export type LegalBlock =
|
||||
/** A body paragraph. `content` may carry inline `<strong>` / {@link ProseLink}. */
|
||||
| { kind: 'paragraph'; content: ReactNode }
|
||||
/** A sub-heading inside a section - rendered as `<h3>`. */
|
||||
| { kind: 'subheading'; text: string }
|
||||
/** A bulleted list. Each item may carry inline rich text. */
|
||||
| { kind: 'list'; items: ReactNode[] }
|
||||
/** An emphasized callout box (e.g. the arbitration / GDPR notices). */
|
||||
| { kind: 'callout'; content: ReactNode }
|
||||
|
||||
/** A numbered (or named) legal section - an `<h2>` plus its ordered blocks. */
|
||||
export interface LegalSection {
|
||||
/** Stable id for the `<section>` landmark and `aria-labelledby` wiring. */
|
||||
id: string
|
||||
/** The section's `<h2>` heading. */
|
||||
heading: string
|
||||
/** The section's ordered content blocks. */
|
||||
blocks: LegalBlock[]
|
||||
}
|
||||
|
||||
/** The complete legal page: hero copy plus intro blocks and ordered sections. */
|
||||
export interface LegalPageConfig {
|
||||
/** The page's single `<h1>` (e.g. "Terms of Service"). */
|
||||
title: string
|
||||
/** Constitution-compliant lead beneath the heading, in the body color. */
|
||||
description: string
|
||||
/** The "Last updated" date string (e.g. "October 11, 2025"). */
|
||||
lastUpdated: string
|
||||
/** Intro blocks rendered under the `<h1>`, before the first numbered section. */
|
||||
intro: LegalBlock[]
|
||||
/** The ordered legal sections. */
|
||||
sections: LegalSection[]
|
||||
}
|
||||
Reference in New Issue
Block a user