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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -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'
@@ -0,0 +1 @@
export { LegalBlockView } from './legal-block'
@@ -0,0 +1 @@
export { LegalBlockView } from './legal-block'
@@ -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'
@@ -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'
@@ -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[]
}