From dd8a6961a778d15f56c852e8f5d60c5f419341bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Thu, 20 Aug 2026 21:18:01 +0900 Subject: [PATCH 01/14] =?UTF-8?q?=EC=96=B4=ED=8F=AC=EB=8D=98=EC=8A=A4:=20?= =?UTF-8?q?=EC=A0=9C=ED=92=88=20=EC=B5=9C=EC=A0=84=EC=84=A0=EC=9D=84=20API?= =?UTF-8?q?=EC=99=80=20=EC=82=AC=EC=9A=A9=EB=B2=95=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=97=B0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 키보드·마우스 손의 정본을 패키지로 두고, 사이트 입구를 고르기·접기·드래그·되돌리기 사용법으로 바꾼다. 위젯 화면은 증명으로만 남긴다. --- docs/README.md | 13 +-- docs/changelog.md | 3 + docs/evaluate.mjs | 11 ++ docs/public/affordance-drag.md | 24 ++++ docs/public/affordance-fold.md | 25 ++++ docs/public/affordance-history.md | 16 +++ docs/public/affordance-select.md | 30 +++++ docs/public/affordance.md | 28 +++++ docs/public/concepts.md | 7 +- docs/public/overview.md | 10 +- package-lock.json | 20 ++++ package.json | 1 + packages/json-document-affordance/LICENSE | 21 ++++ packages/json-document-affordance/README.md | 37 ++++++ .../json-document-affordance/package.json | 44 +++++++ packages/json-document-affordance/src/drag.ts | 20 ++++ packages/json-document-affordance/src/fold.ts | 24 ++++ .../json-document-affordance/src/history.ts | 21 ++++ .../json-document-affordance/src/index.ts | 8 ++ .../json-document-affordance/src/select.ts | 19 ++++ .../tests/affordance.test.ts | 76 +++++++++++++ .../json-document-affordance/tsconfig.json | 10 ++ .../tsconfig.test.json | 10 ++ .../json-document-affordance/vitest.config.ts | 12 ++ site/config/json-document-source-aliases.ts | 4 + site/package.json | 1 + site/scripts/route-checks.mjs | 1 + site/site-routes.json | 77 +++++++++++-- site/src/app/legacy-page-redirects.ts | 4 + site/src/app/page-descriptors.ts | 3 +- site/src/app/routeTree.gen.ts | 107 ++++++++++++++++++ .../app/routes/_page/docs/affordance/drag.tsx | 8 ++ .../app/routes/_page/docs/affordance/fold.tsx | 8 ++ .../routes/_page/docs/affordance/history.tsx | 8 ++ .../routes/_page/docs/affordance/index.tsx | 6 + .../routes/_page/docs/affordance/select.tsx | 8 ++ site/src/app/routes/_page/widgets/index.tsx | 6 +- site/src/app/site-layers.ts | 2 +- site/src/routes/docs/DocsRoute.tsx | 11 +- site/src/routes/docs/doc-pages.ts | 10 ++ site/src/routes/home/HomeRoute.tsx | 4 +- site/src/routes/widgets/CanvasWidgetRoute.tsx | 94 +++++++++++---- .../src/routes/widgets/ListboxWidgetRoute.tsx | 12 +- .../src/routes/widgets/ToolbarWidgetRoute.tsx | 11 +- site/src/routes/widgets/TreeWidgetRoute.tsx | 34 +++++- .../src/routes/widgets/WidgetCatalogRoute.tsx | 58 ---------- site/src/shared/widget-binding/history.ts | 29 ++--- site/tests/browser/site-shell.spec.ts | 21 ++-- site/tests/browser/widgets.spec.ts | 18 ++- site/tests/unit/app-shell.test.tsx | 8 +- site/tests/unit/breadcrumb.test.ts | 15 +-- site/tsconfig.json | 1 + standards/repository-implementation-shape.md | 3 +- tsconfig.build.json | 1 + 54 files changed, 885 insertions(+), 178 deletions(-) create mode 100644 docs/public/affordance-drag.md create mode 100644 docs/public/affordance-fold.md create mode 100644 docs/public/affordance-history.md create mode 100644 docs/public/affordance-select.md create mode 100644 docs/public/affordance.md create mode 100644 packages/json-document-affordance/LICENSE create mode 100644 packages/json-document-affordance/README.md create mode 100644 packages/json-document-affordance/package.json create mode 100644 packages/json-document-affordance/src/drag.ts create mode 100644 packages/json-document-affordance/src/fold.ts create mode 100644 packages/json-document-affordance/src/history.ts create mode 100644 packages/json-document-affordance/src/index.ts create mode 100644 packages/json-document-affordance/src/select.ts create mode 100644 packages/json-document-affordance/tests/affordance.test.ts create mode 100644 packages/json-document-affordance/tsconfig.json create mode 100644 packages/json-document-affordance/tsconfig.test.json create mode 100644 packages/json-document-affordance/vitest.config.ts create mode 100644 site/src/app/routes/_page/docs/affordance/drag.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/fold.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/history.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/index.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/select.tsx delete mode 100644 site/src/routes/widgets/WidgetCatalogRoute.tsx diff --git a/docs/README.md b/docs/README.md index 231c9e69..ddc89762 100644 --- a/docs/README.md +++ b/docs/README.md @@ -76,14 +76,11 @@ Connector |-- Zod `-- TanStack Table -제품 화면 -|-- Toolbar -|-- Listbox -|-- Grid -|-- Document -|-- Canvas -|-- Tree -`-- Board +어포던스 +|-- 고르기 +|-- 접기 +|-- 드래그 +`-- 되돌리기 ``` ## 규범 우선순위 diff --git a/docs/changelog.md b/docs/changelog.md index 68022853..2bd1df53 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,9 @@ source and release history remains available from Git commits and version tags. ## Next +- Opened Affordance as the product-facing keyboard and mouse contract, with + usage and API for select, fold, drag, and undo/redo. Live widget screens + stay as proofs, not the catalog entrance. - Opened Document, Canvas, Tree, and Board on 제품 화면, and made Toolbar, Listbox, Grid, and those widgets share prop bindings that live demos consume without forking. Database stays on Grid. diff --git a/docs/evaluate.mjs b/docs/evaluate.mjs index b3b015a1..c7c76440 100644 --- a/docs/evaluate.mjs +++ b/docs/evaluate.mjs @@ -51,6 +51,11 @@ 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"), connectors: read("docs/public/connectors.md"), reactEditing: read("docs/public/react-editing.md"), collaboration: read("docs/public/collaboration.md"), @@ -95,6 +100,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 +111,11 @@ const activeCompanionPackages = new Set([ if (JSON.stringify(fileNames("docs/public")) !== JSON.stringify([ "adapters.md", + "affordance-drag.md", + "affordance-fold.md", + "affordance-history.md", + "affordance-select.md", + "affordance.md", "api.md", "clipboard.md", "collaboration-history.md", diff --git a/docs/public/affordance-drag.md b/docs/public/affordance-drag.md new file mode 100644 index 00000000..d35bd04f --- /dev/null +++ b/docs/public/affordance-drag.md @@ -0,0 +1,24 @@ +# 드래그 + +드래그는 고른 대상을 포인터로 옮기는 손입니다. 누른 점에서 현재 점까지 +이동량이 생기고, 이동량이 있을 때만 옮기기를 확정합니다. 기하와 히트 +테스트는 호스트가 계산하고, editor에는 대상 ID와 이동량만 넘깁니다. + +```ts +import { + dragOffset, + dragShouldCommit, +} from "@interactive-os/json-document-affordance"; + +const offset = dragOffset({ x: 24, y: 40 }, { x: 80, y: 36 }); +if (dragShouldCommit(offset)) { + editor.dispatch({ + type: "object.translate", + objectIds: selectedIds, + dx: offset.dx, + dy: offset.dy, + }); +} +``` + +모양과 그림은 제품이 정합니다. 고른 것을 잡고 옮기는 문법은 닫혀 있습니다. diff --git a/docs/public/affordance-fold.md b/docs/public/affordance-fold.md new file mode 100644 index 00000000..e01c925a --- /dev/null +++ b/docs/public/affordance-fold.md @@ -0,0 +1,25 @@ +# 접기 + +접기는 나무에서 가지를 접고 펼치는 손입니다. 오른쪽은 닫힌 부모를 펼치고, +왼쪽은 열린 부모를 접습니다. 펼칠 가지가 없으면 그 방향은 이웃 이동으로 +남습니다. 위아래 화살표는 보이는 줄을 옮깁니다. + +```ts +import { treeAffordance } from "@interactive-os/json-document-affordance"; + +treeAffordance( + { type: "move", direction: "right" }, + { expanded: false, hasChildren: true }, +); +// { type: "expand" } + +treeAffordance( + { type: "move", direction: "left" }, + { expanded: true, hasChildren: true }, +); +// { type: "collapse" } +``` + +접힘 상태는 호스트가 가진 화면 상태입니다. editor는 보이는 줄 Topology만 +받습니다. 호스트는 `expand` / `collapse`를 접힘 집합에 적용하고, `move`만 +Topology 이웃으로 보냅니다. diff --git a/docs/public/affordance-history.md b/docs/public/affordance-history.md new file mode 100644 index 00000000..ba453b60 --- /dev/null +++ b/docs/public/affordance-history.md @@ -0,0 +1,16 @@ +# 되돌리기 + +되돌리기는 값과 선택을 함께 되돌리는 손입니다. Mod+Z는 undo, Mod+Shift+Z는 +redo입니다. 버튼은 `canUndo` / `canRedo`를 읽어 꺼집니다. + +```ts +import { historyAffordance } from "@interactive-os/json-document-affordance"; + +const commands = historyAffordance({ canUndo: true, canRedo: false }); +commands.undo.disabled; // false +commands.redo.disabled; // true +``` + +`resolveAffordanceKey`가 `undo` / `redo` command를 내면 호스트는 +`editor.undo()` / `editor.redo()`만 호출합니다. History 기록 자체는 +Editing이 가지고, 어포던스는 그 손을 닫습니다. diff --git a/docs/public/affordance-select.md b/docs/public/affordance-select.md new file mode 100644 index 00000000..8e47b20f --- /dev/null +++ b/docs/public/affordance-select.md @@ -0,0 +1,30 @@ +# 고르기 + +고르기는 대상을 집는 손입니다. 클릭은 그 대상으로 바꾸고, Shift는 +범위를 늘리며, Mod는 토글합니다. 화살표는 이웃으로 옮기고, Shift+화살표는 +범위를 늘립니다. + +```ts +import { + pointerSelect, + resolveAffordanceKey, +} from "@interactive-os/json-document-affordance"; + +pointerSelect({ shiftKey: true, metaKey: false, ctrlKey: false }); +// "extend" + +const command = resolveAffordanceKey({ + key: "ArrowDown", + shiftKey: true, + metaKey: false, + ctrlKey: false, +}); +// { type: "move", direction: "down", operation: "extend" } +``` + +호스트는 보이는 키와 장르 Intent만 넘깁니다. keymap을 덮어쓰지 않습니다. +React에서 범위와 커서를 그리려면 `useEditing`의 `onSelect`에 +`pointerSelect` 결과를 연결합니다. + +Keyboard Adapter는 chord를 command로 바꿉니다. 고르기는 그 command가 +replace인지 extend인지를 닫습니다. diff --git a/docs/public/affordance.md b/docs/public/affordance.md new file mode 100644 index 00000000..abebd43f --- /dev/null +++ b/docs/public/affordance.md @@ -0,0 +1,28 @@ +# 어포던스 + +제품의 화면은 호스트가 그립니다. json-document가 최전선에서 주는 것은 +리스트박스나 나무가 아니라, 30년 동안 같은 손으로 학습된 키보드와 +마우스 행동입니다. + +`@interactive-os/json-document-affordance`가 그 계약을 닫습니다. Shift는 +범위를 늘리고, Mod는 토글하고, 나무는 왼쪽이 접힘·오른쪽이 펼침이며, +Delete는 고른 것을 지우고, Mod+Z는 되돌립니다. 단축키를 제품마다 바꾸지 +않습니다. + +```sh +npm i @interactive-os/json-document-affordance +``` + +Editing은 선택과 작업을 기억합니다. Adapter는 키 chord를 command로 +번역합니다. Connector는 구독과 질의를 붙입니다. 어포던스는 그 command가 +무슨 손인지를 정합니다. 호스트는 마크업과 장르 Intent만 가집니다. + +| 어포던스 | API | 손 | +| --- | --- | --- | +| [고르기](affordance-select.md) | `pointerSelect`, `resolveAffordanceKey` | 클릭, Shift 범위, Mod 토글, 화살표 | +| [접기](affordance-fold.md) | `treeAffordance` | 나무 왼쪽 접힘, 오른쪽 펼침 | +| [드래그](affordance-drag.md) | `dragOffset`, `dragShouldCommit` | 고른 대상을 포인터로 옮김 | +| [되돌리기](affordance-history.md) | `historyAffordance` | Mod+Z, Mod+Shift+Z | + +라이브 화면은 이 손을 호스트 그림에 붙인 증명입니다. 위젯을 가져가는 +입구가 아닙니다. diff --git a/docs/public/concepts.md b/docs/public/concepts.md index c48e5079..241e0935 100644 --- a/docs/public/concepts.md +++ b/docs/public/concepts.md @@ -7,11 +7,11 @@ Quickstart에서는 JSON을 읽고, 변경을 적용하고, 결과를 구독했 json-document를 제품에 연결하는 흐름은 이렇게 이어집니다. 먼저 JSON Document가 값을 다루고, Editing이 사용자의 편집 상태를 더합니다. Adapter는 브라우저 플랫폼 계약을 붙이고, Connector는 라이브러리 생태계를 -연결합니다. +연결합니다. 어포던스는 키보드와 마우스의 관례적 손을 최전선에서 닫습니다. ```txt -JSON Document → Editing → Adapter → Connector → 제품 화면 -값과 변경 선택과 작업 플랫폼 계약 라이브러리 렌더링과 입력 +JSON Document → Editing → Adapter → Connector → 어포던스 +값과 변경 선택과 작업 플랫폼 계약 라이브러리 키보드와 마우스 ``` ## JSON Document가 값을 다룬다 @@ -84,6 +84,7 @@ React의 구독 방식으로 전달합니다. TanStack Table Connector는 화면 - [Adapters](adapters.md): 키보드, clipboard, contenteditable 플랫폼 변환 - [Connectors](connectors.md): React, Zod, TanStack Table 같은 라이브러리 연결 - [React editing](react-editing.md): 선택 범위와 커서를 React 질의로 그리기 +- [어포던스](affordance.md): 키보드와 마우스의 관례적 손 같은 공책을 여러 참여자가 쓰려면 [Collaboration](collaboration.md)으로 갑니다. 장르의 손은 [Hands](hands.md)에서 고릅니다. diff --git a/docs/public/overview.md b/docs/public/overview.md index 304955d6..81db7f27 100644 --- a/docs/public/overview.md +++ b/docs/public/overview.md @@ -77,7 +77,7 @@ Hands는 그 전형적 손의 최소 완성본입니다. 한 줄 목록을 집 Object, Sheet, Tree, Kanban, Database가 그 목록입니다. 고르려면 [Hands](hands.md)로 갑니다. -## 플랫폼, 라이브러리, 제품 화면 +## 플랫폼, 라이브러리, 어포던스 브라우저에서 쓰려면 키보드와 clipboard, contenteditable 같은 플랫폼 계약을 같은 공개 API에 맞춰 번역해야 합니다. Adapter가 그 일을 합니다. @@ -88,19 +88,19 @@ React로 그리거나 Zod로 검사하려면 이름 있는 라이브러리의 계약에 맞춰야 합니다. Connector가 그 일을 합니다. document 변경은 React 구독으로 흐르고, 표의 보이는 행과 열은 Sheet의 Topology가 됩니다. -툴바, 리스트박스, 격자, 문서 줄, 캔버스, 나무, 보드는 제품이 그리는 -화면입니다. 편집 상태를 읽어 되돌리기 버튼을 끄거나, 고른 칸을 칠합니다. +고르기, 접기, 드래그, 되돌리기는 제품이 json-document를 만지는 손입니다. +화면은 호스트가 그리고, 단축키와 마우스 문법은 어포던스가 닫습니다. 플랫폼 계약과 라이브러리 연결과 호스트 화면은 높이가 같고 하는 일만 다릅니다. 필요한 것만 고릅니다. [Adapters](adapters.md)와 -[Connectors](connectors.md)에서 이어서 읽습니다. +[Connectors](connectors.md), [어포던스](affordance.md)에서 이어서 읽습니다. ## 편집 플랫폼의 커널 json-document는 여러 화면이 같은 문서를 쓰게 하는 편집 플랫폼의 커널입니다. JSON Document와 Collaboration이 값을 다룹니다. Editing이 손과 기억을 더합니다. Hands가 장르의 전형을 보여 줍니다. Adapter와 -Connector와 제품 화면이 그 손을 실제 환경에 붙입니다. +Connector와 어포던스가 그 손을 실제 환경에 붙입니다. 여러 화면이 같은 주소와 실행 취소와 협업을 쓰게 하려는 자리가 이 커널입니다. 먼저 값을 만져 보려면 [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..682c3fd4 --- /dev/null +++ b/packages/json-document-affordance/README.md @@ -0,0 +1,37 @@ +# @interactive-os/json-document-affordance + +Official keyboard and mouse editing affordances for json-document hosts. +The package closes conventional select, fold, drag, and undo/redo grammar. +Hosts keep markup and genre Intent. It does not render widgets. + +```sh +npm i @interactive-os/json-document-affordance +``` + +```ts +import { + pointerSelect, + resolveAffordanceKey, + treeAffordance, + dragOffset, + dragShouldCommit, + historyAffordance, +} from "@interactive-os/json-document-affordance"; + +pointerSelect({ shiftKey: true, metaKey: false, ctrlKey: false }); +// "extend" + +resolveAffordanceKey({ + key: "ArrowDown", + shiftKey: false, + metaKey: false, + ctrlKey: false, +}); +// { type: "move", direction: "down", operation: "replace" } +``` + +Keyboard Adapter still translates chords. This package decides the +affordance policy those commands mean. React Connector still answers +selection queries. Hosts still draw the product screen. + +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..4e03e26a --- /dev/null +++ b/packages/json-document-affordance/src/drag.ts @@ -0,0 +1,20 @@ +export type Point = { + readonly x: number; + readonly y: number; +}; + +export type DragOffset = { + readonly dx: number; + readonly dy: number; +}; + +export function dragOffset(origin: Point, point: Point): DragOffset { + return { + dx: point.x - origin.x, + dy: point.y - origin.y, + }; +} + +export function dragShouldCommit(offset: DragOffset): boolean { + return offset.dx !== 0 || offset.dy !== 0; +} diff --git a/packages/json-document-affordance/src/fold.ts b/packages/json-document-affordance/src/fold.ts new file mode 100644 index 00000000..eb4a1e7c --- /dev/null +++ b/packages/json-document-affordance/src/fold.ts @@ -0,0 +1,24 @@ +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, +): TreeAffordance { + if (command.direction === "right" && node.hasChildren && !node.expanded) { + return { type: "expand" }; + } + if (command.direction === "left" && node.hasChildren && node.expanded) { + return { type: "collapse" }; + } + return { type: "move", direction: command.direction }; +} diff --git a/packages/json-document-affordance/src/history.ts b/packages/json-document-affordance/src/history.ts new file mode 100644 index 00000000..9ad144ba --- /dev/null +++ b/packages/json-document-affordance/src/history.ts @@ -0,0 +1,21 @@ +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; +}): HistoryAffordanceMap { + return { + 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..1cf9a5e9 --- /dev/null +++ b/packages/json-document-affordance/src/index.ts @@ -0,0 +1,8 @@ +export { dragOffset, dragShouldCommit } from "./drag.js"; +export type { DragOffset, Point } from "./drag.js"; +export { 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 { pointerSelect, resolveAffordanceKey } from "./select.js"; +export type { SelectOperation } from "./select.js"; diff --git a/packages/json-document-affordance/src/select.ts b/packages/json-document-affordance/src/select.ts new file mode 100644 index 00000000..a2ed43a8 --- /dev/null +++ b/packages/json-document-affordance/src/select.ts @@ -0,0 +1,19 @@ +import { + createWebKeyboardAdapter, + selectionOperationFromModifiers, + type WebKeyboardCommand, + type WebKeyboardStroke, + type WebModifierState, +} from "@interactive-os/json-document-web"; + +const keyboard = createWebKeyboardAdapter(); + +export type SelectOperation = "replace" | "extend" | "toggle"; + +export function pointerSelect(modifiers: WebModifierState): SelectOperation { + return selectionOperationFromModifiers(modifiers); +} + +export function resolveAffordanceKey(stroke: WebKeyboardStroke): WebKeyboardCommand | null { + return keyboard.resolve(stroke); +} 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..ad3cd309 --- /dev/null +++ b/packages/json-document-affordance/tests/affordance.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, test } from "vitest"; +import { + dragOffset, + dragShouldCommit, + historyAffordance, + pointerSelect, + resolveAffordanceKey, + treeAffordance, +} from "../src/index.js"; + +describe("pointerSelect", () => { + test("maps conventional modifiers to replace, extend, and toggle", () => { + expect(pointerSelect({ shiftKey: false, metaKey: false, ctrlKey: false })).toBe("replace"); + expect(pointerSelect({ shiftKey: true, metaKey: false, ctrlKey: false })).toBe("extend"); + expect(pointerSelect({ shiftKey: false, metaKey: true, ctrlKey: false })).toBe("toggle"); + expect(pointerSelect({ shiftKey: false, metaKey: false, ctrlKey: true })).toBe("toggle"); + }); +}); + +describe("resolveAffordanceKey", () => { + test("closes the conventional keymap without a host override", () => { + expect(resolveAffordanceKey({ key: "ArrowDown", shiftKey: false, metaKey: false, ctrlKey: false })).toEqual({ + type: "move", + direction: "down", + operation: "replace", + }); + expect(resolveAffordanceKey({ key: "ArrowDown", shiftKey: true, metaKey: false, ctrlKey: false })).toEqual({ + type: "move", + direction: "down", + operation: "extend", + }); + expect(resolveAffordanceKey({ key: "z", shiftKey: false, metaKey: true, ctrlKey: false })).toEqual({ type: "undo" }); + expect(resolveAffordanceKey({ key: "z", shiftKey: true, metaKey: true, ctrlKey: false })).toEqual({ type: "redo" }); + expect(resolveAffordanceKey({ key: "Delete", shiftKey: false, metaKey: false, ctrlKey: false })).toEqual({ 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 })).toEqual({ + type: "expand", + }); + expect(treeAffordance({ type: "move", direction: "left" }, { expanded: true, hasChildren: true })).toEqual({ + type: "collapse", + }); + expect(treeAffordance({ type: "move", direction: "right" }, { expanded: true, hasChildren: true })).toEqual({ + type: "move", + direction: "right", + }); + expect(treeAffordance({ type: "move", direction: "left" }, { expanded: false, hasChildren: true })).toEqual({ + type: "move", + direction: "left", + }); + expect(treeAffordance({ type: "move", direction: "down" }, { expanded: true, hasChildren: true })).toEqual({ + type: "move", + direction: "down", + }); + }); +}); + +describe("dragOffset", () => { + test("commits only when the pointer actually moved", () => { + expect(dragOffset({ x: 10, y: 20 }, { x: 14, y: 18 })).toEqual({ dx: 4, dy: -2 }); + expect(dragShouldCommit({ dx: 0, dy: 0 })).toBe(false); + expect(dragShouldCommit({ dx: 4, dy: -2 })).toBe(true); + }); +}); + +describe("historyAffordance", () => { + test("binds disabled to canUndo and canRedo", () => { + expect(historyAffordance({ canUndo: false, canRedo: true })).toEqual({ + undo: { name: "undo", disabled: true }, + redo: { name: "redo", disabled: false }, + }); + }); +}); 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..487e9238 100644 --- a/site/scripts/route-checks.mjs +++ b/site/scripts/route-checks.mjs @@ -7,6 +7,7 @@ const navigationGroups = new Set([ "Adapter", "Connector", "제품 화면", + "어포던스", ]); export function validateSiteRoutes(routes, fail) { diff --git a/site/site-routes.json b/site/site-routes.json index 08468156..69828c63 100644 --- a/site/site-routes.json +++ b/site/site-routes.json @@ -434,60 +434,113 @@ } }, { - "path": "/widgets", - "label": "제품 화면", - "title": "Widgets - json-document", - "description": "Sample widgets that bind only editing affordances: Toolbar, Listbox, Grid, Document, Canvas, Tree, and Board.", - "navigationGroup": "제품 화면", + "path": "/docs/affordance", + "label": "어포던스", + "title": "Affordance - json-document", + "heading": "어포던스", + "description": "제품이 json-document를 만지는 최전선의 키보드·마우스 손을 API와 사용법으로 설명합니다.", + "language": "ko", + "navigationGroup": "어포던스", "sidebar": false }, + { + "path": "/docs/affordance/select", + "label": "고르기", + "title": "고르기 - json-document", + "heading": "고르기", + "description": "클릭, Shift 범위, Mod 토글, 화살표로 대상을 집는 손을 붙입니다.", + "language": "ko", + "navigationGroup": "어포던스", + "relatedDemoPath": "/widgets/listbox", + "relatedDemoLabel": "증명 열기" + }, + { + "path": "/docs/affordance/fold", + "label": "접기", + "title": "접기 - json-document", + "heading": "접기", + "description": "나무에서 왼쪽은 접고 오른쪽은 펼치는 손을 붙입니다.", + "language": "ko", + "navigationGroup": "어포던스", + "relatedDemoPath": "/widgets/tree", + "relatedDemoLabel": "증명 열기" + }, + { + "path": "/docs/affordance/drag", + "label": "드래그", + "title": "드래그 - json-document", + "heading": "드래그", + "description": "고른 대상을 포인터로 옮기는 손을 붙입니다.", + "language": "ko", + "navigationGroup": "어포던스", + "relatedDemoPath": "/widgets/canvas", + "relatedDemoLabel": "증명 열기" + }, + { + "path": "/docs/affordance/history", + "label": "되돌리기", + "title": "되돌리기 - json-document", + "heading": "되돌리기", + "description": "Mod+Z와 canUndo로 값과 선택을 함께 되돌리는 손을 붙입니다.", + "language": "ko", + "navigationGroup": "어포던스", + "relatedDemoPath": "/widgets/toolbar", + "relatedDemoLabel": "증명 열기" + }, { "path": "/widgets/toolbar", "label": "Toolbar", "title": "Toolbar Widget - 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", "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", "description": "A grid reads visible topology and selected cells from Sheet selection.", - "navigationGroup": "제품 화면" + "parentPath": "/docs/affordance/select", + "sidebar": false }, { "path": "/widgets/document", "label": "Document", "title": "Document Widget - json-document", "description": "A document line reads selected keys, focus, and text offset from block selection.", - "navigationGroup": "제품 화면" + "parentPath": "/docs/affordance/select", + "sidebar": false }, { "path": "/widgets/canvas", "label": "Canvas", "title": "Canvas Widget - json-document", "description": "A canvas reads selected objects on a plane from Object selection.", - "navigationGroup": "제품 화면" + "parentPath": "/docs/affordance/drag", + "sidebar": false }, { "path": "/widgets/tree", "label": "Tree", "title": "Tree Widget - json-document", "description": "A tree reads the visible hierarchical line from Tree selection.", - "navigationGroup": "제품 화면" + "parentPath": "/docs/affordance/fold", + "sidebar": false }, { "path": "/widgets/board", "label": "Board", "title": "Board Widget - json-document", "description": "A board reads columns of cards and selected keys from Kanban selection.", - "navigationGroup": "제품 화면" + "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..e33e0756 100644 --- a/site/src/app/page-descriptors.ts +++ b/site/src/app/page-descriptors.ts @@ -8,7 +8,8 @@ export type SiteNavigationGroup = | "Hands" | "Adapter" | "Connector" - | "제품 화면"; + | "제품 화면" + | "어포던스"; 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 d80d82c0..26acb53e 100644 --- a/site/src/app/routeTree.gen.ts +++ b/site/src/app/routeTree.gen.ts @@ -63,6 +63,11 @@ import { Route as PageWidgetsToolbarRouteImport } from "./routes/_page/widgets/t 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 PageDocsAffordanceDragRouteImport } from "./routes/_page/docs/affordance/drag"; +import { Route as PageDocsAffordanceFoldRouteImport } from "./routes/_page/docs/affordance/fold"; +import { Route as PageDocsAffordanceHistoryRouteImport } from "./routes/_page/docs/affordance/history"; +import { Route as PageDocsAffordanceSelectRouteImport } from "./routes/_page/docs/affordance/select"; 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"; @@ -344,6 +349,33 @@ 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 PageDocsAffordanceDragRoute = PageDocsAffordanceDragRouteImport.update({ + id: "/docs/affordance/drag", + path: "/docs/affordance/drag", + getParentRoute: () => PageRoute, +} as any); +const PageDocsAffordanceFoldRoute = PageDocsAffordanceFoldRouteImport.update({ + id: "/docs/affordance/fold", + path: "/docs/affordance/fold", + getParentRoute: () => PageRoute, +} as any); +const PageDocsAffordanceHistoryRoute = + PageDocsAffordanceHistoryRouteImport.update({ + id: "/docs/affordance/history", + path: "/docs/affordance/history", + getParentRoute: () => PageRoute, + } as any); +const PageDocsAffordanceSelectRoute = + PageDocsAffordanceSelectRouteImport.update({ + id: "/docs/affordance/select", + path: "/docs/affordance/select", + getParentRoute: () => PageRoute, + } as any); const PageDocsCollaborationIndexRoute = PageDocsCollaborationIndexRouteImport.update({ id: "/docs/collaboration/", @@ -434,10 +466,15 @@ export interface FileRoutesByFullPath { "/docs/": typeof PageDocsIndexRoute; "/widgets/": typeof PageWidgetsIndexRoute; "/connectors/zod/validate": typeof PageConnectorsZodValidateRoute; + "/docs/affordance/drag": typeof PageDocsAffordanceDragRoute; + "/docs/affordance/fold": typeof PageDocsAffordanceFoldRoute; + "/docs/affordance/history": typeof PageDocsAffordanceHistoryRoute; + "/docs/affordance/select": typeof PageDocsAffordanceSelectRoute; "/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; @@ -495,10 +532,15 @@ export interface FileRoutesByTo { "/docs": typeof PageDocsIndexRoute; "/widgets": typeof PageWidgetsIndexRoute; "/connectors/zod/validate": typeof PageConnectorsZodValidateRoute; + "/docs/affordance/drag": typeof PageDocsAffordanceDragRoute; + "/docs/affordance/fold": typeof PageDocsAffordanceFoldRoute; + "/docs/affordance/history": typeof PageDocsAffordanceHistoryRoute; + "/docs/affordance/select": typeof PageDocsAffordanceSelectRoute; "/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; @@ -558,10 +600,15 @@ export interface FileRoutesById { "/_page/docs/": typeof PageDocsIndexRoute; "/_page/widgets/": typeof PageWidgetsIndexRoute; "/_page/connectors/zod/validate": typeof PageConnectorsZodValidateRoute; + "/_page/docs/affordance/drag": typeof PageDocsAffordanceDragRoute; + "/_page/docs/affordance/fold": typeof PageDocsAffordanceFoldRoute; + "/_page/docs/affordance/history": typeof PageDocsAffordanceHistoryRoute; + "/_page/docs/affordance/select": typeof PageDocsAffordanceSelectRoute; "/_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; @@ -621,10 +668,15 @@ export interface FileRouteTypes { | "/docs/" | "/widgets/" | "/connectors/zod/validate" + | "/docs/affordance/drag" + | "/docs/affordance/fold" + | "/docs/affordance/history" + | "/docs/affordance/select" | "/docs/collaboration/history" | "/docs/collaboration/lifecycle" | "/docs/collaboration/replica" | "/connectors/zod/" + | "/docs/affordance/" | "/docs/collaboration/" | "/docs/collaboration/text/lease" | "/docs/collaboration/text/"; @@ -682,10 +734,15 @@ export interface FileRouteTypes { | "/docs" | "/widgets" | "/connectors/zod/validate" + | "/docs/affordance/drag" + | "/docs/affordance/fold" + | "/docs/affordance/history" + | "/docs/affordance/select" | "/docs/collaboration/history" | "/docs/collaboration/lifecycle" | "/docs/collaboration/replica" | "/connectors/zod" + | "/docs/affordance" | "/docs/collaboration" | "/docs/collaboration/text/lease" | "/docs/collaboration/text"; @@ -744,10 +801,15 @@ export interface FileRouteTypes { | "/_page/docs/" | "/_page/widgets/" | "/_page/connectors/zod/validate" + | "/_page/docs/affordance/drag" + | "/_page/docs/affordance/fold" + | "/_page/docs/affordance/history" + | "/_page/docs/affordance/select" | "/_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/"; @@ -1138,6 +1200,41 @@ 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/drag": { + id: "/_page/docs/affordance/drag"; + path: "/docs/affordance/drag"; + fullPath: "/docs/affordance/drag"; + preLoaderRoute: typeof PageDocsAffordanceDragRouteImport; + 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/history": { + id: "/_page/docs/affordance/history"; + path: "/docs/affordance/history"; + fullPath: "/docs/affordance/history"; + preLoaderRoute: typeof PageDocsAffordanceHistoryRouteImport; + 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/collaboration/": { id: "/_page/docs/collaboration/"; path: "/docs/collaboration"; @@ -1235,10 +1332,15 @@ interface PageRouteChildren { PageDocsIndexRoute: typeof PageDocsIndexRoute; PageWidgetsIndexRoute: typeof PageWidgetsIndexRoute; PageConnectorsZodValidateRoute: typeof PageConnectorsZodValidateRoute; + PageDocsAffordanceDragRoute: typeof PageDocsAffordanceDragRoute; + PageDocsAffordanceFoldRoute: typeof PageDocsAffordanceFoldRoute; + PageDocsAffordanceHistoryRoute: typeof PageDocsAffordanceHistoryRoute; + PageDocsAffordanceSelectRoute: typeof PageDocsAffordanceSelectRoute; PageDocsCollaborationHistoryRoute: typeof PageDocsCollaborationHistoryRoute; PageDocsCollaborationLifecycleRoute: typeof PageDocsCollaborationLifecycleRoute; PageDocsCollaborationReplicaRoute: typeof PageDocsCollaborationReplicaRoute; PageConnectorsZodIndexRoute: typeof PageConnectorsZodIndexRoute; + PageDocsAffordanceIndexRoute: typeof PageDocsAffordanceIndexRoute; PageDocsCollaborationIndexRoute: typeof PageDocsCollaborationIndexRoute; PageDocsCollaborationTextLeaseRoute: typeof PageDocsCollaborationTextLeaseRoute; PageDocsCollaborationTextIndexRoute: typeof PageDocsCollaborationTextIndexRoute; @@ -1296,10 +1398,15 @@ const PageRouteChildren: PageRouteChildren = { PageDocsIndexRoute: PageDocsIndexRoute, PageWidgetsIndexRoute: PageWidgetsIndexRoute, PageConnectorsZodValidateRoute: PageConnectorsZodValidateRoute, + PageDocsAffordanceDragRoute: PageDocsAffordanceDragRoute, + PageDocsAffordanceFoldRoute: PageDocsAffordanceFoldRoute, + PageDocsAffordanceHistoryRoute: PageDocsAffordanceHistoryRoute, + PageDocsAffordanceSelectRoute: PageDocsAffordanceSelectRoute, 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/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/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/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/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/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/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/site-layers.ts b/site/src/app/site-layers.ts index 376ba0fb..c2b88031 100644 --- a/site/src/app/site-layers.ts +++ b/site/src/app/site-layers.ts @@ -14,7 +14,7 @@ export const siteLayers: ReadonlyArray = [ { 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, Grid, Document, Canvas, Tree, and Board" }, + { group: "어포던스", path: "/docs/affordance", label: "어포던스", blurb: "고르기, 접기, 드래그, 되돌리기" }, ]; export const siteLayerGroups: ReadonlyArray = siteLayers.map((layer) => layer.group); diff --git a/site/src/routes/docs/DocsRoute.tsx b/site/src/routes/docs/DocsRoute.tsx index f933aa94..fe86e134 100644 --- a/site/src/routes/docs/DocsRoute.tsx +++ b/site/src/routes/docs/DocsRoute.tsx @@ -10,6 +10,11 @@ const docIllustrations: Record = { overview: "package", quickstart: "terminal", adapters: "peek", + affordance: "cursor", + affordanceSelect: "cursor", + affordanceFold: "branch", + affordanceDrag: "peek", + affordanceHistory: "clipboard", connectors: "connector", reactEditing: "cursor", collaboration: "package", @@ -43,6 +48,10 @@ export function AdapterDocsRoute() { return ; } +export function AffordanceDocsRoute() { + return ; +} + export function ConnectorDocsRoute() { return ; } @@ -83,7 +92,7 @@ export function DocsRoute({ pageId }: { readonly pageId: DocPageId }) { {demo ? (
- {demo.label} 열기 + {page.relatedDemoLabel ?? `${demo.label} 열기`}
) : null} diff --git a/site/src/routes/widgets/CanvasWidgetRoute.tsx b/site/src/routes/widgets/CanvasWidgetRoute.tsx index 8b72501c..a724bebb 100644 --- a/site/src/routes/widgets/CanvasWidgetRoute.tsx +++ b/site/src/routes/widgets/CanvasWidgetRoute.tsx @@ -1,9 +1,14 @@ -import { useState } from "react"; +import { useState, type PointerEvent } from "react"; import { createObjectEditor, type ObjectDocument } from "@interactive-os/json-document-editing"; import { useEditing } from "@interactive-os/json-document-react"; +import { + dragOffset, + dragShouldCommit, + pointerSelect, +} from "@interactive-os/json-document-affordance"; import { SelectableItem } from "../../shared/ui/interactive"; -import { WidgetDemoFrame } from "./WidgetDemoFrame"; import { optionProps } from "../../shared/widget-binding"; +import { WidgetDemoFrame } from "./WidgetDemoFrame"; const initialObjects: ObjectDocument = { objects: [ @@ -13,8 +18,17 @@ const initialObjects: ObjectDocument = { ], }; +type DragState = { + readonly ids: ReadonlyArray; + readonly originX: number; + readonly originY: number; + readonly dx: number; + readonly dy: number; +}; + export function CanvasWidgetRoute() { const [editor] = useState(() => createObjectEditor(initialObjects)); + const [drag, setDrag] = useState(null); const editing = useEditing({ source: editor, selectedKeys: editor.selectedObjects.map((object) => object.id), @@ -26,35 +40,73 @@ export function CanvasWidgetRoute() { mode: mode === "extend" ? "add" : mode, }); }, + operationFromEvent: (event) => pointerSelect({ + shiftKey: event.shiftKey ?? false, + metaKey: event.metaKey ?? false, + ctrlKey: event.ctrlKey ?? false, + }), }); const document = editing.snapshot.value as ObjectDocument; + function handlePointerDown(event: PointerEvent, objectId: string) { + editing.getItem(objectId).getPressHandler()(event); + const ids = pointerSelect(event) === "replace" + ? [objectId] + : [...new Set([...editor.selectedObjects.map((object) => object.id), objectId])]; + event.currentTarget.setPointerCapture(event.pointerId); + setDrag({ ids, originX: event.clientX, originY: event.clientY, dx: 0, dy: 0 }); + } + + function handlePointerMove(event: PointerEvent) { + if (!drag) return; + const offset = dragOffset({ x: drag.originX, y: drag.originY }, { x: event.clientX, y: event.clientY }); + setDrag({ ...drag, dx: offset.dx, dy: offset.dy }); + } + + function handlePointerUp() { + if (!drag) return; + const offset = { dx: drag.dx, dy: drag.dy }; + if (dragShouldCommit(offset)) { + editor.dispatch({ type: "object.translate", objectIds: drag.ids, dx: offset.dx, dy: offset.dy }); + } + setDrag(null); + } + return ( - {document.objects.map((object) => ( - - {object.label} - - ))} + {document.objects.map((object) => { + const offset = drag?.ids.includes(object.id) ? drag : null; + const option = optionProps(editing.getItem(object.id)); + return ( + handlePointerDown(event, object.id)} + onPointerMove={handlePointerMove} + onPointerUp={handlePointerUp} + > + {object.label} + + ); + })} )} values={[ diff --git a/site/src/routes/widgets/ListboxWidgetRoute.tsx b/site/src/routes/widgets/ListboxWidgetRoute.tsx index 51cd1d92..fd8f9b08 100644 --- a/site/src/routes/widgets/ListboxWidgetRoute.tsx +++ b/site/src/routes/widgets/ListboxWidgetRoute.tsx @@ -4,6 +4,7 @@ import { useEditing } from "@interactive-os/json-document-react"; import { lineBoundary, moveLinePoint } from "@interactive-os/json-document-web"; import { SelectableItem } from "../../shared/ui/interactive"; import { classes, ui } from "../../shared/ui/styles"; +import { pointerSelect, resolveAffordanceKey } from "@interactive-os/json-document-affordance"; import { optionProps, useWidgetKeyboard } from "../../shared/widget-binding"; import { WidgetDemoFrame } from "./WidgetDemoFrame"; @@ -28,8 +29,17 @@ export function ListboxWidgetRoute() { onSelect: (itemId, mode) => { editor.dispatch({ type: "selection.set", itemId, mode }); }, + operationFromEvent: (event) => pointerSelect({ + shiftKey: event.shiftKey ?? false, + metaKey: event.metaKey ?? false, + ctrlKey: event.ctrlKey ?? false, + }), keyboard: { - resolve: (stroke) => keyboard.resolve(stroke), + resolve: (stroke) => { + const command = resolveAffordanceKey(stroke); + keyboard.resolve(stroke); + return command; + }, focusKey: () => editor.snapshot.selection.ranges[editor.snapshot.selection.primaryIndex ?? 0]?.focus.itemId ?? undefined, neighbor: (key, command) => command.type === "move" ? moveLinePoint(ids(), key, command.direction) diff --git a/site/src/routes/widgets/ToolbarWidgetRoute.tsx b/site/src/routes/widgets/ToolbarWidgetRoute.tsx index e8169c50..48b6b1a9 100644 --- a/site/src/routes/widgets/ToolbarWidgetRoute.tsx +++ b/site/src/routes/widgets/ToolbarWidgetRoute.tsx @@ -4,7 +4,8 @@ import { useEditing } from "@interactive-os/json-document-react"; import { lineBoundary, moveLinePoint } from "@interactive-os/json-document-web"; import { ActionButton, SelectableItem } from "../../shared/ui/interactive"; import { classes, ui } from "../../shared/ui/styles"; -import { historyCommands, optionProps, useWidgetKeyboard } from "../../shared/widget-binding"; +import { historyAffordance, resolveAffordanceKey } from "@interactive-os/json-document-affordance"; +import { optionProps, useWidgetKeyboard } from "../../shared/widget-binding"; import { WidgetDemoFrame } from "./WidgetDemoFrame"; const initialOrder: OrderDocument = { @@ -27,7 +28,11 @@ export function ToolbarWidgetRoute() { editor.dispatch({ type: "selection.set", itemId, mode }); }, keyboard: { - resolve: (stroke) => keyboard.resolve(stroke), + resolve: (stroke) => { + const command = resolveAffordanceKey(stroke); + keyboard.resolve(stroke); + return command; + }, focusKey: () => editor.snapshot.selection.ranges[editor.snapshot.selection.primaryIndex ?? 0]?.focus.itemId ?? undefined, neighbor: (key, command) => command.type === "move" ? moveLinePoint(ids(), key, command.direction) @@ -45,7 +50,7 @@ export function ToolbarWidgetRoute() { }); const snapshot = editing.snapshot; const document = snapshot.value as OrderDocument; - const commands = historyCommands(snapshot); + const commands = historyAffordance(snapshot); return ( keyboard.resolve(stroke), + resolve: (stroke) => { + const command = resolveAffordanceKey(stroke); + keyboard.resolve(stroke); + return command; + }, focusKey: () => editor.snapshot.selection.ranges[editor.snapshot.selection.primaryIndex ?? 0]?.focus.nodeId ?? undefined, - neighbor: (key, command) => command.type === "move" - ? moveLinePoint(topology.visibleIds, key, command.direction) - : lineBoundary(topology.visibleIds, command.edge), + neighbor: (key, command) => { + const row = rows.find((item) => item.id === key); + if (row && command.type === "move") { + const action = treeAffordance(command, { + expanded: expanded.has(row.id), + hasChildren: document.nodes.some((node) => node.parentId === row.id), + }); + if (action.type === "expand") { + setExpanded((current) => new Set(current).add(row.id)); + return key; + } + if (action.type === "collapse") { + setExpanded((current) => { + const next = new Set(current); + next.delete(row.id); + return next; + }); + return key; + } + } + return command.type === "move" + ? moveLinePoint(topology.visibleIds, key, command.direction) + : lineBoundary(topology.visibleIds, command.edge); + }, onDelete: () => { editor.dispatch({ type: "selection.remove", topology }); }, diff --git a/site/src/routes/widgets/WidgetCatalogRoute.tsx b/site/src/routes/widgets/WidgetCatalogRoute.tsx deleted file mode 100644 index 7904d38d..00000000 --- a/site/src/routes/widgets/WidgetCatalogRoute.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { pageDescriptor } from "../../app/page-descriptors"; -import { ActionLink } from "../../shared/ui/interactive"; -import { PageFrame, PageHeader } from "../../shared/ui/primitives"; -import { classes, ui } from "../../shared/ui/styles"; - -const widgets = [ - { - ...pageDescriptor("/widgets/toolbar"), - affordance: "canUndo / canRedo", - }, - { - ...pageDescriptor("/widgets/listbox"), - affordance: "selected keys / focus", - }, - { - ...pageDescriptor("/widgets/grid"), - affordance: "topology / selected cells", - }, - { - ...pageDescriptor("/widgets/document"), - affordance: "block line / text offset", - }, - { - ...pageDescriptor("/widgets/canvas"), - affordance: "selected objects", - }, - { - ...pageDescriptor("/widgets/tree"), - affordance: "visible topology", - }, - { - ...pageDescriptor("/widgets/board"), - affordance: "columns / selected cards", - }, -] as const; - -export function WidgetCatalogRoute() { - return ( - - - Bindings turn editing values into Toolbar, Listbox, Grid, Document, Canvas, Tree, and Board props. The demos only render. Button and Card are not widgets. - - -
- {widgets.map((widget) => ( -
-

{widget.affordance}

-

{widget.label}

-

{widget.description}

- - Open {widget.label} - -
- ))} -
-
- ); -} diff --git a/site/src/shared/widget-binding/history.ts b/site/src/shared/widget-binding/history.ts index 7dbd6f85..75a84267 100644 --- a/site/src/shared/widget-binding/history.ts +++ b/site/src/shared/widget-binding/history.ts @@ -1,21 +1,8 @@ -export type HistoryCommandName = "undo" | "redo"; - -export type HistoryCommand = { - readonly name: HistoryCommandName; - readonly disabled: boolean; -}; - -export type HistoryCommandMap = { - readonly undo: HistoryCommand; - readonly redo: HistoryCommand; -}; - -export function historyCommands(snapshot: { - readonly canUndo: boolean; - readonly canRedo: boolean; -}): HistoryCommandMap { - return { - undo: { name: "undo", disabled: !snapshot.canUndo }, - redo: { name: "redo", disabled: !snapshot.canRedo }, - }; -} +export { + historyAffordance as historyCommands, +} from "@interactive-os/json-document-affordance"; +export type { + HistoryAffordance as HistoryCommand, + HistoryAffordanceMap as HistoryCommandMap, + HistoryAffordanceName as HistoryCommandName, +} from "@interactive-os/json-document-affordance"; diff --git a/site/tests/browser/site-shell.spec.ts b/site/tests/browser/site-shell.spec.ts index 94907181..8bf2ada7 100644 --- a/site/tests/browser/site-shell.spec.ts +++ b/site/tests/browser/site-shell.spec.ts @@ -18,7 +18,7 @@ test("official overview exposes the product hierarchy", async ({ page }) => { "Hands", "Adapter", "Connector", - "제품 화면", + "어포던스", ]); await expect(navigation.getByRole("link", { name: "Why" })).toHaveCount(0); await expect(navigation.getByRole("link", { name: "Replica" })).toHaveCount(0); @@ -60,15 +60,12 @@ test("official overview exposes the product hierarchy", async ({ page }) => { await expect(navigation.getByRole("group", { name: "Adapter" }).getByRole("link")).toHaveText(["Keyboard", "Clipboard adapter", "Contenteditable"]); await navigation.getByRole("button", { name: "Connector" }).click(); await expect(navigation.getByRole("group", { name: "Connector" }).getByRole("link")).toHaveText(["React", "React Hook Form", "Ajv", "Zod", "TanStack Table"]); - await navigation.getByRole("button", { name: "제품 화면" }).click(); - await expect(navigation.getByRole("group", { name: "제품 화면" }).getByRole("link")).toHaveText([ - "Toolbar", - "Listbox", - "Grid", - "Document", - "Canvas", - "Tree", - "Board", + await navigation.getByRole("button", { name: "어포던스" }).click(); + await expect(navigation.getByRole("group", { name: "어포던스" }).getByRole("link")).toHaveText([ + "고르기", + "접기", + "드래그", + "되돌리기", ]); await expect(navigation.getByRole("group", { name: "Demos" })).toHaveCount(0); await expect(navigation.getByRole("group", { name: "Reference" })).toHaveCount(0); @@ -79,7 +76,7 @@ test("official overview exposes the product hierarchy", async ({ page }) => { "Hands", "Adapter", "Connector", - "제품 화면", + "어포던스", ]); await expect(navigation.getByRole("link", { name: "Extensions" })).toHaveCount(0); expect(requests.some(isLegacyRequest)).toBe(false); @@ -97,7 +94,7 @@ test("mobile navigation preserves the product groups without duplicating documen await expect(siteNavigation.getByRole("group", { name: "Adapter" })).toBeVisible(); await expect(siteNavigation.getByRole("group", { name: "Demos" })).toHaveCount(0); await expect(siteNavigation.getByRole("group", { name: "Connector" })).toBeVisible(); - await expect(siteNavigation.getByRole("group", { name: "제품 화면" })).toBeVisible(); + await expect(siteNavigation.getByRole("group", { name: "어포던스" })).toBeVisible(); await expect(siteNavigation.getByRole("group", { name: "Collaboration" })).toBeVisible(); await page.goto("/docs/tutorial"); diff --git a/site/tests/browser/widgets.spec.ts b/site/tests/browser/widgets.spec.ts index 887b6882..12a799e7 100644 --- a/site/tests/browser/widgets.spec.ts +++ b/site/tests/browser/widgets.spec.ts @@ -1,16 +1,14 @@ import { expect, test, type Page } from "@playwright/test"; -test("Widgets catalog lists only editing widgets", async ({ page }) => { +test("Widgets catalog redirects to affordance usage", async ({ page }) => { await page.goto("/widgets"); - await expect(page.getByRole("heading", { level: 1, name: "제품 화면" })).toBeVisible(); - await expect(page.getByRole("link", { name: "Open Toolbar" })).toHaveAttribute("href", "/widgets/toolbar"); - await expect(page.getByRole("link", { name: "Open Listbox" })).toHaveAttribute("href", "/widgets/listbox"); - await expect(page.getByRole("link", { name: "Open Grid" })).toHaveAttribute("href", "/widgets/grid"); - await expect(page.getByRole("link", { name: "Open Document" })).toHaveAttribute("href", "/widgets/document"); - await expect(page.getByRole("link", { name: "Open Canvas" })).toHaveAttribute("href", "/widgets/canvas"); - await expect(page.getByRole("link", { name: "Open Tree" })).toHaveAttribute("href", "/widgets/tree"); - await expect(page.getByRole("link", { name: "Open Board" })).toHaveAttribute("href", "/widgets/board"); - await expect(page.getByRole("link", { name: "Open Database" })).toHaveCount(0); + await expect(page).toHaveURL(/\/docs\/affordance$/); + await expect(page.getByRole("heading", { level: 1, name: "어포던스" })).toBeVisible(); + const navigation = page.getByRole("navigation", { name: "Site navigation" }); + await expect(navigation.getByRole("link", { name: "고르기" })).toHaveAttribute("href", "/docs/affordance/select"); + await expect(navigation.getByRole("link", { name: "접기" })).toHaveAttribute("href", "/docs/affordance/fold"); + await expect(navigation.getByRole("link", { name: "드래그" })).toHaveAttribute("href", "/docs/affordance/drag"); + await expect(navigation.getByRole("link", { name: "되돌리기" })).toHaveAttribute("href", "/docs/affordance/history"); }); test("Toolbar binds Undo and Redo to canUndo and canRedo", async ({ page }) => { diff --git a/site/tests/unit/app-shell.test.tsx b/site/tests/unit/app-shell.test.tsx index 2c0d072e..9d0d0b8f 100644 --- a/site/tests/unit/app-shell.test.tsx +++ b/site/tests/unit/app-shell.test.tsx @@ -42,7 +42,7 @@ describe("official site shell", () => { "Hands", "Adapter", "Connector", - "제품 화면", + "어포던스", ]); await user.click(nav.getByRole("button", { name: "JSON Document" })); expect(groupLinks(nav, "JSON Document")).toEqual([ @@ -82,8 +82,8 @@ describe("official site shell", () => { expect(groupLinks(nav, "Adapter")).toEqual(["Keyboard", "Clipboard adapter", "Contenteditable"]); await user.click(nav.getByRole("button", { name: "Connector" })); expect(groupLinks(nav, "Connector")).toEqual(["React", "React Hook Form", "Ajv", "Zod", "TanStack Table"]); - await user.click(nav.getByRole("button", { name: "제품 화면" })); - expect(groupLinks(nav, "제품 화면")).toEqual(["Toolbar", "Listbox", "Grid", "Document", "Canvas", "Tree", "Board"]); + await user.click(nav.getByRole("button", { name: "어포던스" })); + expect(groupLinks(nav, "어포던스")).toEqual(["고르기", "접기", "드래그", "되돌리기"]); expect(nav.queryByRole("group", { name: "Demos" })).toBeNull(); expect(nav.queryByRole("group", { name: "Reference" })).toBeNull(); expect(nav.getAllByRole("group").map((group) => group.getAttribute("aria-label"))).toEqual([ @@ -93,7 +93,7 @@ describe("official site shell", () => { "Hands", "Adapter", "Connector", - "제품 화면", + "어포던스", ]); expect(nav.queryByRole("link", { name: "Extensions" })).toBeNull(); diff --git a/site/tests/unit/breadcrumb.test.ts b/site/tests/unit/breadcrumb.test.ts index 5bfc0c88..43ab5fec 100644 --- a/site/tests/unit/breadcrumb.test.ts +++ b/site/tests/unit/breadcrumb.test.ts @@ -87,16 +87,17 @@ describe("breadcrumbTrail", () => { "Zod:/connectors/zod", "Validate:/connectors/zod/validate", ]); - expect(trail("/widgets")).toEqual(["Overview:/", "제품 화면:/widgets"]); - expect(trail("/widgets/toolbar")).toEqual([ + expect(trail("/docs/affordance")).toEqual(["Overview:/", "어포던스:/docs/affordance"]); + expect(trail("/docs/affordance/select")).toEqual([ "Overview:/", - "제품 화면:/widgets", - "Toolbar:/widgets/toolbar", + "어포던스:/docs/affordance", + "고르기:/docs/affordance/select", ]); - expect(trail("/widgets/board")).toEqual([ + expect(trail("/widgets/toolbar")).toEqual([ "Overview:/", - "제품 화면:/widgets", - "Board:/widgets/board", + "어포던스:/docs/affordance", + "되돌리기:/docs/affordance/history", + "Toolbar:/widgets/toolbar", ]); }); diff --git a/site/tsconfig.json b/site/tsconfig.json index 6076f736..c733fc06 100644 --- a/site/tsconfig.json +++ b/site/tsconfig.json @@ -12,6 +12,7 @@ "@interactive-os/json-document-react": ["../packages/json-document-react/src/index.ts"], "@interactive-os/json-document-react-hook-form": ["../packages/json-document-react-hook-form/src/index.ts"], "@interactive-os/json-document-ajv": ["../packages/json-document-ajv/src/index.ts"], + "@interactive-os/json-document-affordance": ["../packages/json-document-affordance/src/index.ts"], "@interactive-os/json-document-tanstack-table": ["../packages/json-document-tanstack-table/src/index.ts"], "@interactive-os/json-document-web": ["../packages/json-document-web/src/index.ts"], "@interactive-os/json-document-contenteditable": ["../packages/json-document-contenteditable/src/index.ts"], diff --git a/standards/repository-implementation-shape.md b/standards/repository-implementation-shape.md index 8050be23..6d60b128 100644 --- a/standards/repository-implementation-shape.md +++ b/standards/repository-implementation-shape.md @@ -204,7 +204,7 @@ foundation으로 유지한다. ## 현재 package 분류 -아래 표는 현재 15개 library package를 이 문서의 모형으로 빠짐없이 분류한다. +아래 표는 현재 16개 library package를 이 문서의 모형으로 빠짐없이 분류한다. `후속`은 이 RFC가 source를 이동하지 않고 별도 이슈가 책임짐을 뜻한다. | Package path | 정본 모형 | 현재 판단 | @@ -215,6 +215,7 @@ foundation으로 유지한다. | `packages/json-document-react` | Single-native Connector | 하나의 React subscription/lifecycle entry로 flat 유지 | | `packages/json-document-react-hook-form` | Single-native Connector | RHF lifecycle이 하나인 동안 flat 유지; 독립 binding이 생기면 분리 | | `packages/json-document-ajv` | Single-native Connector | 하나의 validator translation으로 flat 유지 | +| `packages/json-document-affordance` | Responsibility family | select/fold/drag/history 책임 file과 root facade 유지 | | `packages/json-document-zod` | Composite Connector | validator와 Database translation을 책임 file로 분리한 현재 모양 유지 | | `packages/json-document-tanstack-table` | Single-native Connector | 하나의 Table/Sheet binding으로 flat 유지 | | `packages/json-document-web` | Adapter family | keyboard/clipboard/input/modifier 책임 file과 root facade 유지 | diff --git a/tsconfig.build.json b/tsconfig.build.json index 54776707..12edb1eb 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -8,6 +8,7 @@ { "path": "./packages/json-document-react" }, { "path": "./packages/json-document-react-hook-form" }, { "path": "./packages/json-document-ajv" }, + { "path": "./packages/json-document-affordance" }, { "path": "./packages/json-document-zod" }, { "path": "./packages/json-document-tanstack-table" }, { "path": "./packages/json-document-web" }, From 88ae4380efdeb370aad74e1c03bcaf5560c3b36b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Thu, 20 Aug 2026 21:24:56 +0900 Subject: [PATCH 02/14] =?UTF-8?q?=EC=96=B4=ED=8F=AC=EB=8D=98=EC=8A=A4:=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EB=B2=95=20=EB=B3=B8=EB=AC=B8=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=EC=99=80=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=EC=A6=9D?= =?UTF-8?q?=EB=AA=85=EC=9D=84=20=ED=8C=A8=ED=82=A4=EC=A7=80=EC=97=90=20?= =?UTF-8?q?=EB=8B=AB=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공개 문서의 .md 링크를 사이트 경로로 잇고, Grid·Document가 호스트 keymap이 아니라 pointerSelect와 resolveAffordanceKey를 쓰게 한다. --- site/src/routes/docs/MarkdownViewer.tsx | 5 +++++ site/src/routes/widgets/DocumentWidgetRoute.tsx | 12 +++++++++++- site/src/routes/widgets/GridWidgetRoute.tsx | 14 ++++++++++++-- site/src/routes/widgets/ListboxWidgetRoute.tsx | 2 +- site/tests/browser/widgets.spec.ts | 5 +++++ 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/site/src/routes/docs/MarkdownViewer.tsx b/site/src/routes/docs/MarkdownViewer.tsx index 50ecb9c2..6affea1d 100644 --- a/site/src/routes/docs/MarkdownViewer.tsx +++ b/site/src/routes/docs/MarkdownViewer.tsx @@ -95,6 +95,11 @@ const markdownHrefs: Readonly> = { "connectors.md": "/docs/connectors", "react-editing.md": "/docs/react-editing", "adapters.md": "/docs/adapters", + "affordance.md": "/docs/affordance", + "affordance-select.md": "/docs/affordance/select", + "affordance-fold.md": "/docs/affordance/fold", + "affordance-drag.md": "/docs/affordance/drag", + "affordance-history.md": "/docs/affordance/history", "collaboration.md": "/docs/collaboration", "collaboration-replica.md": "/docs/collaboration/replica", "collaboration-history.md": "/docs/collaboration/history", diff --git a/site/src/routes/widgets/DocumentWidgetRoute.tsx b/site/src/routes/widgets/DocumentWidgetRoute.tsx index ebb98a26..853f0773 100644 --- a/site/src/routes/widgets/DocumentWidgetRoute.tsx +++ b/site/src/routes/widgets/DocumentWidgetRoute.tsx @@ -4,6 +4,7 @@ import { useEditing } from "@interactive-os/json-document-react"; import { lineBoundary, moveLinePoint } from "@interactive-os/json-document-web"; import { SelectableItem } from "../../shared/ui/interactive"; import { classes, ui } from "../../shared/ui/styles"; +import { pointerSelect, resolveAffordanceKey } from "@interactive-os/json-document-affordance"; import { optionProps, useWidgetKeyboard } from "../../shared/widget-binding"; import { WidgetDemoFrame } from "./WidgetDemoFrame"; @@ -27,8 +28,17 @@ export function DocumentWidgetRoute() { onSelect: (blockId, mode) => { editor.dispatch({ type: "selection.set", blockId, mode }); }, + operationFromEvent: (event) => pointerSelect({ + shiftKey: event.shiftKey ?? false, + metaKey: event.metaKey ?? false, + ctrlKey: event.ctrlKey ?? false, + }), keyboard: { - resolve: (stroke) => keyboard.resolve(stroke), + resolve: (stroke) => { + const command = resolveAffordanceKey(stroke); + keyboard.resolve(stroke); + return command; + }, focusKey: () => editor.selectedBlockIds.at(-1), neighbor: (key, command) => { const ids = (editor.snapshot.value as BlockDocument).blocks.map((block) => block.id); diff --git a/site/src/routes/widgets/GridWidgetRoute.tsx b/site/src/routes/widgets/GridWidgetRoute.tsx index e2334a69..7439fe71 100644 --- a/site/src/routes/widgets/GridWidgetRoute.tsx +++ b/site/src/routes/widgets/GridWidgetRoute.tsx @@ -4,6 +4,7 @@ import { useEditing } from "@interactive-os/json-document-react"; import { gridBoundary, moveGridPoint } from "@interactive-os/json-document-web"; import { SelectableItem } from "../../shared/ui/interactive"; import { classes, ui } from "../../shared/ui/styles"; +import { pointerSelect, resolveAffordanceKey } from "@interactive-os/json-document-affordance"; import { gridCellProps, useWidgetKeyboard } from "../../shared/widget-binding"; import { WidgetDemoFrame } from "./WidgetDemoFrame"; @@ -31,8 +32,17 @@ export function GridWidgetRoute() { const { rowId, columnId } = parseCellKey(key); editor.dispatch({ type: "selection.set", rowId, columnId, mode }); }, + operationFromEvent: (event) => pointerSelect({ + shiftKey: event.shiftKey ?? false, + metaKey: event.metaKey ?? false, + ctrlKey: event.ctrlKey ?? false, + }), keyboard: { - resolve: (stroke) => keyboard.resolve(stroke), + resolve: (stroke) => { + const command = resolveAffordanceKey(stroke); + keyboard.resolve(stroke); + return command; + }, focusKey: () => { const next = editor.snapshot.selection.focus; return next ? cellKey(next.rowId, next.columnId) : undefined; @@ -69,7 +79,7 @@ export function GridWidgetRoute() { return ( { await expect(navigation.getByRole("link", { name: "접기" })).toHaveAttribute("href", "/docs/affordance/fold"); await expect(navigation.getByRole("link", { name: "드래그" })).toHaveAttribute("href", "/docs/affordance/drag"); await expect(navigation.getByRole("link", { name: "되돌리기" })).toHaveAttribute("href", "/docs/affordance/history"); + const content = page.getByRole("main"); + await expect(content.getByRole("link", { name: "고르기" })).toHaveAttribute("href", "/docs/affordance/select"); + await expect(content.getByRole("link", { name: "접기" })).toHaveAttribute("href", "/docs/affordance/fold"); + await expect(content.getByRole("link", { name: "드래그" })).toHaveAttribute("href", "/docs/affordance/drag"); + await expect(content.getByRole("link", { name: "되돌리기" })).toHaveAttribute("href", "/docs/affordance/history"); }); test("Toolbar binds Undo and Redo to canUndo and canRedo", async ({ page }) => { From b5358dfcf44f4668e1193b3dabd1560879074bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Thu, 20 Aug 2026 21:41:50 +0900 Subject: [PATCH 03/14] =?UTF-8?q?=EC=96=B4=ED=8F=AC=EB=8D=98=EC=8A=A4:=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=20=EA=B7=B8=EB=A3=B9=EC=9D=84=20Eng?= =?UTF-8?q?lish=EB=A1=9C=20=EB=91=90=EA=B3=A0=20=ED=82=A4=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=C2=B7=EB=A7=88=EC=9A=B0=EC=8A=A4=C2=B7=EC=BB=A4?= =?UTF-8?q?=EC=84=9C=20TBD=EB=A5=BC=20=EC=B1=84=EC=9A=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CSS UI 커서 집합, Pointer Events, APG 키보드 관례로 손을 닫고, 구현 없는 손은 사용법 페이지로만 남긴다. --- docs/README.md | 25 +- docs/evaluate.mjs | 42 ++ docs/public/affordance-activate.md | 19 + docs/public/affordance-cancel.md | 18 + docs/public/affordance-caret.md | 21 + docs/public/affordance-context-menu.md | 20 + docs/public/affordance-copy-drag.md | 19 + docs/public/affordance-delete.md | 23 + docs/public/affordance-double-click.md | 18 + docs/public/affordance-drag.md | 7 + docs/public/affordance-drop.md | 19 + docs/public/affordance-focus.md | 21 + docs/public/affordance-fold.md | 6 + docs/public/affordance-forbid.md | 19 + docs/public/affordance-history.md | 6 + docs/public/affordance-hover.md | 21 + docs/public/affordance-marquee.md | 21 + docs/public/affordance-nudge.md | 20 + docs/public/affordance-pan.md | 20 + docs/public/affordance-rename.md | 19 + docs/public/affordance-resize.md | 22 + docs/public/affordance-scroll.md | 19 + docs/public/affordance-select.md | 9 + docs/public/affordance-snap.md | 20 + docs/public/affordance-triple-click.md | 16 + docs/public/affordance-typeahead.md | 19 + docs/public/affordance-zoom.md | 19 + docs/public/affordance.md | 84 +++- site/scripts/route-checks.mjs | 2 +- site/site-routes.json | 248 +++++++++- site/src/app/page-descriptors.ts | 2 +- site/src/app/routeTree.gen.ts | 454 ++++++++++++++++++ .../routes/_page/docs/affordance/activate.tsx | 8 + .../routes/_page/docs/affordance/cancel.tsx | 8 + .../routes/_page/docs/affordance/caret.tsx | 8 + .../_page/docs/affordance/context-menu.tsx | 8 + .../_page/docs/affordance/copy-drag.tsx | 8 + .../routes/_page/docs/affordance/delete.tsx | 8 + .../_page/docs/affordance/double-click.tsx | 8 + .../app/routes/_page/docs/affordance/drop.tsx | 8 + .../routes/_page/docs/affordance/focus.tsx | 8 + .../routes/_page/docs/affordance/forbid.tsx | 8 + .../routes/_page/docs/affordance/hover.tsx | 8 + .../routes/_page/docs/affordance/marquee.tsx | 8 + .../routes/_page/docs/affordance/nudge.tsx | 8 + .../app/routes/_page/docs/affordance/pan.tsx | 8 + .../routes/_page/docs/affordance/rename.tsx | 8 + .../routes/_page/docs/affordance/resize.tsx | 8 + .../routes/_page/docs/affordance/scroll.tsx | 8 + .../app/routes/_page/docs/affordance/snap.tsx | 8 + .../_page/docs/affordance/triple-click.tsx | 8 + .../_page/docs/affordance/typeahead.tsx | 8 + .../app/routes/_page/docs/affordance/zoom.tsx | 8 + site/src/app/site-layers.ts | 2 +- site/src/routes/docs/DocsRoute.tsx | 21 + site/src/routes/docs/MarkdownViewer.tsx | 21 + site/src/routes/docs/doc-pages.ts | 42 ++ site/src/routes/home/HomeRoute.tsx | 4 +- site/tests/browser/site-shell.spec.ts | 33 +- site/tests/browser/widgets.spec.ts | 18 +- site/tests/unit/app-shell.test.tsx | 34 +- site/tests/unit/breadcrumb.test.ts | 6 +- 62 files changed, 1596 insertions(+), 51 deletions(-) create mode 100644 docs/public/affordance-activate.md create mode 100644 docs/public/affordance-cancel.md create mode 100644 docs/public/affordance-caret.md create mode 100644 docs/public/affordance-context-menu.md create mode 100644 docs/public/affordance-copy-drag.md create mode 100644 docs/public/affordance-delete.md create mode 100644 docs/public/affordance-double-click.md create mode 100644 docs/public/affordance-drop.md create mode 100644 docs/public/affordance-focus.md create mode 100644 docs/public/affordance-forbid.md create mode 100644 docs/public/affordance-hover.md create mode 100644 docs/public/affordance-marquee.md create mode 100644 docs/public/affordance-nudge.md create mode 100644 docs/public/affordance-pan.md create mode 100644 docs/public/affordance-rename.md create mode 100644 docs/public/affordance-resize.md create mode 100644 docs/public/affordance-scroll.md create mode 100644 docs/public/affordance-snap.md create mode 100644 docs/public/affordance-triple-click.md create mode 100644 docs/public/affordance-typeahead.md create mode 100644 docs/public/affordance-zoom.md create mode 100644 site/src/app/routes/_page/docs/affordance/activate.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/cancel.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/caret.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/context-menu.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/copy-drag.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/delete.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/double-click.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/drop.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/focus.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/forbid.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/hover.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/marquee.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/nudge.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/pan.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/rename.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/resize.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/scroll.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/snap.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/triple-click.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/typeahead.tsx create mode 100644 site/src/app/routes/_page/docs/affordance/zoom.tsx diff --git a/docs/README.md b/docs/README.md index ddc89762..c9af368b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -76,11 +76,32 @@ Connector |-- Zod `-- TanStack Table -어포던스 +Affordance +|-- 초점 +|-- 캐럿 |-- 고르기 +|-- 찾아 점프 +|-- 활성화 +|-- 취소 |-- 접기 +|-- 되돌리기 +|-- 지우기 +|-- 이름 바꾸기 +|-- 살짝 밀기 +|-- 가리키기 +|-- 두 번 누르기 +|-- 세 번 누르기 +|-- 차 메뉴 |-- 드래그 -`-- 되돌리기 +|-- 쓸어 담기 +|-- 놓기 +|-- 복사해서 옮기기 +|-- 크기 바꾸기 +|-- 밀기 +|-- 굴리기 +|-- 확대 +|-- 붙이기 +`-- 금지 ``` ## 규범 우선순위 diff --git a/docs/evaluate.mjs b/docs/evaluate.mjs index c7c76440..5ef47556 100644 --- a/docs/evaluate.mjs +++ b/docs/evaluate.mjs @@ -56,6 +56,27 @@ const publicDocs = { 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"), @@ -111,10 +132,31 @@ 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", diff --git a/docs/public/affordance-activate.md b/docs/public/affordance-activate.md new file mode 100644 index 00000000..53968aa6 --- /dev/null +++ b/docs/public/affordance-activate.md @@ -0,0 +1,19 @@ +# 활성화 + +TBD. + +활성화는 고른 대상의 기본 동작을 실행하는 손입니다. `pointer` 커서가 이 +손을 가리킵니다. 클릭과 Enter·Space는 같은 손입니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- 기본 클릭 +- Enter / Space +- UIEvent.detail이 1일 때의 click + +호스트는 기본 동작이 무엇인지 정합니다. 어포던스는 그 동작을 부르는 손을 닫습니다. + +근거: [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..fb1d3e40 --- /dev/null +++ b/docs/public/affordance-cancel.md @@ -0,0 +1,18 @@ +# 취소 + +TBD. + +취소는 진행 중인 손과 임시 UI를 버리는 손입니다. Escape는 메뉴·대화·드래그를 +닫고, `pointercancel`은 포인터 제스처를 중단합니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- Escape: 메뉴, 대화, 드래그, 이름 바꾸기 중단 +- 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..ce53ecde --- /dev/null +++ b/docs/public/affordance-caret.md @@ -0,0 +1,21 @@ +# 캐럿 + +TBD. + +캐럿은 글 안의 삽입점입니다. 항목 [고르기](affordance-select.md)와 다릅니다. +`text` / `vertical-text` 커서가 이 손을 가리킵니다. 클릭은 삽입점을 두고, +드래그는 글 범위를 고릅니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- 클릭: 삽입점 +- 드래그: 글 범위 +- 화살표 / Home / End: 글자·줄 끝 +- IME composition은 삽입 경로. 값 편집 자체는 Hands + +호스트는 I-beam과 글 기하를 그립니다. 어포던스는 삽입점과 글 범위 손을 닫습니다. + +근거: [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..07c1a06e --- /dev/null +++ b/docs/public/affordance-context-menu.md @@ -0,0 +1,20 @@ +# 차 메뉴 + +TBD. + +차 메뉴는 대상이 있는 자리에서 보조 동작을 여는 손입니다. +`context-menu` 커서가 이 손을 가리킵니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- 보조 버튼 / `contextmenu` / `auxclick` +- Shift+F10 +- Menu 키 +- 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..a36a6d0d --- /dev/null +++ b/docs/public/affordance-copy-drag.md @@ -0,0 +1,19 @@ +# 복사해서 옮기기 + +TBD. + +복사해서 옮기기는 수정 키를 누른 채 드래그하면 원본을 복제하는 손입니다. +`copy` / `alias` 커서가 이 손을 가리킵니다. 터치와 포인터에서 같은 +수정 키 결과여야 합니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- Alt/Option + 드래그 → 복제 +- 커서 `copy` 또는 `alias` + +값의 클립보드 복사/붙이기는 Hands입니다. 이 손은 포인터 복제입니다. + +근거: [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..a78d75f2 --- /dev/null +++ b/docs/public/affordance-delete.md @@ -0,0 +1,23 @@ +# 지우기 + +TBD. + +지우기는 고른 대상을 없애는 손입니다. `resolveAffordanceKey`는 이미 +Delete를 `delete` command로 닫습니다. Backspace와의 차이, 칸 비우기와 +항목 제거의 차이는 아직 닫히지 않았습니다. + +```ts +import { resolveAffordanceKey } from "@interactive-os/json-document-affordance"; + +resolveAffordanceKey({ key: "Delete", shiftKey: false, metaKey: false, ctrlKey: false }); +// { type: "delete" } +``` + +남은 TBD: +- Backspace와 Delete +- 칸 비우기 vs 항목 제거 (Sheet vs Order) +- 지울 것이 없을 때 + +호스트는 장르 Intent만 가집니다. 값 삽입은 Hands입니다. + +근거: 이미 닫힌 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..eb6764ec --- /dev/null +++ b/docs/public/affordance-double-click.md @@ -0,0 +1,18 @@ +# 두 번 누르기 + +TBD. + +두 번 누르기는 `UIEvent.detail === 2`입니다. 글에서는 단어를 고르고, +목록·파일에서는 열거나 [이름 바꾸기](affordance-rename.md)로 갈 수 있습니다. +장르 Intent가 뜻을 정하고, 횟수 손 자체는 닫혀 있어야 합니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- click / dblclick, detail 2 +- 글: 단어 범위 +- 항목: 열기 또는 이름 바꾸기 (장르 Intent) + +근거: [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 index d35bd04f..601e4f6e 100644 --- a/docs/public/affordance-drag.md +++ b/docs/public/affordance-drag.md @@ -22,3 +22,10 @@ if (dragShouldCommit(offset)) { ``` 모양과 그림은 제품이 정합니다. 고른 것을 잡고 옮기는 문법은 닫혀 있습니다. + +## TBD + +- [놓기](affordance-drop.md) 대상과 `no-drop` +- [복사해서 옮기기](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..5ed89a3f --- /dev/null +++ b/docs/public/affordance-drop.md @@ -0,0 +1,19 @@ +# 놓기 + +TBD. + +놓기는 [드래그](affordance-drag.md)의 끝입니다. 어디에 둘 수 있는지를 +커서가 말하고, 못 두는 자리는 `no-drop`입니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- drop 대상 위: `move` / `copy` / `alias` +- 불가능한 자리: `no-drop` +- 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..479ab3be --- /dev/null +++ b/docs/public/affordance-focus.md @@ -0,0 +1,21 @@ +# 초점 + +TBD. + +초점은 키보드의 포인터입니다. Tab과 Shift+Tab은 컴포넌트 사이로 옮기고, +화살표·Home·End는 컴포넌트 안에서 옮깁니다. 초점과 [고르기](affordance-select.md)는 +다릅니다. 초점 표시는 항상 보여야 하고, 선택 표시와 겹치면 안 됩니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- 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 index e01c925a..ca841f7e 100644 --- a/docs/public/affordance-fold.md +++ b/docs/public/affordance-fold.md @@ -23,3 +23,9 @@ treeAffordance( 접힘 상태는 호스트가 가진 화면 상태입니다. editor는 보이는 줄 Topology만 받습니다. 호스트는 `expand` / `collapse`를 접힘 집합에 적용하고, `move`만 Topology 이웃으로 보냅니다. + +## TBD + +- 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..e62e01cd --- /dev/null +++ b/docs/public/affordance-forbid.md @@ -0,0 +1,19 @@ +# 금지 + +TBD. + +금지는 지금 그 손을 쓸 수 없음을 커서로 말하는 손입니다. 비활성 대상은 +`not-allowed`, 놓을 수 없는 자리는 `no-drop`입니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- `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 index ba453b60..0079ea65 100644 --- a/docs/public/affordance-history.md +++ b/docs/public/affordance-history.md @@ -14,3 +14,9 @@ commands.redo.disabled; // true `resolveAffordanceKey`가 `undo` / `redo` command를 내면 호스트는 `editor.undo()` / `editor.redo()`만 호출합니다. History 기록 자체는 Editing이 가지고, 어포던스는 그 손을 닫습니다. + +## TBD + +- 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..11e9bd11 --- /dev/null +++ b/docs/public/affordance-hover.md @@ -0,0 +1,21 @@ +# 가리키기 + +TBD. + +가리키기는 누르지 않은 포인터가 대상 위에 있을 때 가능한 손을 드러냅니다. +커서가 바뀌고, 잠시 뒤 툴팁이 열릴 수 있습니다. `help` 커서는 도움말이 +있음을 가리킵니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- pointerenter / pointerleave +- 툴팁 지연 +- 커서 교체 (이 위치에서 가능한 손) +- `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..34446efa --- /dev/null +++ b/docs/public/affordance-marquee.md @@ -0,0 +1,21 @@ +# 쓸어 담기 + +TBD. + +쓸어 담기는 빈 평면에서 사각형(또는 올가미)을 끌어 여러 대상을 집는 +손입니다. 고른 대상을 옮기는 [드래그](affordance-drag.md)와 다릅니다. +`crosshair` / `cell` 커서가 이 손을 가리킬 수 있습니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- 빈 곳에서 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..b7b0ce5a --- /dev/null +++ b/docs/public/affordance-nudge.md @@ -0,0 +1,20 @@ +# 살짝 밀기 + +TBD. + +살짝 밀기는 고른 대상을 키보드로 조금 옮기는 손입니다. 화살표는 한 단위, +Shift+화살표는 큰 단위입니다. 키를 누르면 반복됩니다. 항목 이웃으로 초점을 +옮기는 [고르기](affordance-select.md)와 다릅니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- Arrow: 1 단위 +- Shift+Arrow: 큰 단위 +- 키 반복 + +호스트는 단위(px, 칸, 그리드)를 정합니다. 어포던스는 손과 수정 키를 닫습니다. + +근거: 캔버스·슬라이드 편집기 관례 (Object 장르 Intent가 단위를 가짐) diff --git a/docs/public/affordance-pan.md b/docs/public/affordance-pan.md new file mode 100644 index 00000000..b0339e6d --- /dev/null +++ b/docs/public/affordance-pan.md @@ -0,0 +1,20 @@ +# 밀기 + +TBD. + +밀기는 대상을 옮기지 않고 보이는 평면을 옮기는 손입니다. `grab` / +`grabbing` / `all-scroll` 커서가 이 손을 가리킵니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- 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..78aa9cbf --- /dev/null +++ b/docs/public/affordance-rename.md @@ -0,0 +1,19 @@ +# 이름 바꾸기 + +TBD. + +이름 바꾸기는 고른 대상의 레이블을 고치는 손입니다. F2, Enter(어떤 목록), +느린 두 번 누르기가 같은 손을 엽니다. Escape는 [취소](affordance-cancel.md)입니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- F2 +- 느린 두 번 누르기 (빠른 두 번은 [두 번 누르기](affordance-double-click.md)) +- Enter로 확정, Escape로 취소 + +호스트는 레이블 필드를 그립니다. 글 편집 자체는 Hands·캐럿입니다. + +근거: Finder/Explorer/VS Code 관례, [UI Events click 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..6bb1a739 --- /dev/null +++ b/docs/public/affordance-resize.md @@ -0,0 +1,22 @@ +# 크기 바꾸기 + +TBD. + +크기 바꾸기는 가장자리·모서리·칸 경계·창 분할선을 움직이는 손입니다. +CSS predefined 리사이즈 커서가 이 손을 닫습니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- 모서리: `n-resize` … `nwse-resize` +- 칸/행: `col-resize` / `row-resize` +- 분할선: 화살표로 이동, Enter로 접기 (APG Window Splitter) +- Shift: 비율 고정 (관례) +- Alt: 가운데 기준 (관례) +- CSS `resize` 속성: 스크롤 상자의 사용자 리사이즈 + +호스트는 핸들과 기하를 그립니다. 어포던스는 손과 커서를 닫습니다. + +근거: [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..be281f86 --- /dev/null +++ b/docs/public/affordance-scroll.md @@ -0,0 +1,19 @@ +# 굴리기 + +TBD. + +굴리기는 휠로 보이는 내용을 옮기는 손입니다. 드래그 중 가장자리에 닿으면 +자동으로 더 굴러갑니다. [확대](affordance-zoom.md)의 Mod+휠과 구분합니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- wheel (수정 키 없음) +- 드래그 중 가장자리 autoscroll +- 초점 대상이 보이도록 scroll-into-view + +호스트는 overflow를 그립니다. 어포던스는 휠과 자동 굴리기를 닫습니다. + +근거: [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 index 8e47b20f..e3229a58 100644 --- a/docs/public/affordance-select.md +++ b/docs/public/affordance-select.md @@ -28,3 +28,12 @@ React에서 범위와 커서를 그리려면 `useEditing`의 `onSelect`에 Keyboard Adapter는 chord를 command로 바꿉니다. 고르기는 그 command가 replace인지 extend인지를 닫습니다. + +## TBD + +- Mod+A 모두 고르기 토글 +- Home / End / PageUp / PageDown +- 초점 따라 선택 vs 초점만 이동 +- 글 단어·줄 범위는 [두 번 누르기](affordance-double-click.md)· + [세 번 누르기](affordance-triple-click.md)·[캐럿](affordance-caret.md) +- 빈 평면의 여러 대상은 [쓸어 담기](affordance-marquee.md) diff --git a/docs/public/affordance-snap.md b/docs/public/affordance-snap.md new file mode 100644 index 00000000..77690cdf --- /dev/null +++ b/docs/public/affordance-snap.md @@ -0,0 +1,20 @@ +# 붙이기 + +TBD. + +붙이기는 [드래그](affordance-drag.md)·[크기 바꾸기](affordance-resize.md)· +[살짝 밀기](affordance-nudge.md) 중에 그리드나 가이드에 붙는 손입니다. +수정 키를 누르면 붙지 않습니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- 이동·리사이즈 중 스냅 +- 수정 키로 스냅 해제 +- 회전 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..7a66318c --- /dev/null +++ b/docs/public/affordance-triple-click.md @@ -0,0 +1,16 @@ +# 세 번 누르기 + +TBD. + +세 번 누르기는 `UIEvent.detail === 3`입니다. 글에서는 줄 또는 문단을 +고릅니다. 항목 목록에서는 보통 쓰지 않습니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- 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..ecefbc0e --- /dev/null +++ b/docs/public/affordance-typeahead.md @@ -0,0 +1,19 @@ +# 찾아 점프 + +TBD. + +찾아 점프는 인쇄 글쇠로 목록·나무에서 이름으로 건너뛰는 손입니다. 한 글자는 +그 글자로 시작하는 다음 항목, 빠르게 이어 치면 그 문자열로 시작합니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- 한 문자: 그 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..b25d163f --- /dev/null +++ b/docs/public/affordance-zoom.md @@ -0,0 +1,19 @@ +# 확대 + +TBD. + +확대는 보이는 배율을 바꾸는 손입니다. `zoom-in` / `zoom-out` 커서가 +이 손을 가리킵니다. CSS `resize`와 무관합니다. + +```ts +import { /* TBD */ } from "@interactive-os/json-document-affordance"; +``` + +닫는 손: +- 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 index abebd43f..626ab6e9 100644 --- a/docs/public/affordance.md +++ b/docs/public/affordance.md @@ -2,12 +2,11 @@ 제품의 화면은 호스트가 그립니다. json-document가 최전선에서 주는 것은 리스트박스나 나무가 아니라, 30년 동안 같은 손으로 학습된 키보드와 -마우스 행동입니다. +마우스와 커서입니다. -`@interactive-os/json-document-affordance`가 그 계약을 닫습니다. Shift는 -범위를 늘리고, Mod는 토글하고, 나무는 왼쪽이 접힘·오른쪽이 펼침이며, -Delete는 고른 것을 지우고, Mod+Z는 되돌립니다. 단축키를 제품마다 바꾸지 -않습니다. +`@interactive-os/json-document-affordance`가 그 계약을 닫습니다. 단축키를 +제품마다 바꾸지 않습니다. 구현이 없는 손은 TBD로 남기고, 화면 위젯으로 +대체하지 않습니다. ```sh npm i @interactive-os/json-document-affordance @@ -17,6 +16,18 @@ 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)가 + 지금 이 자리에서 가능한 손을 키워드 집합으로 닫는다. + +## 이미 닫힌 손 + | 어포던스 | API | 손 | | --- | --- | --- | | [고르기](affordance-select.md) | `pointerSelect`, `resolveAffordanceKey` | 클릭, Shift 범위, Mod 토글, 화살표 | @@ -24,5 +35,66 @@ Editing은 선택과 작업을 기억합니다. Adapter는 키 chord를 command | [드래그](affordance-drag.md) | `dragOffset`, `dragShouldCommit` | 고른 대상을 포인터로 옮김 | | [되돌리기](affordance-history.md) | `historyAffordance` | Mod+Z, Mod+Shift+Z | -라이브 화면은 이 손을 호스트 그림에 붙인 증명입니다. 위젯을 가져가는 +## 키보드 TBD + +| 어포던스 | API | 손 | +| --- | --- | --- | +| [초점](affordance-focus.md) | TBD | Tab 사이, 화살표 안, 초점 ≠ 선택 | +| [캐럿](affordance-caret.md) | TBD | I-beam 삽입점, 글 범위 | +| [찾아 점프](affordance-typeahead.md) | TBD | 인쇄 글쇠 prefix 점프 | +| [활성화](affordance-activate.md) | TBD | Enter, Space, 기본 클릭 | +| [취소](affordance-cancel.md) | TBD | Escape, pointercancel | +| [지우기](affordance-delete.md) | TBD | Delete, Backspace. Delete chord는 이미 닫힘 | +| [이름 바꾸기](affordance-rename.md) | TBD | F2, 느린 두 번 누르기 | +| [살짝 밀기](affordance-nudge.md) | TBD | 화살표 한 단위, Shift 큰 단위 | + +## 마우스 TBD + +| 어포던스 | API | 손 | +| --- | --- | --- | +| [가리키기](affordance-hover.md) | TBD | hover, 툴팁 지연, 커서 교체 | +| [두 번 누르기](affordance-double-click.md) | TBD | `detail` 2 | +| [세 번 누르기](affordance-triple-click.md) | TBD | `detail` 3 | +| [차 메뉴](affordance-context-menu.md) | TBD | 오른쪽 클릭, Shift+F10, Menu | +| [쓸어 담기](affordance-marquee.md) | TBD | 빈 곳에서 사각형으로 여러 대상 | +| [놓기](affordance-drop.md) | TBD | drop 대상, no-drop | +| [복사해서 옮기기](affordance-copy-drag.md) | TBD | Alt/Option 드래그 복제 | +| [크기 바꾸기](affordance-resize.md) | TBD | 모서리, 칸, 분할선 | +| [밀기](affordance-pan.md) | TBD | Space+드래그, grab | +| [굴리기](affordance-scroll.md) | TBD | wheel, autoscroll | +| [확대](affordance-zoom.md) | TBD | Mod+휠, +/− | +| [붙이기](affordance-snap.md) | TBD | 그리드·가이드, 수정 키로 해제 | +| [금지](affordance-forbid.md) | TBD | not-allowed, no-drop | + +## 커서가 닫는 손 + +CSS UI 4 ``는 이 집합이 끝입니다. 새 커서 이름을 +만들지 않습니다. + +| 커서 | 손 | +| --- | --- | +| `auto`, `default`, `none` | 호스트·UA 기본. 이 층이 새 문법을 열지 않음 | +| `pointer` | [활성화](affordance-activate.md) | +| `text`, `vertical-text` | [캐럿](affordance-caret.md) | +| `cell`, `crosshair` | [쓸어 담기](affordance-marquee.md), [고르기](affordance-select.md) | +| `context-menu` | [차 메뉴](affordance-context-menu.md) | +| `help` | [가리키기](affordance-hover.md) | +| `move`, `grab`, `grabbing` | [드래그](affordance-drag.md), [밀기](affordance-pan.md) | +| `copy`, `alias` | [복사해서 옮기기](affordance-copy-drag.md) | +| `no-drop`, `not-allowed` | [금지](affordance-forbid.md), [놓기](affordance-drop.md) | +| `n-resize` … `nwse-resize`, `col-resize`, `row-resize` | [크기 바꾸기](affordance-resize.md) | +| `all-scroll` | [밀기](affordance-pan.md) | +| `zoom-in`, `zoom-out` | [확대](affordance-zoom.md) | +| `progress`, `wait` | UA 바쁨. 편집 손이 아님 | + +## 이 층이 아닌 것 + +- 값 삽입, IME로 글자를 쓰는 일, 클립보드로 값을 옮기는 완전한 편집기는 + Hands입니다. +- chord를 command로 번역하는 일은 Adapter입니다. +- 구독과 질의는 Connector입니다. +- 마크업, ARIA role, 히트 테스트, 기하, 장르 Intent는 호스트입니다. +- 터치·펜·눈은 이 패키지가 닫는 손이 아닙니다. + +라이브 화면은 닫힌 손을 호스트 그림에 붙인 증명입니다. 위젯을 가져가는 입구가 아닙니다. diff --git a/site/scripts/route-checks.mjs b/site/scripts/route-checks.mjs index 487e9238..aa3fc7e1 100644 --- a/site/scripts/route-checks.mjs +++ b/site/scripts/route-checks.mjs @@ -7,7 +7,7 @@ const navigationGroups = new Set([ "Adapter", "Connector", "제품 화면", - "어포던스", + "Affordance", ]); export function validateSiteRoutes(routes, fail) { diff --git a/site/site-routes.json b/site/site-routes.json index 69828c63..b4fb684a 100644 --- a/site/site-routes.json +++ b/site/site-routes.json @@ -435,14 +435,34 @@ }, { "path": "/docs/affordance", - "label": "어포던스", + "label": "Affordance", "title": "Affordance - json-document", "heading": "어포던스", - "description": "제품이 json-document를 만지는 최전선의 키보드·마우스 손을 API와 사용법으로 설명합니다.", + "description": "제품이 json-document를 만지는 최전선의 키보드·마우스·커서 손을 API와 사용법으로 설명합니다.", "language": "ko", - "navigationGroup": "어포던스", + "navigationGroup": "Affordance", "sidebar": false }, + { + "path": "/docs/affordance/focus", + "label": "초점", + "title": "초점 - json-document", + "heading": "초점", + "description": "Tab은 컴포넌트 사이로, 화살표는 컴포넌트 안에서 초점을 옮기는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/caret", + "label": "캐럿", + "title": "캐럿 - json-document", + "heading": "캐럿", + "description": "I-beam 클릭으로 글 삽입점을 두고, 화살표로 글자 사이를 옮기는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, { "path": "/docs/affordance/select", "label": "고르기", @@ -450,9 +470,40 @@ "heading": "고르기", "description": "클릭, Shift 범위, Mod 토글, 화살표로 대상을 집는 손을 붙입니다.", "language": "ko", - "navigationGroup": "어포던스", + "navigationGroup": "Affordance", "relatedDemoPath": "/widgets/listbox", - "relatedDemoLabel": "증명 열기" + "relatedDemoLabel": "증명 열기", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/typeahead", + "label": "찾아 점프", + "title": "찾아 점프 - json-document", + "heading": "찾아 점프", + "description": "글쇠를 누르면 그 글자로 시작하는 다음 항목으로 초점을 옮기는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/activate", + "label": "활성화", + "title": "활성화 - json-document", + "heading": "활성화", + "description": "클릭·Enter·Space로 대상의 기본 동작을 실행하는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/cancel", + "label": "취소", + "title": "취소 - json-document", + "heading": "취소", + "description": "Escape와 pointercancel로 진행 중인 손을 버리는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" }, { "path": "/docs/affordance/fold", @@ -461,9 +512,92 @@ "heading": "접기", "description": "나무에서 왼쪽은 접고 오른쪽은 펼치는 손을 붙입니다.", "language": "ko", - "navigationGroup": "어포던스", + "navigationGroup": "Affordance", "relatedDemoPath": "/widgets/tree", - "relatedDemoLabel": "증명 열기" + "relatedDemoLabel": "증명 열기", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/history", + "label": "되돌리기", + "title": "되돌리기 - json-document", + "heading": "되돌리기", + "description": "Mod+Z와 canUndo로 값과 선택을 함께 되돌리는 손을 붙입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "relatedDemoPath": "/widgets/toolbar", + "relatedDemoLabel": "증명 열기", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/delete", + "label": "지우기", + "title": "지우기 - json-document", + "heading": "지우기", + "description": "Delete와 Backspace로 고른 대상을 지우는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/rename", + "label": "이름 바꾸기", + "title": "이름 바꾸기 - json-document", + "heading": "이름 바꾸기", + "description": "F2와 느린 두 번 누르기로 고른 대상의 이름을 고치는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/nudge", + "label": "살짝 밀기", + "title": "살짝 밀기 - json-document", + "heading": "살짝 밀기", + "description": "화살표로 고른 대상을 한 단위 옮기고, Shift로 큰 단위를 쓰는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/hover", + "label": "가리키기", + "title": "가리키기 - json-document", + "heading": "가리키기", + "description": "누르지 않고 포인터를 올려 손과 도움말을 드러내는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/double-click", + "label": "두 번 누르기", + "title": "두 번 누르기 - json-document", + "heading": "두 번 누르기", + "description": "click detail 2로 열기·단어 고르기·이름 바꾸기를 여는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/triple-click", + "label": "세 번 누르기", + "title": "세 번 누르기 - json-document", + "heading": "세 번 누르기", + "description": "click detail 3으로 줄이나 문단을 고르는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/context-menu", + "label": "차 메뉴", + "title": "차 메뉴 - json-document", + "heading": "차 메뉴", + "description": "오른쪽 클릭, Shift+F10, Menu 키로 자리 메뉴를 여는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" }, { "path": "/docs/affordance/drag", @@ -472,20 +606,100 @@ "heading": "드래그", "description": "고른 대상을 포인터로 옮기는 손을 붙입니다.", "language": "ko", - "navigationGroup": "어포던스", + "navigationGroup": "Affordance", "relatedDemoPath": "/widgets/canvas", - "relatedDemoLabel": "증명 열기" + "relatedDemoLabel": "증명 열기", + "parentPath": "/docs/affordance" }, { - "path": "/docs/affordance/history", - "label": "되돌리기", - "title": "되돌리기 - json-document", - "heading": "되돌리기", - "description": "Mod+Z와 canUndo로 값과 선택을 함께 되돌리는 손을 붙입니다.", + "path": "/docs/affordance/marquee", + "label": "쓸어 담기", + "title": "쓸어 담기 - json-document", + "heading": "쓸어 담기", + "description": "빈 곳에서 끌어서 여러 대상을 한 번에 고르는 손입니다.", "language": "ko", - "navigationGroup": "어포던스", - "relatedDemoPath": "/widgets/toolbar", - "relatedDemoLabel": "증명 열기" + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/drop", + "label": "놓기", + "title": "놓기 - json-document", + "heading": "놓기", + "description": "드래그한 대상을 어디에 둘지 정하고, 못 두면 no-drop을 보여주는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/copy-drag", + "label": "복사해서 옮기기", + "title": "복사해서 옮기기 - json-document", + "heading": "복사해서 옮기기", + "description": "Alt/Option을 누른 채 드래그하면 원본을 복제하는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/resize", + "label": "크기 바꾸기", + "title": "크기 바꾸기 - json-document", + "heading": "크기 바꾸기", + "description": "모서리·칸·분할선을 끌어 크기를 바꾸고, 화살표로 미세 조절하는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/pan", + "label": "밀기", + "title": "밀기 - json-document", + "heading": "밀기", + "description": "손바닥 커서로 평면을 밀고, Space+드래그로 화면을 옮기는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/scroll", + "label": "굴리기", + "title": "굴리기 - json-document", + "heading": "굴리기", + "description": "휠과 드래그 중 자동 스크롤로 보이는 줄을 옮기는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/zoom", + "label": "확대", + "title": "확대 - json-document", + "heading": "확대", + "description": "Mod+휠과 +/- 키, zoom-in/out 커서로 보이는 배율을 바꾸는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/snap", + "label": "붙이기", + "title": "붙이기 - json-document", + "heading": "붙이기", + "description": "드래그와 크기 바꾸기 중 그리드·가이드에 붙고, 수정 키로 푸는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" + }, + { + "path": "/docs/affordance/forbid", + "label": "금지", + "title": "금지 - json-document", + "heading": "금지", + "description": "할 수 없는 자리에서 not-allowed와 no-drop 커서를 보여주는 손입니다.", + "language": "ko", + "navigationGroup": "Affordance", + "parentPath": "/docs/affordance" }, { "path": "/widgets/toolbar", diff --git a/site/src/app/page-descriptors.ts b/site/src/app/page-descriptors.ts index e33e0756..6803a9cf 100644 --- a/site/src/app/page-descriptors.ts +++ b/site/src/app/page-descriptors.ts @@ -9,7 +9,7 @@ export type SiteNavigationGroup = | "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 26acb53e..ec4137ab 100644 --- a/site/src/app/routeTree.gen.ts +++ b/site/src/app/routeTree.gen.ts @@ -64,10 +64,31 @@ 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"; @@ -354,28 +375,146 @@ const PageDocsAffordanceIndexRoute = PageDocsAffordanceIndexRouteImport.update({ 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/", @@ -466,10 +605,31 @@ export interface FileRoutesByFullPath { "/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; @@ -532,10 +692,31 @@ export interface FileRoutesByTo { "/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; @@ -600,10 +781,31 @@ export interface FileRoutesById { "/_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; @@ -668,10 +870,31 @@ export interface FileRouteTypes { | "/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" @@ -734,10 +957,31 @@ export interface FileRouteTypes { | "/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" @@ -801,10 +1045,31 @@ export interface FileRouteTypes { | "/_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" @@ -1207,6 +1472,55 @@ declare module "@tanstack/react-router" { 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"; @@ -1214,6 +1528,20 @@ declare module "@tanstack/react-router" { 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"; @@ -1221,6 +1549,13 @@ declare module "@tanstack/react-router" { 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"; @@ -1228,6 +1563,55 @@ declare module "@tanstack/react-router" { 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"; @@ -1235,6 +1619,34 @@ declare module "@tanstack/react-router" { 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"; @@ -1332,10 +1744,31 @@ interface PageRouteChildren { 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; @@ -1398,10 +1831,31 @@ const PageRouteChildren: PageRouteChildren = { 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, 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/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/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/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/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/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/site-layers.ts b/site/src/app/site-layers.ts index c2b88031..af94263c 100644 --- a/site/src/app/site-layers.ts +++ b/site/src/app/site-layers.ts @@ -14,7 +14,7 @@ export const siteLayers: ReadonlyArray = [ { 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: "/docs/affordance", label: "어포던스", blurb: "고르기, 접기, 드래그, 되돌리기" }, + { group: "Affordance", path: "/docs/affordance", label: "Affordance", blurb: "Keyboard, mouse, and cursor" }, ]; export const siteLayerGroups: ReadonlyArray = siteLayers.map((layer) => layer.group); diff --git a/site/src/routes/docs/DocsRoute.tsx b/site/src/routes/docs/DocsRoute.tsx index fe86e134..0c7ca675 100644 --- a/site/src/routes/docs/DocsRoute.tsx +++ b/site/src/routes/docs/DocsRoute.tsx @@ -15,6 +15,27 @@ const docIllustrations: Record = { affordanceFold: "branch", affordanceDrag: "peek", affordanceHistory: "clipboard", + affordanceFocus: "cursor", + affordanceCaret: "terminal", + affordanceTypeahead: "braces", + affordanceActivate: "peek", + affordanceCancel: "debug", + affordanceDelete: "patch", + affordanceRename: "terminal", + affordanceNudge: "peek", + affordanceHover: "cursor", + affordanceDoubleClick: "peek", + affordanceTripleClick: "peek", + affordanceContextMenu: "clipboard", + affordanceMarquee: "cursor", + affordanceDrop: "connector", + affordanceCopyDrag: "peek", + affordanceResize: "branch", + affordancePan: "sleep", + affordanceScroll: "sleep", + affordanceZoom: "peek", + affordanceSnap: "braces", + affordanceForbid: "debug", connectors: "connector", reactEditing: "cursor", collaboration: "package", diff --git a/site/src/routes/docs/MarkdownViewer.tsx b/site/src/routes/docs/MarkdownViewer.tsx index 6affea1d..5045a4ef 100644 --- a/site/src/routes/docs/MarkdownViewer.tsx +++ b/site/src/routes/docs/MarkdownViewer.tsx @@ -100,6 +100,27 @@ const markdownHrefs: Readonly> = { "affordance-fold.md": "/docs/affordance/fold", "affordance-drag.md": "/docs/affordance/drag", "affordance-history.md": "/docs/affordance/history", + "affordance-focus.md": "/docs/affordance/focus", + "affordance-caret.md": "/docs/affordance/caret", + "affordance-typeahead.md": "/docs/affordance/typeahead", + "affordance-activate.md": "/docs/affordance/activate", + "affordance-cancel.md": "/docs/affordance/cancel", + "affordance-delete.md": "/docs/affordance/delete", + "affordance-rename.md": "/docs/affordance/rename", + "affordance-nudge.md": "/docs/affordance/nudge", + "affordance-hover.md": "/docs/affordance/hover", + "affordance-double-click.md": "/docs/affordance/double-click", + "affordance-triple-click.md": "/docs/affordance/triple-click", + "affordance-context-menu.md": "/docs/affordance/context-menu", + "affordance-marquee.md": "/docs/affordance/marquee", + "affordance-drop.md": "/docs/affordance/drop", + "affordance-copy-drag.md": "/docs/affordance/copy-drag", + "affordance-resize.md": "/docs/affordance/resize", + "affordance-pan.md": "/docs/affordance/pan", + "affordance-scroll.md": "/docs/affordance/scroll", + "affordance-zoom.md": "/docs/affordance/zoom", + "affordance-snap.md": "/docs/affordance/snap", + "affordance-forbid.md": "/docs/affordance/forbid", "collaboration.md": "/docs/collaboration", "collaboration-replica.md": "/docs/collaboration/replica", "collaboration-history.md": "/docs/collaboration/history", diff --git a/site/src/routes/docs/doc-pages.ts b/site/src/routes/docs/doc-pages.ts index 1b536f88..e6f48d84 100644 --- a/site/src/routes/docs/doc-pages.ts +++ b/site/src/routes/docs/doc-pages.ts @@ -5,6 +5,27 @@ import affordanceDragMarkdown from "../../../../docs/public/affordance-drag.md?r import affordanceFoldMarkdown from "../../../../docs/public/affordance-fold.md?raw"; import affordanceHistoryMarkdown from "../../../../docs/public/affordance-history.md?raw"; import affordanceSelectMarkdown from "../../../../docs/public/affordance-select.md?raw"; +import affordanceFocusMarkdown from "../../../../docs/public/affordance-focus.md?raw"; +import affordanceCaretMarkdown from "../../../../docs/public/affordance-caret.md?raw"; +import affordanceTypeaheadMarkdown from "../../../../docs/public/affordance-typeahead.md?raw"; +import affordanceActivateMarkdown from "../../../../docs/public/affordance-activate.md?raw"; +import affordanceCancelMarkdown from "../../../../docs/public/affordance-cancel.md?raw"; +import affordanceDeleteMarkdown from "../../../../docs/public/affordance-delete.md?raw"; +import affordanceRenameMarkdown from "../../../../docs/public/affordance-rename.md?raw"; +import affordanceNudgeMarkdown from "../../../../docs/public/affordance-nudge.md?raw"; +import affordanceHoverMarkdown from "../../../../docs/public/affordance-hover.md?raw"; +import affordanceDoubleClickMarkdown from "../../../../docs/public/affordance-double-click.md?raw"; +import affordanceTripleClickMarkdown from "../../../../docs/public/affordance-triple-click.md?raw"; +import affordanceContextMenuMarkdown from "../../../../docs/public/affordance-context-menu.md?raw"; +import affordanceMarqueeMarkdown from "../../../../docs/public/affordance-marquee.md?raw"; +import affordanceDropMarkdown from "../../../../docs/public/affordance-drop.md?raw"; +import affordanceCopyDragMarkdown from "../../../../docs/public/affordance-copy-drag.md?raw"; +import affordanceResizeMarkdown from "../../../../docs/public/affordance-resize.md?raw"; +import affordancePanMarkdown from "../../../../docs/public/affordance-pan.md?raw"; +import affordanceScrollMarkdown from "../../../../docs/public/affordance-scroll.md?raw"; +import affordanceZoomMarkdown from "../../../../docs/public/affordance-zoom.md?raw"; +import affordanceSnapMarkdown from "../../../../docs/public/affordance-snap.md?raw"; +import affordanceForbidMarkdown from "../../../../docs/public/affordance-forbid.md?raw"; import adaptersMarkdown from "../../../../docs/public/adapters.md?raw"; import collaborationMarkdown from "../../../../docs/public/collaboration.md?raw"; import collaborationHistoryMarkdown from "../../../../docs/public/collaboration-history.md?raw"; @@ -40,6 +61,27 @@ export const docPages = { affordanceFold: docPage("/docs/affordance/fold", affordanceFoldMarkdown), affordanceDrag: docPage("/docs/affordance/drag", affordanceDragMarkdown), affordanceHistory: docPage("/docs/affordance/history", affordanceHistoryMarkdown), + affordanceFocus: docPage("/docs/affordance/focus", affordanceFocusMarkdown), + affordanceCaret: docPage("/docs/affordance/caret", affordanceCaretMarkdown), + affordanceTypeahead: docPage("/docs/affordance/typeahead", affordanceTypeaheadMarkdown), + affordanceActivate: docPage("/docs/affordance/activate", affordanceActivateMarkdown), + affordanceCancel: docPage("/docs/affordance/cancel", affordanceCancelMarkdown), + affordanceDelete: docPage("/docs/affordance/delete", affordanceDeleteMarkdown), + affordanceRename: docPage("/docs/affordance/rename", affordanceRenameMarkdown), + affordanceNudge: docPage("/docs/affordance/nudge", affordanceNudgeMarkdown), + affordanceHover: docPage("/docs/affordance/hover", affordanceHoverMarkdown), + affordanceDoubleClick: docPage("/docs/affordance/double-click", affordanceDoubleClickMarkdown), + affordanceTripleClick: docPage("/docs/affordance/triple-click", affordanceTripleClickMarkdown), + affordanceContextMenu: docPage("/docs/affordance/context-menu", affordanceContextMenuMarkdown), + affordanceMarquee: docPage("/docs/affordance/marquee", affordanceMarqueeMarkdown), + affordanceDrop: docPage("/docs/affordance/drop", affordanceDropMarkdown), + affordanceCopyDrag: docPage("/docs/affordance/copy-drag", affordanceCopyDragMarkdown), + affordanceResize: docPage("/docs/affordance/resize", affordanceResizeMarkdown), + affordancePan: docPage("/docs/affordance/pan", affordancePanMarkdown), + affordanceScroll: docPage("/docs/affordance/scroll", affordanceScrollMarkdown), + affordanceZoom: docPage("/docs/affordance/zoom", affordanceZoomMarkdown), + affordanceSnap: docPage("/docs/affordance/snap", affordanceSnapMarkdown), + affordanceForbid: docPage("/docs/affordance/forbid", affordanceForbidMarkdown), connectors: docPage("/docs/connectors", connectorsMarkdown), reactEditing: docPage("/docs/react-editing", reactEditingMarkdown), collaboration: docPage("/docs/collaboration", collaborationMarkdown), diff --git a/site/src/routes/home/HomeRoute.tsx b/site/src/routes/home/HomeRoute.tsx index 03898ffb..d49beaf5 100644 --- a/site/src/routes/home/HomeRoute.tsx +++ b/site/src/routes/home/HomeRoute.tsx @@ -63,8 +63,8 @@ export function HomeRoute() { Library ecosystems
  • - 어포던스 - 고르기, 접기, 드래그, 되돌리기 + Affordance + Keyboard, mouse, and cursor
  • diff --git a/site/tests/browser/site-shell.spec.ts b/site/tests/browser/site-shell.spec.ts index 8bf2ada7..0034e4bf 100644 --- a/site/tests/browser/site-shell.spec.ts +++ b/site/tests/browser/site-shell.spec.ts @@ -18,7 +18,7 @@ test("official overview exposes the product hierarchy", async ({ page }) => { "Hands", "Adapter", "Connector", - "어포던스", + "Affordance", ]); await expect(navigation.getByRole("link", { name: "Why" })).toHaveCount(0); await expect(navigation.getByRole("link", { name: "Replica" })).toHaveCount(0); @@ -60,12 +60,33 @@ test("official overview exposes the product hierarchy", async ({ page }) => { await expect(navigation.getByRole("group", { name: "Adapter" }).getByRole("link")).toHaveText(["Keyboard", "Clipboard adapter", "Contenteditable"]); await navigation.getByRole("button", { name: "Connector" }).click(); await expect(navigation.getByRole("group", { name: "Connector" }).getByRole("link")).toHaveText(["React", "React Hook Form", "Ajv", "Zod", "TanStack Table"]); - await navigation.getByRole("button", { name: "어포던스" }).click(); - await expect(navigation.getByRole("group", { name: "어포던스" }).getByRole("link")).toHaveText([ + await navigation.getByRole("button", { name: "Affordance" }).click(); + await expect(navigation.getByRole("group", { name: "Affordance" }).getByRole("link")).toHaveText([ + "초점", + "캐럿", "고르기", + "찾아 점프", + "활성화", + "취소", "접기", - "드래그", "되돌리기", + "지우기", + "이름 바꾸기", + "살짝 밀기", + "가리키기", + "두 번 누르기", + "세 번 누르기", + "차 메뉴", + "드래그", + "쓸어 담기", + "놓기", + "복사해서 옮기기", + "크기 바꾸기", + "밀기", + "굴리기", + "확대", + "붙이기", + "금지", ]); await expect(navigation.getByRole("group", { name: "Demos" })).toHaveCount(0); await expect(navigation.getByRole("group", { name: "Reference" })).toHaveCount(0); @@ -76,7 +97,7 @@ test("official overview exposes the product hierarchy", async ({ page }) => { "Hands", "Adapter", "Connector", - "어포던스", + "Affordance", ]); await expect(navigation.getByRole("link", { name: "Extensions" })).toHaveCount(0); expect(requests.some(isLegacyRequest)).toBe(false); @@ -94,7 +115,7 @@ test("mobile navigation preserves the product groups without duplicating documen await expect(siteNavigation.getByRole("group", { name: "Adapter" })).toBeVisible(); await expect(siteNavigation.getByRole("group", { name: "Demos" })).toHaveCount(0); await expect(siteNavigation.getByRole("group", { name: "Connector" })).toBeVisible(); - await expect(siteNavigation.getByRole("group", { name: "어포던스" })).toBeVisible(); + await expect(siteNavigation.getByRole("group", { name: "Affordance" })).toBeVisible(); await expect(siteNavigation.getByRole("group", { name: "Collaboration" })).toBeVisible(); await page.goto("/docs/tutorial"); diff --git a/site/tests/browser/widgets.spec.ts b/site/tests/browser/widgets.spec.ts index f5b662c8..9f716f31 100644 --- a/site/tests/browser/widgets.spec.ts +++ b/site/tests/browser/widgets.spec.ts @@ -5,15 +5,17 @@ test("Widgets catalog redirects to affordance usage", async ({ page }) => { await expect(page).toHaveURL(/\/docs\/affordance$/); await expect(page.getByRole("heading", { level: 1, name: "어포던스" })).toBeVisible(); const navigation = page.getByRole("navigation", { name: "Site navigation" }); - await expect(navigation.getByRole("link", { name: "고르기" })).toHaveAttribute("href", "/docs/affordance/select"); - await expect(navigation.getByRole("link", { name: "접기" })).toHaveAttribute("href", "/docs/affordance/fold"); - await expect(navigation.getByRole("link", { name: "드래그" })).toHaveAttribute("href", "/docs/affordance/drag"); - await expect(navigation.getByRole("link", { name: "되돌리기" })).toHaveAttribute("href", "/docs/affordance/history"); + await expect(navigation.getByRole("link", { name: "고르기", exact: true })).toHaveAttribute("href", "/docs/affordance/select"); + await expect(navigation.getByRole("link", { name: "접기", exact: true })).toHaveAttribute("href", "/docs/affordance/fold"); + await expect(navigation.getByRole("link", { name: "드래그", exact: true })).toHaveAttribute("href", "/docs/affordance/drag"); + await expect(navigation.getByRole("link", { name: "되돌리기", exact: true })).toHaveAttribute("href", "/docs/affordance/history"); const content = page.getByRole("main"); - await expect(content.getByRole("link", { name: "고르기" })).toHaveAttribute("href", "/docs/affordance/select"); - await expect(content.getByRole("link", { name: "접기" })).toHaveAttribute("href", "/docs/affordance/fold"); - await expect(content.getByRole("link", { name: "드래그" })).toHaveAttribute("href", "/docs/affordance/drag"); - await expect(content.getByRole("link", { name: "되돌리기" })).toHaveAttribute("href", "/docs/affordance/history"); + await expect(content.getByRole("link", { name: "고르기" }).first()).toHaveAttribute("href", "/docs/affordance/select"); + await expect(content.getByRole("link", { name: "접기" }).first()).toHaveAttribute("href", "/docs/affordance/fold"); + await expect(content.getByRole("link", { name: "드래그" }).first()).toHaveAttribute("href", "/docs/affordance/drag"); + await expect(content.getByRole("link", { name: "되돌리기" }).first()).toHaveAttribute("href", "/docs/affordance/history"); + await expect(content.getByRole("link", { name: "초점" }).first()).toHaveAttribute("href", "/docs/affordance/focus"); + await expect(content.getByRole("link", { name: "크기 바꾸기" }).first()).toHaveAttribute("href", "/docs/affordance/resize"); }); test("Toolbar binds Undo and Redo to canUndo and canRedo", async ({ page }) => { diff --git a/site/tests/unit/app-shell.test.tsx b/site/tests/unit/app-shell.test.tsx index 9d0d0b8f..cf716c4d 100644 --- a/site/tests/unit/app-shell.test.tsx +++ b/site/tests/unit/app-shell.test.tsx @@ -42,7 +42,7 @@ describe("official site shell", () => { "Hands", "Adapter", "Connector", - "어포던스", + "Affordance", ]); await user.click(nav.getByRole("button", { name: "JSON Document" })); expect(groupLinks(nav, "JSON Document")).toEqual([ @@ -82,8 +82,34 @@ describe("official site shell", () => { expect(groupLinks(nav, "Adapter")).toEqual(["Keyboard", "Clipboard adapter", "Contenteditable"]); await user.click(nav.getByRole("button", { name: "Connector" })); expect(groupLinks(nav, "Connector")).toEqual(["React", "React Hook Form", "Ajv", "Zod", "TanStack Table"]); - await user.click(nav.getByRole("button", { name: "어포던스" })); - expect(groupLinks(nav, "어포던스")).toEqual(["고르기", "접기", "드래그", "되돌리기"]); + await user.click(nav.getByRole("button", { name: "Affordance" })); + expect(groupLinks(nav, "Affordance")).toEqual([ + "초점", + "캐럿", + "고르기", + "찾아 점프", + "활성화", + "취소", + "접기", + "되돌리기", + "지우기", + "이름 바꾸기", + "살짝 밀기", + "가리키기", + "두 번 누르기", + "세 번 누르기", + "차 메뉴", + "드래그", + "쓸어 담기", + "놓기", + "복사해서 옮기기", + "크기 바꾸기", + "밀기", + "굴리기", + "확대", + "붙이기", + "금지", + ]); expect(nav.queryByRole("group", { name: "Demos" })).toBeNull(); expect(nav.queryByRole("group", { name: "Reference" })).toBeNull(); expect(nav.getAllByRole("group").map((group) => group.getAttribute("aria-label"))).toEqual([ @@ -93,7 +119,7 @@ describe("official site shell", () => { "Hands", "Adapter", "Connector", - "어포던스", + "Affordance", ]); expect(nav.queryByRole("link", { name: "Extensions" })).toBeNull(); diff --git a/site/tests/unit/breadcrumb.test.ts b/site/tests/unit/breadcrumb.test.ts index 43ab5fec..4de04d1d 100644 --- a/site/tests/unit/breadcrumb.test.ts +++ b/site/tests/unit/breadcrumb.test.ts @@ -87,15 +87,15 @@ describe("breadcrumbTrail", () => { "Zod:/connectors/zod", "Validate:/connectors/zod/validate", ]); - expect(trail("/docs/affordance")).toEqual(["Overview:/", "어포던스:/docs/affordance"]); + expect(trail("/docs/affordance")).toEqual(["Overview:/", "Affordance:/docs/affordance"]); expect(trail("/docs/affordance/select")).toEqual([ "Overview:/", - "어포던스:/docs/affordance", + "Affordance:/docs/affordance", "고르기:/docs/affordance/select", ]); expect(trail("/widgets/toolbar")).toEqual([ "Overview:/", - "어포던스:/docs/affordance", + "Affordance:/docs/affordance", "되돌리기:/docs/affordance/history", "Toolbar:/widgets/toolbar", ]); From 988498890a3f342d70b6aa491bbb8dc4c8cd3db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Thu, 20 Aug 2026 21:48:08 +0900 Subject: [PATCH 04/14] =?UTF-8?q?=EC=96=B4=ED=8F=AC=EB=8D=98=EC=8A=A4:=20?= =?UTF-8?q?=EC=84=A0=EB=B0=98=20=EC=9D=B4=EB=A6=84=EC=9D=84=20Select,=20Fo?= =?UTF-8?q?cus,=20Marquee=20=EA=B0=99=EC=9D=80=20de=20facto=EB=A1=9C=20?= =?UTF-8?q?=EB=91=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit APG·CSS UI·Pointer Events가 쓰는 말을 그대로 쓰고, URL 슬러그는 유지한다. --- docs/README.md | 50 ++++---- docs/public/affordance-activate.md | 4 +- docs/public/affordance-cancel.md | 4 +- docs/public/affordance-caret.md | 4 +- docs/public/affordance-context-menu.md | 6 +- docs/public/affordance-copy-drag.md | 4 +- docs/public/affordance-delete.md | 4 +- docs/public/affordance-double-click.md | 6 +- docs/public/affordance-drag.md | 8 +- docs/public/affordance-drop.md | 6 +- docs/public/affordance-focus.md | 8 +- docs/public/affordance-fold.md | 4 +- docs/public/affordance-forbid.md | 4 +- docs/public/affordance-history.md | 4 +- docs/public/affordance-hover.md | 4 +- docs/public/affordance-marquee.md | 6 +- docs/public/affordance-nudge.md | 6 +- docs/public/affordance-pan.md | 4 +- docs/public/affordance-rename.md | 8 +- docs/public/affordance-resize.md | 4 +- docs/public/affordance-scroll.md | 6 +- docs/public/affordance-select.md | 16 +-- docs/public/affordance-snap.md | 6 +- docs/public/affordance-triple-click.md | 4 +- docs/public/affordance-typeahead.md | 4 +- docs/public/affordance-zoom.md | 4 +- docs/public/affordance.md | 80 ++++++------- site/site-routes.json | 152 ++++++++++++------------- site/tests/browser/site-shell.spec.ts | 50 ++++---- site/tests/browser/widgets.spec.ts | 22 ++-- site/tests/unit/app-shell.test.tsx | 50 ++++---- site/tests/unit/breadcrumb.test.ts | 4 +- 32 files changed, 273 insertions(+), 273 deletions(-) diff --git a/docs/README.md b/docs/README.md index c9af368b..db2aa51f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -77,31 +77,31 @@ Connector `-- TanStack Table Affordance -|-- 초점 -|-- 캐럿 -|-- 고르기 -|-- 찾아 점프 -|-- 활성화 -|-- 취소 -|-- 접기 -|-- 되돌리기 -|-- 지우기 -|-- 이름 바꾸기 -|-- 살짝 밀기 -|-- 가리키기 -|-- 두 번 누르기 -|-- 세 번 누르기 -|-- 차 메뉴 -|-- 드래그 -|-- 쓸어 담기 -|-- 놓기 -|-- 복사해서 옮기기 -|-- 크기 바꾸기 -|-- 밀기 -|-- 굴리기 -|-- 확대 -|-- 붙이기 -`-- 금지 +|-- Focus +|-- Caret +|-- Select +|-- Typeahead +|-- Activate +|-- Escape +|-- Expand/Collapse +|-- Undo +|-- Delete +|-- Rename +|-- Nudge +|-- Hover +|-- Double-click +|-- Triple-click +|-- Context menu +|-- Drag +|-- Marquee +|-- Drop +|-- Duplicate +|-- Resize +|-- Pan +|-- Scroll +|-- Zoom +|-- Snap +`-- Not-allowed ``` ## 규범 우선순위 diff --git a/docs/public/affordance-activate.md b/docs/public/affordance-activate.md index 53968aa6..f243eefb 100644 --- a/docs/public/affordance-activate.md +++ b/docs/public/affordance-activate.md @@ -1,8 +1,8 @@ -# 활성화 +# Activate TBD. -활성화는 고른 대상의 기본 동작을 실행하는 손입니다. `pointer` 커서가 이 +Activate는 고른 대상의 기본 동작을 실행하는 손입니다. `pointer` 커서가 이 손을 가리킵니다. 클릭과 Enter·Space는 같은 손입니다. ```ts diff --git a/docs/public/affordance-cancel.md b/docs/public/affordance-cancel.md index fb1d3e40..4b5b2214 100644 --- a/docs/public/affordance-cancel.md +++ b/docs/public/affordance-cancel.md @@ -1,8 +1,8 @@ -# 취소 +# Escape TBD. -취소는 진행 중인 손과 임시 UI를 버리는 손입니다. Escape는 메뉴·대화·드래그를 +Escape는 진행 중인 손과 임시 UI를 버리는 손입니다. Escape는 메뉴·대화·드래그를 닫고, `pointercancel`은 포인터 제스처를 중단합니다. ```ts diff --git a/docs/public/affordance-caret.md b/docs/public/affordance-caret.md index ce53ecde..7a6e4109 100644 --- a/docs/public/affordance-caret.md +++ b/docs/public/affordance-caret.md @@ -1,8 +1,8 @@ -# 캐럿 +# Caret TBD. -캐럿은 글 안의 삽입점입니다. 항목 [고르기](affordance-select.md)와 다릅니다. +Caret은 글 안의 삽입점입니다. 항목 [Select](affordance-select.md)와 다릅니다. `text` / `vertical-text` 커서가 이 손을 가리킵니다. 클릭은 삽입점을 두고, 드래그는 글 범위를 고릅니다. diff --git a/docs/public/affordance-context-menu.md b/docs/public/affordance-context-menu.md index 07c1a06e..081b0cf9 100644 --- a/docs/public/affordance-context-menu.md +++ b/docs/public/affordance-context-menu.md @@ -1,8 +1,8 @@ -# 차 메뉴 +# Context menu TBD. -차 메뉴는 대상이 있는 자리에서 보조 동작을 여는 손입니다. +Context menu는 대상이 있는 자리에서 보조 동작을 여는 손입니다. `context-menu` 커서가 이 손을 가리킵니다. ```ts @@ -13,7 +13,7 @@ import { /* TBD */ } from "@interactive-os/json-document-affordance"; - 보조 버튼 / `contextmenu` / `auxclick` - Shift+F10 - Menu 키 -- Escape로 [취소](affordance-cancel.md) +- Escape로 [Escape](affordance-cancel.md) 호스트는 메뉴 항목을 그립니다. 어포던스는 여는 손을 닫습니다. diff --git a/docs/public/affordance-copy-drag.md b/docs/public/affordance-copy-drag.md index a36a6d0d..cdad4b68 100644 --- a/docs/public/affordance-copy-drag.md +++ b/docs/public/affordance-copy-drag.md @@ -1,8 +1,8 @@ -# 복사해서 옮기기 +# Duplicate TBD. -복사해서 옮기기는 수정 키를 누른 채 드래그하면 원본을 복제하는 손입니다. +Duplicate는 수정 키를 누른 채 드래그하면 원본을 복제하는 손입니다. `copy` / `alias` 커서가 이 손을 가리킵니다. 터치와 포인터에서 같은 수정 키 결과여야 합니다. diff --git a/docs/public/affordance-delete.md b/docs/public/affordance-delete.md index a78d75f2..454f9168 100644 --- a/docs/public/affordance-delete.md +++ b/docs/public/affordance-delete.md @@ -1,8 +1,8 @@ -# 지우기 +# Delete TBD. -지우기는 고른 대상을 없애는 손입니다. `resolveAffordanceKey`는 이미 +Delete는 고른 대상을 없애는 손입니다. `resolveAffordanceKey`는 이미 Delete를 `delete` command로 닫습니다. Backspace와의 차이, 칸 비우기와 항목 제거의 차이는 아직 닫히지 않았습니다. diff --git a/docs/public/affordance-double-click.md b/docs/public/affordance-double-click.md index eb6764ec..8787ab57 100644 --- a/docs/public/affordance-double-click.md +++ b/docs/public/affordance-double-click.md @@ -1,9 +1,9 @@ -# 두 번 누르기 +# Double-click TBD. -두 번 누르기는 `UIEvent.detail === 2`입니다. 글에서는 단어를 고르고, -목록·파일에서는 열거나 [이름 바꾸기](affordance-rename.md)로 갈 수 있습니다. +Double-click은 `UIEvent.detail === 2`입니다. 글에서는 단어를 고르고, +목록·파일에서는 열거나 [Rename](affordance-rename.md)로 갈 수 있습니다. 장르 Intent가 뜻을 정하고, 횟수 손 자체는 닫혀 있어야 합니다. ```ts diff --git a/docs/public/affordance-drag.md b/docs/public/affordance-drag.md index 601e4f6e..391e8080 100644 --- a/docs/public/affordance-drag.md +++ b/docs/public/affordance-drag.md @@ -1,6 +1,6 @@ -# 드래그 +# Drag -드래그는 고른 대상을 포인터로 옮기는 손입니다. 누른 점에서 현재 점까지 +Drag는 고른 대상을 포인터로 옮기는 손입니다. 누른 점에서 현재 점까지 이동량이 생기고, 이동량이 있을 때만 옮기기를 확정합니다. 기하와 히트 테스트는 호스트가 계산하고, editor에는 대상 ID와 이동량만 넘깁니다. @@ -25,7 +25,7 @@ if (dragShouldCommit(offset)) { ## TBD -- [놓기](affordance-drop.md) 대상과 `no-drop` -- [복사해서 옮기기](affordance-copy-drag.md) +- [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 index 5ed89a3f..35d6c1e8 100644 --- a/docs/public/affordance-drop.md +++ b/docs/public/affordance-drop.md @@ -1,8 +1,8 @@ -# 놓기 +# Drop TBD. -놓기는 [드래그](affordance-drag.md)의 끝입니다. 어디에 둘 수 있는지를 +Drop은 [Drag](affordance-drag.md)의 끝입니다. 어디에 둘 수 있는지를 커서가 말하고, 못 두는 자리는 `no-drop`입니다. ```ts @@ -12,7 +12,7 @@ import { /* TBD */ } from "@interactive-os/json-document-affordance"; 닫는 손: - drop 대상 위: `move` / `copy` / `alias` - 불가능한 자리: `no-drop` -- Escape: [취소](affordance-cancel.md) +- Escape: [Escape](affordance-cancel.md) 호스트는 드롭 자리를 그립니다. 어포던스는 허용·거절 손을 닫습니다. diff --git a/docs/public/affordance-focus.md b/docs/public/affordance-focus.md index 479ab3be..1b028a1c 100644 --- a/docs/public/affordance-focus.md +++ b/docs/public/affordance-focus.md @@ -1,10 +1,10 @@ -# 초점 +# Focus TBD. -초점은 키보드의 포인터입니다. Tab과 Shift+Tab은 컴포넌트 사이로 옮기고, -화살표·Home·End는 컴포넌트 안에서 옮깁니다. 초점과 [고르기](affordance-select.md)는 -다릅니다. 초점 표시는 항상 보여야 하고, 선택 표시와 겹치면 안 됩니다. +Focus는 키보드의 포인터입니다. Tab과 Shift+Tab은 컴포넌트 사이로 옮기고, +화살표·Home·End는 컴포넌트 안에서 옮깁니다. Focus와 [Select](affordance-select.md)는 +다릅니다. Focus 표시는 항상 보여야 하고, 선택 표시와 겹치면 안 됩니다. ```ts import { /* TBD */ } from "@interactive-os/json-document-affordance"; diff --git a/docs/public/affordance-fold.md b/docs/public/affordance-fold.md index ca841f7e..7fb042b3 100644 --- a/docs/public/affordance-fold.md +++ b/docs/public/affordance-fold.md @@ -1,6 +1,6 @@ -# 접기 +# Expand/Collapse -접기는 나무에서 가지를 접고 펼치는 손입니다. 오른쪽은 닫힌 부모를 펼치고, +Expand/Collapse는 나무에서 가지를 접고 펼치는 손입니다. 오른쪽은 닫힌 부모를 펼치고, 왼쪽은 열린 부모를 접습니다. 펼칠 가지가 없으면 그 방향은 이웃 이동으로 남습니다. 위아래 화살표는 보이는 줄을 옮깁니다. diff --git a/docs/public/affordance-forbid.md b/docs/public/affordance-forbid.md index e62e01cd..b865d570 100644 --- a/docs/public/affordance-forbid.md +++ b/docs/public/affordance-forbid.md @@ -1,8 +1,8 @@ -# 금지 +# Not-allowed TBD. -금지는 지금 그 손을 쓸 수 없음을 커서로 말하는 손입니다. 비활성 대상은 +Not-allowed는 지금 그 손을 쓸 수 없음을 커서로 말하는 손입니다. 비활성 대상은 `not-allowed`, 놓을 수 없는 자리는 `no-drop`입니다. ```ts diff --git a/docs/public/affordance-history.md b/docs/public/affordance-history.md index 0079ea65..5dfbfa99 100644 --- a/docs/public/affordance-history.md +++ b/docs/public/affordance-history.md @@ -1,6 +1,6 @@ -# 되돌리기 +# Undo -되돌리기는 값과 선택을 함께 되돌리는 손입니다. Mod+Z는 undo, Mod+Shift+Z는 +Undo는 값과 선택을 함께 되돌리는 손입니다. Mod+Z는 undo, Mod+Shift+Z는 redo입니다. 버튼은 `canUndo` / `canRedo`를 읽어 꺼집니다. ```ts diff --git a/docs/public/affordance-hover.md b/docs/public/affordance-hover.md index 11e9bd11..52cb13f1 100644 --- a/docs/public/affordance-hover.md +++ b/docs/public/affordance-hover.md @@ -1,8 +1,8 @@ -# 가리키기 +# Hover TBD. -가리키기는 누르지 않은 포인터가 대상 위에 있을 때 가능한 손을 드러냅니다. +Hover는 누르지 않은 포인터가 대상 위에 있을 때 가능한 손을 드러냅니다. 커서가 바뀌고, 잠시 뒤 툴팁이 열릴 수 있습니다. `help` 커서는 도움말이 있음을 가리킵니다. diff --git a/docs/public/affordance-marquee.md b/docs/public/affordance-marquee.md index 34446efa..f104248c 100644 --- a/docs/public/affordance-marquee.md +++ b/docs/public/affordance-marquee.md @@ -1,9 +1,9 @@ -# 쓸어 담기 +# Marquee TBD. -쓸어 담기는 빈 평면에서 사각형(또는 올가미)을 끌어 여러 대상을 집는 -손입니다. 고른 대상을 옮기는 [드래그](affordance-drag.md)와 다릅니다. +Marquee는 빈 평면에서 사각형(또는 올가미)을 끌어 여러 대상을 집는 +손입니다. 고른 대상을 옮기는 [Drag](affordance-drag.md)와 다릅니다. `crosshair` / `cell` 커서가 이 손을 가리킬 수 있습니다. ```ts diff --git a/docs/public/affordance-nudge.md b/docs/public/affordance-nudge.md index b7b0ce5a..427eddc2 100644 --- a/docs/public/affordance-nudge.md +++ b/docs/public/affordance-nudge.md @@ -1,10 +1,10 @@ -# 살짝 밀기 +# Nudge TBD. -살짝 밀기는 고른 대상을 키보드로 조금 옮기는 손입니다. 화살표는 한 단위, +Nudge는 고른 대상을 키보드로 조금 옮기는 손입니다. 화살표는 한 단위, Shift+화살표는 큰 단위입니다. 키를 누르면 반복됩니다. 항목 이웃으로 초점을 -옮기는 [고르기](affordance-select.md)와 다릅니다. +옮기는 [Select](affordance-select.md)와 다릅니다. ```ts import { /* TBD */ } from "@interactive-os/json-document-affordance"; diff --git a/docs/public/affordance-pan.md b/docs/public/affordance-pan.md index b0339e6d..3267534b 100644 --- a/docs/public/affordance-pan.md +++ b/docs/public/affordance-pan.md @@ -1,8 +1,8 @@ -# 밀기 +# Pan TBD. -밀기는 대상을 옮기지 않고 보이는 평면을 옮기는 손입니다. `grab` / +Pan은 대상을 옮기지 않고 보이는 평면을 옮기는 손입니다. `grab` / `grabbing` / `all-scroll` 커서가 이 손을 가리킵니다. ```ts diff --git a/docs/public/affordance-rename.md b/docs/public/affordance-rename.md index 78aa9cbf..ae406e40 100644 --- a/docs/public/affordance-rename.md +++ b/docs/public/affordance-rename.md @@ -1,9 +1,9 @@ -# 이름 바꾸기 +# Rename TBD. -이름 바꾸기는 고른 대상의 레이블을 고치는 손입니다. F2, Enter(어떤 목록), -느린 두 번 누르기가 같은 손을 엽니다. Escape는 [취소](affordance-cancel.md)입니다. +Rename은 고른 대상의 레이블을 고치는 손입니다. F2, Enter(어떤 목록), +느린 두 번 누르기가 같은 손을 엽니다. Escape는 [Escape](affordance-cancel.md)입니다. ```ts import { /* TBD */ } from "@interactive-os/json-document-affordance"; @@ -11,7 +11,7 @@ import { /* TBD */ } from "@interactive-os/json-document-affordance"; 닫는 손: - F2 -- 느린 두 번 누르기 (빠른 두 번은 [두 번 누르기](affordance-double-click.md)) +- 느린 두 번 누르기 (빠른 두 번은 [Double-click](affordance-double-click.md)) - Enter로 확정, Escape로 취소 호스트는 레이블 필드를 그립니다. 글 편집 자체는 Hands·캐럿입니다. diff --git a/docs/public/affordance-resize.md b/docs/public/affordance-resize.md index 6bb1a739..00fa8498 100644 --- a/docs/public/affordance-resize.md +++ b/docs/public/affordance-resize.md @@ -1,8 +1,8 @@ -# 크기 바꾸기 +# Resize TBD. -크기 바꾸기는 가장자리·모서리·칸 경계·창 분할선을 움직이는 손입니다. +Resize는 가장자리·모서리·칸 경계·창 분할선을 움직이는 손입니다. CSS predefined 리사이즈 커서가 이 손을 닫습니다. ```ts diff --git a/docs/public/affordance-scroll.md b/docs/public/affordance-scroll.md index be281f86..7aa8c67d 100644 --- a/docs/public/affordance-scroll.md +++ b/docs/public/affordance-scroll.md @@ -1,9 +1,9 @@ -# 굴리기 +# Scroll TBD. -굴리기는 휠로 보이는 내용을 옮기는 손입니다. 드래그 중 가장자리에 닿으면 -자동으로 더 굴러갑니다. [확대](affordance-zoom.md)의 Mod+휠과 구분합니다. +Scroll은 휠로 보이는 내용을 옮기는 손입니다. 드래그 중 가장자리에 닿으면 +자동으로 더 굴러갑니다. [Zoom](affordance-zoom.md)의 Mod+휠과 구분합니다. ```ts import { /* TBD */ } from "@interactive-os/json-document-affordance"; diff --git a/docs/public/affordance-select.md b/docs/public/affordance-select.md index e3229a58..3b27df7c 100644 --- a/docs/public/affordance-select.md +++ b/docs/public/affordance-select.md @@ -1,6 +1,6 @@ -# 고르기 +# Select -고르기는 대상을 집는 손입니다. 클릭은 그 대상으로 바꾸고, Shift는 +Select는 대상을 집는 손입니다. 클릭은 그 대상으로 바꾸고, Shift는 범위를 늘리며, Mod는 토글합니다. 화살표는 이웃으로 옮기고, Shift+화살표는 범위를 늘립니다. @@ -26,14 +26,14 @@ const command = resolveAffordanceKey({ React에서 범위와 커서를 그리려면 `useEditing`의 `onSelect`에 `pointerSelect` 결과를 연결합니다. -Keyboard Adapter는 chord를 command로 바꿉니다. 고르기는 그 command가 +Keyboard Adapter는 chord를 command로 바꿉니다. Select는 그 command가 replace인지 extend인지를 닫습니다. ## TBD -- Mod+A 모두 고르기 토글 +- Mod+A Select all 토글 - Home / End / PageUp / PageDown -- 초점 따라 선택 vs 초점만 이동 -- 글 단어·줄 범위는 [두 번 누르기](affordance-double-click.md)· - [세 번 누르기](affordance-triple-click.md)·[캐럿](affordance-caret.md) -- 빈 평면의 여러 대상은 [쓸어 담기](affordance-marquee.md) +- selection follows focus vs focus-only move +- 글 단어·줄 범위는 [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 index 77690cdf..d5d0fea5 100644 --- a/docs/public/affordance-snap.md +++ b/docs/public/affordance-snap.md @@ -1,9 +1,9 @@ -# 붙이기 +# Snap TBD. -붙이기는 [드래그](affordance-drag.md)·[크기 바꾸기](affordance-resize.md)· -[살짝 밀기](affordance-nudge.md) 중에 그리드나 가이드에 붙는 손입니다. +Snap은 [Drag](affordance-drag.md)·[Resize](affordance-resize.md)· +[Nudge](affordance-nudge.md) 중에 그리드나 가이드에 붙는 손입니다. 수정 키를 누르면 붙지 않습니다. ```ts diff --git a/docs/public/affordance-triple-click.md b/docs/public/affordance-triple-click.md index 7a66318c..999b403f 100644 --- a/docs/public/affordance-triple-click.md +++ b/docs/public/affordance-triple-click.md @@ -1,8 +1,8 @@ -# 세 번 누르기 +# Triple-click TBD. -세 번 누르기는 `UIEvent.detail === 3`입니다. 글에서는 줄 또는 문단을 +Triple-click은 `UIEvent.detail === 3`입니다. 글에서는 줄 또는 문단을 고릅니다. 항목 목록에서는 보통 쓰지 않습니다. ```ts diff --git a/docs/public/affordance-typeahead.md b/docs/public/affordance-typeahead.md index ecefbc0e..0ab9fe64 100644 --- a/docs/public/affordance-typeahead.md +++ b/docs/public/affordance-typeahead.md @@ -1,8 +1,8 @@ -# 찾아 점프 +# Typeahead TBD. -찾아 점프는 인쇄 글쇠로 목록·나무에서 이름으로 건너뛰는 손입니다. 한 글자는 +Typeahead는 인쇄 글쇠로 목록·나무에서 이름으로 건너뛰는 손입니다. 한 글자는 그 글자로 시작하는 다음 항목, 빠르게 이어 치면 그 문자열로 시작합니다. ```ts diff --git a/docs/public/affordance-zoom.md b/docs/public/affordance-zoom.md index b25d163f..4640aa2a 100644 --- a/docs/public/affordance-zoom.md +++ b/docs/public/affordance-zoom.md @@ -1,8 +1,8 @@ -# 확대 +# Zoom TBD. -확대는 보이는 배율을 바꾸는 손입니다. `zoom-in` / `zoom-out` 커서가 +Zoom은 보이는 배율을 바꾸는 손입니다. `zoom-in` / `zoom-out` 커서가 이 손을 가리킵니다. CSS `resize`와 무관합니다. ```ts diff --git a/docs/public/affordance.md b/docs/public/affordance.md index 626ab6e9..5f7a7cba 100644 --- a/docs/public/affordance.md +++ b/docs/public/affordance.md @@ -1,4 +1,4 @@ -# 어포던스 +# Affordance 제품의 화면은 호스트가 그립니다. json-document가 최전선에서 주는 것은 리스트박스나 나무가 아니라, 30년 동안 같은 손으로 학습된 키보드와 @@ -28,43 +28,43 @@ Editing은 선택과 작업을 기억합니다. Adapter는 키 chord를 command ## 이미 닫힌 손 -| 어포던스 | API | 손 | +| Affordance | API | Hand | | --- | --- | --- | -| [고르기](affordance-select.md) | `pointerSelect`, `resolveAffordanceKey` | 클릭, Shift 범위, Mod 토글, 화살표 | -| [접기](affordance-fold.md) | `treeAffordance` | 나무 왼쪽 접힘, 오른쪽 펼침 | -| [드래그](affordance-drag.md) | `dragOffset`, `dragShouldCommit` | 고른 대상을 포인터로 옮김 | -| [되돌리기](affordance-history.md) | `historyAffordance` | Mod+Z, Mod+Shift+Z | +| [Select](affordance-select.md) | `pointerSelect`, `resolveAffordanceKey` | 클릭, Shift 범위, Mod 토글, 화살표 | +| [Expand/Collapse](affordance-fold.md) | `treeAffordance` | 나무 왼쪽 접힘, 오른쪽 펼침 | +| [Drag](affordance-drag.md) | `dragOffset`, `dragShouldCommit` | 고른 대상을 포인터로 옮김 | +| [Undo](affordance-history.md) | `historyAffordance` | Mod+Z, Mod+Shift+Z | ## 키보드 TBD -| 어포던스 | API | 손 | +| Affordance | API | Hand | | --- | --- | --- | -| [초점](affordance-focus.md) | TBD | Tab 사이, 화살표 안, 초점 ≠ 선택 | -| [캐럿](affordance-caret.md) | TBD | I-beam 삽입점, 글 범위 | -| [찾아 점프](affordance-typeahead.md) | TBD | 인쇄 글쇠 prefix 점프 | -| [활성화](affordance-activate.md) | TBD | Enter, Space, 기본 클릭 | -| [취소](affordance-cancel.md) | TBD | Escape, pointercancel | -| [지우기](affordance-delete.md) | TBD | Delete, Backspace. Delete chord는 이미 닫힘 | -| [이름 바꾸기](affordance-rename.md) | TBD | F2, 느린 두 번 누르기 | -| [살짝 밀기](affordance-nudge.md) | TBD | 화살표 한 단위, Shift 큰 단위 | +| [Focus](affordance-focus.md) | TBD | Tab 사이, 화살표 안, 초점 ≠ 선택 | +| [Caret](affordance-caret.md) | TBD | I-beam 삽입점, 글 범위 | +| [Typeahead](affordance-typeahead.md) | TBD | 인쇄 글쇠 prefix 점프 | +| [Activate](affordance-activate.md) | TBD | Enter, Space, 기본 클릭 | +| [Escape](affordance-cancel.md) | TBD | Escape, pointercancel | +| [Delete](affordance-delete.md) | TBD | Delete, Backspace. Delete chord는 이미 닫힘 | +| [Rename](affordance-rename.md) | TBD | F2, 느린 두 번 누르기 | +| [Nudge](affordance-nudge.md) | TBD | 화살표 한 단위, Shift 큰 단위 | ## 마우스 TBD -| 어포던스 | API | 손 | +| Affordance | API | Hand | | --- | --- | --- | -| [가리키기](affordance-hover.md) | TBD | hover, 툴팁 지연, 커서 교체 | -| [두 번 누르기](affordance-double-click.md) | TBD | `detail` 2 | -| [세 번 누르기](affordance-triple-click.md) | TBD | `detail` 3 | -| [차 메뉴](affordance-context-menu.md) | TBD | 오른쪽 클릭, Shift+F10, Menu | -| [쓸어 담기](affordance-marquee.md) | TBD | 빈 곳에서 사각형으로 여러 대상 | -| [놓기](affordance-drop.md) | TBD | drop 대상, no-drop | -| [복사해서 옮기기](affordance-copy-drag.md) | TBD | Alt/Option 드래그 복제 | -| [크기 바꾸기](affordance-resize.md) | TBD | 모서리, 칸, 분할선 | -| [밀기](affordance-pan.md) | TBD | Space+드래그, grab | -| [굴리기](affordance-scroll.md) | TBD | wheel, autoscroll | -| [확대](affordance-zoom.md) | TBD | Mod+휠, +/− | -| [붙이기](affordance-snap.md) | TBD | 그리드·가이드, 수정 키로 해제 | -| [금지](affordance-forbid.md) | TBD | not-allowed, no-drop | +| [Hover](affordance-hover.md) | TBD | hover, 툴팁 지연, 커서 교체 | +| [Double-click](affordance-double-click.md) | TBD | `detail` 2 | +| [Triple-click](affordance-triple-click.md) | TBD | `detail` 3 | +| [Context menu](affordance-context-menu.md) | TBD | 오른쪽 클릭, Shift+F10, Menu | +| [Marquee](affordance-marquee.md) | TBD | 빈 곳에서 사각형으로 여러 대상 | +| [Drop](affordance-drop.md) | TBD | drop 대상, no-drop | +| [Duplicate](affordance-copy-drag.md) | TBD | Alt/Option 드래그 복제 | +| [Resize](affordance-resize.md) | TBD | 모서리, 칸, 분할선 | +| [Pan](affordance-pan.md) | TBD | Space+드래그, grab | +| [Scroll](affordance-scroll.md) | TBD | wheel, autoscroll | +| [Zoom](affordance-zoom.md) | TBD | Mod+휠, +/− | +| [Snap](affordance-snap.md) | TBD | 그리드·가이드, 수정 키로 해제 | +| [Not-allowed](affordance-forbid.md) | TBD | not-allowed, no-drop | ## 커서가 닫는 손 @@ -74,17 +74,17 @@ CSS UI 4 ``는 이 집합이 끝입니다. 새 커서 이름 | 커서 | 손 | | --- | --- | | `auto`, `default`, `none` | 호스트·UA 기본. 이 층이 새 문법을 열지 않음 | -| `pointer` | [활성화](affordance-activate.md) | -| `text`, `vertical-text` | [캐럿](affordance-caret.md) | -| `cell`, `crosshair` | [쓸어 담기](affordance-marquee.md), [고르기](affordance-select.md) | -| `context-menu` | [차 메뉴](affordance-context-menu.md) | -| `help` | [가리키기](affordance-hover.md) | -| `move`, `grab`, `grabbing` | [드래그](affordance-drag.md), [밀기](affordance-pan.md) | -| `copy`, `alias` | [복사해서 옮기기](affordance-copy-drag.md) | -| `no-drop`, `not-allowed` | [금지](affordance-forbid.md), [놓기](affordance-drop.md) | -| `n-resize` … `nwse-resize`, `col-resize`, `row-resize` | [크기 바꾸기](affordance-resize.md) | -| `all-scroll` | [밀기](affordance-pan.md) | -| `zoom-in`, `zoom-out` | [확대](affordance-zoom.md) | +| `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 바쁨. 편집 손이 아님 | ## 이 층이 아닌 것 diff --git a/site/site-routes.json b/site/site-routes.json index b4fb684a..df0c9481 100644 --- a/site/site-routes.json +++ b/site/site-routes.json @@ -437,7 +437,7 @@ "path": "/docs/affordance", "label": "Affordance", "title": "Affordance - json-document", - "heading": "어포던스", + "heading": "Affordance", "description": "제품이 json-document를 만지는 최전선의 키보드·마우스·커서 손을 API와 사용법으로 설명합니다.", "language": "ko", "navigationGroup": "Affordance", @@ -445,9 +445,9 @@ }, { "path": "/docs/affordance/focus", - "label": "초점", - "title": "초점 - json-document", - "heading": "초점", + "label": "Focus", + "title": "Focus - json-document", + "heading": "Focus", "description": "Tab은 컴포넌트 사이로, 화살표는 컴포넌트 안에서 초점을 옮기는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -455,9 +455,9 @@ }, { "path": "/docs/affordance/caret", - "label": "캐럿", - "title": "캐럿 - json-document", - "heading": "캐럿", + "label": "Caret", + "title": "Caret - json-document", + "heading": "Caret", "description": "I-beam 클릭으로 글 삽입점을 두고, 화살표로 글자 사이를 옮기는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -465,9 +465,9 @@ }, { "path": "/docs/affordance/select", - "label": "고르기", - "title": "고르기 - json-document", - "heading": "고르기", + "label": "Select", + "title": "Select - json-document", + "heading": "Select", "description": "클릭, Shift 범위, Mod 토글, 화살표로 대상을 집는 손을 붙입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -477,9 +477,9 @@ }, { "path": "/docs/affordance/typeahead", - "label": "찾아 점프", - "title": "찾아 점프 - json-document", - "heading": "찾아 점프", + "label": "Typeahead", + "title": "Typeahead - json-document", + "heading": "Typeahead", "description": "글쇠를 누르면 그 글자로 시작하는 다음 항목으로 초점을 옮기는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -487,9 +487,9 @@ }, { "path": "/docs/affordance/activate", - "label": "활성화", - "title": "활성화 - json-document", - "heading": "활성화", + "label": "Activate", + "title": "Activate - json-document", + "heading": "Activate", "description": "클릭·Enter·Space로 대상의 기본 동작을 실행하는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -497,9 +497,9 @@ }, { "path": "/docs/affordance/cancel", - "label": "취소", - "title": "취소 - json-document", - "heading": "취소", + "label": "Escape", + "title": "Escape - json-document", + "heading": "Escape", "description": "Escape와 pointercancel로 진행 중인 손을 버리는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -507,9 +507,9 @@ }, { "path": "/docs/affordance/fold", - "label": "접기", - "title": "접기 - json-document", - "heading": "접기", + "label": "Expand/Collapse", + "title": "Expand/Collapse - json-document", + "heading": "Expand/Collapse", "description": "나무에서 왼쪽은 접고 오른쪽은 펼치는 손을 붙입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -519,9 +519,9 @@ }, { "path": "/docs/affordance/history", - "label": "되돌리기", - "title": "되돌리기 - json-document", - "heading": "되돌리기", + "label": "Undo", + "title": "Undo - json-document", + "heading": "Undo", "description": "Mod+Z와 canUndo로 값과 선택을 함께 되돌리는 손을 붙입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -531,9 +531,9 @@ }, { "path": "/docs/affordance/delete", - "label": "지우기", - "title": "지우기 - json-document", - "heading": "지우기", + "label": "Delete", + "title": "Delete - json-document", + "heading": "Delete", "description": "Delete와 Backspace로 고른 대상을 지우는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -541,9 +541,9 @@ }, { "path": "/docs/affordance/rename", - "label": "이름 바꾸기", - "title": "이름 바꾸기 - json-document", - "heading": "이름 바꾸기", + "label": "Rename", + "title": "Rename - json-document", + "heading": "Rename", "description": "F2와 느린 두 번 누르기로 고른 대상의 이름을 고치는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -551,9 +551,9 @@ }, { "path": "/docs/affordance/nudge", - "label": "살짝 밀기", - "title": "살짝 밀기 - json-document", - "heading": "살짝 밀기", + "label": "Nudge", + "title": "Nudge - json-document", + "heading": "Nudge", "description": "화살표로 고른 대상을 한 단위 옮기고, Shift로 큰 단위를 쓰는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -561,9 +561,9 @@ }, { "path": "/docs/affordance/hover", - "label": "가리키기", - "title": "가리키기 - json-document", - "heading": "가리키기", + "label": "Hover", + "title": "Hover - json-document", + "heading": "Hover", "description": "누르지 않고 포인터를 올려 손과 도움말을 드러내는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -571,9 +571,9 @@ }, { "path": "/docs/affordance/double-click", - "label": "두 번 누르기", - "title": "두 번 누르기 - json-document", - "heading": "두 번 누르기", + "label": "Double-click", + "title": "Double-click - json-document", + "heading": "Double-click", "description": "click detail 2로 열기·단어 고르기·이름 바꾸기를 여는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -581,9 +581,9 @@ }, { "path": "/docs/affordance/triple-click", - "label": "세 번 누르기", - "title": "세 번 누르기 - json-document", - "heading": "세 번 누르기", + "label": "Triple-click", + "title": "Triple-click - json-document", + "heading": "Triple-click", "description": "click detail 3으로 줄이나 문단을 고르는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -591,9 +591,9 @@ }, { "path": "/docs/affordance/context-menu", - "label": "차 메뉴", - "title": "차 메뉴 - json-document", - "heading": "차 메뉴", + "label": "Context menu", + "title": "Context menu - json-document", + "heading": "Context menu", "description": "오른쪽 클릭, Shift+F10, Menu 키로 자리 메뉴를 여는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -601,9 +601,9 @@ }, { "path": "/docs/affordance/drag", - "label": "드래그", - "title": "드래그 - json-document", - "heading": "드래그", + "label": "Drag", + "title": "Drag - json-document", + "heading": "Drag", "description": "고른 대상을 포인터로 옮기는 손을 붙입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -613,9 +613,9 @@ }, { "path": "/docs/affordance/marquee", - "label": "쓸어 담기", - "title": "쓸어 담기 - json-document", - "heading": "쓸어 담기", + "label": "Marquee", + "title": "Marquee - json-document", + "heading": "Marquee", "description": "빈 곳에서 끌어서 여러 대상을 한 번에 고르는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -623,9 +623,9 @@ }, { "path": "/docs/affordance/drop", - "label": "놓기", - "title": "놓기 - json-document", - "heading": "놓기", + "label": "Drop", + "title": "Drop - json-document", + "heading": "Drop", "description": "드래그한 대상을 어디에 둘지 정하고, 못 두면 no-drop을 보여주는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -633,9 +633,9 @@ }, { "path": "/docs/affordance/copy-drag", - "label": "복사해서 옮기기", - "title": "복사해서 옮기기 - json-document", - "heading": "복사해서 옮기기", + "label": "Duplicate", + "title": "Duplicate - json-document", + "heading": "Duplicate", "description": "Alt/Option을 누른 채 드래그하면 원본을 복제하는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -643,9 +643,9 @@ }, { "path": "/docs/affordance/resize", - "label": "크기 바꾸기", - "title": "크기 바꾸기 - json-document", - "heading": "크기 바꾸기", + "label": "Resize", + "title": "Resize - json-document", + "heading": "Resize", "description": "모서리·칸·분할선을 끌어 크기를 바꾸고, 화살표로 미세 조절하는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -653,9 +653,9 @@ }, { "path": "/docs/affordance/pan", - "label": "밀기", - "title": "밀기 - json-document", - "heading": "밀기", + "label": "Pan", + "title": "Pan - json-document", + "heading": "Pan", "description": "손바닥 커서로 평면을 밀고, Space+드래그로 화면을 옮기는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -663,9 +663,9 @@ }, { "path": "/docs/affordance/scroll", - "label": "굴리기", - "title": "굴리기 - json-document", - "heading": "굴리기", + "label": "Scroll", + "title": "Scroll - json-document", + "heading": "Scroll", "description": "휠과 드래그 중 자동 스크롤로 보이는 줄을 옮기는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -673,9 +673,9 @@ }, { "path": "/docs/affordance/zoom", - "label": "확대", - "title": "확대 - json-document", - "heading": "확대", + "label": "Zoom", + "title": "Zoom - json-document", + "heading": "Zoom", "description": "Mod+휠과 +/- 키, zoom-in/out 커서로 보이는 배율을 바꾸는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -683,9 +683,9 @@ }, { "path": "/docs/affordance/snap", - "label": "붙이기", - "title": "붙이기 - json-document", - "heading": "붙이기", + "label": "Snap", + "title": "Snap - json-document", + "heading": "Snap", "description": "드래그와 크기 바꾸기 중 그리드·가이드에 붙고, 수정 키로 푸는 손입니다.", "language": "ko", "navigationGroup": "Affordance", @@ -693,9 +693,9 @@ }, { "path": "/docs/affordance/forbid", - "label": "금지", - "title": "금지 - json-document", - "heading": "금지", + "label": "Not-allowed", + "title": "Not-allowed - json-document", + "heading": "Not-allowed", "description": "할 수 없는 자리에서 not-allowed와 no-drop 커서를 보여주는 손입니다.", "language": "ko", "navigationGroup": "Affordance", diff --git a/site/tests/browser/site-shell.spec.ts b/site/tests/browser/site-shell.spec.ts index 0034e4bf..f235fc26 100644 --- a/site/tests/browser/site-shell.spec.ts +++ b/site/tests/browser/site-shell.spec.ts @@ -62,31 +62,31 @@ test("official overview exposes the product hierarchy", async ({ page }) => { await expect(navigation.getByRole("group", { name: "Connector" }).getByRole("link")).toHaveText(["React", "React Hook Form", "Ajv", "Zod", "TanStack Table"]); await navigation.getByRole("button", { name: "Affordance" }).click(); await expect(navigation.getByRole("group", { name: "Affordance" }).getByRole("link")).toHaveText([ - "초점", - "캐럿", - "고르기", - "찾아 점프", - "활성화", - "취소", - "접기", - "되돌리기", - "지우기", - "이름 바꾸기", - "살짝 밀기", - "가리키기", - "두 번 누르기", - "세 번 누르기", - "차 메뉴", - "드래그", - "쓸어 담기", - "놓기", - "복사해서 옮기기", - "크기 바꾸기", - "밀기", - "굴리기", - "확대", - "붙이기", - "금지", + "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", ]); await expect(navigation.getByRole("group", { name: "Demos" })).toHaveCount(0); await expect(navigation.getByRole("group", { name: "Reference" })).toHaveCount(0); diff --git a/site/tests/browser/widgets.spec.ts b/site/tests/browser/widgets.spec.ts index 9f716f31..3fa1470f 100644 --- a/site/tests/browser/widgets.spec.ts +++ b/site/tests/browser/widgets.spec.ts @@ -3,19 +3,19 @@ import { expect, test, type Page } from "@playwright/test"; test("Widgets catalog redirects to affordance usage", async ({ page }) => { await page.goto("/widgets"); await expect(page).toHaveURL(/\/docs\/affordance$/); - await expect(page.getByRole("heading", { level: 1, name: "어포던스" })).toBeVisible(); + await expect(page.getByRole("heading", { level: 1, name: "Affordance" })).toBeVisible(); const navigation = page.getByRole("navigation", { name: "Site navigation" }); - await expect(navigation.getByRole("link", { name: "고르기", exact: true })).toHaveAttribute("href", "/docs/affordance/select"); - await expect(navigation.getByRole("link", { name: "접기", exact: true })).toHaveAttribute("href", "/docs/affordance/fold"); - await expect(navigation.getByRole("link", { name: "드래그", exact: true })).toHaveAttribute("href", "/docs/affordance/drag"); - await expect(navigation.getByRole("link", { name: "되돌리기", exact: true })).toHaveAttribute("href", "/docs/affordance/history"); + await expect(navigation.getByRole("link", { name: "Select", exact: true })).toHaveAttribute("href", "/docs/affordance/select"); + await expect(navigation.getByRole("link", { name: "Expand/Collapse", exact: true })).toHaveAttribute("href", "/docs/affordance/fold"); + await expect(navigation.getByRole("link", { name: "Drag", exact: true })).toHaveAttribute("href", "/docs/affordance/drag"); + await expect(navigation.getByRole("link", { name: "Undo", exact: true })).toHaveAttribute("href", "/docs/affordance/history"); const content = page.getByRole("main"); - await expect(content.getByRole("link", { name: "고르기" }).first()).toHaveAttribute("href", "/docs/affordance/select"); - await expect(content.getByRole("link", { name: "접기" }).first()).toHaveAttribute("href", "/docs/affordance/fold"); - await expect(content.getByRole("link", { name: "드래그" }).first()).toHaveAttribute("href", "/docs/affordance/drag"); - await expect(content.getByRole("link", { name: "되돌리기" }).first()).toHaveAttribute("href", "/docs/affordance/history"); - await expect(content.getByRole("link", { name: "초점" }).first()).toHaveAttribute("href", "/docs/affordance/focus"); - await expect(content.getByRole("link", { name: "크기 바꾸기" }).first()).toHaveAttribute("href", "/docs/affordance/resize"); + await expect(content.getByRole("link", { name: "Select" }).first()).toHaveAttribute("href", "/docs/affordance/select"); + await expect(content.getByRole("link", { name: "Expand/Collapse" }).first()).toHaveAttribute("href", "/docs/affordance/fold"); + await expect(content.getByRole("link", { name: "Drag" }).first()).toHaveAttribute("href", "/docs/affordance/drag"); + await expect(content.getByRole("link", { name: "Undo" }).first()).toHaveAttribute("href", "/docs/affordance/history"); + await expect(content.getByRole("link", { name: "Focus" }).first()).toHaveAttribute("href", "/docs/affordance/focus"); + await expect(content.getByRole("link", { name: "Resize" }).first()).toHaveAttribute("href", "/docs/affordance/resize"); }); test("Toolbar binds Undo and Redo to canUndo and canRedo", async ({ page }) => { diff --git a/site/tests/unit/app-shell.test.tsx b/site/tests/unit/app-shell.test.tsx index cf716c4d..45bebe77 100644 --- a/site/tests/unit/app-shell.test.tsx +++ b/site/tests/unit/app-shell.test.tsx @@ -84,31 +84,31 @@ describe("official site shell", () => { expect(groupLinks(nav, "Connector")).toEqual(["React", "React Hook Form", "Ajv", "Zod", "TanStack Table"]); await user.click(nav.getByRole("button", { name: "Affordance" })); expect(groupLinks(nav, "Affordance")).toEqual([ - "초점", - "캐럿", - "고르기", - "찾아 점프", - "활성화", - "취소", - "접기", - "되돌리기", - "지우기", - "이름 바꾸기", - "살짝 밀기", - "가리키기", - "두 번 누르기", - "세 번 누르기", - "차 메뉴", - "드래그", - "쓸어 담기", - "놓기", - "복사해서 옮기기", - "크기 바꾸기", - "밀기", - "굴리기", - "확대", - "붙이기", - "금지", + "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", ]); expect(nav.queryByRole("group", { name: "Demos" })).toBeNull(); expect(nav.queryByRole("group", { name: "Reference" })).toBeNull(); diff --git a/site/tests/unit/breadcrumb.test.ts b/site/tests/unit/breadcrumb.test.ts index 4de04d1d..c9804009 100644 --- a/site/tests/unit/breadcrumb.test.ts +++ b/site/tests/unit/breadcrumb.test.ts @@ -91,12 +91,12 @@ describe("breadcrumbTrail", () => { expect(trail("/docs/affordance/select")).toEqual([ "Overview:/", "Affordance:/docs/affordance", - "고르기:/docs/affordance/select", + "Select:/docs/affordance/select", ]); expect(trail("/widgets/toolbar")).toEqual([ "Overview:/", "Affordance:/docs/affordance", - "되돌리기:/docs/affordance/history", + "Undo:/docs/affordance/history", "Toolbar:/widgets/toolbar", ]); }); From 1cff18f4852d3f8971fe15f08e4f58eaac60eace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Thu, 20 Aug 2026 21:55:51 +0900 Subject: [PATCH 05/14] =?UTF-8?q?=EC=96=B4=ED=8F=AC=EB=8D=98=EC=8A=A4:=20T?= =?UTF-8?q?BD=20=EC=82=AC=EC=9A=A9=EB=B2=95=20=EB=AA=85=EC=84=B8=EB=A5=BC?= =?UTF-8?q?=20pointerSelect=EC=99=80=20=EA=B0=99=EC=9D=80=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=EB=A1=9C=20=EC=A0=81=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 패키지 구현은 아직 없다. 호스트가 넘긴 사실만 받아 손을 닫는 API를 페이지마다 가상 usage로 고정한다. --- docs/public/affordance-activate.md | 34 +++++++++++++----- docs/public/affordance-cancel.md | 21 +++++++---- docs/public/affordance-caret.md | 26 +++++++++++--- docs/public/affordance-context-menu.md | 18 ++++++++-- docs/public/affordance-copy-drag.md | 26 ++++++++++---- docs/public/affordance-delete.md | 35 ++++++++++++++----- docs/public/affordance-double-click.md | 22 +++++++++--- docs/public/affordance-drag.md | 10 ++++++ docs/public/affordance-drop.md | 15 ++++++-- docs/public/affordance-focus.md | 19 ++++++++-- docs/public/affordance-fold.md | 10 ++++++ docs/public/affordance-forbid.md | 21 +++++++---- docs/public/affordance-history.md | 12 +++++++ docs/public/affordance-hover.md | 22 +++++++++--- docs/public/affordance-marquee.md | 26 ++++++++++---- docs/public/affordance-nudge.md | 25 +++++++++----- docs/public/affordance-pan.md | 22 ++++++++++-- docs/public/affordance-rename.md | 29 ++++++++++++---- docs/public/affordance-resize.md | 48 ++++++++++++++++++++++---- docs/public/affordance-scroll.md | 26 +++++++++++--- docs/public/affordance-select.md | 14 ++++++-- docs/public/affordance-snap.md | 18 ++++++++-- docs/public/affordance-triple-click.md | 10 ++++-- docs/public/affordance-typeahead.md | 33 ++++++++++++++++-- docs/public/affordance-zoom.md | 22 ++++++++++-- docs/public/affordance.md | 45 ++++++++++++------------ 26 files changed, 481 insertions(+), 128 deletions(-) diff --git a/docs/public/affordance-activate.md b/docs/public/affordance-activate.md index f243eefb..e9b0990f 100644 --- a/docs/public/affordance-activate.md +++ b/docs/public/affordance-activate.md @@ -2,18 +2,36 @@ TBD. -Activate는 고른 대상의 기본 동작을 실행하는 손입니다. `pointer` 커서가 이 -손을 가리킵니다. 클릭과 Enter·Space는 같은 손입니다. +Activate는 대상의 기본 동작을 실행하는 손입니다. `pointer` 커서가 이 +손을 가리킵니다. Enter와 기본 클릭은 같은 손입니다. Listbox에서 Space는 +[Select](affordance-select.md)의 toggle입니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; -``` +import { + activateAffordance, + resolveAffordanceKey, +} from "@interactive-os/json-document-affordance"; -닫는 손: -- 기본 클릭 -- Enter / Space -- UIEvent.detail이 1일 때의 click +activateAffordance({ key: "Enter" }); +// "activate" + +activateAffordance({ type: "pointer", button: 0, detail: 1 }); +// "activate" + +resolveAffordanceKey({ + key: " ", + shiftKey: false, + metaKey: false, + ctrlKey: false, +}); +// { type: "toggle" } +``` 호스트는 기본 동작이 무엇인지 정합니다. 어포던스는 그 동작을 부르는 손을 닫습니다. +닫는 손: +- 기본 클릭 (`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 index 4b5b2214..46147324 100644 --- a/docs/public/affordance-cancel.md +++ b/docs/public/affordance-cancel.md @@ -2,17 +2,26 @@ TBD. -Escape는 진행 중인 손과 임시 UI를 버리는 손입니다. Escape는 메뉴·대화·드래그를 -닫고, `pointercancel`은 포인터 제스처를 중단합니다. +Escape는 진행 중인 손과 임시 UI를 버리는 손입니다. Escape 키는 메뉴·대화· +드래그를 닫고, `pointercancel`은 포인터 제스처를 중단합니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { escapeAffordance } from "@interactive-os/json-document-affordance"; + +escapeAffordance({ key: "Escape" }); +// "cancel" + +escapeAffordance({ type: "pointercancel" }); +// "cancel" + +escapeAffordance({ type: "lostpointercapture" }); +// "cancel" ``` +호스트는 무엇이 열려 있는지를 가집니다. 어포던스는 버리는 키와 포인터 취소를 닫습니다. + 닫는 손: -- Escape: 메뉴, 대화, 드래그, 이름 바꾸기 중단 +- 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 index 7a6e4109..8898753a 100644 --- a/docs/public/affordance-caret.md +++ b/docs/public/affordance-caret.md @@ -7,15 +7,33 @@ Caret은 글 안의 삽입점입니다. 항목 [Select](affordance-select.md)와 드래그는 글 범위를 고릅니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { caretAffordance, caretCursor } from "@interactive-os/json-document-affordance"; + +caretCursor("horizontal"); +// "text" + +caretCursor("vertical"); +// "vertical-text" + +caretAffordance({ key: "ArrowRight", shiftKey: false, altKey: false }); +// { type: "move", unit: "character", direction: "right", operation: "replace" } + +caretAffordance({ key: "ArrowRight", shiftKey: true, altKey: false }); +// { type: "move", unit: "character", direction: "right", operation: "extend" } + +caretAffordance({ key: "ArrowRight", shiftKey: false, altKey: true }); +// { type: "move", unit: "word", direction: "right", operation: "replace" } + +caretAffordance({ type: "pointer", detail: 1 }); +// { type: "place" } ``` +호스트는 I-beam과 글 기하를 그립니다. 값 삽입은 Hands입니다. + 닫는 손: - 클릭: 삽입점 - 드래그: 글 범위 - 화살표 / Home / End: 글자·줄 끝 -- IME composition은 삽입 경로. 값 편집 자체는 Hands - -호스트는 I-beam과 글 기하를 그립니다. 어포던스는 삽입점과 글 범위 손을 닫습니다. +- 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 index 081b0cf9..9bba8ca1 100644 --- a/docs/public/affordance-context-menu.md +++ b/docs/public/affordance-context-menu.md @@ -6,15 +6,27 @@ Context menu는 대상이 있는 자리에서 보조 동작을 여는 손입니 `context-menu` 커서가 이 손을 가리킵니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { contextMenuAffordance } from "@interactive-os/json-document-affordance"; + +contextMenuAffordance({ button: 2 }); +// "open" + +contextMenuAffordance({ key: "ContextMenu" }); +// "open" + +contextMenuAffordance({ key: "F10", shiftKey: true }); +// "open" + +contextMenuAffordance({ key: "Escape" }); +// "cancel" ``` +호스트는 메뉴 항목을 그립니다. 어포던스는 여는 손을 닫습니다. + 닫는 손: - 보조 버튼 / `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 index cdad4b68..88ba03ed 100644 --- a/docs/public/affordance-copy-drag.md +++ b/docs/public/affordance-copy-drag.md @@ -3,17 +3,31 @@ TBD. Duplicate는 수정 키를 누른 채 드래그하면 원본을 복제하는 손입니다. -`copy` / `alias` 커서가 이 손을 가리킵니다. 터치와 포인터에서 같은 -수정 키 결과여야 합니다. +`copy` / `alias` 커서가 이 손을 가리킵니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { + dragOperation, + dropAffordance, +} from "@interactive-os/json-document-affordance"; + +dragOperation({ altKey: false, metaKey: false, ctrlKey: false }); +// "move" + +dragOperation({ altKey: true, metaKey: false, ctrlKey: false }); +// "copy" + +dropAffordance({ + canDrop: true, + operation: dragOperation({ altKey: true, metaKey: false, ctrlKey: false }), +}); +// { accept: true, cursor: "copy" } ``` +값의 클립보드 복사/붙이기는 Hands입니다. 이 손은 포인터 복제입니다. + 닫는 손: -- Alt/Option + 드래그 → 복제 +- Alt/Option + 드래그 → copy - 커서 `copy` 또는 `alias` -값의 클립보드 복사/붙이기는 Hands입니다. 이 손은 포인터 복제입니다. - 근거: [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 index 454f9168..919bbecc 100644 --- a/docs/public/affordance-delete.md +++ b/docs/public/affordance-delete.md @@ -3,21 +3,38 @@ TBD. Delete는 고른 대상을 없애는 손입니다. `resolveAffordanceKey`는 이미 -Delete를 `delete` command로 닫습니다. Backspace와의 차이, 칸 비우기와 -항목 제거의 차이는 아직 닫히지 않았습니다. +Delete와 Backspace를 `delete` command로 닫습니다. 칸 비우기와 항목 제거는 +장르 Intent입니다. ```ts -import { resolveAffordanceKey } from "@interactive-os/json-document-affordance"; +import { + deleteAffordance, + resolveAffordanceKey, +} from "@interactive-os/json-document-affordance"; -resolveAffordanceKey({ key: "Delete", shiftKey: false, metaKey: false, ctrlKey: false }); +resolveAffordanceKey({ + key: "Delete", + shiftKey: false, + metaKey: false, + ctrlKey: false, +}); // { type: "delete" } -``` -남은 TBD: -- Backspace와 Delete -- 칸 비우기 vs 항목 제거 (Sheet vs Order) -- 지울 것이 없을 때 +deleteAffordance({ key: "Delete" }); +// { type: "delete", source: "forward" } + +deleteAffordance({ key: "Backspace" }); +// { type: "delete", source: "backward" } + +deleteAffordance({ key: "Backspace", hasSelection: true }); +// { type: "delete", source: "selection" } +``` 호스트는 장르 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 index 8787ab57..364ea034 100644 --- a/docs/public/affordance-double-click.md +++ b/docs/public/affordance-double-click.md @@ -3,16 +3,28 @@ TBD. Double-click은 `UIEvent.detail === 2`입니다. 글에서는 단어를 고르고, -목록·파일에서는 열거나 [Rename](affordance-rename.md)로 갈 수 있습니다. -장르 Intent가 뜻을 정하고, 횟수 손 자체는 닫혀 있어야 합니다. +목록·파일에서는 열거나 [Rename](affordance-rename.md)으로 갈 수 있습니다. +횟수 손 자체는 닫혀 있고, 뜻은 장르 Intent입니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { clickCountAffordance } from "@interactive-os/json-document-affordance"; + +clickCountAffordance(1); +// "click" + +clickCountAffordance(2); +// "double-click" + +clickCountAffordance(3); +// "triple-click" ``` +호스트는 장르 Intent만 가집니다. 글 단어 범위는 [Caret](affordance-caret.md)에 +붙입니다. + 닫는 손: -- click / dblclick, detail 2 +- click / dblclick, `detail` 2 - 글: 단어 범위 -- 항목: 열기 또는 이름 바꾸기 (장르 Intent) +- 항목: 열기 또는 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 index 391e8080..731ea98c 100644 --- a/docs/public/affordance-drag.md +++ b/docs/public/affordance-drag.md @@ -25,6 +25,16 @@ if (dragShouldCommit(offset)) { ## TBD +```ts +import { dragKeyboardAffordance } from "@interactive-os/json-document-affordance"; + +dragKeyboardAffordance({ key: "ArrowDown", grab: true }); +// { type: "nudge", dy: 1 } + +dragKeyboardAffordance({ key: "Escape", grab: true }); +// "cancel" +``` + - [Drop](affordance-drop.md) 대상과 `no-drop` - [Duplicate](affordance-copy-drag.md) - 키보드만으로 옮기기 (APG는 드래그의 키보드 대안을 요구함) diff --git a/docs/public/affordance-drop.md b/docs/public/affordance-drop.md index 35d6c1e8..68c4ee13 100644 --- a/docs/public/affordance-drop.md +++ b/docs/public/affordance-drop.md @@ -6,14 +6,23 @@ Drop은 [Drag](affordance-drag.md)의 끝입니다. 어디에 둘 수 있는지 커서가 말하고, 못 두는 자리는 `no-drop`입니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { dropAffordance } from "@interactive-os/json-document-affordance"; + +dropAffordance({ canDrop: true, operation: "move" }); +// { accept: true, cursor: "move" } + +dropAffordance({ canDrop: true, operation: "copy" }); +// { accept: true, cursor: "copy" } + +dropAffordance({ canDrop: false }); +// { accept: false, cursor: "no-drop" } ``` +호스트는 드롭 자리와 가능한지를 그립니다. 어포던스는 허용·거절 손을 닫습니다. + 닫는 손: - 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 index 1b028a1c..74392c0a 100644 --- a/docs/public/affordance-focus.md +++ b/docs/public/affordance-focus.md @@ -7,15 +7,28 @@ Focus는 키보드의 포인터입니다. Tab과 Shift+Tab은 컴포넌트 사 다릅니다. Focus 표시는 항상 보여야 하고, 선택 표시와 겹치면 안 됩니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { focusAffordance } from "@interactive-os/json-document-affordance"; + +focusAffordance({ key: "Tab", shiftKey: false }); +// { type: "tab", direction: "next" } + +focusAffordance({ key: "Tab", shiftKey: true }); +// { type: "tab", direction: "prev" } + +focusAffordance({ key: "ArrowDown", shiftKey: false }); +// { type: "move", direction: "down" } + +focusAffordance({ key: "Home", shiftKey: false }); +// { type: "boundary", edge: "start" } ``` +호스트는 마크업과 초점 고리를 그립니다. 화살표가 선택까지 바꿀지는 +장르 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 index 7fb042b3..1eaf1e9d 100644 --- a/docs/public/affordance-fold.md +++ b/docs/public/affordance-fold.md @@ -26,6 +26,16 @@ Topology 이웃으로 보냅니다. ## TBD +```ts +import { disclosureAffordance } from "@interactive-os/json-document-affordance"; + +disclosureAffordance({ key: "Enter", expanded: false }); +// "expand" + +disclosureAffordance({ key: "Enter", expanded: true }); +// "collapse" +``` + - Accordion / Disclosure의 Enter·Space 접힘 - `aria-expanded`와 호스트 접힘 집합의 동기 - 가로 나무에서 위·아래가 접힘인지 이웃인지 diff --git a/docs/public/affordance-forbid.md b/docs/public/affordance-forbid.md index b865d570..47ec8ff5 100644 --- a/docs/public/affordance-forbid.md +++ b/docs/public/affordance-forbid.md @@ -2,18 +2,27 @@ TBD. -Not-allowed는 지금 그 손을 쓸 수 없음을 커서로 말하는 손입니다. 비활성 대상은 -`not-allowed`, 놓을 수 없는 자리는 `no-drop`입니다. +Not-allowed는 지금 그 손을 쓸 수 없음을 커서로 말하는 손입니다. 비활성 +대상은 `not-allowed`, 놓을 수 없는 자리는 `no-drop`입니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { forbiddenCursor } from "@interactive-os/json-document-affordance"; + +forbiddenCursor({ allowed: false, dropping: false }); +// "not-allowed" + +forbiddenCursor({ allowed: false, dropping: true }); +// "no-drop" + +forbiddenCursor({ allowed: true, dropping: true, operation: "move" }); +// "move" ``` +호스트는 비활성 그림을 그립니다. 어포던스는 거절 커서를 닫습니다. + 닫는 손: - `not-allowed`: 요청한 동작 불가 - `no-drop`: 이 자리에 놓을 수 없음 -- 비활성 대상은 초점 가능 여부를 APG 규칙으로 따로 정함 - -호스트는 비활성 그림을 그립니다. 어포던스는 거절 커서를 닫습니다. +- 비활성 대상의 초점 가능 여부는 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 index 5dfbfa99..61573f94 100644 --- a/docs/public/affordance-history.md +++ b/docs/public/affordance-history.md @@ -17,6 +17,18 @@ Editing이 가지고, 어포던스는 그 손을 닫습니다. ## TBD +```ts +import { resolveAffordanceKey } from "@interactive-os/json-document-affordance"; + +resolveAffordanceKey({ + key: "y", + shiftKey: false, + metaKey: true, + ctrlKey: false, +}); +// { type: "redo" } +``` + - Mod+Y redo 변종 - 연속 입력의 묶음 단위 - 협업 replica에서 내 기여만 되돌리기는 Collaboration History diff --git a/docs/public/affordance-hover.md b/docs/public/affordance-hover.md index 52cb13f1..12d0782b 100644 --- a/docs/public/affordance-hover.md +++ b/docs/public/affordance-hover.md @@ -7,15 +7,27 @@ Hover는 누르지 않은 포인터가 대상 위에 있을 때 가능한 손을 있음을 가리킵니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { hoverAffordance, hoverCursor } from "@interactive-os/json-document-affordance"; + +hoverCursor("help"); +// "help" + +hoverAffordance({ elapsedMs: 120 }); +// "hint" + +hoverAffordance({ elapsedMs: 400 }); +// "tooltip" + +hoverAffordance({ elapsedMs: 0, inside: false }); +// null ``` +호스트는 툴팁 그림을 그립니다. 어포던스는 지연과 커서 손을 닫습니다. + 닫는 손: - 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 index f104248c..8f90ed47 100644 --- a/docs/public/affordance-marquee.md +++ b/docs/public/affordance-marquee.md @@ -2,20 +2,34 @@ TBD. -Marquee는 빈 평면에서 사각형(또는 올가미)을 끌어 여러 대상을 집는 -손입니다. 고른 대상을 옮기는 [Drag](affordance-drag.md)와 다릅니다. -`crosshair` / `cell` 커서가 이 손을 가리킬 수 있습니다. +Marquee는 빈 평면에서 사각형을 끌어 여러 대상을 집는 손입니다. 고른 +대상을 옮기는 [Drag](affordance-drag.md)와 다릅니다. `crosshair` / `cell` +커서가 이 손을 가리킬 수 있습니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { + marqueeRect, + marqueeShouldCommit, + pointerSelect, +} from "@interactive-os/json-document-affordance"; + +const rect = marqueeRect({ x: 24, y: 40 }, { x: 120, y: 90 }); +// { x: 24, y: 40, width: 96, height: 50 } + +marqueeShouldCommit(rect); +// true + +pointerSelect({ shiftKey: true, metaKey: false, ctrlKey: false }); +// "extend" ``` +호스트는 히트 테스트와 기하를 계산합니다. 어떤 키가 범위에 들어오는지는 +호스트가 보고, 손이 replace인지 extend인지는 `pointerSelect`가 닫습니다. + 닫는 손: - 빈 곳에서 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 index 427eddc2..21fad51f 100644 --- a/docs/public/affordance-nudge.md +++ b/docs/public/affordance-nudge.md @@ -3,18 +3,27 @@ TBD. Nudge는 고른 대상을 키보드로 조금 옮기는 손입니다. 화살표는 한 단위, -Shift+화살표는 큰 단위입니다. 키를 누르면 반복됩니다. 항목 이웃으로 초점을 -옮기는 [Select](affordance-select.md)와 다릅니다. +Shift+화살표는 큰 단위입니다. 항목 이웃으로 초점을 옮기는 +[Select](affordance-select.md)와 다릅니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { nudgeAffordance } from "@interactive-os/json-document-affordance"; + +nudgeAffordance({ key: "ArrowRight", shiftKey: false }); +// { dx: 1, dy: 0 } + +nudgeAffordance({ key: "ArrowUp", shiftKey: true }); +// { dx: 0, dy: -10 } + +nudgeAffordance({ key: "ArrowDown", shiftKey: false }); +// { dx: 0, dy: 1 } ``` +호스트는 단위(px, 칸, 그리드)를 곱합니다. 어포던스는 손과 수정 키를 닫습니다. + 닫는 손: - Arrow: 1 단위 -- Shift+Arrow: 큰 단위 -- 키 반복 - -호스트는 단위(px, 칸, 그리드)를 정합니다. 어포던스는 손과 수정 키를 닫습니다. +- Shift+Arrow: 10 단위 +- 키 반복은 호스트 플랫폼 기본 -근거: 캔버스·슬라이드 편집기 관례 (Object 장르 Intent가 단위를 가짐) +근거: 캔버스·슬라이드 편집기 관례. Object 장르 Intent가 단위를 가짐. diff --git a/docs/public/affordance-pan.md b/docs/public/affordance-pan.md index 3267534b..0fee89f0 100644 --- a/docs/public/affordance-pan.md +++ b/docs/public/affordance-pan.md @@ -6,15 +6,31 @@ Pan은 대상을 옮기지 않고 보이는 평면을 옮기는 손입니다. `g `grabbing` / `all-scroll` 커서가 이 손을 가리킵니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { + panAffordance, + dragOffset, +} from "@interactive-os/json-document-affordance"; + +panAffordance({ spaceKey: true, buttons: 0 }); +// { cursor: "grab" } + +panAffordance({ spaceKey: true, buttons: 1 }); +// { cursor: "grabbing" } + +panAffordance({ buttons: 4 }); +// { cursor: "all-scroll" } + +dragOffset({ x: 80, y: 40 }, { x: 64, y: 52 }); +// { dx: -16, dy: 12 } ``` +호스트는 뷰포트 변환을 가집니다. 이동량은 [Drag](affordance-drag.md)와 +같은 `dragOffset`입니다. + 닫는 손: - 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 index ae406e40..6f20c3c3 100644 --- a/docs/public/affordance-rename.md +++ b/docs/public/affordance-rename.md @@ -2,18 +2,33 @@ TBD. -Rename은 고른 대상의 레이블을 고치는 손입니다. F2, Enter(어떤 목록), -느린 두 번 누르기가 같은 손을 엽니다. Escape는 [Escape](affordance-cancel.md)입니다. +Rename은 고른 대상의 레이블을 고치는 손입니다. F2와 느린 double-click이 +같은 손을 엽니다. Escape는 [Escape](affordance-cancel.md)입니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { renameAffordance } from "@interactive-os/json-document-affordance"; + +renameAffordance({ key: "F2" }); +// "begin" + +renameAffordance({ type: "pointer", detail: 2, intervalMs: 900 }); +// "begin" + +renameAffordance({ type: "pointer", detail: 2, intervalMs: 200 }); +// null + +renameAffordance({ key: "Enter" }); +// "commit" + +renameAffordance({ key: "Escape" }); +// "cancel" ``` +호스트는 레이블 필드를 그립니다. 글 편집 자체는 Hands와 [Caret](affordance-caret.md)입니다. + 닫는 손: - F2 -- 느린 두 번 누르기 (빠른 두 번은 [Double-click](affordance-double-click.md)) +- 느린 double-click (빠른 것은 [Double-click](affordance-double-click.md)) - Enter로 확정, Escape로 취소 -호스트는 레이블 필드를 그립니다. 글 편집 자체는 Hands·캐럿입니다. - -근거: Finder/Explorer/VS Code 관례, [UI Events click detail](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail) +근거: 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 index 00fa8498..8f4c1661 100644 --- a/docs/public/affordance-resize.md +++ b/docs/public/affordance-resize.md @@ -6,17 +6,51 @@ Resize는 가장자리·모서리·칸 경계·창 분할선을 움직이는 손 CSS predefined 리사이즈 커서가 이 손을 닫습니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { + resizeCursor, + resizeOffset, + resolveAffordanceKey, +} from "@interactive-os/json-document-affordance"; + +resizeCursor("se"); +// "se-resize" + +resizeCursor("column"); +// "col-resize" + +resizeOffset( + { x: 80, y: 40 }, + { x: 112, y: 72 }, + "se", + { shiftKey: false, altKey: false }, +); +// { dx: 32, dy: 32 } + +resizeOffset( + { x: 80, y: 40 }, + { x: 112, y: 56 }, + "se", + { shiftKey: true, altKey: false }, +); +// { dx: 32, dy: 32 } + +resolveAffordanceKey({ + key: "ArrowRight", + shiftKey: false, + metaKey: false, + ctrlKey: false, +}); +// { type: "move", direction: "right", operation: "replace" } ``` +호스트는 핸들과 기하를 그립니다. 분할선 화살표는 APG Window Splitter와 +같고, Shift는 비율, Alt는 가운데 기준입니다. + 닫는 손: - 모서리: `n-resize` … `nwse-resize` - 칸/행: `col-resize` / `row-resize` -- 분할선: 화살표로 이동, Enter로 접기 (APG Window Splitter) -- Shift: 비율 고정 (관례) -- Alt: 가운데 기준 (관례) -- CSS `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 index 7aa8c67d..2dbe24fc 100644 --- a/docs/public/affordance-scroll.md +++ b/docs/public/affordance-scroll.md @@ -6,14 +6,32 @@ Scroll은 휠로 보이는 내용을 옮기는 손입니다. 드래그 중 가 자동으로 더 굴러갑니다. [Zoom](affordance-zoom.md)의 Mod+휠과 구분합니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { + wheelAffordance, + autoscrollAffordance, +} from "@interactive-os/json-document-affordance"; + +wheelAffordance({ deltaY: 80, ctrlKey: false, metaKey: false }); +// { type: "scroll", dy: 80 } + +wheelAffordance({ deltaY: 80, ctrlKey: true, metaKey: false }); +// { type: "zoom", delta: -80 } + +autoscrollAffordance({ + x: 4, + y: 80, + width: 240, + height: 180, + edge: 16, +}); +// { dx: -1, dy: 0 } ``` +호스트는 overflow를 그립니다. 어포던스는 휠과 자동 굴리기를 닫습니다. + 닫는 손: - wheel (수정 키 없음) - 드래그 중 가장자리 autoscroll -- 초점 대상이 보이도록 scroll-into-view - -호스트는 overflow를 그립니다. 어포던스는 휠과 자동 굴리기를 닫습니다. +- 초점 대상이 보이도록 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 index 3b27df7c..2f305dda 100644 --- a/docs/public/affordance-select.md +++ b/docs/public/affordance-select.md @@ -31,9 +31,19 @@ replace인지 extend인지를 닫습니다. ## TBD +```ts +import { selectAllAffordance } from "@interactive-os/json-document-affordance"; + +selectAllAffordance({ allSelected: false }); +// "select-all" + +selectAllAffordance({ allSelected: true }); +// "clear" +``` + - Mod+A Select all 토글 -- Home / End / PageUp / PageDown -- selection follows focus vs focus-only move +- 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 index d5d0fea5..4ec296ac 100644 --- a/docs/public/affordance-snap.md +++ b/docs/public/affordance-snap.md @@ -7,14 +7,26 @@ Snap은 [Drag](affordance-drag.md)·[Resize](affordance-resize.md)· 수정 키를 누르면 붙지 않습니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { snapAffordance } from "@interactive-os/json-document-affordance"; + +snapAffordance({ x: 47, y: 51 }, { grid: 8, disable: false }); +// { x: 48, y: 48 } + +snapAffordance({ x: 47, y: 51 }, { grid: 8, disable: true }); +// { x: 47, y: 51 } + +snapAffordance( + { x: 47, y: 51 }, + { guides: [{ x: 50 }, { y: 48 }], disable: false }, +); +// { x: 50, y: 48 } ``` +호스트는 가이드 기하를 가집니다. 어포던스는 붙임/해제 손을 닫습니다. + 닫는 손: - 이동·리사이즈 중 스냅 - 수정 키로 스냅 해제 - 회전 15° 스냅은 장르 Intent (Object) -호스트는 가이드 기하를 가집니다. 어포던스는 붙임/해제 손을 닫습니다. - 근거: 캔버스·슬라이드 편집기 관례. CSS predefined 커서는 없음. diff --git a/docs/public/affordance-triple-click.md b/docs/public/affordance-triple-click.md index 999b403f..104bcbd8 100644 --- a/docs/public/affordance-triple-click.md +++ b/docs/public/affordance-triple-click.md @@ -6,11 +6,17 @@ Triple-click은 `UIEvent.detail === 3`입니다. 글에서는 줄 또는 문단 고릅니다. 항목 목록에서는 보통 쓰지 않습니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { clickCountAffordance } from "@interactive-os/json-document-affordance"; + +clickCountAffordance(3); +// "triple-click" ``` +호스트는 줄/문단 기하를 그립니다. 횟수는 [Double-click](affordance-double-click.md)과 +같은 손을 씁니다. + 닫는 손: -- click detail 3 +- 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 index 0ab9fe64..070c9596 100644 --- a/docs/public/affordance-typeahead.md +++ b/docs/public/affordance-typeahead.md @@ -6,14 +6,41 @@ Typeahead는 인쇄 글쇠로 목록·나무에서 이름으로 건너뛰는 손 그 글자로 시작하는 다음 항목, 빠르게 이어 치면 그 문자열로 시작합니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { typeaheadAffordance } from "@interactive-os/json-document-affordance"; + +typeaheadAffordance({ + buffer: "", + key: "A", + elapsedMs: 0, + names: ["Apple", "Apricot", "Pear"], + from: "Pear", +}); +// { buffer: "A", name: "Apple" } + +typeaheadAffordance({ + buffer: "A", + key: "p", + elapsedMs: 80, + names: ["Apple", "Apricot", "Pear"], + from: "Apple", +}); +// { buffer: "Ap", name: "Apple" } + +typeaheadAffordance({ + buffer: "Ap", + key: "x", + elapsedMs: 1200, + names: ["Apple", "Apricot", "Pear"], + from: "Apple", +}); +// { buffer: "x", name: null } ``` +호스트는 보이는 이름을 줍니다. 어포던스는 버퍼 창과 점프 규칙을 닫습니다. + 닫는 손: - 한 문자: 그 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 index 4640aa2a..03c811f1 100644 --- a/docs/public/affordance-zoom.md +++ b/docs/public/affordance-zoom.md @@ -6,14 +6,30 @@ Zoom은 보이는 배율을 바꾸는 손입니다. `zoom-in` / `zoom-out` 커 이 손을 가리킵니다. CSS `resize`와 무관합니다. ```ts -import { /* TBD */ } from "@interactive-os/json-document-affordance"; +import { + zoomAffordance, + zoomCursor, + wheelAffordance, +} from "@interactive-os/json-document-affordance"; + +zoomCursor("in"); +// "zoom-in" + +zoomAffordance({ key: "=", metaKey: true, ctrlKey: false }); +// { type: "in" } + +zoomAffordance({ key: "-", metaKey: true, ctrlKey: false }); +// { type: "out" } + +wheelAffordance({ deltaY: -80, ctrlKey: true, metaKey: false }); +// { type: "zoom", delta: 80 } ``` +호스트는 배율 변환을 가집니다. + 닫는 손: - 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 index 5f7a7cba..5afdfb57 100644 --- a/docs/public/affordance.md +++ b/docs/public/affordance.md @@ -6,7 +6,8 @@ `@interactive-os/json-document-affordance`가 그 계약을 닫습니다. 단축키를 제품마다 바꾸지 않습니다. 구현이 없는 손은 TBD로 남기고, 화면 위젯으로 -대체하지 않습니다. +대체하지 않습니다. TBD 페이지의 코드는 사용법 명세입니다. 패키지에 +아직 없습니다. ```sh npm i @interactive-os/json-document-affordance @@ -39,32 +40,32 @@ Editing은 선택과 작업을 기억합니다. Adapter는 키 chord를 command | Affordance | API | Hand | | --- | --- | --- | -| [Focus](affordance-focus.md) | TBD | Tab 사이, 화살표 안, 초점 ≠ 선택 | -| [Caret](affordance-caret.md) | TBD | I-beam 삽입점, 글 범위 | -| [Typeahead](affordance-typeahead.md) | TBD | 인쇄 글쇠 prefix 점프 | -| [Activate](affordance-activate.md) | TBD | Enter, Space, 기본 클릭 | -| [Escape](affordance-cancel.md) | TBD | Escape, pointercancel | -| [Delete](affordance-delete.md) | TBD | Delete, Backspace. Delete chord는 이미 닫힘 | -| [Rename](affordance-rename.md) | TBD | F2, 느린 두 번 누르기 | -| [Nudge](affordance-nudge.md) | TBD | 화살표 한 단위, Shift 큰 단위 | +| [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) | TBD | hover, 툴팁 지연, 커서 교체 | -| [Double-click](affordance-double-click.md) | TBD | `detail` 2 | -| [Triple-click](affordance-triple-click.md) | TBD | `detail` 3 | -| [Context menu](affordance-context-menu.md) | TBD | 오른쪽 클릭, Shift+F10, Menu | -| [Marquee](affordance-marquee.md) | TBD | 빈 곳에서 사각형으로 여러 대상 | -| [Drop](affordance-drop.md) | TBD | drop 대상, no-drop | -| [Duplicate](affordance-copy-drag.md) | TBD | Alt/Option 드래그 복제 | -| [Resize](affordance-resize.md) | TBD | 모서리, 칸, 분할선 | -| [Pan](affordance-pan.md) | TBD | Space+드래그, grab | -| [Scroll](affordance-scroll.md) | TBD | wheel, autoscroll | -| [Zoom](affordance-zoom.md) | TBD | Mod+휠, +/− | -| [Snap](affordance-snap.md) | TBD | 그리드·가이드, 수정 키로 해제 | -| [Not-allowed](affordance-forbid.md) | TBD | not-allowed, no-drop | +| [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) | `marqueeRect`, `marqueeShouldCommit` | 빈 곳에서 사각형으로 여러 대상 | +| [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 | ## 커서가 닫는 손 From 669a66c3cb9d5aa8017c628af5f6819900e41905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Thu, 20 Aug 2026 22:00:12 +0900 Subject: [PATCH 06/14] =?UTF-8?q?=EC=96=B4=ED=8F=AC=EB=8D=98=EC=8A=A4:=20u?= =?UTF-8?q?sage=EB=A5=BC=20=ED=98=B8=EC=8A=A4=ED=8A=B8=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=EC=99=80=20editor.dispatch=EC=9D=98=20?= =?UTF-8?q?=EC=9D=B4=EC=9D=8C=EC=9C=BC=EB=A1=9C=20=EC=A0=81=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 함수 반환값만 보이지 않게, 손은 문서 작업이거나 커서·접힘·뷰포트 같은 호스트 화면 상태가 된다. --- docs/public/affordance-activate.md | 27 +++++------ docs/public/affordance-cancel.md | 21 ++++---- docs/public/affordance-caret.md | 43 ++++++++++------- docs/public/affordance-context-menu.md | 25 +++++----- docs/public/affordance-copy-drag.md | 38 ++++++++++----- docs/public/affordance-delete.md | 24 +++------ docs/public/affordance-double-click.md | 12 ++--- docs/public/affordance-drag.md | 46 ++++++++++++------ docs/public/affordance-drop.md | 30 +++++++----- docs/public/affordance-focus.md | 23 ++++----- docs/public/affordance-fold.md | 64 +++++++++++++++--------- docs/public/affordance-forbid.md | 17 +++---- docs/public/affordance-history.md | 42 ++++++++++------ docs/public/affordance-hover.md | 22 ++++----- docs/public/affordance-marquee.md | 18 +++---- docs/public/affordance-nudge.md | 20 ++++---- docs/public/affordance-pan.md | 32 ++++++------ docs/public/affordance-rename.md | 32 ++++++------ docs/public/affordance-resize.md | 67 ++++++++++++-------------- docs/public/affordance-scroll.md | 32 ++++++------ docs/public/affordance-select.md | 50 +++++++++++-------- docs/public/affordance-snap.md | 35 ++++++++------ docs/public/affordance-triple-click.md | 11 ++++- docs/public/affordance-typeahead.md | 44 +++++++---------- docs/public/affordance-zoom.md | 27 ++++++----- docs/public/affordance.md | 22 +++++++++ 26 files changed, 460 insertions(+), 364 deletions(-) diff --git a/docs/public/affordance-activate.md b/docs/public/affordance-activate.md index e9b0990f..748cb733 100644 --- a/docs/public/affordance-activate.md +++ b/docs/public/affordance-activate.md @@ -12,22 +12,21 @@ import { resolveAffordanceKey, } from "@interactive-os/json-document-affordance"; -activateAffordance({ key: "Enter" }); -// "activate" - -activateAffordance({ type: "pointer", button: 0, detail: 1 }); -// "activate" - -resolveAffordanceKey({ - key: " ", - shiftKey: false, - metaKey: false, - ctrlKey: false, -}); -// { type: "toggle" } +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) diff --git a/docs/public/affordance-cancel.md b/docs/public/affordance-cancel.md index 46147324..a269babd 100644 --- a/docs/public/affordance-cancel.md +++ b/docs/public/affordance-cancel.md @@ -8,17 +8,20 @@ Escape는 진행 중인 손과 임시 UI를 버리는 손입니다. Escape 키 ```ts import { escapeAffordance } from "@interactive-os/json-document-affordance"; -escapeAffordance({ key: "Escape" }); -// "cancel" - -escapeAffordance({ type: "pointercancel" }); -// "cancel" - -escapeAffordance({ type: "lostpointercapture" }); -// "cancel" +function onKeyDown(event: KeyboardEvent) { + if (escapeAffordance(event) === "cancel") { + setMenu(null); + drag = null; + } +} + +function onPointerCancel(event: PointerEvent) { + if (escapeAffordance(event) === "cancel") drag = null; +} ``` -호스트는 무엇이 열려 있는지를 가집니다. 어포던스는 버리는 키와 포인터 취소를 닫습니다. +호스트는 무엇이 열려 있는지를 가집니다. 버리는 손은 보통 호스트 화면 +상태입니다. 문서 값은 바꾸지 않습니다. 닫는 손: - Escape: 메뉴, 대화, 드래그, Rename 중단 diff --git a/docs/public/affordance-caret.md b/docs/public/affordance-caret.md index 8898753a..1dfd1a9e 100644 --- a/docs/public/affordance-caret.md +++ b/docs/public/affordance-caret.md @@ -9,23 +9,32 @@ Caret은 글 안의 삽입점입니다. 항목 [Select](affordance-select.md)와 ```ts import { caretAffordance, caretCursor } from "@interactive-os/json-document-affordance"; -caretCursor("horizontal"); -// "text" - -caretCursor("vertical"); -// "vertical-text" - -caretAffordance({ key: "ArrowRight", shiftKey: false, altKey: false }); -// { type: "move", unit: "character", direction: "right", operation: "replace" } - -caretAffordance({ key: "ArrowRight", shiftKey: true, altKey: false }); -// { type: "move", unit: "character", direction: "right", operation: "extend" } - -caretAffordance({ key: "ArrowRight", shiftKey: false, altKey: true }); -// { type: "move", unit: "word", direction: "right", operation: "replace" } - -caretAffordance({ type: "pointer", detail: 1 }); -// { type: "place" } +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입니다. diff --git a/docs/public/affordance-context-menu.md b/docs/public/affordance-context-menu.md index 9bba8ca1..2340b34b 100644 --- a/docs/public/affordance-context-menu.md +++ b/docs/public/affordance-context-menu.md @@ -8,20 +8,21 @@ Context menu는 대상이 있는 자리에서 보조 동작을 여는 손입니 ```ts import { contextMenuAffordance } from "@interactive-os/json-document-affordance"; -contextMenuAffordance({ button: 2 }); -// "open" - -contextMenuAffordance({ key: "ContextMenu" }); -// "open" - -contextMenuAffordance({ key: "F10", shiftKey: true }); -// "open" - -contextMenuAffordance({ key: "Escape" }); -// "cancel" +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` diff --git a/docs/public/affordance-copy-drag.md b/docs/public/affordance-copy-drag.md index 88ba03ed..b6f962e2 100644 --- a/docs/public/affordance-copy-drag.md +++ b/docs/public/affordance-copy-drag.md @@ -7,24 +7,38 @@ Duplicate는 수정 키를 누른 채 드래그하면 원본을 복제하는 손 ```ts import { + dragOffset, dragOperation, + dragShouldCommit, dropAffordance, } from "@interactive-os/json-document-affordance"; -dragOperation({ altKey: false, metaKey: false, ctrlKey: false }); -// "move" - -dragOperation({ altKey: true, metaKey: false, ctrlKey: false }); -// "copy" - -dropAffordance({ - canDrop: true, - operation: dragOperation({ altKey: true, metaKey: false, ctrlKey: false }), -}); -// { accept: true, cursor: "copy" } +function onPointerMove(event: PointerEvent) { + const operation = dragOperation(event); + event.currentTarget.style.cursor = dropAffordance({ + canDrop: true, + operation, + }).cursor; +} + +function onPointerUp(event: PointerEvent) { + const offset = dragOffset(origin, { x: event.clientX, y: event.clientY }); + if (!dragShouldCommit(offset)) return; + if (dragOperation(event) === "copy") { + hostDuplicate(objectIds, offset); + return; + } + editor.dispatch({ + type: "object.translate", + objectIds, + dx: offset.dx, + dy: offset.dy, + }); +} ``` -값의 클립보드 복사/붙이기는 Hands입니다. 이 손은 포인터 복제입니다. +복제 생성은 장르 Intent(호스트 또는 editor)이고, 옮기기는 json-document로 +갑니다. 값의 클립보드 복사/붙이기는 Hands입니다. 닫는 손: - Alt/Option + 드래그 → copy diff --git a/docs/public/affordance-delete.md b/docs/public/affordance-delete.md index 919bbecc..a5d410d4 100644 --- a/docs/public/affordance-delete.md +++ b/docs/public/affordance-delete.md @@ -12,22 +12,14 @@ import { resolveAffordanceKey, } from "@interactive-os/json-document-affordance"; -resolveAffordanceKey({ - key: "Delete", - shiftKey: false, - metaKey: false, - ctrlKey: false, -}); -// { type: "delete" } - -deleteAffordance({ key: "Delete" }); -// { type: "delete", source: "forward" } - -deleteAffordance({ key: "Backspace" }); -// { type: "delete", source: "backward" } - -deleteAffordance({ key: "Backspace", hasSelection: true }); -// { type: "delete", source: "selection" } +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입니다. diff --git a/docs/public/affordance-double-click.md b/docs/public/affordance-double-click.md index 364ea034..2e0293e7 100644 --- a/docs/public/affordance-double-click.md +++ b/docs/public/affordance-double-click.md @@ -9,14 +9,10 @@ Double-click은 `UIEvent.detail === 2`입니다. 글에서는 단어를 고르 ```ts import { clickCountAffordance } from "@interactive-os/json-document-affordance"; -clickCountAffordance(1); -// "click" - -clickCountAffordance(2); -// "double-click" - -clickCountAffordance(3); -// "triple-click" +function onClick(event: MouseEvent, itemId: string) { + if (clickCountAffordance(event.detail) !== "double-click") return; + hostOpen(itemId); +} ``` 호스트는 장르 Intent만 가집니다. 글 단어 범위는 [Caret](affordance-caret.md)에 diff --git a/docs/public/affordance-drag.md b/docs/public/affordance-drag.md index 731ea98c..091a2be2 100644 --- a/docs/public/affordance-drag.md +++ b/docs/public/affordance-drag.md @@ -8,16 +8,27 @@ Drag는 고른 대상을 포인터로 옮기는 손입니다. 누른 점에서 import { dragOffset, dragShouldCommit, + pointerSelect, } from "@interactive-os/json-document-affordance"; -const offset = dragOffset({ x: 24, y: 40 }, { x: 80, y: 36 }); -if (dragShouldCommit(offset)) { - editor.dispatch({ - type: "object.translate", - objectIds: selectedIds, - dx: offset.dx, - dy: offset.dy, - }); +function onPointerDown(event: PointerEvent, objectId: string) { + const mode = pointerSelect(event); + editor.dispatch({ type: "selection.set", objectIds: [objectId], mode }); + event.currentTarget.setPointerCapture(event.pointerId); + drag = { origin: { x: event.clientX, y: event.clientY } }; +} + +function onPointerUp(event: PointerEvent) { + const offset = dragOffset(drag.origin, { x: event.clientX, y: event.clientY }); + if (dragShouldCommit(offset)) { + editor.dispatch({ + type: "object.translate", + objectIds: editor.selectedObjects.map((object) => object.id), + dx: offset.dx, + dy: offset.dy, + }); + } + drag = null; } ``` @@ -26,13 +37,18 @@ if (dragShouldCommit(offset)) { ## TBD ```ts -import { dragKeyboardAffordance } from "@interactive-os/json-document-affordance"; - -dragKeyboardAffordance({ key: "ArrowDown", grab: true }); -// { type: "nudge", dy: 1 } - -dragKeyboardAffordance({ key: "Escape", grab: true }); -// "cancel" +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` diff --git a/docs/public/affordance-drop.md b/docs/public/affordance-drop.md index 68c4ee13..23551a0b 100644 --- a/docs/public/affordance-drop.md +++ b/docs/public/affordance-drop.md @@ -6,19 +6,27 @@ Drop은 [Drag](affordance-drag.md)의 끝입니다. 어디에 둘 수 있는지 커서가 말하고, 못 두는 자리는 `no-drop`입니다. ```ts -import { dropAffordance } from "@interactive-os/json-document-affordance"; - -dropAffordance({ canDrop: true, operation: "move" }); -// { accept: true, cursor: "move" } - -dropAffordance({ canDrop: true, operation: "copy" }); -// { accept: true, cursor: "copy" } - -dropAffordance({ canDrop: false }); -// { accept: false, cursor: "no-drop" } +import { dropAffordance, dragOperation } from "@interactive-os/json-document-affordance"; + +function onPointerMove(event: PointerEvent) { + const drop = dropAffordance({ + canDrop: hostCanDrop(event), + operation: dragOperation(event), + }); + event.currentTarget.style.cursor = drop.cursor; +} + +function onPointerUp(event: PointerEvent, cardId: string, columnId: string) { + const drop = dropAffordance({ + canDrop: hostCanDrop(event), + operation: dragOperation(event), + }); + if (!drop.accept) return; + editor.dispatch({ type: "card.move", cardId, columnId }); +} ``` -호스트는 드롭 자리와 가능한지를 그립니다. 어포던스는 허용·거절 손을 닫습니다. +커서는 호스트 화면 상태이고, 허용된 놓기만 json-document로 갑니다. 닫는 손: - drop 대상 위: `move` / `copy` / `alias` diff --git a/docs/public/affordance-focus.md b/docs/public/affordance-focus.md index 74392c0a..c14b1cc1 100644 --- a/docs/public/affordance-focus.md +++ b/docs/public/affordance-focus.md @@ -9,17 +9,18 @@ Focus는 키보드의 포인터입니다. Tab과 Shift+Tab은 컴포넌트 사 ```ts import { focusAffordance } from "@interactive-os/json-document-affordance"; -focusAffordance({ key: "Tab", shiftKey: false }); -// { type: "tab", direction: "next" } - -focusAffordance({ key: "Tab", shiftKey: true }); -// { type: "tab", direction: "prev" } - -focusAffordance({ key: "ArrowDown", shiftKey: false }); -// { type: "move", direction: "down" } - -focusAffordance({ key: "Home", shiftKey: false }); -// { type: "boundary", edge: "start" } +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)); + } +} ``` 호스트는 마크업과 초점 고리를 그립니다. 화살표가 선택까지 바꿀지는 diff --git a/docs/public/affordance-fold.md b/docs/public/affordance-fold.md index 1eaf1e9d..f42a0306 100644 --- a/docs/public/affordance-fold.md +++ b/docs/public/affordance-fold.md @@ -5,35 +5,55 @@ Expand/Collapse는 나무에서 가지를 접고 펼치는 손입니다. 오른 남습니다. 위아래 화살표는 보이는 줄을 옮깁니다. ```ts -import { treeAffordance } from "@interactive-os/json-document-affordance"; - -treeAffordance( - { type: "move", direction: "right" }, - { expanded: false, hasChildren: true }, -); -// { type: "expand" } - -treeAffordance( - { type: "move", direction: "left" }, - { expanded: true, hasChildren: true }, -); -// { type: "collapse" } +import { + resolveAffordanceKey, + treeAffordance, +} from "@interactive-os/json-document-affordance"; + +function onKeyDown(event: KeyboardEvent, nodeId: string) { + const command = resolveAffordanceKey(event); + if (command?.type !== "move") return; + const hand = treeAffordance(command, { + expanded: expanded.has(nodeId), + hasChildren: hasChildren(nodeId), + }); + 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; + } + editor.dispatch({ type: "selection.set", nodeId, topology, mode: "replace" }); +} ``` 접힘 상태는 호스트가 가진 화면 상태입니다. editor는 보이는 줄 Topology만 -받습니다. 호스트는 `expand` / `collapse`를 접힘 집합에 적용하고, `move`만 -Topology 이웃으로 보냅니다. +받습니다. expand/collapse는 호스트 접힘 집합으로, move는 json-document +선택으로 갑니다. ## TBD ```ts -import { disclosureAffordance } from "@interactive-os/json-document-affordance"; - -disclosureAffordance({ key: "Enter", expanded: false }); -// "expand" - -disclosureAffordance({ key: "Enter", expanded: true }); -// "collapse" +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 접힘 diff --git a/docs/public/affordance-forbid.md b/docs/public/affordance-forbid.md index 47ec8ff5..c2d3b483 100644 --- a/docs/public/affordance-forbid.md +++ b/docs/public/affordance-forbid.md @@ -8,17 +8,16 @@ Not-allowed는 지금 그 손을 쓸 수 없음을 커서로 말하는 손입니 ```ts import { forbiddenCursor } from "@interactive-os/json-document-affordance"; -forbiddenCursor({ allowed: false, dropping: false }); -// "not-allowed" - -forbiddenCursor({ allowed: false, dropping: true }); -// "no-drop" - -forbiddenCursor({ allowed: true, dropping: true, operation: "move" }); -// "move" +function onPointerMove(event: PointerEvent, itemId: string) { + event.currentTarget.style.cursor = forbiddenCursor({ + allowed: hostCanEdit(itemId), + dropping: drag !== null, + }); +} ``` -호스트는 비활성 그림을 그립니다. 어포던스는 거절 커서를 닫습니다. +이 손은 json-document가 아니라 호스트 커서입니다. 거절된 포인터는 +`editor.dispatch`를 호출하지 않습니다. 닫는 손: - `not-allowed`: 요청한 동작 불가 diff --git a/docs/public/affordance-history.md b/docs/public/affordance-history.md index 61573f94..b0056f3d 100644 --- a/docs/public/affordance-history.md +++ b/docs/public/affordance-history.md @@ -4,29 +4,39 @@ Undo는 값과 선택을 함께 되돌리는 손입니다. Mod+Z는 undo, Mod+Sh redo입니다. 버튼은 `canUndo` / `canRedo`를 읽어 꺼집니다. ```ts -import { historyAffordance } from "@interactive-os/json-document-affordance"; +import { + historyAffordance, + resolveAffordanceKey, +} from "@interactive-os/json-document-affordance"; + +const commands = historyAffordance({ + canUndo: editor.snapshot.canUndo, + canRedo: editor.snapshot.canRedo, +}); -const commands = historyAffordance({ canUndo: true, canRedo: false }); -commands.undo.disabled; // false -commands.redo.disabled; // true +function onKeyDown(event: KeyboardEvent) { + const command = resolveAffordanceKey(event); + if (command?.type === "undo") editor.undo(); + if (command?.type === "redo") editor.redo(); +} + + ``` -`resolveAffordanceKey`가 `undo` / `redo` command를 내면 호스트는 -`editor.undo()` / `editor.redo()`만 호출합니다. History 기록 자체는 -Editing이 가지고, 어포던스는 그 손을 닫습니다. +History 기록 자체는 Editing이 가지고, 어포던스는 그 손을 닫습니다. ## TBD ```ts -import { resolveAffordanceKey } from "@interactive-os/json-document-affordance"; - -resolveAffordanceKey({ - key: "y", - shiftKey: false, - metaKey: true, - ctrlKey: false, -}); -// { type: "redo" } +function onKeyDown(event: KeyboardEvent) { + const command = resolveAffordanceKey(event); + if (command?.type === "redo") editor.redo(); +} ``` - Mod+Y redo 변종 diff --git a/docs/public/affordance-hover.md b/docs/public/affordance-hover.md index 12d0782b..03e2c37c 100644 --- a/docs/public/affordance-hover.md +++ b/docs/public/affordance-hover.md @@ -3,26 +3,22 @@ TBD. Hover는 누르지 않은 포인터가 대상 위에 있을 때 가능한 손을 드러냅니다. -커서가 바뀌고, 잠시 뒤 툴팁이 열릴 수 있습니다. `help` 커서는 도움말이 +커서가 바뀌고, 잠시 뒤 툴팁이 열릴 수 있습니다. `help` 커서가 도움말이 있음을 가리킵니다. ```ts import { hoverAffordance, hoverCursor } from "@interactive-os/json-document-affordance"; -hoverCursor("help"); -// "help" - -hoverAffordance({ elapsedMs: 120 }); -// "hint" - -hoverAffordance({ elapsedMs: 400 }); -// "tooltip" - -hoverAffordance({ elapsedMs: 0, inside: false }); -// null +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 diff --git a/docs/public/affordance-marquee.md b/docs/public/affordance-marquee.md index 8f90ed47..95ab9d67 100644 --- a/docs/public/affordance-marquee.md +++ b/docs/public/affordance-marquee.md @@ -13,18 +13,18 @@ import { pointerSelect, } from "@interactive-os/json-document-affordance"; -const rect = marqueeRect({ x: 24, y: 40 }, { x: 120, y: 90 }); -// { x: 24, y: 40, width: 96, height: 50 } - -marqueeShouldCommit(rect); -// true - -pointerSelect({ shiftKey: true, metaKey: false, ctrlKey: false }); -// "extend" +function onPointerUp(event: PointerEvent) { + const rect = marqueeRect(origin, { x: event.clientX, y: event.clientY }); + if (!marqueeShouldCommit(rect)) return; + const objectIds = hostHits(rect); + const mode = pointerSelect(event); + editor.dispatch({ type: "selection.set", objectIds, mode }); +} ``` 호스트는 히트 테스트와 기하를 계산합니다. 어떤 키가 범위에 들어오는지는 -호스트가 보고, 손이 replace인지 extend인지는 `pointerSelect`가 닫습니다. +호스트가 보고, 손이 replace인지 extend인지는 Affordance가 닫고, 선택은 +json-document로 갑니다. 닫는 손: - 빈 곳에서 pointerdown → move → up diff --git a/docs/public/affordance-nudge.md b/docs/public/affordance-nudge.md index 21fad51f..f70d329b 100644 --- a/docs/public/affordance-nudge.md +++ b/docs/public/affordance-nudge.md @@ -9,17 +9,19 @@ Shift+화살표는 큰 단위입니다. 항목 이웃으로 초점을 옮기는 ```ts import { nudgeAffordance } from "@interactive-os/json-document-affordance"; -nudgeAffordance({ key: "ArrowRight", shiftKey: false }); -// { dx: 1, dy: 0 } - -nudgeAffordance({ key: "ArrowUp", shiftKey: true }); -// { dx: 0, dy: -10 } - -nudgeAffordance({ key: "ArrowDown", shiftKey: false }); -// { dx: 0, dy: 1 } +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, 칸, 그리드)를 곱합니다. 어포던스는 손과 수정 키를 닫습니다. +호스트는 단위(px, 칸, 그리드)를 곱합니다. 이동은 json-document로 갑니다. 닫는 손: - Arrow: 1 단위 diff --git a/docs/public/affordance-pan.md b/docs/public/affordance-pan.md index 0fee89f0..410ac4c5 100644 --- a/docs/public/affordance-pan.md +++ b/docs/public/affordance-pan.md @@ -6,26 +6,22 @@ Pan은 대상을 옮기지 않고 보이는 평면을 옮기는 손입니다. `g `grabbing` / `all-scroll` 커서가 이 손을 가리킵니다. ```ts -import { - panAffordance, - dragOffset, -} from "@interactive-os/json-document-affordance"; - -panAffordance({ spaceKey: true, buttons: 0 }); -// { cursor: "grab" } - -panAffordance({ spaceKey: true, buttons: 1 }); -// { cursor: "grabbing" } - -panAffordance({ buttons: 4 }); -// { cursor: "all-scroll" } - -dragOffset({ x: 80, y: 40 }, { x: 64, y: 52 }); -// { dx: -16, dy: 12 } +import { panAffordance, dragOffset } from "@interactive-os/json-document-affordance"; + +function onPointerMove(event: PointerEvent) { + const pan = panAffordance({ spaceKey, buttons: event.buttons }); + if (!pan) return; + event.currentTarget.style.cursor = pan.cursor; + const offset = dragOffset(origin, { x: event.clientX, y: event.clientY }); + setViewport({ + x: viewport.x + offset.dx, + y: viewport.y + offset.dy, + }); +} ``` -호스트는 뷰포트 변환을 가집니다. 이동량은 [Drag](affordance-drag.md)와 -같은 `dragOffset`입니다. +이 손은 json-document가 아니라 호스트 뷰포트입니다. 이동량은 +[Drag](affordance-drag.md)와 같은 `dragOffset`입니다. 닫는 손: - Space + 드래그 diff --git a/docs/public/affordance-rename.md b/docs/public/affordance-rename.md index 6f20c3c3..18c7a3cc 100644 --- a/docs/public/affordance-rename.md +++ b/docs/public/affordance-rename.md @@ -8,23 +8,25 @@ Rename은 고른 대상의 레이블을 고치는 손입니다. F2와 느린 dou ```ts import { renameAffordance } from "@interactive-os/json-document-affordance"; -renameAffordance({ key: "F2" }); -// "begin" - -renameAffordance({ type: "pointer", detail: 2, intervalMs: 900 }); -// "begin" - -renameAffordance({ type: "pointer", detail: 2, intervalMs: 200 }); -// null - -renameAffordance({ key: "Enter" }); -// "commit" - -renameAffordance({ key: "Escape" }); -// "cancel" +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); + } +} ``` -호스트는 레이블 필드를 그립니다. 글 편집 자체는 Hands와 [Caret](affordance-caret.md)입니다. +호스트는 레이블 필드를 그립니다. begin/cancel은 호스트 화면 상태이고, +commit만 json-document로 갑니다. 글 편집 자체는 Hands와 [Caret](affordance-caret.md)입니다. 닫는 손: - F2 diff --git a/docs/public/affordance-resize.md b/docs/public/affordance-resize.md index 8f4c1661..2545b408 100644 --- a/docs/public/affordance-resize.md +++ b/docs/public/affordance-resize.md @@ -6,45 +6,38 @@ Resize는 가장자리·모서리·칸 경계·창 분할선을 움직이는 손 CSS predefined 리사이즈 커서가 이 손을 닫습니다. ```ts -import { - resizeCursor, - resizeOffset, - resolveAffordanceKey, -} from "@interactive-os/json-document-affordance"; - -resizeCursor("se"); -// "se-resize" - -resizeCursor("column"); -// "col-resize" - -resizeOffset( - { x: 80, y: 40 }, - { x: 112, y: 72 }, - "se", - { shiftKey: false, altKey: false }, -); -// { dx: 32, dy: 32 } - -resizeOffset( - { x: 80, y: 40 }, - { x: 112, y: 56 }, - "se", - { shiftKey: true, altKey: false }, -); -// { dx: 32, dy: 32 } - -resolveAffordanceKey({ - key: "ArrowRight", - shiftKey: false, - metaKey: false, - ctrlKey: false, -}); -// { type: "move", direction: "right", operation: "replace" } +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, + }); +} ``` -호스트는 핸들과 기하를 그립니다. 분할선 화살표는 APG Window Splitter와 -같고, Shift는 비율, Alt는 가운데 기준입니다. +커서는 호스트 화면 상태이고, 확정된 크기만 json-document로 갑니다. +분할선 화살표는 APG Window Splitter와 같고, Shift는 비율, Alt는 가운데 +기준입니다. 닫는 손: - 모서리: `n-resize` … `nwse-resize` diff --git a/docs/public/affordance-scroll.md b/docs/public/affordance-scroll.md index 2dbe24fc..99413306 100644 --- a/docs/public/affordance-scroll.md +++ b/docs/public/affordance-scroll.md @@ -11,23 +11,25 @@ import { autoscrollAffordance, } from "@interactive-os/json-document-affordance"; -wheelAffordance({ deltaY: 80, ctrlKey: false, metaKey: false }); -// { type: "scroll", dy: 80 } - -wheelAffordance({ deltaY: 80, ctrlKey: true, metaKey: false }); -// { type: "zoom", delta: -80 } - -autoscrollAffordance({ - x: 4, - y: 80, - width: 240, - height: 180, - edge: 16, -}); -// { dx: -1, dy: 0 } +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를 그립니다. 어포던스는 휠과 자동 굴리기를 닫습니다. +이 손은 호스트 overflow입니다. 문서 값은 바꾸지 않습니다. 닫는 손: - wheel (수정 키 없음) diff --git a/docs/public/affordance-select.md b/docs/public/affordance-select.md index 2f305dda..5c671062 100644 --- a/docs/public/affordance-select.md +++ b/docs/public/affordance-select.md @@ -10,35 +10,43 @@ import { resolveAffordanceKey, } from "@interactive-os/json-document-affordance"; -pointerSelect({ shiftKey: true, metaKey: false, ctrlKey: false }); -// "extend" - -const command = resolveAffordanceKey({ - key: "ArrowDown", - shiftKey: true, - metaKey: false, - ctrlKey: false, +function onPointerDown(event: PointerEvent, itemId: string) { + const mode = pointerSelect(event); + editor.dispatch({ type: "selection.set", itemId, mode }); +} + +const editing = useEditing({ + source: editor, + operationFromEvent: (event) => pointerSelect(event), + onSelect: (itemId, mode) => { + editor.dispatch({ type: "selection.set", itemId, mode }); + }, + keyboard: { + resolve: resolveAffordanceKey, + neighbor: (key, command) => moveLinePoint(ids, key, command.direction), + }, }); -// { type: "move", direction: "down", operation: "extend" } ``` 호스트는 보이는 키와 장르 Intent만 넘깁니다. keymap을 덮어쓰지 않습니다. -React에서 범위와 커서를 그리려면 `useEditing`의 `onSelect`에 -`pointerSelect` 결과를 연결합니다. - -Keyboard Adapter는 chord를 command로 바꿉니다. Select는 그 command가 -replace인지 extend인지를 닫습니다. ## TBD ```ts -import { selectAllAffordance } from "@interactive-os/json-document-affordance"; - -selectAllAffordance({ allSelected: false }); -// "select-all" - -selectAllAffordance({ allSelected: true }); -// "clear" +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 토글 diff --git a/docs/public/affordance-snap.md b/docs/public/affordance-snap.md index 4ec296ac..47d76927 100644 --- a/docs/public/affordance-snap.md +++ b/docs/public/affordance-snap.md @@ -7,22 +7,29 @@ Snap은 [Drag](affordance-drag.md)·[Resize](affordance-resize.md)· 수정 키를 누르면 붙지 않습니다. ```ts -import { snapAffordance } from "@interactive-os/json-document-affordance"; - -snapAffordance({ x: 47, y: 51 }, { grid: 8, disable: false }); -// { x: 48, y: 48 } - -snapAffordance({ x: 47, y: 51 }, { grid: 8, disable: true }); -// { x: 47, y: 51 } - -snapAffordance( - { x: 47, y: 51 }, - { guides: [{ x: 50 }, { y: 48 }], disable: false }, -); -// { x: 50, y: 48 } +import { + dragOffset, + dragShouldCommit, + snapAffordance, +} from "@interactive-os/json-document-affordance"; + +function onPointerUp(event: PointerEvent, object: { id: string; x: number; y: number }) { + const offset = dragOffset(origin, { x: event.clientX, y: event.clientY }); + if (!dragShouldCommit(offset)) return; + const snapped = snapAffordance( + { x: object.x + offset.dx, y: object.y + offset.dy }, + { grid: 8, disable: event.metaKey || event.ctrlKey }, + ); + editor.dispatch({ + type: "object.translate", + objectIds: [object.id], + dx: snapped.x - object.x, + dy: snapped.y - object.y, + }); +} ``` -호스트는 가이드 기하를 가집니다. 어포던스는 붙임/해제 손을 닫습니다. +가이드 기하는 호스트가 가지고, 붙은 좌표만 json-document로 갑니다. 닫는 손: - 이동·리사이즈 중 스냅 diff --git a/docs/public/affordance-triple-click.md b/docs/public/affordance-triple-click.md index 104bcbd8..99c353a3 100644 --- a/docs/public/affordance-triple-click.md +++ b/docs/public/affordance-triple-click.md @@ -8,8 +8,15 @@ Triple-click은 `UIEvent.detail === 3`입니다. 글에서는 줄 또는 문단 ```ts import { clickCountAffordance } from "@interactive-os/json-document-affordance"; -clickCountAffordance(3); -// "triple-click" +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)과 diff --git a/docs/public/affordance-typeahead.md b/docs/public/affordance-typeahead.md index 070c9596..662ddde7 100644 --- a/docs/public/affordance-typeahead.md +++ b/docs/public/affordance-typeahead.md @@ -8,35 +8,25 @@ Typeahead는 인쇄 글쇠로 목록·나무에서 이름으로 건너뛰는 손 ```ts import { typeaheadAffordance } from "@interactive-os/json-document-affordance"; -typeaheadAffordance({ - buffer: "", - key: "A", - elapsedMs: 0, - names: ["Apple", "Apricot", "Pear"], - from: "Pear", -}); -// { buffer: "A", name: "Apple" } - -typeaheadAffordance({ - buffer: "A", - key: "p", - elapsedMs: 80, - names: ["Apple", "Apricot", "Pear"], - from: "Apple", -}); -// { buffer: "Ap", name: "Apple" } - -typeaheadAffordance({ - buffer: "Ap", - key: "x", - elapsedMs: 1200, - names: ["Apple", "Apricot", "Pear"], - from: "Apple", -}); -// { buffer: "x", name: null } +function onKeyDown(event: KeyboardEvent) { + const hand = typeaheadAffordance({ + buffer, + key: event.key, + elapsedMs: event.timeStamp - lastType, + names: items.map((item) => item.label), + from: focusKey, + }); + setBuffer(hand.buffer); + lastType = event.timeStamp; + if (hand.name) { + const itemId = items.find((item) => item.label === hand.name)?.id; + if (itemId) setFocusKey(itemId); + } +} ``` -호스트는 보이는 이름을 줍니다. 어포던스는 버퍼 창과 점프 규칙을 닫습니다. +호스트는 보이는 이름을 줍니다. 점프한 키는 호스트 Focus로 가고, 선택까지 +바꿀지는 장르 Intent입니다. 닫는 손: - 한 문자: 그 prefix의 다음 항목 diff --git a/docs/public/affordance-zoom.md b/docs/public/affordance-zoom.md index 03c811f1..97cef23a 100644 --- a/docs/public/affordance-zoom.md +++ b/docs/public/affordance-zoom.md @@ -12,20 +12,23 @@ import { wheelAffordance, } from "@interactive-os/json-document-affordance"; -zoomCursor("in"); -// "zoom-in" - -zoomAffordance({ key: "=", metaKey: true, ctrlKey: false }); -// { type: "in" } - -zoomAffordance({ key: "-", metaKey: true, ctrlKey: false }); -// { type: "out" } - -wheelAffordance({ deltaY: -80, ctrlKey: true, metaKey: false }); -// { type: "zoom", delta: 80 } +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 diff --git a/docs/public/affordance.md b/docs/public/affordance.md index 5afdfb57..b9c4309e 100644 --- a/docs/public/affordance.md +++ b/docs/public/affordance.md @@ -9,6 +9,28 @@ 대체하지 않습니다. TBD 페이지의 코드는 사용법 명세입니다. 패키지에 아직 없습니다. +호스트 이벤트와 json-document 사이를 이렇게 잇습니다. 손은 문서 작업이 +되기도 하고, 커서·접힘·뷰포트 같은 호스트 화면 상태가 되기도 합니다. + +```ts +import { + pointerSelect, + forbiddenCursor, +} from "@interactive-os/json-document-affordance"; + +function onPointerDown(event: PointerEvent, itemId: string) { + const mode = pointerSelect(event); + editor.dispatch({ type: "selection.set", itemId, mode }); +} + +function onPointerMove(event: PointerEvent) { + event.currentTarget.style.cursor = forbiddenCursor({ + allowed: hostCanDrop(event), + dropping: true, + }); +} +``` + ```sh npm i @interactive-os/json-document-affordance ``` From 44aa004443fc2375f3e07e51e9d8f1b2f4553737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Thu, 20 Aug 2026 22:41:50 +0900 Subject: [PATCH 07/14] =?UTF-8?q?=EC=96=B4=ED=8F=AC=EB=8D=98=EC=8A=A4:=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20{=20hand,=20cursor,=20commit=20}=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=EB=A1=9C=20=EB=8D=B0=EB=AA=A8=EA=B0=80=20?= =?UTF-8?q?=EC=9B=80=EC=A7=81=EC=9D=B8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 모든 손은 AffordanceResult를 돌리고 applyAffordance로 잇는다. Listbox typeahead와 Canvas drag/marquee/nudge가 그 usage로 동작한다. --- docs/public/affordance-select.md | 16 +- docs/public/affordance.md | 19 +- packages/json-document-affordance/README.md | 29 +- packages/json-document-affordance/src/drag.ts | 111 ++++++++ packages/json-document-affordance/src/fold.ts | 18 +- .../json-document-affordance/src/history.ts | 21 +- .../json-document-affordance/src/index.ts | 13 +- .../json-document-affordance/src/result.ts | 68 +++++ .../json-document-affordance/src/select.ts | 88 +++++- .../tests/affordance.test.ts | 97 +++++-- site/src/routes/widgets/CanvasWidgetRoute.tsx | 255 ++++++++++++++---- .../routes/widgets/DocumentWidgetRoute.tsx | 17 +- site/src/routes/widgets/GridWidgetRoute.tsx | 17 +- .../src/routes/widgets/ListboxWidgetRoute.tsx | 51 +++- .../src/routes/widgets/ToolbarWidgetRoute.tsx | 13 +- site/src/routes/widgets/TreeWidgetRoute.tsx | 16 +- site/src/shared/widget-binding/history.ts | 22 +- site/tests/browser/widgets.spec.ts | 7 + 18 files changed, 705 insertions(+), 173 deletions(-) create mode 100644 packages/json-document-affordance/src/result.ts diff --git a/docs/public/affordance-select.md b/docs/public/affordance-select.md index 5c671062..ef0918e8 100644 --- a/docs/public/affordance-select.md +++ b/docs/public/affordance-select.md @@ -6,23 +6,31 @@ Select는 대상을 집는 손입니다. 클릭은 그 대상으로 바꾸고, S ```ts import { + applyAffordance, + keyboardCommandFrom, pointerSelect, resolveAffordanceKey, + selectOperationFrom, } from "@interactive-os/json-document-affordance"; function onPointerDown(event: PointerEvent, itemId: string) { - const mode = pointerSelect(event); - editor.dispatch({ type: "selection.set", itemId, mode }); + applyAffordance(pointerSelect(event), { + hand: (hand) => { + if (hand.type === "select") { + editor.dispatch({ type: "selection.set", itemId, mode: hand.operation }); + } + }, + }); } const editing = useEditing({ source: editor, - operationFromEvent: (event) => pointerSelect(event), + operationFromEvent: (event) => selectOperationFrom(pointerSelect(event)), onSelect: (itemId, mode) => { editor.dispatch({ type: "selection.set", itemId, mode }); }, keyboard: { - resolve: resolveAffordanceKey, + resolve: (stroke) => keyboardCommandFrom(resolveAffordanceKey(stroke)), neighbor: (key, command) => moveLinePoint(ids, key, command.direction), }, }); diff --git a/docs/public/affordance.md b/docs/public/affordance.md index b9c4309e..6eb54b0c 100644 --- a/docs/public/affordance.md +++ b/docs/public/affordance.md @@ -14,19 +14,26 @@ ```ts import { - pointerSelect, + applyAffordance, forbiddenCursor, + pointerSelect, } from "@interactive-os/json-document-affordance"; function onPointerDown(event: PointerEvent, itemId: string) { - const mode = pointerSelect(event); - editor.dispatch({ type: "selection.set", itemId, mode }); + applyAffordance(pointerSelect(event), { + hand: (hand) => { + if (hand.type === "select") { + editor.dispatch({ type: "selection.set", itemId, mode: hand.operation }); + } + }, + }); } function onPointerMove(event: PointerEvent) { - event.currentTarget.style.cursor = forbiddenCursor({ - allowed: hostCanDrop(event), - dropping: true, + applyAffordance(forbiddenCursor({ allowed: hostCanDrop(event), dropping: true }), { + cursor: (cursor) => { + event.currentTarget.style.cursor = cursor; + }, }); } ``` diff --git a/packages/json-document-affordance/README.md b/packages/json-document-affordance/README.md index 682c3fd4..f40df07a 100644 --- a/packages/json-document-affordance/README.md +++ b/packages/json-document-affordance/README.md @@ -1,8 +1,8 @@ # @interactive-os/json-document-affordance Official keyboard and mouse editing affordances for json-document hosts. -The package closes conventional select, fold, drag, and undo/redo grammar. -Hosts keep markup and genre Intent. It does not render widgets. +Every affordance returns `{ hand, cursor?, commit? }`. Hosts keep markup +and genre Intent. It does not render widgets. ```sh npm i @interactive-os/json-document-affordance @@ -10,28 +10,21 @@ npm i @interactive-os/json-document-affordance ```ts import { + applyAffordance, pointerSelect, - resolveAffordanceKey, - treeAffordance, - dragOffset, - dragShouldCommit, - historyAffordance, } from "@interactive-os/json-document-affordance"; -pointerSelect({ shiftKey: true, metaKey: false, ctrlKey: false }); -// "extend" - -resolveAffordanceKey({ - key: "ArrowDown", - shiftKey: false, - metaKey: false, - ctrlKey: false, +applyAffordance(pointerSelect(event), { + hand: (hand) => { + if (hand.type === "select") { + editor.dispatch({ type: "selection.set", itemId, mode: hand.operation }); + } + }, }); -// { type: "move", direction: "down", operation: "replace" } ``` Keyboard Adapter still translates chords. This package decides the -affordance policy those commands mean. React Connector still answers -selection queries. Hosts still draw the product screen. +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/src/drag.ts b/packages/json-document-affordance/src/drag.ts index 4e03e26a..87ff49a0 100644 --- a/packages/json-document-affordance/src/drag.ts +++ b/packages/json-document-affordance/src/drag.ts @@ -1,3 +1,6 @@ +import type { WebModifierState } from "@interactive-os/json-document-web"; +import type { AffordanceResult } from "./result.js"; + export type Point = { readonly x: number; readonly y: number; @@ -8,6 +11,13 @@ export type DragOffset = { readonly dy: number; }; +export type Rect = { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +}; + export function dragOffset(origin: Point, point: Point): DragOffset { return { dx: point.x - origin.x, @@ -18,3 +28,104 @@ export function dragOffset(origin: Point, point: Point): DragOffset { export function dragShouldCommit(offset: DragOffset): boolean { return offset.dx !== 0 || offset.dy !== 0; } + +export function dragAffordance(origin: Point, point: Point): AffordanceResult { + const offset = dragOffset(origin, point); + return { + hand: { type: "translate", dx: offset.dx, dy: offset.dy }, + cursor: "grabbing", + commit: dragShouldCommit(offset), + }; +} + +export function dragOperation(modifiers: WebModifierState & { readonly altKey?: boolean }): AffordanceResult { + if (modifiers.altKey) return { hand: { type: "copy" }, cursor: "copy" }; + return { hand: { type: "move-drop" }, cursor: "move" }; +} + +export 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): AffordanceResult { + const rect = marqueeRect(origin, point); + return { + hand: { type: "select", operation: "replace" }, + cursor: "crosshair", + commit: rect.width > 0 || rect.height > 0, + }; +} + +export function panAffordance(input: { + readonly spaceKey: boolean; + readonly buttons: number; +}): AffordanceResult { + if (!input.spaceKey && input.buttons !== 4) return { hand: null }; + if (input.buttons === 0) return { hand: null, cursor: "grab" }; + return { hand: { type: "translate", dx: 0, dy: 0 }, cursor: "grabbing" }; +} + +export function snapAffordance( + point: Point, + options: { readonly grid: number; readonly disable?: boolean }, +): AffordanceResult { + if (options.disable || options.grid <= 0) { + return { hand: { type: "translate", dx: point.x, dy: point.y }, commit: true }; + } + const grid = options.grid; + return { + hand: { + type: "translate", + dx: Math.round(point.x / grid) * grid, + dy: Math.round(point.y / grid) * grid, + }, + commit: true, + }; +} + +export function nudgeAffordance(stroke: { + readonly key: string; + readonly shiftKey: boolean; +}): AffordanceResult { + 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"; +}): AffordanceResult { + if (!input.canDrop) return { hand: null, cursor: "no-drop", commit: false }; + if (input.operation === "copy") return { hand: { type: "copy" }, cursor: "copy", commit: true }; + return { hand: { type: "move-drop" }, cursor: "move", commit: true }; +} + +export function forbiddenCursor(input: { + readonly allowed: boolean; + readonly dropping?: boolean; +}): AffordanceResult { + if (input.allowed) { + return { hand: null, cursor: input.dropping ? "move" : "default" }; + } + return { hand: null, cursor: input.dropping ? "no-drop" : "not-allowed", commit: false }; +} + +export function hoverAffordance(input: { + readonly elapsedMs: number; + readonly inside: boolean; + readonly delayMs?: number; +}): AffordanceResult { + 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 index eb4a1e7c..61aa4c68 100644 --- a/packages/json-document-affordance/src/fold.ts +++ b/packages/json-document-affordance/src/fold.ts @@ -1,3 +1,5 @@ +import type { AffordanceResult } from "./result.js"; + export type TreeFoldNode = { readonly expanded: boolean; readonly hasChildren: boolean; @@ -13,12 +15,20 @@ export type TreeAffordance = export function treeAffordance( command: { readonly type: "move"; readonly direction: TreeMoveDirection }, node: TreeFoldNode, -): TreeAffordance { +): AffordanceResult { if (command.direction === "right" && node.hasChildren && !node.expanded) { - return { type: "expand" }; + return { hand: { type: "expand" } }; } if (command.direction === "left" && node.hasChildren && node.expanded) { - return { type: "collapse" }; + return { hand: { type: "collapse" } }; } - return { type: "move", direction: command.direction }; + return { hand: { type: "move", direction: command.direction, operation: "replace" } }; +} + +export function disclosureAffordance(input: { + readonly key: string; + readonly expanded: boolean; +}): AffordanceResult { + 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 index 9ad144ba..00306d1a 100644 --- a/packages/json-document-affordance/src/history.ts +++ b/packages/json-document-affordance/src/history.ts @@ -1,3 +1,5 @@ +import type { AffordanceResult } from "./result.js"; + export type HistoryAffordanceName = "undo" | "redo"; export type HistoryAffordance = { @@ -13,9 +15,22 @@ export type HistoryAffordanceMap = { export function historyAffordance(snapshot: { readonly canUndo: boolean; readonly canRedo: boolean; -}): HistoryAffordanceMap { +}): AffordanceResult { return { - undo: { name: "undo", disabled: !snapshot.canUndo }, - redo: { name: "redo", disabled: !snapshot.canRedo }, + hand: { + type: "history", + undo: { name: "undo", disabled: !snapshot.canUndo }, + redo: { name: "redo", disabled: !snapshot.canRedo }, + }, }; } + +export function historyCommandsFrom(result: AffordanceResult): HistoryAffordanceMap { + if (result.hand?.type !== "history") { + return { + undo: { name: "undo", disabled: true }, + redo: { name: "redo", disabled: true }, + }; + } + return { undo: result.hand.undo, redo: result.hand.redo }; +} diff --git a/packages/json-document-affordance/src/index.ts b/packages/json-document-affordance/src/index.ts index 1cf9a5e9..a9a5fb8b 100644 --- a/packages/json-document-affordance/src/index.ts +++ b/packages/json-document-affordance/src/index.ts @@ -1,8 +1,9 @@ -export { dragOffset, dragShouldCommit } from "./drag.js"; -export type { DragOffset, Point } from "./drag.js"; -export { treeAffordance } from "./fold.js"; +export { dragAffordance, dragOffset, dragOperation, dragShouldCommit, dropAffordance, forbiddenCursor, hoverAffordance, marqueeAffordance, marqueeRect, nudgeAffordance, panAffordance, snapAffordance } from "./drag.js"; +export type { DragOffset, 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 { historyAffordance, historyCommandsFrom } from "./history.js"; export type { HistoryAffordance, HistoryAffordanceMap, HistoryAffordanceName } from "./history.js"; -export { pointerSelect, resolveAffordanceKey } from "./select.js"; -export type { SelectOperation } from "./select.js"; +export { applyAffordance, keyboardCommandFrom, selectOperationFrom } from "./result.js"; +export type { AffordanceHand, 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..24e23ae2 --- /dev/null +++ b/packages/json-document-affordance/src/result.ts @@ -0,0 +1,68 @@ +import type { WebKeyboardCommand } from "@interactive-os/json-document-web"; + +export type SelectOperation = "replace" | "extend" | "toggle"; + +export type AffordanceHand = + | { readonly type: "select"; readonly operation: SelectOperation } + | WebKeyboardCommand + | { 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: "place" } + | { readonly type: "open-menu" } + | { readonly type: "rename"; readonly phase: "begin" | "commit" | "cancel" } + | { readonly type: "hover"; readonly phase: "hint" | "tooltip" } + | { readonly type: "scroll"; readonly dx: number; readonly dy: number } + | { readonly type: "zoom"; readonly direction?: "in" | "out"; readonly delta?: number } + | { 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 AffordanceResult = { + readonly hand: H | null; + readonly cursor?: string; + readonly commit?: boolean; +}; + +export function applyAffordance( + result: AffordanceResult, + actions: { + readonly cursor?: (cursor: string) => void; + readonly hand?: (hand: AffordanceHand) => void; + }, +): void { + if (result.cursor !== undefined) actions.cursor?.(result.cursor); + if (result.hand) actions.hand?.(result.hand); +} + +export function selectOperationFrom(result: AffordanceResult): SelectOperation { + return result.hand?.type === "select" ? result.hand.operation : "replace"; +} + +export function keyboardCommandFrom(result: AffordanceResult): WebKeyboardCommand | null { + const hand = result.hand; + if (!hand) return null; + if ( + hand.type === "move" + || hand.type === "boundary" + || hand.type === "toggle" + || hand.type === "delete" + || hand.type === "undo" + || hand.type === "redo" + ) { + return hand; + } + return null; +} diff --git a/packages/json-document-affordance/src/select.ts b/packages/json-document-affordance/src/select.ts index a2ed43a8..9be02330 100644 --- a/packages/json-document-affordance/src/select.ts +++ b/packages/json-document-affordance/src/select.ts @@ -1,19 +1,93 @@ import { createWebKeyboardAdapter, selectionOperationFromModifiers, - type WebKeyboardCommand, type WebKeyboardStroke, - type WebModifierState, } from "@interactive-os/json-document-web"; +import { + type AffordanceResult, + type SelectOperation, +} from "./result.js"; const keyboard = createWebKeyboardAdapter(); -export type SelectOperation = "replace" | "extend" | "toggle"; +export type { SelectOperation }; + +export function pointerSelect(modifiers: { + readonly shiftKey?: boolean; + readonly metaKey?: boolean; + readonly ctrlKey?: boolean; +}): AffordanceResult { + return { + hand: { + type: "select", + operation: selectionOperationFromModifiers({ + shiftKey: modifiers.shiftKey ?? false, + metaKey: modifiers.metaKey ?? false, + ctrlKey: modifiers.ctrlKey ?? false, + }), + }, + }; +} + +export function resolveAffordanceKey(stroke: WebKeyboardStroke): AffordanceResult { + return { hand: keyboard.resolve(stroke) }; +} + +export function selectAllAffordance( + stroke: Pick, + state: { readonly allSelected: boolean }, +): AffordanceResult { + 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; +}): AffordanceResult { + 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): AffordanceResult { + 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 }): AffordanceResult { + 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 pointerSelect(modifiers: WebModifierState): SelectOperation { - return selectionOperationFromModifiers(modifiers); +export function escapeAffordance(input: { readonly key?: string; readonly type?: string }): AffordanceResult { + if (input.key === "Escape" || input.type === "pointercancel" || input.type === "lostpointercapture") { + return { hand: { type: "cancel" } }; + } + return { hand: null }; } -export function resolveAffordanceKey(stroke: WebKeyboardStroke): WebKeyboardCommand | null { - return keyboard.resolve(stroke); +export function focusAffordance(stroke: Pick): AffordanceResult { + 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 index ad3cd309..c7965b78 100644 --- a/packages/json-document-affordance/tests/affordance.test.ts +++ b/packages/json-document-affordance/tests/affordance.test.ts @@ -1,76 +1,119 @@ import { describe, expect, test } from "vitest"; import { - dragOffset, - dragShouldCommit, + applyAffordance, + dragAffordance, historyAffordance, + keyboardCommandFrom, pointerSelect, resolveAffordanceKey, + selectOperationFrom, + snapAffordance, treeAffordance, + typeaheadAffordance, } from "../src/index.js"; describe("pointerSelect", () => { test("maps conventional modifiers to replace, extend, and toggle", () => { - expect(pointerSelect({ shiftKey: false, metaKey: false, ctrlKey: false })).toBe("replace"); - expect(pointerSelect({ shiftKey: true, metaKey: false, ctrlKey: false })).toBe("extend"); - expect(pointerSelect({ shiftKey: false, metaKey: true, ctrlKey: false })).toBe("toggle"); - expect(pointerSelect({ shiftKey: false, metaKey: false, ctrlKey: true })).toBe("toggle"); + expect(selectOperationFrom(pointerSelect({ shiftKey: false, metaKey: false, ctrlKey: false }))).toBe("replace"); + expect(selectOperationFrom(pointerSelect({ shiftKey: true, metaKey: false, ctrlKey: false }))).toBe("extend"); + expect(selectOperationFrom(pointerSelect({ shiftKey: false, metaKey: true, ctrlKey: false }))).toBe("toggle"); + expect(selectOperationFrom(pointerSelect({ shiftKey: false, metaKey: false, ctrlKey: true }))).toBe("toggle"); }); }); describe("resolveAffordanceKey", () => { test("closes the conventional keymap without a host override", () => { - expect(resolveAffordanceKey({ key: "ArrowDown", shiftKey: false, metaKey: false, ctrlKey: false })).toEqual({ + expect(keyboardCommandFrom(resolveAffordanceKey({ key: "ArrowDown", shiftKey: false, metaKey: false, ctrlKey: false }))).toEqual({ type: "move", direction: "down", operation: "replace", }); - expect(resolveAffordanceKey({ key: "ArrowDown", shiftKey: true, metaKey: false, ctrlKey: false })).toEqual({ + expect(keyboardCommandFrom(resolveAffordanceKey({ key: "ArrowDown", shiftKey: true, metaKey: false, ctrlKey: false }))).toEqual({ type: "move", direction: "down", operation: "extend", }); - expect(resolveAffordanceKey({ key: "z", shiftKey: false, metaKey: true, ctrlKey: false })).toEqual({ type: "undo" }); - expect(resolveAffordanceKey({ key: "z", shiftKey: true, metaKey: true, ctrlKey: false })).toEqual({ type: "redo" }); - expect(resolveAffordanceKey({ key: "Delete", shiftKey: false, metaKey: false, ctrlKey: false })).toEqual({ type: "delete" }); + expect(keyboardCommandFrom(resolveAffordanceKey({ key: "z", shiftKey: false, metaKey: true, ctrlKey: false }))).toEqual({ type: "undo" }); + expect(keyboardCommandFrom(resolveAffordanceKey({ key: "z", shiftKey: true, metaKey: true, ctrlKey: false }))).toEqual({ type: "redo" }); + expect(keyboardCommandFrom(resolveAffordanceKey({ key: "Delete", shiftKey: false, metaKey: false, ctrlKey: false }))).toEqual({ 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 })).toEqual({ + expect(treeAffordance({ type: "move", direction: "right" }, { expanded: false, hasChildren: true }).hand).toEqual({ type: "expand", }); - expect(treeAffordance({ type: "move", direction: "left" }, { expanded: true, hasChildren: true })).toEqual({ + expect(treeAffordance({ type: "move", direction: "left" }, { expanded: true, hasChildren: true }).hand).toEqual({ type: "collapse", }); - expect(treeAffordance({ type: "move", direction: "right" }, { expanded: true, hasChildren: true })).toEqual({ + expect(treeAffordance({ type: "move", direction: "right" }, { expanded: true, hasChildren: true }).hand).toEqual({ type: "move", direction: "right", - }); - expect(treeAffordance({ type: "move", direction: "left" }, { expanded: false, hasChildren: true })).toEqual({ - type: "move", - direction: "left", - }); - expect(treeAffordance({ type: "move", direction: "down" }, { expanded: true, hasChildren: true })).toEqual({ - type: "move", - direction: "down", + operation: "replace", }); }); }); -describe("dragOffset", () => { +describe("dragAffordance", () => { test("commits only when the pointer actually moved", () => { - expect(dragOffset({ x: 10, y: 20 }, { x: 14, y: 18 })).toEqual({ dx: 4, dy: -2 }); - expect(dragShouldCommit({ dx: 0, dy: 0 })).toBe(false); - expect(dragShouldCommit({ dx: 4, dy: -2 })).toBe(true); + expect(dragAffordance({ x: 10, y: 20 }, { x: 14, y: 18 })).toEqual({ + hand: { type: "translate", dx: 4, dy: -2 }, + cursor: "grabbing", + commit: true, + }); + expect(dragAffordance({ x: 10, y: 20 }, { x: 10, y: 20 }).commit).toBe(false); }); }); describe("historyAffordance", () => { test("binds disabled to canUndo and canRedo", () => { - expect(historyAffordance({ canUndo: false, canRedo: true })).toEqual({ + 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 the same result", () => { + 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"]); + }); +}); + +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 }); + }); +}); diff --git a/site/src/routes/widgets/CanvasWidgetRoute.tsx b/site/src/routes/widgets/CanvasWidgetRoute.tsx index a724bebb..b065a871 100644 --- a/site/src/routes/widgets/CanvasWidgetRoute.tsx +++ b/site/src/routes/widgets/CanvasWidgetRoute.tsx @@ -1,12 +1,19 @@ -import { useState, type PointerEvent } from "react"; +import { useRef, useState, type KeyboardEvent, type PointerEvent } from "react"; import { createObjectEditor, type ObjectDocument } from "@interactive-os/json-document-editing"; import { useEditing } from "@interactive-os/json-document-react"; import { - dragOffset, - dragShouldCommit, + applyAffordance, + dragAffordance, + forbiddenCursor, + marqueeRect, + nudgeAffordance, + panAffordance, pointerSelect, + selectOperationFrom, + snapAffordance, } from "@interactive-os/json-document-affordance"; import { SelectableItem } from "../../shared/ui/interactive"; +import { classes, ui } from "../../shared/ui/styles"; import { optionProps } from "../../shared/widget-binding"; import { WidgetDemoFrame } from "./WidgetDemoFrame"; @@ -26,9 +33,22 @@ type DragState = { readonly dy: number; }; +type MarqueeState = { + readonly originX: number; + readonly originY: number; + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +}; + export function CanvasWidgetRoute() { const [editor] = useState(() => createObjectEditor(initialObjects)); const [drag, setDrag] = useState(null); + const [marquee, setMarquee] = useState(null); + const [pan, setPan] = useState({ x: 0, y: 0, originX: 0, originY: 0, active: false }); + const [space, setSpace] = useState(false); + const surface = useRef(null); const editing = useEditing({ source: editor, selectedKeys: editor.selectedObjects.map((object) => object.id), @@ -40,73 +60,190 @@ export function CanvasWidgetRoute() { mode: mode === "extend" ? "add" : mode, }); }, - operationFromEvent: (event) => pointerSelect({ - shiftKey: event.shiftKey ?? false, - metaKey: event.metaKey ?? false, - ctrlKey: event.ctrlKey ?? false, - }), + operationFromEvent: (event) => selectOperationFrom(pointerSelect(event)), }); const document = editing.snapshot.value as ObjectDocument; - function handlePointerDown(event: PointerEvent, objectId: string) { - editing.getItem(objectId).getPressHandler()(event); - const ids = pointerSelect(event) === "replace" - ? [objectId] - : [...new Set([...editor.selectedObjects.map((object) => object.id), objectId])]; - event.currentTarget.setPointerCapture(event.pointerId); - setDrag({ ids, originX: event.clientX, originY: event.clientY, dx: 0, dy: 0 }); + function handlePointerDown(event: PointerEvent, objectId?: string) { + const panResult = panAffordance({ spaceKey: space, buttons: event.buttons }); + surface.current?.setPointerCapture(event.pointerId); + if (panResult.cursor === "grabbing" || (space && event.buttons === 1)) { + setPan({ x: pan.x, y: pan.y, originX: event.clientX - pan.x, originY: event.clientY - pan.y, active: true }); + return; + } + if (objectId) { + const result = pointerSelect(event); + applyAffordance(result, { + hand: (hand) => { + if (hand.type !== "select") return; + editor.dispatch({ + type: "selection.set", + objectIds: [objectId], + mode: hand.operation === "extend" ? "add" : hand.operation, + }); + }, + }); + const ids = selectOperationFrom(result) === "replace" + ? [objectId] + : [...new Set([...editor.selectedObjects.map((object) => object.id), objectId])]; + setDrag({ ids, originX: event.clientX, originY: event.clientY, dx: 0, dy: 0 }); + return; + } + const origin = { x: event.nativeEvent.offsetX, y: event.nativeEvent.offsetY }; + setMarquee({ originX: origin.x, originY: origin.y, x: origin.x, y: origin.y, width: 0, height: 0 }); + } + + function handlePointerMove(event: PointerEvent) { + applyAffordance(forbiddenCursor({ allowed: true, dropping: drag !== null }), { + cursor: (cursor) => { + event.currentTarget.style.cursor = space ? "grab" : cursor; + }, + }); + if (pan.active) { + applyAffordance(panAffordance({ spaceKey: true, buttons: event.buttons }), { + cursor: (cursor) => { + event.currentTarget.style.cursor = cursor; + }, + hand: () => { + setPan((current) => ({ + ...current, + x: event.clientX - current.originX, + y: event.clientY - current.originY, + })); + }, + }); + return; + } + if (drag) { + applyAffordance( + dragAffordance({ x: drag.originX, y: drag.originY }, { x: event.clientX, y: event.clientY }), + { + cursor: (cursor) => { + event.currentTarget.style.cursor = cursor; + }, + hand: (hand) => { + if (hand.type === "translate") setDrag({ ...drag, dx: hand.dx, dy: hand.dy }); + }, + }, + ); + return; + } + if (marquee) { + const rect = marqueeRect( + { x: marquee.originX, y: marquee.originY }, + { x: event.nativeEvent.offsetX, y: event.nativeEvent.offsetY }, + ); + setMarquee({ ...marquee, ...rect }); + } } - function handlePointerMove(event: PointerEvent) { - if (!drag) return; - const offset = dragOffset({ x: drag.originX, y: drag.originY }, { x: event.clientX, y: event.clientY }); - setDrag({ ...drag, dx: offset.dx, dy: offset.dy }); + function handlePointerUp(event: PointerEvent) { + if (pan.active) { + setPan((current) => ({ ...current, active: false })); + return; + } + if (drag) { + const result = dragAffordance({ x: drag.originX, y: drag.originY }, { x: event.clientX, y: event.clientY }); + if (result.commit && result.hand?.type === "translate") { + const snapped = snapAffordance( + { x: result.hand.dx, y: result.hand.dy }, + { grid: 8, disable: event.metaKey || event.ctrlKey }, + ); + const point = snapped.hand?.type === "translate" ? snapped.hand : result.hand; + editor.dispatch({ type: "object.translate", objectIds: drag.ids, dx: point.dx, dy: point.dy }); + } + setDrag(null); + return; + } + if (marquee) { + const hits = document.objects + .filter((object) => intersects(marquee, object)) + .map((object) => object.id); + if (hits.length > 0) { + editor.dispatch({ + type: "selection.set", + objectIds: hits, + mode: selectOperationFrom(pointerSelect(event)) === "extend" ? "add" : "replace", + }); + } + setMarquee(null); + } } - function handlePointerUp() { - if (!drag) return; - const offset = { dx: drag.dx, dy: drag.dy }; - if (dragShouldCommit(offset)) { - editor.dispatch({ type: "object.translate", objectIds: drag.ids, dx: offset.dx, dy: offset.dy }); + function onKeyDown(event: KeyboardEvent) { + if (event.key === " ") { + setSpace(true); + event.preventDefault(); } - setDrag(null); + applyAffordance(nudgeAffordance(event), { + hand: (hand) => { + if (hand.type !== "nudge") return; + const ids = editor.selectedObjects.map((object) => object.id); + if (ids.length === 0) return; + editor.dispatch({ type: "object.translate", objectIds: ids, dx: hand.dx, dy: hand.dy }); + event.preventDefault(); + }, + }); } return ( - {document.objects.map((object) => { - const offset = drag?.ids.includes(object.id) ? drag : null; - const option = optionProps(editing.getItem(object.id)); - return ( - handlePointerDown(event, object.id)} - onPointerMove={handlePointerMove} - onPointerUp={handlePointerUp} - > - {object.label} - - ); - })} +
    handlePointerDown(event)} + onPointerMove={handlePointerMove} + onPointerUp={handlePointerUp} + onKeyDown={onKeyDown} + onKeyUp={(event) => { + if (event.key === " ") setSpace(false); + }} + > +
    + {document.objects.map((object) => { + const offset = drag?.ids.includes(object.id) ? drag : null; + const option = optionProps(editing.getItem(object.id)); + return ( + { + event.stopPropagation(); + handlePointerDown(event, object.id); + }} + > + {object.label} + + ); + })} + {marquee ? ( +
    + ) : null} +
    )} values={[ @@ -117,3 +254,13 @@ export function CanvasWidgetRoute() { /> ); } + +function intersects( + rect: { readonly x: number; readonly y: number; readonly width: number; readonly height: number }, + object: { readonly x: number; readonly y: number; readonly width: number; readonly height: number }, +): boolean { + return rect.x < object.x + object.width + && rect.x + rect.width > object.x + && rect.y < object.y + object.height + && rect.y + rect.height > object.y; +} diff --git a/site/src/routes/widgets/DocumentWidgetRoute.tsx b/site/src/routes/widgets/DocumentWidgetRoute.tsx index 853f0773..82689eef 100644 --- a/site/src/routes/widgets/DocumentWidgetRoute.tsx +++ b/site/src/routes/widgets/DocumentWidgetRoute.tsx @@ -4,7 +4,12 @@ import { useEditing } from "@interactive-os/json-document-react"; import { lineBoundary, moveLinePoint } from "@interactive-os/json-document-web"; import { SelectableItem } from "../../shared/ui/interactive"; import { classes, ui } from "../../shared/ui/styles"; -import { pointerSelect, resolveAffordanceKey } from "@interactive-os/json-document-affordance"; +import { + keyboardCommandFrom, + pointerSelect, + resolveAffordanceKey, + selectOperationFrom, +} from "@interactive-os/json-document-affordance"; import { optionProps, useWidgetKeyboard } from "../../shared/widget-binding"; import { WidgetDemoFrame } from "./WidgetDemoFrame"; @@ -28,16 +33,12 @@ export function DocumentWidgetRoute() { onSelect: (blockId, mode) => { editor.dispatch({ type: "selection.set", blockId, mode }); }, - operationFromEvent: (event) => pointerSelect({ - shiftKey: event.shiftKey ?? false, - metaKey: event.metaKey ?? false, - ctrlKey: event.ctrlKey ?? false, - }), + operationFromEvent: (event) => selectOperationFrom(pointerSelect(event)), keyboard: { resolve: (stroke) => { - const command = resolveAffordanceKey(stroke); + const result = resolveAffordanceKey(stroke); keyboard.resolve(stroke); - return command; + return keyboardCommandFrom(result); }, focusKey: () => editor.selectedBlockIds.at(-1), neighbor: (key, command) => { diff --git a/site/src/routes/widgets/GridWidgetRoute.tsx b/site/src/routes/widgets/GridWidgetRoute.tsx index 7439fe71..b1435777 100644 --- a/site/src/routes/widgets/GridWidgetRoute.tsx +++ b/site/src/routes/widgets/GridWidgetRoute.tsx @@ -4,7 +4,12 @@ import { useEditing } from "@interactive-os/json-document-react"; import { gridBoundary, moveGridPoint } from "@interactive-os/json-document-web"; import { SelectableItem } from "../../shared/ui/interactive"; import { classes, ui } from "../../shared/ui/styles"; -import { pointerSelect, resolveAffordanceKey } from "@interactive-os/json-document-affordance"; +import { + keyboardCommandFrom, + pointerSelect, + resolveAffordanceKey, + selectOperationFrom, +} from "@interactive-os/json-document-affordance"; import { gridCellProps, useWidgetKeyboard } from "../../shared/widget-binding"; import { WidgetDemoFrame } from "./WidgetDemoFrame"; @@ -32,16 +37,12 @@ export function GridWidgetRoute() { const { rowId, columnId } = parseCellKey(key); editor.dispatch({ type: "selection.set", rowId, columnId, mode }); }, - operationFromEvent: (event) => pointerSelect({ - shiftKey: event.shiftKey ?? false, - metaKey: event.metaKey ?? false, - ctrlKey: event.ctrlKey ?? false, - }), + operationFromEvent: (event) => selectOperationFrom(pointerSelect(event)), keyboard: { resolve: (stroke) => { - const command = resolveAffordanceKey(stroke); + const result = resolveAffordanceKey(stroke); keyboard.resolve(stroke); - return command; + return keyboardCommandFrom(result); }, focusKey: () => { const next = editor.snapshot.selection.focus; diff --git a/site/src/routes/widgets/ListboxWidgetRoute.tsx b/site/src/routes/widgets/ListboxWidgetRoute.tsx index 38378083..f1ef5729 100644 --- a/site/src/routes/widgets/ListboxWidgetRoute.tsx +++ b/site/src/routes/widgets/ListboxWidgetRoute.tsx @@ -1,10 +1,17 @@ -import { useState } from "react"; +import { useState, type KeyboardEvent } from "react"; import { createOrderEditor, type OrderDocument } from "@interactive-os/json-document-editing"; import { useEditing } from "@interactive-os/json-document-react"; import { lineBoundary, moveLinePoint } from "@interactive-os/json-document-web"; import { SelectableItem } from "../../shared/ui/interactive"; import { classes, ui } from "../../shared/ui/styles"; -import { pointerSelect, resolveAffordanceKey } from "@interactive-os/json-document-affordance"; +import { + applyAffordance, + keyboardCommandFrom, + pointerSelect, + resolveAffordanceKey, + selectOperationFrom, + typeaheadAffordance, +} from "@interactive-os/json-document-affordance"; import { optionProps, useWidgetKeyboard } from "../../shared/widget-binding"; import { WidgetDemoFrame } from "./WidgetDemoFrame"; @@ -19,6 +26,7 @@ const initialOrder: OrderDocument = { export function ListboxWidgetRoute() { const [editor] = useState(() => createOrderEditor(initialOrder)); + const [typeahead, setTypeahead] = useState({ buffer: "", at: 0 }); const keyboard = useWidgetKeyboard(); const ids = () => (editor.snapshot.value as OrderDocument).items.map((item) => item.id); const focusKey = editor.snapshot.selection.ranges[editor.snapshot.selection.primaryIndex ?? 0]?.focus.itemId ?? null; @@ -29,16 +37,12 @@ export function ListboxWidgetRoute() { onSelect: (itemId, mode) => { editor.dispatch({ type: "selection.set", itemId, mode }); }, - operationFromEvent: (event) => pointerSelect({ - shiftKey: event.shiftKey ?? false, - metaKey: event.metaKey ?? false, - ctrlKey: event.ctrlKey ?? false, - }), + operationFromEvent: (event) => selectOperationFrom(pointerSelect(event)), keyboard: { resolve: (stroke) => { - const command = resolveAffordanceKey(stroke); + const result = resolveAffordanceKey(stroke); keyboard.resolve(stroke); - return command; + return keyboardCommandFrom(result); }, focusKey: () => editor.snapshot.selection.ranges[editor.snapshot.selection.primaryIndex ?? 0]?.focus.itemId ?? undefined, neighbor: (key, command) => command.type === "move" @@ -57,10 +61,35 @@ export function ListboxWidgetRoute() { }); const document = editing.snapshot.value as OrderDocument; + function onKeyDown(event: KeyboardEvent) { + const names = document.items.map((item) => item.label); + const from = document.items.find((item) => item.id === focusKey)?.label ?? null; + const result = typeaheadAffordance({ + buffer: typeahead.buffer, + key: event.key, + elapsedMs: event.timeStamp - typeahead.at, + names, + from, + }); + applyAffordance(result, { + hand: (hand) => { + if (hand.type !== "typeahead") return; + setTypeahead({ buffer: hand.buffer, at: event.timeStamp }); + const item = document.items.find((candidate) => candidate.label === hand.name); + if (item) editor.dispatch({ type: "selection.set", itemId: item.id, mode: "replace" }); + }, + }); + if (result.hand?.type === "typeahead") { + event.preventDefault(); + return; + } + editing.getKeyDownHandler()(event); + } + return ( {document.items.map((item) => ( diff --git a/site/src/routes/widgets/ToolbarWidgetRoute.tsx b/site/src/routes/widgets/ToolbarWidgetRoute.tsx index 48b6b1a9..8d726420 100644 --- a/site/src/routes/widgets/ToolbarWidgetRoute.tsx +++ b/site/src/routes/widgets/ToolbarWidgetRoute.tsx @@ -4,7 +4,12 @@ import { useEditing } from "@interactive-os/json-document-react"; import { lineBoundary, moveLinePoint } from "@interactive-os/json-document-web"; import { ActionButton, SelectableItem } from "../../shared/ui/interactive"; import { classes, ui } from "../../shared/ui/styles"; -import { historyAffordance, resolveAffordanceKey } from "@interactive-os/json-document-affordance"; +import { + historyAffordance, + historyCommandsFrom, + keyboardCommandFrom, + resolveAffordanceKey, +} from "@interactive-os/json-document-affordance"; import { optionProps, useWidgetKeyboard } from "../../shared/widget-binding"; import { WidgetDemoFrame } from "./WidgetDemoFrame"; @@ -29,9 +34,9 @@ export function ToolbarWidgetRoute() { }, keyboard: { resolve: (stroke) => { - const command = resolveAffordanceKey(stroke); + const result = resolveAffordanceKey(stroke); keyboard.resolve(stroke); - return command; + return keyboardCommandFrom(result); }, focusKey: () => editor.snapshot.selection.ranges[editor.snapshot.selection.primaryIndex ?? 0]?.focus.itemId ?? undefined, neighbor: (key, command) => command.type === "move" @@ -50,7 +55,7 @@ export function ToolbarWidgetRoute() { }); const snapshot = editing.snapshot; const document = snapshot.value as OrderDocument; - const commands = historyAffordance(snapshot); + const commands = historyCommandsFrom(historyAffordance(snapshot)); return ( { - const command = resolveAffordanceKey(stroke); + const result = resolveAffordanceKey(stroke); keyboard.resolve(stroke); - return command; + return keyboardCommandFrom(result); }, focusKey: () => editor.snapshot.selection.ranges[editor.snapshot.selection.primaryIndex ?? 0]?.focus.nodeId ?? undefined, neighbor: (key, command) => { const row = rows.find((item) => item.id === key); if (row && command.type === "move") { - const action = treeAffordance(command, { + const result = treeAffordance(command, { expanded: expanded.has(row.id), hasChildren: document.nodes.some((node) => node.parentId === row.id), }); - if (action.type === "expand") { + if (result.hand?.type === "expand") { setExpanded((current) => new Set(current).add(row.id)); return key; } - if (action.type === "collapse") { + if (result.hand?.type === "collapse") { setExpanded((current) => { const next = new Set(current); next.delete(row.id); diff --git a/site/src/shared/widget-binding/history.ts b/site/src/shared/widget-binding/history.ts index 75a84267..7bc5a985 100644 --- a/site/src/shared/widget-binding/history.ts +++ b/site/src/shared/widget-binding/history.ts @@ -1,8 +1,16 @@ -export { - historyAffordance as historyCommands, -} from "@interactive-os/json-document-affordance"; -export type { - HistoryAffordance as HistoryCommand, - HistoryAffordanceMap as HistoryCommandMap, - HistoryAffordanceName as HistoryCommandName, +import { + historyAffordance, + historyCommandsFrom, + type HistoryAffordance, + type HistoryAffordanceMap, + type HistoryAffordanceName, } from "@interactive-os/json-document-affordance"; + +export type { HistoryAffordance as HistoryCommand, HistoryAffordanceMap as HistoryCommandMap, HistoryAffordanceName as HistoryCommandName }; + +export function historyCommands(snapshot: { + readonly canUndo: boolean; + readonly canRedo: boolean; +}): HistoryAffordanceMap { + return historyCommandsFrom(historyAffordance(snapshot)); +} diff --git a/site/tests/browser/widgets.spec.ts b/site/tests/browser/widgets.spec.ts index 3fa1470f..7c9c38f5 100644 --- a/site/tests/browser/widgets.spec.ts +++ b/site/tests/browser/widgets.spec.ts @@ -46,6 +46,13 @@ test("Toolbar binds Undo and Redo to canUndo and canRedo", async ({ page }) => { expect(await json(page, "widget-toolbar-keyboard")).toEqual({ type: "undo" }); }); +test("Listbox typeahead jumps to the matching label", async ({ page }) => { + await page.goto("/widgets/listbox"); + await page.getByRole("listbox", { name: "Order items" }).focus(); + await page.keyboard.type("T"); + expect(await json(page, "widget-listbox-selected")).toEqual(["today"]); +}); + test("Listbox reads selected keys and focus from Order", async ({ page }) => { await page.goto("/widgets/listbox"); await page.getByRole("option", { name: "Today" }).click(); From 0a998d60d9a41a6f0f742689d94145f752decebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Thu, 20 Aug 2026 23:02:41 +0900 Subject: [PATCH 08/14] =?UTF-8?q?=EC=96=B4=ED=8F=AC=EB=8D=98=EC=8A=A4:=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20applyAffordance=EB=A1=9C=20=EC=9E=A5?= =?UTF-8?q?=EB=A5=B4=20=EC=A6=9D=EB=AA=85=EC=9D=B4=20=EC=9B=80=EC=A7=81?= =?UTF-8?q?=EC=9D=B8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/public/affordance-cancel.md | 22 +++- docs/public/affordance-drag.md | 33 ++--- docs/public/affordance-drop.md | 23 ++-- docs/public/affordance-fold.md | 44 ++++--- docs/public/affordance-history.md | 18 +-- docs/public/affordance-marquee.md | 26 +++- docs/public/affordance-select.md | 15 --- docs/public/affordance-typeahead.md | 22 ++-- .../tests/affordance.test.ts | 17 +++ site/src/routes/widgets/BoardWidgetRoute.tsx | 114 ++++++++++++++++-- site/src/routes/widgets/CanvasWidgetRoute.tsx | 66 +++++++--- .../routes/widgets/DocumentWidgetRoute.tsx | 13 +- site/src/routes/widgets/GridWidgetRoute.tsx | 18 ++- .../src/routes/widgets/ListboxWidgetRoute.tsx | 17 ++- .../src/routes/widgets/ToolbarWidgetRoute.tsx | 23 +++- site/src/routes/widgets/TreeWidgetRoute.tsx | 57 ++++++--- site/tests/browser/widgets.spec.ts | 45 +++++++ 17 files changed, 433 insertions(+), 140 deletions(-) diff --git a/docs/public/affordance-cancel.md b/docs/public/affordance-cancel.md index a269babd..e7d444b1 100644 --- a/docs/public/affordance-cancel.md +++ b/docs/public/affordance-cancel.md @@ -6,17 +6,27 @@ Escape는 진행 중인 손과 임시 UI를 버리는 손입니다. Escape 키 드래그를 닫고, `pointercancel`은 포인터 제스처를 중단합니다. ```ts -import { escapeAffordance } from "@interactive-os/json-document-affordance"; +import { + applyAffordance, + escapeAffordance, +} from "@interactive-os/json-document-affordance"; function onKeyDown(event: KeyboardEvent) { - if (escapeAffordance(event) === "cancel") { - setMenu(null); - drag = null; - } + applyAffordance(escapeAffordance(event), { + hand: (hand) => { + if (hand.type !== "cancel") return; + setMarquee(null); + setDrag(null); + }, + }); } function onPointerCancel(event: PointerEvent) { - if (escapeAffordance(event) === "cancel") drag = null; + applyAffordance(escapeAffordance(event), { + hand: (hand) => { + if (hand.type === "cancel") setDrag(null); + }, + }); } ``` diff --git a/docs/public/affordance-drag.md b/docs/public/affordance-drag.md index 091a2be2..7703f4af 100644 --- a/docs/public/affordance-drag.md +++ b/docs/public/affordance-drag.md @@ -6,29 +6,34 @@ Drag는 고른 대상을 포인터로 옮기는 손입니다. 누른 점에서 ```ts import { - dragOffset, - dragShouldCommit, - pointerSelect, + applyAffordance, + dragAffordance, } from "@interactive-os/json-document-affordance"; -function onPointerDown(event: PointerEvent, objectId: string) { - const mode = pointerSelect(event); - editor.dispatch({ type: "selection.set", objectIds: [objectId], mode }); - event.currentTarget.setPointerCapture(event.pointerId); - drag = { origin: { x: event.clientX, y: event.clientY } }; +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 offset = dragOffset(drag.origin, { x: event.clientX, y: event.clientY }); - if (dragShouldCommit(offset)) { + const result = dragAffordance(origin, { x: event.clientX, y: event.clientY }); + if (result.commit && result.hand?.type === "translate") { editor.dispatch({ type: "object.translate", - objectIds: editor.selectedObjects.map((object) => object.id), - dx: offset.dx, - dy: offset.dy, + objectIds, + dx: result.hand.dx, + dy: result.hand.dy, }); } - drag = null; } ``` diff --git a/docs/public/affordance-drop.md b/docs/public/affordance-drop.md index 23551a0b..78c2dcbb 100644 --- a/docs/public/affordance-drop.md +++ b/docs/public/affordance-drop.md @@ -6,23 +6,26 @@ Drop은 [Drag](affordance-drag.md)의 끝입니다. 어디에 둘 수 있는지 커서가 말하고, 못 두는 자리는 `no-drop`입니다. ```ts -import { dropAffordance, dragOperation } from "@interactive-os/json-document-affordance"; +import { + applyAffordance, + dropAffordance, +} from "@interactive-os/json-document-affordance"; function onPointerMove(event: PointerEvent) { - const drop = dropAffordance({ - canDrop: hostCanDrop(event), - operation: dragOperation(event), + applyAffordance(dropAffordance({ canDrop: hostCanDrop(event) }), { + cursor: (cursor) => { + event.currentTarget.style.cursor = cursor; + }, }); - event.currentTarget.style.cursor = drop.cursor; } function onPointerUp(event: PointerEvent, cardId: string, columnId: string) { - const drop = dropAffordance({ - canDrop: hostCanDrop(event), - operation: dragOperation(event), + applyAffordance(dropAffordance({ canDrop: hostCanDrop(event) }), { + hand: (hand) => { + if (hand.type !== "move-drop") return; + editor.dispatch({ type: "card.move", cardId, columnId }); + }, }); - if (!drop.accept) return; - editor.dispatch({ type: "card.move", cardId, columnId }); } ``` diff --git a/docs/public/affordance-fold.md b/docs/public/affordance-fold.md index f42a0306..88837052 100644 --- a/docs/public/affordance-fold.md +++ b/docs/public/affordance-fold.md @@ -6,31 +6,35 @@ Expand/Collapse는 나무에서 가지를 접고 펼치는 손입니다. 오른 ```ts import { - resolveAffordanceKey, + applyAffordance, treeAffordance, } from "@interactive-os/json-document-affordance"; -function onKeyDown(event: KeyboardEvent, nodeId: string) { - const command = resolveAffordanceKey(event); - if (command?.type !== "move") return; - const hand = treeAffordance(command, { +applyAffordance( + treeAffordance(command, { expanded: expanded.has(nodeId), hasChildren: hasChildren(nodeId), - }); - 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; - } - editor.dispatch({ type: "selection.set", nodeId, topology, mode: "replace" }); -} + }), + { + 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만 diff --git a/docs/public/affordance-history.md b/docs/public/affordance-history.md index b0056f3d..14601c6c 100644 --- a/docs/public/affordance-history.md +++ b/docs/public/affordance-history.md @@ -5,21 +5,23 @@ redo입니다. 버튼은 `canUndo` / `canRedo`를 읽어 꺼집니다. ```ts import { + applyAffordance, historyAffordance, - resolveAffordanceKey, } from "@interactive-os/json-document-affordance"; -const commands = historyAffordance({ +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 }; + }, }); -function onKeyDown(event: KeyboardEvent) { - const command = resolveAffordanceKey(event); - if (command?.type === "undo") editor.undo(); - if (command?.type === "redo") editor.redo(); -} -