Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions site/scripts/route-checks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@ export function validateSiteRoutes(routes, fail) {
if (route.navigationGroup !== undefined && !navigationGroups.has(route.navigationGroup)) {
fail(`site route ${route.path} has an invalid navigation group.`);
}
if (route.heading !== undefined && (typeof route.heading !== "string" || route.heading.trim() === "")) {
fail(`site route ${route.path} has an invalid heading.`);
}
if (route.relatedDemoLabel !== undefined && (typeof route.relatedDemoLabel !== "string" || route.relatedDemoLabel.trim() === "")) {
fail(`site route ${route.path} has an invalid related demo label.`);
}
if (route.integration !== undefined) {
const expectedGroup = route.integration.kind === "adapter" ? "Adapters" : "Connectors";
if (!new Set(["adapter", "connector"]).has(route.integration.kind)) {
fail(`site route ${route.path} has an invalid integration kind.`);
}
if (route.navigationGroup !== expectedGroup) {
fail(`site route ${route.path} integration kind does not match its navigation group.`);
}
if (typeof route.integration.packageName !== "string" || !route.integration.packageName.startsWith("@interactive-os/")) {
fail(`site route ${route.path} has an invalid integration package.`);
}
if (!new Set(["available", "planned"]).has(route.integration.status)) {
fail(`site route ${route.path} has an invalid integration status.`);
}
}

if (typeof route.path === "string") {
if (paths.has(route.path)) fail(`site routes contain duplicate path ${route.path}.`);
Expand All @@ -61,6 +82,16 @@ export function validateSiteRoutes(routes, fail) {
if (routes[0]?.path !== "/") {
fail("site routes must start with the overview route.");
}

for (const route of routes) {
if (route.relatedDemoPath !== undefined && !paths.has(route.relatedDemoPath)) {
fail(`site route ${route.path} points to an unknown related demo ${route.relatedDemoPath}.`);
}
const isIntegrationDemo = /^\/(?:adapters|connectors)\/[^/]+$/.test(route.path);
if (isIntegrationDemo && route.integration === undefined) {
fail(`integration demo ${route.path} must declare catalog metadata.`);
}
}
}

export function routeFile(path) {
Expand Down
93 changes: 80 additions & 13 deletions site/site-routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"path": "/docs",
"label": "Why",
"title": "json-document Docs - json-document",
"heading": "왜 json-document인가",
"description": "JSON 문서가 읽기, 검사, commit, 구독에 하나의 계약을 사용하는 이유를 설명합니다.",
"language": "ko",
"navigationGroup": "JSON Document"
Expand All @@ -17,6 +18,7 @@
"path": "/docs/tutorial",
"label": "Quickstart",
"title": "Tutorial - json-document",
"heading": "작은 카드 문서 만들기",
"description": "JSON 문서를 만들고 읽고 검사해 commit한 뒤 변경을 구독하는 과정을 따라갑니다.",
"language": "ko",
"navigationGroup": "JSON Document"
Expand All @@ -33,6 +35,7 @@
"path": "/docs/api",
"label": "API Reference",
"title": "json-document API - json-document",
"heading": "json-document API",
"description": "여섯 가지 JSON Document 진입점과 JSON Patch, Pointer, JSONPath 공개 API를 정리합니다.",
"language": "ko",
"navigationGroup": "JSON Document"
Expand All @@ -41,6 +44,7 @@
"path": "/docs/collaboration",
"label": "Collaboration",
"title": "Collaboration - json-document",
"heading": "Collaboration",
"description": "같은 JSON Document 계약을 여러 참여자의 인과 변경으로 수렴시킵니다.",
"language": "ko",
"navigationGroup": "JSON Document"
Expand All @@ -49,6 +53,7 @@
"path": "/docs/collaboration/replica",
"label": "Replica",
"title": "Replica - json-document",
"heading": "Replica",
"description": "한 참여자가 소유한 인과 상태와 아직 적용하지 못한 변경을 설명합니다.",
"language": "ko",
"parentPath": "/docs/collaboration"
Expand All @@ -57,6 +62,7 @@
"path": "/docs/collaboration/history",
"label": "Collaborative History",
"title": "Collaborative History - json-document",
"heading": "Collaborative History",
"description": "다른 참여자를 덮어쓰지 않고 현재 참여자의 인과 기여를 끄거나 켭니다.",
"language": "ko",
"parentPath": "/docs/collaboration"
Expand All @@ -65,6 +71,7 @@
"path": "/docs/collaboration/text",
"label": "Text",
"title": "Collaborative Text - json-document",
"heading": "Collaborative Text",
"description": "같이 쓰는 문자열의 안정된 글자 단위와 상대 위치를 설명합니다.",
"language": "ko",
"parentPath": "/docs/collaboration"
Expand All @@ -73,6 +80,7 @@
"path": "/docs/collaboration/text/lease",
"label": "Contenteditable lease",
"title": "Contenteditable lease - json-document",
"heading": "Contenteditable lease",
"description": "협업 문자열이 입력 중에도 인과 변경을 멈추지 않게 하는 native-input DOM lease를 설명합니다.",
"language": "ko",
"parentPath": "/docs/collaboration/text"
Expand All @@ -81,6 +89,7 @@
"path": "/docs/collaboration/lifecycle",
"label": "Lifecycle",
"title": "Collaboration Lifecycle - json-document",
"heading": "Lifecycle",
"description": "epoch, checkpoint, restore, compaction이 협업 세대를 접고 되돌리는 방식을 설명합니다.",
"language": "ko",
"parentPath": "/docs/collaboration"
Expand All @@ -89,6 +98,7 @@
"path": "/docs/intent-guide",
"label": "Intent guide",
"title": "Intent guide - json-document",
"heading": "Editor와 Intent 만들기",
"description": "Document editor를 만들고 요청을 EditingIntent로 표현해 EditingResult를 처리합니다.",
"language": "ko",
"navigationGroup": "Editing"
Expand All @@ -97,6 +107,7 @@
"path": "/docs/intent",
"label": "Intent",
"title": "Intent reference - json-document",
"heading": "Intent 레퍼런스",
"description": "EditingIntent, dispatch, EditingResult와 editor별 Intent의 공개 시그니처를 정리합니다.",
"language": "ko",
"parentPath": "/docs/intent-guide"
Expand All @@ -105,9 +116,11 @@
"path": "/docs/selection",
"label": "Selection",
"title": "Selection - json-document",
"heading": "Selection",
"description": "문서 값을 바꾸지 않고 편집 대상을 기억하는 구조적 Selection을 설명합니다.",
"language": "ko",
"navigationGroup": "Editing"
"navigationGroup": "Editing",
"relatedDemoPath": "/demo/selection"
},
{
"path": "/demo/selection",
Expand All @@ -121,9 +134,11 @@
"path": "/docs/topology",
"label": "Topology",
"title": "Topology - json-document",
"heading": "Topology",
"description": "Selection과 Clipboard가 공유하는 화면 순서인 LineTopology와 GridTopology를 설명합니다.",
"language": "ko",
"navigationGroup": "Editing"
"navigationGroup": "Editing",
"relatedDemoPath": "/demo/topology"
},
{
"path": "/demo/topology",
Expand All @@ -137,9 +152,11 @@
"path": "/docs/clipboard",
"label": "Clipboard",
"title": "Clipboard - json-document",
"heading": "Clipboard",
"description": "현재 구조적 Selection에서 copy, cut, paste에 사용할 Clipboard payload를 만듭니다.",
"language": "ko",
"navigationGroup": "Editing"
"navigationGroup": "Editing",
"relatedDemoPath": "/demo/clipboard"
},
{
"path": "/demo/clipboard",
Expand All @@ -153,9 +170,11 @@
"path": "/docs/history",
"label": "History",
"title": "History - json-document",
"heading": "History",
"description": "document value와 구조적 Selection을 함께 복원하는 로컬 undo와 redo를 설명합니다.",
"language": "ko",
"navigationGroup": "Editing"
"navigationGroup": "Editing",
"relatedDemoPath": "/demo/history"
},
{
"path": "/demo/history",
Expand All @@ -177,6 +196,7 @@
"path": "/editors",
"label": "Editors",
"title": "Editors - json-document",
"heading": "Editors",
"description": "Document, Canvas, Sheet, Tree, Kanban 최소 완성본을 고릅니다.",
"language": "ko",
"navigationGroup": "Editors"
Expand Down Expand Up @@ -227,6 +247,7 @@
"path": "/docs/order",
"label": "Order",
"title": "Order - json-document",
"heading": "Order",
"description": "한 줄 목록을 범위 선택으로 집고 옮기는 Order editor를 설명합니다.",
"language": "ko",
"parentPath": "/"
Expand All @@ -242,6 +263,7 @@
"path": "/docs/object",
"label": "Object",
"title": "Object - json-document",
"heading": "Object",
"description": "안정된 객체 ID를 키 가족으로 집는 Object editor를 설명합니다.",
"language": "ko",
"parentPath": "/"
Expand All @@ -257,9 +279,11 @@
"path": "/docs/tree",
"label": "Tree",
"title": "Tree - json-document",
"heading": "Tree",
"description": "호스트가 만든 보이는 노드 줄에서 범위를 집는 Tree editor를 설명합니다.",
"language": "ko",
"parentPath": "/"
"parentPath": "/",
"relatedDemoPath": "/demo/tree"
},
{
"path": "/demos",
Expand All @@ -279,6 +303,7 @@
"path": "/docs/adapters",
"label": "Adapter guide",
"title": "Adapter Docs - json-document",
"heading": "json-document Adapters",
"description": "Keyboard, Clipboard, Contenteditable 공식 adapter 계약을 설명합니다.",
"language": "ko",
"navigationGroup": "Adapters"
Expand All @@ -288,21 +313,36 @@
"label": "Keyboard",
"title": "Keyboard Adapter Live Demo - json-document",
"description": "Official keyboard adapter: conventional chords become commands, then existing Intent, Clipboard, and History doors.",
"navigationGroup": "Adapters"
"navigationGroup": "Adapters",
"integration": {
"kind": "adapter",
"packageName": "@interactive-os/json-document-web",
"status": "available"
}
},
{
"path": "/adapters/clipboard",
"label": "Clipboard",
"title": "Clipboard Adapter Live Demo - json-document",
"description": "Official clipboard adapter: native ClipboardEvent copy, cut, and paste bind to public editing contracts.",
"navigationGroup": "Adapters"
"navigationGroup": "Adapters",
"integration": {
"kind": "adapter",
"packageName": "@interactive-os/json-document-web",
"status": "available"
}
},
{
"path": "/adapters/contenteditable",
"label": "Contenteditable",
"title": "Contenteditable Adapter Live Demo - json-document",
"description": "Official DOM adapter: a React contenteditable root leases native input and commits one JSON Document string pointer.",
"navigationGroup": "Adapters"
"navigationGroup": "Adapters",
"integration": {
"kind": "adapter",
"packageName": "@interactive-os/json-document-contenteditable",
"status": "available"
}
},
{
"path": "/connectors",
Expand All @@ -315,6 +355,7 @@
"path": "/docs/connectors",
"label": "Connector guide",
"title": "Connector Docs - json-document",
"heading": "json-document Connectors",
"description": "React, React Hook Form, Ajv, Zod, TanStack Table 연결 계약을 설명합니다.",
"language": "ko",
"navigationGroup": "Connectors"
Expand All @@ -324,32 +365,53 @@
"label": "React",
"title": "React Connector Live Demo - json-document",
"description": "Live React integration for JSON Document subscriptions and component-owned editing lifecycle.",
"navigationGroup": "Connectors"
"navigationGroup": "Connectors",
"integration": {
"kind": "connector",
"packageName": "@interactive-os/json-document-react",
"status": "available"
}
},
{
"path": "/connectors/react-hook-form",
"label": "React Hook Form",
"title": "React Hook Form Connector Live Demo - json-document",
"description": "Live React Hook Form drafts, canonical validation, atomic submit history, and undo/redo reset.",
"navigationGroup": "Connectors"
"navigationGroup": "Connectors",
"integration": {
"kind": "connector",
"packageName": "@interactive-os/json-document-react-hook-form",
"status": "available"
}
},
{
"path": "/connectors/ajv",
"label": "Ajv",
"title": "Ajv Connector Live Demo - json-document",
"description": "Live Ajv validation with JSON Pointer diagnostics and non-adoption of validator mutations.",
"navigationGroup": "Connectors"
"navigationGroup": "Connectors",
"integration": {
"kind": "connector",
"packageName": "@interactive-os/json-document-ajv",
"status": "available"
}
},
{
"path": "/connectors/zod",
"label": "Zod",
"title": "Zod Connector Live Demo - json-document",
"description": "A Zod object schema and record array become a Database admin table.",
"navigationGroup": "Connectors"
"navigationGroup": "Connectors",
"integration": {
"kind": "connector",
"packageName": "@interactive-os/json-document-zod",
"status": "available"
}
},
{
"path": "/connectors/zod/validate",
"label": "Validate",
"relatedDemoLabel": "Validate commits",
"title": "Zod Validate Live Demo - json-document",
"description": "Live Zod validation with JSON Pointer diagnostics and canonical JSON preservation.",
"parentPath": "/connectors/zod"
Expand All @@ -359,6 +421,11 @@
"label": "TanStack Table",
"title": "TanStack Table Connector Live Demo - json-document",
"description": "Live TanStack Table v8 integration for visible-order Sheet selection, clipboard, history, and canonical JSON editing.",
"navigationGroup": "Connectors"
"navigationGroup": "Connectors",
"integration": {
"kind": "connector",
"packageName": "@interactive-os/json-document-tanstack-table",
"status": "available"
}
}
]
10 changes: 10 additions & 0 deletions site/src/app/legacy-page-redirects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const legacyPageRedirects = {
contenteditableConnector: {
from: "/connectors/contenteditable",
to: "/adapters/contenteditable",
},
webConnector: {
from: "/connectors/web",
to: "/adapters",
},
} as const;
35 changes: 35 additions & 0 deletions site/src/app/page-descriptors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import siteRoutesJson from "../../site-routes.json";
export { legacyPageRedirects } from "./legacy-page-redirects";

export type SiteNavigationGroup = "JSON Document" | "Editing" | "Editors" | "Adapters" | "Connectors";
export type IntegrationKind = "adapter" | "connector";

export type SiteRoute = {
readonly path: string;
readonly label: string;
readonly title: string;
readonly heading?: string;
readonly description: string;
readonly language?: "en" | "ko";
readonly navigationGroup?: SiteNavigationGroup;
readonly parentPath?: string;
readonly relatedDemoPath?: string;
readonly relatedDemoLabel?: string;
readonly integration?: {
readonly kind: IntegrationKind;
readonly packageName: string;
readonly status: "available" | "planned";
};
};

export const pageDescriptors = siteRoutesJson as ReadonlyArray<SiteRoute>;

export function pageDescriptor(path: string): SiteRoute {
const descriptor = pageDescriptors.find((route) => route.path === path);
if (descriptor === undefined) throw new Error(`Unknown public page descriptor: ${path}`);
return descriptor;
}

export function integrationPageDescriptors(kind: IntegrationKind): ReadonlyArray<SiteRoute> {
return pageDescriptors.filter((route) => route.integration?.kind === kind);
}
Loading