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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ source and release history remains available from Git commits and version tags.

## Next

- Closed Focus, Caret, Rename, Double-click, and Triple-click as package hands,
and dogfooded them on the Order and Document product screens.
- Closed Database Hands on the header and cell: order, width, sort,
hide, and filter persist in the saved Table view, and the named
toolbar toggles no longer replace those hands.
Expand Down
38 changes: 18 additions & 20 deletions docs/public/affordance-caret.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,37 @@
# Caret

TBD.

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

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

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

function onPointerDown(event: PointerEvent, blockId: string) {
const hand = caretAffordance({ type: "pointer", detail: event.detail });
if (hand.type === "place") {
editor.dispatch({
type: "selection.set",
blockId,
offset: hostHitOffset(event),
});
}
applyAffordance(caretAffordance({ type: "pointer" }), {
hand: (hand) => {
if (hand.type !== "caret" || hand.action !== "place") return;
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,
});
}
applyAffordance(caretAffordance(event), {
hand: (hand) => {
if (hand.type !== "caret-move") return;
editor.dispatch({
type: "selection.set",
blockId: focus.blockId,
offset: hostMoveOffset(focus.offset, hand),
mode: hand.operation,
});
},
});
}
```

Expand Down
11 changes: 6 additions & 5 deletions docs/public/affordance-double-click.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# Double-click

TBD.

Double-click은 `UIEvent.detail === 2`입니다. 글에서는 단어를 고르고,
목록·파일에서는 열거나 [Rename](affordance-rename.md)으로 갈 수 있습니다.
횟수 손 자체는 닫혀 있고, 뜻은 장르 Intent입니다.

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

function onClick(event: MouseEvent, itemId: string) {
if (clickCountAffordance(event.detail) !== "double-click") return;
hostOpen(itemId);
applyAffordance(clickCountAffordance(event.detail), {
hand: (hand) => {
if (hand.type === "click" && hand.count === 2) hostOpen(itemId);
},
});
}
```

Expand Down
21 changes: 8 additions & 13 deletions docs/public/affordance-focus.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
# Focus

TBD.

Focus는 키보드의 포인터입니다. Tab과 Shift+Tab은 컴포넌트 사이로 옮기고,
화살표·Home·End는 컴포넌트 안에서 옮깁니다. Focus와 [Select](affordance-select.md)는
다릅니다. Focus 표시는 항상 보여야 하고, 선택 표시와 겹치면 안 됩니다.

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

function onKeyDown(event: KeyboardEvent) {
const hand = focusAffordance(event);
if (hand?.type === "tab") return;
if (hand?.type === "move") {
const next = neighbor(focusKey, hand.direction);
setFocusKey(next);
return;
}
if (hand?.type === "boundary") {
setFocusKey(hand.edge === "start" ? ids[0] : ids.at(-1));
}
applyAffordance(focusAffordance(event), {
hand: (hand) => {
if (hand.type === "tab") return;
if (hand.type === "move") setFocusKey(neighbor(focusKey, hand.direction));
if (hand.type === "boundary") setFocusKey(hand.edge === "start" ? ids[0] : ids.at(-1));
},
});
}
```

Expand Down
30 changes: 17 additions & 13 deletions docs/public/affordance-rename.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
# Rename

TBD.

Rename은 고른 대상의 레이블을 고치는 손입니다. F2와 느린 double-click이
같은 손을 엽니다. Escape는 [Escape](affordance-cancel.md)입니다.

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

function onKeyDown(event: KeyboardEvent) {
const hand = renameAffordance(event);
if (hand === "begin") setRenaming(focusKey);
if (hand === "cancel") setRenaming(null);
if (hand === "commit" && renaming) {
editor.dispatch({ type: "item.rename", itemId: renaming, label: draft });
setRenaming(null);
}
applyAffordance(renameAffordance(event), {
hand: (hand) => {
if (hand.type !== "rename") return;
if (hand.action === "begin") setRenaming(focusKey);
if (hand.action === "cancel") setRenaming(null);
if (hand.action === "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);
}
applyAffordance(renameAffordance({ type: "pointer", detail: event.detail, intervalMs }), {
hand: (hand) => {
if (hand.type === "rename" && hand.action === "begin") setRenaming(itemId);
},
});
}
```

Expand Down
15 changes: 6 additions & 9 deletions docs/public/affordance-triple-click.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
# Triple-click

TBD.

Triple-click은 `UIEvent.detail === 3`입니다. 글에서는 줄 또는 문단을
고릅니다. 항목 목록에서는 보통 쓰지 않습니다.

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

function onClick(event: MouseEvent, blockId: string) {
if (clickCountAffordance(event.detail) !== "triple-click") return;
editor.dispatch({
type: "selection.set",
blockId,
mode: "replace",
offset: 0,
applyAffordance(clickCountAffordance(event.detail), {
hand: (hand) => {
if (hand.type !== "click" || hand.count !== 3) return;
editor.dispatch({ type: "selection.set", blockId, mode: "replace", offset: 0 });
},
});
}
```
Expand Down
10 changes: 5 additions & 5 deletions docs/public/affordance.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,24 @@ Editing은 선택과 작업을 기억합니다. Adapter는 키 chord를 command
| [Marquee](affordance-marquee.md) | `marqueeAffordance`, `commitAffordance` | 빈 곳에서 사각형으로 여러 대상 |
| [Pan](affordance-pan.md) | `panAffordance` | Space+드래그, grab |
| [Snap](affordance-snap.md) | `snapAffordance` | 그리드·가이드, 수정 키로 해제 |
| [Focus](affordance-focus.md) | `focusAffordance` | Tab 사이, 화살표 안, 초점 ≠ 선택 |
| [Caret](affordance-caret.md) | `caretAffordance`, `caretCursor` | I-beam 삽입점, 글 범위 |
| [Rename](affordance-rename.md) | `renameAffordance` | F2, 느린 두 번 누르기 |
| [Double-click](affordance-double-click.md) | `clickCountAffordance` | `detail` 2 |
| [Triple-click](affordance-triple-click.md) | `clickCountAffordance` | `detail` 3 |

## 키보드 TBD

| Affordance | API | Hand |
| --- | --- | --- |
| [Focus](affordance-focus.md) | `focusAffordance` | Tab 사이, 화살표 안, 초점 ≠ 선택 |
| [Caret](affordance-caret.md) | `caretAffordance`, `caretCursor` | I-beam 삽입점, 글 범위 |
| [Activate](affordance-activate.md) | `activateAffordance` | Enter, Space, 기본 클릭 |
| [Delete](affordance-delete.md) | `deleteAffordance` | Delete, Backspace. Delete chord는 이미 닫힘 |
| [Rename](affordance-rename.md) | `renameAffordance` | F2, 느린 두 번 누르기 |

## 마우스 TBD

| Affordance | API | Hand |
| --- | --- | --- |
| [Hover](affordance-hover.md) | `hoverAffordance`, `hoverCursor` | hover, 툴팁 지연, 커서 교체 |
| [Double-click](affordance-double-click.md) | `clickCountAffordance` | `detail` 2 |
| [Triple-click](affordance-triple-click.md) | `clickCountAffordance` | `detail` 3 |
| [Context menu](affordance-context-menu.md) | `contextMenuAffordance` | 오른쪽 클릭, Shift+F10, Menu |
| [Drop](affordance-drop.md) | `dropAffordance` | drop 대상, no-drop |
| [Duplicate](affordance-copy-drag.md) | `dragOperation` | Alt/Option 드래그 복제 |
Expand Down
2 changes: 1 addition & 1 deletion packages/json-document-affordance/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ export type {
AffordanceResult,
SelectOperation,
} from "./result.js";
export { activateAffordance, clickCountAffordance, escapeAffordance, focusAffordance, planeHitAffordance, pointerSelect, resolveAffordanceKey, selectAllAffordance, typeaheadAffordance } from "./select.js";
export { activateAffordance, caretAffordance, caretCursor, clickCountAffordance, escapeAffordance, focusAffordance, planeHitAffordance, pointerSelect, renameAffordance, resolveAffordanceKey, selectAllAffordance, typeaheadAffordance } from "./select.js";
11 changes: 8 additions & 3 deletions packages/json-document-affordance/src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ export type AffordanceHand =
| { readonly type: "clear" }
| { readonly type: "typeahead"; readonly buffer: string; readonly name: string | null }
| { readonly type: "click"; readonly count: number }
| { readonly type: "caret"; readonly action: "place" | "range"; readonly operation: "replace" | "extend" }
| {
readonly type: "caret-move";
readonly direction?: AffordanceMoveDirection;
readonly edge?: "start" | "end";
readonly operation: "replace" | "extend";
}
| { readonly type: "rename"; readonly action: "begin" | "commit" | "cancel" }
| { readonly type: "activate" }
| { readonly type: "cancel" }
| { readonly type: "tab"; readonly direction: "next" | "prev" }
Expand Down Expand Up @@ -84,7 +92,6 @@ export function applyAffordance<H extends AffordanceHand>(
if (result.hand) actions.hand?.(result.hand);
if ("commit" in result && result.commit && result.hand) actions.commit?.(result.hand);
}

export function commitAffordance<H extends AffordanceHand>(
result: AffordancePreview<H>,
): AffordanceCommit<H> | null {
Expand All @@ -95,5 +102,3 @@ export function commitAffordance<H extends AffordanceHand>(
? { hand, commit: true }
: { hand, cursor: result.cursor, commit: true };
}


66 changes: 66 additions & 0 deletions packages/json-document-affordance/src/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,72 @@ export function clickCountAffordance(detail: number): AffordancePreview {
return { hand: { type: "click", count: detail } };
}

export function caretCursor(direction: "horizontal" | "vertical"): "text" | "vertical-text" {
return direction === "vertical" ? "vertical-text" : "text";
}

export function caretAffordance(input:
| { readonly type: "pointer"; readonly dragging?: boolean }
| Pick<WebKeyboardStroke, "key" | "shiftKey">
): AffordancePreview {
if (!("key" in input)) {
return {
hand: {
type: "caret",
action: input.dragging ? "range" : "place",
operation: input.dragging ? "extend" : "replace",
},
cursor: "text",
};
}
const command = keyboard.resolve({
key: input.key,
shiftKey: input.shiftKey,
metaKey: false,
ctrlKey: false,
});
if (command?.type === "move") {
return {
hand: {
type: "caret-move",
direction: command.direction,
operation: command.operation,
},
};
}
if (command?.type === "boundary") {
return {
hand: {
type: "caret-move",
edge: command.edge,
operation: command.operation,
},
};
}
return { hand: null };
}

export function renameAffordance(input:
| Pick<WebKeyboardStroke, "key">
| {
readonly type: "pointer";
readonly detail: number;
readonly intervalMs: number;
readonly slowMs?: number;
}
): AffordancePreview {
if (!("key" in input)) {
const slowMs = input.slowMs ?? 400;
return input.detail === 2 && input.intervalMs >= slowMs
? { hand: { type: "rename", action: "begin" } }
: { hand: null };
}
if (input.key === "F2") return { hand: { type: "rename", action: "begin" } };
if (input.key === "Enter") return { hand: { type: "rename", action: "commit" } };
if (input.key === "Escape") return { hand: { type: "rename", action: "cancel" } };
return { hand: null };
}

export function activateAffordance(input: { readonly key?: string; readonly detail?: number; readonly button?: number }): AffordancePreview {
if (input.key === "Enter") return { hand: { type: "activate" } };
if (input.button === 0 && (input.detail ?? 1) === 1) return { hand: { type: "activate" } };
Expand Down
Loading