diff --git a/docs/README.md b/docs/README.md index 37b0e180..07333523 100644 --- a/docs/README.md +++ b/docs/README.md @@ -55,15 +55,6 @@ Editing |-- Clipboard `-- History -Hands -|-- Document -|-- Order -|-- Object -|-- Sheet -|-- Tree -|-- Kanban -`-- Database - Adapter |-- Keyboard |-- Clipboard adapter @@ -76,10 +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 +|-- Order +|-- Object +|-- Sheet +|-- Tree +|-- Kanban +`-- Database ``` ## 규범 우선순위 diff --git a/docs/changelog.md b/docs/changelog.md index 7f290d3c..2bd1df53 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,12 @@ 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. - Rewrote Why as the background of the editing-platform kernel: the same JSON Document port for Collaboration, session state in Editing, Hands as the genre shelf, and Adapter, Connector, and 제품 화면 as diff --git a/docs/evaluate.mjs b/docs/evaluate.mjs index b3b015a1..5ef47556 100644 --- a/docs/evaluate.mjs +++ b/docs/evaluate.mjs @@ -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"), @@ -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", @@ -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", diff --git a/docs/public/affordance-activate.md b/docs/public/affordance-activate.md new file mode 100644 index 00000000..748cb733 --- /dev/null +++ b/docs/public/affordance-activate.md @@ -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) diff --git a/docs/public/affordance-cancel.md b/docs/public/affordance-cancel.md new file mode 100644 index 00000000..e7d444b1 --- /dev/null +++ b/docs/public/affordance-cancel.md @@ -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/) diff --git a/docs/public/affordance-caret.md b/docs/public/affordance-caret.md new file mode 100644 index 00000000..1dfd1a9e --- /dev/null +++ b/docs/public/affordance-caret.md @@ -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/) diff --git a/docs/public/affordance-context-menu.md b/docs/public/affordance-context-menu.md new file mode 100644 index 00000000..2340b34b --- /dev/null +++ b/docs/public/affordance-context-menu.md @@ -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) diff --git a/docs/public/affordance-copy-drag.md b/docs/public/affordance-copy-drag.md new file mode 100644 index 00000000..45c523d2 --- /dev/null +++ b/docs/public/affordance-copy-drag.md @@ -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) diff --git a/docs/public/affordance-delete.md b/docs/public/affordance-delete.md new file mode 100644 index 00000000..a5d410d4 --- /dev/null +++ b/docs/public/affordance-delete.md @@ -0,0 +1,32 @@ +# Delete + +TBD. + +Delete는 고른 대상을 없애는 손입니다. `resolveAffordanceKey`는 이미 +Delete와 Backspace를 `delete` command로 닫습니다. 칸 비우기와 항목 제거는 +장르 Intent입니다. + +```ts +import { + deleteAffordance, + resolveAffordanceKey, +} from "@interactive-os/json-document-affordance"; + +function onKeyDown(event: KeyboardEvent) { + const command = resolveAffordanceKey(event); + const hand = deleteAffordance(event); + if (command?.type !== "delete" || !hand) return; + if (hand.source === "selection" || hand.source === "forward") { + editor.dispatch({ type: "selection.remove" }); + } +} +``` + +호스트는 장르 Intent만 가집니다. 값 삽입은 Hands입니다. + +닫는 손: +- Delete: 앞으로 지우기 +- Backspace: 뒤로 지우기 +- 범위가 있으면 고른 것만 지움 + +근거: 이미 닫힌 Delete chord, [APG](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/) diff --git a/docs/public/affordance-double-click.md b/docs/public/affordance-double-click.md new file mode 100644 index 00000000..2e0293e7 --- /dev/null +++ b/docs/public/affordance-double-click.md @@ -0,0 +1,26 @@ +# Double-click + +TBD. + +Double-click은 `UIEvent.detail === 2`입니다. 글에서는 단어를 고르고, +목록·파일에서는 열거나 [Rename](affordance-rename.md)으로 갈 수 있습니다. +횟수 손 자체는 닫혀 있고, 뜻은 장르 Intent입니다. + +```ts +import { clickCountAffordance } from "@interactive-os/json-document-affordance"; + +function onClick(event: MouseEvent, itemId: string) { + if (clickCountAffordance(event.detail) !== "double-click") return; + hostOpen(itemId); +} +``` + +호스트는 장르 Intent만 가집니다. 글 단어 범위는 [Caret](affordance-caret.md)에 +붙입니다. + +닫는 손: +- click / dblclick, `detail` 2 +- 글: 단어 범위 +- 항목: 열기 또는 Rename + +근거: [UIEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail) diff --git a/docs/public/affordance-drag.md b/docs/public/affordance-drag.md new file mode 100644 index 00000000..96ff75aa --- /dev/null +++ b/docs/public/affordance-drag.md @@ -0,0 +1,69 @@ +# Drag + +Drag는 고른 대상을 포인터로 옮기는 손입니다. 누른 점에서 현재 점까지 +이동량이 생기고, 이동량이 있을 때만 옮기기를 확정합니다. 기하와 히트 +테스트는 호스트가 계산하고, editor에는 대상 ID와 이동량만 넘깁니다. + +```ts +import { + applyAffordance, + commitAffordance, + dragAffordance, +} from "@interactive-os/json-document-affordance"; + +function onPointerMove(event: PointerEvent) { + applyAffordance( + dragAffordance(origin, { x: event.clientX, y: event.clientY }), + { + cursor: (cursor) => { + event.currentTarget.style.cursor = cursor; + }, + hand: (hand) => { + if (hand.type === "translate") setOffset({ dx: hand.dx, dy: hand.dy }); + }, + }, + ); +} + +function onPointerUp(event: PointerEvent) { + const committed = commitAffordance( + dragAffordance(origin, { x: event.clientX, y: event.clientY }), + ); + if (!committed) return; + applyAffordance(committed, { + commit: (hand) => { + if (hand.type !== "translate") return; + editor.dispatch({ + type: "object.translate", + objectIds, + dx: hand.dx, + dy: hand.dy, + }); + }, + }); +} +``` + +모양과 그림은 제품이 정합니다. 고른 것을 잡고 옮기는 문법은 닫혀 있습니다. + +## TBD + +```ts +function onKeyDown(event: KeyboardEvent) { + const hand = dragKeyboardAffordance({ key: event.key, grab: drag !== null }); + if (hand === "cancel") drag = null; + if (hand?.type === "nudge") { + editor.dispatch({ + type: "object.translate", + objectIds, + dx: hand.dx ?? 0, + dy: hand.dy ?? 0, + }); + } +} +``` + +- [Drop](affordance-drop.md) 대상과 `no-drop` +- [Duplicate](affordance-copy-drag.md) +- 키보드만으로 옮기기 (APG는 드래그의 키보드 대안을 요구함) +- pointer capture 수명 diff --git a/docs/public/affordance-drop.md b/docs/public/affordance-drop.md new file mode 100644 index 00000000..6dc46cdc --- /dev/null +++ b/docs/public/affordance-drop.md @@ -0,0 +1,48 @@ +# Drop + +TBD. + +Drop은 [Drag](affordance-drag.md)의 끝입니다. 어디에 둘 수 있는지를 +커서가 말하고, 못 두는 자리는 `no-drop`입니다. + +```ts +import { + applyAffordance, + commitAffordance, + dropAffordance, +} from "@interactive-os/json-document-affordance"; + +function onPointerMove(event: PointerEvent) { + applyAffordance(dropAffordance({ canDrop: hostCanDrop(event) }), { + cursor: (cursor) => { + event.currentTarget.style.cursor = cursor; + }, + }); +} + +function onPointerUp(event: PointerEvent, cardId: string, columnId: string) { + const drop = dropAffordance({ canDrop: hostCanDrop(event) }); + applyAffordance(drop, { + cursor: (cursor) => { + event.currentTarget.style.cursor = cursor; + }, + }); + const committed = commitAffordance(drop); + if (!committed) return; + applyAffordance(committed, { + commit: (hand) => { + if (hand.type !== "move-drop") return; + editor.dispatch({ type: "card.move", cardId, columnId }); + }, + }); +} +``` + +커서는 호스트 화면 상태이고, 허용된 놓기만 json-document로 갑니다. + +닫는 손: +- drop 대상 위: `move` / `copy` / `alias` +- 불가능한 자리: `no-drop` +- Escape: [Escape](affordance-cancel.md) + +근거: [HTML Drag and Drop](https://html.spec.whatwg.org/multipage/dnd.html), [CSS UI cursor `no-drop`](https://www.w3.org/TR/css-ui-4/#cursor) diff --git a/docs/public/affordance-focus.md b/docs/public/affordance-focus.md new file mode 100644 index 00000000..c14b1cc1 --- /dev/null +++ b/docs/public/affordance-focus.md @@ -0,0 +1,35 @@ +# Focus + +TBD. + +Focus는 키보드의 포인터입니다. Tab과 Shift+Tab은 컴포넌트 사이로 옮기고, +화살표·Home·End는 컴포넌트 안에서 옮깁니다. Focus와 [Select](affordance-select.md)는 +다릅니다. Focus 표시는 항상 보여야 하고, 선택 표시와 겹치면 안 됩니다. + +```ts +import { focusAffordance } from "@interactive-os/json-document-affordance"; + +function onKeyDown(event: KeyboardEvent) { + const hand = focusAffordance(event); + if (hand?.type === "tab") return; + if (hand?.type === "move") { + const next = neighbor(focusKey, hand.direction); + setFocusKey(next); + return; + } + if (hand?.type === "boundary") { + setFocusKey(hand.edge === "start" ? ids[0] : ids.at(-1)); + } +} +``` + +호스트는 마크업과 초점 고리를 그립니다. 화살표가 선택까지 바꿀지는 +장르 Intent입니다. keymap을 제품마다 열지 않습니다. + +닫는 손: +- Tab / Shift+Tab: 컴포넌트 사이 탭 순서 +- 화살표 / Home / End / PageUp / PageDown: 컴포넌트 안 +- roving tabindex 또는 `aria-activedescendant` +- `:focus-visible` 윤곽. 마우스 클릭 초점과 키보드 초점을 같은 그림으로 강제하지 않음 + +근거: [APG Developing a Keyboard Interface](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/), [CSS UI outline](https://www.w3.org/TR/css-ui-4/#outline) diff --git a/docs/public/affordance-fold.md b/docs/public/affordance-fold.md new file mode 100644 index 00000000..88837052 --- /dev/null +++ b/docs/public/affordance-fold.md @@ -0,0 +1,65 @@ +# Expand/Collapse + +Expand/Collapse는 나무에서 가지를 접고 펼치는 손입니다. 오른쪽은 닫힌 부모를 펼치고, +왼쪽은 열린 부모를 접습니다. 펼칠 가지가 없으면 그 방향은 이웃 이동으로 +남습니다. 위아래 화살표는 보이는 줄을 옮깁니다. + +```ts +import { + applyAffordance, + treeAffordance, +} from "@interactive-os/json-document-affordance"; + +applyAffordance( + treeAffordance(command, { + expanded: expanded.has(nodeId), + hasChildren: hasChildren(nodeId), + }), + { + hand: (hand) => { + if (hand.type === "expand") { + setExpanded((current) => new Set(current).add(nodeId)); + return; + } + if (hand.type === "collapse") { + setExpanded((current) => { + const next = new Set(current); + next.delete(nodeId); + return next; + }); + return; + } + if (hand.type === "move") { + editor.dispatch({ type: "selection.set", nodeId, topology, mode: "replace" }); + } + }, + }, +); +``` + +접힘 상태는 호스트가 가진 화면 상태입니다. editor는 보이는 줄 Topology만 +받습니다. expand/collapse는 호스트 접힘 집합으로, move는 json-document +선택으로 갑니다. + +## TBD + +```ts +function onKeyDown(event: KeyboardEvent) { + const hand = disclosureAffordance({ + key: event.key, + expanded: expanded.has(sectionId), + }); + if (hand === "expand") setExpanded((current) => new Set(current).add(sectionId)); + if (hand === "collapse") { + setExpanded((current) => { + const next = new Set(current); + next.delete(sectionId); + return next; + }); + } +} +``` + +- Accordion / Disclosure의 Enter·Space 접힘 +- `aria-expanded`와 호스트 접힘 집합의 동기 +- 가로 나무에서 위·아래가 접힘인지 이웃인지 diff --git a/docs/public/affordance-forbid.md b/docs/public/affordance-forbid.md new file mode 100644 index 00000000..c2d3b483 --- /dev/null +++ b/docs/public/affordance-forbid.md @@ -0,0 +1,27 @@ +# Not-allowed + +TBD. + +Not-allowed는 지금 그 손을 쓸 수 없음을 커서로 말하는 손입니다. 비활성 +대상은 `not-allowed`, 놓을 수 없는 자리는 `no-drop`입니다. + +```ts +import { forbiddenCursor } from "@interactive-os/json-document-affordance"; + +function onPointerMove(event: PointerEvent, itemId: string) { + event.currentTarget.style.cursor = forbiddenCursor({ + allowed: hostCanEdit(itemId), + dropping: drag !== null, + }); +} +``` + +이 손은 json-document가 아니라 호스트 커서입니다. 거절된 포인터는 +`editor.dispatch`를 호출하지 않습니다. + +닫는 손: +- `not-allowed`: 요청한 동작 불가 +- `no-drop`: 이 자리에 놓을 수 없음 +- 비활성 대상의 초점 가능 여부는 APG 규칙, 호스트가 가짐 + +근거: [CSS UI cursor `not-allowed` / `no-drop`](https://www.w3.org/TR/css-ui-4/#cursor), [APG disabled focus](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/) diff --git a/docs/public/affordance-history.md b/docs/public/affordance-history.md new file mode 100644 index 00000000..14601c6c --- /dev/null +++ b/docs/public/affordance-history.md @@ -0,0 +1,46 @@ +# Undo + +Undo는 값과 선택을 함께 되돌리는 손입니다. Mod+Z는 undo, Mod+Shift+Z는 +redo입니다. 버튼은 `canUndo` / `canRedo`를 읽어 꺼집니다. + +```ts +import { + applyAffordance, + historyAffordance, +} from "@interactive-os/json-document-affordance"; + +let commands = { + undo: { name: "undo" as const, disabled: true }, + redo: { name: "redo" as const, disabled: true }, +}; +applyAffordance(historyAffordance({ + canUndo: editor.snapshot.canUndo, + canRedo: editor.snapshot.canRedo, +}), { + hand: (hand) => { + if (hand.type === "history") commands = { undo: hand.undo, redo: hand.redo }; + }, +}); + + +``` + +History 기록 자체는 Editing이 가지고, 어포던스는 그 손을 닫습니다. + +## TBD + +```ts +function onKeyDown(event: KeyboardEvent) { + const command = resolveAffordanceKey(event); + if (command?.type === "redo") editor.redo(); +} +``` + +- Mod+Y redo 변종 +- 연속 입력의 묶음 단위 +- 협업 replica에서 내 기여만 되돌리기는 Collaboration History diff --git a/docs/public/affordance-hover.md b/docs/public/affordance-hover.md new file mode 100644 index 00000000..03e2c37c --- /dev/null +++ b/docs/public/affordance-hover.md @@ -0,0 +1,29 @@ +# Hover + +TBD. + +Hover는 누르지 않은 포인터가 대상 위에 있을 때 가능한 손을 드러냅니다. +커서가 바뀌고, 잠시 뒤 툴팁이 열릴 수 있습니다. `help` 커서가 도움말이 +있음을 가리킵니다. + +```ts +import { hoverAffordance, hoverCursor } from "@interactive-os/json-document-affordance"; + +function onPointerMove(event: PointerEvent, itemId: string) { + event.currentTarget.style.cursor = hoverCursor("help"); + const hand = hoverAffordance({ elapsedMs, inside: true }); + if (hand === "tooltip") setTooltip(itemId); + if (hand === null) setTooltip(null); +} +``` + +호스트는 툴팁 그림을 그립니다. 이 손은 json-document가 아니라 호스트 +화면 상태입니다. + +닫는 손: +- pointerenter / pointerleave +- 짧은 체류: 커서 hint +- 기본 지연 후 tooltip +- `help` 커서 + +근거: [APG Tooltip](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/), [Apple HIG pointing devices](https://developer.apple.com/design/human-interface-guidelines/pointing-devices), [CSS UI cursor](https://www.w3.org/TR/css-ui-4/#cursor) diff --git a/docs/public/affordance-marquee.md b/docs/public/affordance-marquee.md new file mode 100644 index 00000000..c0dea6f2 --- /dev/null +++ b/docs/public/affordance-marquee.md @@ -0,0 +1,60 @@ +# Marquee + +TBD. + +Marquee는 빈 평면에서 사각형을 끌어 여러 대상을 집는 손입니다. 고른 +대상을 옮기는 [Drag](affordance-drag.md)와 다릅니다. `crosshair` / `cell` +커서가 이 손을 가리킬 수 있습니다. + +```ts +import { + applyAffordance, + commitAffordance, + marqueeAffordance, + pointerSelect, +} from "@interactive-os/json-document-affordance"; + +function onPointerMove(event: PointerEvent) { + const point = { x: event.offsetX, y: event.offsetY }; + applyAffordance(marqueeAffordance(origin, point), { + cursor: (cursor) => { + event.currentTarget.style.cursor = cursor; + }, + hand: (hand) => { + if (hand.type === "select" && hand.rect) setRect(hand.rect); + }, + }); +} + +function onPointerUp(event: PointerEvent) { + const committed = commitAffordance(marqueeAffordance(origin, { x: event.offsetX, y: event.offsetY })); + if (!committed) return; + applyAffordance(committed, { + commit: (hand) => { + if (hand.type !== "select" || !hand.rect) return; + applyAffordance(pointerSelect(event), { + hand: (selectHand) => { + if (selectHand.type !== "select") return; + editor.dispatch({ + type: "selection.set", + objectIds: hostHits(hand.rect), + mode: selectHand.operation, + }); + }, + }); + }, + }); +} +``` + +호스트는 히트 테스트와 기하를 계산합니다. 어떤 키가 범위에 들어오는지는 +호스트가 보고, 손이 replace인지 extend인지는 Affordance가 닫고, 선택은 +json-document로 갑니다. + +닫는 손: +- 빈 곳에서 pointerdown → move → up +- Shift: 기존 고르기에 더함 +- Mod: 토글 +- 칸 위 `cell` 커서, 그림 위 `crosshair` + +근거: [Apple HIG band selection](https://developer.apple.com/design/human-interface-guidelines/pointing-devices), [CSS UI cursor `cell` / `crosshair`](https://www.w3.org/TR/css-ui-4/#cursor) diff --git a/docs/public/affordance-nudge.md b/docs/public/affordance-nudge.md new file mode 100644 index 00000000..f70d329b --- /dev/null +++ b/docs/public/affordance-nudge.md @@ -0,0 +1,31 @@ +# Nudge + +TBD. + +Nudge는 고른 대상을 키보드로 조금 옮기는 손입니다. 화살표는 한 단위, +Shift+화살표는 큰 단위입니다. 항목 이웃으로 초점을 옮기는 +[Select](affordance-select.md)와 다릅니다. + +```ts +import { nudgeAffordance } from "@interactive-os/json-document-affordance"; + +function onKeyDown(event: KeyboardEvent) { + const hand = nudgeAffordance(event); + if (!hand) return; + editor.dispatch({ + type: "object.translate", + objectIds: editor.selectedObjects.map((object) => object.id), + dx: hand.dx * unit, + dy: hand.dy * unit, + }); +} +``` + +호스트는 단위(px, 칸, 그리드)를 곱합니다. 이동은 json-document로 갑니다. + +닫는 손: +- Arrow: 1 단위 +- Shift+Arrow: 10 단위 +- 키 반복은 호스트 플랫폼 기본 + +근거: 캔버스·슬라이드 편집기 관례. Object 장르 Intent가 단위를 가짐. diff --git a/docs/public/affordance-pan.md b/docs/public/affordance-pan.md new file mode 100644 index 00000000..342deab8 --- /dev/null +++ b/docs/public/affordance-pan.md @@ -0,0 +1,41 @@ +# Pan + +TBD. + +Pan은 대상을 옮기지 않고 보이는 평면을 옮기는 손입니다. `grab` / +`grabbing` / `all-scroll` 커서가 이 손을 가리킵니다. + +```ts +import { applyAffordance, panAffordance } from "@interactive-os/json-document-affordance"; + +function onPointerMove(event: PointerEvent) { + applyAffordance( + panAffordance({ + spaceKey, + buttons: event.buttons, + origin, + point: { x: event.clientX, y: event.clientY }, + }), + { + cursor: (cursor) => { + event.currentTarget.style.cursor = cursor; + }, + hand: (hand) => { + if (hand.type !== "translate") return; + setViewport({ x: hand.dx, y: hand.dy }); + }, + }, + ); +} +``` + +이 손은 json-document가 아니라 호스트 뷰포트입니다. 이동량은 +[Drag](affordance-drag.md)와 같은 `translate` 손입니다. + +닫는 손: +- Space + 드래그 +- 가운데 버튼 드래그 +- `grab` → `grabbing` +- `all-scroll` + +근거: [CSS UI cursor `grab` / `all-scroll`](https://www.w3.org/TR/css-ui-4/#cursor) diff --git a/docs/public/affordance-rename.md b/docs/public/affordance-rename.md new file mode 100644 index 00000000..18c7a3cc --- /dev/null +++ b/docs/public/affordance-rename.md @@ -0,0 +1,36 @@ +# Rename + +TBD. + +Rename은 고른 대상의 레이블을 고치는 손입니다. F2와 느린 double-click이 +같은 손을 엽니다. Escape는 [Escape](affordance-cancel.md)입니다. + +```ts +import { renameAffordance } from "@interactive-os/json-document-affordance"; + +function onKeyDown(event: KeyboardEvent) { + const hand = renameAffordance(event); + if (hand === "begin") setRenaming(focusKey); + if (hand === "cancel") setRenaming(null); + if (hand === "commit" && renaming) { + editor.dispatch({ type: "item.rename", itemId: renaming, label: draft }); + setRenaming(null); + } +} + +function onClick(event: MouseEvent, itemId: string) { + if (renameAffordance({ type: "pointer", detail: event.detail, intervalMs }) === "begin") { + setRenaming(itemId); + } +} +``` + +호스트는 레이블 필드를 그립니다. begin/cancel은 호스트 화면 상태이고, +commit만 json-document로 갑니다. 글 편집 자체는 Hands와 [Caret](affordance-caret.md)입니다. + +닫는 손: +- F2 +- 느린 double-click (빠른 것은 [Double-click](affordance-double-click.md)) +- Enter로 확정, Escape로 취소 + +근거: Finder/Explorer/VS Code, [UIEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail) diff --git a/docs/public/affordance-resize.md b/docs/public/affordance-resize.md new file mode 100644 index 00000000..2545b408 --- /dev/null +++ b/docs/public/affordance-resize.md @@ -0,0 +1,49 @@ +# Resize + +TBD. + +Resize는 가장자리·모서리·칸 경계·창 분할선을 움직이는 손입니다. +CSS predefined 리사이즈 커서가 이 손을 닫습니다. + +```ts +import { resizeCursor, resizeOffset } from "@interactive-os/json-document-affordance"; + +function onPointerMove(event: PointerEvent, edge: "se") { + event.currentTarget.style.cursor = resizeCursor(edge); + const offset = resizeOffset( + origin, + { x: event.clientX, y: event.clientY }, + edge, + event, + ); + setPreview(offset); +} + +function onPointerUp(event: PointerEvent, objectId: string, edge: "se") { + const offset = resizeOffset( + origin, + { x: event.clientX, y: event.clientY }, + edge, + event, + ); + editor.dispatch({ + type: "object.resize", + objectId, + dx: offset.dx, + dy: offset.dy, + }); +} +``` + +커서는 호스트 화면 상태이고, 확정된 크기만 json-document로 갑니다. +분할선 화살표는 APG Window Splitter와 같고, Shift는 비율, Alt는 가운데 +기준입니다. + +닫는 손: +- 모서리: `n-resize` … `nwse-resize` +- 칸/행: `col-resize` / `row-resize` +- 분할선: 화살표, Enter로 접기 +- Shift: 비율 고정 +- Alt: 가운데 기준 + +근거: [CSS UI cursor resize](https://www.w3.org/TR/css-ui-4/#cursor), [CSS UI resize](https://www.w3.org/TR/css-ui-4/#resize), [APG Window Splitter](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/) diff --git a/docs/public/affordance-scroll.md b/docs/public/affordance-scroll.md new file mode 100644 index 00000000..99413306 --- /dev/null +++ b/docs/public/affordance-scroll.md @@ -0,0 +1,39 @@ +# Scroll + +TBD. + +Scroll은 휠로 보이는 내용을 옮기는 손입니다. 드래그 중 가장자리에 닿으면 +자동으로 더 굴러갑니다. [Zoom](affordance-zoom.md)의 Mod+휠과 구분합니다. + +```ts +import { + wheelAffordance, + autoscrollAffordance, +} from "@interactive-os/json-document-affordance"; + +function onWheel(event: WheelEvent) { + const hand = wheelAffordance(event); + if (hand.type === "scroll") scroller.scrollBy(0, hand.dy); + if (hand.type === "zoom") setScale(hostZoom(scale, hand.delta)); +} + +function onPointerMove(event: PointerEvent) { + const auto = autoscrollAffordance({ + x: event.clientX, + y: event.clientY, + width: scroller.clientWidth, + height: scroller.clientHeight, + edge: 16, + }); + if (auto) scroller.scrollBy(auto.dx, auto.dy); +} +``` + +이 손은 호스트 overflow입니다. 문서 값은 바꾸지 않습니다. + +닫는 손: +- wheel (수정 키 없음) +- 드래그 중 가장자리 autoscroll +- 초점 대상이 보이도록 scroll-into-view는 호스트 + +근거: [UI Events wheel](https://www.w3.org/TR/uievents/), [Pointer Events](https://www.w3.org/TR/pointerevents/) diff --git a/docs/public/affordance-select.md b/docs/public/affordance-select.md new file mode 100644 index 00000000..1e688171 --- /dev/null +++ b/docs/public/affordance-select.md @@ -0,0 +1,50 @@ +# Select + +Select는 대상을 집는 손입니다. 클릭은 그 대상으로 바꾸고, Shift는 +범위를 늘리며, Mod는 토글합니다. 화살표는 이웃으로 옮기고, Shift+화살표는 +범위를 늘립니다. + +```ts +import { + applyAffordance, + pointerSelect, +} from "@interactive-os/json-document-affordance"; + +function onPointerDown(event: PointerEvent, itemId: string) { + applyAffordance(pointerSelect(event), { + hand: (hand) => { + if (hand.type === "select") { + editor.dispatch({ type: "selection.set", itemId, mode: hand.operation }); + } + }, + }); +} +``` + +호스트는 보이는 키와 장르 Intent만 넘깁니다. keymap을 덮어쓰지 않습니다. + +## TBD + +```ts +function onKeyDown(event: KeyboardEvent) { + const hand = selectAllAffordance({ + key: event.key, + metaKey: event.metaKey, + ctrlKey: event.ctrlKey, + allSelected: editor.selectedItemIds.length === ids.length, + }); + if (hand === "select-all") { + editor.dispatch({ type: "selection.set", itemIds: ids, mode: "replace" }); + } + if (hand === "clear") { + editor.dispatch({ type: "selection.set", itemIds: [], mode: "replace" }); + } +} +``` + +- Mod+A Select all 토글 +- Home / End / PageUp / PageDown은 `resolveAffordanceKey`의 boundary +- selection follows focus vs focus-only move는 [Focus](affordance-focus.md) +- 글 단어·줄 범위는 [Double-click](affordance-double-click.md)· + [Triple-click](affordance-triple-click.md)·[Caret](affordance-caret.md) +- 빈 평면의 여러 대상은 [Marquee](affordance-marquee.md) diff --git a/docs/public/affordance-snap.md b/docs/public/affordance-snap.md new file mode 100644 index 00000000..2c71819c --- /dev/null +++ b/docs/public/affordance-snap.md @@ -0,0 +1,54 @@ +# Snap + +TBD. + +Snap은 [Drag](affordance-drag.md)·[Resize](affordance-resize.md)· +[Nudge](affordance-nudge.md) 중에 그리드나 가이드에 붙는 손입니다. +수정 키를 누르면 붙지 않습니다. + +```ts +import { + applyAffordance, + commitAffordance, + dragAffordance, + snapAffordance, +} from "@interactive-os/json-document-affordance"; + +function onPointerUp(event: PointerEvent) { + const committed = commitAffordance( + dragAffordance(origin, { x: event.clientX, y: event.clientY }), + ); + if (!committed) return; + applyAffordance(committed, { + commit: (hand) => { + if (hand.type !== "translate") return; + applyAffordance( + snapAffordance( + { x: hand.dx, y: hand.dy }, + { grid: 8, disable: event.metaKey || event.ctrlKey }, + ), + { + hand: (snapped) => { + if (snapped.type !== "translate") return; + editor.dispatch({ + type: "object.translate", + objectIds, + dx: snapped.dx, + dy: snapped.dy, + }); + }, + }, + ); + }, + }); +} +``` + +가이드 기하는 호스트가 가지고, 붙은 좌표만 json-document로 갑니다. + +닫는 손: +- 이동·리사이즈 중 스냅 +- 수정 키로 스냅 해제 +- 회전 15° 스냅은 장르 Intent (Object) + +근거: 캔버스·슬라이드 편집기 관례. CSS predefined 커서는 없음. diff --git a/docs/public/affordance-triple-click.md b/docs/public/affordance-triple-click.md new file mode 100644 index 00000000..99c353a3 --- /dev/null +++ b/docs/public/affordance-triple-click.md @@ -0,0 +1,29 @@ +# Triple-click + +TBD. + +Triple-click은 `UIEvent.detail === 3`입니다. 글에서는 줄 또는 문단을 +고릅니다. 항목 목록에서는 보통 쓰지 않습니다. + +```ts +import { clickCountAffordance } from "@interactive-os/json-document-affordance"; + +function onClick(event: MouseEvent, blockId: string) { + if (clickCountAffordance(event.detail) !== "triple-click") return; + editor.dispatch({ + type: "selection.set", + blockId, + mode: "replace", + offset: 0, + }); +} +``` + +호스트는 줄/문단 기하를 그립니다. 횟수는 [Double-click](affordance-double-click.md)과 +같은 손을 씁니다. + +닫는 손: +- click `detail` 3 +- 글: 줄 / 문단 범위 + +근거: [UIEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail) diff --git a/docs/public/affordance-typeahead.md b/docs/public/affordance-typeahead.md new file mode 100644 index 00000000..3f57d60e --- /dev/null +++ b/docs/public/affordance-typeahead.md @@ -0,0 +1,40 @@ +# Typeahead + +TBD. + +Typeahead는 인쇄 글쇠로 목록·나무에서 이름으로 건너뛰는 손입니다. 한 글자는 +그 글자로 시작하는 다음 항목, 빠르게 이어 치면 그 문자열로 시작합니다. + +```ts +import { + applyAffordance, + typeaheadAffordance, +} from "@interactive-os/json-document-affordance"; + +function onKeyDown(event: KeyboardEvent) { + applyAffordance(typeaheadAffordance({ + buffer, + key: event.key, + elapsedMs: event.timeStamp - lastType, + names: items.map((item) => item.label), + from: focusedLabel, + }), { + hand: (hand) => { + if (hand.type !== "typeahead") return; + setBuffer(hand.buffer); + const itemId = items.find((item) => item.label === hand.name)?.id; + if (itemId) editor.dispatch({ type: "selection.set", itemId, mode: "replace" }); + }, + }); +} +``` + +호스트는 보이는 이름을 줍니다. 점프한 키는 호스트 Focus로 가고, 선택까지 +바꿀지는 장르 Intent입니다. + +닫는 손: +- 한 문자: 그 prefix의 다음 항목 +- 연속 문자: 문자열 prefix +- 짧은 시간 안에만 이어 붙임 + +근거: [APG Listbox type-ahead](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/) diff --git a/docs/public/affordance-zoom.md b/docs/public/affordance-zoom.md new file mode 100644 index 00000000..97cef23a --- /dev/null +++ b/docs/public/affordance-zoom.md @@ -0,0 +1,38 @@ +# Zoom + +TBD. + +Zoom은 보이는 배율을 바꾸는 손입니다. `zoom-in` / `zoom-out` 커서가 +이 손을 가리킵니다. CSS `resize`와 무관합니다. + +```ts +import { + zoomAffordance, + zoomCursor, + wheelAffordance, +} from "@interactive-os/json-document-affordance"; + +function onPointerMove(event: PointerEvent) { + event.currentTarget.style.cursor = zoomCursor("in"); +} + +function onWheel(event: WheelEvent) { + const hand = wheelAffordance(event); + if (hand.type === "zoom") setScale(hostZoom(scale, hand.delta)); +} + +function onKeyDown(event: KeyboardEvent) { + const hand = zoomAffordance(event); + if (hand?.type === "in") setScale(scale * 1.1); + if (hand?.type === "out") setScale(scale / 1.1); +} +``` + +이 손은 호스트 뷰포트입니다. 문서 값은 바꾸지 않습니다. + +닫는 손: +- Mod + wheel +- + / − +- `zoom-in` / `zoom-out` 커서 + +근거: [CSS UI cursor `zoom-in` / `zoom-out`](https://www.w3.org/TR/css-ui-4/#cursor), [CSS UI resize note](https://www.w3.org/TR/css-ui-4/#resize) diff --git a/docs/public/affordance.md b/docs/public/affordance.md new file mode 100644 index 00000000..ee921af8 --- /dev/null +++ b/docs/public/affordance.md @@ -0,0 +1,131 @@ +# Affordance + +제품의 화면은 호스트가 그립니다. json-document가 최전선에서 주는 것은 +리스트박스나 나무가 아니라, 30년 동안 같은 손으로 학습된 키보드와 +마우스와 커서입니다. + +`@interactive-os/json-document-affordance`가 그 계약을 닫습니다. 단축키를 +제품마다 바꾸지 않습니다. 구현이 없는 손은 TBD로 남기고, 화면 위젯으로 +대체하지 않습니다. TBD 페이지의 코드는 사용법 명세입니다. 패키지에 +아직 없습니다. + +호스트 이벤트와 json-document 사이를 이렇게 잇습니다. 시점은 리스너가 +고르고, 읽는 법은 `applyAffordance` 하나입니다. 미리보기는 `{ hand, cursor }` +이고, 쓰기는 `commitAffordance` 다음 `{ commit }` 포트입니다. + +```ts +import { + applyAffordance, + forbiddenCursor, + pointerSelect, +} from "@interactive-os/json-document-affordance"; + +function onPointerDown(event: PointerEvent, itemId: string) { + applyAffordance(pointerSelect(event), { + hand: (hand) => { + if (hand.type === "select") { + editor.dispatch({ type: "selection.set", itemId, mode: hand.operation }); + } + }, + }); +} + +function onPointerMove(event: PointerEvent) { + applyAffordance(forbiddenCursor({ allowed: hostCanDrop(event), dropping: true }), { + cursor: (cursor) => { + event.currentTarget.style.cursor = cursor; + }, + }); +} +``` + +```sh +npm i @interactive-os/json-document-affordance +``` + +Editing은 선택과 작업을 기억합니다. Adapter는 키 chord를 command로 +번역합니다. Connector는 구독과 질의를 붙입니다. 어포던스는 그 command가 +무슨 손인지를 정합니다. 호스트는 마크업과 장르 Intent만 가집니다. + +이 층이 닫히는 축은 세 개입니다. + +- 키보드: [APG Keyboard Interface](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/)가 + Tab은 컴포넌트 사이, 화살표는 컴포넌트 안, 초점과 선택은 다르다고 닫는다. +- 마우스: [Pointer Events](https://www.w3.org/TR/pointerevents/)와 + [UI Events](https://www.w3.org/TR/uievents/)가 click·auxclick·contextmenu· + wheel·pointer capture·`detail` 횟수를 닫는다. +- 커서: [CSS UI 4 predefined cursors](https://www.w3.org/TR/css-ui-4/#cursor)가 + 지금 이 자리에서 가능한 손을 키워드 집합으로 닫는다. + +## 이미 닫힌 손 + +| Affordance | API | Hand | +| --- | --- | --- | +| [Select](affordance-select.md) | `pointerSelect`, `resolveAffordanceKey` | 클릭, Shift 범위, Mod 토글, 화살표 | +| [Expand/Collapse](affordance-fold.md) | `treeAffordance` | 나무 왼쪽 접힘, 오른쪽 펼침 | +| [Drag](affordance-drag.md) | `dragAffordance`, `commitAffordance` | 고른 대상을 포인터로 옮김 | +| [Undo](affordance-history.md) | `historyAffordance` | Mod+Z, Mod+Shift+Z | + +## 키보드 TBD + +| Affordance | API | Hand | +| --- | --- | --- | +| [Focus](affordance-focus.md) | `focusAffordance` | Tab 사이, 화살표 안, 초점 ≠ 선택 | +| [Caret](affordance-caret.md) | `caretAffordance`, `caretCursor` | I-beam 삽입점, 글 범위 | +| [Typeahead](affordance-typeahead.md) | `typeaheadAffordance` | 인쇄 글쇠 prefix 점프 | +| [Activate](affordance-activate.md) | `activateAffordance` | Enter, Space, 기본 클릭 | +| [Escape](affordance-cancel.md) | `escapeAffordance` | Escape, pointercancel | +| [Delete](affordance-delete.md) | `deleteAffordance` | Delete, Backspace. Delete chord는 이미 닫힘 | +| [Rename](affordance-rename.md) | `renameAffordance` | F2, 느린 두 번 누르기 | +| [Nudge](affordance-nudge.md) | `nudgeAffordance` | 화살표 한 단위, Shift 큰 단위 | + +## 마우스 TBD + +| Affordance | API | Hand | +| --- | --- | --- | +| [Hover](affordance-hover.md) | `hoverAffordance`, `hoverCursor` | hover, 툴팁 지연, 커서 교체 | +| [Double-click](affordance-double-click.md) | `clickCountAffordance` | `detail` 2 | +| [Triple-click](affordance-triple-click.md) | `clickCountAffordance` | `detail` 3 | +| [Context menu](affordance-context-menu.md) | `contextMenuAffordance` | 오른쪽 클릭, Shift+F10, Menu | +| [Marquee](affordance-marquee.md) | `marqueeAffordance`, `commitAffordance` | 빈 곳에서 사각형으로 여러 대상 | +| [Drop](affordance-drop.md) | `dropAffordance` | drop 대상, no-drop | +| [Duplicate](affordance-copy-drag.md) | `dragOperation` | Alt/Option 드래그 복제 | +| [Resize](affordance-resize.md) | `resizeCursor`, `resizeOffset` | 모서리, 칸, 분할선 | +| [Pan](affordance-pan.md) | `panAffordance` | Space+드래그, grab | +| [Scroll](affordance-scroll.md) | `wheelAffordance`, `autoscrollAffordance` | wheel, autoscroll | +| [Zoom](affordance-zoom.md) | `zoomAffordance`, `zoomCursor` | Mod+휠, +/− | +| [Snap](affordance-snap.md) | `snapAffordance` | 그리드·가이드, 수정 키로 해제 | +| [Not-allowed](affordance-forbid.md) | `forbiddenCursor` | not-allowed, no-drop | + +## 커서가 닫는 손 + +CSS UI 4 ``는 이 집합이 끝입니다. 새 커서 이름을 +만들지 않습니다. + +| 커서 | 손 | +| --- | --- | +| `auto`, `default`, `none` | 호스트·UA 기본. 이 층이 새 문법을 열지 않음 | +| `pointer` | [Activate](affordance-activate.md) | +| `text`, `vertical-text` | [Caret](affordance-caret.md) | +| `cell`, `crosshair` | [Marquee](affordance-marquee.md), [Select](affordance-select.md) | +| `context-menu` | [Context menu](affordance-context-menu.md) | +| `help` | [Hover](affordance-hover.md) | +| `move`, `grab`, `grabbing` | [Drag](affordance-drag.md), [Pan](affordance-pan.md) | +| `copy`, `alias` | [Duplicate](affordance-copy-drag.md) | +| `no-drop`, `not-allowed` | [Not-allowed](affordance-forbid.md), [Drop](affordance-drop.md) | +| `n-resize` … `nwse-resize`, `col-resize`, `row-resize` | [Resize](affordance-resize.md) | +| `all-scroll` | [Pan](affordance-pan.md) | +| `zoom-in`, `zoom-out` | [Zoom](affordance-zoom.md) | +| `progress`, `wait` | UA 바쁨. 편집 손이 아님 | + +## 이 층이 아닌 것 + +- 값 삽입, IME로 글자를 쓰는 일, 클립보드로 값을 옮기는 완전한 편집기는 + Hands입니다. +- chord를 command로 번역하는 일은 Adapter입니다. +- 구독과 질의는 Connector입니다. +- 마크업, ARIA role, 히트 테스트, 기하, 장르 Intent는 호스트입니다. +- 터치·펜·눈은 이 패키지가 닫는 손이 아닙니다. + +라이브 화면은 닫힌 손을 호스트 그림에 붙인 증명입니다. 위젯을 가져가는 +입구가 아닙니다. diff --git a/docs/public/concepts.md b/docs/public/concepts.md index c48e5079..167d30c6 100644 --- a/docs/public/concepts.md +++ b/docs/public/concepts.md @@ -7,11 +7,11 @@ Quickstart에서는 JSON을 읽고, 변경을 적용하고, 결과를 구독했 json-document를 제품에 연결하는 흐름은 이렇게 이어집니다. 먼저 JSON Document가 값을 다루고, Editing이 사용자의 편집 상태를 더합니다. Adapter는 브라우저 플랫폼 계약을 붙이고, Connector는 라이브러리 생태계를 -연결합니다. +연결합니다. Affordance는 키보드와 마우스의 관례적 손을 최전선에서 닫습니다. ```txt -JSON Document → Editing → Adapter → Connector → 제품 화면 -값과 변경 선택과 작업 플랫폼 계약 라이브러리 렌더링과 입력 +JSON Document → Editing → Adapter → Connector → Affordance +값과 변경 선택과 작업 플랫폼 계약 라이브러리 키보드와 마우스 ``` ## JSON Document가 값을 다룬다 @@ -84,6 +84,8 @@ React의 구독 방식으로 전달합니다. TanStack Table Connector는 화면 - [Adapters](adapters.md): 키보드, clipboard, contenteditable 플랫폼 변환 - [Connectors](connectors.md): React, Zod, TanStack Table 같은 라이브러리 연결 - [React editing](react-editing.md): 선택 범위와 커서를 React 질의로 그리기 +- [Affordance](affordance.md): 키보드와 마우스의 관례적 손 같은 공책을 여러 참여자가 쓰려면 [Collaboration](collaboration.md)으로 -갑니다. 장르의 손은 [Hands](hands.md)에서 고릅니다. +갑니다. 장르 선반은 [Hands](hands.md)에서 고릅니다. Hands는 위 파이프라인 +한 층이 아닙니다. diff --git a/docs/public/overview.md b/docs/public/overview.md index e0d43c04..662d5c3e 100644 --- a/docs/public/overview.md +++ b/docs/public/overview.md @@ -77,7 +77,7 @@ Hands는 그 전형적 손의 최소 완성본입니다. 한 줄 목록을 집 Object, Sheet, Tree, Kanban, Database가 그 목록입니다. 고르려면 [Hands](hands.md)로 갑니다. -## 플랫폼, 라이브러리, 제품 화면 +## 플랫폼, 라이브러리, Affordance 브라우저에서 쓰려면 키보드와 clipboard, contenteditable 같은 플랫폼 계약을 같은 공개 API에 맞춰 번역해야 합니다. Adapter가 그 일을 합니다. @@ -88,19 +88,20 @@ React로 그리거나 Zod로 검사하려면 이름 있는 라이브러리의 계약에 맞춰야 합니다. Connector가 그 일을 합니다. document 변경은 React 구독으로 흐르고, 표의 보이는 행과 열은 Sheet의 Topology가 됩니다. -툴바와 리스트박스, 격자는 제품이 그리는 화면입니다. 편집 상태를 읽어 -되돌리기 버튼을 끄거나, 고른 칸을 칠합니다. +고르기, 접기, 드래그, 되돌리기는 제품이 json-document를 만지는 손입니다. +화면은 호스트가 그리고, 단축키와 마우스 문법은 Affordance가 닫습니다. -플랫폼 계약과 라이브러리 연결과 호스트 화면은 높이가 같고 하는 일만 -다릅니다. 필요한 것만 고릅니다. [Adapters](adapters.md)와 -[Connectors](connectors.md)에서 이어서 읽습니다. +Adapter와 Connector는 환경에 붙입니다. Affordance는 그 위에서 호스트가 +만지는 최전선입니다. 필요한 Adapter와 Connector만 고릅니다. +[Adapters](adapters.md)와 [Connectors](connectors.md), +[Affordance](affordance.md)에서 이어서 읽습니다. ## 편집 플랫폼의 커널 json-document는 여러 화면이 같은 문서를 쓰게 하는 편집 플랫폼의 커널입니다. JSON Document와 Collaboration이 값을 다룹니다. Editing이 -손과 기억을 더합니다. Hands가 장르의 전형을 보여 줍니다. Adapter와 -Connector와 제품 화면이 그 손을 실제 환경에 붙입니다. +손과 기억을 더합니다. Adapter와 Connector가 환경에 붙이고, Affordance가 +최전선의 손을 닫습니다. Hands는 그 다음에 보는 장르 선반입니다. 여러 화면이 같은 주소와 실행 취소와 협업을 쓰게 하려는 자리가 이 커널입니다. 먼저 값을 만져 보려면 [Quickstart](quickstart.md)에서 작은 diff --git a/package-lock.json b/package-lock.json index 4ef8d301..61a63001 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "packages/json-document-react", "packages/json-document-react-hook-form", "packages/json-document-ajv", + "packages/json-document-affordance", "packages/json-document-zod", "packages/json-document-tanstack-table", "packages/json-document-web", @@ -940,6 +941,10 @@ "resolved": "packages/json-document", "link": true }, + "node_modules/@interactive-os/json-document-affordance": { + "resolved": "packages/json-document-affordance", + "link": true + }, "node_modules/@interactive-os/json-document-ajv": { "resolved": "packages/json-document-ajv", "link": true @@ -5433,6 +5438,20 @@ "vitest": "^4.1.7" } }, + "packages/json-document-affordance": { + "name": "@interactive-os/json-document-affordance", + "version": "0.1.0-rc.0", + "license": "MIT", + "devDependencies": { + "@interactive-os/json-document-web": "*", + "@types/node": "^25.9.0", + "typescript": "^5.0.0", + "vitest": "^4.1.7" + }, + "peerDependencies": { + "@interactive-os/json-document-web": ">=0.1.0-rc.0 <1" + } + }, "packages/json-document-ajv": { "name": "@interactive-os/json-document-ajv", "version": "0.1.0-rc.0", @@ -5674,6 +5693,7 @@ "site": { "name": "@interactive-os/json-document-site", "dependencies": { + "@interactive-os/json-document-affordance": "*", "@interactive-os/json-document-ajv": "*", "@interactive-os/json-document-contenteditable": "*", "@interactive-os/json-document-editing": "*", diff --git a/package.json b/package.json index b02b44f5..37e4db44 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "packages/json-document-react", "packages/json-document-react-hook-form", "packages/json-document-ajv", + "packages/json-document-affordance", "packages/json-document-zod", "packages/json-document-tanstack-table", "packages/json-document-web", diff --git a/packages/json-document-affordance/LICENSE b/packages/json-document-affordance/LICENSE new file mode 100644 index 00000000..6a984193 --- /dev/null +++ b/packages/json-document-affordance/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 interactive-os contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/json-document-affordance/README.md b/packages/json-document-affordance/README.md new file mode 100644 index 00000000..c5834684 --- /dev/null +++ b/packages/json-document-affordance/README.md @@ -0,0 +1,31 @@ +# @interactive-os/json-document-affordance + +Official keyboard and mouse editing affordances for json-document hosts. +Preview results are `{ hand, cursor? }`. A write is `commitAffordance` +then `applyAffordance(..., { commit })`. Hosts keep markup and genre +Intent. It does not render widgets. + +```sh +npm i @interactive-os/json-document-affordance +``` + +```ts +import { + applyAffordance, + pointerSelect, +} from "@interactive-os/json-document-affordance"; + +applyAffordance(pointerSelect(event), { + hand: (hand) => { + if (hand.type === "select") { + editor.dispatch({ type: "selection.set", itemId, mode: hand.operation }); + } + }, +}); +``` + +Keyboard Adapter still translates chords. This package decides the +affordance those commands mean. React Connector still answers selection +queries through `useEditing` ports. + +Usage: [Affordance](https://developer-1px.github.io/json-document/docs/affordance) diff --git a/packages/json-document-affordance/package.json b/packages/json-document-affordance/package.json new file mode 100644 index 00000000..fbf340d0 --- /dev/null +++ b/packages/json-document-affordance/package.json @@ -0,0 +1,44 @@ +{ + "name": "@interactive-os/json-document-affordance", + "version": "0.1.0-rc.0", + "description": "Official keyboard and mouse editing affordances for json-document hosts.", + "type": "module", + "license": "MIT", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/developer-1px/json-document.git", + "directory": "packages/json-document-affordance" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "tag": "next" + }, + "files": ["dist", "!dist/.tsbuildinfo", "README.md", "LICENSE"], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "clean": "rm -rf dist", + "build": "npm run clean && tsc -b tsconfig.json", + "test": "vitest run --config vitest.config.ts", + "pretypecheck": "node ../../scripts/workspace-tasks.mjs build-dependencies", + "typecheck": "tsc -p tsconfig.test.json --noEmit", + "verify": "npm run typecheck && npm test && npm run build" + }, + "peerDependencies": { + "@interactive-os/json-document-web": ">=0.1.0-rc.0 <1" + }, + "devDependencies": { + "@interactive-os/json-document-web": "*", + "@types/node": "^25.9.0", + "typescript": "^5.0.0", + "vitest": "^4.1.7" + } +} diff --git a/packages/json-document-affordance/src/drag.ts b/packages/json-document-affordance/src/drag.ts new file mode 100644 index 00000000..3378cb10 --- /dev/null +++ b/packages/json-document-affordance/src/drag.ts @@ -0,0 +1,134 @@ +import type { WebModifierState } from "@interactive-os/json-document-web"; +import type { + AffordancePreview, + AffordanceRect, +} from "./result.js"; + +export type Point = { + readonly x: number; + readonly y: number; +}; + +export type DragOffset = { + readonly dx: number; + readonly dy: number; +}; + +export type Rect = AffordanceRect; + +function dragOffset(origin: Point, point: Point): DragOffset { + return { + dx: point.x - origin.x, + dy: point.y - origin.y, + }; +} + +export function dragAffordance(origin: Point, point: Point): AffordancePreview { + const offset = dragOffset(origin, point); + return { + hand: { type: "translate", dx: offset.dx, dy: offset.dy }, + cursor: "grabbing", + }; +} + +export function dragOperation(modifiers: WebModifierState & { readonly altKey?: boolean }): AffordancePreview { + if (modifiers.altKey) return { hand: { type: "copy" }, cursor: "copy" }; + return { hand: { type: "move-drop" }, cursor: "move" }; +} + +function marqueeRect(origin: Point, point: Point): Rect { + return { + x: Math.min(origin.x, point.x), + y: Math.min(origin.y, point.y), + width: Math.abs(point.x - origin.x), + height: Math.abs(point.y - origin.y), + }; +} + +export function marqueeAffordance(origin: Point, point: Point): AffordancePreview { + const rect = marqueeRect(origin, point); + return { + hand: { type: "select", operation: "replace", rect }, + cursor: "crosshair", + }; +} + +export function panAffordance(input: { + readonly spaceKey: boolean; + readonly buttons: number; + readonly origin?: Point; + readonly point?: Point; +}): AffordancePreview { + if (!input.spaceKey && input.buttons !== 4) return { hand: null }; + if (input.buttons === 0) return { hand: null, cursor: "grab" }; + if (input.origin && input.point) { + return { + hand: { + type: "translate", + dx: input.point.x - input.origin.x, + dy: input.point.y - input.origin.y, + }, + cursor: "grabbing", + }; + } + return { hand: { type: "translate", dx: 0, dy: 0 }, cursor: "grabbing" }; +} + +export function snapAffordance( + point: Point, + options: { readonly grid: number; readonly disable?: boolean }, +): AffordancePreview { + if (options.disable || options.grid <= 0) { + return { hand: { type: "translate", dx: point.x, dy: point.y } }; + } + const grid = options.grid; + return { + hand: { + type: "translate", + dx: Math.round(point.x / grid) * grid, + dy: Math.round(point.y / grid) * grid, + }, + }; +} + +export function nudgeAffordance(stroke: { + readonly key: string; + readonly shiftKey: boolean; +}): AffordancePreview { + const step = stroke.shiftKey ? 10 : 1; + if (stroke.key === "ArrowRight") return { hand: { type: "nudge", dx: step, dy: 0 } }; + if (stroke.key === "ArrowLeft") return { hand: { type: "nudge", dx: -step, dy: 0 } }; + if (stroke.key === "ArrowDown") return { hand: { type: "nudge", dx: 0, dy: step } }; + if (stroke.key === "ArrowUp") return { hand: { type: "nudge", dx: 0, dy: -step } }; + return { hand: null }; +} + +export function dropAffordance(input: { + readonly canDrop: boolean; + readonly operation?: "move" | "copy"; +}): AffordancePreview { + if (!input.canDrop) return { hand: null, cursor: "no-drop" }; + if (input.operation === "copy") return { hand: { type: "copy" }, cursor: "copy" }; + return { hand: { type: "move-drop" }, cursor: "move" }; +} + +export function forbiddenCursor(input: { + readonly allowed: boolean; + readonly dropping?: boolean; +}): AffordancePreview { + if (input.allowed) { + return { hand: null, cursor: input.dropping ? "move" : "default" }; + } + return { hand: null, cursor: input.dropping ? "no-drop" : "not-allowed" }; +} + +export function hoverAffordance(input: { + readonly elapsedMs: number; + readonly inside: boolean; + readonly delayMs?: number; +}): AffordancePreview { + if (!input.inside) return { hand: null }; + const delayMs = input.delayMs ?? 400; + if (input.elapsedMs >= delayMs) return { hand: { type: "hover", phase: "tooltip" } }; + return { hand: { type: "hover", phase: "hint" }, cursor: "help" }; +} diff --git a/packages/json-document-affordance/src/fold.ts b/packages/json-document-affordance/src/fold.ts new file mode 100644 index 00000000..0860b13b --- /dev/null +++ b/packages/json-document-affordance/src/fold.ts @@ -0,0 +1,34 @@ +import type { AffordancePreview } from "./result.js"; + +export type TreeFoldNode = { + readonly expanded: boolean; + readonly hasChildren: boolean; +}; + +export type TreeMoveDirection = "up" | "down" | "left" | "right" | "previous" | "next"; + +export type TreeAffordance = + | { readonly type: "expand" } + | { readonly type: "collapse" } + | { readonly type: "move"; readonly direction: TreeMoveDirection }; + +export function treeAffordance( + command: { readonly type: "move"; readonly direction: TreeMoveDirection }, + node: TreeFoldNode, +): AffordancePreview { + if (command.direction === "right" && node.hasChildren && !node.expanded) { + return { hand: { type: "expand" } }; + } + if (command.direction === "left" && node.hasChildren && node.expanded) { + return { hand: { type: "collapse" } }; + } + return { hand: { type: "move", direction: command.direction, operation: "replace" } }; +} + +export function disclosureAffordance(input: { + readonly key: string; + readonly expanded: boolean; +}): AffordancePreview { + if (input.key !== "Enter" && input.key !== " ") return { hand: null }; + return { hand: { type: input.expanded ? "collapse" : "expand" } }; +} diff --git a/packages/json-document-affordance/src/history.ts b/packages/json-document-affordance/src/history.ts new file mode 100644 index 00000000..16cb0bf0 --- /dev/null +++ b/packages/json-document-affordance/src/history.ts @@ -0,0 +1,26 @@ +import type { AffordancePreview } from "./result.js"; + +export type HistoryAffordanceName = "undo" | "redo"; + +export type HistoryAffordance = { + readonly name: HistoryAffordanceName; + readonly disabled: boolean; +}; + +export type HistoryAffordanceMap = { + readonly undo: HistoryAffordance; + readonly redo: HistoryAffordance; +}; + +export function historyAffordance(snapshot: { + readonly canUndo: boolean; + readonly canRedo: boolean; +}): AffordancePreview { + return { + hand: { + type: "history", + undo: { name: "undo", disabled: !snapshot.canUndo }, + redo: { name: "redo", disabled: !snapshot.canRedo }, + }, + }; +} diff --git a/packages/json-document-affordance/src/index.ts b/packages/json-document-affordance/src/index.ts new file mode 100644 index 00000000..22ea905c --- /dev/null +++ b/packages/json-document-affordance/src/index.ts @@ -0,0 +1,19 @@ +export { dragAffordance, dragOperation, dropAffordance, forbiddenCursor, hoverAffordance, marqueeAffordance, nudgeAffordance, panAffordance, snapAffordance } from "./drag.js"; +export type { Point, Rect } from "./drag.js"; +export { disclosureAffordance, treeAffordance } from "./fold.js"; +export type { TreeAffordance, TreeFoldNode, TreeMoveDirection } from "./fold.js"; +export { historyAffordance } from "./history.js"; +export type { HistoryAffordance, HistoryAffordanceMap, HistoryAffordanceName } from "./history.js"; +export { applyAffordance, commitAffordance } from "./result.js"; +export type { + AffordanceCommit, + AffordanceCommitActions, + AffordanceHand, + AffordanceMoveDirection, + AffordancePreview, + AffordancePreviewActions, + AffordanceRect, + AffordanceResult, + SelectOperation, +} from "./result.js"; +export { activateAffordance, clickCountAffordance, escapeAffordance, focusAffordance, pointerSelect, resolveAffordanceKey, selectAllAffordance, typeaheadAffordance } from "./select.js"; diff --git a/packages/json-document-affordance/src/result.ts b/packages/json-document-affordance/src/result.ts new file mode 100644 index 00000000..f4f85fc7 --- /dev/null +++ b/packages/json-document-affordance/src/result.ts @@ -0,0 +1,97 @@ +export type SelectOperation = "replace" | "extend" | "toggle"; + +export type AffordanceMoveDirection = "previous" | "next" | "up" | "down" | "left" | "right"; + +export type AffordanceRect = { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +}; + +export type AffordanceHand = + | { readonly type: "select"; readonly operation: SelectOperation; readonly rect?: AffordanceRect } + | { readonly type: "move"; readonly direction: AffordanceMoveDirection; readonly operation: "replace" | "extend" } + | { readonly type: "boundary"; readonly edge: "start" | "end"; readonly operation: "replace" | "extend" } + | { readonly type: "toggle" } + | { readonly type: "delete" } + | { readonly type: "undo" } + | { readonly type: "redo" } + | { readonly type: "expand" } + | { readonly type: "collapse" } + | { readonly type: "translate"; readonly dx: number; readonly dy: number } + | { readonly type: "nudge"; readonly dx: number; readonly dy: number } + | { readonly type: "select-all" } + | { readonly type: "clear" } + | { readonly type: "typeahead"; readonly buffer: string; readonly name: string | null } + | { readonly type: "click"; readonly count: number } + | { readonly type: "activate" } + | { readonly type: "cancel" } + | { readonly type: "tab"; readonly direction: "next" | "prev" } + | { readonly type: "hover"; readonly phase: "hint" | "tooltip" } + | { readonly type: "copy" } + | { readonly type: "move-drop" } + | { + readonly type: "history"; + readonly undo: { readonly name: "undo"; readonly disabled: boolean }; + readonly redo: { readonly name: "redo"; readonly disabled: boolean }; + }; + +export type AffordancePreview = { + readonly hand: H | null; + readonly cursor?: string; +}; + +export type AffordanceCommit = { + readonly hand: H; + readonly cursor?: string; + readonly commit: true; +}; + +export type AffordanceResult = + | AffordancePreview + | AffordanceCommit; + +export type AffordancePreviewActions = { + readonly cursor?: (cursor: string) => void; + readonly hand?: (hand: H) => void; +}; + +export type AffordanceCommitActions = + & AffordancePreviewActions + & { + readonly commit?: (hand: H) => void; + }; + +export function applyAffordance( + result: AffordanceCommit, + actions: AffordanceCommitActions, +): void; +export function applyAffordance( + result: AffordancePreview, + actions: AffordancePreviewActions, +): void; +export function applyAffordance( + result: AffordanceResult, + actions: AffordanceCommitActions, +): void { + if (result.cursor !== undefined) actions.cursor?.(result.cursor); + if (result.hand) actions.hand?.(result.hand); + if ("commit" in result && result.commit && result.hand) actions.commit?.(result.hand); +} + +export function commitAffordance( + result: AffordancePreview, +): AffordanceCommit | null { + const hand = result.hand; + if (hand == null) return null; + if (hand.type === "translate" && hand.dx === 0 && hand.dy === 0) return null; + if (hand.type === "select" && hand.rect && hand.rect.width === 0 && hand.rect.height === 0) { + return null; + } + return result.cursor === undefined + ? { hand, commit: true } + : { hand, cursor: result.cursor, commit: true }; +} + + diff --git a/packages/json-document-affordance/src/select.ts b/packages/json-document-affordance/src/select.ts new file mode 100644 index 00000000..53098c3f --- /dev/null +++ b/packages/json-document-affordance/src/select.ts @@ -0,0 +1,94 @@ +import { + createWebKeyboardAdapter, + selectionOperationFromModifiers, + type WebKeyboardStroke, +} from "@interactive-os/json-document-web"; +import { + type AffordancePreview, + type SelectOperation, +} from "./result.js"; + + +const keyboard = createWebKeyboardAdapter(); + +export type { SelectOperation }; + +export function pointerSelect(modifiers: { + readonly shiftKey?: boolean; + readonly metaKey?: boolean; + readonly ctrlKey?: boolean; +}): AffordancePreview { + return { + hand: { + type: "select", + operation: selectionOperationFromModifiers({ + shiftKey: modifiers.shiftKey ?? false, + metaKey: modifiers.metaKey ?? false, + ctrlKey: modifiers.ctrlKey ?? false, + }), + }, + }; +} + +export function resolveAffordanceKey(stroke: WebKeyboardStroke): AffordancePreview { + return { hand: keyboard.resolve(stroke) }; +} + +export function selectAllAffordance( + stroke: Pick, + state: { readonly allSelected: boolean }, +): AffordancePreview { + const mod = stroke.metaKey || stroke.ctrlKey; + if (!mod || stroke.key.toLowerCase() !== "a") return { hand: null }; + return { hand: { type: state.allSelected ? "clear" : "select-all" } }; +} + +export function typeaheadAffordance(input: { + readonly buffer: string; + readonly key: string; + readonly elapsedMs: number; + readonly names: ReadonlyArray; + readonly from: string | null; + readonly windowMs?: number; +}): AffordancePreview { + if (input.key.length !== 1 || input.key < " ") return { hand: null }; + const windowMs = input.windowMs ?? 500; + const buffer = input.elapsedMs <= windowMs ? `${input.buffer}${input.key}` : input.key; + const query = buffer.toLowerCase(); + const start = Math.max(0, input.names.findIndex((name) => name === input.from)); + const ordered = [...input.names.slice(start + 1), ...input.names.slice(0, start + 1)]; + const name = ordered.find((candidate) => candidate.toLowerCase().startsWith(query)) ?? null; + return { hand: { type: "typeahead", buffer, name } }; +} + +export function clickCountAffordance(detail: number): AffordancePreview { + if (detail < 1) return { hand: null }; + return { hand: { type: "click", count: detail } }; +} + +export function activateAffordance(input: { readonly key?: string; readonly detail?: number; readonly button?: number }): AffordancePreview { + if (input.key === "Enter") return { hand: { type: "activate" } }; + if (input.button === 0 && (input.detail ?? 1) === 1) return { hand: { type: "activate" } }; + return { hand: null }; +} + +export function escapeAffordance(input: { readonly key?: string; readonly type?: string }): AffordancePreview { + if (input.key === "Escape" || input.type === "pointercancel" || input.type === "lostpointercapture") { + return { hand: { type: "cancel" } }; + } + return { hand: null }; +} + +export function focusAffordance(stroke: Pick): AffordancePreview { + if (stroke.key === "Tab") { + return { hand: { type: "tab", direction: stroke.shiftKey ? "prev" : "next" } }; + } + const command = keyboard.resolve({ + key: stroke.key, + shiftKey: false, + metaKey: false, + ctrlKey: false, + }); + if (command?.type === "move" || command?.type === "boundary") return { hand: command }; + return { hand: null }; +} diff --git a/packages/json-document-affordance/tests/affordance.test.ts b/packages/json-document-affordance/tests/affordance.test.ts new file mode 100644 index 00000000..e943dd28 --- /dev/null +++ b/packages/json-document-affordance/tests/affordance.test.ts @@ -0,0 +1,197 @@ +import { describe, expect, test } from "vitest"; +import { + applyAffordance, + commitAffordance, + dragAffordance, + dropAffordance, + escapeAffordance, + historyAffordance, + pointerSelect, + resolveAffordanceKey, + snapAffordance, + treeAffordance, + typeaheadAffordance, +} from "../src/index.js"; + +describe("pointerSelect", () => { + test("maps conventional modifiers to replace, extend, and toggle", () => { + const operations: string[] = []; + applyAffordance(pointerSelect({ shiftKey: false, metaKey: false, ctrlKey: false }), { + hand: (hand) => { + if (hand.type === "select") operations.push(hand.operation); + }, + }); + applyAffordance(pointerSelect({ shiftKey: true, metaKey: false, ctrlKey: false }), { + hand: (hand) => { + if (hand.type === "select") operations.push(hand.operation); + }, + }); + applyAffordance(pointerSelect({ shiftKey: false, metaKey: true, ctrlKey: false }), { + hand: (hand) => { + if (hand.type === "select") operations.push(hand.operation); + }, + }); + applyAffordance(pointerSelect({ shiftKey: false, metaKey: false, ctrlKey: true }), { + hand: (hand) => { + if (hand.type === "select") operations.push(hand.operation); + }, + }); + expect(operations).toEqual(["replace", "extend", "toggle", "toggle"]); + }); +}); + +describe("resolveAffordanceKey", () => { + test("closes the conventional keymap without a host override", () => { + const hands: unknown[] = []; + applyAffordance(resolveAffordanceKey({ key: "ArrowDown", shiftKey: false, metaKey: false, ctrlKey: false }), { + hand: (hand) => hands.push(hand), + }); + applyAffordance(resolveAffordanceKey({ key: "ArrowDown", shiftKey: true, metaKey: false, ctrlKey: false }), { + hand: (hand) => hands.push(hand), + }); + applyAffordance(resolveAffordanceKey({ key: "z", shiftKey: false, metaKey: true, ctrlKey: false }), { + hand: (hand) => hands.push(hand), + }); + applyAffordance(resolveAffordanceKey({ key: "z", shiftKey: true, metaKey: true, ctrlKey: false }), { + hand: (hand) => hands.push(hand), + }); + applyAffordance(resolveAffordanceKey({ key: "Delete", shiftKey: false, metaKey: false, ctrlKey: false }), { + hand: (hand) => hands.push(hand), + }); + expect(hands).toEqual([ + { type: "move", direction: "down", operation: "replace" }, + { type: "move", direction: "down", operation: "extend" }, + { type: "undo" }, + { type: "redo" }, + { type: "delete" }, + ]); + }); +}); + +describe("treeAffordance", () => { + test("uses right to expand a collapsed parent and left to collapse an expanded parent", () => { + expect(treeAffordance({ type: "move", direction: "right" }, { expanded: false, hasChildren: true }).hand).toEqual({ + type: "expand", + }); + expect(treeAffordance({ type: "move", direction: "left" }, { expanded: true, hasChildren: true }).hand).toEqual({ + type: "collapse", + }); + expect(treeAffordance({ type: "move", direction: "right" }, { expanded: true, hasChildren: true }).hand).toEqual({ + type: "move", + direction: "right", + operation: "replace", + }); + }); +}); + +describe("dragAffordance", () => { + test("preview carries translate facts without a commit slot", () => { + expect(dragAffordance({ x: 10, y: 20 }, { x: 14, y: 18 })).toEqual({ + hand: { type: "translate", dx: 4, dy: -2 }, + cursor: "grabbing", + }); + expect("commit" in dragAffordance({ x: 10, y: 20 }, { x: 14, y: 18 })).toBe(false); + }); +}); + +describe("commitAffordance", () => { + test("commits a moved drag and ignores a stationary one", () => { + expect(commitAffordance(dragAffordance({ x: 10, y: 20 }, { x: 14, y: 18 }))).toEqual({ + hand: { type: "translate", dx: 4, dy: -2 }, + cursor: "grabbing", + commit: true, + }); + expect(commitAffordance(dragAffordance({ x: 10, y: 20 }, { x: 10, y: 20 }))).toBeNull(); + }); +}); + +describe("historyAffordance", () => { + test("binds disabled to canUndo and canRedo", () => { + expect(historyAffordance({ canUndo: false, canRedo: true }).hand).toEqual({ + type: "history", + undo: { name: "undo", disabled: true }, + redo: { name: "redo", disabled: false }, + }); + }); +}); + +describe("applyAffordance", () => { + test("applies cursor and hand from a preview", () => { + const cursors: string[] = []; + const hands: string[] = []; + applyAffordance( + { hand: { type: "select", operation: "extend" }, cursor: "cell" }, + { + cursor: (cursor) => cursors.push(cursor), + hand: (hand) => hands.push(hand.type), + }, + ); + expect(cursors).toEqual(["cell"]); + expect(hands).toEqual(["select"]); + }); + + test("applies commit only on a commit result", () => { + const committed: string[] = []; + const previewed: string[] = []; + applyAffordance( + { hand: { type: "translate", dx: 4, dy: -2 }, cursor: "grabbing", commit: true }, + { + hand: (hand) => previewed.push(hand.type), + commit: (hand) => committed.push(hand.type), + }, + ); + expect(previewed).toEqual(["translate"]); + expect(committed).toEqual(["translate"]); + }); +}); + +describe("typeaheadAffordance", () => { + test("jumps to the next name with the typed prefix", () => { + expect(typeaheadAffordance({ + buffer: "", + key: "A", + elapsedMs: 0, + names: ["Inbox", "Today", "Later"], + from: "Inbox", + }).hand).toEqual({ type: "typeahead", buffer: "A", name: null }); + expect(typeaheadAffordance({ + buffer: "", + key: "T", + elapsedMs: 0, + names: ["Inbox", "Today", "Later"], + from: "Inbox", + }).hand).toEqual({ type: "typeahead", buffer: "T", name: "Today" }); + }); +}); + +describe("snapAffordance", () => { + test("snaps to the grid unless disabled", () => { + expect(snapAffordance({ x: 47, y: 51 }, { grid: 8 }).hand).toEqual({ type: "translate", dx: 48, dy: 48 }); + expect(snapAffordance({ x: 47, y: 51 }, { grid: 8, disable: true }).hand).toEqual({ type: "translate", dx: 47, dy: 51 }); + }); +}); + +describe("escapeAffordance", () => { + test("cancels Escape and pointercancel", () => { + expect(escapeAffordance({ key: "Escape" }).hand).toEqual({ type: "cancel" }); + expect(escapeAffordance({ type: "pointercancel" }).hand).toEqual({ type: "cancel" }); + expect(escapeAffordance({ key: "Enter" }).hand).toBeNull(); + }); +}); + +describe("dropAffordance", () => { + test("previews a drop; only commitAffordance mints the write", () => { + expect(dropAffordance({ canDrop: false })).toEqual({ hand: null, cursor: "no-drop" }); + expect(dropAffordance({ canDrop: true })).toEqual({ + hand: { type: "move-drop" }, + cursor: "move", + }); + expect("commit" in dropAffordance({ canDrop: true })).toBe(false); + expect(commitAffordance(dropAffordance({ canDrop: false }))).toBeNull(); + expect(commitAffordance(dropAffordance({ canDrop: true }))).toEqual({ + hand: { type: "move-drop" }, + cursor: "move", + commit: true, + }); + }); +}); diff --git a/packages/json-document-affordance/tsconfig.json b/packages/json-document-affordance/tsconfig.json new file mode 100644 index 00000000..ee593812 --- /dev/null +++ b/packages/json-document-affordance/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig/library.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/.tsbuildinfo" + }, + "references": [{ "path": "../json-document-web" }], + "include": ["src/**/*.ts"] +} diff --git a/packages/json-document-affordance/tsconfig.test.json b/packages/json-document-affordance/tsconfig.test.json new file mode 100644 index 00000000..adf237ce --- /dev/null +++ b/packages/json-document-affordance/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": false, + "noEmit": true, + "rootDir": "../..", + "types": ["node", "vitest/globals"] + }, + "include": ["src/**/*.ts", "tests/**/*.ts", "vitest.config.ts"] +} diff --git a/packages/json-document-affordance/vitest.config.ts b/packages/json-document-affordance/vitest.config.ts new file mode 100644 index 00000000..e2670c2a --- /dev/null +++ b/packages/json-document-affordance/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineNodeProject } from "../../test/vitest.shared.js"; + +export default defineNodeProject("json-document-affordance", { + resolve: { + alias: { + "@interactive-os/json-document-web": new URL("../json-document-web/src/index.ts", import.meta.url).pathname, + "@interactive-os/json-document-selection": new URL("../json-document-selection/src/index.ts", import.meta.url).pathname, + "@interactive-os/json-document-editing": new URL("../json-document-editing/src/index.ts", import.meta.url).pathname, + "@interactive-os/json-document": new URL("../json-document/src/application/document/index.ts", import.meta.url).pathname, + }, + }, +}); diff --git a/site/config/json-document-source-aliases.ts b/site/config/json-document-source-aliases.ts index 7892df13..5d75d60d 100644 --- a/site/config/json-document-source-aliases.ts +++ b/site/config/json-document-source-aliases.ts @@ -31,6 +31,10 @@ export function jsonDocumentSourceAliases(): SourceAlias[] { find: "@interactive-os/json-document-ajv", replacement: sourceFile("packages/json-document-ajv/src/index.ts"), }, + { + find: "@interactive-os/json-document-affordance", + replacement: sourceFile("packages/json-document-affordance/src/index.ts"), + }, { find: "@interactive-os/json-document-zod", replacement: sourceFile("packages/json-document-zod/src/index.ts"), diff --git a/site/package.json b/site/package.json index aaed0a4d..df7d351d 100644 --- a/site/package.json +++ b/site/package.json @@ -16,6 +16,7 @@ "@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-tanstack-table": "*", "@interactive-os/json-document-web": "*", "@interactive-os/json-document-contenteditable": "*", diff --git a/site/scripts/route-checks.mjs b/site/scripts/route-checks.mjs index 04ebf17d..d60e6528 100644 --- a/site/scripts/route-checks.mjs +++ b/site/scripts/route-checks.mjs @@ -3,10 +3,10 @@ const navigationGroups = new Set([ "JSON Document", "Collaboration", "Editing", - "Hands", "Adapter", "Connector", - "제품 화면", + "Affordance", + "Hands", ]); export function validateSiteRoutes(routes, fail) { diff --git a/site/site-routes.json b/site/site-routes.json index 8c1b0517..8df70b02 100644 --- a/site/site-routes.json +++ b/site/site-routes.json @@ -434,32 +434,327 @@ } }, { - "path": "/widgets", - "label": "제품 화면", - "title": "Widgets - json-document", - "description": "Sample widgets that bind only editing affordances: Toolbar, Listbox, and Grid.", - "navigationGroup": "제품 화면", + "path": "/docs/affordance", + "label": "Affordance", + "title": "Affordance - json-document", + "heading": "Affordance", + "description": "제품이 json-document를 만지는 최전선의 키보드·마우스·커서 손을 API와 사용법으로 설명합니다.", + "language": "ko", + "navigationGroup": "Affordance", "sidebar": false }, + { + "path": "/docs/affordance/focus", + "label": "Focus", + "title": "Focus - json-document", + "heading": "Focus", + "description": "Tab은 컴포넌트 사이로, 화살표는 컴포넌트 안에서 초점을 옮기는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/caret", + "label": "Caret", + "title": "Caret - json-document", + "heading": "Caret", + "description": "I-beam 클릭으로 글 삽입점을 두고, 화살표로 글자 사이를 옮기는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/select", + "label": "Select", + "title": "Select - json-document", + "heading": "Select", + "description": "클릭, Shift 범위, Mod 토글, 화살표로 대상을 집는 손을 붙입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "relatedDemoPath": "/widgets/listbox", + "relatedDemoLabel": "증명 열기", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/typeahead", + "label": "Typeahead", + "title": "Typeahead - json-document", + "heading": "Typeahead", + "description": "글쇠를 누르면 그 글자로 시작하는 다음 항목으로 초점을 옮기는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/activate", + "label": "Activate", + "title": "Activate - json-document", + "heading": "Activate", + "description": "클릭·Enter·Space로 대상의 기본 동작을 실행하는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/cancel", + "label": "Escape", + "title": "Escape - json-document", + "heading": "Escape", + "description": "Escape와 pointercancel로 진행 중인 손을 버리는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/fold", + "label": "Expand/Collapse", + "title": "Expand/Collapse - json-document", + "heading": "Expand/Collapse", + "description": "나무에서 왼쪽은 접고 오른쪽은 펼치는 손을 붙입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "relatedDemoPath": "/widgets/tree", + "relatedDemoLabel": "증명 열기", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/history", + "label": "Undo", + "title": "Undo - json-document", + "heading": "Undo", + "description": "Mod+Z와 canUndo로 값과 선택을 함께 되돌리는 손을 붙입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "relatedDemoPath": "/widgets/toolbar", + "relatedDemoLabel": "증명 열기", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/delete", + "label": "Delete", + "title": "Delete - json-document", + "heading": "Delete", + "description": "Delete와 Backspace로 고른 대상을 지우는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/rename", + "label": "Rename", + "title": "Rename - json-document", + "heading": "Rename", + "description": "F2와 느린 두 번 누르기로 고른 대상의 이름을 고치는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/nudge", + "label": "Nudge", + "title": "Nudge - json-document", + "heading": "Nudge", + "description": "화살표로 고른 대상을 한 단위 옮기고, Shift로 큰 단위를 쓰는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/hover", + "label": "Hover", + "title": "Hover - json-document", + "heading": "Hover", + "description": "누르지 않고 포인터를 올려 손과 도움말을 드러내는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/double-click", + "label": "Double-click", + "title": "Double-click - json-document", + "heading": "Double-click", + "description": "click detail 2로 열기·단어 고르기·이름 바꾸기를 여는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/triple-click", + "label": "Triple-click", + "title": "Triple-click - json-document", + "heading": "Triple-click", + "description": "click detail 3으로 줄이나 문단을 고르는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/context-menu", + "label": "Context menu", + "title": "Context menu - json-document", + "heading": "Context menu", + "description": "오른쪽 클릭, Shift+F10, Menu 키로 자리 메뉴를 여는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/drag", + "label": "Drag", + "title": "Drag - json-document", + "heading": "Drag", + "description": "고른 대상을 포인터로 옮기는 손을 붙입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "relatedDemoPath": "/widgets/canvas", + "relatedDemoLabel": "증명 열기", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/marquee", + "label": "Marquee", + "title": "Marquee - json-document", + "heading": "Marquee", + "description": "빈 곳에서 끌어서 여러 대상을 한 번에 고르는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/drop", + "label": "Drop", + "title": "Drop - json-document", + "heading": "Drop", + "description": "드래그한 대상을 어디에 둘지 정하고, 못 두면 no-drop을 보여주는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/copy-drag", + "label": "Duplicate", + "title": "Duplicate - json-document", + "heading": "Duplicate", + "description": "Alt/Option을 누른 채 드래그하면 원본을 복제하는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/resize", + "label": "Resize", + "title": "Resize - json-document", + "heading": "Resize", + "description": "모서리·칸·분할선을 끌어 크기를 바꾸고, 화살표로 미세 조절하는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/pan", + "label": "Pan", + "title": "Pan - json-document", + "heading": "Pan", + "description": "손바닥 커서로 평면을 밀고, Space+드래그로 화면을 옮기는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/scroll", + "label": "Scroll", + "title": "Scroll - json-document", + "heading": "Scroll", + "description": "휠과 드래그 중 자동 스크롤로 보이는 줄을 옮기는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/zoom", + "label": "Zoom", + "title": "Zoom - json-document", + "heading": "Zoom", + "description": "Mod+휠과 +/- 키, zoom-in/out 커서로 보이는 배율을 바꾸는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/snap", + "label": "Snap", + "title": "Snap - json-document", + "heading": "Snap", + "description": "드래그와 크기 바꾸기 중 그리드·가이드에 붙고, 수정 키로 푸는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/forbid", + "label": "Not-allowed", + "title": "Not-allowed - json-document", + "heading": "Not-allowed", + "description": "할 수 없는 자리에서 not-allowed와 no-drop 커서를 보여주는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, { "path": "/widgets/toolbar", - "label": "Toolbar", - "title": "Toolbar Widget - json-document", + "label": "Toolbar proof", + "title": "Toolbar proof - json-document", "description": "Undo and Redo disabled states come from canUndo and canRedo.", - "navigationGroup": "제품 화면" + "parentPath": "/docs/affordance/history", + "sidebar": false }, { "path": "/widgets/listbox", - "label": "Listbox", - "title": "Listbox Widget - json-document", + "label": "Listbox proof", + "title": "Listbox proof - json-document", "description": "A listbox reads selected keys and focus from Order selection.", - "navigationGroup": "제품 화면" + "parentPath": "/docs/affordance/select", + "sidebar": false }, { "path": "/widgets/grid", - "label": "Grid", - "title": "Grid Widget - json-document", + "label": "Grid proof", + "title": "Grid proof - json-document", "description": "A grid reads visible topology and selected cells from Sheet selection.", - "navigationGroup": "제품 화면" + "parentPath": "/docs/affordance/select", + "sidebar": false + }, + { + "path": "/widgets/document", + "label": "Document proof", + "title": "Document proof - json-document", + "description": "A document line reads selected keys, focus, and text offset from block selection.", + "parentPath": "/docs/affordance/select", + "sidebar": false + }, + { + "path": "/widgets/canvas", + "label": "Canvas proof", + "title": "Canvas proof - json-document", + "description": "A canvas reads selected objects on a plane from Object selection.", + "parentPath": "/docs/affordance/drag", + "sidebar": false + }, + { + "path": "/widgets/tree", + "label": "Tree proof", + "title": "Tree proof - json-document", + "description": "A tree reads the visible hierarchical line from Tree selection.", + "parentPath": "/docs/affordance/fold", + "sidebar": false + }, + { + "path": "/widgets/board", + "label": "Board proof", + "title": "Board proof - json-document", + "description": "A board reads columns of cards and selected keys from Kanban selection.", + "parentPath": "/docs/affordance/drag", + "sidebar": false } ] diff --git a/site/src/app/legacy-page-redirects.ts b/site/src/app/legacy-page-redirects.ts index 7b348a85..3131189a 100644 --- a/site/src/app/legacy-page-redirects.ts +++ b/site/src/app/legacy-page-redirects.ts @@ -11,4 +11,8 @@ export const legacyPageRedirects = { from: "/demos", to: "/editors", }, + widgetsCatalog: { + from: "/widgets", + to: "/docs/affordance", + }, } as const; diff --git a/site/src/app/page-descriptors.ts b/site/src/app/page-descriptors.ts index fa87b7c1..efc0c493 100644 --- a/site/src/app/page-descriptors.ts +++ b/site/src/app/page-descriptors.ts @@ -8,7 +8,7 @@ export type SiteNavigationGroup = | "Hands" | "Adapter" | "Connector" - | "제품 화면"; + | "Affordance"; export type IntegrationKind = "adapter" | "connector"; export type SiteRoute = { diff --git a/site/src/app/routeTree.gen.ts b/site/src/app/routeTree.gen.ts index 9bdf6ba1..ec4137ab 100644 --- a/site/src/app/routeTree.gen.ts +++ b/site/src/app/routeTree.gen.ts @@ -54,11 +54,41 @@ import { Route as PageDocsTreeRouteImport } from "./routes/_page/docs/tree"; import { Route as PageDocsTutorialRouteImport } from "./routes/_page/docs/tutorial"; import { Route as PageEditingRichTextRouteImport } from "./routes/_page/editing/rich-text"; import { Route as PageWidgetsIndexRouteImport } from "./routes/_page/widgets/index"; +import { Route as PageWidgetsBoardRouteImport } from "./routes/_page/widgets/board"; +import { Route as PageWidgetsCanvasRouteImport } from "./routes/_page/widgets/canvas"; +import { Route as PageWidgetsDocumentRouteImport } from "./routes/_page/widgets/document"; import { Route as PageWidgetsGridRouteImport } from "./routes/_page/widgets/grid"; import { Route as PageWidgetsListboxRouteImport } from "./routes/_page/widgets/listbox"; import { Route as PageWidgetsToolbarRouteImport } from "./routes/_page/widgets/toolbar"; +import { Route as PageWidgetsTreeRouteImport } from "./routes/_page/widgets/tree"; import { Route as PageConnectorsZodIndexRouteImport } from "./routes/_page/connectors/zod/index"; import { Route as PageConnectorsZodValidateRouteImport } from "./routes/_page/connectors/zod/validate"; +import { Route as PageDocsAffordanceIndexRouteImport } from "./routes/_page/docs/affordance/index"; +import { Route as PageDocsAffordanceActivateRouteImport } from "./routes/_page/docs/affordance/activate"; +import { Route as PageDocsAffordanceCancelRouteImport } from "./routes/_page/docs/affordance/cancel"; +import { Route as PageDocsAffordanceCaretRouteImport } from "./routes/_page/docs/affordance/caret"; +import { Route as PageDocsAffordanceContextMenuRouteImport } from "./routes/_page/docs/affordance/context-menu"; +import { Route as PageDocsAffordanceCopyDragRouteImport } from "./routes/_page/docs/affordance/copy-drag"; +import { Route as PageDocsAffordanceDeleteRouteImport } from "./routes/_page/docs/affordance/delete"; +import { Route as PageDocsAffordanceDoubleClickRouteImport } from "./routes/_page/docs/affordance/double-click"; +import { Route as PageDocsAffordanceDragRouteImport } from "./routes/_page/docs/affordance/drag"; +import { Route as PageDocsAffordanceDropRouteImport } from "./routes/_page/docs/affordance/drop"; +import { Route as PageDocsAffordanceFocusRouteImport } from "./routes/_page/docs/affordance/focus"; +import { Route as PageDocsAffordanceFoldRouteImport } from "./routes/_page/docs/affordance/fold"; +import { Route as PageDocsAffordanceForbidRouteImport } from "./routes/_page/docs/affordance/forbid"; +import { Route as PageDocsAffordanceHistoryRouteImport } from "./routes/_page/docs/affordance/history"; +import { Route as PageDocsAffordanceHoverRouteImport } from "./routes/_page/docs/affordance/hover"; +import { Route as PageDocsAffordanceMarqueeRouteImport } from "./routes/_page/docs/affordance/marquee"; +import { Route as PageDocsAffordanceNudgeRouteImport } from "./routes/_page/docs/affordance/nudge"; +import { Route as PageDocsAffordancePanRouteImport } from "./routes/_page/docs/affordance/pan"; +import { Route as PageDocsAffordanceRenameRouteImport } from "./routes/_page/docs/affordance/rename"; +import { Route as PageDocsAffordanceResizeRouteImport } from "./routes/_page/docs/affordance/resize"; +import { Route as PageDocsAffordanceScrollRouteImport } from "./routes/_page/docs/affordance/scroll"; +import { Route as PageDocsAffordanceSelectRouteImport } from "./routes/_page/docs/affordance/select"; +import { Route as PageDocsAffordanceSnapRouteImport } from "./routes/_page/docs/affordance/snap"; +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 PageDocsCollaborationIndexRouteImport } from "./routes/_page/docs/collaboration/index"; import { Route as PageDocsCollaborationHistoryRouteImport } from "./routes/_page/docs/collaboration/history"; import { Route as PageDocsCollaborationLifecycleRouteImport } from "./routes/_page/docs/collaboration/lifecycle"; @@ -294,6 +324,21 @@ const PageWidgetsIndexRoute = PageWidgetsIndexRouteImport.update({ path: "/widgets/", getParentRoute: () => PageRoute, } as any); +const PageWidgetsBoardRoute = PageWidgetsBoardRouteImport.update({ + id: "/widgets/board", + path: "/widgets/board", + getParentRoute: () => PageRoute, +} as any); +const PageWidgetsCanvasRoute = PageWidgetsCanvasRouteImport.update({ + id: "/widgets/canvas", + path: "/widgets/canvas", + getParentRoute: () => PageRoute, +} as any); +const PageWidgetsDocumentRoute = PageWidgetsDocumentRouteImport.update({ + id: "/widgets/document", + path: "/widgets/document", + getParentRoute: () => PageRoute, +} as any); const PageWidgetsGridRoute = PageWidgetsGridRouteImport.update({ id: "/widgets/grid", path: "/widgets/grid", @@ -309,6 +354,11 @@ const PageWidgetsToolbarRoute = PageWidgetsToolbarRouteImport.update({ path: "/widgets/toolbar", getParentRoute: () => PageRoute, } as any); +const PageWidgetsTreeRoute = PageWidgetsTreeRouteImport.update({ + id: "/widgets/tree", + path: "/widgets/tree", + getParentRoute: () => PageRoute, +} as any); const PageConnectorsZodIndexRoute = PageConnectorsZodIndexRouteImport.update({ id: "/connectors/zod/", path: "/connectors/zod/", @@ -320,6 +370,151 @@ const PageConnectorsZodValidateRoute = path: "/connectors/zod/validate", getParentRoute: () => PageRoute, } as any); +const PageDocsAffordanceIndexRoute = PageDocsAffordanceIndexRouteImport.update({ + id: "/docs/affordance/", + path: "/docs/affordance/", + getParentRoute: () => PageRoute, +} as any); +const PageDocsAffordanceActivateRoute = + PageDocsAffordanceActivateRouteImport.update({ + id: "/docs/affordance/activate", + path: "/docs/affordance/activate", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceCancelRoute = + PageDocsAffordanceCancelRouteImport.update({ + id: "/docs/affordance/cancel", + path: "/docs/affordance/cancel", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceCaretRoute = PageDocsAffordanceCaretRouteImport.update({ + id: "/docs/affordance/caret", + path: "/docs/affordance/caret", + getParentRoute: () => PageRoute, +} as any); +const PageDocsAffordanceContextMenuRoute = + PageDocsAffordanceContextMenuRouteImport.update({ + id: "/docs/affordance/context-menu", + path: "/docs/affordance/context-menu", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceCopyDragRoute = + PageDocsAffordanceCopyDragRouteImport.update({ + id: "/docs/affordance/copy-drag", + path: "/docs/affordance/copy-drag", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceDeleteRoute = + PageDocsAffordanceDeleteRouteImport.update({ + id: "/docs/affordance/delete", + path: "/docs/affordance/delete", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceDoubleClickRoute = + PageDocsAffordanceDoubleClickRouteImport.update({ + id: "/docs/affordance/double-click", + path: "/docs/affordance/double-click", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceDragRoute = PageDocsAffordanceDragRouteImport.update({ + id: "/docs/affordance/drag", + path: "/docs/affordance/drag", + getParentRoute: () => PageRoute, +} as any); +const PageDocsAffordanceDropRoute = PageDocsAffordanceDropRouteImport.update({ + id: "/docs/affordance/drop", + path: "/docs/affordance/drop", + getParentRoute: () => PageRoute, +} as any); +const PageDocsAffordanceFocusRoute = PageDocsAffordanceFocusRouteImport.update({ + id: "/docs/affordance/focus", + path: "/docs/affordance/focus", + getParentRoute: () => PageRoute, +} as any); +const PageDocsAffordanceFoldRoute = PageDocsAffordanceFoldRouteImport.update({ + id: "/docs/affordance/fold", + path: "/docs/affordance/fold", + getParentRoute: () => PageRoute, +} as any); +const PageDocsAffordanceForbidRoute = + PageDocsAffordanceForbidRouteImport.update({ + id: "/docs/affordance/forbid", + path: "/docs/affordance/forbid", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceHistoryRoute = + PageDocsAffordanceHistoryRouteImport.update({ + id: "/docs/affordance/history", + path: "/docs/affordance/history", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceHoverRoute = PageDocsAffordanceHoverRouteImport.update({ + id: "/docs/affordance/hover", + path: "/docs/affordance/hover", + getParentRoute: () => PageRoute, +} as any); +const PageDocsAffordanceMarqueeRoute = + PageDocsAffordanceMarqueeRouteImport.update({ + id: "/docs/affordance/marquee", + path: "/docs/affordance/marquee", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceNudgeRoute = PageDocsAffordanceNudgeRouteImport.update({ + id: "/docs/affordance/nudge", + path: "/docs/affordance/nudge", + getParentRoute: () => PageRoute, +} as any); +const PageDocsAffordancePanRoute = PageDocsAffordancePanRouteImport.update({ + id: "/docs/affordance/pan", + path: "/docs/affordance/pan", + getParentRoute: () => PageRoute, +} as any); +const PageDocsAffordanceRenameRoute = + PageDocsAffordanceRenameRouteImport.update({ + id: "/docs/affordance/rename", + path: "/docs/affordance/rename", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceResizeRoute = + PageDocsAffordanceResizeRouteImport.update({ + id: "/docs/affordance/resize", + path: "/docs/affordance/resize", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceScrollRoute = + PageDocsAffordanceScrollRouteImport.update({ + id: "/docs/affordance/scroll", + path: "/docs/affordance/scroll", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceSelectRoute = + PageDocsAffordanceSelectRouteImport.update({ + id: "/docs/affordance/select", + path: "/docs/affordance/select", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceSnapRoute = PageDocsAffordanceSnapRouteImport.update({ + id: "/docs/affordance/snap", + path: "/docs/affordance/snap", + getParentRoute: () => PageRoute, +} as any); +const PageDocsAffordanceTripleClickRoute = + PageDocsAffordanceTripleClickRouteImport.update({ + id: "/docs/affordance/triple-click", + path: "/docs/affordance/triple-click", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceTypeaheadRoute = + PageDocsAffordanceTypeaheadRouteImport.update({ + id: "/docs/affordance/typeahead", + path: "/docs/affordance/typeahead", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceZoomRoute = PageDocsAffordanceZoomRouteImport.update({ + id: "/docs/affordance/zoom", + path: "/docs/affordance/zoom", + getParentRoute: () => PageRoute, +} as any); const PageDocsCollaborationIndexRoute = PageDocsCollaborationIndexRouteImport.update({ id: "/docs/collaboration/", @@ -397,19 +592,49 @@ export interface FileRoutesByFullPath { "/docs/tree": typeof PageDocsTreeRoute; "/docs/tutorial": typeof PageDocsTutorialRoute; "/editing/rich-text": typeof PageEditingRichTextRoute; + "/widgets/board": typeof PageWidgetsBoardRoute; + "/widgets/canvas": typeof PageWidgetsCanvasRoute; + "/widgets/document": typeof PageWidgetsDocumentRoute; "/widgets/grid": typeof PageWidgetsGridRoute; "/widgets/listbox": typeof PageWidgetsListboxRoute; "/widgets/toolbar": typeof PageWidgetsToolbarRoute; + "/widgets/tree": typeof PageWidgetsTreeRoute; "/adapters/": typeof PageAdaptersIndexRoute; "/connectors/": typeof PageConnectorsIndexRoute; "/demo/": typeof PageDemoIndexRoute; "/docs/": typeof PageDocsIndexRoute; "/widgets/": typeof PageWidgetsIndexRoute; "/connectors/zod/validate": typeof PageConnectorsZodValidateRoute; + "/docs/affordance/activate": typeof PageDocsAffordanceActivateRoute; + "/docs/affordance/cancel": typeof PageDocsAffordanceCancelRoute; + "/docs/affordance/caret": typeof PageDocsAffordanceCaretRoute; + "/docs/affordance/context-menu": typeof PageDocsAffordanceContextMenuRoute; + "/docs/affordance/copy-drag": typeof PageDocsAffordanceCopyDragRoute; + "/docs/affordance/delete": typeof PageDocsAffordanceDeleteRoute; + "/docs/affordance/double-click": typeof PageDocsAffordanceDoubleClickRoute; + "/docs/affordance/drag": typeof PageDocsAffordanceDragRoute; + "/docs/affordance/drop": typeof PageDocsAffordanceDropRoute; + "/docs/affordance/focus": typeof PageDocsAffordanceFocusRoute; + "/docs/affordance/fold": typeof PageDocsAffordanceFoldRoute; + "/docs/affordance/forbid": typeof PageDocsAffordanceForbidRoute; + "/docs/affordance/history": typeof PageDocsAffordanceHistoryRoute; + "/docs/affordance/hover": typeof PageDocsAffordanceHoverRoute; + "/docs/affordance/marquee": typeof PageDocsAffordanceMarqueeRoute; + "/docs/affordance/nudge": typeof PageDocsAffordanceNudgeRoute; + "/docs/affordance/pan": typeof PageDocsAffordancePanRoute; + "/docs/affordance/rename": typeof PageDocsAffordanceRenameRoute; + "/docs/affordance/resize": typeof PageDocsAffordanceResizeRoute; + "/docs/affordance/scroll": typeof PageDocsAffordanceScrollRoute; + "/docs/affordance/select": typeof PageDocsAffordanceSelectRoute; + "/docs/affordance/snap": typeof PageDocsAffordanceSnapRoute; + "/docs/affordance/triple-click": typeof PageDocsAffordanceTripleClickRoute; + "/docs/affordance/typeahead": typeof PageDocsAffordanceTypeaheadRoute; + "/docs/affordance/zoom": typeof PageDocsAffordanceZoomRoute; "/docs/collaboration/history": typeof PageDocsCollaborationHistoryRoute; "/docs/collaboration/lifecycle": typeof PageDocsCollaborationLifecycleRoute; "/docs/collaboration/replica": typeof PageDocsCollaborationReplicaRoute; "/connectors/zod/": typeof PageConnectorsZodIndexRoute; + "/docs/affordance/": typeof PageDocsAffordanceIndexRoute; "/docs/collaboration/": typeof PageDocsCollaborationIndexRoute; "/docs/collaboration/text/lease": typeof PageDocsCollaborationTextLeaseRoute; "/docs/collaboration/text/": typeof PageDocsCollaborationTextIndexRoute; @@ -454,19 +679,49 @@ export interface FileRoutesByTo { "/docs/tree": typeof PageDocsTreeRoute; "/docs/tutorial": typeof PageDocsTutorialRoute; "/editing/rich-text": typeof PageEditingRichTextRoute; + "/widgets/board": typeof PageWidgetsBoardRoute; + "/widgets/canvas": typeof PageWidgetsCanvasRoute; + "/widgets/document": typeof PageWidgetsDocumentRoute; "/widgets/grid": typeof PageWidgetsGridRoute; "/widgets/listbox": typeof PageWidgetsListboxRoute; "/widgets/toolbar": typeof PageWidgetsToolbarRoute; + "/widgets/tree": typeof PageWidgetsTreeRoute; "/adapters": typeof PageAdaptersIndexRoute; "/connectors": typeof PageConnectorsIndexRoute; "/demo": typeof PageDemoIndexRoute; "/docs": typeof PageDocsIndexRoute; "/widgets": typeof PageWidgetsIndexRoute; "/connectors/zod/validate": typeof PageConnectorsZodValidateRoute; + "/docs/affordance/activate": typeof PageDocsAffordanceActivateRoute; + "/docs/affordance/cancel": typeof PageDocsAffordanceCancelRoute; + "/docs/affordance/caret": typeof PageDocsAffordanceCaretRoute; + "/docs/affordance/context-menu": typeof PageDocsAffordanceContextMenuRoute; + "/docs/affordance/copy-drag": typeof PageDocsAffordanceCopyDragRoute; + "/docs/affordance/delete": typeof PageDocsAffordanceDeleteRoute; + "/docs/affordance/double-click": typeof PageDocsAffordanceDoubleClickRoute; + "/docs/affordance/drag": typeof PageDocsAffordanceDragRoute; + "/docs/affordance/drop": typeof PageDocsAffordanceDropRoute; + "/docs/affordance/focus": typeof PageDocsAffordanceFocusRoute; + "/docs/affordance/fold": typeof PageDocsAffordanceFoldRoute; + "/docs/affordance/forbid": typeof PageDocsAffordanceForbidRoute; + "/docs/affordance/history": typeof PageDocsAffordanceHistoryRoute; + "/docs/affordance/hover": typeof PageDocsAffordanceHoverRoute; + "/docs/affordance/marquee": typeof PageDocsAffordanceMarqueeRoute; + "/docs/affordance/nudge": typeof PageDocsAffordanceNudgeRoute; + "/docs/affordance/pan": typeof PageDocsAffordancePanRoute; + "/docs/affordance/rename": typeof PageDocsAffordanceRenameRoute; + "/docs/affordance/resize": typeof PageDocsAffordanceResizeRoute; + "/docs/affordance/scroll": typeof PageDocsAffordanceScrollRoute; + "/docs/affordance/select": typeof PageDocsAffordanceSelectRoute; + "/docs/affordance/snap": typeof PageDocsAffordanceSnapRoute; + "/docs/affordance/triple-click": typeof PageDocsAffordanceTripleClickRoute; + "/docs/affordance/typeahead": typeof PageDocsAffordanceTypeaheadRoute; + "/docs/affordance/zoom": typeof PageDocsAffordanceZoomRoute; "/docs/collaboration/history": typeof PageDocsCollaborationHistoryRoute; "/docs/collaboration/lifecycle": typeof PageDocsCollaborationLifecycleRoute; "/docs/collaboration/replica": typeof PageDocsCollaborationReplicaRoute; "/connectors/zod": typeof PageConnectorsZodIndexRoute; + "/docs/affordance": typeof PageDocsAffordanceIndexRoute; "/docs/collaboration": typeof PageDocsCollaborationIndexRoute; "/docs/collaboration/text/lease": typeof PageDocsCollaborationTextLeaseRoute; "/docs/collaboration/text": typeof PageDocsCollaborationTextIndexRoute; @@ -513,19 +768,49 @@ export interface FileRoutesById { "/_page/docs/tree": typeof PageDocsTreeRoute; "/_page/docs/tutorial": typeof PageDocsTutorialRoute; "/_page/editing/rich-text": typeof PageEditingRichTextRoute; + "/_page/widgets/board": typeof PageWidgetsBoardRoute; + "/_page/widgets/canvas": typeof PageWidgetsCanvasRoute; + "/_page/widgets/document": typeof PageWidgetsDocumentRoute; "/_page/widgets/grid": typeof PageWidgetsGridRoute; "/_page/widgets/listbox": typeof PageWidgetsListboxRoute; "/_page/widgets/toolbar": typeof PageWidgetsToolbarRoute; + "/_page/widgets/tree": typeof PageWidgetsTreeRoute; "/_page/adapters/": typeof PageAdaptersIndexRoute; "/_page/connectors/": typeof PageConnectorsIndexRoute; "/_page/demo/": typeof PageDemoIndexRoute; "/_page/docs/": typeof PageDocsIndexRoute; "/_page/widgets/": typeof PageWidgetsIndexRoute; "/_page/connectors/zod/validate": typeof PageConnectorsZodValidateRoute; + "/_page/docs/affordance/activate": typeof PageDocsAffordanceActivateRoute; + "/_page/docs/affordance/cancel": typeof PageDocsAffordanceCancelRoute; + "/_page/docs/affordance/caret": typeof PageDocsAffordanceCaretRoute; + "/_page/docs/affordance/context-menu": typeof PageDocsAffordanceContextMenuRoute; + "/_page/docs/affordance/copy-drag": typeof PageDocsAffordanceCopyDragRoute; + "/_page/docs/affordance/delete": typeof PageDocsAffordanceDeleteRoute; + "/_page/docs/affordance/double-click": typeof PageDocsAffordanceDoubleClickRoute; + "/_page/docs/affordance/drag": typeof PageDocsAffordanceDragRoute; + "/_page/docs/affordance/drop": typeof PageDocsAffordanceDropRoute; + "/_page/docs/affordance/focus": typeof PageDocsAffordanceFocusRoute; + "/_page/docs/affordance/fold": typeof PageDocsAffordanceFoldRoute; + "/_page/docs/affordance/forbid": typeof PageDocsAffordanceForbidRoute; + "/_page/docs/affordance/history": typeof PageDocsAffordanceHistoryRoute; + "/_page/docs/affordance/hover": typeof PageDocsAffordanceHoverRoute; + "/_page/docs/affordance/marquee": typeof PageDocsAffordanceMarqueeRoute; + "/_page/docs/affordance/nudge": typeof PageDocsAffordanceNudgeRoute; + "/_page/docs/affordance/pan": typeof PageDocsAffordancePanRoute; + "/_page/docs/affordance/rename": typeof PageDocsAffordanceRenameRoute; + "/_page/docs/affordance/resize": typeof PageDocsAffordanceResizeRoute; + "/_page/docs/affordance/scroll": typeof PageDocsAffordanceScrollRoute; + "/_page/docs/affordance/select": typeof PageDocsAffordanceSelectRoute; + "/_page/docs/affordance/snap": typeof PageDocsAffordanceSnapRoute; + "/_page/docs/affordance/triple-click": typeof PageDocsAffordanceTripleClickRoute; + "/_page/docs/affordance/typeahead": typeof PageDocsAffordanceTypeaheadRoute; + "/_page/docs/affordance/zoom": typeof PageDocsAffordanceZoomRoute; "/_page/docs/collaboration/history": typeof PageDocsCollaborationHistoryRoute; "/_page/docs/collaboration/lifecycle": typeof PageDocsCollaborationLifecycleRoute; "/_page/docs/collaboration/replica": typeof PageDocsCollaborationReplicaRoute; "/_page/connectors/zod/": typeof PageConnectorsZodIndexRoute; + "/_page/docs/affordance/": typeof PageDocsAffordanceIndexRoute; "/_page/docs/collaboration/": typeof PageDocsCollaborationIndexRoute; "/_page/docs/collaboration/text/lease": typeof PageDocsCollaborationTextLeaseRoute; "/_page/docs/collaboration/text/": typeof PageDocsCollaborationTextIndexRoute; @@ -572,19 +857,49 @@ export interface FileRouteTypes { | "/docs/tree" | "/docs/tutorial" | "/editing/rich-text" + | "/widgets/board" + | "/widgets/canvas" + | "/widgets/document" | "/widgets/grid" | "/widgets/listbox" | "/widgets/toolbar" + | "/widgets/tree" | "/adapters/" | "/connectors/" | "/demo/" | "/docs/" | "/widgets/" | "/connectors/zod/validate" + | "/docs/affordance/activate" + | "/docs/affordance/cancel" + | "/docs/affordance/caret" + | "/docs/affordance/context-menu" + | "/docs/affordance/copy-drag" + | "/docs/affordance/delete" + | "/docs/affordance/double-click" + | "/docs/affordance/drag" + | "/docs/affordance/drop" + | "/docs/affordance/focus" + | "/docs/affordance/fold" + | "/docs/affordance/forbid" + | "/docs/affordance/history" + | "/docs/affordance/hover" + | "/docs/affordance/marquee" + | "/docs/affordance/nudge" + | "/docs/affordance/pan" + | "/docs/affordance/rename" + | "/docs/affordance/resize" + | "/docs/affordance/scroll" + | "/docs/affordance/select" + | "/docs/affordance/snap" + | "/docs/affordance/triple-click" + | "/docs/affordance/typeahead" + | "/docs/affordance/zoom" | "/docs/collaboration/history" | "/docs/collaboration/lifecycle" | "/docs/collaboration/replica" | "/connectors/zod/" + | "/docs/affordance/" | "/docs/collaboration/" | "/docs/collaboration/text/lease" | "/docs/collaboration/text/"; @@ -629,19 +944,49 @@ export interface FileRouteTypes { | "/docs/tree" | "/docs/tutorial" | "/editing/rich-text" + | "/widgets/board" + | "/widgets/canvas" + | "/widgets/document" | "/widgets/grid" | "/widgets/listbox" | "/widgets/toolbar" + | "/widgets/tree" | "/adapters" | "/connectors" | "/demo" | "/docs" | "/widgets" | "/connectors/zod/validate" + | "/docs/affordance/activate" + | "/docs/affordance/cancel" + | "/docs/affordance/caret" + | "/docs/affordance/context-menu" + | "/docs/affordance/copy-drag" + | "/docs/affordance/delete" + | "/docs/affordance/double-click" + | "/docs/affordance/drag" + | "/docs/affordance/drop" + | "/docs/affordance/focus" + | "/docs/affordance/fold" + | "/docs/affordance/forbid" + | "/docs/affordance/history" + | "/docs/affordance/hover" + | "/docs/affordance/marquee" + | "/docs/affordance/nudge" + | "/docs/affordance/pan" + | "/docs/affordance/rename" + | "/docs/affordance/resize" + | "/docs/affordance/scroll" + | "/docs/affordance/select" + | "/docs/affordance/snap" + | "/docs/affordance/triple-click" + | "/docs/affordance/typeahead" + | "/docs/affordance/zoom" | "/docs/collaboration/history" | "/docs/collaboration/lifecycle" | "/docs/collaboration/replica" | "/connectors/zod" + | "/docs/affordance" | "/docs/collaboration" | "/docs/collaboration/text/lease" | "/docs/collaboration/text"; @@ -687,19 +1032,49 @@ export interface FileRouteTypes { | "/_page/docs/tree" | "/_page/docs/tutorial" | "/_page/editing/rich-text" + | "/_page/widgets/board" + | "/_page/widgets/canvas" + | "/_page/widgets/document" | "/_page/widgets/grid" | "/_page/widgets/listbox" | "/_page/widgets/toolbar" + | "/_page/widgets/tree" | "/_page/adapters/" | "/_page/connectors/" | "/_page/demo/" | "/_page/docs/" | "/_page/widgets/" | "/_page/connectors/zod/validate" + | "/_page/docs/affordance/activate" + | "/_page/docs/affordance/cancel" + | "/_page/docs/affordance/caret" + | "/_page/docs/affordance/context-menu" + | "/_page/docs/affordance/copy-drag" + | "/_page/docs/affordance/delete" + | "/_page/docs/affordance/double-click" + | "/_page/docs/affordance/drag" + | "/_page/docs/affordance/drop" + | "/_page/docs/affordance/focus" + | "/_page/docs/affordance/fold" + | "/_page/docs/affordance/forbid" + | "/_page/docs/affordance/history" + | "/_page/docs/affordance/hover" + | "/_page/docs/affordance/marquee" + | "/_page/docs/affordance/nudge" + | "/_page/docs/affordance/pan" + | "/_page/docs/affordance/rename" + | "/_page/docs/affordance/resize" + | "/_page/docs/affordance/scroll" + | "/_page/docs/affordance/select" + | "/_page/docs/affordance/snap" + | "/_page/docs/affordance/triple-click" + | "/_page/docs/affordance/typeahead" + | "/_page/docs/affordance/zoom" | "/_page/docs/collaboration/history" | "/_page/docs/collaboration/lifecycle" | "/_page/docs/collaboration/replica" | "/_page/connectors/zod/" + | "/_page/docs/affordance/" | "/_page/docs/collaboration/" | "/_page/docs/collaboration/text/lease" | "/_page/docs/collaboration/text/"; @@ -1027,6 +1402,27 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof PageWidgetsIndexRouteImport; parentRoute: typeof PageRoute; }; + "/_page/widgets/board": { + id: "/_page/widgets/board"; + path: "/widgets/board"; + fullPath: "/widgets/board"; + preLoaderRoute: typeof PageWidgetsBoardRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/widgets/canvas": { + id: "/_page/widgets/canvas"; + path: "/widgets/canvas"; + fullPath: "/widgets/canvas"; + preLoaderRoute: typeof PageWidgetsCanvasRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/widgets/document": { + id: "/_page/widgets/document"; + path: "/widgets/document"; + fullPath: "/widgets/document"; + preLoaderRoute: typeof PageWidgetsDocumentRouteImport; + parentRoute: typeof PageRoute; + }; "/_page/widgets/grid": { id: "/_page/widgets/grid"; path: "/widgets/grid"; @@ -1048,6 +1444,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof PageWidgetsToolbarRouteImport; parentRoute: typeof PageRoute; }; + "/_page/widgets/tree": { + id: "/_page/widgets/tree"; + path: "/widgets/tree"; + fullPath: "/widgets/tree"; + preLoaderRoute: typeof PageWidgetsTreeRouteImport; + parentRoute: typeof PageRoute; + }; "/_page/connectors/zod/": { id: "/_page/connectors/zod/"; path: "/connectors/zod"; @@ -1062,6 +1465,188 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof PageConnectorsZodValidateRouteImport; parentRoute: typeof PageRoute; }; + "/_page/docs/affordance/": { + id: "/_page/docs/affordance/"; + path: "/docs/affordance"; + fullPath: "/docs/affordance/"; + preLoaderRoute: typeof PageDocsAffordanceIndexRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/activate": { + id: "/_page/docs/affordance/activate"; + path: "/docs/affordance/activate"; + fullPath: "/docs/affordance/activate"; + preLoaderRoute: typeof PageDocsAffordanceActivateRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/cancel": { + id: "/_page/docs/affordance/cancel"; + path: "/docs/affordance/cancel"; + fullPath: "/docs/affordance/cancel"; + preLoaderRoute: typeof PageDocsAffordanceCancelRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/caret": { + id: "/_page/docs/affordance/caret"; + path: "/docs/affordance/caret"; + fullPath: "/docs/affordance/caret"; + preLoaderRoute: typeof PageDocsAffordanceCaretRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/context-menu": { + id: "/_page/docs/affordance/context-menu"; + path: "/docs/affordance/context-menu"; + fullPath: "/docs/affordance/context-menu"; + preLoaderRoute: typeof PageDocsAffordanceContextMenuRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/copy-drag": { + id: "/_page/docs/affordance/copy-drag"; + path: "/docs/affordance/copy-drag"; + fullPath: "/docs/affordance/copy-drag"; + preLoaderRoute: typeof PageDocsAffordanceCopyDragRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/delete": { + id: "/_page/docs/affordance/delete"; + path: "/docs/affordance/delete"; + fullPath: "/docs/affordance/delete"; + preLoaderRoute: typeof PageDocsAffordanceDeleteRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/double-click": { + id: "/_page/docs/affordance/double-click"; + path: "/docs/affordance/double-click"; + fullPath: "/docs/affordance/double-click"; + preLoaderRoute: typeof PageDocsAffordanceDoubleClickRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/drag": { + id: "/_page/docs/affordance/drag"; + path: "/docs/affordance/drag"; + fullPath: "/docs/affordance/drag"; + preLoaderRoute: typeof PageDocsAffordanceDragRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/drop": { + id: "/_page/docs/affordance/drop"; + path: "/docs/affordance/drop"; + fullPath: "/docs/affordance/drop"; + preLoaderRoute: typeof PageDocsAffordanceDropRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/focus": { + id: "/_page/docs/affordance/focus"; + path: "/docs/affordance/focus"; + fullPath: "/docs/affordance/focus"; + preLoaderRoute: typeof PageDocsAffordanceFocusRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/fold": { + id: "/_page/docs/affordance/fold"; + path: "/docs/affordance/fold"; + fullPath: "/docs/affordance/fold"; + preLoaderRoute: typeof PageDocsAffordanceFoldRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/forbid": { + id: "/_page/docs/affordance/forbid"; + path: "/docs/affordance/forbid"; + fullPath: "/docs/affordance/forbid"; + preLoaderRoute: typeof PageDocsAffordanceForbidRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/history": { + id: "/_page/docs/affordance/history"; + path: "/docs/affordance/history"; + fullPath: "/docs/affordance/history"; + preLoaderRoute: typeof PageDocsAffordanceHistoryRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/hover": { + id: "/_page/docs/affordance/hover"; + path: "/docs/affordance/hover"; + fullPath: "/docs/affordance/hover"; + preLoaderRoute: typeof PageDocsAffordanceHoverRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/marquee": { + id: "/_page/docs/affordance/marquee"; + path: "/docs/affordance/marquee"; + fullPath: "/docs/affordance/marquee"; + preLoaderRoute: typeof PageDocsAffordanceMarqueeRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/nudge": { + id: "/_page/docs/affordance/nudge"; + path: "/docs/affordance/nudge"; + fullPath: "/docs/affordance/nudge"; + preLoaderRoute: typeof PageDocsAffordanceNudgeRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/pan": { + id: "/_page/docs/affordance/pan"; + path: "/docs/affordance/pan"; + fullPath: "/docs/affordance/pan"; + preLoaderRoute: typeof PageDocsAffordancePanRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/rename": { + id: "/_page/docs/affordance/rename"; + path: "/docs/affordance/rename"; + fullPath: "/docs/affordance/rename"; + preLoaderRoute: typeof PageDocsAffordanceRenameRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/resize": { + id: "/_page/docs/affordance/resize"; + path: "/docs/affordance/resize"; + fullPath: "/docs/affordance/resize"; + preLoaderRoute: typeof PageDocsAffordanceResizeRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/scroll": { + id: "/_page/docs/affordance/scroll"; + path: "/docs/affordance/scroll"; + fullPath: "/docs/affordance/scroll"; + preLoaderRoute: typeof PageDocsAffordanceScrollRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/select": { + id: "/_page/docs/affordance/select"; + path: "/docs/affordance/select"; + fullPath: "/docs/affordance/select"; + preLoaderRoute: typeof PageDocsAffordanceSelectRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/snap": { + id: "/_page/docs/affordance/snap"; + path: "/docs/affordance/snap"; + fullPath: "/docs/affordance/snap"; + preLoaderRoute: typeof PageDocsAffordanceSnapRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/triple-click": { + id: "/_page/docs/affordance/triple-click"; + path: "/docs/affordance/triple-click"; + fullPath: "/docs/affordance/triple-click"; + preLoaderRoute: typeof PageDocsAffordanceTripleClickRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/typeahead": { + id: "/_page/docs/affordance/typeahead"; + path: "/docs/affordance/typeahead"; + fullPath: "/docs/affordance/typeahead"; + preLoaderRoute: typeof PageDocsAffordanceTypeaheadRouteImport; + parentRoute: typeof PageRoute; + }; + "/_page/docs/affordance/zoom": { + id: "/_page/docs/affordance/zoom"; + path: "/docs/affordance/zoom"; + fullPath: "/docs/affordance/zoom"; + preLoaderRoute: typeof PageDocsAffordanceZoomRouteImport; + parentRoute: typeof PageRoute; + }; "/_page/docs/collaboration/": { id: "/_page/docs/collaboration/"; path: "/docs/collaboration"; @@ -1146,19 +1731,49 @@ interface PageRouteChildren { PageDocsTreeRoute: typeof PageDocsTreeRoute; PageDocsTutorialRoute: typeof PageDocsTutorialRoute; PageEditingRichTextRoute: typeof PageEditingRichTextRoute; + PageWidgetsBoardRoute: typeof PageWidgetsBoardRoute; + PageWidgetsCanvasRoute: typeof PageWidgetsCanvasRoute; + PageWidgetsDocumentRoute: typeof PageWidgetsDocumentRoute; PageWidgetsGridRoute: typeof PageWidgetsGridRoute; PageWidgetsListboxRoute: typeof PageWidgetsListboxRoute; PageWidgetsToolbarRoute: typeof PageWidgetsToolbarRoute; + PageWidgetsTreeRoute: typeof PageWidgetsTreeRoute; PageAdaptersIndexRoute: typeof PageAdaptersIndexRoute; PageConnectorsIndexRoute: typeof PageConnectorsIndexRoute; PageDemoIndexRoute: typeof PageDemoIndexRoute; PageDocsIndexRoute: typeof PageDocsIndexRoute; PageWidgetsIndexRoute: typeof PageWidgetsIndexRoute; PageConnectorsZodValidateRoute: typeof PageConnectorsZodValidateRoute; + PageDocsAffordanceActivateRoute: typeof PageDocsAffordanceActivateRoute; + PageDocsAffordanceCancelRoute: typeof PageDocsAffordanceCancelRoute; + PageDocsAffordanceCaretRoute: typeof PageDocsAffordanceCaretRoute; + PageDocsAffordanceContextMenuRoute: typeof PageDocsAffordanceContextMenuRoute; + PageDocsAffordanceCopyDragRoute: typeof PageDocsAffordanceCopyDragRoute; + PageDocsAffordanceDeleteRoute: typeof PageDocsAffordanceDeleteRoute; + PageDocsAffordanceDoubleClickRoute: typeof PageDocsAffordanceDoubleClickRoute; + PageDocsAffordanceDragRoute: typeof PageDocsAffordanceDragRoute; + PageDocsAffordanceDropRoute: typeof PageDocsAffordanceDropRoute; + PageDocsAffordanceFocusRoute: typeof PageDocsAffordanceFocusRoute; + PageDocsAffordanceFoldRoute: typeof PageDocsAffordanceFoldRoute; + PageDocsAffordanceForbidRoute: typeof PageDocsAffordanceForbidRoute; + PageDocsAffordanceHistoryRoute: typeof PageDocsAffordanceHistoryRoute; + PageDocsAffordanceHoverRoute: typeof PageDocsAffordanceHoverRoute; + PageDocsAffordanceMarqueeRoute: typeof PageDocsAffordanceMarqueeRoute; + PageDocsAffordanceNudgeRoute: typeof PageDocsAffordanceNudgeRoute; + PageDocsAffordancePanRoute: typeof PageDocsAffordancePanRoute; + PageDocsAffordanceRenameRoute: typeof PageDocsAffordanceRenameRoute; + PageDocsAffordanceResizeRoute: typeof PageDocsAffordanceResizeRoute; + PageDocsAffordanceScrollRoute: typeof PageDocsAffordanceScrollRoute; + PageDocsAffordanceSelectRoute: typeof PageDocsAffordanceSelectRoute; + PageDocsAffordanceSnapRoute: typeof PageDocsAffordanceSnapRoute; + PageDocsAffordanceTripleClickRoute: typeof PageDocsAffordanceTripleClickRoute; + PageDocsAffordanceTypeaheadRoute: typeof PageDocsAffordanceTypeaheadRoute; + PageDocsAffordanceZoomRoute: typeof PageDocsAffordanceZoomRoute; PageDocsCollaborationHistoryRoute: typeof PageDocsCollaborationHistoryRoute; PageDocsCollaborationLifecycleRoute: typeof PageDocsCollaborationLifecycleRoute; PageDocsCollaborationReplicaRoute: typeof PageDocsCollaborationReplicaRoute; PageConnectorsZodIndexRoute: typeof PageConnectorsZodIndexRoute; + PageDocsAffordanceIndexRoute: typeof PageDocsAffordanceIndexRoute; PageDocsCollaborationIndexRoute: typeof PageDocsCollaborationIndexRoute; PageDocsCollaborationTextLeaseRoute: typeof PageDocsCollaborationTextLeaseRoute; PageDocsCollaborationTextIndexRoute: typeof PageDocsCollaborationTextIndexRoute; @@ -1203,19 +1818,49 @@ const PageRouteChildren: PageRouteChildren = { PageDocsTreeRoute: PageDocsTreeRoute, PageDocsTutorialRoute: PageDocsTutorialRoute, PageEditingRichTextRoute: PageEditingRichTextRoute, + PageWidgetsBoardRoute: PageWidgetsBoardRoute, + PageWidgetsCanvasRoute: PageWidgetsCanvasRoute, + PageWidgetsDocumentRoute: PageWidgetsDocumentRoute, PageWidgetsGridRoute: PageWidgetsGridRoute, PageWidgetsListboxRoute: PageWidgetsListboxRoute, PageWidgetsToolbarRoute: PageWidgetsToolbarRoute, + PageWidgetsTreeRoute: PageWidgetsTreeRoute, PageAdaptersIndexRoute: PageAdaptersIndexRoute, PageConnectorsIndexRoute: PageConnectorsIndexRoute, PageDemoIndexRoute: PageDemoIndexRoute, PageDocsIndexRoute: PageDocsIndexRoute, PageWidgetsIndexRoute: PageWidgetsIndexRoute, PageConnectorsZodValidateRoute: PageConnectorsZodValidateRoute, + PageDocsAffordanceActivateRoute: PageDocsAffordanceActivateRoute, + PageDocsAffordanceCancelRoute: PageDocsAffordanceCancelRoute, + PageDocsAffordanceCaretRoute: PageDocsAffordanceCaretRoute, + PageDocsAffordanceContextMenuRoute: PageDocsAffordanceContextMenuRoute, + PageDocsAffordanceCopyDragRoute: PageDocsAffordanceCopyDragRoute, + PageDocsAffordanceDeleteRoute: PageDocsAffordanceDeleteRoute, + PageDocsAffordanceDoubleClickRoute: PageDocsAffordanceDoubleClickRoute, + PageDocsAffordanceDragRoute: PageDocsAffordanceDragRoute, + PageDocsAffordanceDropRoute: PageDocsAffordanceDropRoute, + PageDocsAffordanceFocusRoute: PageDocsAffordanceFocusRoute, + PageDocsAffordanceFoldRoute: PageDocsAffordanceFoldRoute, + PageDocsAffordanceForbidRoute: PageDocsAffordanceForbidRoute, + PageDocsAffordanceHistoryRoute: PageDocsAffordanceHistoryRoute, + PageDocsAffordanceHoverRoute: PageDocsAffordanceHoverRoute, + PageDocsAffordanceMarqueeRoute: PageDocsAffordanceMarqueeRoute, + PageDocsAffordanceNudgeRoute: PageDocsAffordanceNudgeRoute, + PageDocsAffordancePanRoute: PageDocsAffordancePanRoute, + PageDocsAffordanceRenameRoute: PageDocsAffordanceRenameRoute, + PageDocsAffordanceResizeRoute: PageDocsAffordanceResizeRoute, + PageDocsAffordanceScrollRoute: PageDocsAffordanceScrollRoute, + PageDocsAffordanceSelectRoute: PageDocsAffordanceSelectRoute, + PageDocsAffordanceSnapRoute: PageDocsAffordanceSnapRoute, + PageDocsAffordanceTripleClickRoute: PageDocsAffordanceTripleClickRoute, + PageDocsAffordanceTypeaheadRoute: PageDocsAffordanceTypeaheadRoute, + PageDocsAffordanceZoomRoute: PageDocsAffordanceZoomRoute, PageDocsCollaborationHistoryRoute: PageDocsCollaborationHistoryRoute, PageDocsCollaborationLifecycleRoute: PageDocsCollaborationLifecycleRoute, PageDocsCollaborationReplicaRoute: PageDocsCollaborationReplicaRoute, PageConnectorsZodIndexRoute: PageConnectorsZodIndexRoute, + PageDocsAffordanceIndexRoute: PageDocsAffordanceIndexRoute, PageDocsCollaborationIndexRoute: PageDocsCollaborationIndexRoute, PageDocsCollaborationTextLeaseRoute: PageDocsCollaborationTextLeaseRoute, PageDocsCollaborationTextIndexRoute: PageDocsCollaborationTextIndexRoute, diff --git a/site/src/app/routes/_page/docs/affordance/activate.tsx b/site/src/app/routes/_page/docs/affordance/activate.tsx new file mode 100644 index 00000000..a2a7b192 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/activate.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/activate")({ + component: function AffordanceActivateDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/cancel.tsx b/site/src/app/routes/_page/docs/affordance/cancel.tsx new file mode 100644 index 00000000..f8571939 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/cancel.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/cancel")({ + component: function AffordanceCancelDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/caret.tsx b/site/src/app/routes/_page/docs/affordance/caret.tsx new file mode 100644 index 00000000..89295592 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/caret.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/caret")({ + component: function AffordanceCaretDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/context-menu.tsx b/site/src/app/routes/_page/docs/affordance/context-menu.tsx new file mode 100644 index 00000000..1eec96ee --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/context-menu.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/context-menu")({ + component: function AffordanceContextMenuDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/copy-drag.tsx b/site/src/app/routes/_page/docs/affordance/copy-drag.tsx new file mode 100644 index 00000000..f04cf1d5 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/copy-drag.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/copy-drag")({ + component: function AffordanceCopyDragDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/delete.tsx b/site/src/app/routes/_page/docs/affordance/delete.tsx new file mode 100644 index 00000000..9432c644 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/delete.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/delete")({ + component: function AffordanceDeleteDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/double-click.tsx b/site/src/app/routes/_page/docs/affordance/double-click.tsx new file mode 100644 index 00000000..043534e7 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/double-click.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/double-click")({ + component: function AffordanceDoubleClickDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/drag.tsx b/site/src/app/routes/_page/docs/affordance/drag.tsx new file mode 100644 index 00000000..3b6a7f44 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/drag.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/drag")({ + component: function AffordanceDragDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/drop.tsx b/site/src/app/routes/_page/docs/affordance/drop.tsx new file mode 100644 index 00000000..c343c8ca --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/drop.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/drop")({ + component: function AffordanceDropDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/focus.tsx b/site/src/app/routes/_page/docs/affordance/focus.tsx new file mode 100644 index 00000000..d3b6743c --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/focus.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/focus")({ + component: function AffordanceFocusDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/fold.tsx b/site/src/app/routes/_page/docs/affordance/fold.tsx new file mode 100644 index 00000000..e70825b5 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/fold.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/fold")({ + component: function AffordanceFoldDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/forbid.tsx b/site/src/app/routes/_page/docs/affordance/forbid.tsx new file mode 100644 index 00000000..2f64a387 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/forbid.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/forbid")({ + component: function AffordanceForbidDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/history.tsx b/site/src/app/routes/_page/docs/affordance/history.tsx new file mode 100644 index 00000000..0775731a --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/history.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/history")({ + component: function AffordanceHistoryDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/hover.tsx b/site/src/app/routes/_page/docs/affordance/hover.tsx new file mode 100644 index 00000000..92e8f569 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/hover.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/hover")({ + component: function AffordanceHoverDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/index.tsx b/site/src/app/routes/_page/docs/affordance/index.tsx new file mode 100644 index 00000000..d10ce56a --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/index.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { AffordanceDocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/")({ + component: AffordanceDocsRoute, +}); diff --git a/site/src/app/routes/_page/docs/affordance/marquee.tsx b/site/src/app/routes/_page/docs/affordance/marquee.tsx new file mode 100644 index 00000000..7cff632a --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/marquee.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/marquee")({ + component: function AffordanceMarqueeDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/nudge.tsx b/site/src/app/routes/_page/docs/affordance/nudge.tsx new file mode 100644 index 00000000..055bc5bc --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/nudge.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/nudge")({ + component: function AffordanceNudgeDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/pan.tsx b/site/src/app/routes/_page/docs/affordance/pan.tsx new file mode 100644 index 00000000..bb70d1fe --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/pan.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/pan")({ + component: function AffordancePanDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/rename.tsx b/site/src/app/routes/_page/docs/affordance/rename.tsx new file mode 100644 index 00000000..f1e02ba9 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/rename.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/rename")({ + component: function AffordanceRenameDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/resize.tsx b/site/src/app/routes/_page/docs/affordance/resize.tsx new file mode 100644 index 00000000..fc0795f6 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/resize.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/resize")({ + component: function AffordanceResizeDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/scroll.tsx b/site/src/app/routes/_page/docs/affordance/scroll.tsx new file mode 100644 index 00000000..6cf575e9 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/scroll.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/scroll")({ + component: function AffordanceScrollDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/select.tsx b/site/src/app/routes/_page/docs/affordance/select.tsx new file mode 100644 index 00000000..38cb567c --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/select.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/select")({ + component: function AffordanceSelectDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/snap.tsx b/site/src/app/routes/_page/docs/affordance/snap.tsx new file mode 100644 index 00000000..a1da11a7 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/snap.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/snap")({ + component: function AffordanceSnapDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/triple-click.tsx b/site/src/app/routes/_page/docs/affordance/triple-click.tsx new file mode 100644 index 00000000..cdc0024f --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/triple-click.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/triple-click")({ + component: function AffordanceTripleClickDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/typeahead.tsx b/site/src/app/routes/_page/docs/affordance/typeahead.tsx new file mode 100644 index 00000000..3687a2ae --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/typeahead.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/typeahead")({ + component: function AffordanceTypeaheadDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/docs/affordance/zoom.tsx b/site/src/app/routes/_page/docs/affordance/zoom.tsx new file mode 100644 index 00000000..150d6c24 --- /dev/null +++ b/site/src/app/routes/_page/docs/affordance/zoom.tsx @@ -0,0 +1,8 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocsRoute } from "../../../../../routes/docs/DocsRoute"; + +export const Route = createFileRoute("/_page/docs/affordance/zoom")({ + component: function AffordanceZoomDocsRoute() { + return ; + }, +}); diff --git a/site/src/app/routes/_page/widgets/board.tsx b/site/src/app/routes/_page/widgets/board.tsx new file mode 100644 index 00000000..c2fdcdfc --- /dev/null +++ b/site/src/app/routes/_page/widgets/board.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { BoardWidgetRoute } from "../../../../routes/widgets/BoardWidgetRoute"; + +export const Route = createFileRoute("/_page/widgets/board")({ + component: BoardWidgetRoute, +}); diff --git a/site/src/app/routes/_page/widgets/canvas.tsx b/site/src/app/routes/_page/widgets/canvas.tsx new file mode 100644 index 00000000..75a549b8 --- /dev/null +++ b/site/src/app/routes/_page/widgets/canvas.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { CanvasWidgetRoute } from "../../../../routes/widgets/CanvasWidgetRoute"; + +export const Route = createFileRoute("/_page/widgets/canvas")({ + component: CanvasWidgetRoute, +}); diff --git a/site/src/app/routes/_page/widgets/document.tsx b/site/src/app/routes/_page/widgets/document.tsx new file mode 100644 index 00000000..85aab7ab --- /dev/null +++ b/site/src/app/routes/_page/widgets/document.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { DocumentWidgetRoute } from "../../../../routes/widgets/DocumentWidgetRoute"; + +export const Route = createFileRoute("/_page/widgets/document")({ + component: DocumentWidgetRoute, +}); diff --git a/site/src/app/routes/_page/widgets/index.tsx b/site/src/app/routes/_page/widgets/index.tsx index f460b6a2..91d28541 100644 --- a/site/src/app/routes/_page/widgets/index.tsx +++ b/site/src/app/routes/_page/widgets/index.tsx @@ -1,6 +1,6 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { WidgetCatalogRoute } from "../../../../routes/widgets/WidgetCatalogRoute"; +import { Navigate, createFileRoute } from "@tanstack/react-router"; +import { legacyPageRedirects } from "../../../page-descriptors"; export const Route = createFileRoute("/_page/widgets/")({ - component: WidgetCatalogRoute, + component: () => , }); diff --git a/site/src/app/routes/_page/widgets/tree.tsx b/site/src/app/routes/_page/widgets/tree.tsx new file mode 100644 index 00000000..b64dbf01 --- /dev/null +++ b/site/src/app/routes/_page/widgets/tree.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { TreeWidgetRoute } from "../../../../routes/widgets/TreeWidgetRoute"; + +export const Route = createFileRoute("/_page/widgets/tree")({ + component: TreeWidgetRoute, +}); diff --git a/site/src/app/site-layers.ts b/site/src/app/site-layers.ts index b247a495..f26f7efd 100644 --- a/site/src/app/site-layers.ts +++ b/site/src/app/site-layers.ts @@ -11,10 +11,10 @@ export const siteLayers: ReadonlyArray = [ { group: "JSON Document", path: "/docs", label: "JSON Document", blurb: "Values and changes" }, { group: "Collaboration", path: "/docs/collaboration", label: "Collaboration", blurb: "The same contract, other implementation" }, { group: "Editing", path: "/docs/intent-guide", label: "Editing", blurb: "Selection and work" }, - { group: "Hands", path: "/editors", label: "Hands", blurb: "Genre hands" }, { group: "Adapter", path: "/adapters", label: "Adapter", blurb: "Platform contracts" }, { group: "Connector", path: "/connectors", label: "Connector", blurb: "Library ecosystems" }, - { group: "제품 화면", path: "/widgets", label: "제품 화면", blurb: "Toolbar, Listbox, and Grid" }, + { group: "Affordance", path: "/docs/affordance", label: "Affordance", blurb: "Keyboard, mouse, and cursor" }, + { group: "Hands", path: "/editors", label: "Hands", blurb: "Genre hands" }, ]; export const siteLayerGroups: ReadonlyArray = siteLayers.map((layer) => layer.group); diff --git a/site/src/routes/adapters/clipboard/ClipboardAdapterLab.tsx b/site/src/routes/adapters/clipboard/ClipboardAdapterLab.tsx index d97745d4..45b9992e 100644 --- a/site/src/routes/adapters/clipboard/ClipboardAdapterLab.tsx +++ b/site/src/routes/adapters/clipboard/ClipboardAdapterLab.tsx @@ -13,6 +13,7 @@ import { import { Inspector } from "../../../shared/ui/inspector"; import { SelectableItem } from "../../../shared/ui/interactive"; import { classes, ui } from "../../../shared/ui/styles"; +import { optionProps } from "../../../shared/widget-binding"; const initialDocument: BlockDocument = { blocks: [ @@ -82,11 +83,9 @@ export function ClipboardAdapterLab() {