diff --git a/apps/site/src/generated/repo-catalog.ts b/apps/site/src/generated/repo-catalog.ts index 040055bf..c7cfeb67 100644 --- a/apps/site/src/generated/repo-catalog.ts +++ b/apps/site/src/generated/repo-catalog.ts @@ -320,7 +320,7 @@ export const repoCatalog = { "version": "1.0.0", "description": "Headless JSON editing primitives guarded by Zod schemas.", "license": "MIT", - "summary": "json-document는 Zod schema가 있는 JSON 문서를 편집하기 위한 headless document\nengine입니다.", + "summary": "Zod schema로 보호되는 JSON state를 읽고, 바꾸고, 선택하고, 복사하고,\n붙여넣고, 되돌리기 위한 headless document layer입니다.", "guidance": null, "publicExports": [ "ClipboardCopyError", diff --git a/docs/generated/repo-catalog.json b/docs/generated/repo-catalog.json index 166352ac..b95e2a2f 100644 --- a/docs/generated/repo-catalog.json +++ b/docs/generated/repo-catalog.json @@ -319,7 +319,7 @@ "version": "1.0.0", "description": "Headless JSON editing primitives guarded by Zod schemas.", "license": "MIT", - "summary": "json-document는 Zod schema가 있는 JSON 문서를 편집하기 위한 headless document\nengine입니다.", + "summary": "Zod schema로 보호되는 JSON state를 읽고, 바꾸고, 선택하고, 복사하고,\n붙여넣고, 되돌리기 위한 headless document layer입니다.", "guidance": null, "publicExports": [ "ClipboardCopyError", diff --git a/packages/json-document/README.md b/packages/json-document/README.md index 76d11fc1..6a409d43 100644 --- a/packages/json-document/README.md +++ b/packages/json-document/README.md @@ -1,26 +1,35 @@ # json-document -json-document는 Zod schema가 있는 JSON 문서를 편집하기 위한 headless document -engine입니다. +Zod schema로 보호되는 JSON state를 읽고, 바꾸고, 선택하고, 복사하고, +붙여넣고, 되돌리기 위한 headless document layer입니다. -UI component, admin CRUD framework, app state manager가 아닙니다. json-document는 -편집 툴들이 공유하는 schema-safe document layer를 제공합니다: JSON Pointer -주소, JSON Patch 변경, JSONPath 검색, schema validation, selection, clipboard -payload, undo/redo history, reasoned `can*` checks. +`json-document`는 UI component library, CRUD framework, app state manager가 +아닙니다. Form, CMS block, kanban board, outliner, settings editor처럼 서로 +다른 제품이 반복해서 구현하는 document editing core를 제공합니다. -공식 사이트와 데모: https://developer-1px.github.io/json-document/ +```txt +schema -> document -> pointer/query -> can* -> change -> result +``` -## 왜 json-document인가 +- 공식 사이트: https://developer-1px.github.io/json-document/ +- GitHub Wiki: https://github.com/developer-1px/json-document/wiki -폼, CMS block, kanban board, outliner, settings editor는 UI가 달라도 같은 일을 합니다. schema가 있는 JSON document를 읽고, 바꾸고, 선택하고, 복사하고, 붙여넣고, 되돌립니다. +## 왜 json-document인가 -json-document는 이 공통 규칙을 UI component 밖으로 빼서 하나의 document engine으로 제공합니다. +JSON을 단순 data blob으로만 다루면 값 하나를 바꾸는 일은 쉽습니다. 하지만 실제 +제품에서는 곧 더 많은 질문이 생깁니다. -```txt -schema -> document -> pointer/query -> can* -> change -> result -``` +- 이 변경이 schema를 통과하는가? +- 실행 전에 버튼이나 command palette에서 가능 여부를 알 수 있는가? +- 실패하면 어떤 `code`, `pointer`, `violations`로 설명할 수 있는가? +- 선택된 항목을 삭제, 이동, 복제할 때 source는 어떻게 정해지는가? +- 여러 항목을 copy/cut/paste하면 array insertion target에서 어떻게 들어가는가? +- undo/redo는 value뿐 아니라 selection도 함께 복원하는가? +- React 밖에서도 같은 document behavior를 쓸 수 있는가? -처음 쓰는 사람은 내부 폴더를 몰라도 됩니다. 필요한 것은 schema, JSON value, Pointer, change, Result입니다. +`json-document`는 이 질문들을 UI 밖의 stable core로 옮깁니다. 앱은 rendering, +focus, keyboard, drag/drop, network sync를 계속 소유하고, `json-document`는 +schema-safe JSON document semantics를 소유합니다. ## 설치 @@ -28,9 +37,10 @@ schema -> document -> pointer/query -> can* -> change -> result npm install @interactive-os/json-document zod ``` -`zod`는 peer dependency입니다. React를 쓰는 앱만 `@interactive-os/json-document/react`를 import합니다. +`zod`는 peer dependency입니다. React 앱에서만 +`@interactive-os/json-document/react`를 import합니다. -## 시작 예제 +## 60초 시작 ```ts import { z } from "zod"; @@ -51,10 +61,24 @@ const doc = createJSONDocument(Card, { selection: true, }); -const patch = [{ op: "replace", path: "/status", value: "doing" }] as const; +const canChange = doc.canReplace("/status", "doing"); -if (doc.canPatch(patch).ok) { - doc.commit(patch, { label: "change status" }); +if (canChange.ok) { + doc.replace("/status", "doing"); +} +``` + +모든 실행 API는 성공 또는 실패 result를 반환합니다. UI는 같은 `can*` result를 +사용해 button disabled, command availability, validation message를 만들 수 +있습니다. + +```ts +const result = doc.canReplace("/title", ""); + +if (!result.ok) { + result.code; + result.reason; + result.violations; } ``` @@ -77,16 +101,110 @@ if (doc.canPatch(patch).ok) { | undo, redo | `doc.undo()`, `doc.redo()`, `doc.history` | | 위치별 schema 확인 | `doc.schema.at`, `doc.schema.kind`, `doc.schema.describe`, `doc.schema.accepts` | +## 1.0 public contract + +1.x에서 고정되는 것은 export 이름만이 아닙니다. 사용자 코드가 의존하는 result +shape, error code, signature/call shape, selection/history semantics도 public +contract입니다. + +- public import는 `@interactive-os/json-document`와 + `@interactive-os/json-document/react`입니다. +- 실패 분기는 `reason` 문구가 아니라 stable `code`와 구조로 합니다. +- 기본값은 `strict: false`입니다. 실패는 throw가 아니라 result로 돌아옵니다. +- `doc.undo()`와 `doc.redo()`는 top-level command이며 + `JSONCapabilityResult`를 반환합니다. +- `createJSONDocument`와 `useJSONDocument`는 trusted/untrusted initial overload를 + 유지합니다. +- `doc.insert(value)`, `doc.insert(target, value, options)`, + `doc.move(source, target)`, `doc.move(target)`, + `doc.paste(target, options)` call shape는 public API입니다. +- `commit(..., { selectionAfter })`는 patch와 final selection을 같은 history + entry에 기록합니다. + +상세 API와 method reference는 GitHub Wiki에 있습니다. + +- Core API Reference: https://github.com/developer-1px/json-document/wiki/Core-API-Reference +- JSONDocument Method Reference: https://github.com/developer-1px/json-document/wiki/JSONDocument-Method-Reference +- 1.0 Semantic Contract: https://github.com/developer-1px/json-document/wiki/1.0-Semantic-Contract + ## 핵심 규칙 -- Patch path와 source는 JSON Pointer입니다. `insert`와 `move` target은 exact Pointer 또는 `{ before }`, `{ after }`, `{ into }`를 받습니다. +- Patch path와 source는 JSON Pointer입니다. - JSONPath는 값을 찾는 언어이며 직접 변경하지 않습니다. - `doc.at(pointer)`는 raw value가 아니라 `ReadResult`를 반환합니다. - `can*`는 boolean이 아니라 이유 있는 capability result입니다. -- `doc.duplicate`, `doc.cut`, `doc.paste`는 성공하면 즉시 적용됩니다. 성공 결과의 `applied`는 다시 `commit`하지 않습니다. +- `doc.duplicate`, `doc.cut`, `doc.paste`는 성공하면 즉시 적용됩니다. +- 성공 결과의 `applied` patch는 이미 적용된 record이므로 다시 `commit`하지 않습니다. - Pointer 배열을 copy/cut하면 clipboard payload도 배열입니다. +- Multi-source clipboard payload를 array insertion target에 paste하면 기본적으로 + item별 sibling insert가 됩니다. - Tree semantics는 app-owned입니다. json-document는 JSON을 검증하고 mutate합니다. +## Selection, clipboard, history + +Selection은 DOM focus가 아니라 JSON-safe document state입니다. + +```ts +doc.selection?.selectRanges([ + "/lists/0/cards/0", + "/lists/0/cards/1", +]); + +doc.copy(doc.selection?.selectedPointers ?? []); +doc.paste("/lists/1/cards/-"); +``` + +Clipboard는 browser clipboard가 아니라 document instance 안의 headless JSON +payload buffer입니다. Browser system clipboard는 +`@interactive-os/json-document-clipboard-web` extension에서 조립합니다. + +History는 사용자 의도 단위로 묶을 수 있습니다. + +```ts +doc.history.transaction({ label: "bulk status change" }, () => { + doc.replace("/status", "doing"); + doc.replace("/title", "In progress"); +}); + +doc.undo(); +doc.redo(); +``` + +구조 편집 후 다음 selection을 command가 정확히 알고 있으면 `selectionAfter`를 +명시합니다. + +```ts +doc.commit([ + { op: "replace", path: "/status", value: "done" }, +], { + label: "complete card", + selectionAfter: "/status", +}); +``` + +## Placement targets + +삽입/이동 위치를 이미 알고 있으면 `/items/-`나 `/lists/1/cards/-` 같은 exact +Pointer를 그대로 넘깁니다. + +```ts +doc.insert("/items/-", item); +doc.move("/items/0", "/items/2"); +``` + +Array container 안에 append할 때는 `{ into }`, array item 기준으로 배치할 때는 +`{ before }`, `{ after }`를 사용합니다. 기존 값을 바꾸는 작업은 `replace`이고, +document clipboard paste만 `{ replace: pointer }`를 추가로 지원합니다. + +```ts +doc.insert({ into: "/items" }, item); +doc.move("/items/0", { after: "/items/2" }); +doc.paste({ replace: "/items/0" }); +``` + +삽입 위치를 직접 지정하려면 placement object를 만들지 말고 pointer 문자열을 +그대로 넘깁니다. + ## React — `useJSONDocument` ```tsx @@ -124,23 +242,12 @@ export function App() { } ``` -## 클립보드 - -Core clipboard는 `navigator.clipboard`가 아니라 headless JSON payload buffer입니다. Browser system clipboard는 `@interactive-os/json-document-clipboard-web` extension에서 조립합니다. - -```ts -const copied = doc.copy(["/lists/0/cards/0"]); - -if (copied.ok) { - doc.paste("/lists/1/cards/-"); -} -``` - -삽입/이동 위치를 이미 알고 있으면 `/items/-`나 `/lists/1/cards/-` 같은 exact Pointer를 그대로 넘깁니다. Array container 안에 append할 때는 `{ into: "/items" }`, array item 기준으로 배치할 때는 `{ before: "/items/0" }`, `{ after: "/items/0" }`를 사용합니다. Object member 추가는 `/record/key` 같은 exact Pointer를 씁니다. 기존 값을 바꿀 때는 `doc.replace(...)`를 쓰고, document clipboard paste만 `{ replace: pointer }`를 추가로 지원합니다. +Root package는 React-free입니다. React hook은 별도 entrypoint에만 있습니다. ## Official extensions -공식 extension은 core에 plugin 등록하지 않고 public `JSONDocument` surface를 함수로 조립합니다. +공식 extension은 core에 plugin 등록하지 않고 public `JSONDocument` surface를 +함수로 조립합니다. ```ts import { createCollection } from "@interactive-os/json-document-collection"; @@ -162,7 +269,10 @@ import { createComments } from "@interactive-os/json-document-comments"; import { createWebClipboard } from "@interactive-os/json-document-clipboard-web"; ``` -공식 package는 현재 `packages/*`에 있는 extension만 뜻합니다. `labs/extensions/*`는 후보이며 public API로 약속하지 않습니다. +공식 package는 현재 `packages/*`에 있는 extension만 뜻합니다. +`labs/extensions/*`는 후보이며 public API로 약속하지 않습니다. + +Extension guide: https://github.com/developer-1px/json-document/wiki/Labs-and-Extensions ## 순수 core @@ -204,3 +314,11 @@ const body = JSON.stringify(operations); body satisfies string; ``` + +## 문서 + +- Overview: https://developer-1px.github.io/json-document/docs +- Tutorial: https://developer-1px.github.io/json-document/docs/tutorial +- API reference: https://developer-1px.github.io/json-document/docs/api +- Recipes: https://developer-1px.github.io/json-document/docs/recipes +- Wiki: https://github.com/developer-1px/json-document/wiki