From 819ab9b4bb0e118135daf1a809173bae3be556de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Tue, 8 Sep 2026 07:57:57 +0900 Subject: [PATCH 01/20] =?UTF-8?q?docs:=20=ED=8E=B8=EC=A7=91=20=EC=82=AC?= =?UTF-8?q?=EB=A1=80=EC=97=90=EC=84=9C=20=EC=B5=9C=EC=86=8C=20=EB=AC=B8?= =?UTF-8?q?=EB=B2=95=EA=B3=BC=20=EC=A1=B0=ED=95=A9=20=ED=95=98=EC=9D=B4?= =?UTF-8?q?=EB=9D=BC=ED=82=A4=EB=A5=BC=20=EB=8F=84=EC=B6=9C=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- standards/editing-grammar.md | 257 +++++++++++++++++++++++++++++++---- 1 file changed, 228 insertions(+), 29 deletions(-) diff --git a/standards/editing-grammar.md b/standards/editing-grammar.md index 1cdc03b3..84e71cf3 100644 --- a/standards/editing-grammar.md +++ b/standards/editing-grammar.md @@ -18,10 +18,11 @@ member와 편집 문법의 크기는 서로 다른 문제다. - Don't: 이번 설계로 Core API를 늘리거나 기존 동작을 변경하지 않는다. 새로운 범용 editor, command bus, profile registry나 package hierarchy를 만들지 않는다. -구현 근거의 범위는 JSON Document, Selection, Editing, Affordance, Web과 -Document·Sheet·Rich Text의 대표 편집 경로다. Object의 전체 선택과 Database의 -cut 미지원은 profile 차이를 판정하는 사례로 포함한다. 모든 Hands의 완성도, -브라우저별 입력 일치, 협업 wire protocol의 수렴은 이 설계의 검증 결과가 아니다. +아래 도출은 11개 내부 편집 사례의 model·의도·조합과 외부 관습을 비교한다. +실행된 공통 적합성 증거의 범위는 Document·Sheet·Rich Text의 대표 편집 경로와 +Selection·Editing·Affordance·Web 연결이다. 사례를 읽은 것과 같은 profile의 +독립 구현을 검증한 것은 구별한다. 모든 Hands의 완성도, 브라우저별 입력 일치, +협업 wire protocol의 수렴은 이 설계의 검증 결과가 아니다. ## 현재 문제와 원인 @@ -30,39 +31,207 @@ cut 미지원은 profile 차이를 판정하는 사례로 포함한다. 모든 H `dispatch` 형태이며, 동사의 사전 조건과 결과는 editor별 코드와 문서에 있다. `Official Hands`도 아직 호환성 약속을 확정하지 않은 후보다. -이 때문에 여러 editor가 같은 API 형태를 제공해도 같은 편집 문법을 따른다는 -증거가 되지 않는다. `selectAllAffordance`의 전체 선택 토글처럼 특정 관습이 -범용 기본값으로 보이고, `selection.move`처럼 문서 변경과 선택 변경이 이름에서 -구별되지 않는 사례도 있다. 동작을 선언하는 규칙과 그 규칙을 검증하는 사례의 -연결이 부족한 것이 원인이다. +현재 공용 적합성 runner는 대표 editor의 공통 의미를 검증한다. 그 증거를 다른 +장르로 확대하려면 규칙의 적용 조건과 profile 차이가 먼저 드러나야 한다. +`selectAllAffordance`의 전체 선택 토글과 선택 의도의 멱등성, +`selection.move`라는 이름 아래의 블록 이동과 선택 위치 이동도 구별해야 한다. +아직 남은 공백은 앱 사례에서 공통 의미를 도출하는 근거, 중첩된 편집 맥락의 +계약, 같은 profile의 독립 구현 증거다. -## 세 종류의 계약과 기존 소유자 +## 앱 사례에서 최소 문법 도출하기 -다음은 새 runtime 계층이 아니라 기존 책임들이 약속할 내용의 구분이다. +### 명령 이름보다 대상과 결과를 먼저 비교한다 -| 계약 | 고정할 의미 | 정본 소유자 | +Apple의 [1992년 Macintosh Human Interface Guidelines](https://www.vintageapple.org/inside_r/pdf/Human_Interface_Guidelines_1992.pdf) +인쇄 쪽수 286–300은 선택 후 작업하는 관습과 텍스트·배열·그래픽 선택을 구분하고, +필드 전체 선택과 내부 텍스트 편집의 차이도 설명한다. Windows의 +[표준 Edit 메뉴](https://learn.microsoft.com/en-us/windows/win32/uxguide/cmd-menus#standard-menus)에도 +같은 명령 어휘가 반복된다. 전자는 역사적 제품 지침, 후자는 Windows 7 시대의 +플랫폼 지침이다. 오래된 공통 어휘의 근거이며 모든 동작이 30년간 동일했다는 +증명은 아니다. 사람의 편집 관습에서 도출하는 것은 관찰 가능한 의미다. Core의 +여섯 member, JSON Patch 표현, package 구조가 그 관습의 유일한 구현이라는 +결론은 나오지 않는다. + +다음 표는 현재 구현의 표본이다. 링크는 각 책임의 구현으로 연결된다. 마지막 +열의 중첩 가능성이 모두 구현·검증됐다는 뜻은 아니며 현재 지원과 분리해 읽는다. + +| 사례 | 대상·위치·선택 | 현재 대표 작업 | 도출에 필요한 차이 또는 중첩 | +| --- | --- | --- | --- | +| [Document](../packages/json-document-editing/src/document.ts) | block ID + text offset, 여러 범위 | 블록 이동·삭제·복사, 블록 text 교체 | Copy는 전체 블록. native 내부 문자열 선택과 대상이 다름 | +| [Rich Text](../packages/json-document-rich-text/src/editor.ts) | node ID + text/child point + affinity, 여러 범위 | text 삽입, structured slice 대체, mark 변경 | collapsed point에서도 삽입 가능. schema가 허용하는 구간·구조를 함께 판단 | +| [Order](../packages/json-document-editing/src/order.ts) | item ID, 선형 순서의 범위 | 이름 변경, 항목 제거·복사·삽입 | 항목 선택과 이름 입력 중 문자열 선택을 구별 | +| [Tree](../packages/json-document-editing/src/tree.ts) | node ID + parent 관계, visible topology의 범위 | 노드 선택·제거·복사·삽입 | Copy·제거는 선택한 노드의 후손까지 포함. 보이는 선택과 실제 영향 집합이 다름 | +| [Sheet](../packages/json-document-editing/src/sheet.ts) | row/column ID, 셀 직사각형과 primary | 셀 commit, fill, Cut·Paste | Cut은 primary 값을 비움. 셀 구조를 제거하거나 내부 문자열을 잘라내는 작업과 다름 | +| [Database](../packages/json-document-editing/src/database.ts) | record/property ID, view가 제공하는 표 topology | cell commit, record 추가·삭제, view configure, Paste | 셀 값과 record 구조, 저장되는 view 설정을 구별. 현재 Cut 부재는 보편 규칙이 아님 | +| [Object](../packages/json-document-editing/src/object.ts) | object ID 집합, x/y/width/height | translate, resize, fill, Copy·Paste | 선택 위치의 이동과 객체 좌표 변경은 다름. 현재 객체 model이 완전한 중첩 text editor는 아님 | +| [Kanban](../packages/json-document-editing/src/kanban.ts) | card ID 집합, column ID + beforeCardId | 카드 이동·제거 | 이동은 소속과 순서를 바꿈. 보드 모양만으로 새 공통 선택 family를 요구하지 않음 | +| [Calendar](../packages/json-document-editing/src/calendar.ts) | event ID + occurrenceStart, 구체화된 occurrence 범위 | 생성·이동·resize, Copy·Paste | 선택한 occurrence와 반복 일정 변경 범위는 다름. 시간·반복 규칙은 domain 의미 | +| [Annotation](../packages/json-document-editing/src/annotation.ts) | annotation ID 집합, source ID + selector | 생성·본문 변경·이동·resize·삭제 | 편집되는 annotation과 가리키는 source 영역을 구별. 원본 이미지 픽셀 편집 사례가 아님 | +| [Composer](../packages/json-document-composer/src/commands.ts) | Rich Text point, reference atom, attachment ID | text·reference 삽입, attachment·model 변경 | 여러 편집 대상을 한 draft에서 조합. 제출·외부 실행의 효과는 별도 Application 계약 | + +11개 사례는 같은 레포의 구현이며 다수가 같은 EditingSession을 쓴다. 그 반복은 +내부 재사용 가능성의 증거다. 서로 다른 생태계가 독립적으로 같은 의미를 채택했다는 +증거로 11번 세지 않는다. Raster·파일 이동은 아래 외부 반례로만 사용한다. + +### 하나의 편집을 설명하는 최소 질문 + +의미를 설명하는 단위는 **작업을 받을 편집 맥락 + 대상·위치 + 의도 → 결과**다. +API 인자에 모두 넣으라는 뜻은 아니다. 상태나 명시 인자로 이미 정해지는 것은 +그 계약에서 읽으면 된다. 한 범용 state 객체나 command envelope를 추가하지 않는다. + +```text +편집의 의미 +├─ 대상: 무엇의 내용·속성·구조를 다루는가 +│ ├─ identity: 같은 대상을 어떻게 알아보는가 +│ └─ position / topology: 어디이며 어떤 순서·관계로 해석하는가 +├─ 작업 맥락: 어느 편집기가 무엇을 대상으로 받는가 +│ ├─ selection: 대상 집합·범위·영역 또는 삽입점 +│ ├─ 입력 focus: 현재 입력을 받는 곳 +│ └─ 중첩: 객체 / 셀 / 그 안의 텍스트 사이의 진입·복귀 +├─ 의도: 탐색·선택 / 내용·구조 변경 / 교환 / 복원 +└─ 결과와 작업 경계 + ├─ 진행 중인 draft·preview / 확정 / 취소·거절 + ├─ 변경된 내용과 후속 선택·위치, 교환 payload + └─ 관찰 시점과 Undo/Redo의 대상·단위 +``` + +이 트리는 질문의 분류다. 상속 관계나 새 runtime 모듈 목록이 아니다. 다음 구별을 +없애면 실제 사례의 결과를 설명할 수 없다. + +| 구별 | 없애면 설명할 수 없는 사례 | 남길 계약 / 기존 연결 | | --- | --- | --- | -| 공통 편집 규칙 | 선택 전이, 편집의 원자성, 복사와 History의 관계 | Selection·Editing | -| Hands profile | 편집 대상, 범위 해석, 삭제·붙여넣기 결과, 지원 작업 | Document Type과 해당 editor의 기존 owner를 조합한 Hands | -| 입력 매핑 | 키·포인터·IME를 어떤 편집 의도로 해석하는가 | Adapter·Affordance와 framework Connector | +| Identity와 position | 같은 블록의 offset 변경, 이동한 같은 카드, 같은 event의 여러 occurrence | Point 단위·동등성·mapping은 해당 editor와 Selection. EG-TARGET | +| Selection과 실제 작업 대상 | `annotation.create`의 새 대상, `record.delete(recordId)`의 명시 대상, Tree의 선택된 부모와 숨은 후손 | 대상은 selection·명시 인자·삽입 위치와 의도로 정한다. 모든 작업에 non-empty selection을 강제하거나 효과를 보이는 선택에만 제한하지 않음 | +| 입력 focus와 range의 `focus` | 표 셀 입력 중 문자열의 역방향 선택 | 전자는 입력을 받는 곳, 후자는 범위의 한 endpoint. 이름이 같아도 같은 상태가 아님. EG-SELECT·EG-TARGET | +| 저장 내용과 보이는 topology | 접힌 Tree, 정렬된 Database, 저장되는 `view.configure` | 가시 순서를 읽는 것과 view 설정을 수정하는 것은 다른 작업. EG-TARGET·EG-EDIT | +| 요청·진행 중 작업과 확정된 편집 | 여러 pointer preview, IME composition, 여러 입력을 묶는 History | 사건 하나·dispatch 하나·commit 하나·Undo 한 step은 일대일이 아님. EG-GESTURE·EG-HISTORY·DOM lifecycle | +| 작업 결과와 이후 현재 상태 | 구독 중 재진입, 외부 변경 이후 Undo | 결과의 관찰 시점과 선택한 History owner의 복원 의미를 보존. EG-RESULT·EG-HISTORY | + +문자마다 stable ID가 있어야 한다거나 모든 위치를 숫자 하나로 표현해야 한다는 +결론은 나오지 않는다. [Rich Text point](../packages/json-document-rich-text/src/model.ts)의 +text/child 구별·affinity와 [Selection family](../packages/json-document-selection/src/core/family.ts)의 +transition·map·reconcile·targets가 이미 이 차이의 정본 경계를 제공한다. + +### 중첩은 입력을 받을 곳의 계약이다 + +[W3C APG Grid](https://www.w3.org/WAI/ARIA/apg/patterns/grid/#editing-and-navigating-inside-a-cell)는 +셀 탐색에 쓰는 화살표와 셀 안의 caret·widget 조작을 구분한다. +[Keynote의 텍스트 상자](https://support.apple.com/en-ca/guide/keynote/tan4fd6ee725/mac)는 +객체 자체와 그 안의 텍스트를 따로 선택한다. 두 자료는 지침·제품 관습의 근거이며 +범용 editing scope API를 규정하지 않는다. ```text -keyboard / pointer / IME ─ Adapter·Affordance ─┐ -직접 호출하는 프로그램 ──────────────────────┤ - ↓ - 해당 editor의 편집 의도 - + Selection / Topology - ↓ - EditingPlan + selectionAfter - ↓ - EditingSession → JSONDocument.commit - ↓ - 해당 편집의 snapshot / History +Application +└─ 표를 다루는 Hand + ├─ 셀 선택: Copy → 셀 payload, Delete 입력 → 선택한 셀 정책 + └─ 셀 내부 편집 + └─ 텍스트 선택: Copy → 문자열, Delete 입력 → 문자 편집 + +Canvas Hand +└─ 텍스트 객체 선택 + └─ 내부 텍스트 편집 ``` -이는 대표적인 값 변경 흐름이다. Copy는 조회이며, 선택만 바꾸는 작업은 document -commit을 만들지 않고, Undo/Redo는 선택한 History owner를 실행한다. 순수 JSON -소비자에게 Editing이나 Hands 설치를 요구하지 않는다. +중첩을 조합하는 profile은 진입·복귀, 입력 수신자, 지원하지 않거나 거절한 의도의 +전달 여부, 후속 선택을 결정해야 한다. 내부 편집기가 삭제를 거절했다는 이유만으로 +바깥 객체 삭제가 실행되는 해석을 허용할지 명시해야 한다. 이 설계의 후보는 +**해당 맥락이 맡은 편집의 거절을 바깥의 다른 편집으로 암묵 변환하지 않는 것**이다. +처리하지 않은 입력의 명시적 위임과 실패를 구별하며, 정확한 키·진입 방법은 입력 +profile이 정한다. 아직 전체 Hands에 실행 검증된 새 규칙은 아니다. + +여기서 편집 맥락은 HTML `editing host`, DOM element, JSON subtree, History +instance와 일대일 대응하지 않는다. Toolbar로 focus가 옮겨져도 편집 명령은 저장된 +선택을 대상으로 할 수 있다. Headless 호출은 DOM focus 없이 대상을 지정할 수 있다. +Calendar intent의 `scope: "this" | "this-and-following" | "all"`은 반복 일정의 +**변경 범위**이며 이 입력 맥락과 다른 개념이다. + +현재 [DocumentTextControl](../packages/json-document-react/src/use-document-text-control.ts)은 +native range의 방향을 투영하고, [Web virtual selection scope](../packages/json-document-web/src/virtual-selection-scope.ts)는 +자신의 영역에서 전체 텍스트 선택·Copy와 editable target 우선 처리를 맡는다. +이는 기존 소유자의 증거이며 둘 중 하나를 모든 편집 맥락의 정본이라고 선언할 +근거는 아니다. 범용 scope registry나 Host별 dispatcher를 새로 만들지 않는다. + +### 반례가 정하는 공통 규칙의 경계 + +| 과도한 일반화 | 근거 있는 반례 | 이 설계의 처리 | +| --- | --- | --- | +| 선택은 언제나 Undo 대상이 아님 | [GIMP Undoing](https://docs.gimp.org/3.0/en/gimp-concepts-undo.html)은 Scissors Select 작업과 Quick Mask 전환의 Undo를 설명 | 현재 일시적 Selection의 EG-SELECT·EG-HISTORY를 유지. Raster 선택 제작의 History 정책으로 일반화하지 않음 | +| 이동은 항상 Copy 후 즉시 제거한 뒤 Paste | [Finder 이동](https://support.apple.com/en-gb/102650)은 Clipboard의 파일을 목적지에 옮기는 동작을 제공 | EG-CUT은 현재 즉시 제거하는 cut 계약. 지연 이동은 같은 계약의 이름만 다른 구현이 아님 | +| 복사한 저장 값을 그대로 넣으면 의미도 보존 | [Excel 수식 이동·복사](https://support.microsoft.com/en-us/excel/move-or-copy-a-formula-in-excel)는 복사 시 상대 참조 변경, 수식 이동 시 참조 유지를 구별 | payload의 참조·identity·배치 변환은 profile 계약. JSON 모양이나 text 일치만으로 교환 호환성을 판정하지 않음 | +| Selection은 항상 문자열의 시작과 끝 | 레포의 Sheet rectangle, Calendar materialized occurrences와 Selection mask 확장 | 대상별 family를 유지. mask algebra의 존재는 raster Hand 완성의 증거가 아님 | +| 입력이 끝나면 Undo 한 step | 여러 preview와 typing/composition grouping, GIMP의 선택 도구 작업 | 작업의 확정·취소와 History grouping을 따로 명시 | + +공통 의미로 남는 것은 대상과 위치의 구별, 같은 의도의 명시된 결과, 효과의 +관찰·실패 경계다. 구체적인 선택 모양, 복사할 내용, 붙여넣기 위치, 영향을 받는 +참조, 기록할 상태와 작업 묶음은 profile이 채운다. Copy의 source 내용 보존과 +clipboard 자체의 변경도 별개의 효과다. 원자적 문서 편집은 이 레포가 제공하는 +강한 계약이며 모든 제품이 이미 같은 실패 보장을 구현했다는 역사적 주장은 아니다. + +## 하이라키: 구체화와 조합 + +고정할 공통 의미와 profile이 결정할 내용을 먼저 나눈다. 그 다음 기존 모듈이 +어떤 부분을 실현하는지 연결한다. 앱 이름과 package 폴더 순서에서 의미의 상속 +관계를 역으로 추정하지 않는다. + +```text +편집 계약 +├─ 공통 규칙: 대상·선택·효과·관찰·실패의 의미 +├─ 대상별 구체화 +│ └─ model / 유효한 position / selection·topology / 연산·교환·복원 정책 +└─ 입력 관습별 구체화 + └─ key·pointer·IME / focus·중첩 / gesture·native lifecycle + +대상별 계약 + 선택한 입력 계약 + │ 조합하고 함께 검증 + v + Hands profile + │ 여러 Hand와 외부 서비스를 조합 + v + Application / Host +``` + +대상별 계약과 입력 계약은 서로 다른 축이다. Sheet가 Rich Text를 상속하는 것이 +아니며 Windows·macOS 관습 때문에 문서 model을 복제할 이유도 없다. Hands profile은 +함께 약속하는 조합이다. 위의 구분을 새 package나 public descriptor type으로 +등록하지 않는다. 실제 문서·API·Usage는 아래의 기존 정본 owner에 둔다. + +의미를 실현하는 책임 지도는 다음과 같다. 화살표는 이 설계의 책임 연결이며 +현재 package import graph의 모든 edge를 표현하지 않는다. + +```text +Headless API Keyboard / Pointer / IME + | | + | Adapter / Affordance + +-------------------+---------------+ + v + Domain editor + / \ + Selection·Topology EditingPlan + selectionAfter + \ / + v v + EditingSession + 관찰·작업·History 연결 + | + v + JSONDocument + 값·원자적 commit + local / collaboration 구현 + +Connector: 이 책임들을 framework의 구독·render·focus lifecycle에 연결 +Host: 정본 API의 조합 순서·제품 정책·데이터·layout·외부 instance 주입 +``` + +Selection의 수학적 전이는 JSONDocument mutation 없이 사용할 수 있고, 순수 JSON +소비자는 Editing 없이 Core를 사용할 수 있다. Collaboration은 같은 JSONDocument +계약의 다른 구현이며 `EditingHistory` 연결은 별도 선택이다. 입력 맥락 하나마다 +History를 하나씩 생성하거나 Application 전체에 단일 History를 강제하지 않는다. +같은 문서를 여러 view에서 편집할 때의 History 귀속은 선택한 owner의 계약이다. + +### 기존 소유자와 연결 + +Copy는 조회이며, 선택만 바꾸는 작업은 document commit을 만들지 않고, +Undo/Redo는 선택한 History owner를 실행한다. 위의 흐름을 모든 요청이 +Core commit으로 향하는 단일 경로로 해석하지 않는다. | 기존 owner | 계속 소유할 책임 | 다른 곳으로 넘기지 않을 결정 | | --- | --- | --- | @@ -85,6 +254,13 @@ layout·concrete external instance와 조합을 소유한다. 증거를 통해 이루어진다. DOM API 모양, JSON 저장 shape, 특정 키 조합은 이 규칙의 전제가 아니다. +이 표의 적용 범위는 현재 구조·텍스트 편집 profile의 계약이다. EG-SELECT의 +일시적 선택과 문서 History 분리, EG-CUT의 즉시 제거를 다른 장르의 모든 관습으로 +일반화하지 않는다. 범위 확장의 anchor 규칙은 anchor를 가진 range family에 +적용한다. 다른 family나 raster·지연 이동 profile을 동결하려면 해당 상태와 +History·transfer 의미를 먼저 명시해야 하며, 기존 profile의 규칙을 느슨하게 +바꾸어 같은 profile이라고 부르지 않는다. + | ID | 관찰 가능한 규칙 | Owner | | --- | --- | --- | | EG-SELECT | 선택만 바꾸는 작업은 document value와 문서 Undo/Redo 기록을 바꾸지 않는다. 범위 확장은 유효한 기존 anchor를 보존하고 focus를 이동한다. | Selection·Editing | @@ -266,6 +442,29 @@ offset도 비교하며 블록 toggle과 전체 블록 Copy의 의미는 유지 유지하는 증거다. offset 하나로 과거 native range 전체를 복원한다는 계약이나 profile 동결로 확대하지 않는다. +## 새 도출의 검증 질문 + +다음은 실행할 수 있도록 시작 상태와 판정 결과를 적은 **후보 사례**다. 앞의 +기존 적합성 표의 통과 항목과 합산하지 않는다. 이 문서 변경은 새 behavior vector나 +구현을 추가하지 않는다. + +| 사례 | 시작 상태 → 작업 → 판정할 결과 | 연결·현재 증거의 한계 | +| --- | --- | --- | +| 셀과 내부 문자열 | `Alpha` 셀 선택 / 내부 `ph` 선택 각각에서 Copy → 셀 payload / `ph` | EG-TARGET·EG-COPY. 기존 Sheet·native selection 증거는 각각 있으며 이 중첩 전체의 공통 binding은 없음 | +| 거절 시 바깥으로 전이 금지 | 선택한 객체 안의 text editor가 Delete 거절 → 객체 제거·외부 History entry가 생기지 않음 | EG-EDIT·입력 계약 후보. [Web clipboard ownership](../packages/json-document-web/tests/clipboard-rejection.test.ts)은 한 binding의 거절 처리 증거이며 일반적인 중첩 거절 vector는 없음 | +| Toolbar 대상 보존 | editor A에서 범위 선택 → Toolbar에 focus → Copy 실행 → A의 선택 payload | 입력 focus와 Selection 구별. 전체 Hands에 대한 공통 실행 증거 없음 | +| 셀 값과 행 구조 | 값이 있는 셀을 비움 / 해당 record 삭제 → 전자는 구조 유지, 후자는 record 제거 | EG-EDIT·profile 대상. 현재 Sheet Cut과 Database `record.delete`는 서로 다른 계약 | +| 반복 일정 영향 범위 | 같은 occurrence에 `this` / `all` 이동 → profile이 정한 반복 일정 집합만 영향 | EG-TARGET·EG-EDIT. Calendar domain 사례이며 입력 맥락 전환으로 해석하지 않음 | +| Draft 취소와 Undo | 미확정 rename draft 수정 → cancel → 확정 label 불변; 확정 rename → Undo → 이전 label | EG-GESTURE·EG-HISTORY. 구조 gesture runner가 모든 rename·IME 취소를 증명하지 않음 | +| 외부 참조·다른 profile로 Paste | 수식 또는 reference를 다른 위치·profile에 Paste → 변환·보존·거절 중 선언한 결과 | EG-PASTE. 현재 세 editor의 내부 round trip만으로 cross-profile 교환을 인증할 수 없음 | +| 공유 문서의 두 편집 맥락 | A에서 편집 → B로 입력 focus 전환 → Undo → 선택한 History owner가 명시한 기여·선택 복원 | EG-HISTORY·EG-RESULT. 외부 History 연결 증거와 모든 Hand의 입력 routing 증거는 다름 | + +이번 도출로 설계 방향을 정할 수 있는 부분은 대상·위치·선택·입력 수신자의 구별, +대상별 계약과 입력 계약의 조합, 기존 owner의 유지다. 아직 동결할 수 없는 부분은 +중첩 편집의 진입·거절·복귀 규칙, profile 간 payload 변환, raster/file 편집의 +적용 범위, 여러 편집 맥락의 History routing이다. 같은 profile을 독립적으로 +구현했을 때 이 사례들의 결과가 일치해야 장기 호환성 약속으로 승격할 수 있다. + ## 장기 호환성 약속하는 것은 **고정된 profile의 지원 입력과 관찰 가능한 결과**다. 같은 profile From c8570dc76bbc6ea03ddb23c53ffc21fa4103e74f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Tue, 8 Sep 2026 08:01:14 +0900 Subject: [PATCH 02/20] =?UTF-8?q?docs:=20=EC=97=AD=EC=82=AC=EC=A0=81=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=ED=99=95=EC=9E=A5=EC=9D=98=20=EC=B0=A8?= =?UTF-8?q?=EC=9D=B4=EC=99=80=20Host=20=EC=A1=B0=ED=95=A9=20=EC=97=AD?= =?UTF-8?q?=ED=95=A0=EC=9D=84=20=EB=AA=85=ED=99=95=ED=9E=88=20=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- standards/editing-grammar.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/standards/editing-grammar.md b/standards/editing-grammar.md index 84e71cf3..6d255542 100644 --- a/standards/editing-grammar.md +++ b/standards/editing-grammar.md @@ -158,6 +158,7 @@ native range의 방향을 투영하고, [Web virtual selection scope](../package | 선택은 언제나 Undo 대상이 아님 | [GIMP Undoing](https://docs.gimp.org/3.0/en/gimp-concepts-undo.html)은 Scissors Select 작업과 Quick Mask 전환의 Undo를 설명 | 현재 일시적 Selection의 EG-SELECT·EG-HISTORY를 유지. Raster 선택 제작의 History 정책으로 일반화하지 않음 | | 이동은 항상 Copy 후 즉시 제거한 뒤 Paste | [Finder 이동](https://support.apple.com/en-gb/102650)은 Clipboard의 파일을 목적지에 옮기는 동작을 제공 | EG-CUT은 현재 즉시 제거하는 cut 계약. 지연 이동은 같은 계약의 이름만 다른 구현이 아님 | | 복사한 저장 값을 그대로 넣으면 의미도 보존 | [Excel 수식 이동·복사](https://support.microsoft.com/en-us/excel/move-or-copy-a-formula-in-excel)는 복사 시 상대 참조 변경, 수식 이동 시 참조 유지를 구별 | payload의 참조·identity·배치 변환은 profile 계약. JSON 모양이나 text 일치만으로 교환 호환성을 판정하지 않음 | +| Shift-click 확장은 항상 같은 anchor를 고정 | Apple HIG 1992 인쇄 쪽수 290–291은 addition과 fixed-point 두 방식을 설명 | 현재 EG-SELECT의 고정 anchor 전이를 유지. 다른 입력 관습을 같은 전이로 일반화하지 않음 | | Selection은 항상 문자열의 시작과 끝 | 레포의 Sheet rectangle, Calendar materialized occurrences와 Selection mask 확장 | 대상별 family를 유지. mask algebra의 존재는 raster Hand 완성의 증거가 아님 | | 입력이 끝나면 Undo 한 step | 여러 preview와 typing/composition grouping, GIMP의 선택 도구 작업 | 작업의 확정·취소와 History grouping을 따로 명시 | @@ -185,9 +186,9 @@ clipboard 자체의 변경도 별개의 효과다. 원자적 문서 편집은 │ 조합하고 함께 검증 v Hands profile - │ 여러 Hand와 외부 서비스를 조합 + │ Host가 여러 Hand와 외부 서비스를 조합 v - Application / Host + Application ``` 대상별 계약과 입력 계약은 서로 다른 축이다. Sheet가 Rich Text를 상속하는 것이 @@ -256,8 +257,8 @@ layout·concrete external instance와 조합을 소유한다. 이 표의 적용 범위는 현재 구조·텍스트 편집 profile의 계약이다. EG-SELECT의 일시적 선택과 문서 History 분리, EG-CUT의 즉시 제거를 다른 장르의 모든 관습으로 -일반화하지 않는다. 범위 확장의 anchor 규칙은 anchor를 가진 range family에 -적용한다. 다른 family나 raster·지연 이동 profile을 동결하려면 해당 상태와 +일반화하지 않는다. 범위 확장의 anchor 규칙은 고정 anchor로 확장하는 현재 +range 전이에 적용한다. 다른 family나 raster·지연 이동 profile을 동결하려면 해당 상태와 History·transfer 의미를 먼저 명시해야 하며, 기존 profile의 규칙을 느슨하게 바꾸어 같은 profile이라고 부르지 않는다. From 412f5121547927027c001d7911fee07e9939cc48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Tue, 8 Sep 2026 09:00:03 +0900 Subject: [PATCH 03/20] =?UTF-8?q?docs:=20=EA=B8=B0=EB=B3=B8=20Profile?= =?UTF-8?q?=EC=9D=98=20=ED=8E=B8=EC=A7=91=20=EC=95=BD=EC=86=8D=EA=B3=BC=20?= =?UTF-8?q?=EB=8B=A8=EC=B6=95=ED=82=A4=20=EA=B4=80=EC=8A=B5=EC=9D=84=20?= =?UTF-8?q?=EA=B5=AC=EC=B2=B4=ED=99=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- standards/editing-grammar.md | 173 +++++++++++++++++++++++++++++++++-- 1 file changed, 164 insertions(+), 9 deletions(-) diff --git a/standards/editing-grammar.md b/standards/editing-grammar.md index 6d255542..9caf48c7 100644 --- a/standards/editing-grammar.md +++ b/standards/editing-grammar.md @@ -11,6 +11,10 @@ Stable admission이나 외부 상호운용성을 주장하지 않는다. 작은 Core는 장기간 약속할 수 있는 의미만 소유한다. JSON Document의 여섯 member와 편집 문법의 크기는 서로 다른 문제다. +기반 계약은 여러 편집 정책을 수용하고, **기본 Profile은 사람들이 익숙한 편집 +동작을 구체적인 약속으로 제공한다**. 예외의 존재는 기본값의 적용 범위와 변형을 +설명하는 근거다. 기본값 자체도 장기간 호환성을 지킬 계약에 포함한다. + - Outcome: 같은 Hands profile을 소비하는 제품은 선택·편집·복사·복원에서 같은 관찰 가능한 의미를 얻는다. - Done: 이 설계에서 공통 규칙과 profile 선택을 구분하고, 각 규칙의 기존 owner, @@ -36,10 +40,40 @@ Selection·Editing·Affordance·Web 연결이다. 사례를 읽은 것과 같은 `selectAllAffordance`의 전체 선택 토글과 선택 의도의 멱등성, `selection.move`라는 이름 아래의 블록 이동과 선택 위치 이동도 구별해야 한다. 아직 남은 공백은 앱 사례에서 공통 의미를 도출하는 근거, 중첩된 편집 맥락의 -계약, 같은 profile의 독립 구현 증거다. +계약, 같은 profile의 독립 구현 증거다. 각 owner에 있는 기본 동작을 함께 쓰는 +Profile의 약속과, 그 Profile이 허용할 차이도 연결해야 한다. ## 앱 사례에서 최소 문법 도출하기 +### 단축키가 드러내는 관습부터 찾는다 + +핵심 기능을 찾는 출발점은 **단축키로 반복 실행하도록 드러낸 작업**이다. +사용자가 근육기억으로 익힌 조작을 장기 호환성의 대상으로 보고, 여러 제품과 +오랜 자료에 반복되는 기능부터 조사한다. 단축키의 존재는 조사 우선순위의 단서이며, +그 기능의 영구 불변성이나 Core 소유권을 단독으로 증명하지는 않는다. + +[Mac 단축키](https://support.apple.com/en-us/102650)와 +[Excel 단축키](https://support.microsoft.com/en-us/accessibility/excel/keyboard-shortcuts-in-excel)는 +Undo·Copy·선택 확장을 반복해서 제공한다. 반면 문자와 셀의 선택 단위는 다르고, +Excel의 `F2`는 셀 내부 편집으로 들어가는 맥락을 드러낸다. 현재 제품의 공개 +동작과 아래 역사적 지침을 대조하며, 현재 목록만으로 30년의 연속성을 주장하지 않는다. + +| 먼저 살펴볼 입력·기능 | 추출할 의미와 차이 | 기존 책임에 연결할 후보 | +| --- | --- | --- | +| Undo·Redo 단축키 | 되돌릴 작업의 단위, 전후 내용·선택, 실행할 History | Editing·History와 입력 계약 | +| Copy·Cut·Paste 단축키 | 대상 확보, 교환 표현, 제거·삽입 결과 | Editing·domain codec·Web | +| 방향키·Shift 확장·Select All | 탐색과 선택 확장, 전체의 범위와 반복 입력 결과 | Selection family·topology·입력 계약 | +| Delete·Backspace | 선택 대상 제거와 삽입점 주변 삭제, 값 비우기와 구조 제거 | 대상별 Editing 계약 | +| Enter·F2·Escape | 내부 편집 진입, 확정·취소·복귀의 맥락 | Affordance lifecycle·domain·Web | +| 서식·채우기 등 대상별 단축키 | 같은 입력 계열에서도 다른 대상의 속성·내용 변경 | 해당 domain 계약 | +| Save·Print | 저장·출력처럼 문서 편집 밖의 효과 | Application 계약 | + +이 표는 기능을 모두 추가하라는 목록이 아니다. 발견한 작업을 **시작 맥락·대상 → +의도 → 결과·선택·History**로 풀고, 여러 대상에서 유지되는 의미를 공통 문법으로, +대상별 차이를 domain 계약으로, 키와 수신 조건을 입력 계약으로 구체화한다. +기본 Hands profile은 이를 익숙한 동작으로 조합한다. 단축키에 직접 드러나지 않는 +선택 mapping·원자성·gesture 취소도 그 작업을 성립시키는 데 필요하면 남긴다. + ### 명령 이름보다 대상과 결과를 먼저 비교한다 Apple의 [1992년 Macintosh Human Interface Guidelines](https://www.vintageapple.org/inside_r/pdf/Human_Interface_Guidelines_1992.pdf) @@ -114,7 +148,7 @@ transition·map·reconcile·targets가 이미 이 차이의 정본 경계를 제 ### 중첩은 입력을 받을 곳의 계약이다 -[W3C APG Grid](https://www.w3.org/WAI/ARIA/apg/patterns/grid/#editing-and-navigating-inside-a-cell)는 +[W3C APG Grid](https://www.w3.org/WAI/ARIA/apg/patterns/grid/#keyboardinteraction-settingfocusandnavigatinginsidecells)는 셀 탐색에 쓰는 화살표와 셀 안의 caret·widget 조작을 구분한다. [Keynote의 텍스트 상자](https://support.apple.com/en-ca/guide/keynote/tan4fd6ee725/mac)는 객체 자체와 그 안의 텍스트를 따로 선택한다. 두 자료는 지침·제품 관습의 근거이며 @@ -139,6 +173,13 @@ Canvas Hand 처리하지 않은 입력의 명시적 위임과 실패를 구별하며, 정확한 키·진입 방법은 입력 profile이 정한다. 아직 전체 Hands에 실행 검증된 새 규칙은 아니다. +명령 후보를 찾는 단계의 미적용과 실행한 편집의 거절도 구별한다. +[ProseMirror Commands](https://prosemirror.net/docs/guide/#commands)는 적용할 수 없는 +명령의 `false`와 다음 후보 탐색을 정의한다. 이를 이 레포의 `EditingResult.ok: false`와 +같은 뜻으로 연결하지 않는다. [DOM Event](https://dom.spec.whatwg.org/#interface-event)의 +기본 동작 취소·전파 중단도 편집 성공과 별개의 관찰이며, 어떤 입력을 맡았는지는 +해당 binding의 계약으로 판단한다. + 여기서 편집 맥락은 HTML `editing host`, DOM element, JSON subtree, History instance와 일대일 대응하지 않는다. Toolbar로 focus가 옮겨져도 편집 명령은 저장된 선택을 대상으로 할 수 있다. Headless 호출은 DOM focus 없이 대상을 지정할 수 있다. @@ -155,7 +196,7 @@ native range의 방향을 투영하고, [Web virtual selection scope](../package | 과도한 일반화 | 근거 있는 반례 | 이 설계의 처리 | | --- | --- | --- | -| 선택은 언제나 Undo 대상이 아님 | [GIMP Undoing](https://docs.gimp.org/3.0/en/gimp-concepts-undo.html)은 Scissors Select 작업과 Quick Mask 전환의 Undo를 설명 | 현재 일시적 Selection의 EG-SELECT·EG-HISTORY를 유지. Raster 선택 제작의 History 정책으로 일반화하지 않음 | +| 선택만 바꾼 작업은 어떤 경우에도 Undo 기록이 아님 | [GIMP Undoing](https://docs.gimp.org/3.0/en/gimp-concepts-undo.html)의 선택 도구 작업과 [AppKit Undo](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/UndoArchitecture/Articles/AppKitUndo.html)의 저장되지 않는 view selection 기록 | 편집 전 선택 복원과 selection-only 기록을 구별. 현재 EG-SELECT·EG-HISTORY의 기본 정책을 다른 profile 전체에 강제하지 않음 | | 이동은 항상 Copy 후 즉시 제거한 뒤 Paste | [Finder 이동](https://support.apple.com/en-gb/102650)은 Clipboard의 파일을 목적지에 옮기는 동작을 제공 | EG-CUT은 현재 즉시 제거하는 cut 계약. 지연 이동은 같은 계약의 이름만 다른 구현이 아님 | | 복사한 저장 값을 그대로 넣으면 의미도 보존 | [Excel 수식 이동·복사](https://support.microsoft.com/en-us/excel/move-or-copy-a-formula-in-excel)는 복사 시 상대 참조 변경, 수식 이동 시 참조 유지를 구별 | payload의 참조·identity·배치 변환은 profile 계약. JSON 모양이나 text 일치만으로 교환 호환성을 판정하지 않음 | | Shift-click 확장은 항상 같은 anchor를 고정 | Apple HIG 1992 인쇄 쪽수 290–291은 addition과 fixed-point 두 방식을 설명 | 현재 EG-SELECT의 고정 anchor 전이를 유지. 다른 입력 관습을 같은 전이로 일반화하지 않음 | @@ -249,6 +290,71 @@ Document·Sheet 등은 Editing에, Rich Text는 자신의 package에 구현돼 Hands는 기존 공개 API들의 함께 검증된 조합이다. Host는 제품 데이터·권한·copy· layout·concrete external instance와 조합을 소유한다. +## 기본 Profile이 제공할 편집 동작 + +이 설계에서 기본 Hands profile은 **선택한 대상·입력·History 계약과 그 조합의 +적합성 기준**을 묶는다. +기본 Profile은 해당 편집 대상을 사용할 때 추가 정책 선택 없이 기대할 동작을 정한다. +Text·Sheet·Object가 같은 저장 model이나 선택 모양을 사용할 필요는 없다. +공통 기본 규칙을 각 대상별 계약과 조합하며, 같은 책임은 기존 정본 모듈에서 구현한다. + +| 층위 | 약속하는 것 | 변형의 경계 | +| --- | --- | --- | +| Core와 기반 모듈 | JSONDocument의 원자적 commit·관찰, Selection·Editing 등 각 owner의 공개 계약 | 어떤 Profile도 소비하는 기반 계약의 보장을 약화하지 않음 | +| 기본 Hands profile | 대상별 의미와 정착한 기본 입력·편집 결과·선택·복원 | 설정을 생략했을 때의 동작까지 명시하고 검증 | +| 허용된 설정 또는 다른 Profile | 다른 입력 관습·기록 범위·교환 정책이 필요한 경우의 선언된 차이 | 같은 Profile 안에서 허용한 선택인지, 별도 Profile/revision인지 식별 | + +기존 [JSON Document v3 profile](json-document-v3/profile.md)은 기반 계약을, +[Rich Text v1 profile](json-document-rich-text-v1/profile.md)은 대상별 계약을 정의한다. +[Official Hands Profile](../docs/public/official-hands.md)은 입력까지 조합하는 위치다. +기본 편집 동작을 묶는다는 이유로 이 계약들을 하나의 runtime 객체로 합치지 않는다. + +### 채택할 기본 의미와 구체화할 조건 + +다음은 이 초안이 기본으로 제공하려는 의미다. 기존 동작·EG 규칙과 연결하며, +뒤에 적은 조합 검증이 끝나기 전까지 모든 Hands의 구현 완료로 표시하지 않는다. + +| 동작 | 기본 Profile의 약속 | 구체화할 조건 / 연결 | +| --- | --- | --- | +| Undo 입력 | macOS `Cmd+Z`, Windows `Ctrl+Z`를 Undo 의도로 해석 | 편집 맥락의 수신 History, 실행 가능 여부와 조합 중 입력 처리. Web·Affordance | +| 편집 Undo/Redo | 외부 변경이 개입하지 않은 Undo는 편집 전 내용·선택을, Redo는 편집 후 내용·선택을 함께 복원 | 선택 범위와 방향, 여러 view의 귀속, 외부 변경 후 유효한 위치 복원. EG-HISTORY·EG-RESULT | +| 선택만 변경 | document와 문서 Undo/Redo 기록을 유지 | 선택 변경을 별도 Undo 대상으로 제공하는 정책과 구별. EG-SELECT | +| Copy | 현재 편집 맥락의 대상을 읽고 document·selection·History를 유지 | 셀 전체와 내부 문자열, 구조 표현과 교환 표현. EG-TARGET·EG-COPY | +| Cut/Paste | 확보한 대상에 대한 제거 또는 목적지에 맞는 삽입을 편집 결과와 후속 선택으로 설명 | 표현 선택·변환·거절·native 위임의 경계. EG-CUT·EG-PASTE | +| 작업 단위 | typing·composition·drag를 사용자에게 의미 있는 Undo 단위로 제공 | grouping의 경계와 취소 결과는 대상·입력·History 계약의 조합에서 명시. EG-GESTURE·EG-HISTORY | + +[CKEditor Undo](https://ckeditor.com/docs/ckeditor5/latest/api/module_undo_undo-Undo.html)는 +batch와 편집 전 selection을 함께 기록하며, +[UndoCommand](https://ckeditor.com/docs/ckeditor5/latest/api/module_undo_undocommand-UndoCommand.html)는 +그 selection을 복원한다. [CodeMirror History](https://codemirror.net/docs/ref/#commands.history)는 +일반 Undo와 selection 변경도 되돌리는 `undoSelection`을 구별한다. 해당 editor의 +공개 계약은 기본 복원의 근거가 되며, 특정 내부 알고리즘을 Core에 요구하지 않는다. + +특히 **편집과 함께 선택을 복원하는 것**과 **선택만 바꾼 작업을 새 Undo step으로 +기록하는 것**은 별개의 결정이다. 기본 Profile은 전자를 약속하고 현재 EG-SELECT의 +selection-only 정책을 유지한다. 후자의 변형이 필요하면 History owner의 지원과 +그 Profile의 적합성을 별도로 확인한다. + +### 기본값과 설정의 호환성 + +설정을 생략하는 것도 Profile의 유효한 사용이다. 따라서 기본값만 바꾸는 변경도 +기존 소비자의 결과를 바꾸면 호환성 변경이다. 사용자가 근육기억으로 익힌 조작은 +키 조합뿐 아니라 어느 맥락의 무엇을 어떻게 바꾸는지까지 포함한다. 기본 키가 +같아도 Undo 수신자나 선택 복원을 바꾸면 그 약속을 바꾼 것이다. +문서와 검증 사례에는 Profile revision, 대상·입력 계약, 선택한 설정과 생략 시 +기본값을 식별할 수 있게 남긴다. +모든 payload에 새 식별 필드를 추가하라는 요구는 아니다. + +| 변경 | 같은 Profile을 유지할 조건 | +| --- | --- | +| 내부 구현·캐시 교체 | 같은 입력·설정에서 대상·결과·선택·History·실패 의미가 보존됨 | +| 사용자의 단축키 재설정 | Profile이 허용한 입력 설정이며 Undo 의도와 복원 결과는 유지됨 | +| 기본 단축키·selection-only 기록·Paste 우선순위 변경 | 기존 revision의 생략된 설정과 명시된 설정 모두 기존 의미를 유지해야 함. 보존하지 못하면 별도 Profile/revision으로 제공 | + +허용할 설정의 목록은 각 Profile에 둔다. keymap을 재정의할 수 있는 API가 있다고 +모든 재정의를 같은 기본 Profile의 적합한 동작으로 인정하지 않는다. 선택지는 +열어 두되, 기본 경로를 사용하는 제품이 반복해서 정책을 설계할 필요가 없게 한다. + ## 공통으로 고정할 편집 규칙 아래 ID는 설계 요구사항이다. 실제 동결은 owner의 versioned 계약과 적합성 @@ -256,7 +362,7 @@ layout·concrete external instance와 조합을 소유한다. 규칙의 전제가 아니다. 이 표의 적용 범위는 현재 구조·텍스트 편집 profile의 계약이다. EG-SELECT의 -일시적 선택과 문서 History 분리, EG-CUT의 즉시 제거를 다른 장르의 모든 관습으로 +selection-only 기록 정책과 EG-CUT의 즉시 제거를 다른 장르의 모든 관습으로 일반화하지 않는다. 범위 확장의 anchor 규칙은 고정 anchor로 확장하는 현재 range 전이에 적용한다. 다른 family나 raster·지연 이동 profile을 동결하려면 해당 상태와 History·transfer 의미를 먼저 명시해야 하며, 기존 profile의 규칙을 느슨하게 @@ -284,6 +390,13 @@ Headless `cut()`은 보존 가능한 payload를 반환하고, Web event binding 서로 다른 결과로 검증한다. 브라우저가 custom edit를 다시 실행하지 않도록 하는 event ownership은 Web이 소유한다. +Web의 메모리 Clipboard 테스트가 확인하는 것은 표현 쓰기와 editor callback의 +결과다. 쓰기 실패 시 제거 callback이 호출되지 않았더라도 실제 브라우저의 후속 +기본 동작까지 차단됐다고 단정할 수 없다. [Clipboard API 초안의 cut 처리](https://www.w3.org/TR/2026/WD-clipboard-apis-20260624/#cut-action)와 +연결해 event 취소 상태·후속 native 편집·OS Clipboard 반영을 별도로 관찰해야 한다. +이것은 기본 Cut 보장의 검증 범위이며 확정된 브라우저 결함이나 OS transaction +보장으로 해석하지 않는다. + EG-HISTORY에서 외부 변경 이후의 의미는 선택한 History 계약을 따른다. 현재 local inverse History는 이를 비우며, collaboration History는 내 기여를 선택적으로 되돌린다. 둘을 같은 복원 알고리즘으로 고정하지 않는다. 이미 존재하는 @@ -294,6 +407,12 @@ composition grouping은 [DOM 편집 lifecycle](dom-editing-lifecycle.md)의 별 계약을 따른다. `createGestureSession`의 존재만으로 Host의 preview가 문서를 변경하지 않는다고 증명할 수 없으므로 실제 연결까지 검증한다. +입력 중 native 표시와 확정된 document, History step도 구별한다. +[Input Events Level 2 초안](https://www.w3.org/TR/2026/WD-input-events-2-20260501/#input-event-order-during-composition)은 +취소할 수 없는 IME 조합 갱신을 설명한다. 기본 Profile의 입력 결과·취소·Undo +의미를 고정하고, 실제 입력 trace가 그 결과로 귀결되는지는 Web binding에서 +검증한다. Working Draft의 특정 이벤트 순서나 지연 시간을 Core에 고정하지 않는다. + ## Hands profile이 반드시 결정할 내용 각 profile은 다음 질문에 하나의 답 또는 명시적인 설정별 답을 제공한다. 이는 @@ -346,6 +465,23 @@ Copy를 부분 문자열 Copy로 바꾸지는 않는다. Sheet의 primary rectan 아니다. 이미 다른 의미를 가진 호출들을 한 이름으로 합치면 대상과 결과의 차이가 숨는다. 공통 protocol은 공유하는 규칙에 두고, 문서의 의미는 기존 owner에 둔다. +### 기본 Profile의 코드 연결을 설계할 위치 + +다음 표는 후속 구현에서 확인할 정본과 공백을 연결한다. 이 문서 변경은 해당 API를 +확장하지 않으며, 기존 API로 조합할 수 있는지 확인한 뒤 부족한 owner만 확장한다. + +| 책임 | 기존 코드와 API | 다음 설계에서 닫을 공백 | +| --- | --- | --- | +| 기본 입력 해석·실행 가능 표시 | Web [keyboard.ts](../packages/json-document-web/src/keyboard.ts)의 `defaultWebKeymap`, `createWebKeyboardAdapter`; Affordance의 `historyAffordance` | 설정 생략·허용된 재설정에서 Undo 의도가 보존되는지 확인. 기본 keymap을 Host마다 복제하지 않음 | +| 편집·선택·History 연결 | Editing의 `createEditingSession`, [EditingHistory](../packages/json-document-editing/src/history.ts) | 모델의 전후 선택과 native 선택 투영을 구별하고, 여러 view에서 실행할 History와 복원 대상 명시 | +| 중첩 진입·복귀와 입력 수신 | Affordance의 rename/gesture lifecycle, Web의 `isWebEditingHostTarget`, 각 domain의 Web binding | 후보 미적용·맡은 편집 거절·명시적 위임을 구별하는 연결. 유효한 조합을 지원하지 못하면 정본 API에서 해결 | +| 표현 교환과 Paste | Web의 `createWebClipboardBinding`, domain의 codec·표현·paste API | 표현 선택 실패와 선택한 payload의 편집 거절을 구별. 실제 native 후속 처리까지 관찰 | + +먼저 아래의 기본 Undo 사례를 공개 API 조합에 연결하고, 같은 기대 결과를 입력 +binding까지 확장한다. 중첩·여러 view·외부 표현의 차이는 해당 계약에서 별도로 +구체화한다. 구현을 바꿀 때 필요한 owner reference·Usage·source 등록은 각 owning +package의 기존 위치에서 함께 갱신한다. + ## 적합성 설계 테스트의 단위는 함수 유무가 아니라 **시작 상태 → 의미 있는 작업 → 관찰 결과**다. @@ -452,6 +588,8 @@ profile 동결로 확대하지 않는다. | 사례 | 시작 상태 → 작업 → 판정할 결과 | 연결·현재 증거의 한계 | | --- | --- | --- | | 셀과 내부 문자열 | `Alpha` 셀 선택 / 내부 `ph` 선택 각각에서 Copy → 셀 payload / `ph` | EG-TARGET·EG-COPY. 기존 Sheet·native selection 증거는 각각 있으며 이 중첩 전체의 공통 binding은 없음 | +| 기본 Undo와 선택 복원 | 아래의 `Alpha` 범위 교체 → Undo → Redo; 정방향·역방향 각각 실행 | EG-HISTORY·EG-RESULT. 전후 model selection 증거와 실제 native range 복원 증거를 구별 | +| 선택만 변경한 뒤 Redo | 편집 → Undo → 선택만 이동 → Redo → 기록된 편집 후 내용·선택 | EG-SELECT·EG-HISTORY. 기본 Profile에서 선택 이동이 새 문서 History entry를 만들거나 Redo를 지우지 않음 | | 거절 시 바깥으로 전이 금지 | 선택한 객체 안의 text editor가 Delete 거절 → 객체 제거·외부 History entry가 생기지 않음 | EG-EDIT·입력 계약 후보. [Web clipboard ownership](../packages/json-document-web/tests/clipboard-rejection.test.ts)은 한 binding의 거절 처리 증거이며 일반적인 중첩 거절 vector는 없음 | | Toolbar 대상 보존 | editor A에서 범위 선택 → Toolbar에 focus → Copy 실행 → A의 선택 payload | 입력 focus와 Selection 구별. 전체 Hands에 대한 공통 실행 증거 없음 | | 셀 값과 행 구조 | 값이 있는 셀을 비움 / 해당 record 삭제 → 전자는 구조 유지, 후자는 record 제거 | EG-EDIT·profile 대상. 현재 Sheet Cut과 Database `record.delete`는 서로 다른 계약 | @@ -459,12 +597,29 @@ profile 동결로 확대하지 않는다. | Draft 취소와 Undo | 미확정 rename draft 수정 → cancel → 확정 label 불변; 확정 rename → Undo → 이전 label | EG-GESTURE·EG-HISTORY. 구조 gesture runner가 모든 rename·IME 취소를 증명하지 않음 | | 외부 참조·다른 profile로 Paste | 수식 또는 reference를 다른 위치·profile에 Paste → 변환·보존·거절 중 선언한 결과 | EG-PASTE. 현재 세 editor의 내부 round trip만으로 cross-profile 교환을 인증할 수 없음 | | 공유 문서의 두 편집 맥락 | A에서 편집 → B로 입력 focus 전환 → Undo → 선택한 History owner가 명시한 기여·선택 복원 | EG-HISTORY·EG-RESULT. 외부 History 연결 증거와 모든 Hand의 입력 routing 증거는 다름 | +| Cut 쓰기 실패 후 native 처리 | editable selection에서 지원하는 Cut의 표현 쓰기 실패 → 해당 작업의 원본 제거 없음 | EG-CUT·Web. 제거 callback 미호출 외에 event 취소·후속 beforeinput/input·document·History를 관찰. 브라우저 재현 전 검증 공백 | +| IME와 Undo 수신 | 조합 완료 / 취소 / 조합 중 Undo·focus 이동 → 선언된 내용·선택·History·입력 상태 | EG-HISTORY·Web lifecycle. 합성 composition fixture와 OS IME 검증을 구별 | + +기본 Undo 사례는 Rich Text에서 다른 변경이 끼어들지 않고 `ph`를 `X`로 교체하는 +상황으로 구체화한다. offset은 같은 text node 안의 위치다. -이번 도출로 설계 방향을 정할 수 있는 부분은 대상·위치·선택·입력 수신자의 구별, -대상별 계약과 입력 계약의 조합, 기존 owner의 유지다. 아직 동결할 수 없는 부분은 -중첩 편집의 진입·거절·복귀 규칙, profile 간 payload 변환, raster/file 편집의 -적용 범위, 여러 편집 맥락의 History routing이다. 같은 profile을 독립적으로 -구현했을 때 이 사례들의 결과가 일치해야 장기 호환성 약속으로 승격할 수 있다. +| 단계 | 내용 | 선택과 History의 기대 결과 | +| --- | --- | --- | +| 시작 | `Alpha` | 정방향 `(anchor: 2, focus: 4)` 또는 역방향 `(anchor: 4, focus: 2)`으로 `ph` 선택. History 비어 있음 | +| 교체 | `AlXa` | offset 3의 collapsed caret. 한 번의 Undo 가능 | +| Undo | `Alpha` | 시작 때의 범위와 방향 복원. Undo 불가·Redo 가능 | +| Redo | `AlXa` | offset 3의 collapsed caret 복원. Undo 가능·Redo 불가 | + +선택만 바꾼 뒤 Redo하는 후보는 Undo 행과 Redo 행 사이에서 caret을 옮겨 실행한다. +이 선택 이동은 별도 step이 아니며, Redo는 기록된 교체 후 선택을 복원해야 한다. +이 표는 공개 동작에 대한 기대 결과다. 새 테스트의 통과를 주장하지 않으며, +Document의 offset 하나만으로 과거 native range 전체를 복원할 수 있다고 가정하지 않는다. + +기본 Profile의 역할과 Undo·선택 복원의 기본 의미는 설계에 채택한다. 입력 수신, +중첩의 진입·거절·복귀, profile 간 payload 변환과 여러 view의 History 귀속은 +위 사례로 더 구체화한다. 기본값을 정하는 것과 동결을 인증하는 것은 다른 단계다. +같은 profile을 독립적으로 구현했을 때 결과가 일치하고 실제 입력 경로에서도 +약속이 유지되어야 장기 호환성 증거로 승격할 수 있다. ## 장기 호환성 From 97405f78f464b7b311a822d68fddc999422b05e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Tue, 8 Sep 2026 09:38:49 +0900 Subject: [PATCH 04/20] =?UTF-8?q?docs:=20=EA=B8=B0=EB=B3=B8=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=20=EA=B3=84=EC=95=BD=ED=91=9C=EC=99=80=20=EC=B5=9C?= =?UTF-8?q?=EC=86=8C=20=EC=BD=94=EB=93=9C=20=EB=B3=80=EA=B2=BD=EC=95=88?= =?UTF-8?q?=EC=9D=84=20=EA=B5=AC=EC=B2=B4=ED=99=94=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- standards/editing-grammar.md | 151 +++++++++++++++++++++++++---------- 1 file changed, 107 insertions(+), 44 deletions(-) diff --git a/standards/editing-grammar.md b/standards/editing-grammar.md index 9caf48c7..9c43ce7c 100644 --- a/standards/editing-grammar.md +++ b/standards/editing-grammar.md @@ -309,19 +309,37 @@ Text·Sheet·Object가 같은 저장 model이나 선택 모양을 사용할 필 [Official Hands Profile](../docs/public/official-hands.md)은 입력까지 조합하는 위치다. 기본 편집 동작을 묶는다는 이유로 이 계약들을 하나의 runtime 객체로 합치지 않는다. -### 채택할 기본 의미와 구체화할 조건 +### 기본 동작별 계약 -다음은 이 초안이 기본으로 제공하려는 의미다. 기존 동작·EG 규칙과 연결하며, -뒤에 적은 조합 검증이 끝나기 전까지 모든 Hands의 구현 완료로 표시하지 않는다. +기본 Profile에서 먼저 닫을 범위는 아래 12개 동작이다. 각 대상에 적용되는 동작을 +기본 경로로 제공하며, 미지원과 일시적 실행 불가를 구별한다. 예를 들어 내부 편집을 +제공하는 Hand에는 진입·확정·취소·복귀가 필요하지만, 모든 객체에 text editor를 +내장할 필요는 없다. 표는 이 초안이 채택하는 계약이며 전체 Hands의 구현 완료나 +기존 API 기본값의 즉시 변경을 뜻하지 않는다. -| 동작 | 기본 Profile의 약속 | 구체화할 조건 / 연결 | -| --- | --- | --- | -| Undo 입력 | macOS `Cmd+Z`, Windows `Ctrl+Z`를 Undo 의도로 해석 | 편집 맥락의 수신 History, 실행 가능 여부와 조합 중 입력 처리. Web·Affordance | -| 편집 Undo/Redo | 외부 변경이 개입하지 않은 Undo는 편집 전 내용·선택을, Redo는 편집 후 내용·선택을 함께 복원 | 선택 범위와 방향, 여러 view의 귀속, 외부 변경 후 유효한 위치 복원. EG-HISTORY·EG-RESULT | -| 선택만 변경 | document와 문서 Undo/Redo 기록을 유지 | 선택 변경을 별도 Undo 대상으로 제공하는 정책과 구별. EG-SELECT | -| Copy | 현재 편집 맥락의 대상을 읽고 document·selection·History를 유지 | 셀 전체와 내부 문자열, 구조 표현과 교환 표현. EG-TARGET·EG-COPY | -| Cut/Paste | 확보한 대상에 대한 제거 또는 목적지에 맞는 삽입을 편집 결과와 후속 선택으로 설명 | 표현 선택·변환·거절·native 위임의 경계. EG-CUT·EG-PASTE | -| 작업 단위 | typing·composition·drag를 사용자에게 의미 있는 Undo 단위로 제공 | grouping의 경계와 취소 결과는 대상·입력·History 계약의 조합에서 명시. EG-GESTURE·EG-HISTORY | +| 동작 | 대상 → 기본 결과 | 후속 선택·History | 구체화할 차이 / 기존 owner | +| --- | --- | --- | --- | +| 탐색 | 현재 맥락의 위치 → 유효한 이웃·경계로 이동, 문서 불변 | 대상별 caret 또는 논리 focus·선택 전이. 문서 History 유지 | 문자·셀·가시 노드의 단위와 선택 동반 여부. Selection·Topology·입력 계약 | +| 선택 | 유효한 대상·삽입점 → 요청한 선택으로 전이 | 내용·문서 Undo/Redo 기록 유지 | range·key set, primary와 복수 범위. Selection·editor | +| 선택 확장 | 기존 선택 → 확장한 범위 | 고정 anchor 범위는 anchor를 보존하며 focus 이동. 문서 History 유지 | 문자·셀·가시 순서; key 집합 추가는 별도 전이. Selection·Topology | +| 전체 선택 | 현재 맥락이 정한 전체 대상 → 한 선택 전이로 전부 선택 | 같은 대상 집합에서 반복해도 유지. 문서 History 유지 | 기본 입력도 선택 해제로 토글하지 않음. 단계적 범위 확대·토글은 다른 입력 정책. Selection·Affordance | +| 삽입·교체 | 삽입점·대체 범위 또는 명시 대상 → 유효한 새 내용·구조 | 삽입 후 작업 위치 또는 삽입 대상 선택. 확정 작업 단위로 Undo | 문자열 대체·셀 값 commit·객체 삽입의 의미. domain·Editing | +| 삭제 | 선택 대상 또는 삽입점 주변 → 선언한 내용·구조 제거 | 유효한 남은 위치·대상 또는 빈 선택. 확정 작업 단위로 Undo | 문자 삭제·셀 값 비우기·subtree 제거 구별. domain·Editing | +| Copy | 현재 맥락의 대상 → 구조 payload와 교환 표현 | document·selection·History 유지 | 셀 전체와 내부 문자열, 표현과 대상 폐포. domain·Web | +| Cut | 확보한 대상 → payload 확보 후 그 대상 제거 | domain이 정한 삭제 후 선택. 성공한 편집의 Undo | 확보 실패 시 제거 없음, 제거 거절 시 편집 상태 유지. domain·Editing·Web | +| Paste | 목적지·대체 범위와 payload → 목적지 계약에 맞는 삽입 | 삽입 결과에 맞는 선택. 확정 작업 단위로 Undo | 표현 우선순위·identity·참조 변환·경계·거절. domain·Web·Editing | +| Undo·Redo | 선택한 History의 작업 → 편집 전·후 상태 복원 | 외부 변경이 없으면 내용과 선택 범위·방향을 함께 복원 | grouping·외부 변경·여러 view의 귀속. Editing·선택한 History owner | +| 내부 편집 진입 | 선택한 셀·항목·객체 → 해당 내부 편집 맥락 | 진입만으로 문서 History를 만들지 않음. 외부 선택과 복귀 대상 보존 | F2·Enter·pointer의 의미와 내부 선택 초기화. Affordance·domain·Web | +| 확정·취소·복귀 | 진행 중인 명시적 draft → 성공 시 확정·복귀, 취소 시 확정 내용 유지 | 성공한 확정만 Undo 대상. 확정 거절 시 draft와 편집 맥락 유지 | 대상이 유효한 rename·cell draft의 기본 정책. 연속 입력·IME는 별도 lifecycle. Affordance·domain·Web | + +기본 Undo 입력은 macOS `Cmd+Z`, Windows `Ctrl+Z`다. Copy·Cut·Paste와 전체 +선택도 선택한 플랫폼의 기본 입력으로 연결한다. 키 이름만으로 수신자를 정하지 +않으며, 중첩 맥락·native control·조합 중 입력의 소유권을 함께 결정한다. +typing·composition·drag의 grouping은 각 계약의 작업 경계로 명시한다. + +Object의 방향키가 좌표 변경을 뜻하면 그것은 탐색과 다른 편집이며 Undo 대상이다. +Rich Text의 Enter가 단락 삽입이면 그것을 draft 확정과 같은 작업으로 해석하지 +않는다. 이런 차이는 새 전역 command union 대신 대상별 계약에 남긴다. [CKEditor Undo](https://ckeditor.com/docs/ckeditor5/latest/api/module_undo_undo-Undo.html)는 batch와 편집 전 selection을 함께 기록하며, @@ -433,23 +451,28 @@ composition grouping은 [DOM 편집 lifecycle](dom-editing-lifecycle.md)의 별 기대되는 기본 편집 흐름을 생략할 수 없으며, 그 선택의 외부 관습이나 제품 목적을 profile에서 설명한다. -### 현재 구현을 설명하는 대표 매핑 +### 대상별로 비교한 현재 결과 이 표는 현재 동작의 관찰이다. 관찰된 기본값이 곧 영구히 고정할 규칙은 아니다. -| 항목 | Document | Sheet | Rich Text | +| 대상 / binding | 선택·Copy 대상 | 교체·제거 → 후속 선택 | Paste → 후속 선택 | | --- | --- | --- | --- | -| 편집 단위 | stable ID를 가진 블록과 text | stable row/column ID의 셀 | stable node ID와 text/child point | -| Copy 대상 | 선택된 블록 전체 | primary rectangle | 선택된 structured slice | -| Paste | 기본적으로 마지막 선택 블록 뒤에 삽입 | focus 셀부터 고정 경계 안에 기록 | 선택 구간과 schema에 맞게 slice 삽입 | -| 제거 | 선택 블록 제거 | Cut은 primary rectangle 값을 `null`로 비움 | 선택 구간 제거·schema 제약 적용 | -| 후속 선택 | 삽입 블록마다 collapsed range, 첫 블록 primary; 삭제 후 남은 이웃 | 붙여넣은 직사각형, Cut은 기존 선택 | transform 후 mapping된 text/child point | - -현재 binding은 각각 `json-document-editing/src/document.ts`, `sheet.ts`, -`json-document-rich-text/src/editor.ts`에 있다. Document의 text offset이 블록 -Copy를 부분 문자열 Copy로 바꾸지는 않는다. Sheet의 primary rectangle 정책도 -여러 범위 전체의 Copy와 구별한다. 이런 차이는 profile 이름과 사용법에서 드러나야 -하며, 공통 함수 이름 때문에 사용자가 동일한 대상을 예상하게 해서는 안 된다. +| [Rich Text](../packages/json-document-rich-text/src/editor.ts) | node ID + text/child range, structured slice | `text.insert`는 선택 구간 대체; 제거는 schema 제약 적용 → mapping된 point | 선택 구간에 slice 삽입 → 삽입 결과의 point | +| [Document](../packages/json-document-editing/src/document.ts) | block ID + offset, Copy는 블록 전체 | `text.replace`는 블록 문자열 교체 → 지정 offset 또는 끝; 제거 → 남은 이웃 | 기본적으로 마지막 선택 블록 뒤에 삽입 → 블록마다 collapsed range, 첫 블록 primary | +| [Tree](../packages/json-document-editing/src/tree.ts) | 가시 노드 범위; Copy·제거는 숨은 후손도 포함 | 선택 subtree 제거 → 남은 가시 이웃 또는 빈 선택 | 대상 노드와 같은 parent 아래 새 ID의 subtree 추가 → 삽입 root들 선택 | +| [Sheet](../packages/json-document-editing/src/sheet.ts) | 셀 range들의 집합; Copy는 primary rectangle | `cell.commit` → 해당 셀; `selection.fill(null)`은 선택된 셀 값, Cut은 primary 값만 비움 → 선택 유지 | focus 셀부터 경계 내 기록 → 붙여넣은 직사각형 | +| [Object](../packages/json-document-editing/src/object.ts) | object ID 집합; Copy는 해당 객체 | 속성 변경·이동과 객체 제거를 구별; 제거 → 저장 순서의 남은 이웃 또는 빈 선택 | 새 ID와 선언한 좌표 offset으로 추가 → 삽입 객체들 선택 | + +이들의 기본 local History는 외부 변경이 없는 편집의 전후 내용·선택을 기록한다. +같은 Undo 규칙을 공유해도 문자열 range·가시 노드 범위·셀 rectangle·object ID +집합을 하나의 선택 모양으로 합칠 필요는 없다. + +탐색의 단위도 대상별이다. Text의 caret, Tree의 가시 순서, Sheet의 행·열 topology를 +구별하고 Object의 좌표 변경과 분리한다. 항목 label의 내부 편집은 현재 +[Order](../packages/json-document-editing/src/order.ts)의 `item.rename`과 +`createRenameSession` 조합으로 확인한다. Tree·Object에 같은 내부 편집이 이미 +제공된다고 일반화하지 않는다. Database의 record, Calendar의 occurrence, Annotation의 +source 참조와 Composer의 중첩은 앞의 11개 사례에서 정한 domain 차이를 유지한다. ## 구현에서 계약으로 옮길 때의 결정 @@ -465,22 +488,54 @@ Copy를 부분 문자열 Copy로 바꾸지는 않는다. Sheet의 primary rectan 아니다. 이미 다른 의미를 가진 호출들을 한 이름으로 합치면 대상과 결과의 차이가 숨는다. 공통 protocol은 공유하는 규칙에 두고, 문서의 의미는 기존 owner에 둔다. -### 기본 Profile의 코드 연결을 설계할 위치 +### 하이라키 검토와 최소 코드 변경안 -다음 표는 후속 구현에서 확인할 정본과 공백을 연결한다. 이 문서 변경은 해당 API를 -확장하지 않으며, 기존 API로 조합할 수 있는지 확인한 뒤 부족한 owner만 확장한다. +기본 Undo 사례는 기존 `createRichTextEditor`와 Web keymap의 공개 API 조합으로 +실행됐다. 따라서 이 경로를 위해 Core에 Selection·History를 넣거나 새 session +계층을 추가하지 않는다. 현재 공백은 기반 보장을 기본 동작으로 조합하는 계약에 있다. -| 책임 | 기존 코드와 API | 다음 설계에서 닫을 공백 | +| 확인한 계약과 증거 | 판단 / 최소 변경안 | 완료를 판정할 사례 | | --- | --- | --- | -| 기본 입력 해석·실행 가능 표시 | Web [keyboard.ts](../packages/json-document-web/src/keyboard.ts)의 `defaultWebKeymap`, `createWebKeyboardAdapter`; Affordance의 `historyAffordance` | 설정 생략·허용된 재설정에서 Undo 의도가 보존되는지 확인. 기본 keymap을 Host마다 복제하지 않음 | -| 편집·선택·History 연결 | Editing의 `createEditingSession`, [EditingHistory](../packages/json-document-editing/src/history.ts) | 모델의 전후 선택과 native 선택 투영을 구별하고, 여러 view에서 실행할 History와 복원 대상 명시 | -| 중첩 진입·복귀와 입력 수신 | Affordance의 rename/gesture lifecycle, Web의 `isWebEditingHostTarget`, 각 domain의 Web binding | 후보 미적용·맡은 편집 거절·명시적 위임을 구별하는 연결. 유효한 조합을 지원하지 못하면 정본 API에서 해결 | -| 표현 교환과 Paste | Web의 `createWebClipboardBinding`, domain의 codec·표현·paste API | 표현 선택 실패와 선택한 payload의 편집 거절을 구별. 실제 native 후속 처리까지 관찰 | +| Rich Text의 범위 교체·Undo·Redo와 [EditingHistory](../packages/json-document-editing/src/history.ts): 아래 12개 공개 API 사례 통과 | 이 범위의 runtime·API 변경 불필요. 기존 owner의 conformance binding에 사례를 정착시키고 실제 native 선택 경로 확인 | 양방향 범위 복원, Undo 후 선택 이동과 Redo 보존. model과 DOM 관찰 구별 | +| [selectAllAffordance](../packages/json-document-affordance/src/select.ts)는 `allSelected: true`에서 `clear` 반환 | Affordance에 반복 입력 정책을 명시적으로 선택할 수 있는 경로 추가. 기본 Hands는 선택 유지 정책 사용; 기존 토글 호출의 생략된 설정은 보존 | 같은 universe에서 두 번 Mod+A → 전체 선택 유지; 명시한 토글 경로 → 해제 | +| [createRenameSession](../packages/json-document-affordance/src/session.ts)의 `onCommit`은 `void`; editor 거절 후에도 draft를 지우고 `onFinish` 호출 | Affordance에 성공 여부를 받는 확정 경로 추가. 거절 시 draft 유지, 성공 시 한 번 종료. 기존 `void` callback 경로는 보존 | label 검증 거절 → 내용·선택·History와 draft 유지; 수정 후 재시도 성공 → 한 번 확정·복귀 | +| [Rich Text Web](../packages/json-document-rich-text-web/src/contenteditable.ts)은 Undo 키 조건을 직접 판단하고, 구조 입력은 [Web keyboard](../packages/json-document-web/src/keyboard.ts)를 소비 | 공통 Undo/Redo 해석은 Web resolver를 소비하도록 연결. Text 삭제·IME·root 소유권은 Rich Text Web에 유지. 기존 modifier 수용 범위의 차이는 명시적 입력 정책으로 보존 | 기본 Cmd/Ctrl+Z와 Redo가 같은 의도로 해석됨; 추가 modifier·조합 중 입력은 선언한 정책으로 판정 | +| Document·Order·Tree·Sheet의 `selection.set`은 단일 point 전이; 전체 선택을 단일 전이로 받는 공개 경로는 없음 | 기존 domain Intent에 전체 선택 작업을 추가하고 Selection family로 한 번에 적용. 전체 집합은 Document·Order의 순서, Tree의 visible topology, Sheet의 행·열로 결정. Host의 첫·끝 연속 dispatch는 중간 관찰을 노출하므로 채택하지 않음 | 0·1·여러 대상에서 전체 선택 한 작업, 중간 부분 선택 publication 없음, 반복 결과·History 유지 | + +`onCommit` 반환값을 단순히 `void | boolean`으로 바꾸는 안도 기존 callback의 +호환성을 확인해야 한다. 현재 테스트의 `commits.push(...)`처럼 값을 반환하면서 +`void` callback으로 쓰던 호출이 있다. 성공 확인 경로는 기존 호출과 분리해 추가하고, +정확한 public signature는 그 owner의 호환성 검증과 함께 확정한다. + +Affordance의 후속 API 초안은 다음 두 호출을 지원하는 것이다. 아직 존재하지 않는 +옵션을 보여주는 설계 예이며 현재 Usage가 아니다. + +```ts +selectAllAffordance(stroke, state, { repeat: "preserve" }); +createRenameSession({ + tryCommit: (itemId, label) => editor.dispatch({ type: "item.rename", itemId, label }).ok, + onFinish: restoreItemFocus, +}); +``` -먼저 아래의 기본 Undo 사례를 공개 API 조합에 연결하고, 같은 기대 결과를 입력 -binding까지 확장한다. 중첩·여러 view·외부 표현의 차이는 해당 계약에서 별도로 -구체화한다. 구현을 바꿀 때 필요한 owner reference·Usage·source 등록은 각 owning -package의 기존 위치에서 함께 갱신한다. +`repeat` 생략은 기존 토글을 유지하고 기본 Hands의 조합이 `preserve`를 선택한다. +rename은 기존 `onCommit`과 새 동기 `tryCommit` 중 하나를 받으며 둘을 함께 +허용하지 않는 안이다. `false`는 draft 유지, `true`는 종료를 뜻하고 Affordance가 +EditingResult나 domain 검증 규칙을 직접 소유하지 않는다. 비동기 확정은 이 안의 +범위가 아니다. domain의 전체 선택 작업도 기존 point 선택 호출을 보존하는 추가 +계약으로 다루며 새 공통 selection state 객체나 registry를 요구하지 않는다. + +Host 연결도 구별한다. [Order Demo](../site/src/routes/order-demo/OrderDemoRoute.tsx)는 +정본 rename session을 `item.rename`에 연결하므로 거절 후 draft 처리를 Host에 +다시 구현하지 않는다. [Canvas Demo](../site/src/routes/canvas-demo/CanvasDemoRoute.tsx)의 +전체 선택은 `selectAllAffordance` 결과를 Object의 한 `selection.set`에 연결한다. +기본 정책의 소유자는 Affordance·Profile이며 Canvas의 조건문으로 바꾸지 않는다. + +후속 구현 순서는 기존 Undo 증거의 정착, 기본 전체 선택의 정본 입력·domain 연결, +명시적 draft 확정 결과의 연결이다. Native Cut·IME·여러 view의 History는 아래 +검증 질문으로 입력·외부 효과를 확인한 뒤 해당 owner에서 설계한다. 구현을 바꿀 때 +owner reference·Usage·source 등록도 각 package의 기존 위치에서 함께 갱신한다. +이 문서 변경은 표의 runtime 수정이나 public signature를 적용하지 않는다. ## 적합성 설계 @@ -581,20 +636,21 @@ profile 동결로 확대하지 않는다. ## 새 도출의 검증 질문 -다음은 실행할 수 있도록 시작 상태와 판정 결과를 적은 **후보 사례**다. 앞의 -기존 적합성 표의 통과 항목과 합산하지 않는다. 이 문서 변경은 새 behavior vector나 -구현을 추가하지 않는다. +다음은 시작 상태와 판정 결과를 적은 **조합 검증 사례**다. 일부 model 경로는 +임시 공개 API 실행으로 확인했으며, 영구 conformance vector·native 입력 검증과 +구별한다. 앞의 기존 적합성 표의 통과 항목과 합산하지 않는다. 이 문서 변경은 새 +behavior vector나 runtime 구현을 추가하지 않는다. | 사례 | 시작 상태 → 작업 → 판정할 결과 | 연결·현재 증거의 한계 | | --- | --- | --- | | 셀과 내부 문자열 | `Alpha` 셀 선택 / 내부 `ph` 선택 각각에서 Copy → 셀 payload / `ph` | EG-TARGET·EG-COPY. 기존 Sheet·native selection 증거는 각각 있으며 이 중첩 전체의 공통 binding은 없음 | -| 기본 Undo와 선택 복원 | 아래의 `Alpha` 범위 교체 → Undo → Redo; 정방향·역방향 각각 실행 | EG-HISTORY·EG-RESULT. 전후 model selection 증거와 실제 native range 복원 증거를 구별 | -| 선택만 변경한 뒤 Redo | 편집 → Undo → 선택만 이동 → Redo → 기록된 편집 후 내용·선택 | EG-SELECT·EG-HISTORY. 기본 Profile에서 선택 이동이 새 문서 History entry를 만들거나 Redo를 지우지 않음 | +| 기본 Undo와 선택 복원 | 아래의 `Alpha` 범위 교체 → Undo → Redo; 정방향·역방향 각각 실행 | EG-HISTORY·EG-RESULT. 임시 공개 API 실행에서 model 복원 확인; native range와 영구 vector는 별도 | +| 선택만 변경한 뒤 Redo | 편집 → Undo → 선택만 이동 → Redo → 기록된 편집 후 내용·선택 | EG-SELECT·EG-HISTORY. 임시 실행에서 새 문서 History entry 없이 Redo·편집 후 선택 보존 확인 | | 거절 시 바깥으로 전이 금지 | 선택한 객체 안의 text editor가 Delete 거절 → 객체 제거·외부 History entry가 생기지 않음 | EG-EDIT·입력 계약 후보. [Web clipboard ownership](../packages/json-document-web/tests/clipboard-rejection.test.ts)은 한 binding의 거절 처리 증거이며 일반적인 중첩 거절 vector는 없음 | | Toolbar 대상 보존 | editor A에서 범위 선택 → Toolbar에 focus → Copy 실행 → A의 선택 payload | 입력 focus와 Selection 구별. 전체 Hands에 대한 공통 실행 증거 없음 | | 셀 값과 행 구조 | 값이 있는 셀을 비움 / 해당 record 삭제 → 전자는 구조 유지, 후자는 record 제거 | EG-EDIT·profile 대상. 현재 Sheet Cut과 Database `record.delete`는 서로 다른 계약 | | 반복 일정 영향 범위 | 같은 occurrence에 `this` / `all` 이동 → profile이 정한 반복 일정 집합만 영향 | EG-TARGET·EG-EDIT. Calendar domain 사례이며 입력 맥락 전환으로 해석하지 않음 | -| Draft 취소와 Undo | 미확정 rename draft 수정 → cancel → 확정 label 불변; 확정 rename → Undo → 이전 label | EG-GESTURE·EG-HISTORY. 구조 gesture runner가 모든 rename·IME 취소를 증명하지 않음 | +| Draft 취소·거절과 Undo | draft 수정 → cancel 또는 검증 거절 → 확정 label 불변; 성공한 rename → Undo → 이전 label | EG-GESTURE·EG-HISTORY. 현재 rename 거절에서 문서 보존은 확인했으나 draft 종료도 관찰. 제안한 거절 후 유지 계약의 공백 | | 외부 참조·다른 profile로 Paste | 수식 또는 reference를 다른 위치·profile에 Paste → 변환·보존·거절 중 선언한 결과 | EG-PASTE. 현재 세 editor의 내부 round trip만으로 cross-profile 교환을 인증할 수 없음 | | 공유 문서의 두 편집 맥락 | A에서 편집 → B로 입력 focus 전환 → Undo → 선택한 History owner가 명시한 기여·선택 복원 | EG-HISTORY·EG-RESULT. 외부 History 연결 증거와 모든 Hand의 입력 routing 증거는 다름 | | Cut 쓰기 실패 후 native 처리 | editable selection에서 지원하는 Cut의 표현 쓰기 실패 → 해당 작업의 원본 제거 없음 | EG-CUT·Web. 제거 callback 미호출 외에 event 취소·후속 beforeinput/input·document·History를 관찰. 브라우저 재현 전 검증 공백 | @@ -612,8 +668,15 @@ profile 동결로 확대하지 않는다. 선택만 바꾼 뒤 Redo하는 후보는 Undo 행과 Redo 행 사이에서 caret을 옮겨 실행한다. 이 선택 이동은 별도 step이 아니며, Redo는 기록된 교체 후 선택을 복원해야 한다. -이 표는 공개 동작에 대한 기대 결과다. 새 테스트의 통과를 주장하지 않으며, -Document의 offset 하나만으로 과거 native range 전체를 복원할 수 있다고 가정하지 않는다. + +이 사례는 기존 공개 entrypoint의 빌드 결과로 실행했다. 정방향·역방향 2가지, +Undo 후 caret 이동 여부 2가지, 직접 호출·기본 keymap의 metaKey·ctrlKey 해석 +3가지를 조합한 12개 실행에서 내용·선택·Undo/Redo 상태가 기대와 일치했다. +키 경로는 `createWebKeyboardAdapter().resolve`의 결과를 editor의 `undo`·`redo`에 +전달했다. 실제 DOM event 수신이나 native range 복원을 검증한 것은 아니다. +재현 코드와 실행 조건은 [이슈의 학습 기록](https://github.com/developer-1px/json-document/issues/725#issuecomment-5577252880)에 보관하고, 영구 suite의 새 테스트로 +집계하지 않는다. Document의 offset 하나로 과거 native range 전체를 복원할 수 +있다고 가정하지 않는다. 기본 Profile의 역할과 Undo·선택 복원의 기본 의미는 설계에 채택한다. 입력 수신, 중첩의 진입·거절·복귀, profile 간 payload 변환과 여러 view의 History 귀속은 From 62decf587e4d0e4a648fb5d3d512f0090821ad09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Tue, 8 Sep 2026 16:40:11 +0900 Subject: [PATCH 05/20] fix(keyboard): consume canonical default chord interpretation --- docs/api-reference/affordance.md | 4 +- docs/api-reference/composer.md | 1 + package-lock.json | 4 +- packages/json-document-affordance/README.md | 11 +++- .../json-document-affordance/src/select.ts | 28 ++++++--- .../tests/affordance.test.ts | 38 ++++++++++++- .../json-document-composer-react/README.md | 5 ++ .../src/use-composer.tsx | 4 +- .../tests/composer-react.test.tsx | 20 ++++++- packages/json-document-composer/README.md | 12 ++++ packages/json-document-composer/package.json | 2 + .../json-document-composer/src/interaction.ts | 15 ++++- .../tests/composer.test.ts | 13 +++++ packages/json-document-composer/tsconfig.json | 1 + packages/json-document-web/README.md | 10 ++++ .../tests/web-adapters.test.ts | 18 ++++++ .../src/shared/demo-workbench/demo-sources.ts | 10 ++++ site/tests/unit/composer-demo.test.tsx | 3 + site/tests/unit/default-keyboard.test.tsx | 57 +++++++++++++++++++ site/tests/unit/demo-workbench.test.tsx | 1 + 20 files changed, 240 insertions(+), 17 deletions(-) create mode 100644 site/tests/unit/default-keyboard.test.tsx diff --git a/docs/api-reference/affordance.md b/docs/api-reference/affordance.md index 370e11f0..a032e743 100644 --- a/docs/api-reference/affordance.md +++ b/docs/api-reference/affordance.md @@ -381,7 +381,7 @@ createViewportPositionSession(options: ViewportPositionOptions): Viewp ## `deleteAffordance` ```ts -deleteAffordance(input: { readonly key?: string; }): AffordancePreview +deleteAffordance(input: Partial): AffordancePreview ``` ## `disclosureAffordance` @@ -781,7 +781,7 @@ resolveAffordanceKey(stroke: WebKeyboardStroke): AffordancePreview ## `selectAllAffordance` ```ts -selectAllAffordance(stroke: Pick, state: { readonly allSelected: boolean; }, options?: { readonly repeat?: "preserve" | "toggle"; }): AffordancePreview +selectAllAffordance(stroke: Pick & Partial>, state: { readonly allSelected: boolean; }, options?: { readonly repeat?: "preserve" | "toggle"; }): AffordancePreview ``` ## `SelectOperation` diff --git a/docs/api-reference/composer.md b/docs/api-reference/composer.md index f182ef1a..da3ad8f4 100644 --- a/docs/api-reference/composer.md +++ b/docs/api-reference/composer.md @@ -144,6 +144,7 @@ interface ComposerKeyStroke { readonly key: string; readonly shiftKey?: boolean; readonly commandKey?: boolean; + readonly altKey?: boolean; } ``` ## `ComposerReference` diff --git a/package-lock.json b/package-lock.json index 0f02d817..3e02b7fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6562,6 +6562,7 @@ "@interactive-os/json-document-rich-text": "*", "@interactive-os/json-document-rich-text-mention": "*", "@interactive-os/json-document-rich-text-suggestion": "*", + "@interactive-os/json-document-web": "*", "@types/node": "^25.9.0", "typescript": "^5.0.0", "vitest": "^4.1.7" @@ -6571,7 +6572,8 @@ "@interactive-os/json-document-file-intake": "^0.1.0-rc.0", "@interactive-os/json-document-rich-text": "^0.1.0-rc.0", "@interactive-os/json-document-rich-text-mention": "^0.1.0-rc.0", - "@interactive-os/json-document-rich-text-suggestion": "^0.1.0-rc.0" + "@interactive-os/json-document-rich-text-suggestion": "^0.1.0-rc.0", + "@interactive-os/json-document-web": "^0.1.0-rc.0" } }, "packages/json-document-composer-react": { diff --git a/packages/json-document-affordance/README.md b/packages/json-document-affordance/README.md index 607d3159..2a569e82 100644 --- a/packages/json-document-affordance/README.md +++ b/packages/json-document-affordance/README.md @@ -94,8 +94,8 @@ open/focus semantics remain outside this geometry contract. Usage: [Affordance](https://developer-1px.github.io/json-document/docs/affordance) `selectAllAffordance(stroke, state, { repeat: "preserve" })` emits `select-all` -for Mod+A even when everything is selected. The default editing Usage chooses -this policy. Omission or `{ repeat: "toggle" }` retains the existing behavior: +for Mod+A without Alt or Shift, even when everything is selected. The default +editing Usage chooses this policy. Omission or `{ repeat: "toggle" }` retains the existing behavior: emit `clear` when `state.allSelected`, otherwise `select-all`. This is an input policy; domain editors own the selected universe and its semantic transition. @@ -105,3 +105,10 @@ Document's `selection.move`. Structural preview and cancellation leave committed value/history unchanged; commit dispatches the latest preview once. This proves the tested composition, not every Host callback. IME composition has a separate [DOM editing lifecycle](../../standards/dom-editing-lifecycle.md) contract. + +`deleteAffordance(stroke)` consumes the Web default structural keymap: bare +Delete/Backspace delete; modified variants return no hand. Omitted modifiers +remain false for existing partial-input calls. Pass the full event to preserve +modifier facts. `selectAllAffordance` uses Web `chordFromStroke` for the same +normalization, then applies its own select-all repetition policy. Text word or +line deletion belongs to the text input adapter. diff --git a/packages/json-document-affordance/src/select.ts b/packages/json-document-affordance/src/select.ts index 879a7e39..89f81725 100644 --- a/packages/json-document-affordance/src/select.ts +++ b/packages/json-document-affordance/src/select.ts @@ -1,4 +1,5 @@ import { + chordFromStroke, createWebKeyboardAdapter, selectionOperationFromModifiers, type WebKeyboardCommand, @@ -88,9 +89,16 @@ export function planeHitAffordance(input: { }; } -export function deleteAffordance(input: { readonly key?: string }): AffordancePreview { - if (input.key === "Delete" || input.key === "Backspace") return { hand: { type: "delete" } }; - return { hand: null }; +/** Uses the default structural delete chord; omitted modifiers are false. */ +export function deleteAffordance(input: Partial): AffordancePreview { + const command = keyboard.resolve({ + key: input.key ?? "", + shiftKey: input.shiftKey ?? false, + metaKey: input.metaKey ?? false, + ctrlKey: input.ctrlKey ?? false, + altKey: input.altKey ?? false, + }); + return { hand: command?.type === "delete" ? command : null }; } export function contextMenuAffordance(input: { @@ -111,14 +119,20 @@ export function resolveAffordanceKey(stroke: WebKeyboardStroke): AffordancePrevi return { hand: keyboard.resolve(stroke) }; } -/** Mod+A selects all. Choose preserve for repeated selection; omission retains the legacy toggle. */ +/** Mod+A without Alt/Shift selects all. Choose preserve for repetition; omission retains the legacy toggle. */ export function selectAllAffordance( - stroke: Pick, + stroke: Pick & Partial>, state: { readonly allSelected: boolean }, options: { readonly repeat?: "preserve" | "toggle" } = {}, ): AffordancePreview { - const mod = stroke.metaKey || stroke.ctrlKey; - if (!mod || stroke.key.toLowerCase() !== "a") return { hand: null }; + const chord = chordFromStroke({ + key: stroke.key, + shiftKey: stroke.shiftKey ?? false, + metaKey: stroke.metaKey, + ctrlKey: stroke.ctrlKey, + altKey: stroke.altKey ?? false, + }); + if (chord !== "Mod-a") return { hand: null }; return { hand: { type: state.allSelected && options.repeat !== "preserve" ? "clear" : "select-all" } }; } diff --git a/packages/json-document-affordance/tests/affordance.test.ts b/packages/json-document-affordance/tests/affordance.test.ts index c87a454f..c626f126 100644 --- a/packages/json-document-affordance/tests/affordance.test.ts +++ b/packages/json-document-affordance/tests/affordance.test.ts @@ -29,6 +29,7 @@ import { panAffordance, resizeAffordance, resolveAffordanceKey, + selectAllAffordance, snapAffordance, treeAffordance, createBoardDragSession, @@ -41,7 +42,7 @@ import { wheelAffordance, zoomAffordance, } from "../src/index.js"; -import { pressInteractionFromWeb } from "@interactive-os/json-document-web"; +import { createWebKeyboardAdapter, pressInteractionFromWeb } from "@interactive-os/json-document-web"; describe("Affordance sessions", () => { test("owns drag, resize, and control handle descriptor, cursor, delta, and lifecycle semantics", () => { @@ -780,3 +781,38 @@ describe("snapAffordance disable key", () => { .toEqual({ type: "translate", dx: 47, dy: 51 }); }); }); + + +describe("default keyboard modifier contract", () => { + const keyboard = createWebKeyboardAdapter(); + for (let mask = 0; mask < 16; mask++) { + const modifiers = { metaKey: !!(mask & 1), ctrlKey: !!(mask & 2), shiftKey: !!(mask & 4), altKey: !!(mask & 8) }; + test(`preserves all modifier facts (${mask})`, () => { + for (const key of ["Delete", "Backspace"]) { + const stroke = { key, ...modifiers }; + expect(deleteAffordance(stroke).hand).toEqual(mask === 0 ? { type: "delete" } : null); + expect(deleteAffordance(stroke).hand).toEqual(keyboard.resolve(stroke)); + } + for (const key of ["z", "Z"]) { + const stroke = { key, ...modifiers }; + expect(resolveAffordanceKey(stroke).hand).toEqual(keyboard.resolve(stroke)); + expect(editingCommandFromWebKeyboardStroke(stroke)).toEqual(keyboard.resolve(stroke)); + } + for (const key of ["a", "A"]) { + const stroke = { key, ...modifiers }; + const selected = (modifiers.metaKey || modifiers.ctrlKey) && !modifiers.shiftKey && !modifiers.altKey; + expect(selectAllAffordance(stroke, { allSelected: true }, { repeat: "preserve" }).hand) + .toEqual(selected ? { type: "select-all" } : null); + expect(selectAllAffordance(stroke, { allSelected: true }).hand) + .toEqual(selected ? { type: "clear" } : null); + } + }); + } + + test("keeps legacy partial inputs without inventing modifiers", () => { + expect(deleteAffordance({ key: "Backspace" }).hand).toEqual({ type: "delete" }); + expect(deleteAffordance({}).hand).toBeNull(); + expect(selectAllAffordance({ key: "a", metaKey: true, ctrlKey: false }, { allSelected: false }).hand) + .toEqual({ type: "select-all" }); + }); +}); diff --git a/packages/json-document-composer-react/README.md b/packages/json-document-composer-react/README.md index cbb26934..bd4fda15 100644 --- a/packages/json-document-composer-react/README.md +++ b/packages/json-document-composer-react/README.md @@ -20,3 +20,8 @@ composed from the canonical suggestion packages. Product copy, styling, layout, suggestions, and concrete ports remain Host-owned. Draft/editor subscription, suggestion integration, keyboard/history execution, Web file intake, focus recovery, and submit lifecycle remain canonical across Host replacements. + +`useComposer` preserves Alt when translating key events to Composer interaction +meaning. Its history capture handler consumes only the default Undo/Redo chord; +Alt-modified variants leave the event and canonical history untouched. Submit +and command-menu handling retain the configured Composer policy. diff --git a/packages/json-document-composer-react/src/use-composer.tsx b/packages/json-document-composer-react/src/use-composer.tsx index 733a3fa1..e80662f3 100644 --- a/packages/json-document-composer-react/src/use-composer.tsx +++ b/packages/json-document-composer-react/src/use-composer.tsx @@ -108,7 +108,7 @@ export function useComposer) { - const interaction = composerInteractionFromKeyStroke({ key: event.key, shiftKey: event.shiftKey, commandKey: event.metaKey || event.ctrlKey }, config.interaction); + const interaction = composerInteractionFromKeyStroke({ key: event.key, shiftKey: event.shiftKey, commandKey: event.metaKey || event.ctrlKey, altKey: event.altKey }, config.interaction); if (commandMenu.open) { commandMenu.handleKeyDown(event); if (event.defaultPrevented) { @@ -123,7 +123,7 @@ export function useComposer) { - const interaction = composerInteractionFromKeyStroke({ key: event.key, shiftKey: event.shiftKey, commandKey: event.metaKey || event.ctrlKey }, config.interaction); + const interaction = composerInteractionFromKeyStroke({ key: event.key, shiftKey: event.shiftKey, commandKey: event.metaKey || event.ctrlKey, altKey: event.altKey }, config.interaction); if (interaction !== "history.undo" && interaction !== "history.redo") return; event.preventDefault(); event.stopPropagation(); diff --git a/packages/json-document-composer-react/tests/composer-react.test.tsx b/packages/json-document-composer-react/tests/composer-react.test.tsx index cb0c06da..dadfaad1 100644 --- a/packages/json-document-composer-react/tests/composer-react.test.tsx +++ b/packages/json-document-composer-react/tests/composer-react.test.tsx @@ -37,6 +37,24 @@ describe("Composer React integration", () => { fireEvent.keyDown(screen.getByTestId("mod-keyboard"), { key: "Enter", ctrlKey: true }); expect(modSubmit).toHaveBeenCalledTimes(1); }); + + test.each(["metaKey", "ctrlKey"] as const)("preserves Alt through the real %s history event path", (modifier) => { + render(); + fireEvent.click(screen.getByRole("button", { name: "history-text" })); + const target = screen.getByTestId("history-keyboard"); + const draft = screen.getByTestId("history-draft"); + const inserted = draft.textContent; + expect(inserted).toContain("hello"); + expect(fireEvent.keyDown(target, { key: "z", [modifier]: true, altKey: true })).toBe(true); + expect(draft.textContent).toBe(inserted); + expect(fireEvent.keyDown(target, { key: "z", [modifier]: true })).toBe(false); + expect(draft.textContent).not.toContain("hello"); + const undone = draft.textContent; + expect(fireEvent.keyDown(target, { key: "Z", [modifier]: true, shiftKey: true, altKey: true })).toBe(true); + expect(draft.textContent).toBe(undone); + expect(fireEvent.keyDown(target, { key: "Z", [modifier]: true, shiftKey: true })).toBe(false); + expect(draft.textContent).toBe(inserted); + }); }); function hostConfig(model: Model, submit: "enter" | "mod-enter"): ComposerHostConfig { @@ -57,7 +75,7 @@ function ComposerHostHarness(props: { readonly host: strin ports: { createId: () => `${props.host}-${++id}`, submit: props.submit }, labels: { mentionSuggestions: `${props.host} mentions`, skillSuggestions: `${props.host} skills` }, }); - return
+ return
{JSON.stringify(composer.document.value)}
; diff --git a/packages/json-document-composer/README.md b/packages/json-document-composer/README.md index 32b8262f..71ae0e79 100644 --- a/packages/json-document-composer/README.md +++ b/packages/json-document-composer/README.md @@ -9,3 +9,15 @@ those validated candidates into Composer context attachments. `resolveComposerSuggestions(trigger, suggestions)` owns trigger-aware matching of a product-configured suggestion catalog. React menu lifecycle and atom projection live in `@interactive-os/json-document-composer-react`. + +`composerInteractionFromKeyStroke(stroke, policy)` preserves the existing +`commandKey` input (Meta or Control) and accepts optional `altKey` alongside +`shiftKey`. Omitted modifiers are false. Its keyboard compatibility boundary +uses `@interactive-os/json-document-web`'s pure default resolver for Undo/Redo: +Mod+Z undoes, Mod+Shift+Z redoes, and Alt-modified variants return `null`. +Composer still owns Escape and the configured Enter submit/newline meaning. +The keyboard dependency is confined to `interaction.ts`; draft model, schema, +and commands do not interpret Web events. No DOM environment is required. + +Usage: [Composer](https://developer-1px.github.io/json-document/demo/composer). +The React integration passes all modifier facts to this boundary. diff --git a/packages/json-document-composer/package.json b/packages/json-document-composer/package.json index b0d1612e..49ec3d43 100644 --- a/packages/json-document-composer/package.json +++ b/packages/json-document-composer/package.json @@ -21,6 +21,7 @@ "verify": "npm run typecheck && npm test && npm run build" }, "peerDependencies": { + "@interactive-os/json-document-web": "^0.1.0-rc.0", "@interactive-os/json-document": "^3.0.0", "@interactive-os/json-document-file-intake": "^0.1.0-rc.0", "@interactive-os/json-document-rich-text-mention": "^0.1.0-rc.0", @@ -28,6 +29,7 @@ "@interactive-os/json-document-rich-text": "^0.1.0-rc.0" }, "devDependencies": { + "@interactive-os/json-document-web": "*", "@interactive-os/json-document": "*", "@interactive-os/json-document-file-intake": "*", "@interactive-os/json-document-rich-text-mention": "*", diff --git a/packages/json-document-composer/src/interaction.ts b/packages/json-document-composer/src/interaction.ts index d37f422e..d1eed3e4 100644 --- a/packages/json-document-composer/src/interaction.ts +++ b/packages/json-document-composer/src/interaction.ts @@ -1,19 +1,32 @@ +import { createWebKeyboardAdapter } from "@interactive-os/json-document-web"; import type { ComposerInteractionPolicy } from "./host-config.js"; +const keyboard = createWebKeyboardAdapter(); + export interface ComposerKeyStroke { readonly key: string; readonly shiftKey?: boolean; readonly commandKey?: boolean; + readonly altKey?: boolean; } export type ComposerInteraction = "dismiss" | "history.redo" | "history.undo" | "newline" | "submit"; +/** Uses the Web default history keymap, then applies Composer submit/newline policy. */ export function composerInteractionFromKeyStroke( stroke: ComposerKeyStroke, policy: ComposerInteractionPolicy, ): ComposerInteraction | null { if (stroke.key === "Escape") return "dismiss"; - if (stroke.commandKey && stroke.key.toLowerCase() === "z") return stroke.shiftKey ? "history.redo" : "history.undo"; + const command = keyboard.resolve({ + key: stroke.key, + shiftKey: stroke.shiftKey ?? false, + metaKey: stroke.commandKey ?? false, + ctrlKey: false, + altKey: stroke.altKey ?? false, + }); + if (command?.type === "undo") return "history.undo"; + if (command?.type === "redo") return "history.redo"; if (stroke.key !== "Enter") return null; const submits = policy.submit === "mod-enter" ? stroke.commandKey === true : stroke.commandKey !== true && stroke.shiftKey !== true; if (submits) return "submit"; diff --git a/packages/json-document-composer/tests/composer.test.ts b/packages/json-document-composer/tests/composer.test.ts index 7594cb17..423beb2e 100644 --- a/packages/json-document-composer/tests/composer.test.ts +++ b/packages/json-document-composer/tests/composer.test.ts @@ -120,3 +120,16 @@ describe("Composer domain", () => { expect(composerInteractionFromKeyStroke({ key: "Escape" }, policy)).toBe("dismiss"); }); }); + + +describe("Composer default history keyboard", () => { + for (const commandKey of [false, true]) for (const shiftKey of [false, true]) for (const altKey of [false, true]) { + test(`history command=${commandKey} shift=${shiftKey} alt=${altKey}`, () => { + for (const key of ["z", "Z"]) { + const stroke = { key, commandKey, shiftKey, altKey }; + expect(composerInteractionFromKeyStroke(stroke, { submit: "enter", newline: "shift-enter" })) + .toBe(commandKey && !altKey ? shiftKey ? "history.redo" : "history.undo" : null); + } + }); + } +}); diff --git a/packages/json-document-composer/tsconfig.json b/packages/json-document-composer/tsconfig.json index 04c4097d..074937d3 100644 --- a/packages/json-document-composer/tsconfig.json +++ b/packages/json-document-composer/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "dist/.tsbuildinfo" }, "references": [ { "path": "../json-document" }, + { "path": "../json-document-web" }, { "path": "../json-document-file-intake" }, { "path": "../json-document-rich-text-mention" }, { "path": "../json-document-rich-text-suggestion" }, diff --git a/packages/json-document-web/README.md b/packages/json-document-web/README.md index ac5dfe8d..1c724bc6 100644 --- a/packages/json-document-web/README.md +++ b/packages/json-document-web/README.md @@ -239,3 +239,13 @@ const range = textSelectionFromControl({ currentTarget: textarea }); [Document Usage](https://developer-1px.github.io/json-document/demo); its source view links the React binding to this package's `input.ts` implementation and [API reference](https://developer-1px.github.io/json-document/docs/api/web). + +Default keyboard interpretation has one owner here. `chordFromStroke` folds +Meta/Control into `Mod`, preserves Alt/Shift, normalizes single-character case, +and maps the space key to `Space`. Unlisted chords resolve to `null`; for +example, Mod+Alt+Z and Mod+Backspace have no default structural command. +`createWebKeyboardAdapter({ keymap, defaults: false })` can explicitly assign +such chords for a product profile. Affordance consumes the default delete +mapping; Composer consumes its Undo/Redo mapping. Select-all remains an +Affordance policy over the canonical chord normalizer, outside +`WebKeyboardCommand`. diff --git a/packages/json-document-web/tests/web-adapters.test.ts b/packages/json-document-web/tests/web-adapters.test.ts index 3b93bd46..4ccf8aa8 100644 --- a/packages/json-document-web/tests/web-adapters.test.ts +++ b/packages/json-document-web/tests/web-adapters.test.ts @@ -646,6 +646,24 @@ describe("Web keyboard Adapter", () => { expect(adapter.resolve({ key: "c", shiftKey: false, metaKey: true, ctrlKey: false })).toBeNull(); }); + test("preserves modifiers while allowing explicit product chord assignments", () => { + const product = createWebKeyboardAdapter({ + defaults: false, + keymap: { "Mod-Alt-z": { type: "undo" }, "Mod-Backspace": { type: "delete" } }, + }); + for (const modifiers of [{ metaKey: true, ctrlKey: false }, { metaKey: false, ctrlKey: true }]) { + const undo = { key: "Z", shiftKey: false, altKey: true, ...modifiers }; + const remove = { key: "Backspace", shiftKey: false, ...modifiers }; + expect(adapter.resolve(undo)).toBeNull(); + expect(adapter.resolve(remove)).toBeNull(); + expect(product.resolve(undo)).toEqual({ type: "undo" }); + expect(product.resolve(remove)).toEqual({ type: "delete" }); + expect(product.resolve({ ...undo, altKey: false })).toBeNull(); + expect(adapter.resolve({ ...undo, altKey: false })).toEqual({ type: "undo" }); + expect(adapter.resolve({ ...undo, altKey: false, shiftKey: true })).toEqual({ type: "redo" }); + } + }); + test("lets the host replace chords without inventing editing commands", () => { const custom = createWebKeyboardAdapter({ keymap: { ...defaultWebKeymap, Enter: { type: "toggle" } }, diff --git a/site/src/shared/demo-workbench/demo-sources.ts b/site/src/shared/demo-workbench/demo-sources.ts index 754b4cc0..5d45974e 100644 --- a/site/src/shared/demo-workbench/demo-sources.ts +++ b/site/src/shared/demo-workbench/demo-sources.ts @@ -501,6 +501,11 @@ const registeredPublicUsages = [ symbol: "createWebKeyboardAdapter", sourcePath: "packages/json-document-web/src/keyboard.ts", }, + { + packageName: "@interactive-os/json-document-web", + symbol: "chordFromStroke", + sourcePath: "packages/json-document-web/src/keyboard.ts", + }, { packageName: "@interactive-os/json-document-web", symbol: "calendarMinutesFromWebGrid", @@ -967,6 +972,11 @@ const registeredPublicUsages = [ symbol: "selectAllAffordance", sourcePath: "packages/json-document-affordance/src/select.ts", }, + { + packageName: "@interactive-os/json-document-affordance", + symbol: "deleteAffordance", + sourcePath: "packages/json-document-affordance/src/select.ts", + }, { packageName: "@interactive-os/json-document-editing", symbol: "createDocumentEditor", diff --git a/site/tests/unit/composer-demo.test.tsx b/site/tests/unit/composer-demo.test.tsx index e3838843..b08708bb 100644 --- a/site/tests/unit/composer-demo.test.tsx +++ b/site/tests/unit/composer-demo.test.tsx @@ -71,6 +71,9 @@ describe("Agent Chat Composer Hands", () => { expect(screen.getByTestId("composer-draft-json").textContent).toContain("요구사항.md"); expect(screen.getByRole("button", { name: "전송 (Enter)" }).hasAttribute("disabled")).toBe(false); + const draftBefore = screen.getByTestId("composer-draft-json").textContent; + expect(fireEvent.keyDown(screen.getByTestId("agent-chat-composer"), { key: "z", metaKey: true, altKey: true })).toBe(true); + expect(screen.getByTestId("composer-draft-json").textContent).toBe(draftBefore); fireEvent.keyDown(screen.getByTestId("agent-chat-composer"), { key: "z", metaKey: true }); expect(screen.queryByText("요구사항.md")).toBeNull(); fireEvent.keyDown(screen.getByTestId("agent-chat-composer"), { key: "z", metaKey: true, shiftKey: true }); diff --git a/site/tests/unit/default-keyboard.test.tsx b/site/tests/unit/default-keyboard.test.tsx new file mode 100644 index 00000000..c3b0bdb9 --- /dev/null +++ b/site/tests/unit/default-keyboard.test.tsx @@ -0,0 +1,57 @@ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, expect, test } from "vitest"; +import { deleteAffordance, selectAllAffordance } from "@interactive-os/json-document-affordance"; +import { CanvasDemoRoute } from "../../src/routes/canvas-demo/CanvasDemoRoute"; +import { DocumentDemoRoute } from "../../src/routes/document-demo/DocumentDemoRoute"; + +afterEach(cleanup); + +test.each(["metaKey", "ctrlKey"] as const)("Canvas preserves %s modifier facts for selection and deletion", (modifier) => { + render(); + const canvas = screen.getByLabelText("Canvas"); + const objects = () => canvas.querySelectorAll("[data-object-id][data-locked]"); + const selected = () => canvas.querySelectorAll('[data-object-id][data-locked][data-selected="true"]'); + const count = objects().length; + expect(count).toBeGreaterThan(1); + const initialSelection = selected().length; + for (const extra of [{ altKey: true }, { shiftKey: true }, { altKey: true, shiftKey: true }]) { + expect(fireEvent.keyDown(canvas, { key: "a", [modifier]: true, ...extra })).toBe(true); + expect(selected().length).toBe(initialSelection); + } + expect(fireEvent.keyDown(canvas, { key: "a", [modifier]: true })).toBe(false); + const selectionCount = selected().length; + expect(selectionCount).toBeGreaterThan(0); + fireEvent.keyDown(canvas, { key: "a", [modifier]: true }); + expect(selected().length).toBe(selectionCount); + expect(fireEvent.keyDown(canvas, { key: "Backspace", [modifier]: true })).toBe(true); + expect(objects().length).toBe(count); + expect(selected().length).toBe(selectionCount); + expect(fireEvent.keyDown(canvas, { key: "Backspace" })).toBe(false); + expect(objects().length).toBe(count - selectionCount); +}); + +test("Document select-all preserves its scope, repetition, and native input ownership", () => { + render(); + const document = screen.getByLabelText("Editable document"); + const surface = document.querySelector('[tabindex="0"]')!; + const selected = () => document.querySelectorAll('article[data-block-id][data-selected="true"]'); + const initial = selected().length; + expect(fireEvent.keyDown(surface, { key: "a", ctrlKey: true, altKey: true })).toBe(true); + expect(selected().length).toBe(initial); + const input = document.querySelector("input, textarea")!; + expect(input).not.toBeNull(); + expect(fireEvent.keyDown(input, { key: "a", ctrlKey: true })).toBe(true); + expect(selected().length).toBe(initial); + expect(fireEvent.keyDown(surface, { key: "a", ctrlKey: true })).toBe(false); + const count = document.querySelectorAll("article[data-block-id]").length; + expect(selected().length).toBe(count); + fireEvent.keyDown(surface, { key: "a", ctrlKey: true }); + expect(selected().length).toBe(count); +}); + +test("native KeyboardEvent properties reach the public Affordance APIs", () => { + expect(deleteAffordance(new KeyboardEvent("keydown", { key: "Backspace", ctrlKey: true })).hand).toBeNull(); + expect(deleteAffordance(new KeyboardEvent("keydown", { key: "Backspace" })).hand).toEqual({ type: "delete" }); + expect(selectAllAffordance(new KeyboardEvent("keydown", { key: "a", metaKey: true, altKey: true }), { allSelected: false }).hand).toBeNull(); + expect(selectAllAffordance(new KeyboardEvent("keydown", { key: "a", metaKey: true }), { allSelected: false }).hand).toEqual({ type: "select-all" }); +}); diff --git a/site/tests/unit/demo-workbench.test.tsx b/site/tests/unit/demo-workbench.test.tsx index 17d30ff5..d4c6b14c 100644 --- a/site/tests/unit/demo-workbench.test.tsx +++ b/site/tests/unit/demo-workbench.test.tsx @@ -200,6 +200,7 @@ describe("Demo definition and source discovery", () => { "packages/json-document-composer/src/commands.ts", "packages/json-document-composer/src/host-config.ts", "packages/json-document-composer/src/interaction.ts", + "packages/json-document-web/src/keyboard.ts", "packages/json-document-rich-text-suggestion/src/index.ts", "packages/json-document-rich-text-suggestion-react/src/index.ts", "packages/json-document-composer-react/src/use-composer.tsx", From 34f5547cdb9b0c1f92ef14378432b32ff9381cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Wed, 9 Sep 2026 09:27:12 +0900 Subject: [PATCH 06/20] =?UTF-8?q?docs(site):=20=EB=AA=A9=ED=91=9C=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84=20=EA=B5=AC=EC=A1=B0=EC=99=80=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=86=A0=EC=BD=9C=C2=B7TBD=EB=A5=BC=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 27 +- docs/README.md | 280 ++++------ docs/api-reference/a2ui.md | 4 +- docs/api-reference/affordance.md | 4 +- docs/api-reference/ajv.md | 4 +- docs/api-reference/animation-react.md | 4 +- docs/api-reference/annotation.md | 4 +- docs/api-reference/calendar.md | 4 +- docs/api-reference/collaboration.md | 179 +++++- docs/api-reference/composer-react.md | 4 +- docs/api-reference/composer.md | 4 +- .../contenteditable-collaboration.md | 4 +- docs/api-reference/contenteditable.md | 4 +- docs/api-reference/database.md | 4 +- docs/api-reference/editing.md | 4 +- docs/api-reference/file-intake.md | 4 +- docs/api-reference/json-document.md | 4 +- docs/api-reference/markdown-react.md | 4 +- docs/api-reference/packages.mjs | 31 +- docs/api-reference/packages.test.mjs | 31 ++ docs/api-reference/react-hook-form.md | 4 +- docs/api-reference/react.md | 4 +- docs/api-reference/rich-text-mention-react.md | 4 +- docs/api-reference/rich-text-mention.md | 4 +- docs/api-reference/rich-text-react.md | 4 +- .../rich-text-suggestion-react.md | 4 +- docs/api-reference/rich-text-suggestion.md | 4 +- docs/api-reference/rich-text-web.md | 4 +- docs/api-reference/rich-text.md | 4 +- docs/api-reference/selection.md | 4 +- docs/api-reference/tanstack-table.md | 4 +- docs/api-reference/ui-primitives-react.md | 4 +- docs/api-reference/web.md | 4 +- docs/api-reference/zod.md | 4 +- docs/evaluate.mjs | 125 ++--- docs/public/api.md | 8 +- docs/public/building-blocks.md | 37 ++ docs/public/concepts.md | 211 +++----- docs/public/document-types.md | 37 +- docs/public/editing.md | 76 +++ docs/public/foundation.md | 50 +- docs/public/how-we-build.md | 7 +- docs/public/llms.txt | 94 +++- docs/public/official-hands.md | 50 +- docs/public/overview.md | 31 +- packages/json-document-editing/README.md | 8 +- packages/json-document-selection/README.md | 15 +- packages/json-document-web/README.md | 35 +- packages/json-document/README.md | 19 +- scripts/generate-api-reference.mjs | 20 +- site/scripts/check-documentation-page.mjs | 4 +- site/scripts/route-checks.mjs | 6 +- site/scripts/route-checks.test.mjs | 26 + site/site-routes.json | 350 ++++++++---- site/src/app/breadcrumb.tsx | 14 +- site/src/app/navigation-layer-icon.tsx | 4 +- site/src/app/page-descriptors.ts | 1 + site/src/app/routeTree.gen.ts | 510 ++++++++++-------- site/src/app/routes/__root.tsx | 19 +- site/src/app/routes/_page/docs/api.tsx | 6 - site/src/app/routes/_page/docs/api/index.tsx | 6 + .../app/routes/_page/docs/building-blocks.tsx | 6 + site/src/app/routes/_page/docs/editing.tsx | 6 + site/src/app/site-layers.ts | 17 +- .../artifact-viewer/ArtifactViewerRoute.tsx | 13 +- site/src/routes/docs/ConceptsRoute.tsx | 41 +- site/src/routes/docs/DocsRoute.tsx | 6 +- .../docs/DocumentTypeCandidateRoute.tsx | 14 +- site/src/routes/docs/DocumentationPage.tsx | 19 +- site/src/routes/docs/MarkdownViewer.tsx | 124 +---- site/src/routes/docs/doc-pages.ts | 321 ++++------- site/src/routes/home/HomeRoute.tsx | 6 +- site/tests/browser/artifact-viewer.spec.ts | 2 +- .../tests/browser/documentation-links.spec.ts | 39 ++ site/tests/browser/site-shell.spec.ts | 27 +- site/tests/unit/api-routes.test.tsx | 27 + site/tests/unit/app-shell.test.tsx | 21 +- site/tests/unit/breadcrumb.test.ts | 8 +- site/tests/unit/docs-route.test.tsx | 30 +- site/tests/unit/documentation-page.test.tsx | 84 +++ standards/json-document-v3/profile.md | 10 +- standards/repository-implementation-shape.md | 6 + standards/repository-naming.md | 53 +- 83 files changed, 1901 insertions(+), 1412 deletions(-) create mode 100644 docs/api-reference/packages.test.mjs create mode 100644 docs/public/building-blocks.md create mode 100644 docs/public/editing.md create mode 100644 site/scripts/route-checks.test.mjs delete mode 100644 site/src/app/routes/_page/docs/api.tsx create mode 100644 site/src/app/routes/_page/docs/api/index.tsx create mode 100644 site/src/app/routes/_page/docs/building-blocks.tsx create mode 100644 site/src/app/routes/_page/docs/editing.tsx create mode 100644 site/tests/browser/documentation-links.spec.ts create mode 100644 site/tests/unit/api-routes.test.tsx create mode 100644 site/tests/unit/documentation-page.test.tsx diff --git a/README.md b/README.md index f0a07610..4b1fdba1 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,14 @@ stateless JSON Patch | 목적 | 위치 | | --- | --- | -| 빠른 사용 예제 | [docs/public/quickstart.md](docs/public/quickstart.md) | +| 빠른 사용 예제 | [Intent guide](docs/public/intent-guide.md) | +| 목표 구조와 TBD | [Concept Map](docs/public/concepts.md), [Foundation](docs/public/foundation.md) | | JSON Document 개념 | [docs/public/overview.md](docs/public/overview.md) | -| JSON Document API | [docs/public/api.md](docs/public/api.md) | +| JSON Document Protocol | [docs/public/api.md](docs/public/api.md) | +| Editing Protocol | [docs/public/editing.md](docs/public/editing.md) | +| Document Types · TBD | [후보와 완료 조건](docs/public/document-types.md) | +| Official Hands · TBD | [Profile의 목표와 현재 증거](docs/public/official-hands.md) | +| Building Blocks | [독립적인 네 책임](docs/public/building-blocks.md) | | 편집 개념 | [docs/public/selection.md](docs/public/selection.md), [history](docs/public/history.md), [clipboard](docs/public/clipboard.md), [topology](docs/public/topology.md) | | Adapter | [docs/public/adapters.md](docs/public/adapters.md) | | Connector | [docs/public/connectors.md](docs/public/connectors.md) | @@ -68,7 +73,10 @@ Editing, Adapter, Connector와 collaboration package는 독립 version과 releas lifecycle을 가집니다. Selection, clipboard, history는 editing companion이 제공하는 headless lifecycle 위에서 도메인별 모델을 조합합니다. 플랫폼 계약은 공식 Adapter가 맡고, external framework와 schema의 반복 glue는 공식 Connector가 -맡으며, persistence와 제품별 UI 의미는 host가 소유합니다. +맡습니다. Host는 조합·실행 순서·제품 정책 값·copy·fixture·layout과 구체 +persistence 인스턴스 주입을 소유합니다. 재사용 모델·연산·투영·UI 행동은 각 +정본 모듈에 둡니다. Document Type 후보와 Official Hands의 전체 Profile은 +아직 TBD이며 기존 package/API의 존재만으로 완료를 선언하지 않습니다. 일반 DOM과 Input Events 정규화가 필요한 제품은 별도 수명 주기의 `@interactive-os/editable`도 검토할 수 있습니다. @@ -89,12 +97,15 @@ optional editing companion이 제공하는 것: - range-set과 set-selection transition family - Document·Order·Sheet·Object·Tree domain slice와 selection-restoring history -편집 제품이 계속 소유하는 것: +Application/Host가 소유하는 것: -- rendering, DOM focus, keyboard, drag/drop UI와 geometry hit-test -- DOM focus, system clipboard와 제품별 interaction policy -- formula engine과 제품별 grid projection 정책 -- product command 이름, layout, route, remote protocol +- 정본 모듈의 조합과 실행 순서 +- 제품의 권한·기본값·copy·fixture·layout·route +- 구체 persistence·network 인스턴스와 제품별 정책 값의 주입 + +문서 의미·formula·grid projection은 해당 의미 owner에, DOM focus·keyboard· +clipboard·geometry 관찰은 Adapter에, 조작 수명주기와 재사용 UI는 Affordance· +Connector·UI Primitives에 둡니다. Core 밖의 책임이 모두 Host 책임은 아닙니다. 공식 Adapter가 제공하는 것: diff --git a/docs/README.md b/docs/README.md index 8bc59c7c..783de0c0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,198 +1,118 @@ # 문서 구조 -이 디렉터리는 외부 사용자에게 공개할 문서 원천만 보관한다. -릴리스 과정, 검토 루프, 과거 판단 기록은 Git issue와 version history에 남기고 -현재 문서 트리에는 복제하지 않는다. +이 디렉터리는 외부 사용자에게 공개할 문서 원천과 생성된 owner API reference를 +보관한다. 릴리스 과정·검토 루프·과거 판단은 Git issue와 version history에 남긴다. -```txt +```text docs -|-- changelog.md # 사용자 영향 중심 변경 기록 -|-- evaluate.mjs # 공개 문서 구조·내용 검증 -|-- public-contract-checks.mjs # 문서 원천·Pages 산출물·live 응답의 공통 공개 계약 검증 -`-- public -| |-- overview.md # JSON Document: Why / How / What -| |-- api.md # JSON Document: 레퍼런스 -| |-- concepts.md # Core에서 Artifact까지의 책임·의존 지도 -| |-- selection.md # Editing: 구조 선택 -| |-- history.md # Editing: 로컬 undo/redo -| |-- clipboard.md # Editing: 구조화된 payload -| |-- topology.md # Editing: 화면 줄과 선택 -| |-- intent.md # Editing: Intent 시그니처 -| |-- intent-guide.md # Editing: Intent 따라 하기 -| |-- official-hands.md # Hands TBD: 완성된 기본 SDK와 확장 경계 -| |-- collaboration.md # JSON Document: 같은 계약의 협업 구현 -| |-- hands.md # Hands: 사람의 편집 도구 -| |-- composer.md # Hands: agent 지시와 구조화된 맥락 -| |-- mention.md # Hands: 안정 entity reference atom -| |-- order.md # Hands: 한 줄 목록 -| |-- object.md # Hands: 키 선택 객체 -| |-- tree.md # Hands: 보이는 나무 -| |-- database.md # Hands: 저장된 표 view -| |-- adapters.md # Adapters: 공식 플랫폼 변환 -| |-- adapter-keyboard.md # Adapter: Keyboard / Press / ARIA -| |-- adapter-grid-cell.md # Adapter: GridPoint / DOM cell address -| |-- adapter-interaction.md # Adapter: Pointer / Drag and Drop session -| |-- adapter-clipboard.md # Adapter: ClipboardEvent -| |-- adapter-contenteditable.md # Adapter: native-input DOM -| |-- connectors.md # Connectors: 공식 라이브러리 생태계 연결 -| |-- connector-react.md # Connector: React 구독 -| |-- connector-react-hook-form.md # Connector: form draft와 commit -| |-- connector-ajv.md # Connector: Ajv validation -| |-- connector-zod.md # Connector: Zod database 변환 -| |-- connector-zod-validate.md # Connector: Zod validation -| |-- connector-tanstack-table.md # Connector: visible table topology -| |-- react-editing.md # Connectors: React 선택·커서 질의 -| `-- llms.txt # machine-readable 공개 문서 +├─ public/ # 한국어 개념·계약·사용법, llms.txt +├─ api-reference/ # owner package별 생성 reference와 등록표 +├─ changelog.md # 사용자 영향 중심 변경 기록 +├─ evaluate.mjs # 문서·등록·증거 연결 검사 +└─ public-contract-checks.mjs # 원천·Pages·live의 공통 공개 계약 검사 ``` -사이트의 문서 탐색은 공개 컨셉 트리를 사용한다. 파일은 `public/`의 평평한 책임 -폴더에 유지하고, 별도 중첩 폴더를 개념으로 추가하지 않는다. - -```txt -JSON Document -|-- Why -|-- Concept Map -`-- API - -Editing -|-- Intent guide -|-- Intent -|-- Topology -|-- Selection -|-- Clipboard -`-- History - -Adapter -|-- Overview -|-- Keyboard -|-- Clipboard -`-- Contenteditable - -Connector -|-- Overview -|-- React -| `-- React editing -|-- React Hook Form -|-- Ajv -|-- Zod -| `-- Validate -`-- TanStack Table - -Affordance -|-- Focus -|-- Caret -|-- Select -|-- Typeahead -|-- Activate -|-- Escape -|-- Expand/Collapse -|-- Undo -|-- Delete -|-- Rename -|-- Nudge -|-- Hover -|-- Double-click -|-- Triple-click -|-- Context menu -|-- Drag -|-- Marquee -|-- Drop -|-- Duplicate -|-- Resize -|-- Pan -|-- Scroll -|-- Zoom -|-- Snap -`-- Not-allowed - +## 사이트의 읽기 구조 + +탐색 섹션은 `site/src/app/site-layers.ts`, 페이지 제목·URL·문서 원천의 연결은 +`site/site-routes.json`의 `documentSource`가 소유한다. `doc-pages.ts`는 그 원천을 +읽고, Markdown 링크도 같은 등록표에서 사이트 URL을 찾는다. 별도 파일명→URL +카탈로그를 유지하지 않는다. 아래는 개념 수준의 지도이며 leaf 페이지 목록을 +복제한 탐색 정본이 아니다. + +```text +Introduction +├─ Why +├─ Concept Map +└─ How We Build +Foundation +├─ Overview +├─ JSON Document Protocol +├─ Document Types · TBD +│ └─ 후보별 관찰된 schema·목표 owner·완료 증거 +├─ Editing Protocol +│ └─ Intent · Topology · Selection · Clipboard · History +└─ Collaboration + └─ Replica · Lifecycle · History · Text +Building Blocks +├─ Overview +├─ Adapter +├─ Connector +├─ Affordance +└─ UI Primitives Hands -|-- Overview -|-- Official Hands (TBD) -|-- Order -|-- Object -|-- Tree -|-- Database -|-- Composer -`-- Mention - ----------------------------------------- - -Collaboration -|-- Replica -|-- Lifecycle -|-- Collaborative History -`-- Text - `-- native-input DOM lease +└─ 현재 Usage와 Official Hands Profile · TBD +Artifact +└─ Content Prototype · TBD +Applications +└─ 제품 조합과 제품에서 발견한 책임 ``` +읽기 순서는 필수 package dependency chain이 아니다. Collaboration은 같은 +JSONDocument의 대체 구현이고 Adapter와 Connector는 독립적으로 선택한다. +API reference는 각 owner package의 위치에 유지한다. 탐색 분류는 owner의 +책임 종류나 새 runtime 계층이 아니다. Core 안내를 전체 package catalog처럼 +별도의 Reference 섹션에 중복 노출하지 않는다. + ## 규범 우선순위 Repository 전체의 개념과 이름 정본은 `standards/repository-naming.md`, package 내부 책임 배치 정본은 `standards/repository-implementation-shape.md`, browser event부터 model -reconciliation까지의 DOM 편집 정본은 `standards/dom-editing-lifecycle.md`입니다. +reconciliation까지의 DOM 편집 정본은 `standards/dom-editing-lifecycle.md`다. 현재 v3 portable root의 compatibility 정본은 `standards/json-document-v3/profile.md`, -`standards/json-document-v3/public-surface.json`, 그리고 profile이 -지정한 conformance vector와 language binding입니다. 이름 정본은 stable v3 -identifier나 동작을 바꾸지 않으며, 과거 version 문서는 정본 public surface의 -Root symbol·six-member 계약을 확장하지 않습니다. - -EditingSession의 확정된 공통 의미는 `standards/editing-session.md`가 소유합니다. -ES 규칙은 공통 의미를 규정하고 현재 TypeScript binding·local History 정책은 별도 -표에서 구별합니다. callback 형태·재시도 시점·구독 전략을 보편 조건으로 굳히지 -않습니다. 각 규칙을 owner의 행동 테스트에 연결합니다. `docs:evaluate`는 그 증거 연결을 검사하고 package test가 -실제 행동을 검증합니다. 이 확정은 전체 Hands의 Stable 선언을 뜻하지 않습니다. - -편집 문법의 안정화 설계는 `standards/editing-grammar.md`에 있습니다. 공통 편집 -규칙, Hands profile의 선택, 입력 매핑의 소유자와 적합성 증거를 연결하는 Design -Draft이며 기존 Stable profile의 권위를 변경하지 않습니다. API reference와 Usage는 -각 owner에 유지하고, 설계 문서를 별도의 API catalog로 사용하지 않습니다. - -문서 원천, Pages 산출물, live 응답의 공개 계약 검사는 -`public-contract-checks.mjs`가 소유합니다. Root symbol 수는 Core의 -`public-contract.json`, 유효한 package 참조는 `api-reference/packages.mjs`에서 -읽습니다. 각 evaluator의 파일·HTTP 읽기와 재시도 정책은 그대로 유지합니다. - -## 책임 기준 - -| 위치 | 책임 | 독자 | -| --- | --- | --- | -| `changelog.md` | 사용자 영향 중심 변경 기록 | 외부 사용자, 릴리스 확인자 | -| `public/` | 사용법과 프로젝트 이해를 위한 공식 문서 원천 | 외부 사용자, LLM, 사이트 방문자 | +`standards/json-document-v3/public-surface.json`, 지정된 conformance vector와 +language binding이다. 이름 정본은 Stable v3 identifier나 동작을 바꾸지 않는다. + +EditingSession의 확정된 공통 의미는 `standards/editing-session.md`가 소유한다. +ES 규칙과 현재 TypeScript binding·local History 정책은 구별하며 각 규칙을 +owner의 행동 테스트에 연결한다. 이 확정은 전체 Hands의 Stable 선언이 아니다. + +편집 문법의 안정화 설계는 `standards/editing-grammar.md`의 Design Draft다. +공통 규칙·Profile 선택·입력 owner·적합성 증거를 연결하지만 기존 Stable +계약의 권위를 변경하지 않는다. API reference와 Usage를 대체하지 않는다. + +## 현재 계약과 TBD + +- Core v3 Stable, EditingSession 공통 의미, 개별 package API의 범위를 구분한다. +- Document Type은 책임 이름과 경계가 정해졌더라도 후보별 owner 수렴이 남으면 + `TBD`를 유지한다. schema나 API가 존재한다는 이유만으로 완료 처리하지 않는다. +- Official Hands는 지원 입력·실패·선택 복원·History와 조합 적합성의 미확정 + 경계 및 완료 증거를 표시한다. +- Artifact visual prototype은 시각 가설의 증거다. 실제 문서·Hands 연결이나 + 파일 호환성이 없는 경우 제목·설명에서 `TBD`로 드러낸다. +- TBD는 막연한 미래 목록이 아니라 현재 증거, 목표 책임, 남은 완료 조건을 담는다. + +## 책임과 검증 + +| 원천 | 책임 | +| --- | --- | +| `public/` | 외부 사용자와 사이트 방문자의 개념·계약·Usage | +| `public/llms.txt` | 같은 목표와 현재 계약을 요약한 기계 판독 문서 | +| `api-reference/packages.mjs` | owner package의 source entrypoint와 사이트 탐색 분류 | +| `api-reference/*.md` | package root와 공개 subpath에서 생성한 API reference | +| `changelog.md` | 사용자 영향 중심 변경 기록 | + +`package.json#exports`의 TypeScript 진입점과 API 등록을 비교해 subpath 누락을 +검출한다. `docs:evaluate`는 원천 등록과 상대 파일 링크, ES 규칙의 행동 증거 +연결을 확인한다. API 생성 검사는 등록된 파일의 self-consistency만으로 끝내지 않는다. + +문서의 목차는 실제 렌더러가 만든 heading과 ID에서 읽는다. site 테스트는 실제 +본문 링크와 목차 대상의 존재, 목표 탐색·제목·TBD 경로를 검증한다. +문자열이나 링크의 존재는 의미 적합성을 대신하지 않으며 package/browser 검증을 +함께 봐야 한다. + +문서 원천·Pages·live의 공통 계약은 `public-contract-checks.mjs`가 소유한다. +Root symbol 수는 Core `public-contract.json`에서 읽는다. ## 작성 원칙 -- 본문은 한글로 쓴다. -- 코드 식별자, 명령어, 파일 경로, 표준명은 원문을 유지한다. -- public 문서는 usage와 프로젝트 이해만 다룬다. -- 페이지마다 할 일 하나만 쓴다. overview는 Why/How/What 배경, - api는 시그니처 레퍼런스, selection은 구조 - 선택, history는 로컬 undo/redo, clipboard는 구조화된 payload, - topology는 화면 줄과 선택, intent는 편집 Intent 시그니처, - intent-guide는 Intent 따라 하기, adapters는 플랫폼 변환, - connectors는 연결 방법이다. react-editing은 React에서 선택과 커서를 - 그리는 사용법이다. - concepts는 JSON Document에서 Artifact까지의 책임과 의존 지도다. - official-hands는 디자인과 제품 데이터는 열어 두고 수렴된 편집 기능을 - 완성된 SDK로 제공하는 TBD 관점이다. - collaboration은 같은 JSON Document의 다른 구현이다. hands는 - Editing 위 장르의 손이다. order·object·tree는 그 손의 - 나머지 slice다. database는 저장된 표 view의 손이다. - composer·mention은 Rich Text와 구조화된 context로 구현한 손이다. App 이름과 - 출력 표현은 Hands로 올리지 않는다. -- 배경 문서는 왜 만들었는지부터 쓴다. 컨셉 페이지는 그 아이디어를 - 한 문서 위에서 만져보게 한다. 레퍼런스는 호출과 계약부터 쓴다. -- 새 개념은 독자가 그 개념을 필요로 하는 상황을 본 뒤에 이름 붙인다. -- 초안을 쓴 뒤 선언 전 사용, 선제 부정, 메타 안내와 중복을 제거하고 - 앞뒤 페이지를 이어 읽는다. -- 무엇을 하는지로 정의한다. 소유하지 않는 것의 목록으로 시작하지 않는다. -- 같은 아키텍처 다이어그램과 패키지 카탈로그를 페이지마다 복제하지 않는다. -- 이름·구현 모양 정본과 profile은 `standards/`에 두고 public 가이드에서 인용하지 - 않는다. -- 릴리스 history, 검토 loop, maintainer-only gate는 public 문서에 쓰지 않는다. -- 내부 구현 경로는 public 문서에 쓰지 않는다. -- 새 문서는 기존 책임 폴더 중 하나에 들어가야 한다. -- 새 책임 폴더가 필요하면 먼저 이 파일의 책임 표를 갱신한다. -- 새 public concept와 이름은 - `standards/repository-naming.md`의 admission과 문법을 먼저 - 통과해야 한다. +- 본문은 한글로 쓰고 코드 식별자·명령어·경로·표준명은 원문을 유지한다. +- 페이지마다 책임 하나를 설명하고 인접 owner의 설명과 시그니처는 연결한다. +- 새로운 개념은 책임과 필요성이 드러난 뒤 이름 붙인다. +- 같은 아키텍처 다이어그램과 package 목록을 여러 문서에 복제하지 않는다. +- 표준·명명·구현 배치 정본은 `standards/`에 두며 public 가이드는 사용자 계약과 + 사용법을 설명한다. 내부 경로·검토 루프·maintainer-only gate를 본문에 노출하지 않는다. +- 재사용 모델·연산·입력 해석·projection·UI를 Host 책임으로 설명하지 않는다. + Host는 조합·실행 순서·제품 정책 값·copy·fixture·layout·구체 인스턴스를 소유한다. +- 새 public 개념과 이름은 정본 naming admission을 먼저 통과해야 한다. diff --git a/docs/api-reference/a2ui.md b/docs/api-reference/a2ui.md index c3b66a66..b7a753a8 100644 --- a/docs/api-reference/a2ui.md +++ b/docs/api-reference/a2ui.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-a2ui API -**Owner:** Connector +**탐색 분류:** Connector -A2UI streaming document connector의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +A2UI streaming document connector의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-a2ui/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/affordance.md b/docs/api-reference/affordance.md index 370e11f0..eecd6fb8 100644 --- a/docs/api-reference/affordance.md +++ b/docs/api-reference/affordance.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-affordance API -**Owner:** Affordance +**탐색 분류:** Affordance -입력 문법과 interaction session의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +입력 문법과 interaction session의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-affordance/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/ajv.md b/docs/api-reference/ajv.md index 5d288f97..2a4af271 100644 --- a/docs/api-reference/ajv.md +++ b/docs/api-reference/ajv.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-ajv API -**Owner:** Connector +**탐색 분류:** Connector -Ajv validation connector의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Ajv validation connector의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-ajv/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/animation-react.md b/docs/api-reference/animation-react.md index eb0951e2..5073bcd1 100644 --- a/docs/api-reference/animation-react.md +++ b/docs/api-reference/animation-react.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-animation-react API -**Owner:** UI Primitives +**탐색 분류:** UI Primitives -생성 대기 시각 언어의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +생성 대기 시각 언어의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-animation-react/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/annotation.md b/docs/api-reference/annotation.md index c7e6c8a6..4e8cfed6 100644 --- a/docs/api-reference/annotation.md +++ b/docs/api-reference/annotation.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-annotation API -**Owner:** Hands +**탐색 분류:** Hands -Annotation Hand interaction과 SVG projection의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Annotation interaction과 SVG projection의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-annotation/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/calendar.md b/docs/api-reference/calendar.md index ddd6773c..3699c5c0 100644 --- a/docs/api-reference/calendar.md +++ b/docs/api-reference/calendar.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-calendar API -**Owner:** Hands +**탐색 분류:** Hands -Calendar React lifecycle와 occurrence interaction 계약의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Calendar React lifecycle와 occurrence interaction 계약의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-calendar/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/collaboration.md b/docs/api-reference/collaboration.md index 42a5ce0c..09f7b84a 100644 --- a/docs/api-reference/collaboration.md +++ b/docs/api-reference/collaboration.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-collaboration API -**Owner:** Collaboration +**탐색 분류:** Collaboration -replica, history, text collaboration runtime의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +replica, history, text collaboration runtime의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-collaboration/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. @@ -504,6 +504,181 @@ interface HistoryStatus { ```ts restoreHistoryRuntime(input: unknown, options: CollaborationRestoreOptions): HistoryRestoreResult ``` +## `@interactive-os/json-document-collaboration/text` + +아래 API는 package root가 아닌 이 subpath에서 import합니다. +### `createTextRuntime` + +```ts +createTextRuntime(initial: unknown, options: CollaborationRuntimeOptions): TextRuntime +``` +### `History` + +```ts +interface History { + status(): HistoryStatus; + canUndo(): JSONPatchValidationResult; + undo(): HistoryResult; + canRedo(): JSONPatchValidationResult; + redo(): HistoryResult; +} +``` +### `HistoryResult` + +```ts +type HistoryResult = + | { + readonly ok: true; + readonly changeId: ChangeId; + readonly target: ChangeId; + readonly didChangeDocument: boolean; + /** This operation's applied change; null when it only changes causal history. */ + readonly change: JSONAppliedChange | null; + /** Captured before subscribers can author a later transition. */ + readonly status: HistoryStatus & { + readonly canUndo: boolean; + readonly canRedo: boolean; + }; + } + | { + readonly ok: false; + readonly code: string; + readonly reason?: string; + }; +``` +### `HistoryStatus` + +```ts +interface HistoryStatus { + readonly undoTarget: ChangeId | null; + readonly redoTarget: ChangeId | null; + readonly undoDepth: number; + readonly redoDepth: number; + readonly revision: number; +} +``` +### `restoreTextRuntime` + +```ts +restoreTextRuntime(input: unknown, options: CollaborationRestoreOptions): TextRestoreResult +``` +### `Text` + +```ts +interface Text { + capture(pointer: string): TextCaptureResult; + plan( + capture: TextCapture, + observation: TextObservation, + ): TextPlanResult; + commit( + plan: TextPlan, + options?: JSONDocumentCommitOptions, + ): TextCommitResult; +} +``` +### `TextCapture` + +```ts +interface TextCapture { + readonly pointer: string; + readonly target: MemberId; + readonly textNode: TextNodeId; + readonly value: string; +} +``` +### `TextCaptureResult` + +```ts +type TextCaptureResult = + | { + readonly ok: true; + readonly capture: TextCapture; + } + | { + readonly ok: false; + readonly code: string; + readonly reason: string; + }; +``` +### `TextCommitResult` + +```ts +type TextCommitResult = + | { + readonly ok: true; + readonly change: JSONAppliedChange; + readonly changeId: ChangeId | null; + readonly didChangeDocument: boolean; + readonly value: string; + readonly selection: TextSelection | null; + } + | { + readonly ok: false; + readonly code: string; + readonly reason: string; + }; +``` +### `TextObservation` + +```ts +interface TextObservation { + readonly value: string; + readonly selection?: TextSelection; +} +``` +### `TextPlan` + +```ts +interface TextPlan { + readonly pointer: string; + readonly value: string; + readonly selection?: TextSelection; +} +``` +### `TextPlanResult` + +```ts +type TextPlanResult = + | { + readonly ok: true; + readonly plan: TextPlan; + } + | { + readonly ok: false; + readonly code: string; + readonly reason: string; + }; +``` +### `TextRestoreResult` + +```ts +type TextRestoreResult = + | { + readonly ok: true; + readonly runtime: TextRuntime; + } + | { + readonly ok: false; + readonly code: string; + readonly reason: string; + }; +``` +### `TextRuntime` + +```ts +interface TextRuntime extends HistoryRuntime { + readonly text: Text; +} +``` +### `TextSelection` + +```ts +interface TextSelection { + readonly anchor: number; + readonly focus: number; +} +``` ## `@interactive-os/json-document-collaboration/editing` 아래 API는 package root가 아닌 이 subpath에서 import합니다. diff --git a/docs/api-reference/composer-react.md b/docs/api-reference/composer-react.md index 61f51535..e851ae2f 100644 --- a/docs/api-reference/composer-react.md +++ b/docs/api-reference/composer-react.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-composer-react API -**Owner:** Hands +**탐색 분류:** Hands -Composer React interaction과 reference projection의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Composer React interaction과 reference projection의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-composer-react/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/composer.md b/docs/api-reference/composer.md index f182ef1a..8db9ec02 100644 --- a/docs/api-reference/composer.md +++ b/docs/api-reference/composer.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-composer API -**Owner:** Hands +**탐색 분류:** Hands -Composer draft와 reference/trigger command 계약의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Composer draft와 reference/trigger command 계약의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-composer/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/contenteditable-collaboration.md b/docs/api-reference/contenteditable-collaboration.md index 7dc1a6da..563aa0ee 100644 --- a/docs/api-reference/contenteditable-collaboration.md +++ b/docs/api-reference/contenteditable-collaboration.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-contenteditable-collaboration API -**Owner:** Collaboration +**탐색 분류:** Collaboration -collaborative contenteditable lease의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +collaborative contenteditable lease의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/contenteditable-collaboration/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/contenteditable.md b/docs/api-reference/contenteditable.md index bdae70d6..221c9a7d 100644 --- a/docs/api-reference/contenteditable.md +++ b/docs/api-reference/contenteditable.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-contenteditable API -**Owner:** Adapter +**탐색 분류:** Adapter -contenteditable platform adapter의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +contenteditable platform adapter의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-contenteditable/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/database.md b/docs/api-reference/database.md index b4e8fa55..c3bca26c 100644 --- a/docs/api-reference/database.md +++ b/docs/api-reference/database.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-database API -**Owner:** Hands +**탐색 분류:** Hands -Database Hand domain 계약의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Database 문서 모델·연산·saved-view projection의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-database/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/editing.md b/docs/api-reference/editing.md index 2137c922..e14f160d 100644 --- a/docs/api-reference/editing.md +++ b/docs/api-reference/editing.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-editing API -**Owner:** Editing +**탐색 분류:** Editing -intent, editor, history 편집 계약의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +intent, editor, history 편집 계약의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-editing/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/file-intake.md b/docs/api-reference/file-intake.md index 19b5174d..6c2e915d 100644 --- a/docs/api-reference/file-intake.md +++ b/docs/api-reference/file-intake.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-file-intake API -**Owner:** Artifact +**탐색 분류:** Artifact -플랫폼 독립 파일 후보와 수용 정책의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +플랫폼 독립 파일 후보와 수용 정책의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-file-intake/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/json-document.md b/docs/api-reference/json-document.md index fb35e112..64b44fa0 100644 --- a/docs/api-reference/json-document.md +++ b/docs/api-reference/json-document.md @@ -1,8 +1,8 @@ # @interactive-os/json-document API -**Owner:** JSON Document +**탐색 분류:** JSON Document -Core document 값·주소·patch 계약의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Core document 값·주소·patch 계약의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document/src/application/document/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/markdown-react.md b/docs/api-reference/markdown-react.md index 40fb9626..19f68f06 100644 --- a/docs/api-reference/markdown-react.md +++ b/docs/api-reference/markdown-react.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-markdown-react API -**Owner:** Artifact +**탐색 분류:** Artifact -스트리밍 Markdown 투영과 렌더링의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +스트리밍 Markdown 투영과 렌더링의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-markdown-react/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/packages.mjs b/docs/api-reference/packages.mjs index 0ba9cdfe..f78a08b7 100644 --- a/docs/api-reference/packages.mjs +++ b/docs/api-reference/packages.mjs @@ -12,12 +12,12 @@ export const apiReferencePackages = [ ["ui-primitives-react", "@interactive-os/json-document-ui-primitives-react", "packages/json-document-ui-primitives-react/src/index.ts", "UI Primitives", "표준 React UI primitive"], ["animation-react", "@interactive-os/json-document-animation-react", "packages/json-document-animation-react/src/index.ts", "UI Primitives", "생성 대기 시각 언어"], ["markdown-react", "@interactive-os/json-document-markdown-react", "packages/json-document-markdown-react/src/index.ts", "Artifact", "스트리밍 Markdown 투영과 렌더링"], - ["database", "@interactive-os/json-document-database", "packages/json-document-database/src/index.ts", "Hands", "Database Hand domain 계약"], - ["annotation", "@interactive-os/json-document-annotation", "packages/json-document-annotation/src/index.ts", "Hands", "Annotation Hand interaction과 SVG projection"], + ["database", "@interactive-os/json-document-database", "packages/json-document-database/src/index.ts", "Hands", "Database 문서 모델·연산·saved-view projection"], + ["annotation", "@interactive-os/json-document-annotation", "packages/json-document-annotation/src/index.ts", "Hands", "Annotation interaction과 SVG projection"], ["calendar", "@interactive-os/json-document-calendar", "packages/json-document-calendar/src/index.ts", "Hands", "Calendar React lifecycle와 occurrence interaction 계약"], ["web", "@interactive-os/json-document-web", "packages/json-document-web/src/index.ts", "Adapter", "Web platform adapter"], ["contenteditable", "@interactive-os/json-document-contenteditable", "packages/json-document-contenteditable/src/index.ts", "Adapter", "contenteditable platform adapter"], - ["rich-text", "@interactive-os/json-document-rich-text", "packages/json-document-rich-text/src/index.ts", "Editing", "Rich Text domain과 editing 계약"], + ["rich-text", "@interactive-os/json-document-rich-text", "packages/json-document-rich-text/src/index.ts", "Editing", "Rich Text 문서 의미와 editing 계약"], ["file-intake", "@interactive-os/json-document-file-intake", "packages/json-document-file-intake/src/index.ts", "Artifact", "플랫폼 독립 파일 후보와 수용 정책"], ["rich-text-suggestion", "@interactive-os/json-document-rich-text-suggestion", "packages/json-document-rich-text-suggestion/src/index.ts", "Hands", "Rich Text suggestion trigger와 상태 계약"], ["rich-text-suggestion-react", "@interactive-os/json-document-rich-text-suggestion-react", "packages/json-document-rich-text-suggestion-react/src/index.ts", "Hands", "Rich Text suggestion React interaction binding"], @@ -29,13 +29,34 @@ export const apiReferencePackages = [ ["rich-text-react", "@interactive-os/json-document-rich-text-react", "packages/json-document-rich-text-react/src/index.tsx", "Connector", "Rich Text React connector"], ["collaboration", "@interactive-os/json-document-collaboration", "packages/json-document-collaboration/src/index.ts", "Collaboration", "replica, history, text collaboration runtime"], ["contenteditable-collaboration", "@interactive-os/json-document-contenteditable-collaboration", "packages/contenteditable-collaboration/src/index.ts", "Collaboration", "collaborative contenteditable lease"], -].map(([slug, packageName, entrypoint, owner, responsibility]) => ({ - slug, packageName, entrypoint, owner, responsibility, +].map(([slug, packageName, entrypoint, navigationGroup, responsibility]) => ({ + slug, packageName, entrypoint, navigationGroup, responsibility, subpaths: slug === "collaboration" ? [{ packageName: "@interactive-os/json-document-collaboration/history", entrypoint: "packages/json-document-collaboration/src/history-index.ts", + }, { + packageName: "@interactive-os/json-document-collaboration/text", + entrypoint: "packages/json-document-collaboration/src/text-index.ts", }, { packageName: "@interactive-os/json-document-collaboration/editing", entrypoint: "packages/json-document-collaboration/src/editing-index.ts", }] : [], })); + +export function apiReferenceCoverageErrors(manifests, references = apiReferencePackages) { + const expected = manifests.filter((manifest) => !manifest.private).flatMap((manifest) => + Object.entries(manifest.exports).filter(([, target]) => hasTypes(target)) + .map(([subpath]) => subpath === "." ? manifest.name : `${manifest.name}/${subpath.slice(2)}`)); + const registered = references.flatMap(({ packageName, subpaths }) => + [packageName, ...subpaths.map((subpath) => subpath.packageName)]); + return [ + ...expected.filter((name) => !registered.includes(name)).map((name) => `API reference missing: ${name}`), + ...registered.filter((name) => !expected.includes(name)).map((name) => `API reference is not public: ${name}`), + ...registered.filter((name, index) => registered.indexOf(name) !== index).map((name) => `Duplicate API reference: ${name}`), + ]; +} + +function hasTypes(target) { + return target !== null && typeof target === "object" + && (typeof target.types === "string" || Object.values(target).some(hasTypes)); +} diff --git a/docs/api-reference/packages.test.mjs b/docs/api-reference/packages.test.mjs new file mode 100644 index 00000000..667998ee --- /dev/null +++ b/docs/api-reference/packages.test.mjs @@ -0,0 +1,31 @@ +import { readFileSync } from "node:fs"; +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { apiReferenceCoverageErrors, apiReferencePackages } from "./packages.mjs"; + +const root = new URL("../../", import.meta.url); +const read = (path) => JSON.parse(readFileSync(new URL(path, root), "utf8")); +const manifests = read("package.json").workspaces.map((workspace) => read(`${workspace}/package.json`)); + +test("every published TypeScript entrypoint has its owner reference", () => { + assert.deepEqual(apiReferenceCoverageErrors(manifests), []); +}); + +test("detects the omitted collaboration text subpath even when every package root is registered", () => { + const missingText = apiReferencePackages.map((entry) => ({ + ...entry, + subpaths: entry.subpaths.filter((subpath) => !subpath.packageName.endsWith("/text")), + })); + assert.deepEqual(apiReferenceCoverageErrors(manifests, missingText), [ + "API reference missing: @interactive-os/json-document-collaboration/text", + ]); +}); + +test("does not mistake CSS exports for TypeScript contracts and rejects stale entries", () => { + const entry = { packageName: "example", subpaths: [] }; + const published = [{ name: "example", exports: { ".": { types: "./index.d.ts" }, "./styles.css": "./styles.css" } }]; + assert.deepEqual(apiReferenceCoverageErrors(published, [entry]), []); + assert.deepEqual(apiReferenceCoverageErrors(published, [{ ...entry, subpaths: [{ packageName: "example/removed" }] }]), [ + "API reference is not public: example/removed", + ]); +}); diff --git a/docs/api-reference/react-hook-form.md b/docs/api-reference/react-hook-form.md index aff21a23..b7db9efd 100644 --- a/docs/api-reference/react-hook-form.md +++ b/docs/api-reference/react-hook-form.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-react-hook-form API -**Owner:** Connector +**탐색 분류:** Connector -React Hook Form connector의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +React Hook Form connector의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-react-hook-form/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/react.md b/docs/api-reference/react.md index 36a8d8ab..d84fb355 100644 --- a/docs/api-reference/react.md +++ b/docs/api-reference/react.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-react API -**Owner:** Connector +**탐색 분류:** Connector -React lifecycle connector의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +React lifecycle connector의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-react/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/rich-text-mention-react.md b/docs/api-reference/rich-text-mention-react.md index 8ca8f59a..62754f1f 100644 --- a/docs/api-reference/rich-text-mention-react.md +++ b/docs/api-reference/rich-text-mention-react.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-rich-text-mention-react API -**Owner:** Hands +**탐색 분류:** Hands -Rich Text mention React projection의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Rich Text mention React projection의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-rich-text-mention-react/src/index.tsx`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/rich-text-mention.md b/docs/api-reference/rich-text-mention.md index ac0c6b28..349a4652 100644 --- a/docs/api-reference/rich-text-mention.md +++ b/docs/api-reference/rich-text-mention.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-rich-text-mention API -**Owner:** Hands +**탐색 분류:** Hands -Rich Text entity mention schema와 삽입 계약의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Rich Text entity mention schema와 삽입 계약의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-rich-text-mention/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/rich-text-react.md b/docs/api-reference/rich-text-react.md index 6c9acf7b..9f06ea9a 100644 --- a/docs/api-reference/rich-text-react.md +++ b/docs/api-reference/rich-text-react.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-rich-text-react API -**Owner:** Connector +**탐색 분류:** Connector -Rich Text React connector의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Rich Text React connector의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-rich-text-react/src/index.tsx`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/rich-text-suggestion-react.md b/docs/api-reference/rich-text-suggestion-react.md index 061ca76b..6f40279a 100644 --- a/docs/api-reference/rich-text-suggestion-react.md +++ b/docs/api-reference/rich-text-suggestion-react.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-rich-text-suggestion-react API -**Owner:** Hands +**탐색 분류:** Hands -Rich Text suggestion React interaction binding의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Rich Text suggestion React interaction binding의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-rich-text-suggestion-react/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/rich-text-suggestion.md b/docs/api-reference/rich-text-suggestion.md index d771a4fb..b70154e5 100644 --- a/docs/api-reference/rich-text-suggestion.md +++ b/docs/api-reference/rich-text-suggestion.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-rich-text-suggestion API -**Owner:** Hands +**탐색 분류:** Hands -Rich Text suggestion trigger와 상태 계약의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Rich Text suggestion trigger와 상태 계약의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-rich-text-suggestion/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/rich-text-web.md b/docs/api-reference/rich-text-web.md index 37f35bb0..9311c44f 100644 --- a/docs/api-reference/rich-text-web.md +++ b/docs/api-reference/rich-text-web.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-rich-text-web API -**Owner:** Adapter +**탐색 분류:** Adapter -Rich Text DOM adapter의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Rich Text DOM adapter의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-rich-text-web/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/rich-text.md b/docs/api-reference/rich-text.md index 48247c54..6f648e3a 100644 --- a/docs/api-reference/rich-text.md +++ b/docs/api-reference/rich-text.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-rich-text API -**Owner:** Editing +**탐색 분류:** Editing -Rich Text domain과 editing 계약의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Rich Text 문서 의미와 editing 계약의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-rich-text/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/selection.md b/docs/api-reference/selection.md index 8e2a7e11..05dad5d3 100644 --- a/docs/api-reference/selection.md +++ b/docs/api-reference/selection.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-selection API -**Owner:** Editing +**탐색 분류:** Editing -구조적 selection과 topology 계약의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +구조적 selection과 topology 계약의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-selection/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/tanstack-table.md b/docs/api-reference/tanstack-table.md index a13a487a..ee676c51 100644 --- a/docs/api-reference/tanstack-table.md +++ b/docs/api-reference/tanstack-table.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-tanstack-table API -**Owner:** Connector +**탐색 분류:** Connector -TanStack Table connector의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +TanStack Table connector의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-tanstack-table/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/ui-primitives-react.md b/docs/api-reference/ui-primitives-react.md index ceb8a738..977542c2 100644 --- a/docs/api-reference/ui-primitives-react.md +++ b/docs/api-reference/ui-primitives-react.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-ui-primitives-react API -**Owner:** UI Primitives +**탐색 분류:** UI Primitives -표준 React UI primitive의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +표준 React UI primitive의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-ui-primitives-react/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/web.md b/docs/api-reference/web.md index b39d5cb4..9f1a287d 100644 --- a/docs/api-reference/web.md +++ b/docs/api-reference/web.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-web API -**Owner:** Adapter +**탐색 분류:** Adapter -Web platform adapter의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Web platform adapter의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-web/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/api-reference/zod.md b/docs/api-reference/zod.md index 9a10e315..efa6b560 100644 --- a/docs/api-reference/zod.md +++ b/docs/api-reference/zod.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-zod API -**Owner:** Connector +**탐색 분류:** Connector -Zod schema connector의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다. +Zod schema connector의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다. > 이 문서는 `packages/json-document-zod/src/index.ts`에서 생성됩니다. API를 변경한 뒤 `npm run docs:api`를 실행하세요. diff --git a/docs/evaluate.mjs b/docs/evaluate.mjs index 25fd93e3..708d746d 100644 --- a/docs/evaluate.mjs +++ b/docs/evaluate.mjs @@ -1,15 +1,20 @@ import { existsSync, readFileSync, readdirSync } from "node:fs"; -import { dirname, join } from "node:path"; +import { basename, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; import { validateLlmsContract, validatePublicPackageReferences } from "./public-contract-checks.mjs"; const root = dirname(dirname(fileURLToPath(import.meta.url))); -const apiReferenceCheck = spawnSync(process.execPath, ["scripts/generate-api-reference.mjs", "--check"], { cwd: root, encoding: "utf8" }); -if (apiReferenceCheck.status !== 0) { - process.stderr.write(apiReferenceCheck.stderr || apiReferenceCheck.stdout); - process.exit(apiReferenceCheck.status ?? 1); +for (const args of [ + ["scripts/generate-api-reference.mjs", "--check"], + ["--test", "docs/api-reference/packages.test.mjs", "site/scripts/route-checks.test.mjs"], +]) { + const result = spawnSync(process.execPath, args, { cwd: root, encoding: "utf8" }); + if (result.status !== 0) { + process.stderr.write(result.stderr || result.stdout); + process.exit(result.status ?? 1); + } } function read(path) { @@ -52,6 +57,8 @@ const publicDocs = { applications: read("docs/public/applications.md"), concepts: read("docs/public/concepts.md"), foundation: read("docs/public/foundation.md"), + buildingBlocks: read("docs/public/building-blocks.md"), + editing: read("docs/public/editing.md"), howWeBuild: read("docs/public/how-we-build.md"), documentTypes: read("docs/public/document-types.md"), selection: read("docs/public/selection.md"), @@ -141,83 +148,37 @@ const implementationShape = read("standards/repository-implementation-shape.md") const domEditingLifecycle = read("standards/dom-editing-lifecycle.md"); const editingSession = read("standards/editing-session.md"); -if (JSON.stringify(fileNames("docs/public")) !== JSON.stringify([ - "adapter-clipboard.md", - "adapter-contenteditable.md", - "adapter-grid-cell.md", - "adapter-interaction.md", - "adapter-keyboard.md", - "adapter-virtual-selection.md", - "adapters.md", - "affordance-activate.md", - "affordance-cancel.md", - "affordance-caret.md", - "affordance-context-menu.md", - "affordance-contextual.md", - "affordance-copy-drag.md", - "affordance-delete.md", - "affordance-double-click.md", - "affordance-drag.md", - "affordance-drop.md", - "affordance-focus.md", - "affordance-fold.md", - "affordance-forbid.md", - "affordance-handles.md", - "affordance-history.md", - "affordance-hover.md", - "affordance-marquee.md", - "affordance-nudge.md", - "affordance-pan.md", - "affordance-rename.md", - "affordance-resize.md", - "affordance-scroll.md", - "affordance-select.md", - "affordance-snap.md", - "affordance-triple-click.md", - "affordance-typeahead.md", - "affordance-zoom.md", - "affordance.md", - "animation.md", - "api.md", - "applications.md", - "clipboard.md", - "collaboration-history.md", - "collaboration-lease.md", - "collaboration-lifecycle.md", - "collaboration-replica.md", - "collaboration-text.md", - "collaboration.md", - "composer.md", - "concepts.md", - "connector-a2ui.md", - "connector-ajv.md", - "connector-react-hook-form.md", - "connector-react.md", - "connector-tanstack-table.md", - "connector-zod-validate.md", - "connector-zod.md", - "connectors.md", - "database.md", - "document-types.md", - "foundation.md", - "hands.md", - "history.md", - "how-we-build.md", - "intent-guide.md", - "intent.md", - "llms.txt", - "mention.md", - "object.md", - "official-hands.md", - "order.md", - "overview.md", - "react-editing.md", - "selection.md", - "topology.md", - "tree.md", - "ui-primitives.md", -])) { - fail("docs/public: only the active v3 guides and llms.txt may remain."); +const documentRoutes = readJson("site/site-routes.json").filter((route) => route.documentSource !== undefined); +const registeredSources = documentRoutes.map((route) => route.documentSource); +for (const source of registeredSources) { + if (!/^docs\/(?:public|api-reference)\/[^/]+\.md$/.test(source) || !existsSync(join(root, source))) { + fail(`Invalid documentation source registration: ${source}`); + } +} +if (new Set(registeredSources).size !== registeredSources.length) fail("Documentation sources must have one canonical route."); +for (const directory of ["docs/public", "docs/api-reference"]) { + const actual = fileNames(directory).filter((name) => directory === "docs/public" || name.endsWith(".md")); + const registered = registeredSources.filter((source) => dirname(source) === directory).map((source) => basename(source)); + if (directory === "docs/public") registered.push("llms.txt"); + registered.sort(); + if (JSON.stringify(actual) !== JSON.stringify(registered)) fail(`${directory}: Markdown files must match registered pages.`); +} + +// File links are repository-relative; site Markdown uses the same registered source path. +const linkedDocuments = [ + "README.md", "docs/README.md", "docs/changelog.md", + ...registeredSources, + ...rootPackage.workspaces.filter((workspace) => workspace !== "site").map((workspace) => `${workspace}/README.md`), + ...filesUnder("standards").filter((path) => path.endsWith(".md")), +]; +for (const path of linkedDocuments) { + for (const [, href] of read(path).matchAll(/(? optional native-input DOM lease ``` -Stateless JSON Patch는 JSON value, RFC 6901 JSON Pointer, RFC 9535 JSONPath, -RFC 6902 JSON Patch, Result 의미를 소유한다. JSON Document는 현재 document +Core의 표준 JSON 책임은 JSON value, RFC 6901 JSON Pointer, RFC 9535 JSONPath, +RFC 6902 JSON Patch와 Result 의미를 다룬다. JSON Document는 현재 document value와 change notification을 연결한다. Canonical concept와 identifier 문법은 @@ -201,12 +230,14 @@ Core package는 root entrypoint만 공개한다. Selection, clipboard, history optional editing companion이 여섯-member `JSONDocument` 위에서 조합한다. DOM-free selection state와 semantic interaction은 `@interactive-os/json-document-selection` companion이 key, range, mask family로 -제공하며 topology, geometry와 physical input 해석은 host port 밖에 둔다. +제공한다. Topology는 문서·표시 순서의 owner가, geometry 관찰과 physical input +해석은 플랫폼 Adapter가 제공하며 제품 layout과 구체 입력의 연결은 Host에 둔다. 브라우저 플랫폼 계약은 independently versioned Adapter가 변환한다. 반복되는 외부 생태계 integration은 independently versioned Connector가 제공한다. Adapter와 Connector는 공통 runtime interface가 아니라 package category다. 외부 peer는 해당 package의 peer dependency다. Schema introspection, DOM과 -제품별 UI 의미는 해당 Adapter, Connector 또는 host의 명시적인 책임으로 남는다. +재사용 UI 행동은 해당 Adapter, Connector 또는 UI Primitives의 책임으로 남는다. +Host는 제품의 정책 값과 시각 조합을 선택한다. v3 Kernel release는 dependency-free Core package 하나다. Local-only consumer는 Core만 설치한다. 이 저장소의 `@interactive-os/json-document-editing`은 atomic @@ -215,28 +246,34 @@ Order, Sheet, Object 및 Tree domain slice를 제공하는 browser-independent companion이다. Document, Order, Sheet와 Tree는 range 상태 전이를 공유하고 Object는 key 상태 전이를 사용한다. 화면 줄은 `LineTopology`와 `GridTopology`로 넘긴다. Sheet는 `SheetTopology`, Database는 저장된 -뷰의 `recordIds`×`propertyIds`, Tree는 host `visibleIds`다. JSON Patch -계획은 각 slice 또는 host에 둔다. +뷰의 `recordIds`×`propertyIds`, Tree는 `projectTreeVisibility`가 계산한 +`visibleIds`다. JSON Patch 계획과 의미 연산은 장르 editor와 문서 의미의 +owner에 둔다. Sheet slice는 stable row·column identity, 복수의 anchor/focus rectangular range, primary-range JSON/TSV clipboard, selection fill, cell commit과 selection-restoring undo/redo를 제공한다. -Tree expand/collapse와 visible order, Object pointer geometry와 hit-test는 host -책임이다. 값 변경 history는 selection을 함께 복구한다. Native text selection은 +Tree visible projection은 Editing의 `projectTreeVisibility`, React의 접힘 상태와 +입력 연결은 `useTreeEditing`이 소유한다. Host는 초기 expanded IDs 등 정책 값을 +선택한다. Object geometry의 의미 연산과 플랫폼 hit-test도 각각의 정본 owner에 +둔다. 값 변경 history는 selection을 함께 복구한다. Native text selection은 이 structural selection family에 포함하지 않는다. `@interactive-os/json-document-web`은 KeyboardEvent chord, ClipboardEvent structured MIME, text-control value/caret과 Web modifier state를 public -editing·selection contract로 번역하는 공식 Adapter다. Event target, shortcut, -focus, geometry와 native text selection은 Host가 계속 소유한다. +editing·selection contract로 번역하는 공식 Adapter다. Event target 경계, +modifier·기본 chord 해석과 DOM focus 실현은 Web의 정본 API를 소비한다. +Host는 활성화 조건과 제품별 keymap 정책 값을 선택한다. `@interactive-os/json-document-contenteditable`은 local JSONDocument 문자열 포인터를 leased contenteditable React root에 붙이는 공식 Adapter다. -툴바, atom, marks와 product chrome은 Host가 소유한다. +Host는 toolbar와 product chrome을 조합한다. Atom·mark의 문서 의미와 재사용 +rendering·native selection·IME lifecycle은 각 문서·Adapter·UI owner에 둔다. `@interactive-os/json-document-react`는 React external-store subscription, Document editor component lifecycle, 그리고 선택 범위·focus 커서·text offset 질의(`useEditing`)를 제공하는 공식 Connector다. `@interactive-os/json-document-react-hook-form`은 React Hook Form이 draft, dirty, touched와 field error를 소유하게 두고 유효한 submit만 하나의 canonical editing -transaction으로 적용한다. Undo, redo와 외부 canonical 변경은 `reset`으로 form에 -동기화하며 field UI와 product schema는 Host가 소유한다. +transaction으로 적용한다. Undo, redo와 외부 canonical 변경은 Connector의 +leaf 동기화 또는 `reset` 정책으로 form에 반영한다. Host는 field UI를 조합하고 +문서 의미의 owner가 제공하는 schema와 제품 정책 값을 주입한다. `@interactive-os/json-document-ajv`는 호출자가 컴파일한 동기 Ajv validator의 첫 error message와 `instancePath`를 validation diagnostic으로 번역한다. Mutable clone을 검사하므로 Ajv option이 만든 변형 결과는 canonical JSON에 채택하지 않는다. @@ -249,7 +286,7 @@ row/column model을 Sheet topology로 번역해 정렬·필터·column ordering cell edit, rectangular multi-range selection, selection fill과 clipboard가 화면 순서를 따르게 하는 공식 Connector다. `@interactive-os/json-document-collaboration`은 같은 canonical JSON Document을 -제공하는 independently versioned, transport-free provider이고, +제공하는 independently versioned, transport-free collaboration engine이고, `@interactive-os/json-document-contenteditable-collaboration`은 collaborative string을 위한 optional native-input DOM lease다. 두 companion을 사용해도 Core Root API와 editor가 받는 `JSONDocument` port는 바뀌지 않는다. @@ -273,12 +310,27 @@ catalog의 일부가 아니다. ## Host 책임 -Rendering, DOM focus, geometry, keyboard policy, system clipboard, filesystem, -network, formula engine, CRDT와 OT는 host 또는 adapter가 소유한다. Selection의 -공통 lifecycle은 editing companion에 있지만 grid range나 spatial object 같은 -구체적인 topology는 제품이 소유한다. Tree -indent/outdent, visible-row focus, slide selection box, grid coordinate와 같은 제품 -의도는 host가 Pointer와 Patch operation으로 번역한다. +Host는 모듈 조합·실행 순서·제품 정책 값·권한·copy·fixture·layout·구체 외부 +인스턴스 주입만 소유한다. 모델·schema·Intent·command·selection·history·gesture· +플랫폼 번역·직렬화·projection·재사용 UI의 최종 owner가 아니다. +Tree indent/outdent나 grid coordinate 해석도 해당 정본 모듈의 공개 API를 소비한다. +정본이 유효한 사례를 지원하지 못하면 API를 확장하거나 책임을 등록해야 하며 +Host local 구현을 최종 상태로 인정하지 않는다. + +## Editing Protocol + +장르 editor는 Intent와 Selection/Topology에서 의미 연산을 계획한다. +EditingPlan은 operations·selectionAfter·origin과 history 정책을 담고, +EditingSession.apply가 JSONDocument.commit을 사용한다. EditingSnapshot은 +value·selection·revision·canUndo·canRedo를 일관되게 관찰하게 한다. +거절은 해당 요청의 문서·선택·History를 바꾸지 않는다. Copy는 읽기이며 +selection-only 전이는 document commit과 local History 항목을 만들지 않는다. + +EditingSession의 공통 의미는 현재 계약이다. Hands Profile 전체의 동결과 +독립 구현 간 상호운용 인증은 아니다. 로컬 inverse History와 actor-local 협업 +History는 복원 의미와 owner가 다르다. 협업 document를 주입할 때도 협업 History +owner를 연결해야 하며 현재 외부 History가 지원하지 않는 ignore 정책을 조용히 +다른 의미로 적용하지 않는다. ## 표준과 검증 diff --git a/docs/public/official-hands.md b/docs/public/official-hands.md index 2ca611d2..1fa9b24f 100644 --- a/docs/public/official-hands.md +++ b/docs/public/official-hands.md @@ -1,8 +1,7 @@ # Official Hands · TBD -> **TBD** — 이 페이지는 Official Hands의 제품 방향을 설명하는 초안입니다. -> 새로운 public contract, package boundary, kit admission 기준 또는 -> compatibility 약속을 확정하지 않습니다. +Official Hands의 목표와 남은 경계를 설명하는 초안입니다. 새로운 public contract, +package boundary, kit admission 기준 또는 compatibility 약속을 확정하지 않습니다. Official Hands는 디자인과 제품 데이터는 자유롭게 바꿀 수 있지만, 사람이 편집을 끝내는 데 필요한 기능은 이미 구현되어 있는 SDK를 지향합니다. @@ -40,15 +39,14 @@ Official은 제품 취향을 임의로 정한다는 뜻이 아닙니다. 여러 대부분의 사용자는 Official Hands만으로 편집기를 완성할 수 있어야 합니다. Custom Hands는 기본 경로가 아니라 제품에만 있는 차이를 위한 escape hatch입니다. -## 하나의 Hands Profile +## Hands Profile · TBD 완성된 Hands는 행동 함수만 모은 package가 아닙니다. 그 행동이 항상 같은 뜻을 갖게 하는 최소 profile을 함께 제공합니다. ```text Official Hands Profile -├─ minimum schema와 canonical shape -├─ stable identity와 structural invariant +├─ Document Type Profile 참조: schema · identity · invariant ├─ Selection specialization ├─ Topology interpretation ├─ Intent vocabulary @@ -64,6 +62,11 @@ row identity, column identity와 cell addressability를 먼저 정해야 합니 Sheet다운 편집 행동이 무엇을 대상으로 하는지 안정적으로 정하기 위해 필요합니다. +최소 schema와 의미 연산의 owner는 [Document Type](document-types.md)입니다. +Hands Profile은 이 계약을 참조하고 Editing·Adapter·Affordance·Connector·UI를 +함께 선택합니다. Hands가 각 책임을 다시 구현하거나 Host가 빈칸을 메우는 구조가 +아닙니다. + ### 공통 규칙과 profile의 선택 공통 편집 규칙은 Selection과 Editing이 소유합니다. Profile은 그 규칙이 자신의 @@ -90,17 +93,17 @@ Selection family, EditingSession을 사용하면서 입력부터 편집 결과 Official Hands가 최소 profile을 제공해도 완성 제품을 대신 소유하지는 않습니다. ```text -Official Hands가 소유 -├─ 장르다운 편집을 성립시키는 최소 shape -├─ identity와 structural invariant -├─ 수렴한 편집 행동 -└─ 함께 검증된 기본 조합 +Official Hands Profile이 연결 +├─ Document Type의 shape·identity·invariant +├─ Editing의 Selection·Intent·Clipboard·History +├─ Adapter·Affordance·Connector·UI의 편집 경로 +└─ 함께 검증할 지원 범위와 기본 조합 Host가 소유 -├─ 업무 field와 business rule -├─ permission과 workflow -├─ persistence와 collaboration policy -├─ rendering과 layout +├─ 제품별 정책 값·권한·copy·fixture +├─ workflow와 실행 순서 +├─ persistence·collaboration의 구체 인스턴스 주입 +├─ UI composition과 layout └─ visual design ``` @@ -109,6 +112,9 @@ automation에서 전혀 다르게 보일 수 있습니다. Hands는 object ident Selection, translate와 resize의 의미를 유지하고 Host는 표현과 제품 정책을 결정합니다. +재사용 가능한 업무 모델·규칙과 rendering 행동은 각각 문서 의미와 UI의 정본 +모듈에 둡니다. 제품에서 선택하는 정책 값과 모듈 자체의 의미를 구별합니다. + ## Affordance까지 닫기 Editing capability만으로는 사람이 작업을 끝낼 수 없습니다. Official Hands는 @@ -186,3 +192,17 @@ Official profile의 지향점은 구현이 바뀌어도 같은 지원 입력에 구체적인 profile별 필수 작업, Host field 연결, 여러 Hand가 공유하는 History 단위는 아직 확정하지 않았습니다. 현재 후보 목록과 위 동작 예시는 완성된 SDK의 호환성 보장이 아닙니다. + +## 현재 증거와 완료 조건 · TBD + +| 경계 | 현재 있는 것 | 완료에 필요한 것 | +| --- | --- | --- | +| 문서 의미 | 각 editor와 package의 모델·연산 | Document Type owner와 Profile 참조의 수렴 | +| 편집 작업 | 기존 Intent와 EditingSession 공통 의미 | Profile별 지원/의도적 미지원/미구현 및 결과·실패 조건 | +| 실제 입력 | Hands Live Demo와 platform binding | keyboard·pointer·Clipboard·취소·Undo/Redo가 이어지는 적합성 증거 | +| 공개 사용 | package API와 Usage·Source | Host의 같은 책임 우회 구현 없이 조합되는 완료 경로 | +| 호환성 | 개별 구현과 Profile의 증거 | 기본값·중첩 맥락·공유 History와 변경 정책의 명시 | + +이 조건을 닫기 전에는 Official Hands를 완성된 SDK나 모든 장르가 상호운용하는 +Stable 계약으로 표시하지 않습니다. 목표를 미리 드러내되 현재 동작의 증거와 +미확정 설계를 섞지 않습니다. diff --git a/docs/public/overview.md b/docs/public/overview.md index dd162905..f8721456 100644 --- a/docs/public/overview.md +++ b/docs/public/overview.md @@ -45,7 +45,7 @@ json-document는 그 공통 층을 화면과 분리된 문서 커널로 둡니 통과한 변경만 원자적으로 적용되고, 실제로 값이 달라진 변경만 구독자에게 전달됩니다. 현재 값을 읽고, 한 위치와 여러 위치를 찾고, 검사하고, 적용하고, 구독하는 일이 이 계약의 전부입니다. 호출 모양은 -[API](api.md)에 있습니다. +[JSON Document Protocol](api.md)에 있습니다. ## 같은 문을 여는 협업 @@ -76,8 +76,17 @@ Editing은 이 상태를 JSON Document 옆에 둡니다. 화면은 클릭과 키 보이는 순서를 알려 줍니다. Clipboard는 JSON과 사람이 읽을 텍스트를 함께 나릅니다. History는 값과 선택을 같이 되돌립니다. -같은 문서 위에 선택과 작업을 더하는 일이 Editing입니다. 따라 가려면 -[Intent guide](intent-guide.md)에서 시작합니다. +같은 문서 위에 선택과 작업을 더하는 일이 Editing입니다. +[Editing Protocol](editing.md)에서 계획·적용·관찰의 경계를 보고, +[Intent guide](intent-guide.md)에서 직접 호출해 봅니다. + +## 문서의 의미와 목표 owner + +Calendar의 recurrence나 Tree의 parent/child 관계는 편집 중의 선택과 다른 +책임입니다. Document Type이 model·schema·invariant·의미 연산·Projection을 +소유하고 Editing이 그 계약을 소비하는 구조를 지향합니다. +[Document Types · TBD](document-types.md)에 현재 후보와 남은 소유권 수렴을 +미리 드러냅니다. 기존 package/API의 존재만으로 이 목표가 완료되지는 않습니다. ## Artifact에 손을 붙이기 @@ -85,11 +94,16 @@ Editing은 이 상태를 JSON Document 옆에 둡니다. 화면은 클릭과 키 제품처럼 보입니다. 그 아래에서는 같은 문서와 같은 편집 상태를 씁니다. 다른 것은 그 장르가 손을 얹는 방식입니다. -Hands는 사람이 artifact와 agent를 다루는 편집 도구의 최소 완성본입니다. +Hands는 사람이 artifact와 agent를 다루는 장르별 편집 조합입니다. 한 줄 목록을 집어 옮기는 손, 칸을 채우는 손, 가지를 접는 손이 선반에 있습니다. Agent에게 지시와 맥락을 건네는 Composer와, 안정적인 대상을 글에 넣는 Mention도 Rich Text와 구조화된 context로 동작합니다. 고르려면 [Hands](hands.md)로 갑니다. +[Official Hands · TBD](official-hands.md)는 이 조합을 기본 편집이 완성된 SDK로 +제공하려는 목표입니다. 전체 지원 입력·실패·선택 복원·호환성 조건은 아직 닫히지 +않았습니다. [Artifact](/viewer)도 현재는 visual prototype이며 문서·Hands 연결과 +파일 호환성을 증명하지 않습니다. + ## 플랫폼, 라이브러리, Affordance 브라우저에서 쓰려면 키보드와 clipboard, contenteditable 같은 플랫폼 @@ -102,12 +116,13 @@ React로 그리거나 Zod로 검사하려면 이름 있는 라이브러리의 React 구독으로 흐르고, 표의 보이는 행과 열은 Sheet의 Topology가 됩니다. 고르기, 접기, 드래그, 되돌리기는 제품이 json-document를 만지는 손입니다. -화면은 호스트가 그리고, 단축키와 마우스 문법은 Affordance가 닫습니다. +제품 화면은 Host가 조합하고, 플랫폼 사실은 Adapter가 해석하며, +입력 장치와 독립적인 조작 의미·수명주기는 Affordance가 소유합니다. Adapter와 Connector는 서로 직렬인 계층이 아니라 환경에 따라 독립적으로 고르는 책임입니다. Affordance도 필요한 입력 문법을 선택해 Host에 조합합니다. -[Adapters](adapters.md)와 [Connectors](connectors.md), -[Affordance](affordance.md)에서 이어서 읽습니다. +[Building Blocks](building-blocks.md)에서 Adapter·Connector·Affordance· +UI Primitives의 경계를 함께 봅니다. ## Artifact editing의 Core @@ -118,5 +133,5 @@ Connector, Affordance와 UI Primitive는 필요한 환경과 입력 문법에 Hands는 그 조합이 장르별 최소 편집 loop를 완성했는지 판정합니다. 여러 artifact가 같은 주소와 실행 취소와 협업을 쓰게 하려는 자리가 이 -Core입니다. 각 책임의 경계와 의존 순서는 [Concept Map](concepts.md)에서 +Core입니다. 각 책임의 경계와 선택적인 의존 관계는 [Concept Map](concepts.md)에서 이어집니다. diff --git a/packages/json-document-editing/README.md b/packages/json-document-editing/README.md index eef05856..d1a050a4 100644 --- a/packages/json-document-editing/README.md +++ b/packages/json-document-editing/README.md @@ -119,10 +119,10 @@ Its structural selection slices split into two reusable families without pretending that every topology is the same: - `Document`, `Order`, `Sheet`, and `Tree` use the range family. - Their host or domain slice supplies ordered axes, visible order, and JSON - Patch planning. -- `Object` uses the key family. The host owns pointer - geometry and hit-testing, then sends only stable object IDs to the editor. + Ordered axes, visible projections and JSON Patch planning come from the + canonical document/editor or external-model connector. +- `Object` uses the key family. Canonical platform geometry and hit-testing + APIs supply stable object IDs to the editor; the Host composes that path. Its public `selection.set` accepts the shared `replace`, `extend`, and `toggle` vocabulary directly; `extend` has key-family union semantics. diff --git a/packages/json-document-selection/README.md b/packages/json-document-selection/README.md index 935a6cea..819e5497 100644 --- a/packages/json-document-selection/README.md +++ b/packages/json-document-selection/README.md @@ -6,7 +6,10 @@ DOM-free selection families and semantic interaction controllers for structural The package owns JSON-safe selection state, pure family transitions, reconciliation/mapping, target publication, and pointer/keyboard interaction lifecycles after physical input has been translated into semantic operations. -The host owns DOM or canvas geometry, hit testing implementation, modifier-key mapping, focus and accessibility wiring, native text caret/IME state, and domain edits such as delete, move, fill, and paste. +플랫폼 geometry 관찰·hit testing·modifier 해석·DOM focus·native caret/IME는 +Adapter와 해당 UI owner가, delete·move·fill·paste의 의미는 문서·Editing owner가 +소유합니다. Host는 제품 정책 값과 구체 인스턴스·layout을 조합하며 이 책임을 +직접 재구현하지 않습니다. ```text platform adapter ─┐ @@ -19,7 +22,7 @@ editing history ──┘ - `KeySelection`: explicit keys or symbolic `all` with exclusions and a host-issued universe token. - `RangeSelection`: directional anchor/focus ranges over a host-provided `OrderedTopology`. - `MaterializedRangeSelection`: directional ranges whose resolved points survive virtualized or paged topology changes. Each range keeps its anchor/focus and the points produced by the topology at transition time; reconciliation removes only identities that the topology no longer recognizes. -- `MaskSelection`: an extension protocol whose weighted representation and algebra remain host-owned. +- `MaskSelection`: an extension protocol whose weighted representation and algebra belong to the implementing document/editor owner, not anonymous Host logic. These families share `SelectionFamily`; they do not share a universal reducer. @@ -30,11 +33,9 @@ Use `createMaterializedRangeSelectionFamily` when the visible topology can chang Translate physical input before calling the package: ```ts -const operation: SelectionOperation = event.shiftKey - ? "extend" - : event.metaKey || event.ctrlKey - ? "toggle" - : "replace"; +import { selectionOperationFromModifiers } from "@interactive-os/json-document-web"; + +const operation = selectionOperationFromModifiers(event); ``` Viewport-to-domain coordinate conversion, pointer capture, auto-scroll, and accessibility remain in the adapter. Pass only `PointerSample` values to `reducePressInteraction` or `reduceMarqueeInteraction`. diff --git a/packages/json-document-web/README.md b/packages/json-document-web/README.md index ac5dfe8d..29aa0cb0 100644 --- a/packages/json-document-web/README.md +++ b/packages/json-document-web/README.md @@ -68,8 +68,9 @@ const dragDrop = createWebDragDropSession({ }); ``` -The sessions own platform lifecycle state. Hit testing, valid targets, geometry, -and document Intent remain in the host. +세션은 플랫폼 수명주기를 소유합니다. Hit testing과 geometry의 플랫폼 관찰, +유효 대상과 문서 Intent의 의미는 각각 Adapter와 문서·Editing owner의 계약을 +소비합니다. Host는 제품의 대상·정책 값과 실행 경로를 연결합니다. `createWebViewportPositionPorts` measures an exact target and its paired tail reserve, writes temporary scroll range, performs smooth or instant positioning, @@ -118,12 +119,12 @@ const keyboard = createWebKeyboardAdapter(); surface.addEventListener("click", (event) => { const operation = selectionOperationFromModifiers(event); - // The host resolves geometry and dispatches its domain selection intent. + // Connect canonical geometry/selection APIs with the product's target. }); surface.addEventListener("keydown", (event) => { const command = keyboard.resolve(event); - // Official adapter output. The host maps it through topology to a domain intent. + // Pass the command to the canonical topology/editor API. }); input.addEventListener("input", (event) => { @@ -148,18 +149,19 @@ named codecs remain compatibility aliases over those domain formats. Clipboard surfaces write both the structured json-document MIME payload and its `text/plain` projection. Paste consumes only a valid structured payload. Parsing arbitrary external plain text -into domain records or cells remains a host policy. +into domain records or cells belongs to the document/editor's format contract. +The Host chooses which representations and product policies to enable. The official keyboard adapter owns `defaultWebKeymap`. `resolve` returns a semantic command or `null`; `moveLinePoint` and `moveGridPoint` locate the visible neighbor. The host still decides when a command applies and which domain Intent to dispatch. -The clipboard binding calls `preventDefault()` only after a successful copy, -canonical cut, or canonical paste. Cut writes the selected payload before -asking the Editing companion to remove it. Missing clipboard data, malformed -payloads, unsupported cut, and rejected editing results leave native handling -available. +Cut writes the selected payload before asking Editing to remove it. Once a +Cut payload is owned, native deletion is cancelled before removal; a rejected +removal must not trigger another browser deletion. Copy/Paste cancel native +handling on success. Missing data and unsupported input follow the specific +failure boundary below. `createWebClipboardSurface` is the public surface-level orchestration API. It projects one binding into `onCopy`, `onCut`, and `onPaste` handlers and reports @@ -187,11 +189,16 @@ The Adapter owns: The host owns: -- the event target, canonical focus, when a command applies, and role workflow policy; -- DOM/canvas geometry and hit testing; -- external plain-text interpretation and product-specific paste policy; +- product-specific activation, permissions, and workflow policy; +- concrete DOM/external instances and visual composition; +- selection of canonical geometry, editor, focus, and clipboard APIs; +- product-specific paste policy values; - enabled representations and their priority; -- native text selection, IME, drag/drop, persistence, and remote protocols. +- composition and execution order of persistence and remote-system integrations. + +Native text selection, IME, drag/drop lifecycle, serialization, and reusable UI +behavior remain at their canonical Adapter, Affordance, Connector, or UI owner. +Host composition is not an exemption from those module boundaries. The module does not access `window`, `document`, or `navigator` during import, so non-browser tooling can load it safely. diff --git a/packages/json-document/README.md b/packages/json-document/README.md index d4247e27..d5f1962d 100644 --- a/packages/json-document/README.md +++ b/packages/json-document/README.md @@ -200,15 +200,22 @@ const body = JSON.stringify(operations); body satisfies string; ``` -## Connector와 host 경계 +## 생태계와 Host 경계 Form, data-grid, outliner, rich text, persistence/collaboration extension은 여섯 -member `JSONDocument`를 포트로 받는 것이 권장됩니다. DOM focus, geometry, -keyboard, system clipboard, filesystem, network, formula, CRDT와 OT는 host가 -소유합니다. +member `JSONDocument`를 포트로 받습니다. 문서 고유 모델·의미 연산·Projection은 +Document Type, 선택·작업·History는 Editing, 플랫폼 입력과 DOM lifecycle은 +Adapter의 책임입니다. 이 기능을 Core나 Host에 재구현하지 않습니다. React, Zod와 TanStack Table 같은 외부 생태계의 반복되는 integration은 Root가 아니라 `@interactive-os/json-document-` 공식 Connector가 제공합니다. +Host는 제품 정책 값·copy·fixture·layout, 정본 모듈의 조합·실행 순서와 +구체 외부 인스턴스 주입을 소유합니다. -- GitHub Wiki: https://github.com/developer-1px/json-document/wiki -- Extension guide: https://github.com/developer-1px/json-document/wiki/Labs-and-Extensions +현재 package 배치와 목표 책임의 수렴은 구별합니다. Document Type 후보와 +Official Hands Profile의 전체 완료는 아직 TBD이며 Core v3의 Stable 계약을 +확장하지 않습니다. + +- [Concept Map](../../docs/public/concepts.md) +- [Building Blocks](../../docs/public/building-blocks.md) +- [Document Types · TBD](../../docs/public/document-types.md) diff --git a/scripts/generate-api-reference.mjs b/scripts/generate-api-reference.mjs index d3595a2d..b91ab3a7 100644 --- a/scripts/generate-api-reference.mjs +++ b/scripts/generate-api-reference.mjs @@ -2,14 +2,15 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import ts from "typescript"; -import { apiReferencePackages } from "../docs/api-reference/packages.mjs"; +import { apiReferenceCoverageErrors, apiReferencePackages } from "../docs/api-reference/packages.mjs"; const root = dirname(dirname(fileURLToPath(import.meta.url))); const check = process.argv.includes("--check"); const configPath = join(root, "tsconfig.build.json"); const parsed = ts.parseJsonConfigFileContent(ts.readConfigFile(configPath, ts.sys.readFile).config, ts.sys, root); const entrypoints = apiReferencePackages.flatMap(({ entrypoint, subpaths }) => [entrypoint, ...subpaths.map((subpath) => subpath.entrypoint)]).map((entrypoint) => join(root, entrypoint)); -const sourcePaths = Object.fromEntries(apiReferencePackages.map(({ packageName, entrypoint }) => [packageName, [entrypoint]])); +const sourcePaths = Object.fromEntries(apiReferencePackages.flatMap((descriptor) => + [descriptor, ...descriptor.subpaths].map(({ packageName, entrypoint }) => [packageName, [entrypoint]]))); const program = ts.createProgram([...new Set([...parsed.fileNames, ...entrypoints])], { ...parsed.options, baseUrl: root, @@ -20,7 +21,9 @@ const program = ts.createProgram([...new Set([...parsed.fileNames, ...entrypoint noEmit: true, }); const checker = program.getTypeChecker(); -const failures = []; +const manifests = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).workspaces + .map((workspace) => JSON.parse(readFileSync(join(root, workspace, "package.json"), "utf8"))); +const failures = apiReferenceCoverageErrors(manifests); let exportCount = 0; const siteRoutes = JSON.parse(readFileSync(join(root, "site/site-routes.json"), "utf8")); @@ -60,8 +63,11 @@ function clean(signature) { for (const descriptor of apiReferencePackages) { const referencePath = `/docs/api/${descriptor.slug}`; - const ownerRoutes = siteRoutes.filter((route) => route.path === referencePath && route.navigationGroup === descriptor.owner); - if (ownerRoutes.length !== 1) failures.push(`${descriptor.packageName} owner route`); + const referenceRoutes = siteRoutes.filter((route) => + route.path === referencePath + && route.navigationGroup === descriptor.navigationGroup + && route.documentSource === `docs/api-reference/${descriptor.slug}.md`); + if (referenceRoutes.length !== 1) failures.push(`${descriptor.packageName} owner reference route`); const entry = program.getSourceFile(join(root, descriptor.entrypoint)); if (!entry) throw new Error(`public entrypoint를 찾을 수 없습니다: ${descriptor.entrypoint}`); const moduleSymbol = checker.getSymbolAtLocation(entry); @@ -89,9 +95,9 @@ for (const descriptor of apiReferencePackages) { const output = [ `# ${descriptor.packageName} API`, "", - `**Owner:** ${descriptor.owner}`, + `**탐색 분류:** ${descriptor.navigationGroup}`, "", - `${descriptor.responsibility}의 public entrypoint입니다. 아래 항목은 package root에서 import할 수 있는 안정된 public API이며 internal 경로는 계약이 아닙니다.`, + `${descriptor.responsibility}의 public entrypoint입니다. API의 owner는 이 package이며 탐색 분류는 사이트에서 읽는 위치입니다. 별도 subpath 표시가 없는 항목은 package root에서 import합니다. internal 경로는 계약이 아닙니다.`, "", `> 이 문서는 \`${descriptor.entrypoint}\`에서 생성됩니다. API를 변경한 뒤 \`npm run docs:api\`를 실행하세요.`, "", diff --git a/site/scripts/check-documentation-page.mjs b/site/scripts/check-documentation-page.mjs index 2d3c047f..eefdbd4b 100644 --- a/site/scripts/check-documentation-page.mjs +++ b/site/scripts/check-documentation-page.mjs @@ -4,7 +4,7 @@ import { join } from "node:path"; const root = new URL("../..", import.meta.url).pathname; const docsRoot = join(root, "site/src/routes/docs"); const canonicalOwner = "DocumentationPage.tsx"; -const consumers = ["DocsRoute.tsx", "ConceptsRoute.tsx", "DocumentTypeCandidateRoute.tsx"]; +const consumers = ["DocsRoute.tsx", "DocumentTypeCandidateRoute.tsx"]; for (const name of readdirSync(docsRoot).filter((entry) => entry.endsWith(".tsx"))) { const source = readFileSync(join(docsRoot, name), "utf8"); @@ -14,7 +14,7 @@ for (const name of readdirSync(docsRoot).filter((entry) => entry.endsWith(".tsx" } const owner = readFileSync(join(docsRoot, canonicalOwner), "utf8"); -for (const contract of ["PageFrame", "PageHeader", "MarkdownViewer", "markdownHeadings", "Documentation sections", "On this page", "max-w-3xl"]) { +for (const contract of ["PageFrame", "PageHeader", "MarkdownViewer", "data-doc-heading", "Documentation sections", "On this page", "max-w-3xl"]) { if (!owner.includes(contract)) throw new Error(`DocumentationPage is missing canonical contract: ${contract}`); } for (const consumer of consumers) { diff --git a/site/scripts/route-checks.mjs b/site/scripts/route-checks.mjs index dc0b0835..9a11ee9e 100644 --- a/site/scripts/route-checks.mjs +++ b/site/scripts/route-checks.mjs @@ -53,6 +53,9 @@ export function validateSiteRoutes(routes, fail) { if (route.heading !== undefined && (typeof route.heading !== "string" || route.heading.trim() === "")) { fail(`site route ${route.path} has an invalid heading.`); } + if (route.documentSource !== undefined && !/^docs\/(?:public|api-reference)\/[^/]+\.md$/.test(route.documentSource)) { + fail(`site route ${route.path} has an invalid documentation source.`); + } if (route.chrome !== undefined && route.chrome !== "app") { fail(`site route ${route.path} has an invalid chrome.`); } @@ -81,7 +84,8 @@ export function validateSiteRoutes(routes, fail) { files.add(file); } - const navigationLabel = `${route.navigationGroup ?? "hidden"}:${route.label}`; + const labelScope = route.parentPath ?? route.navigationGroup ?? (route.sidebar === false ? route.path : "root"); + const navigationLabel = `${labelScope}:${route.label}`; if (labels.has(navigationLabel)) fail(`site navigation group contains duplicate label ${route.label}.`); labels.add(navigationLabel); diff --git a/site/scripts/route-checks.test.mjs b/site/scripts/route-checks.test.mjs new file mode 100644 index 00000000..ed84b930 --- /dev/null +++ b/site/scripts/route-checks.test.mjs @@ -0,0 +1,26 @@ +import { readFileSync } from "node:fs"; +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { validateSiteRoutes } from "./route-checks.mjs"; + +const root = { path: "/", label: "Home", title: "Home", description: "Home page" }; +const route = (path, extra = {}) => ({ path, title: path, description: `About ${path}`, label: "Overview", ...extra }); +const errors = (routes) => { + const failures = []; + validateSiteRoutes(routes, (failure) => failures.push(failure)); + return failures; +}; + +test("validates the real site registry and separate section landings", () => { + assert.deepEqual(errors(JSON.parse(readFileSync(new URL("../site-routes.json", import.meta.url), "utf8"))), []); + assert.deepEqual(errors([root, route("/docs/foundation", { sidebar: false }), route("/docs/building-blocks", { sidebar: false })]), []); +}); + +test("rejects duplicate visible sibling labels and invalid documentation paths", () => { + assert.deepEqual(errors([root, route("/docs/one", { navigationGroup: "Editing" }), route("/docs/two", { navigationGroup: "Editing" })]), [ + "site navigation group contains duplicate label Overview.", + ]); + assert.deepEqual(errors([root, route("/docs/one", { documentSource: "../private.md" })]), [ + "site route /docs/one has an invalid documentation source.", + ]); +}); diff --git a/site/site-routes.json b/site/site-routes.json index 6fa7596e..fdcad414 100644 --- a/site/site-routes.json +++ b/site/site-routes.json @@ -7,10 +7,10 @@ }, { "path": "/viewer", - "label": "Document · Presentation · Spreadsheet", - "title": "Artifact - json-document", - "heading": "Artifact", - "description": "MD·PPT·Sheet artifact가 한 host에 나타나고 서로 다른 Hands를 얻는 미래의 mock입니다.", + "label": "Content Prototype · TBD", + "title": "Artifact · TBD - json-document", + "heading": "Artifact · TBD", + "description": "Application 안에서 MD·PPT·Sheet를 다루는 visual prototype과 아직 연결하지 않은 문서·Hands·호환성 계약을 구분합니다.", "language": "ko", "navigationGroup": "Artifact" }, @@ -19,9 +19,10 @@ "label": "Why", "title": "json-document Docs - json-document", "heading": "왜 json-document인가", - "description": "문서·표·보드가 같은 JSON 계약을 쓰고, 협업과 장르의 손과 붙이는 층을 같은 커널 위에 올리는 이유를 설명합니다.", + "description": "문서·표·보드가 JSON Document 계약을 공유하고, 독립적인 편집·플랫폼·생태계 책임을 조합하는 이유를 설명합니다.", "language": "ko", - "navigationGroup": "Introduction" + "navigationGroup": "Introduction", + "documentSource": "docs/public/overview.md" }, { "path": "/docs/foundation", @@ -30,15 +31,17 @@ "heading": "Foundation", "description": "JSON Document, Document Types, Editing과 Collaboration이 공유하는 기반 계약을 설명합니다.", "language": "ko", - "navigationGroup": "JSON Document" + "documentSource": "docs/public/foundation.md", + "sidebar": false }, { "path": "/docs/concepts", "label": "Concept Map", "title": "Concept Map - json-document", - "description": "JSON Document에서 Artifact까지 책임이 쌓이는 순서와 각 계층의 경계를 설명합니다.", + "description": "목표 레이어·책임 경계와 실제 프로토콜을 구분하고, 현재 제공되는 계약과 TBD를 함께 설명합니다.", "language": "ko", - "navigationGroup": "Introduction" + "navigationGroup": "Introduction", + "documentSource": "docs/public/concepts.md" }, { "path": "/docs/how-we-build", @@ -47,7 +50,8 @@ "heading": "제품에서 정본 모듈을 발견하는 방법", "description": "Application을 먼저 만들고 실제 제품 책임을 canonical module로 추출한 뒤 제품이 다시 소비하는 개발 순환을 설명합니다.", "language": "ko", - "navigationGroup": "Introduction" + "navigationGroup": "Introduction", + "documentSource": "docs/public/how-we-build.md" }, { "path": "/applications", @@ -56,7 +60,8 @@ "heading": "Applications", "description": "Artifact와 Hands를 실제 제품 경험으로 조합하고 재사용 책임을 발견하는 Application 목록입니다.", "language": "ko", - "navigationGroup": "Applications" + "navigationGroup": "Applications", + "documentSource": "docs/public/applications.md" }, { "path": "/applications/calendar", @@ -76,6 +81,16 @@ "navigationGroup": "Applications", "chrome": "app" }, + { + "path": "/docs/building-blocks", + "label": "Overview", + "title": "Building Blocks - json-document", + "heading": "Building Blocks", + "description": "Adapter·Connector·Affordance·UI Primitives의 독립적인 책임과 Foundation·Hands를 연결하는 계약을 설명합니다.", + "language": "ko", + "sidebar": false, + "documentSource": "docs/public/building-blocks.md" + }, { "path": "/docs/document-types", "label": "Overview · TBD", @@ -83,7 +98,8 @@ "heading": "Document Types · TBD", "description": "JSON Document의 의미 모델을 소유하는 Document Type 책임과 아직 확정하지 않은 후보를 정리합니다.", "language": "ko", - "navigationGroup": "Document Types" + "navigationGroup": "Document Types", + "documentSource": "docs/public/document-types.md" }, { "path": "/docs/document-types/rich-text", @@ -159,12 +175,13 @@ }, { "path": "/docs/api", - "label": "API Reference", - "title": "json-document API - json-document", - "heading": "json-document API", - "description": "여섯 가지 JSON Document 진입점과 JSON Patch, Pointer, JSONPath 공개 API를 정리합니다.", + "label": "JSON Document Protocol", + "title": "JSON Document Protocol - json-document", + "heading": "JSON Document Protocol", + "description": "로컬·협업 구현이 공유하는 여섯 member의 JSONDocument 계약과 JSON 표준 연산 API를 설명합니다.", "language": "ko", - "navigationGroup": "JSON Document" + "navigationGroup": "JSON Document", + "documentSource": "docs/public/api.md" }, { "path": "/docs/api/json-document", @@ -172,7 +189,8 @@ "title": "JSON Document public API - json-document", "description": "@interactive-os/json-document의 전체 public export와 signature입니다.", "language": "ko", - "navigationGroup": "JSON Document" + "navigationGroup": "JSON Document", + "documentSource": "docs/api-reference/json-document.md" }, { "path": "/docs/api/selection", @@ -180,7 +198,8 @@ "title": "Selection API - json-document", "description": "@interactive-os/json-document-selection의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Editing" + "navigationGroup": "Editing", + "documentSource": "docs/api-reference/selection.md" }, { "path": "/docs/api/editing", @@ -188,7 +207,8 @@ "title": "Editing API - json-document", "description": "@interactive-os/json-document-editing의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Editing" + "navigationGroup": "Editing", + "documentSource": "docs/api-reference/editing.md" }, { "path": "/docs/api/rich-text", @@ -196,7 +216,8 @@ "title": "Rich Text API - json-document", "description": "@interactive-os/json-document-rich-text의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Editing" + "navigationGroup": "Editing", + "documentSource": "docs/api-reference/rich-text.md" }, { "path": "/docs/api/file-intake", @@ -204,7 +225,8 @@ "title": "File Intake API - json-document", "description": "@interactive-os/json-document-file-intake의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Artifact" + "navigationGroup": "Artifact", + "documentSource": "docs/api-reference/file-intake.md" }, { "path": "/docs/api/rich-text-mention", @@ -212,7 +234,8 @@ "title": "Rich Text Mention API - json-document", "description": "@interactive-os/json-document-rich-text-mention의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Hands" + "navigationGroup": "Hands", + "documentSource": "docs/api-reference/rich-text-mention.md" }, { "path": "/docs/api/rich-text-suggestion", @@ -220,7 +243,8 @@ "title": "Rich Text Suggestion API - json-document", "description": "@interactive-os/json-document-rich-text-suggestion의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Hands" + "navigationGroup": "Hands", + "documentSource": "docs/api-reference/rich-text-suggestion.md" }, { "path": "/docs/api/rich-text-suggestion-react", @@ -228,7 +252,8 @@ "title": "Rich Text Suggestion React API - json-document", "description": "@interactive-os/json-document-rich-text-suggestion-react의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Hands" + "navigationGroup": "Hands", + "documentSource": "docs/api-reference/rich-text-suggestion-react.md" }, { "path": "/docs/api/rich-text-mention-react", @@ -236,7 +261,8 @@ "title": "Rich Text Mention React API - json-document", "description": "@interactive-os/json-document-rich-text-mention-react의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Hands" + "navigationGroup": "Hands", + "documentSource": "docs/api-reference/rich-text-mention-react.md" }, { "path": "/docs/api/composer", @@ -244,7 +270,8 @@ "title": "Composer API - json-document", "description": "@interactive-os/json-document-composer의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Hands" + "navigationGroup": "Hands", + "documentSource": "docs/api-reference/composer.md" }, { "path": "/docs/api/composer-react", @@ -252,7 +279,8 @@ "title": "Composer React API - json-document", "description": "@interactive-os/json-document-composer-react의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Hands" + "navigationGroup": "Hands", + "documentSource": "docs/api-reference/composer-react.md" }, { "path": "/docs/api/web", @@ -260,7 +288,8 @@ "title": "Web API - json-document", "description": "@interactive-os/json-document-web의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Adapter" + "navigationGroup": "Adapter", + "documentSource": "docs/api-reference/web.md" }, { "path": "/docs/api/contenteditable", @@ -268,7 +297,8 @@ "title": "Contenteditable API - json-document", "description": "@interactive-os/json-document-contenteditable의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Adapter" + "navigationGroup": "Adapter", + "documentSource": "docs/api-reference/contenteditable.md" }, { "path": "/docs/api/rich-text-web", @@ -276,7 +306,8 @@ "title": "Rich Text Web API - json-document", "description": "@interactive-os/json-document-rich-text-web의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Adapter" + "navigationGroup": "Adapter", + "documentSource": "docs/api-reference/rich-text-web.md" }, { "path": "/docs/api/react", @@ -284,7 +315,8 @@ "title": "React API - json-document", "description": "@interactive-os/json-document-react의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/api-reference/react.md" }, { "path": "/docs/api/react-hook-form", @@ -292,7 +324,8 @@ "title": "React Hook Form API - json-document", "description": "@interactive-os/json-document-react-hook-form의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/api-reference/react-hook-form.md" }, { "path": "/docs/api/ajv", @@ -300,7 +333,8 @@ "title": "Ajv API - json-document", "description": "@interactive-os/json-document-ajv의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/api-reference/ajv.md" }, { "path": "/docs/api/a2ui", @@ -308,7 +342,8 @@ "title": "A2UI API - json-document", "description": "@interactive-os/json-document-a2ui의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/api-reference/a2ui.md" }, { "path": "/docs/connector-a2ui", @@ -316,7 +351,8 @@ "title": "A2UI Connector - json-document", "description": "A2UI 메시지와 JSONL을 JSONDocument로 연결합니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/public/connector-a2ui.md" }, { "path": "/docs/api/zod", @@ -324,7 +360,8 @@ "title": "Zod API - json-document", "description": "@interactive-os/json-document-zod의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/api-reference/zod.md" }, { "path": "/docs/api/tanstack-table", @@ -332,7 +369,8 @@ "title": "TanStack Table API - json-document", "description": "@interactive-os/json-document-tanstack-table의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/api-reference/tanstack-table.md" }, { "path": "/docs/api/rich-text-react", @@ -340,7 +378,8 @@ "title": "Rich Text React API - json-document", "description": "@interactive-os/json-document-rich-text-react의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/api-reference/rich-text-react.md" }, { "path": "/docs/api/affordance", @@ -348,7 +387,8 @@ "title": "Affordance API - json-document", "description": "@interactive-os/json-document-affordance의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Affordance" + "navigationGroup": "Affordance", + "documentSource": "docs/api-reference/affordance.md" }, { "path": "/docs/api/ui-primitives-react", @@ -356,7 +396,8 @@ "title": "UI Primitives API - json-document", "description": "@interactive-os/json-document-ui-primitives-react의 전체 public API입니다.", "language": "ko", - "navigationGroup": "UI Primitives" + "navigationGroup": "UI Primitives", + "documentSource": "docs/api-reference/ui-primitives-react.md" }, { "path": "/docs/api/animation-react", @@ -364,7 +405,8 @@ "title": "Animation API - json-document", "description": "@interactive-os/json-document-animation-react의 전체 public API입니다.", "language": "ko", - "navigationGroup": "UI Primitives" + "navigationGroup": "UI Primitives", + "documentSource": "docs/api-reference/animation-react.md" }, { "path": "/docs/api/markdown-react", @@ -374,7 +416,8 @@ "language": "ko", "navigationGroup": "Artifact", "relatedDemoPath": "/demo/markdown", - "relatedDemoLabel": "Streaming Markdown" + "relatedDemoLabel": "Streaming Markdown", + "documentSource": "docs/api-reference/markdown-react.md" }, { "path": "/demo/markdown", @@ -402,7 +445,8 @@ "title": "Database API - json-document", "description": "@interactive-os/json-document-database의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Hands" + "navigationGroup": "Hands", + "documentSource": "docs/api-reference/database.md" }, { "path": "/docs/api/annotation", @@ -410,7 +454,8 @@ "title": "Annotation API - json-document", "description": "@interactive-os/json-document-annotation의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Hands" + "navigationGroup": "Hands", + "documentSource": "docs/api-reference/annotation.md" }, { "path": "/docs/api/calendar", @@ -418,7 +463,8 @@ "title": "Calendar API - json-document", "description": "@interactive-os/json-document-calendar의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Hands" + "navigationGroup": "Hands", + "documentSource": "docs/api-reference/calendar.md" }, { "path": "/docs/api/collaboration", @@ -426,7 +472,8 @@ "title": "Collaboration API - json-document", "description": "@interactive-os/json-document-collaboration의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Collaboration" + "navigationGroup": "Collaboration", + "documentSource": "docs/api-reference/collaboration.md" }, { "path": "/docs/api/contenteditable-collaboration", @@ -434,17 +481,18 @@ "title": "Contenteditable Collaboration API - json-document", "description": "@interactive-os/json-document-contenteditable-collaboration의 전체 public API입니다.", "language": "ko", - "navigationGroup": "Collaboration" + "navigationGroup": "Collaboration", + "documentSource": "docs/api-reference/contenteditable-collaboration.md" }, { "path": "/docs/collaboration", - "label": "Collaboration", + "label": "Overview", "title": "Collaboration - json-document", "heading": "Collaboration", "description": "같은 JSON Document 계약을 여러 참여자의 인과 변경으로 수렴시킵니다.", "language": "ko", "navigationGroup": "Collaboration", - "sidebar": false + "documentSource": "docs/public/collaboration.md" }, { "path": "/docs/collaboration/replica", @@ -453,7 +501,8 @@ "heading": "Replica", "description": "한 참여자가 소유한 인과 상태와 아직 적용하지 못한 변경을 설명합니다.", "language": "ko", - "navigationGroup": "Collaboration" + "navigationGroup": "Collaboration", + "documentSource": "docs/public/collaboration-replica.md" }, { "path": "/docs/collaboration/lifecycle", @@ -462,7 +511,8 @@ "heading": "Lifecycle", "description": "epoch, checkpoint, restore, compaction이 협업 세대를 접고 되돌리는 방식을 설명합니다.", "language": "ko", - "navigationGroup": "Collaboration" + "navigationGroup": "Collaboration", + "documentSource": "docs/public/collaboration-lifecycle.md" }, { "path": "/docs/collaboration/history", @@ -471,7 +521,8 @@ "heading": "Collaborative History", "description": "다른 참여자를 덮어쓰지 않고 현재 참여자의 인과 기여를 끄거나 켭니다.", "language": "ko", - "navigationGroup": "Collaboration" + "navigationGroup": "Collaboration", + "documentSource": "docs/public/collaboration-history.md" }, { "path": "/docs/collaboration/text", @@ -480,7 +531,8 @@ "heading": "Collaborative Text", "description": "같이 쓰는 문자열의 안정된 글자 단위와 상대 위치를 설명합니다.", "language": "ko", - "navigationGroup": "Collaboration" + "navigationGroup": "Collaboration", + "documentSource": "docs/public/collaboration-text.md" }, { "path": "/docs/collaboration/text/lease", @@ -489,7 +541,18 @@ "heading": "Contenteditable lease", "description": "협업 문자열이 입력 중에도 인과 변경을 멈추지 않게 하는 native-input DOM lease를 설명합니다.", "language": "ko", - "parentPath": "/docs/collaboration/text" + "parentPath": "/docs/collaboration/text", + "documentSource": "docs/public/collaboration-lease.md" + }, + { + "path": "/docs/editing", + "label": "Editing Protocol", + "title": "Editing Protocol - json-document", + "heading": "Editing Protocol", + "description": "Intent, EditingPlan, EditingSession과 EditingSnapshot의 현재 계약 및 아직 동결하지 않은 Hands Profile을 설명합니다.", + "language": "ko", + "navigationGroup": "Editing", + "documentSource": "docs/public/editing.md" }, { "path": "/docs/intent-guide", @@ -498,7 +561,8 @@ "heading": "Editor와 Intent 만들기", "description": "Document editor를 만들고 요청을 EditingIntent로 표현해 EditingResult를 처리합니다.", "language": "ko", - "navigationGroup": "Editing" + "navigationGroup": "Editing", + "documentSource": "docs/public/intent-guide.md" }, { "path": "/docs/intent", @@ -507,7 +571,8 @@ "heading": "Intent 레퍼런스", "description": "EditingIntent, dispatch, EditingResult와 editor별 Intent의 공개 시그니처를 정리합니다.", "language": "ko", - "navigationGroup": "Editing" + "navigationGroup": "Editing", + "documentSource": "docs/public/intent.md" }, { "path": "/docs/topology", @@ -517,7 +582,8 @@ "description": "Selection과 Clipboard가 공유하는 화면 순서인 LineTopology와 GridTopology를 설명합니다.", "language": "ko", "navigationGroup": "Editing", - "relatedDemoPath": "/demo/topology" + "relatedDemoPath": "/demo/topology", + "documentSource": "docs/public/topology.md" }, { "path": "/demo/topology", @@ -536,7 +602,8 @@ "description": "문서 값을 바꾸지 않고 편집 대상을 기억하는 구조적 Selection을 설명합니다.", "language": "ko", "navigationGroup": "Editing", - "relatedDemoPath": "/demo/selection" + "relatedDemoPath": "/demo/selection", + "documentSource": "docs/public/selection.md" }, { "path": "/demo/selection", @@ -555,7 +622,8 @@ "description": "현재 구조적 Selection에서 copy, cut, paste에 사용할 Clipboard payload를 만듭니다.", "language": "ko", "navigationGroup": "Editing", - "relatedDemoPath": "/demo/clipboard" + "relatedDemoPath": "/demo/clipboard", + "documentSource": "docs/public/clipboard.md" }, { "path": "/demo/clipboard", @@ -574,7 +642,8 @@ "description": "document value와 구조적 Selection을 함께 복원하는 로컬 undo와 redo를 설명합니다.", "language": "ko", "navigationGroup": "Editing", - "relatedDemoPath": "/demo/history" + "relatedDemoPath": "/demo/history", + "documentSource": "docs/public/history.md" }, { "path": "/demo/history", @@ -601,7 +670,8 @@ "heading": "Hands", "description": "닫힌 장르의 손과 TBD 손을 고릅니다.", "language": "ko", - "navigationGroup": "Hands" + "navigationGroup": "Hands", + "documentSource": "docs/public/hands.md" }, { "path": "/docs/official-hands", @@ -610,7 +680,8 @@ "heading": "Official Hands · TBD", "description": "디자인과 제품 데이터는 자유롭게 유지하면서 수렴된 편집 기능을 완성된 SDK로 제공하는 방향을 설명합니다.", "language": "ko", - "navigationGroup": "Hands" + "navigationGroup": "Hands", + "documentSource": "docs/public/official-hands.md" }, { "path": "/demo", @@ -637,7 +708,8 @@ "description": "한 줄 목록을 범위 선택으로 집고 옮기는 Order editor를 설명합니다.", "language": "ko", "navigationGroup": "Hands", - "relatedDemoPath": "/demo/order" + "relatedDemoPath": "/demo/order", + "documentSource": "docs/public/order.md" }, { "path": "/demo/order", @@ -655,7 +727,8 @@ "description": "안정된 객체 ID를 키 가족으로 집는 Object editor를 설명합니다.", "language": "ko", "navigationGroup": "Hands", - "relatedDemoPath": "/demo/object" + "relatedDemoPath": "/demo/object", + "documentSource": "docs/public/object.md" }, { "path": "/demo/object", @@ -698,7 +771,8 @@ "description": "호스트가 만든 보이는 노드 줄에서 범위를 집는 Tree editor를 설명합니다.", "language": "ko", "navigationGroup": "Hands", - "relatedDemoPath": "/demo/tree" + "relatedDemoPath": "/demo/tree", + "documentSource": "docs/public/tree.md" }, { "path": "/demo/tree", @@ -743,7 +817,8 @@ "language": "ko", "navigationGroup": "UI Primitives", "relatedDemoPath": "/demo/animation", - "relatedDemoLabel": "Animation" + "relatedDemoLabel": "Animation", + "documentSource": "docs/public/animation.md" }, { "path": "/demo/animation", @@ -763,7 +838,8 @@ "description": "저장된 Table view가 순서·숨김·너비·정렬·필터를 투사하는 Database editor를 설명합니다.", "language": "ko", "navigationGroup": "Hands", - "relatedDemoPath": "/demo/database" + "relatedDemoPath": "/demo/database", + "documentSource": "docs/public/database.md" }, { "path": "/demo/database", @@ -781,7 +857,8 @@ "description": "사람이 agent에게 지시와 구조화된 context를 한 턴으로 건네는 Composer Hands입니다.", "language": "ko", "navigationGroup": "Hands", - "relatedDemoPath": "/demo/composer" + "relatedDemoPath": "/demo/composer", + "documentSource": "docs/public/composer.md" }, { "path": "/demo/composer", @@ -805,7 +882,8 @@ "heading": "Mention", "description": "이름으로 보이는 안정적인 entity reference를 inline atom으로 삽입하는 Hands입니다.", "language": "ko", - "navigationGroup": "Hands" + "navigationGroup": "Hands", + "documentSource": "docs/public/mention.md" }, { "path": "/adapters", @@ -822,7 +900,8 @@ "heading": "json-document Adapters", "description": "Keyboard, Clipboard, Contenteditable 공식 adapter 계약을 설명합니다.", "language": "ko", - "navigationGroup": "Adapter" + "navigationGroup": "Adapter", + "documentSource": "docs/public/adapters.md" }, { "path": "/docs/adapter-keyboard", @@ -831,7 +910,8 @@ "heading": "Keyboard Adapter", "description": "Keyboard, Press와 ARIA platform contract를 Editing 입력으로 번역합니다.", "language": "ko", - "navigationGroup": "Adapter" + "navigationGroup": "Adapter", + "documentSource": "docs/public/adapter-keyboard.md" }, { "path": "/docs/adapter-grid-cell", @@ -840,7 +920,8 @@ "heading": "Grid cell Adapter", "description": "GridPoint를 안정된 DOM cell 주소와 focus lookup에 연결합니다.", "language": "ko", - "navigationGroup": "Adapter" + "navigationGroup": "Adapter", + "documentSource": "docs/public/adapter-grid-cell.md" }, { "path": "/docs/adapter-interaction", @@ -849,7 +930,8 @@ "heading": "Interaction Adapter", "description": "Pointer capture와 HTML Drag and Drop session을 preview, commit, cancel lifecycle에 연결합니다.", "language": "ko", - "navigationGroup": "Adapter" + "navigationGroup": "Adapter", + "documentSource": "docs/public/adapter-interaction.md" }, { "path": "/docs/adapter-clipboard", @@ -858,7 +940,8 @@ "heading": "Clipboard Adapter", "description": "ClipboardEvent를 구조화된 copy, cut, paste 계약에 연결합니다.", "language": "ko", - "navigationGroup": "Adapter" + "navigationGroup": "Adapter", + "documentSource": "docs/public/adapter-clipboard.md" }, { "path": "/docs/adapter-contenteditable", @@ -867,7 +950,8 @@ "heading": "Contenteditable Adapter", "description": "문자열 pointer와 native-input DOM lifecycle을 연결합니다.", "language": "ko", - "navigationGroup": "Adapter" + "navigationGroup": "Adapter", + "documentSource": "docs/public/adapter-contenteditable.md" }, { "path": "/docs/adapter-virtual-selection", @@ -876,7 +960,8 @@ "heading": "Virtual Selection Adapter", "description": "부분 마운트된 DOM의 Native Selection과 전체 model-backed plain-text 복사를 연결합니다.", "language": "ko", - "navigationGroup": "Adapter" + "navigationGroup": "Adapter", + "documentSource": "docs/public/adapter-virtual-selection.md" }, { "path": "/affordances/handles", @@ -949,7 +1034,8 @@ "heading": "json-document Connectors", "description": "React 구독과 선택·커서 질의, React Hook Form, Ajv, Zod, TanStack Table 연결 계약을 설명합니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/public/connectors.md" }, { "path": "/docs/connector-react", @@ -958,7 +1044,8 @@ "heading": "React Connector", "description": "document와 editor의 변경을 React 구독과 선택 질의로 연결합니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/public/connector-react.md" }, { "path": "/docs/react-editing", @@ -969,7 +1056,8 @@ "language": "ko", "parentPath": "/docs/connector-react", "relatedDemoPath": "/connectors/react", - "relatedDemoLabel": "React" + "relatedDemoLabel": "React", + "documentSource": "docs/public/react-editing.md" }, { "path": "/docs/connector-react-hook-form", @@ -978,7 +1066,8 @@ "heading": "React Hook Form Connector", "description": "form draft와 canonical document commit을 연결합니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/public/connector-react-hook-form.md" }, { "path": "/docs/connector-ajv", @@ -987,7 +1076,8 @@ "heading": "Ajv Connector", "description": "Ajv validator와 JSON Document validation을 연결합니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/public/connector-ajv.md" }, { "path": "/docs/connector-zod", @@ -996,7 +1086,8 @@ "heading": "Zod Connector", "description": "Zod schema와 Database document 변환을 설명합니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/public/connector-zod.md" }, { "path": "/docs/connector-zod-validate", @@ -1005,7 +1096,8 @@ "heading": "Zod Validate", "description": "Zod issue path를 JSON Pointer validation 결과로 번역합니다.", "language": "ko", - "parentPath": "/docs/connector-zod" + "parentPath": "/docs/connector-zod", + "documentSource": "docs/public/connector-zod-validate.md" }, { "path": "/docs/connector-tanstack-table", @@ -1014,7 +1106,8 @@ "heading": "TanStack Table Connector", "description": "visible row와 column 순서를 SheetTopology에 연결합니다.", "language": "ko", - "navigationGroup": "Connector" + "navigationGroup": "Connector", + "documentSource": "docs/public/connector-tanstack-table.md" }, { "path": "/connectors/react", @@ -1106,7 +1199,8 @@ "navigationGroup": "UI Primitives", "relatedDemoPath": "/demo/ui-primitives", "relatedDemoLabel": "Design system", - "sidebar": false + "sidebar": false, + "documentSource": "docs/public/ui-primitives.md" }, { "path": "/docs/affordance", @@ -1116,7 +1210,8 @@ "description": "제품이 json-document를 만지는 최전선의 키보드·마우스·커서 손을 API와 사용법으로 설명합니다.", "language": "ko", "navigationGroup": "Affordance", - "sidebar": false + "sidebar": false, + "documentSource": "docs/public/affordance.md" }, { "path": "/docs/affordance/focus", @@ -1126,7 +1221,8 @@ "description": "Tab은 컴포넌트 사이로, 화살표는 컴포넌트 안에서 초점을 옮기는 손입니다.", "language": "ko", "navigationGroup": "Affordance", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-focus.md" }, { "path": "/docs/affordance/caret", @@ -1136,7 +1232,8 @@ "description": "I-beam 클릭으로 글 삽입점을 두고, 화살표로 글자 사이를 옮기는 손입니다.", "language": "ko", "navigationGroup": "Affordance", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-caret.md" }, { "path": "/docs/affordance/select", @@ -1148,7 +1245,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/widgets/listbox", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-select.md" }, { "path": "/docs/affordance/typeahead", @@ -1160,7 +1258,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/order", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-typeahead.md" }, { "path": "/docs/affordance/activate", @@ -1172,7 +1271,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-activate.md" }, { "path": "/docs/affordance/cancel", @@ -1184,7 +1284,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-cancel.md" }, { "path": "/docs/affordance/fold", @@ -1196,7 +1297,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/widgets/tree", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-fold.md" }, { "path": "/docs/affordance/history", @@ -1208,7 +1310,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/widgets/toolbar", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-history.md" }, { "path": "/docs/affordance/delete", @@ -1220,7 +1323,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-delete.md" }, { "path": "/docs/affordance/rename", @@ -1230,7 +1334,8 @@ "description": "F2와 느린 두 번 누르기로 고른 대상의 이름을 고치는 손입니다.", "language": "ko", "navigationGroup": "Affordance", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-rename.md" }, { "path": "/docs/affordance/nudge", @@ -1242,7 +1347,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-nudge.md" }, { "path": "/docs/affordance/hover", @@ -1254,7 +1360,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-hover.md" }, { "path": "/docs/affordance/contextual", @@ -1266,7 +1373,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/calendar", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-contextual.md" }, { "path": "/docs/affordance/double-click", @@ -1276,7 +1384,8 @@ "description": "click detail 2로 열기·단어 고르기·이름 바꾸기를 여는 손입니다.", "language": "ko", "navigationGroup": "Affordance", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-double-click.md" }, { "path": "/docs/affordance/triple-click", @@ -1286,7 +1395,8 @@ "description": "click detail 3으로 줄이나 문단을 고르는 손입니다.", "language": "ko", "navigationGroup": "Affordance", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-triple-click.md" }, { "path": "/docs/affordance/context-menu", @@ -1298,7 +1408,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-context-menu.md" }, { "path": "/docs/affordance/drag", @@ -1310,7 +1421,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/widgets/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-drag.md" }, { "path": "/docs/affordance/marquee", @@ -1322,7 +1434,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-marquee.md" }, { "path": "/docs/affordance/drop", @@ -1334,7 +1447,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-drop.md" }, { "path": "/docs/affordance/copy-drag", @@ -1346,7 +1460,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-copy-drag.md" }, { "path": "/docs/affordance/handles", @@ -1358,7 +1473,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/affordances/handles", "relatedDemoLabel": "Usage 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-handles.md" }, { "path": "/docs/affordance/resize", @@ -1370,7 +1486,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-resize.md" }, { "path": "/docs/affordance/pan", @@ -1382,7 +1499,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-pan.md" }, { "path": "/docs/affordance/scroll", @@ -1394,7 +1512,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-scroll.md" }, { "path": "/docs/affordance/zoom", @@ -1406,7 +1525,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-zoom.md" }, { "path": "/docs/affordance/snap", @@ -1418,7 +1538,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-snap.md" }, { "path": "/docs/affordance/forbid", @@ -1430,7 +1551,8 @@ "navigationGroup": "Affordance", "relatedDemoPath": "/demo/canvas", "relatedDemoLabel": "증명 열기", - "parentPath": "/docs/affordance" + "parentPath": "/docs/affordance", + "documentSource": "docs/public/affordance-forbid.md" }, { "path": "/widgets/toolbar", diff --git a/site/src/app/breadcrumb.tsx b/site/src/app/breadcrumb.tsx index 185b49ed..7ff95394 100644 --- a/site/src/app/breadcrumb.tsx +++ b/site/src/app/breadcrumb.tsx @@ -12,14 +12,14 @@ export type BreadcrumbCrumb = { const overview: BreadcrumbCrumb = { path: "/", label: "Overview" }; const groupLandings: Record = { - Introduction: { path: "/docs", label: "Introduce" }, - "JSON Document": { path: "/docs/foundation", label: "JSON Document" }, + Introduction: { path: "/docs", label: "Introduction" }, + "JSON Document": { path: "/docs/api", label: "JSON Document" }, "Document Types": { path: "/docs/document-types", label: "Document Types" }, - Editing: { path: "/docs/intent-guide", label: "Editing" }, + Editing: { path: "/docs/editing", label: "Editing" }, Collaboration: { path: "/docs/collaboration", label: "Collaboration" }, - Adapter: { path: "/docs/adapters", label: "Platform Adapters" }, - Connector: { path: "/docs/connectors", label: "Ecosystem Connectors" }, - Affordance: { path: "/docs/affordance", label: "Affordances" }, + Adapter: { path: "/docs/adapters", label: "Adapter" }, + Connector: { path: "/docs/connectors", label: "Connector" }, + Affordance: { path: "/docs/affordance", label: "Affordance" }, "UI Primitives": { path: "/docs/ui-primitives", label: "UI Primitives" }, Hands: { path: "/editors", label: "Hands" }, Artifact: { path: "/viewer", label: "Artifact" }, @@ -42,7 +42,7 @@ export function breadcrumbTrail( : undefined; } - const directSection = siteSections.find((section) => section.path === route.path && section.groups.length === 0); + const directSection = siteSections.find((section) => section.path === route.path); const group = routeGroup(route, routes); if (directSection) { stack[0] = { path: directSection.path, label: directSection.label }; diff --git a/site/src/app/navigation-layer-icon.tsx b/site/src/app/navigation-layer-icon.tsx index 8e926627..0c2c3129 100644 --- a/site/src/app/navigation-layer-icon.tsx +++ b/site/src/app/navigation-layer-icon.tsx @@ -4,7 +4,6 @@ import { Braces, Files, Hand, - Library, PanelsTopLeft, type LucideIcon, } from "lucide-react"; @@ -16,13 +15,12 @@ type LayerIcon = { }; const layerIcons: Readonly> = { - introduce: { icon: BookOpen, size: 18 }, + introduction: { icon: BookOpen, size: 18 }, foundation: { icon: Braces, size: 19 }, "building-blocks": { icon: Blocks, size: 18 }, hands: { icon: Hand, size: 19 }, artifact: { icon: Files, size: 18 }, applications: { icon: PanelsTopLeft, size: 19 }, - reference: { icon: Library, size: 18 }, }; export function NavigationLayerIcon(props: { diff --git a/site/src/app/page-descriptors.ts b/site/src/app/page-descriptors.ts index f95c0f9f..08b71676 100644 --- a/site/src/app/page-descriptors.ts +++ b/site/src/app/page-descriptors.ts @@ -21,6 +21,7 @@ export type SiteRoute = { readonly label: string; readonly title: string; readonly heading?: string; + readonly documentSource?: string; readonly description: string; readonly language?: "en" | "ko"; readonly navigationGroup?: SiteNavigationGroup; diff --git a/site/src/app/routeTree.gen.ts b/site/src/app/routeTree.gen.ts index 4251679d..c2aca0f9 100644 --- a/site/src/app/routeTree.gen.ts +++ b/site/src/app/routeTree.gen.ts @@ -61,7 +61,7 @@ import { Route as PageDocsAdapterKeyboardRouteImport } from "./routes/_page/docs import { Route as PageDocsAdapterVirtualSelectionRouteImport } from "./routes/_page/docs/adapter-virtual-selection"; import { Route as PageDocsAdaptersRouteImport } from "./routes/_page/docs/adapters"; import { Route as PageDocsAnimationRouteImport } from "./routes/_page/docs/animation"; -import { Route as PageDocsApiRouteImport } from "./routes/_page/docs/api"; +import { Route as PageDocsBuildingBlocksRouteImport } from "./routes/_page/docs/building-blocks"; import { Route as PageDocsClipboardRouteImport } from "./routes/_page/docs/clipboard"; import { Route as PageDocsComposerRouteImport } from "./routes/_page/docs/composer"; import { Route as PageDocsConceptsRouteImport } from "./routes/_page/docs/concepts"; @@ -74,6 +74,7 @@ import { Route as PageDocsConnectorZodRouteImport } from "./routes/_page/docs/co import { Route as PageDocsConnectorZodValidateRouteImport } from "./routes/_page/docs/connector-zod-validate"; import { Route as PageDocsConnectorsRouteImport } from "./routes/_page/docs/connectors"; import { Route as PageDocsDatabaseRouteImport } from "./routes/_page/docs/database"; +import { Route as PageDocsEditingRouteImport } from "./routes/_page/docs/editing"; import { Route as PageDocsFoundationRouteImport } from "./routes/_page/docs/foundation"; import { Route as PageDocsHistoryRouteImport } from "./routes/_page/docs/history"; import { Route as PageDocsHowWeBuildRouteImport } from "./routes/_page/docs/how-we-build"; @@ -128,6 +129,7 @@ import { Route as PageDocsAffordanceSnapRouteImport } from "./routes/_page/docs/ import { Route as PageDocsAffordanceTripleClickRouteImport } from "./routes/_page/docs/affordance/triple-click"; import { Route as PageDocsAffordanceTypeaheadRouteImport } from "./routes/_page/docs/affordance/typeahead"; import { Route as PageDocsAffordanceZoomRouteImport } from "./routes/_page/docs/affordance/zoom"; +import { Route as PageDocsApiIndexRouteImport } from "./routes/_page/docs/api/index"; import { Route as PageDocsApiA2uiRouteImport } from "./routes/_page/docs/api/a2ui"; import { Route as PageDocsApiAffordanceRouteImport } from "./routes/_page/docs/api/affordance"; import { Route as PageDocsApiAjvRouteImport } from "./routes/_page/docs/api/ajv"; @@ -436,9 +438,9 @@ const PageDocsAnimationRoute = PageDocsAnimationRouteImport.update({ path: "/docs/animation", getParentRoute: () => PageRoute, } as any); -const PageDocsApiRoute = PageDocsApiRouteImport.update({ - id: "/docs/api", - path: "/docs/api", +const PageDocsBuildingBlocksRoute = PageDocsBuildingBlocksRouteImport.update({ + id: "/docs/building-blocks", + path: "/docs/building-blocks", getParentRoute: () => PageRoute, } as any); const PageDocsClipboardRoute = PageDocsClipboardRouteImport.update({ @@ -504,6 +506,11 @@ const PageDocsDatabaseRoute = PageDocsDatabaseRouteImport.update({ path: "/docs/database", getParentRoute: () => PageRoute, } as any); +const PageDocsEditingRoute = PageDocsEditingRouteImport.update({ + id: "/docs/editing", + path: "/docs/editing", + getParentRoute: () => PageRoute, +} as any); const PageDocsFoundationRoute = PageDocsFoundationRouteImport.update({ id: "/docs/foundation", path: "/docs/foundation", @@ -792,169 +799,174 @@ const PageDocsAffordanceZoomRoute = PageDocsAffordanceZoomRouteImport.update({ path: "/docs/affordance/zoom", getParentRoute: () => PageRoute, } as any); +const PageDocsApiIndexRoute = PageDocsApiIndexRouteImport.update({ + id: "/docs/api/", + path: "/docs/api/", + getParentRoute: () => PageRoute, +} as any); const PageDocsApiA2uiRoute = PageDocsApiA2uiRouteImport.update({ - id: "/a2ui", - path: "/a2ui", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/a2ui", + path: "/docs/api/a2ui", + getParentRoute: () => PageRoute, } as any); const PageDocsApiAffordanceRoute = PageDocsApiAffordanceRouteImport.update({ - id: "/affordance", - path: "/affordance", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/affordance", + path: "/docs/api/affordance", + getParentRoute: () => PageRoute, } as any); const PageDocsApiAjvRoute = PageDocsApiAjvRouteImport.update({ - id: "/ajv", - path: "/ajv", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/ajv", + path: "/docs/api/ajv", + getParentRoute: () => PageRoute, } as any); const PageDocsApiAnimationReactRoute = PageDocsApiAnimationReactRouteImport.update({ - id: "/animation-react", - path: "/animation-react", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/animation-react", + path: "/docs/api/animation-react", + getParentRoute: () => PageRoute, } as any); const PageDocsApiAnnotationRoute = PageDocsApiAnnotationRouteImport.update({ - id: "/annotation", - path: "/annotation", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/annotation", + path: "/docs/api/annotation", + getParentRoute: () => PageRoute, } as any); const PageDocsApiCalendarRoute = PageDocsApiCalendarRouteImport.update({ - id: "/calendar", - path: "/calendar", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/calendar", + path: "/docs/api/calendar", + getParentRoute: () => PageRoute, } as any); const PageDocsApiCollaborationRoute = PageDocsApiCollaborationRouteImport.update({ - id: "/collaboration", - path: "/collaboration", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/collaboration", + path: "/docs/api/collaboration", + getParentRoute: () => PageRoute, } as any); const PageDocsApiComposerRoute = PageDocsApiComposerRouteImport.update({ - id: "/composer", - path: "/composer", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/composer", + path: "/docs/api/composer", + getParentRoute: () => PageRoute, } as any); const PageDocsApiComposerReactRoute = PageDocsApiComposerReactRouteImport.update({ - id: "/composer-react", - path: "/composer-react", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/composer-react", + path: "/docs/api/composer-react", + getParentRoute: () => PageRoute, } as any); const PageDocsApiContenteditableRoute = PageDocsApiContenteditableRouteImport.update({ - id: "/contenteditable", - path: "/contenteditable", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/contenteditable", + path: "/docs/api/contenteditable", + getParentRoute: () => PageRoute, } as any); const PageDocsApiContenteditableCollaborationRoute = PageDocsApiContenteditableCollaborationRouteImport.update({ - id: "/contenteditable-collaboration", - path: "/contenteditable-collaboration", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/contenteditable-collaboration", + path: "/docs/api/contenteditable-collaboration", + getParentRoute: () => PageRoute, } as any); const PageDocsApiDatabaseRoute = PageDocsApiDatabaseRouteImport.update({ - id: "/database", - path: "/database", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/database", + path: "/docs/api/database", + getParentRoute: () => PageRoute, } as any); const PageDocsApiEditingRoute = PageDocsApiEditingRouteImport.update({ - id: "/editing", - path: "/editing", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/editing", + path: "/docs/api/editing", + getParentRoute: () => PageRoute, } as any); const PageDocsApiFileIntakeRoute = PageDocsApiFileIntakeRouteImport.update({ - id: "/file-intake", - path: "/file-intake", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/file-intake", + path: "/docs/api/file-intake", + getParentRoute: () => PageRoute, } as any); const PageDocsApiJsonDocumentRoute = PageDocsApiJsonDocumentRouteImport.update({ - id: "/json-document", - path: "/json-document", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/json-document", + path: "/docs/api/json-document", + getParentRoute: () => PageRoute, } as any); const PageDocsApiMarkdownReactRoute = PageDocsApiMarkdownReactRouteImport.update({ - id: "/markdown-react", - path: "/markdown-react", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/markdown-react", + path: "/docs/api/markdown-react", + getParentRoute: () => PageRoute, } as any); const PageDocsApiReactRoute = PageDocsApiReactRouteImport.update({ - id: "/react", - path: "/react", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/react", + path: "/docs/api/react", + getParentRoute: () => PageRoute, } as any); const PageDocsApiReactHookFormRoute = PageDocsApiReactHookFormRouteImport.update({ - id: "/react-hook-form", - path: "/react-hook-form", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/react-hook-form", + path: "/docs/api/react-hook-form", + getParentRoute: () => PageRoute, } as any); const PageDocsApiRichTextRoute = PageDocsApiRichTextRouteImport.update({ - id: "/rich-text", - path: "/rich-text", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/rich-text", + path: "/docs/api/rich-text", + getParentRoute: () => PageRoute, } as any); const PageDocsApiRichTextMentionRoute = PageDocsApiRichTextMentionRouteImport.update({ - id: "/rich-text-mention", - path: "/rich-text-mention", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/rich-text-mention", + path: "/docs/api/rich-text-mention", + getParentRoute: () => PageRoute, } as any); const PageDocsApiRichTextMentionReactRoute = PageDocsApiRichTextMentionReactRouteImport.update({ - id: "/rich-text-mention-react", - path: "/rich-text-mention-react", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/rich-text-mention-react", + path: "/docs/api/rich-text-mention-react", + getParentRoute: () => PageRoute, } as any); const PageDocsApiRichTextReactRoute = PageDocsApiRichTextReactRouteImport.update({ - id: "/rich-text-react", - path: "/rich-text-react", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/rich-text-react", + path: "/docs/api/rich-text-react", + getParentRoute: () => PageRoute, } as any); const PageDocsApiRichTextSuggestionRoute = PageDocsApiRichTextSuggestionRouteImport.update({ - id: "/rich-text-suggestion", - path: "/rich-text-suggestion", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/rich-text-suggestion", + path: "/docs/api/rich-text-suggestion", + getParentRoute: () => PageRoute, } as any); const PageDocsApiRichTextSuggestionReactRoute = PageDocsApiRichTextSuggestionReactRouteImport.update({ - id: "/rich-text-suggestion-react", - path: "/rich-text-suggestion-react", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/rich-text-suggestion-react", + path: "/docs/api/rich-text-suggestion-react", + getParentRoute: () => PageRoute, } as any); const PageDocsApiRichTextWebRoute = PageDocsApiRichTextWebRouteImport.update({ - id: "/rich-text-web", - path: "/rich-text-web", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/rich-text-web", + path: "/docs/api/rich-text-web", + getParentRoute: () => PageRoute, } as any); const PageDocsApiSelectionRoute = PageDocsApiSelectionRouteImport.update({ - id: "/selection", - path: "/selection", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/selection", + path: "/docs/api/selection", + getParentRoute: () => PageRoute, } as any); const PageDocsApiTanstackTableRoute = PageDocsApiTanstackTableRouteImport.update({ - id: "/tanstack-table", - path: "/tanstack-table", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/tanstack-table", + path: "/docs/api/tanstack-table", + getParentRoute: () => PageRoute, } as any); const PageDocsApiUiPrimitivesReactRoute = PageDocsApiUiPrimitivesReactRouteImport.update({ - id: "/ui-primitives-react", - path: "/ui-primitives-react", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/ui-primitives-react", + path: "/docs/api/ui-primitives-react", + getParentRoute: () => PageRoute, } as any); const PageDocsApiWebRoute = PageDocsApiWebRouteImport.update({ - id: "/web", - path: "/web", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/web", + path: "/docs/api/web", + getParentRoute: () => PageRoute, } as any); const PageDocsApiZodRoute = PageDocsApiZodRouteImport.update({ - id: "/zod", - path: "/zod", - getParentRoute: () => PageDocsApiRoute, + id: "/docs/api/zod", + path: "/docs/api/zod", + getParentRoute: () => PageRoute, } as any); const PageDocsCollaborationIndexRoute = PageDocsCollaborationIndexRouteImport.update({ @@ -1052,7 +1064,7 @@ export interface FileRoutesByFullPath { "/docs/adapter-virtual-selection": typeof PageDocsAdapterVirtualSelectionRoute; "/docs/adapters": typeof PageDocsAdaptersRoute; "/docs/animation": typeof PageDocsAnimationRoute; - "/docs/api": typeof PageDocsApiRouteWithChildren; + "/docs/building-blocks": typeof PageDocsBuildingBlocksRoute; "/docs/clipboard": typeof PageDocsClipboardRoute; "/docs/composer": typeof PageDocsComposerRoute; "/docs/concepts": typeof PageDocsConceptsRoute; @@ -1065,6 +1077,7 @@ export interface FileRoutesByFullPath { "/docs/connector-zod-validate": typeof PageDocsConnectorZodValidateRoute; "/docs/connectors": typeof PageDocsConnectorsRoute; "/docs/database": typeof PageDocsDatabaseRoute; + "/docs/editing": typeof PageDocsEditingRoute; "/docs/foundation": typeof PageDocsFoundationRoute; "/docs/history": typeof PageDocsHistoryRoute; "/docs/how-we-build": typeof PageDocsHowWeBuildRoute; @@ -1158,6 +1171,7 @@ export interface FileRoutesByFullPath { "/docs/document-types/$candidate": typeof PageDocsDocumentTypesCandidateRoute; "/connectors/zod/": typeof PageConnectorsZodIndexRoute; "/docs/affordance/": typeof PageDocsAffordanceIndexRoute; + "/docs/api/": typeof PageDocsApiIndexRoute; "/docs/collaboration/": typeof PageDocsCollaborationIndexRoute; "/docs/document-types/": typeof PageDocsDocumentTypesIndexRoute; "/docs/collaboration/text/lease": typeof PageDocsCollaborationTextLeaseRoute; @@ -1210,7 +1224,7 @@ export interface FileRoutesByTo { "/docs/adapter-virtual-selection": typeof PageDocsAdapterVirtualSelectionRoute; "/docs/adapters": typeof PageDocsAdaptersRoute; "/docs/animation": typeof PageDocsAnimationRoute; - "/docs/api": typeof PageDocsApiRouteWithChildren; + "/docs/building-blocks": typeof PageDocsBuildingBlocksRoute; "/docs/clipboard": typeof PageDocsClipboardRoute; "/docs/composer": typeof PageDocsComposerRoute; "/docs/concepts": typeof PageDocsConceptsRoute; @@ -1223,6 +1237,7 @@ export interface FileRoutesByTo { "/docs/connector-zod-validate": typeof PageDocsConnectorZodValidateRoute; "/docs/connectors": typeof PageDocsConnectorsRoute; "/docs/database": typeof PageDocsDatabaseRoute; + "/docs/editing": typeof PageDocsEditingRoute; "/docs/foundation": typeof PageDocsFoundationRoute; "/docs/history": typeof PageDocsHistoryRoute; "/docs/how-we-build": typeof PageDocsHowWeBuildRoute; @@ -1316,6 +1331,7 @@ export interface FileRoutesByTo { "/docs/document-types/$candidate": typeof PageDocsDocumentTypesCandidateRoute; "/connectors/zod": typeof PageConnectorsZodIndexRoute; "/docs/affordance": typeof PageDocsAffordanceIndexRoute; + "/docs/api": typeof PageDocsApiIndexRoute; "/docs/collaboration": typeof PageDocsCollaborationIndexRoute; "/docs/document-types": typeof PageDocsDocumentTypesIndexRoute; "/docs/collaboration/text/lease": typeof PageDocsCollaborationTextLeaseRoute; @@ -1370,7 +1386,7 @@ export interface FileRoutesById { "/_page/docs/adapter-virtual-selection": typeof PageDocsAdapterVirtualSelectionRoute; "/_page/docs/adapters": typeof PageDocsAdaptersRoute; "/_page/docs/animation": typeof PageDocsAnimationRoute; - "/_page/docs/api": typeof PageDocsApiRouteWithChildren; + "/_page/docs/building-blocks": typeof PageDocsBuildingBlocksRoute; "/_page/docs/clipboard": typeof PageDocsClipboardRoute; "/_page/docs/composer": typeof PageDocsComposerRoute; "/_page/docs/concepts": typeof PageDocsConceptsRoute; @@ -1383,6 +1399,7 @@ export interface FileRoutesById { "/_page/docs/connector-zod-validate": typeof PageDocsConnectorZodValidateRoute; "/_page/docs/connectors": typeof PageDocsConnectorsRoute; "/_page/docs/database": typeof PageDocsDatabaseRoute; + "/_page/docs/editing": typeof PageDocsEditingRoute; "/_page/docs/foundation": typeof PageDocsFoundationRoute; "/_page/docs/history": typeof PageDocsHistoryRoute; "/_page/docs/how-we-build": typeof PageDocsHowWeBuildRoute; @@ -1476,6 +1493,7 @@ export interface FileRoutesById { "/_page/docs/document-types/$candidate": typeof PageDocsDocumentTypesCandidateRoute; "/_page/connectors/zod/": typeof PageConnectorsZodIndexRoute; "/_page/docs/affordance/": typeof PageDocsAffordanceIndexRoute; + "/_page/docs/api/": typeof PageDocsApiIndexRoute; "/_page/docs/collaboration/": typeof PageDocsCollaborationIndexRoute; "/_page/docs/document-types/": typeof PageDocsDocumentTypesIndexRoute; "/_page/docs/collaboration/text/lease": typeof PageDocsCollaborationTextLeaseRoute; @@ -1530,7 +1548,7 @@ export interface FileRouteTypes { | "/docs/adapter-virtual-selection" | "/docs/adapters" | "/docs/animation" - | "/docs/api" + | "/docs/building-blocks" | "/docs/clipboard" | "/docs/composer" | "/docs/concepts" @@ -1543,6 +1561,7 @@ export interface FileRouteTypes { | "/docs/connector-zod-validate" | "/docs/connectors" | "/docs/database" + | "/docs/editing" | "/docs/foundation" | "/docs/history" | "/docs/how-we-build" @@ -1636,6 +1655,7 @@ export interface FileRouteTypes { | "/docs/document-types/$candidate" | "/connectors/zod/" | "/docs/affordance/" + | "/docs/api/" | "/docs/collaboration/" | "/docs/document-types/" | "/docs/collaboration/text/lease" @@ -1688,7 +1708,7 @@ export interface FileRouteTypes { | "/docs/adapter-virtual-selection" | "/docs/adapters" | "/docs/animation" - | "/docs/api" + | "/docs/building-blocks" | "/docs/clipboard" | "/docs/composer" | "/docs/concepts" @@ -1701,6 +1721,7 @@ export interface FileRouteTypes { | "/docs/connector-zod-validate" | "/docs/connectors" | "/docs/database" + | "/docs/editing" | "/docs/foundation" | "/docs/history" | "/docs/how-we-build" @@ -1794,6 +1815,7 @@ export interface FileRouteTypes { | "/docs/document-types/$candidate" | "/connectors/zod" | "/docs/affordance" + | "/docs/api" | "/docs/collaboration" | "/docs/document-types" | "/docs/collaboration/text/lease" @@ -1847,7 +1869,7 @@ export interface FileRouteTypes { | "/_page/docs/adapter-virtual-selection" | "/_page/docs/adapters" | "/_page/docs/animation" - | "/_page/docs/api" + | "/_page/docs/building-blocks" | "/_page/docs/clipboard" | "/_page/docs/composer" | "/_page/docs/concepts" @@ -1860,6 +1882,7 @@ export interface FileRouteTypes { | "/_page/docs/connector-zod-validate" | "/_page/docs/connectors" | "/_page/docs/database" + | "/_page/docs/editing" | "/_page/docs/foundation" | "/_page/docs/history" | "/_page/docs/how-we-build" @@ -1953,6 +1976,7 @@ export interface FileRouteTypes { | "/_page/docs/document-types/$candidate" | "/_page/connectors/zod/" | "/_page/docs/affordance/" + | "/_page/docs/api/" | "/_page/docs/collaboration/" | "/_page/docs/document-types/" | "/_page/docs/collaboration/text/lease" @@ -2330,11 +2354,11 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof PageDocsAnimationRouteImport; parentRoute: typeof PageRoute; }; - "/_page/docs/api": { - id: "/_page/docs/api"; - path: "/docs/api"; - fullPath: "/docs/api"; - preLoaderRoute: typeof PageDocsApiRouteImport; + "/_page/docs/building-blocks": { + id: "/_page/docs/building-blocks"; + path: "/docs/building-blocks"; + fullPath: "/docs/building-blocks"; + preLoaderRoute: typeof PageDocsBuildingBlocksRouteImport; parentRoute: typeof PageRoute; }; "/_page/docs/clipboard": { @@ -2421,6 +2445,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof PageDocsDatabaseRouteImport; parentRoute: typeof PageRoute; }; + "/_page/docs/editing": { + id: "/_page/docs/editing"; + path: "/docs/editing"; + fullPath: "/docs/editing"; + preLoaderRoute: typeof PageDocsEditingRouteImport; + parentRoute: typeof PageRoute; + }; "/_page/docs/foundation": { id: "/_page/docs/foundation"; path: "/docs/foundation"; @@ -2799,215 +2830,222 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof PageDocsAffordanceZoomRouteImport; parentRoute: typeof PageRoute; }; + "/_page/docs/api/": { + id: "/_page/docs/api/"; + path: "/docs/api"; + fullPath: "/docs/api/"; + preLoaderRoute: typeof PageDocsApiIndexRouteImport; + parentRoute: typeof PageRoute; + }; "/_page/docs/api/a2ui": { id: "/_page/docs/api/a2ui"; - path: "/a2ui"; + path: "/docs/api/a2ui"; fullPath: "/docs/api/a2ui"; preLoaderRoute: typeof PageDocsApiA2uiRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/affordance": { id: "/_page/docs/api/affordance"; - path: "/affordance"; + path: "/docs/api/affordance"; fullPath: "/docs/api/affordance"; preLoaderRoute: typeof PageDocsApiAffordanceRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/ajv": { id: "/_page/docs/api/ajv"; - path: "/ajv"; + path: "/docs/api/ajv"; fullPath: "/docs/api/ajv"; preLoaderRoute: typeof PageDocsApiAjvRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/animation-react": { id: "/_page/docs/api/animation-react"; - path: "/animation-react"; + path: "/docs/api/animation-react"; fullPath: "/docs/api/animation-react"; preLoaderRoute: typeof PageDocsApiAnimationReactRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/annotation": { id: "/_page/docs/api/annotation"; - path: "/annotation"; + path: "/docs/api/annotation"; fullPath: "/docs/api/annotation"; preLoaderRoute: typeof PageDocsApiAnnotationRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/calendar": { id: "/_page/docs/api/calendar"; - path: "/calendar"; + path: "/docs/api/calendar"; fullPath: "/docs/api/calendar"; preLoaderRoute: typeof PageDocsApiCalendarRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/collaboration": { id: "/_page/docs/api/collaboration"; - path: "/collaboration"; + path: "/docs/api/collaboration"; fullPath: "/docs/api/collaboration"; preLoaderRoute: typeof PageDocsApiCollaborationRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/composer": { id: "/_page/docs/api/composer"; - path: "/composer"; + path: "/docs/api/composer"; fullPath: "/docs/api/composer"; preLoaderRoute: typeof PageDocsApiComposerRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/composer-react": { id: "/_page/docs/api/composer-react"; - path: "/composer-react"; + path: "/docs/api/composer-react"; fullPath: "/docs/api/composer-react"; preLoaderRoute: typeof PageDocsApiComposerReactRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/contenteditable": { id: "/_page/docs/api/contenteditable"; - path: "/contenteditable"; + path: "/docs/api/contenteditable"; fullPath: "/docs/api/contenteditable"; preLoaderRoute: typeof PageDocsApiContenteditableRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/contenteditable-collaboration": { id: "/_page/docs/api/contenteditable-collaboration"; - path: "/contenteditable-collaboration"; + path: "/docs/api/contenteditable-collaboration"; fullPath: "/docs/api/contenteditable-collaboration"; preLoaderRoute: typeof PageDocsApiContenteditableCollaborationRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/database": { id: "/_page/docs/api/database"; - path: "/database"; + path: "/docs/api/database"; fullPath: "/docs/api/database"; preLoaderRoute: typeof PageDocsApiDatabaseRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/editing": { id: "/_page/docs/api/editing"; - path: "/editing"; + path: "/docs/api/editing"; fullPath: "/docs/api/editing"; preLoaderRoute: typeof PageDocsApiEditingRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/file-intake": { id: "/_page/docs/api/file-intake"; - path: "/file-intake"; + path: "/docs/api/file-intake"; fullPath: "/docs/api/file-intake"; preLoaderRoute: typeof PageDocsApiFileIntakeRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/json-document": { id: "/_page/docs/api/json-document"; - path: "/json-document"; + path: "/docs/api/json-document"; fullPath: "/docs/api/json-document"; preLoaderRoute: typeof PageDocsApiJsonDocumentRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/markdown-react": { id: "/_page/docs/api/markdown-react"; - path: "/markdown-react"; + path: "/docs/api/markdown-react"; fullPath: "/docs/api/markdown-react"; preLoaderRoute: typeof PageDocsApiMarkdownReactRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/react": { id: "/_page/docs/api/react"; - path: "/react"; + path: "/docs/api/react"; fullPath: "/docs/api/react"; preLoaderRoute: typeof PageDocsApiReactRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/react-hook-form": { id: "/_page/docs/api/react-hook-form"; - path: "/react-hook-form"; + path: "/docs/api/react-hook-form"; fullPath: "/docs/api/react-hook-form"; preLoaderRoute: typeof PageDocsApiReactHookFormRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/rich-text": { id: "/_page/docs/api/rich-text"; - path: "/rich-text"; + path: "/docs/api/rich-text"; fullPath: "/docs/api/rich-text"; preLoaderRoute: typeof PageDocsApiRichTextRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/rich-text-mention": { id: "/_page/docs/api/rich-text-mention"; - path: "/rich-text-mention"; + path: "/docs/api/rich-text-mention"; fullPath: "/docs/api/rich-text-mention"; preLoaderRoute: typeof PageDocsApiRichTextMentionRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/rich-text-mention-react": { id: "/_page/docs/api/rich-text-mention-react"; - path: "/rich-text-mention-react"; + path: "/docs/api/rich-text-mention-react"; fullPath: "/docs/api/rich-text-mention-react"; preLoaderRoute: typeof PageDocsApiRichTextMentionReactRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/rich-text-react": { id: "/_page/docs/api/rich-text-react"; - path: "/rich-text-react"; + path: "/docs/api/rich-text-react"; fullPath: "/docs/api/rich-text-react"; preLoaderRoute: typeof PageDocsApiRichTextReactRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/rich-text-suggestion": { id: "/_page/docs/api/rich-text-suggestion"; - path: "/rich-text-suggestion"; + path: "/docs/api/rich-text-suggestion"; fullPath: "/docs/api/rich-text-suggestion"; preLoaderRoute: typeof PageDocsApiRichTextSuggestionRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/rich-text-suggestion-react": { id: "/_page/docs/api/rich-text-suggestion-react"; - path: "/rich-text-suggestion-react"; + path: "/docs/api/rich-text-suggestion-react"; fullPath: "/docs/api/rich-text-suggestion-react"; preLoaderRoute: typeof PageDocsApiRichTextSuggestionReactRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/rich-text-web": { id: "/_page/docs/api/rich-text-web"; - path: "/rich-text-web"; + path: "/docs/api/rich-text-web"; fullPath: "/docs/api/rich-text-web"; preLoaderRoute: typeof PageDocsApiRichTextWebRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/selection": { id: "/_page/docs/api/selection"; - path: "/selection"; + path: "/docs/api/selection"; fullPath: "/docs/api/selection"; preLoaderRoute: typeof PageDocsApiSelectionRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/tanstack-table": { id: "/_page/docs/api/tanstack-table"; - path: "/tanstack-table"; + path: "/docs/api/tanstack-table"; fullPath: "/docs/api/tanstack-table"; preLoaderRoute: typeof PageDocsApiTanstackTableRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/ui-primitives-react": { id: "/_page/docs/api/ui-primitives-react"; - path: "/ui-primitives-react"; + path: "/docs/api/ui-primitives-react"; fullPath: "/docs/api/ui-primitives-react"; preLoaderRoute: typeof PageDocsApiUiPrimitivesReactRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/web": { id: "/_page/docs/api/web"; - path: "/web"; + path: "/docs/api/web"; fullPath: "/docs/api/web"; preLoaderRoute: typeof PageDocsApiWebRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/api/zod": { id: "/_page/docs/api/zod"; - path: "/zod"; + path: "/docs/api/zod"; fullPath: "/docs/api/zod"; preLoaderRoute: typeof PageDocsApiZodRouteImport; - parentRoute: typeof PageDocsApiRoute; + parentRoute: typeof PageRoute; }; "/_page/docs/collaboration/": { id: "/_page/docs/collaboration/"; @@ -3068,78 +3106,6 @@ declare module "@tanstack/react-router" { } } -interface PageDocsApiRouteChildren { - PageDocsApiA2uiRoute: typeof PageDocsApiA2uiRoute; - PageDocsApiAffordanceRoute: typeof PageDocsApiAffordanceRoute; - PageDocsApiAjvRoute: typeof PageDocsApiAjvRoute; - PageDocsApiAnimationReactRoute: typeof PageDocsApiAnimationReactRoute; - PageDocsApiAnnotationRoute: typeof PageDocsApiAnnotationRoute; - PageDocsApiCalendarRoute: typeof PageDocsApiCalendarRoute; - PageDocsApiCollaborationRoute: typeof PageDocsApiCollaborationRoute; - PageDocsApiComposerRoute: typeof PageDocsApiComposerRoute; - PageDocsApiComposerReactRoute: typeof PageDocsApiComposerReactRoute; - PageDocsApiContenteditableRoute: typeof PageDocsApiContenteditableRoute; - PageDocsApiContenteditableCollaborationRoute: typeof PageDocsApiContenteditableCollaborationRoute; - PageDocsApiDatabaseRoute: typeof PageDocsApiDatabaseRoute; - PageDocsApiEditingRoute: typeof PageDocsApiEditingRoute; - PageDocsApiFileIntakeRoute: typeof PageDocsApiFileIntakeRoute; - PageDocsApiJsonDocumentRoute: typeof PageDocsApiJsonDocumentRoute; - PageDocsApiMarkdownReactRoute: typeof PageDocsApiMarkdownReactRoute; - PageDocsApiReactRoute: typeof PageDocsApiReactRoute; - PageDocsApiReactHookFormRoute: typeof PageDocsApiReactHookFormRoute; - PageDocsApiRichTextRoute: typeof PageDocsApiRichTextRoute; - PageDocsApiRichTextMentionRoute: typeof PageDocsApiRichTextMentionRoute; - PageDocsApiRichTextMentionReactRoute: typeof PageDocsApiRichTextMentionReactRoute; - PageDocsApiRichTextReactRoute: typeof PageDocsApiRichTextReactRoute; - PageDocsApiRichTextSuggestionRoute: typeof PageDocsApiRichTextSuggestionRoute; - PageDocsApiRichTextSuggestionReactRoute: typeof PageDocsApiRichTextSuggestionReactRoute; - PageDocsApiRichTextWebRoute: typeof PageDocsApiRichTextWebRoute; - PageDocsApiSelectionRoute: typeof PageDocsApiSelectionRoute; - PageDocsApiTanstackTableRoute: typeof PageDocsApiTanstackTableRoute; - PageDocsApiUiPrimitivesReactRoute: typeof PageDocsApiUiPrimitivesReactRoute; - PageDocsApiWebRoute: typeof PageDocsApiWebRoute; - PageDocsApiZodRoute: typeof PageDocsApiZodRoute; -} - -const PageDocsApiRouteChildren: PageDocsApiRouteChildren = { - PageDocsApiA2uiRoute: PageDocsApiA2uiRoute, - PageDocsApiAffordanceRoute: PageDocsApiAffordanceRoute, - PageDocsApiAjvRoute: PageDocsApiAjvRoute, - PageDocsApiAnimationReactRoute: PageDocsApiAnimationReactRoute, - PageDocsApiAnnotationRoute: PageDocsApiAnnotationRoute, - PageDocsApiCalendarRoute: PageDocsApiCalendarRoute, - PageDocsApiCollaborationRoute: PageDocsApiCollaborationRoute, - PageDocsApiComposerRoute: PageDocsApiComposerRoute, - PageDocsApiComposerReactRoute: PageDocsApiComposerReactRoute, - PageDocsApiContenteditableRoute: PageDocsApiContenteditableRoute, - PageDocsApiContenteditableCollaborationRoute: - PageDocsApiContenteditableCollaborationRoute, - PageDocsApiDatabaseRoute: PageDocsApiDatabaseRoute, - PageDocsApiEditingRoute: PageDocsApiEditingRoute, - PageDocsApiFileIntakeRoute: PageDocsApiFileIntakeRoute, - PageDocsApiJsonDocumentRoute: PageDocsApiJsonDocumentRoute, - PageDocsApiMarkdownReactRoute: PageDocsApiMarkdownReactRoute, - PageDocsApiReactRoute: PageDocsApiReactRoute, - PageDocsApiReactHookFormRoute: PageDocsApiReactHookFormRoute, - PageDocsApiRichTextRoute: PageDocsApiRichTextRoute, - PageDocsApiRichTextMentionRoute: PageDocsApiRichTextMentionRoute, - PageDocsApiRichTextMentionReactRoute: PageDocsApiRichTextMentionReactRoute, - PageDocsApiRichTextReactRoute: PageDocsApiRichTextReactRoute, - PageDocsApiRichTextSuggestionRoute: PageDocsApiRichTextSuggestionRoute, - PageDocsApiRichTextSuggestionReactRoute: - PageDocsApiRichTextSuggestionReactRoute, - PageDocsApiRichTextWebRoute: PageDocsApiRichTextWebRoute, - PageDocsApiSelectionRoute: PageDocsApiSelectionRoute, - PageDocsApiTanstackTableRoute: PageDocsApiTanstackTableRoute, - PageDocsApiUiPrimitivesReactRoute: PageDocsApiUiPrimitivesReactRoute, - PageDocsApiWebRoute: PageDocsApiWebRoute, - PageDocsApiZodRoute: PageDocsApiZodRoute, -}; - -const PageDocsApiRouteWithChildren = PageDocsApiRoute._addFileChildren( - PageDocsApiRouteChildren, -); - interface PageRouteChildren { PageDemosRoute: typeof PageDemosRoute; PageEditorsRoute: typeof PageEditorsRoute; @@ -3186,7 +3152,7 @@ interface PageRouteChildren { PageDocsAdapterVirtualSelectionRoute: typeof PageDocsAdapterVirtualSelectionRoute; PageDocsAdaptersRoute: typeof PageDocsAdaptersRoute; PageDocsAnimationRoute: typeof PageDocsAnimationRoute; - PageDocsApiRoute: typeof PageDocsApiRouteWithChildren; + PageDocsBuildingBlocksRoute: typeof PageDocsBuildingBlocksRoute; PageDocsClipboardRoute: typeof PageDocsClipboardRoute; PageDocsComposerRoute: typeof PageDocsComposerRoute; PageDocsConceptsRoute: typeof PageDocsConceptsRoute; @@ -3199,6 +3165,7 @@ interface PageRouteChildren { PageDocsConnectorZodValidateRoute: typeof PageDocsConnectorZodValidateRoute; PageDocsConnectorsRoute: typeof PageDocsConnectorsRoute; PageDocsDatabaseRoute: typeof PageDocsDatabaseRoute; + PageDocsEditingRoute: typeof PageDocsEditingRoute; PageDocsFoundationRoute: typeof PageDocsFoundationRoute; PageDocsHistoryRoute: typeof PageDocsHistoryRoute; PageDocsHowWeBuildRoute: typeof PageDocsHowWeBuildRoute; @@ -3256,12 +3223,43 @@ interface PageRouteChildren { PageDocsAffordanceTripleClickRoute: typeof PageDocsAffordanceTripleClickRoute; PageDocsAffordanceTypeaheadRoute: typeof PageDocsAffordanceTypeaheadRoute; PageDocsAffordanceZoomRoute: typeof PageDocsAffordanceZoomRoute; + PageDocsApiA2uiRoute: typeof PageDocsApiA2uiRoute; + PageDocsApiAffordanceRoute: typeof PageDocsApiAffordanceRoute; + PageDocsApiAjvRoute: typeof PageDocsApiAjvRoute; + PageDocsApiAnimationReactRoute: typeof PageDocsApiAnimationReactRoute; + PageDocsApiAnnotationRoute: typeof PageDocsApiAnnotationRoute; + PageDocsApiCalendarRoute: typeof PageDocsApiCalendarRoute; + PageDocsApiCollaborationRoute: typeof PageDocsApiCollaborationRoute; + PageDocsApiComposerRoute: typeof PageDocsApiComposerRoute; + PageDocsApiComposerReactRoute: typeof PageDocsApiComposerReactRoute; + PageDocsApiContenteditableRoute: typeof PageDocsApiContenteditableRoute; + PageDocsApiContenteditableCollaborationRoute: typeof PageDocsApiContenteditableCollaborationRoute; + PageDocsApiDatabaseRoute: typeof PageDocsApiDatabaseRoute; + PageDocsApiEditingRoute: typeof PageDocsApiEditingRoute; + PageDocsApiFileIntakeRoute: typeof PageDocsApiFileIntakeRoute; + PageDocsApiJsonDocumentRoute: typeof PageDocsApiJsonDocumentRoute; + PageDocsApiMarkdownReactRoute: typeof PageDocsApiMarkdownReactRoute; + PageDocsApiReactRoute: typeof PageDocsApiReactRoute; + PageDocsApiReactHookFormRoute: typeof PageDocsApiReactHookFormRoute; + PageDocsApiRichTextRoute: typeof PageDocsApiRichTextRoute; + PageDocsApiRichTextMentionRoute: typeof PageDocsApiRichTextMentionRoute; + PageDocsApiRichTextMentionReactRoute: typeof PageDocsApiRichTextMentionReactRoute; + PageDocsApiRichTextReactRoute: typeof PageDocsApiRichTextReactRoute; + PageDocsApiRichTextSuggestionRoute: typeof PageDocsApiRichTextSuggestionRoute; + PageDocsApiRichTextSuggestionReactRoute: typeof PageDocsApiRichTextSuggestionReactRoute; + PageDocsApiRichTextWebRoute: typeof PageDocsApiRichTextWebRoute; + PageDocsApiSelectionRoute: typeof PageDocsApiSelectionRoute; + PageDocsApiTanstackTableRoute: typeof PageDocsApiTanstackTableRoute; + PageDocsApiUiPrimitivesReactRoute: typeof PageDocsApiUiPrimitivesReactRoute; + PageDocsApiWebRoute: typeof PageDocsApiWebRoute; + PageDocsApiZodRoute: typeof PageDocsApiZodRoute; PageDocsCollaborationHistoryRoute: typeof PageDocsCollaborationHistoryRoute; PageDocsCollaborationLifecycleRoute: typeof PageDocsCollaborationLifecycleRoute; PageDocsCollaborationReplicaRoute: typeof PageDocsCollaborationReplicaRoute; PageDocsDocumentTypesCandidateRoute: typeof PageDocsDocumentTypesCandidateRoute; PageConnectorsZodIndexRoute: typeof PageConnectorsZodIndexRoute; PageDocsAffordanceIndexRoute: typeof PageDocsAffordanceIndexRoute; + PageDocsApiIndexRoute: typeof PageDocsApiIndexRoute; PageDocsCollaborationIndexRoute: typeof PageDocsCollaborationIndexRoute; PageDocsDocumentTypesIndexRoute: typeof PageDocsDocumentTypesIndexRoute; PageDocsCollaborationTextLeaseRoute: typeof PageDocsCollaborationTextLeaseRoute; @@ -3314,7 +3312,7 @@ const PageRouteChildren: PageRouteChildren = { PageDocsAdapterVirtualSelectionRoute: PageDocsAdapterVirtualSelectionRoute, PageDocsAdaptersRoute: PageDocsAdaptersRoute, PageDocsAnimationRoute: PageDocsAnimationRoute, - PageDocsApiRoute: PageDocsApiRouteWithChildren, + PageDocsBuildingBlocksRoute: PageDocsBuildingBlocksRoute, PageDocsClipboardRoute: PageDocsClipboardRoute, PageDocsComposerRoute: PageDocsComposerRoute, PageDocsConceptsRoute: PageDocsConceptsRoute, @@ -3327,6 +3325,7 @@ const PageRouteChildren: PageRouteChildren = { PageDocsConnectorZodValidateRoute: PageDocsConnectorZodValidateRoute, PageDocsConnectorsRoute: PageDocsConnectorsRoute, PageDocsDatabaseRoute: PageDocsDatabaseRoute, + PageDocsEditingRoute: PageDocsEditingRoute, PageDocsFoundationRoute: PageDocsFoundationRoute, PageDocsHistoryRoute: PageDocsHistoryRoute, PageDocsHowWeBuildRoute: PageDocsHowWeBuildRoute, @@ -3384,12 +3383,45 @@ const PageRouteChildren: PageRouteChildren = { PageDocsAffordanceTripleClickRoute: PageDocsAffordanceTripleClickRoute, PageDocsAffordanceTypeaheadRoute: PageDocsAffordanceTypeaheadRoute, PageDocsAffordanceZoomRoute: PageDocsAffordanceZoomRoute, + PageDocsApiA2uiRoute: PageDocsApiA2uiRoute, + PageDocsApiAffordanceRoute: PageDocsApiAffordanceRoute, + PageDocsApiAjvRoute: PageDocsApiAjvRoute, + PageDocsApiAnimationReactRoute: PageDocsApiAnimationReactRoute, + PageDocsApiAnnotationRoute: PageDocsApiAnnotationRoute, + PageDocsApiCalendarRoute: PageDocsApiCalendarRoute, + PageDocsApiCollaborationRoute: PageDocsApiCollaborationRoute, + PageDocsApiComposerRoute: PageDocsApiComposerRoute, + PageDocsApiComposerReactRoute: PageDocsApiComposerReactRoute, + PageDocsApiContenteditableRoute: PageDocsApiContenteditableRoute, + PageDocsApiContenteditableCollaborationRoute: + PageDocsApiContenteditableCollaborationRoute, + PageDocsApiDatabaseRoute: PageDocsApiDatabaseRoute, + PageDocsApiEditingRoute: PageDocsApiEditingRoute, + PageDocsApiFileIntakeRoute: PageDocsApiFileIntakeRoute, + PageDocsApiJsonDocumentRoute: PageDocsApiJsonDocumentRoute, + PageDocsApiMarkdownReactRoute: PageDocsApiMarkdownReactRoute, + PageDocsApiReactRoute: PageDocsApiReactRoute, + PageDocsApiReactHookFormRoute: PageDocsApiReactHookFormRoute, + PageDocsApiRichTextRoute: PageDocsApiRichTextRoute, + PageDocsApiRichTextMentionRoute: PageDocsApiRichTextMentionRoute, + PageDocsApiRichTextMentionReactRoute: PageDocsApiRichTextMentionReactRoute, + PageDocsApiRichTextReactRoute: PageDocsApiRichTextReactRoute, + PageDocsApiRichTextSuggestionRoute: PageDocsApiRichTextSuggestionRoute, + PageDocsApiRichTextSuggestionReactRoute: + PageDocsApiRichTextSuggestionReactRoute, + PageDocsApiRichTextWebRoute: PageDocsApiRichTextWebRoute, + PageDocsApiSelectionRoute: PageDocsApiSelectionRoute, + PageDocsApiTanstackTableRoute: PageDocsApiTanstackTableRoute, + PageDocsApiUiPrimitivesReactRoute: PageDocsApiUiPrimitivesReactRoute, + PageDocsApiWebRoute: PageDocsApiWebRoute, + PageDocsApiZodRoute: PageDocsApiZodRoute, PageDocsCollaborationHistoryRoute: PageDocsCollaborationHistoryRoute, PageDocsCollaborationLifecycleRoute: PageDocsCollaborationLifecycleRoute, PageDocsCollaborationReplicaRoute: PageDocsCollaborationReplicaRoute, PageDocsDocumentTypesCandidateRoute: PageDocsDocumentTypesCandidateRoute, PageConnectorsZodIndexRoute: PageConnectorsZodIndexRoute, PageDocsAffordanceIndexRoute: PageDocsAffordanceIndexRoute, + PageDocsApiIndexRoute: PageDocsApiIndexRoute, PageDocsCollaborationIndexRoute: PageDocsCollaborationIndexRoute, PageDocsDocumentTypesIndexRoute: PageDocsDocumentTypesIndexRoute, PageDocsCollaborationTextLeaseRoute: PageDocsCollaborationTextLeaseRoute, diff --git a/site/src/app/routes/__root.tsx b/site/src/app/routes/__root.tsx index b87c333f..8f47c8c8 100644 --- a/site/src/app/routes/__root.tsx +++ b/site/src/app/routes/__root.tsx @@ -60,7 +60,7 @@ function AppShell() { key={section.id} to={section.path} activePath={route.path} - className={classes(ui.nav.railItem, section.separated ? ui.nav.railSeparatedItem : undefined, ui.nav.current)} + className={classes(ui.nav.railItem, ui.nav.current)} > {section.label} @@ -100,28 +100,18 @@ function AppShell() { item.navigationGroup !== undefined && section.groups.includes(item.navigationGroup) && item.sidebar !== false - && !item.path.startsWith("/docs/api") + && !item.path.startsWith("/docs/api/") ); + const landingRoute = siteRoutes.find((item) => item.path === section.path && item.navigationGroup === undefined); const sectionLabelId = `site-navigation-${section.id}`; const open = openSections.has(section.id); - if (section.groups.length === 0) return ( - - - {section.label} - - ); if (sectionRoutes.length === 0) return null; return (