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
48 changes: 33 additions & 15 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,6 @@ Editing
|-- Clipboard
`-- History

Hands
|-- Document
|-- Order
|-- Object
|-- Sheet
|-- Tree
|-- Kanban
`-- Database

Adapter
|-- Keyboard
|-- Clipboard adapter
Expand All @@ -76,14 +67,41 @@ Connector
|-- Zod
`-- TanStack Table

제품 화면
|-- Toolbar
|-- Listbox
|-- Grid
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

Hands
|-- Document
|-- Canvas
|-- Order
|-- Object
|-- Sheet
|-- Tree
`-- Board
|-- Kanban
`-- Database
```

## 규범 우선순위
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ source and release history remains available from Git commits and version tags.

## Next

- Opened Affordance as the product-facing keyboard and mouse contract, with
usage and API for select, fold, drag, and undo/redo. Live widget screens
stay as proofs, not the catalog entrance.
- Opened Document, Canvas, Tree, and Board on 제품 화면, and made Toolbar,
Listbox, Grid, and those widgets share prop bindings that live demos
consume without forking. Database stays on Grid.
Expand Down
53 changes: 53 additions & 0 deletions docs/evaluate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,32 @@ const publicDocs = {
quickstart: read("docs/public/quickstart.md"),
api: read("docs/public/api.md"),
adapters: read("docs/public/adapters.md"),
affordance: read("docs/public/affordance.md"),
affordanceSelect: read("docs/public/affordance-select.md"),
affordanceFold: read("docs/public/affordance-fold.md"),
affordanceDrag: read("docs/public/affordance-drag.md"),
affordanceHistory: read("docs/public/affordance-history.md"),
affordanceFocus: read("docs/public/affordance-focus.md"),
affordanceCaret: read("docs/public/affordance-caret.md"),
affordanceTypeahead: read("docs/public/affordance-typeahead.md"),
affordanceActivate: read("docs/public/affordance-activate.md"),
affordanceCancel: read("docs/public/affordance-cancel.md"),
affordanceDelete: read("docs/public/affordance-delete.md"),
affordanceRename: read("docs/public/affordance-rename.md"),
affordanceNudge: read("docs/public/affordance-nudge.md"),
affordanceHover: read("docs/public/affordance-hover.md"),
affordanceDoubleClick: read("docs/public/affordance-double-click.md"),
affordanceTripleClick: read("docs/public/affordance-triple-click.md"),
affordanceContextMenu: read("docs/public/affordance-context-menu.md"),
affordanceMarquee: read("docs/public/affordance-marquee.md"),
affordanceDrop: read("docs/public/affordance-drop.md"),
affordanceCopyDrag: read("docs/public/affordance-copy-drag.md"),
affordanceResize: read("docs/public/affordance-resize.md"),
affordancePan: read("docs/public/affordance-pan.md"),
affordanceScroll: read("docs/public/affordance-scroll.md"),
affordanceZoom: read("docs/public/affordance-zoom.md"),
affordanceSnap: read("docs/public/affordance-snap.md"),
affordanceForbid: read("docs/public/affordance-forbid.md"),
connectors: read("docs/public/connectors.md"),
reactEditing: read("docs/public/react-editing.md"),
collaboration: read("docs/public/collaboration.md"),
Expand Down Expand Up @@ -95,6 +121,7 @@ const activeCompanionPackages = new Set([
"@interactive-os/json-document-react",
"@interactive-os/json-document-react-hook-form",
"@interactive-os/json-document-ajv",
"@interactive-os/json-document-affordance",
"@interactive-os/json-document-zod",
"@interactive-os/json-document-tanstack-table",
"@interactive-os/json-document-web",
Expand All @@ -105,6 +132,32 @@ const activeCompanionPackages = new Set([

if (JSON.stringify(fileNames("docs/public")) !== JSON.stringify([
"adapters.md",
"affordance-activate.md",
"affordance-cancel.md",
"affordance-caret.md",
"affordance-context-menu.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-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",
"api.md",
"clipboard.md",
"collaboration-history.md",
Expand Down
36 changes: 36 additions & 0 deletions docs/public/affordance-activate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Activate

TBD.

Activate는 대상의 기본 동작을 실행하는 손입니다. `pointer` 커서가 이
손을 가리킵니다. Enter와 기본 클릭은 같은 손입니다. Listbox에서 Space는
[Select](affordance-select.md)의 toggle입니다.

```ts
import {
activateAffordance,
resolveAffordanceKey,
} from "@interactive-os/json-document-affordance";

function onPointerDown(event: PointerEvent, itemId: string) {
if (activateAffordance(event) === "activate") hostOpen(itemId);
}

function onKeyDown(event: KeyboardEvent) {
if (activateAffordance(event) === "activate") hostOpen(focusKey);
const command = resolveAffordanceKey(event);
if (command?.type === "toggle") {
editor.dispatch({ type: "selection.set", itemId: focusKey, mode: "toggle" });
}
}
```

호스트는 기본 동작이 무엇인지 정합니다. 목록 toggle은 json-document 선택으로,
열기는 호스트 Intent로 갑니다.

닫는 손:
- 기본 클릭 (`detail` 1)
- Enter
- Space는 장르 Intent: 버튼은 activate, 목록은 toggle

근거: [APG Button](https://www.w3.org/WAI/ARIA/apg/patterns/button/), [CSS UI cursor `pointer`](https://www.w3.org/TR/css-ui-4/#cursor)
40 changes: 40 additions & 0 deletions docs/public/affordance-cancel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Escape

TBD.

Escape는 진행 중인 손과 임시 UI를 버리는 손입니다. Escape 키는 메뉴·대화·
드래그를 닫고, `pointercancel`은 포인터 제스처를 중단합니다.

```ts
import {
applyAffordance,
escapeAffordance,
} from "@interactive-os/json-document-affordance";

function onKeyDown(event: KeyboardEvent) {
applyAffordance(escapeAffordance(event), {
hand: (hand) => {
if (hand.type !== "cancel") return;
setMarquee(null);
setDrag(null);
},
});
}

function onPointerCancel(event: PointerEvent) {
applyAffordance(escapeAffordance(event), {
hand: (hand) => {
if (hand.type === "cancel") setDrag(null);
},
});
}
```

호스트는 무엇이 열려 있는지를 가집니다. 버리는 손은 보통 호스트 화면
상태입니다. 문서 값은 바꾸지 않습니다.

닫는 손:
- Escape: 메뉴, 대화, 드래그, Rename 중단
- pointercancel / lostpointercapture: 포인터 제스처 중단

근거: [APG Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/), [Pointer Events](https://www.w3.org/TR/pointerevents/)
48 changes: 48 additions & 0 deletions docs/public/affordance-caret.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Caret

TBD.

Caret은 글 안의 삽입점입니다. 항목 [Select](affordance-select.md)와 다릅니다.
`text` / `vertical-text` 커서가 이 손을 가리킵니다. 클릭은 삽입점을 두고,
드래그는 글 범위를 고릅니다.

```ts
import { caretAffordance, caretCursor } from "@interactive-os/json-document-affordance";

function onPointerMove(event: PointerEvent) {
event.currentTarget.style.cursor = caretCursor("horizontal");
}

function onPointerDown(event: PointerEvent, blockId: string) {
const hand = caretAffordance({ type: "pointer", detail: event.detail });
if (hand.type === "place") {
editor.dispatch({
type: "selection.set",
blockId,
offset: hostHitOffset(event),
});
}
}

function onKeyDown(event: KeyboardEvent) {
const hand = caretAffordance(event);
if (hand?.type === "move") {
editor.dispatch({
type: "selection.set",
blockId: focus.blockId,
offset: hostMoveOffset(focus.offset, hand),
mode: hand.operation,
});
}
}
```

호스트는 I-beam과 글 기하를 그립니다. 값 삽입은 Hands입니다.

닫는 손:
- 클릭: 삽입점
- 드래그: 글 범위
- 화살표 / Home / End: 글자·줄 끝
- IME composition은 삽입 경로

근거: [CSS UI cursor `text`](https://www.w3.org/TR/css-ui-4/#cursor), [UI Events select](https://www.w3.org/TR/uievents/)
33 changes: 33 additions & 0 deletions docs/public/affordance-context-menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Context menu

TBD.

Context menu는 대상이 있는 자리에서 보조 동작을 여는 손입니다.
`context-menu` 커서가 이 손을 가리킵니다.

```ts
import { contextMenuAffordance } from "@interactive-os/json-document-affordance";

function onContextMenu(event: MouseEvent, itemId: string) {
if (contextMenuAffordance(event) !== "open") return;
event.preventDefault();
setMenu({ itemId, x: event.clientX, y: event.clientY });
}

function onKeyDown(event: KeyboardEvent) {
if (contextMenuAffordance(event) === "open") {
setMenu({ itemId: focusKey, x: 0, y: 0 });
}
if (contextMenuAffordance(event) === "cancel") setMenu(null);
}
```

호스트는 메뉴 항목을 그립니다. 여는 손은 호스트 화면 상태입니다.

닫는 손:
- 보조 버튼 / `contextmenu` / `auxclick`
- Shift+F10
- Menu 키
- Escape로 [Escape](affordance-cancel.md)

근거: [Pointer Events contextmenu](https://www.w3.org/TR/pointerevents/), [CSS UI cursor `context-menu`](https://www.w3.org/TR/css-ui-4/#cursor)
67 changes: 67 additions & 0 deletions docs/public/affordance-copy-drag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Duplicate

TBD.

Duplicate는 수정 키를 누른 채 드래그하면 원본을 복제하는 손입니다.
`copy` / `alias` 커서가 이 손을 가리킵니다.

```ts
import {
applyAffordance,
commitAffordance,
dragAffordance,
dragOperation,
dropAffordance,
} from "@interactive-os/json-document-affordance";

function onPointerMove(event: PointerEvent) {
applyAffordance(dragOperation(event), {
cursor: (cursor) => {
event.currentTarget.style.cursor = cursor;
},
});
}

function onPointerUp(event: PointerEvent) {
const committed = commitAffordance(
dragAffordance(origin, { x: event.clientX, y: event.clientY }),
);
if (!committed) return;
applyAffordance(committed, {
commit: (translate) => {
if (translate.type !== "translate") return;
let copied = false;
applyAffordance(dragOperation(event), {
hand: (operation) => {
if (operation.type !== "copy") return;
copied = true;
hostDuplicate(objectIds, translate);
},
});
if (copied) return;
const drop = commitAffordance(dropAffordance({ canDrop: true }));
if (!drop) return;
applyAffordance(drop, {
commit: (hand) => {
if (hand.type !== "move-drop") return;
editor.dispatch({
type: "object.translate",
objectIds,
dx: translate.dx,
dy: translate.dy,
});
},
});
},
});
}
```

복제 생성은 장르 Intent(호스트 또는 editor)이고, 옮기기는 json-document로
갑니다. 값의 클립보드 복사/붙이기는 Hands입니다.

닫는 손:
- Alt/Option + 드래그 → copy
- 커서 `copy` 또는 `alias`

근거: [Apple HIG pointing devices](https://developer.apple.com/design/human-interface-guidelines/pointing-devices), [CSS UI cursor `copy` / `alias`](https://www.w3.org/TR/css-ui-4/#cursor)
Loading