Skip to content
Open
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
31 changes: 29 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
22 changes: 21 additions & 1 deletion packages/json-document-affordance/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,20 @@ interface GestureState {
readonly type: string;
}
```
## `GridEditingProfile`

```ts
interface GridEditingProfile {
readonly enter: "edit" | "move";
/** Existing-content activation (F2/double click); replacement typing always places the caret at the end. */
readonly editSelection: "all" | "end";
}
```
## `gridEditingProfiles`

```ts
const gridEditingProfiles: Readonly<Record<"document-table" | "spreadsheet-grid" | "spreadsheet-mac", GridEditingProfile>>
```
## `GridFillBounds`

```ts
Expand Down Expand Up @@ -864,7 +878,7 @@ renameAffordance(input: Pick<WebKeyboardStroke, "key"> | { readonly type: "point
```ts
interface RenameSession<Key> {
getSnapshot(): RenameSessionSnapshot<Key> | null;
begin(key: Key, label: string): void;
begin(key: Key, label: string, initialSelection?: "all" | "end"): void;
update(draft: string): void;
handleKey(key: string): boolean;
handlePointer(key: Key, label: string, detail: number, timeStamp: number): boolean;
Expand All @@ -878,6 +892,7 @@ interface RenameSession<Key> {
interface RenameSessionSnapshot<Key> {
readonly key: Key;
readonly draft: string;
readonly initialSelection?: "all" | "end";
}
```
## `resizeAffordance`
Expand Down Expand Up @@ -917,6 +932,11 @@ resizeValueForKey(current: number, key: string, shiftKey: boolean, axis: "x" | "
```ts
resolveAffordanceKey(stroke: WebKeyboardStroke): AffordancePreview
```
## `resolveGridEditActivation`

```ts
resolveGridEditActivation(profile: GridEditingProfile, existingText: string, replacementText?: string): { draft: string; initialSelection: "all" | "end"; }
```
## `selectAllAffordance`

```ts
Expand Down
2 changes: 2 additions & 0 deletions packages/json-document-affordance/docs/cell-editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
편집 중 일반 문자·방향키·Mod+A는 native field에 남깁니다. `rename` hand의 `initialText`는 입력으로 시작하는 초안, `move`는 성공적인 확정 이후 이동 방향입니다. 조합 중 키는 Web owner의 `isWebComposingKey`로 먼저 제외합니다.

[Sheet Usage](/demo/sheet)에서 `useRenameSession`과 함께 사용합니다. 초안 확정이 거절되면 이동하지 않고 초안을 유지해야 합니다.

`gridEditingProfiles["spreadsheet-mac"]`은 선택 상태에서 Enter로 편집을 시작합니다. 편집 중 Enter는 확정 후 아래로 이동합니다. [Sheet Views Usage](/demo/sheet-views)에서 SheetHand는 Mac의 기본 spreadsheet-grid에 이 정책을 적용하며, 명시한 프로파일 객체는 그대로 사용합니다.
8 changes: 8 additions & 0 deletions packages/json-document-affordance/docs/grid-interaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@ const width=storedResizeValue(143.6,{min:40,max:1200});
```

[Sheet Usage](/demo/sheet)의 채우기 핸들과 행열 리사이즈에서 확인할 수 있습니다. 드래그 미리보기는 문서를 변경하지 않고 확정 시 Editing Intent 하나를 실행합니다. 기존 Sheet의 수식 및 수열 채우기는 해당 Sheet 엔진에 유지되며, JSON/Markdown Hand의 `range.fill`은 원본 값 패턴을 반복 복제합니다.

## 표 입력 프로파일

`GridEditingProfile`과 `gridEditingProfiles`는 배치 환경과 독립인 입력 정책입니다. `document-table`은 Enter로 편집하고 `spreadsheet-grid`는 Enter로 이동합니다. `editSelection`은 F2/더블클릭으로 기존 내용을 편집할 때 전체 선택할지 끝에 둘지 정합니다. 직접 타이핑은 기존 값을 대체하며 첫 글자 뒤에서 이어 씁니다. `createRenameSession.begin`의 `initialSelection`이 이 결정을 초안과 함께 전달합니다.

Canvas·Bear는 별도 입력 프로파일 이름이 아닙니다. 문서 형식의 크기 저장 가능 여부는 Editing capability, 외부로 나가는 포커스는 Hand의 `onExit`/`onDeactivate`, 화면 좌표 변환은 Web이 각각 소유합니다. `cellEditingAffordance`는 초안이 없는 Escape를 `cancel`로 내보내므로 배치 환경이 외부 편집기로 복귀시킬 수 있습니다.

`resolveGridEditActivation(profile, existingText, replacementText?)`가 초안과 초기 선택을 함께 계산합니다. Hand는 이를 RenameSession에 전달할 뿐 첫 입력/F2의 정책을 다시 판단하지 않습니다.
1 change: 1 addition & 0 deletions packages/json-document-affordance/src/cell-editing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function cellEditingAffordance(stroke: WebKeyboardStroke, state: {readonl
if (action === "enter" || action === "previous") return {hand: {type: "rename", action: "commit", move: action === "previous" ? "up" : "down"}};
return {hand: null};
}
if (action === "cancel") return {hand:{type:"cancel"}};
const all = selectAllAffordance(stroke, state, {repeat: "preserve"});
if (all.hand) return all;
if (stroke.key === " " && !stroke.metaKey && !stroke.altKey && stroke.ctrlKey !== stroke.shiftKey) return {hand:{type:"select",operation:"replace",axis:stroke.ctrlKey ? "column" : "row"}};
Expand Down
16 changes: 16 additions & 0 deletions packages/json-document-affordance/src/grid-editing-profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** Input policy is independent of the document format and its embedding environment. */
export interface GridEditingProfile {
readonly enter: "edit" | "move";
/** Existing-content activation (F2/double click); replacement typing always places the caret at the end. */
readonly editSelection: "all" | "end";
}
export const gridEditingProfiles: Readonly<Record<"document-table" | "spreadsheet-grid" | "spreadsheet-mac", GridEditingProfile>> = {
"document-table": {enter:"edit",editSelection:"all"},
"spreadsheet-mac": {enter:"edit",editSelection:"all"},
"spreadsheet-grid": {enter:"move",editSelection:"all"},
};

/** Replacement typing and existing-content editing have distinct initial caret contracts. */
export function resolveGridEditActivation(profile: GridEditingProfile, existingText: string, replacementText?: string): {draft: string; initialSelection: "all" | "end"} {
return {draft:replacementText ?? existingText,initialSelection:replacementText === undefined ? profile.editSelection : "end"};
}
1 change: 1 addition & 0 deletions packages/json-document-affordance/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,4 @@ export { clampResizeValue, storedResizeValue, resizeValueForKey, collapseResizeV
export type { ResizeBounds } from "./axis-resize.js";
export { extendGridFill } from "./grid-fill.js";
export type { GridFillBounds } from "./grid-fill.js";
export {gridEditingProfiles, resolveGridEditActivation, type GridEditingProfile} from "./grid-editing-profile.js";
7 changes: 4 additions & 3 deletions packages/json-document-affordance/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,12 @@ export function createTypeaheadSession<Key>(options: {
export interface RenameSessionSnapshot<Key> {
readonly key: Key;
readonly draft: string;
readonly initialSelection?: "all" | "end";
}

export interface RenameSession<Key> {
getSnapshot(): RenameSessionSnapshot<Key> | null;
begin(key: Key, label: string): void;
begin(key: Key, label: string, initialSelection?: "all" | "end"): void;
update(draft: string): void;
handleKey(key: string): boolean;
handlePointer(key: Key, label: string, detail: number, timeStamp: number): boolean;
Expand Down Expand Up @@ -111,8 +112,8 @@ export function createRenameSession<Key>(options: ({
}
return {
getSnapshot: () => snapshot,
begin(key, label) {
publish({ key, draft: label });
begin(key, label, initialSelection) {
publish({ key, draft: label, ...(initialSelection ? {initialSelection} : {}) });
},
update(draft) {
if (snapshot !== null) publish({ ...snapshot, draft });
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {expect,test} from "vitest";
import {cellEditingAffordance,createRenameSession,gridEditingProfiles} from "../src/index.js";
test("input policy distinguishes activation from replacement and outer cancellation",()=>{
expect(gridEditingProfiles['document-table'].enter).toBe('edit');
expect(cellEditingAffordance({key:'Escape',shiftKey:false,metaKey:false,ctrlKey:false},{editing:false,allSelected:false}).hand).toEqual({type:'cancel'});
expect(cellEditingAffordance({key:'a',shiftKey:false,metaKey:false,ctrlKey:false},{editing:false,allSelected:false}).hand).toMatchObject({type:'rename',initialText:'a'});
const rename=createRenameSession({onCommit:()=>{}});rename.begin('cell','a','end');rename.update('ab');expect(rename.getSnapshot()).toEqual({key:'cell',draft:'ab',initialSelection:'end'});
});
9 changes: 7 additions & 2 deletions packages/json-document-canvas/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,15 @@ interface CanvasHandProps {
readonly selectProfile?: PlaneSelectProfile;
}
```
## `CanvasSheetObject`

```ts
CanvasSheetObject({ object, editor, active, onDeactivate }: { readonly object: Extract<CanvasObject, { kind: "embedded-document"; }>; readonly editor: ObjectEditor; readonly active: boolean; readonly onDeactivate: () => void; }): import("<repository>/node_modules/@types/react/jsx-runtime").JSX.Element
```
## `CanvasTool`

```ts
type CanvasTool = "select" | Exclude<CanvasObjectKind, "image">;
type CanvasTool = "select" | "table" | Exclude<CanvasObjectKind, "image" | "embedded-document">;
```
## `createCanvasClipboardBinding`

Expand All @@ -59,5 +64,5 @@ createCanvasClipboardBinding(editor: ObjectEditor, policy: CanvasClipboardPolicy
## `useCanvasHand`

```ts
useCanvasHand(editor: ObjectEditor, style: CanvasCreationStyle, selectProfile?: PlaneSelectProfile): { document: CanvasDocument; snapshot: import("<repository>/packages/json-document-editing/src/session").EditingSnapshot<ObjectSelection>; ... 23 more ...; surfaceProps: { ...; }; }
useCanvasHand(editor: ObjectEditor, style: CanvasCreationStyle, selectProfile?: PlaneSelectProfile, activateEmbedded?: (objectId: string) => void): { document: CanvasDocument; ... 24 more ...; surfaceProps: { ...; }; }
```
24 changes: 24 additions & 0 deletions packages/json-document-canvas/docs/embedded-sheet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Canvas 안의 표

Canvas 도구 모음의 표 아이콘으로 객체를 생성합니다. 클릭은 기본 크기, 드래그는 지정한 크기로 만듭니다. 객체 선택 상태에서는 이동·리사이즈·복제·삭제가 작동하고 더블클릭·F2 또는 선택 타깃의 Enter로 내부 셀 편집에 들어갑니다. 셀 편집 중 Escape는 초안을 취소하고, 셀 선택 상태의 Escape는 Canvas 객체 선택으로 돌아갑니다. 표 끝의 Tab/Shift+Tab도 바깥으로 돌아갑니다.

내부 UI는 Bear와 Sheet 앱이 소비하는 `SheetHand`입니다. Canvas가 별도의 셀 키보드·선택·클립보드 구현을 갖지 않습니다. `CanvasSheetObject`는 SVG foreignObject 프레임, 활성화와 포커스 연결만 조합하며, 활성 여부가 표의 모양과 크기를 바꾸지 않습니다.

```tsx
import {createCanvasSheet, createObjectEditor} from '@interactive-os/json-document-editing';
import {CanvasHand} from '@interactive-os/json-document-canvas';

const table = createCanvasSheet({x:100, y:100, width:480, height:280});
const editor = createObjectEditor({profile:'canvas/1',width:1280,height:720,objects:[]});
editor.dispatch({type:'object.create',object:table});
// <CanvasHand editor={editor} creationStyle={creationStyle} />
```

- Object Document 소유자는 `kind: "embedded-document"`, `documentType`, `document`로 공간 객체와 내장 문서의 경계를 정의합니다. 표 데이터 모델은 중복 정의하지 않습니다.
- Editing의 `createCanvasSheet`가 `sheet/1` 문서를 생성하고, `createObjectSheetEditor`가 부모의 해당 객체에 연결합니다.
- `createProjectedSheetEditor`는 Markdown 표와 Object 표에 공통인 projection·선택·부모 History 연결을 소유합니다. 표 명령 하나는 부모 문서 transaction 하나가 됩니다.
- 바깥 객체 이동/리사이즈와 내부 행열 리사이즈는 별개입니다. 내부 크기는 layout 좌표로 저장하므로 SVG 화면 배율이 저장값에 섞이지 않습니다.

표 내용은 Canvas JSON 안에 저장되며 JSON 재열기·객체 복제·복사/붙여넣기·삭제/Undo에서도 보존됩니다. 알 수 없는 embedded documentType은 임의 편집기를 만들지 않고 지원하지 않는 문서로 표시합니다. Object 소유자는 내장 payload를 보존하며 payload의 구체 모델 검증은 각 편집 어댑터가 소유합니다.

현재 좌표 보정은 Canvas의 축 정렬 SVG viewport 및 CSS 확대/축소를 대상으로 합니다. 회전/기울이기와 수식, Excel 파일 호환성은 지원 범위가 아닙니다. 실제 Usage는 `/demo/canvas`와 `/widgets/canvas`입니다.
2 changes: 2 additions & 0 deletions packages/json-document-canvas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
},
"dependencies": { "lucide-react": "^1.33.0" },
"peerDependencies": {
"@interactive-os/json-document-sheet": ">=0.1.0-rc.0 <1",
"@interactive-os/json-document-file-intake": ">=0.1.0-rc.0 <1",
"@interactive-os/json-document-object-document": ">=0.1.0-rc.0 <1",
"@interactive-os/json-document-editing": ">=0.1.0-rc.0 <1",
Expand All @@ -25,6 +26,7 @@
"react": "^18.0.0 || ^19.0.0"
},
"devDependencies": {
"@interactive-os/json-document-sheet": "*",
"@interactive-os/json-document-file-intake": "*",
"@interactive-os/json-document-object-document": "*", "@interactive-os/json-document-editing": "*",
"@interactive-os/json-document-affordance": "*", "@interactive-os/json-document-web": "*",
Expand Down
Loading