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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions architecture/modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -430,5 +430,16 @@
"entrypoint": "packages/json-document-sheet/src/index.ts",
"subpaths": [],
"referencePath": "packages/json-document-sheet/docs/api-reference.md"
},
{
"packageName": "@interactive-os/json-document-sheet-document",
"sourceDirectory": "packages/json-document-sheet-document",
"positions": [
"Document Types"
],
"responsibility": "표 정본 스키마·타입·검증·생성",
"entrypoint": "packages/json-document-sheet-document/src/index.ts",
"subpaths": [],
"referencePath": "packages/json-document-sheet-document/docs/api-reference.md"
}
]
3 changes: 2 additions & 1 deletion audits/document-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"symbol": "CalendarDocument"
},
"sheet": {
"statusNote": "표 스키마·타입·검증·생성은 Sheet Document 정본으로 이동했습니다. [소유자 API](/docs/api/sheet-document)를 Bear·Canvas·Sheet가 함께 사용합니다. Intent·변경 계획·부모 문서 연결은 Editing이 소유하며, 전체 Document Type Profile의 Stable 승격과 형제 레포 통합은 이 범위에서 선언하지 않습니다.",
"why": "grid cell 값은 안정된 row·column identity와 좌표 관계를 유지해야 배열 index 변화에도 의미가 보존되기 때문입니다.",
"does": "column, row와 column-keyed cell 모델을 정의하고 grid topology와 range projection의 기반을 제공합니다.",
"schema": "interface SheetColumn { readonly id: string; readonly label: string }\ninterface SheetRow { readonly id: string; readonly cells: Readonly<Record<string, JSONValue>> }\ninterface SheetDocument {\n readonly columns: ReadonlyArray<SheetColumn>;\n readonly rows: ReadonlyArray<SheetRow>;\n}",
Expand All @@ -102,7 +103,7 @@
{ "name": "rows[].id", "description": "selection과 range가 배열 위치와 무관하게 row를 참조하는 identity입니다." },
{ "name": "rows[].cells", "description": "column id를 key로 JSON cell value를 저장합니다. 알려진 column만 key로 사용해야 합니다." }
],
"sourcePath": "packages/json-document-editing/src/sheet.ts",
"sourcePath": "packages/json-document-sheet-document/src/schema.ts",
"symbol": "SheetDocument"
},
"kanban": {
Expand Down
29 changes: 27 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"packages/json-document-collaboration",
"packages/contenteditable-collaboration",
"site",
"packages/json-document-sheet"
"packages/json-document-sheet",
"packages/json-document-sheet-document"
],
"scripts": {
"dev": "npm run dev -w @interactive-os/json-document-site",
Expand Down
35 changes: 22 additions & 13 deletions packages/json-document-editing/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,11 @@ parseSheetClipboardText(text: string): SheetClipboard | null
```ts
planCalendarSelectionMove(events: ReadonlyArray<CalendarEvent>, occurrences: ReadonlyArray<CalendarOccurrenceSelection>, anchor: CalendarOccurrencePoint, target: CalendarSelectionMoveTarget, options?: { readonly scope?: "this" | "this-and-following" | "all"; readonly createId?: () => string; readonly primary?: CalendarOccurrencePoint; }): CalendarSelectionMovePlan
```
## `planSheetIntent`

```ts
planSheetIntent(document: SheetDocument, selection: SheetSelection, intent: SheetIntent, options?: SheetEditorOptions): SheetPlanResult
```
## `previewCalendarAllDay`

```ts
Expand All @@ -1521,8 +1526,10 @@ projectCalendarOccurrences(events: ReadonlyArray<CalendarEvent>, rangeStart: str
```ts
interface ProjectedSheetOptions {
readonly source: ProjectedSheetSource;
readonly read: () => SheetDocument | null;
/** Null means absent; a present value is validated by the canonical Sheet schema. */
readonly read: () => unknown;
readonly write: (value: SheetDocument) => {readonly ok:boolean;readonly code?:string};
readonly readOnly?:()=>boolean;
readonly sheet?: SheetEditorOptions;
/** Formats with position-based IDs remap selection after structural edits. */
readonly mapSelection?: (selection:SheetSelection,value:SheetDocument) => SheetSelection;
Expand All @@ -1543,6 +1550,11 @@ interface ProjectedSheetSource {
```ts
projectTreeVisibility(nodes: ReadonlyArray<TreeNode>, expandedIds: ReadonlySet<string>): TreeVisibility
```
## `SheetAvailability`

```ts
type SheetAvailability = "ready" | "missing" | "invalid" | "readonly";
```
## `SheetCell`

```ts
Expand All @@ -1567,10 +1579,7 @@ const sheetClipboardFormat: { mimeType: "application/vnd.interactive-os.sheet+js
## `SheetColumn`

```ts
interface SheetColumn extends Record<string, JSONValue> {
readonly id: string;
readonly label: string;
}
type SheetColumn = z.infer<typeof sheetColumnSchema>;
```
## `sheetColumnLabel`

Expand All @@ -1580,15 +1589,13 @@ sheetColumnLabel(index: number): string
## `SheetDocument`

```ts
interface SheetDocument extends Record<string, JSONValue> {
readonly columns: ReadonlyArray<SheetColumn>;
readonly rows: ReadonlyArray<SheetRow>;
}
type SheetDocument = z.infer<typeof sheetDocumentSchema>;
```
## `SheetEditor`

```ts
interface SheetEditor {
readonly availability: SheetAvailability;
readonly capabilities: {readonly resize: boolean};
readonly structure: SheetStructureActions;
readonly snapshot: EditingSnapshot<SheetSelection>;
Expand Down Expand Up @@ -1661,6 +1668,11 @@ type SheetIntent =
```ts
sheetNavigationTarget(topology: GridTopology, selection: SheetSelection, direction: SheetTraversalDirection): { readonly point: GridPoint; readonly preserveRange: boolean; } | null
```
## `SheetPlanResult`

```ts
type SheetPlanResult = {readonly ok:true;readonly plan:EditingPlan<SheetSelection>} | {readonly ok:false;readonly code:string};
```
## `SheetPoint`

```ts
Expand All @@ -1680,10 +1692,7 @@ interface SheetRange extends Record<string, JSONValue> {
## `SheetRow`

```ts
interface SheetRow extends Record<string, JSONValue> {
readonly id: string;
readonly cells: Readonly<Record<string, JSONValue>>;
}
type SheetRow = z.infer<typeof sheetRowSchema>;
```
## `SheetSelection`

Expand Down
34 changes: 34 additions & 0 deletions packages/json-document-editing/docs/sheet-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 하나의 Sheet 편집 계약

Sheet Document 패키지가 공통 스키마와 생성 책임을 소유합니다. Editing의 Sheet 타입
export는 같은 타입을 재노출합니다. Bear·Canvas·독립 Sheet에 별도 문서 모델을 두지 않습니다.

`planSheetIntent(document, selection, intent, options)`는 변경 계획과 다음 선택을
계산하며 입력 문서 변경, 구독 알림, History 기록을 하지 않습니다. 내부 공통 명령
해석은 독립 SheetEditor와 부모 문서 연결도 함께 사용합니다. 선택 전이는 빈 operations를
반환하고 구조 정책·크기 저장 지원 여부도 같은 경로에서 검사합니다.

```ts
import {createSheetDocument} from "@interactive-os/json-document-sheet-document";
import {createSheetEditor, planSheetIntent} from "@interactive-os/json-document-editing";
const value = createSheetDocument();
const editor = createSheetEditor(value);
const planned = planSheetIntent(value, editor.snapshot.selection,
{type: "cell.commit", rowId: "row-1", columnId: "column-1", value: "hello"});
```

`createProjectedSheetEditor`는 read/write/subscribe와 부모 Undo/Redo를 공통 편집 로직에
연결합니다. 임시 자식 편집기나 자식 History를 생성하지 않습니다. 선택은 저장하지 않고
문서 변경은 검증 후 write 한 번으로 전달합니다. 실패한 write는 다음 선택을 적용하지
않습니다. write 구현은 부모 문서에 원자적으로 반영하고 실패 시 원본을 유지해야 합니다.
read는 부모 snapshot.value의 변경에 맞춰 갱신됩니다.

`availability`는 ready/missing/invalid/readonly입니다. read의 null은 누락을 뜻하고,
나머지 값은 Sheet 정본 스키마로 검증합니다. readOnly 정책은 변경을 막되 선택·복사는
허용합니다. SheetHand는 상태를 표시하고 편집 진입을 막으며 열린 draft를 취소합니다.

Markdown 연결은 표와 원문 사이의 변환, Object 연결은 내장 Sheet payload 접근을 소유합니다.
Canvas가 바깥 객체 위치·크기와 활성화 경계를 소유하고 Sheet가 내부 셀과 행·열 편집을
소유합니다. 입력 프로파일은 Affordance, 표현과 편집 필드는 Hand의 계약으로 교체합니다.

Usage: [표 편집](/demo/sheet), [Sheet 앱](/applications/sheet), [Canvas](/demo/canvas).
14 changes: 11 additions & 3 deletions packages/json-document-editing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
"provenance": true,
"tag": "next"
},
"files": ["dist", "!dist/.tsbuildinfo", "README.md", "docs", "LICENSE"],
"files": [
"dist",
"!dist/.tsbuildinfo",
"README.md",
"docs",
"LICENSE"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
Expand All @@ -38,7 +44,8 @@
"@interactive-os/json-document-calendar-document": "^0.1.0-rc.0",
"@interactive-os/json-document": "^3.0.0",
"@interactive-os/json-document-selection": "^0.1.0-rc.0",
"@interactive-os/json-document-markdown": "^0.1.0-rc.0"
"@interactive-os/json-document-markdown": "^0.1.0-rc.0",
"@interactive-os/json-document-sheet-document": "^0.1.0-rc.0"
},
"devDependencies": {
"@interactive-os/json-document-object-document": "*",
Expand All @@ -48,6 +55,7 @@
"@types/node": "^25.9.0",
"typescript": "^5.0.0",
"vitest": "^4.1.7",
"@interactive-os/json-document-markdown": "*"
"@interactive-os/json-document-markdown": "*",
"@interactive-os/json-document-sheet-document": "*"
}
}
4 changes: 4 additions & 0 deletions packages/json-document-editing/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,7 @@ export {sheetSelectionSummary} from "./sheet-summary.js";
export {parseSheetClipboardText} from "./sheet-text-clipboard.js";
export {createProjectedSheetEditor, type ProjectedSheetOptions, type ProjectedSheetSource} from "./projected-sheet.js";
export {createCanvasSheet, createObjectSheetEditor, sheetEmbeddedDocumentType} from "./object-sheet.js";

export {planSheetIntent} from "./sheet-plan.js";
export type {SheetPlanResult} from "./sheet-plan.js";
export type {SheetAvailability} from "./sheet.js";
10 changes: 4 additions & 6 deletions packages/json-document-editing/src/object-sheet.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import {createCanvasEmbeddedDocument,type CanvasObjectDraft,type ObjectBounds} from "@interactive-os/json-document-object-document";
import {createProjectedSheetEditor} from "./projected-sheet.js";
import {assertSheetDocument} from "./sheet-validation.js";
import {sheetColumnLabel} from "./sheet-structure.js";
import {createSheetDocument} from "@interactive-os/json-document-sheet-document";
import type {ObjectEditor} from "./object.js";
import type {SheetDocument,SheetEditor} from "./sheet.js";
import type {SheetEditor} from "./sheet.js";

export const sheetEmbeddedDocumentType="sheet/1";
/** Create a table document within the Object owner's spatial container. */
export function createCanvasSheet(bounds:ObjectBounds,options:{rows?:number;columns?:number}={}):CanvasObjectDraft {
const rowCount=options.rows ?? 4,columnCount=options.columns ?? 3;
if(!Number.isSafeInteger(rowCount)||!Number.isSafeInteger(columnCount)||rowCount<1||columnCount<1)throw new RangeError("Sheet dimensions must be positive integers.");
const columns=Array.from({length:columnCount},(_,i)=>({id:`c${i}`,label:sheetColumnLabel(i),width:120}));
const document:SheetDocument={columns,rows:Array.from({length:rowCount},(_,i)=>({id:`r${i}`,cells:Object.fromEntries(columns.map(column=>[column.id,""]))}))};
const document=createSheetDocument({rows:rowCount,columns:columnCount,columnWidth:120});
return createCanvasEmbeddedDocument(sheetEmbeddedDocumentType,document,bounds,"표");
}
/** The parent remains the only persisted history owner, including edits from the table toolbar. */
Expand All @@ -20,7 +18,7 @@ export function createObjectSheetEditor(parent:ObjectEditor,objectId:string):She
read(){
const object=(parent.snapshot.value as {objects:ReadonlyArray<Record<string,unknown>>}).objects.find(object=>object.id === objectId);
if(object?.kind !== "embedded-document" || object.documentType !== sheetEmbeddedDocumentType)return null;
try{assertSheetDocument(object.document as SheetDocument);return object.document as SheetDocument;}catch{return null;}
return object.document;
},
write(document){return parent.dispatch({type:"object.document",objectId,document});},
});
Expand Down
Loading