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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 87 additions & 64 deletions docs/specs/mobile-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ tethering environment.

`/playground/pocket` uses the same fake playground terminal stack as
`/playground/desktop`: `PlaygroundShellRegistry` attaches a `TutorialShell` to
every spawned pane, the same fake commands dispatch to browser-side runners, and
the first pane simply auto-runs `ascii-splash` as its initial command.
every spawned pane, and the same fake commands dispatch to browser-side runners.
The first mobile session auto-runs `tut` with the Pocket tutorial profile; a
second `changelog` session auto-runs `changelog` for the tutorial's copy/paste
coverage.

## 2. Prototype Goals

Expand Down Expand Up @@ -106,14 +108,36 @@ Touch modes:
| Mode | Button label | Icon | Availability | Behavior |
| --- | --- | --- | --- | --- |
| Gestures | `Gestures` | `HandPointingIcon` | Always available | Pane-content touches, pen presses, and primary mouse/trackpad clicks open the Gesture mode radial menu. |
| Text selection | `Select` | `CursorTextIcon` | Always available | Touches are reserved for terminal text selection and copy/paste. If the TUI is capturing mouse events, Dormouse activates mouse override for the active pane. |
| Text selection | `Select` | `CursorTextIcon` | Always available | Pane-content touch, pen, and primary mouse/trackpad drags use the same terminal text selection and copy/paste behavior as desktop. If a mounted pane's TUI is capturing mouse events, Dormouse activates mouse override for that pane. |
| Mouse | `Mouse` | `CursorClickIcon` | Only when the active TUI is capturing mouse events | Touches are passed through as terminal mouse input. |

Default touch mode is **Gestures**.

Touch mode is a global mobile UI state. Select mode derives each mounted pane's
mouse override from that global touch mode and the pane's own mouse-reporting
state, so switching sessions cannot leave an inactive pane stuck in a stale
override.

If Mouse mode is active and the active pane stops capturing mouse events, the
selector must fall back to Gestures.

Wheel, trackpad-scroll, and touchmove events in the pane content are terminal
input only in Mouse mode. Gestures and Select mode must suppress those
scroll-like events before xterm can translate them into mouse reports,
alternate-screen arrow keys, or scrollback motion.

In Mouse mode, primary touch and pen pointers synthesize left-button terminal
mouse input: pointerdown emits a mouse press, pointermove emits mouse motion,
and pointerup or pointercancel emits a mouse release. The wrapper suppresses
the native touch gesture while emitting those mouse events so a tap or drag is
seen by the TUI, not by browser panning, browser selection, or xterm's native
touch-scroll fallback.

Select mode must route touch and pen drags through the shared terminal
mouse-selection router, not through a mobile-only selection implementation, so
selection geometry, smart token extension, copy popups, rewrapped copy, and TUI
mouse-capture override rules match desktop behavior.

Gesture mode intentionally consumes primary mouse/trackpad clicks in addition to
touch input. This keeps the `/playground/pocket` prototype usable in desktop
browsers, narrow desktop viewports, and Storybook without a touchscreen. A primary
Expand Down Expand Up @@ -157,6 +181,10 @@ The select circle and its eight compass-direction ticks render at full opacity.
The current highlighted or selected direction uses a stronger tick so the circle
and label clusters read as one gesture system.

Because the mobile composition does not mount the desktop `Wall`,
`MobileTerminalUi` must publish the shared dynamic palette variables, including
`--color-focus-ring`, before rendering gesture UI that depends on those tokens.

When the rose opens on touch-down, root labels fade in with a subtle scale-in
and the select circle grows from zero radius to `RADIUS_SELECT`. This is a short
state-reveal motion, not an ongoing decoration; reduced-motion users get the
Expand All @@ -174,45 +202,50 @@ opacity blends smoothly from `1` at `RADIUS_FADE_START` to that target at
RADIUS_FADE_START), 0, 1)` and
`opacity = 1 + (targetOpacity - 1) * fadeProgress`.

Each root compass group renders as three separate labels placed close together,
not as one combined pill. When a group is selected, those same three labels tween
from their root group positions to their exploded positions in the opposite
directions. They must not fade out and be replaced by newly spawned option
labels.

Root labels are laid out as a square keypad, not on a circle. N, S, E, and W
use a hierarchical layout: the arrow chip sits closest to the select circle, and
the four arrow chips use one shared `GAP_CARDINAL_RING` from the select circle
edge. The two secondary chips sit just outside each arrow. N/S secondary pairs
use `GAP_CLUSTER` as the horizontal edge-to-edge gap across the axis; E/W
secondary pairs use the same `GAP_CLUSTER` as the vertical edge-to-edge gap.
Diagonal groups use an EW-dominant corner-and-stack layout: the center option's inward corner
is aligned with the diagonal tick mark at the same ring gap used by the cardinal
arrow chips, measured on screen as the same horizontal/vertical visual gap rather
than as a longer diagonal distance. The diagonal center corner contract is: SE
aligns Enter's top-left corner, NE aligns Backspace's bottom-left corner, SW
aligns Tab's top-right corner, and NW aligns Esc's bottom-right corner. NE and
SE place their secondary options relative to the center option exactly like the
E cluster places `End` and `l` relative to `▶`: both to the right of the center,
one above and one below. NW and SW place their secondary options exactly like the
W cluster places `Home` and `h` relative to `◀`: both to the left of the center,
one above and one below. Exploded option labels use the square direction anchors
directly. The root label pack stays close to the select circle, while preserving
enough room for long labels like Backspace.

Each root cluster uses `GAP_CLUSTER = 2px`. The first option in each group is
the cluster center. For N/S/E/W groups, items to the left are right-aligned to
the center chip's left edge plus the cluster gap; items to the right are
left-aligned to the center chip's right edge plus the cluster gap. Vertical
neighbors use the same edge-and-gap rule above or below the center chip.
Diagonal groups combine the tick-corner rule above with the same secondary-stack
rule used by the matching E or W cardinal group.

The radial menu is a two-stage gesture:
N, S, E, and W root labels render as single arrow chips. Dragging to
`RADIUS_SELECT` in one of those four cardinal directions immediately sends the
matching arrow key; there is no second-swipe confirmation.

Diagonal root compass groups render as three separate labels placed close
together, not as one combined pill. When a diagonal group is selected, those
same three labels tween from their root group positions to their exploded
positions in the opposite directions. They must not fade out and be replaced by
newly spawned option labels.

Root labels are laid out as a square keypad, not on a circle. The four cardinal
arrow chips use one shared `GAP_CARDINAL_RING` from the select circle edge.
Diagonal groups use an EW-dominant corner-and-stack layout: the center option's
inward corner is aligned with the diagonal tick mark at the same ring gap used
by the cardinal arrow chips, measured on screen as the same horizontal/vertical
visual gap rather than as a longer diagonal distance. The diagonal center corner
contract is: SE aligns Enter's top-left corner, NE aligns Backspace's
bottom-left corner, SW aligns Tab's top-right corner, and NW aligns Esc's
bottom-right corner. NE and SE place their secondary options to the right of the
center option, one above and one below. NW and SW place their secondary options
to the left of the center option, one above and one below. Exploded option
labels use the square direction anchors directly. The root label pack stays
close to the select circle, while preserving enough room for long labels like
Backspace.

Each diagonal root cluster uses `GAP_CLUSTER = 2px`. The first option in each
diagonal group is the cluster center. Secondary options use the same edge-and-gap
rule above or below the center chip.

For cardinal directions, the radial menu is a one-stage gesture:

1. Touch down to open the menu.
2. Drag to `RADIUS_HIGHLIGHT` to preview the closest compass point.
3. Drag to `RADIUS_SELECT` on N, S, E, or W to immediately send the matching
arrow key. The app must not wait for touch release.
4. After the arrow sends, the radial menu remains for a short completion
animation: removed labels fade out, and the selected arrow label expands and
fades out for positive confirmation before the overlay clears.

For diagonal directions, the radial menu is a two-stage gesture:

1. Touch down to open the menu.
2. Drag to `RADIUS_HIGHLIGHT` to preview the closest compass point.
3. Drag to `RADIUS_SELECT` to choose that compass point's group.
3. Drag to `RADIUS_SELECT` to choose that diagonal compass point's group.
4. The other seven compass groups fade out.
5. The compass center resets to the point where the user's drag intersected the
`RADIUS_SELECT` circle.
Expand All @@ -228,40 +261,37 @@ The radial menu is a two-stage gesture:
If the user releases after the first group selection but before choosing one of
the exploded options, the gesture is cancelled.

Exploded option directions:
Exploded option directions for diagonal groups:

| Selected group | Option directions |
| --- | --- |
| N | S, SW, SE |
| NE | SW, W, S |
| E | W, NW, SW |
| SE | NW, N, W |
| S | N, NW, NE |
| SW | NE, N, E |
| W | E, NE, SE |
| NW | SE, E, S |

Examples:

* Right arrow: tap, drag right to choose the E group, then drag left from the reset center until it sends.
* End: tap, drag right to choose the E group, then drag up-left from the reset center until it sends.
* `l`: tap, drag right to choose the E group, then drag down-left from the reset center until it sends.
* Right arrow: tap, drag right to `RADIUS_SELECT`; it sends immediately.
* Enter: tap, drag down-right to choose the SE group, then drag up-left from
the reset center until it sends.
* Shift+Enter: tap, drag down-right to choose the SE group, then drag up from
the reset center until it sends.

Root gesture menu labels use compact key glyphs: `⌃` for Ctrl, `⬆︎` for
Shift, and `▲`/`▼`/`◀`/`▶` for arrow keys. Enter, Backspace, PgUp, and PgDn
remain spelled out.
Shift, and `▲`/`▼`/`◀`/`▶` for arrow keys. Enter and Backspace remain spelled
out.

Root gesture menu:

```text
Esc ⌃C* k PgUp n Backspace
Quit** Paste*
Esc ⌃C* n Backspace
Quit** Paste*

Home ◀ ▶ End
h l
◀ ▶

⬆︎Tab y ⬆︎Enter
Tab Space j PgDn Enter
⬆︎Tab y ⬆︎Enter
Tab Space Enter
```

`⌃C` and `Paste` require an in-pane confirmation modal before they run.
Expand All @@ -286,23 +316,15 @@ Gesture action mappings:
| ⌃X | `\x18` |
| `:q↵` | `:q\r` |
| ▲ | `\x1B[A` |
| PgUp | `\x1B[5~` |
| k | `k` |
| Backspace | `\x7F` |
| Paste | Existing Dormouse paste flow for the active pane |
| n | `n` |
| ◀ | `\x1B[D` |
| Home | `\x1B[H` |
| h | `h` |
| ▶ | `\x1B[C` |
| End | `\x1B[F` |
| l | `l` |
| Tab | `\x09` |
| ⬆︎Tab | `\x1B[Z` |
| Space | ` ` |
| ▼ | `\x1B[B` |
| PgDn | `\x1B[6~` |
| j | `j` |
| Enter | `\r` |
| ⬆︎Enter | `\x1B[13;2u` |
| y | `y` |
Expand Down Expand Up @@ -453,7 +475,7 @@ Prototype behavior:

Build exactly this:

* One terminal playground screen.
* One mobile terminal playground screen with one visible session at a time.
* Floating theme switcher using the shared Dormouse theme picker.
* Touch mode selector:

Expand All @@ -473,6 +495,7 @@ Sessions | Recent | Type | Draft
* Draft reserve content: `WIP - this will be a place to draft prompts before pasting into the terminal`.
* Type mode native mobile keyboard input.
* Gesture mode radial menu for arrows, navigation keys, Esc, Tab, Enter, simple vim-like keys, confirmed Ctrl+C, confirmed Paste, and Quit breakout.
* Pocket `tut` starts directly in the Gesture navigation section, uses the title `Dormouse Pocket Tutorial`, and credits gesture items from radial-menu input callbacks rather than from native keyboard input.
* Simple local playground terminal behavior.

## 13. Prototype Success Criteria
Expand Down
6 changes: 6 additions & 0 deletions docs/specs/mouse-and-clipboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ The terminal makes the current regime visible in the pane header, provides a way
Activated by clicking the Mouse icon. While the temporary override is active:

- Mouse events are handled by the terminal, not forwarded to the inside program.
- Wheel events are also suppressed so xterm cannot translate scroll input into
mouse reports or alternate-screen arrow-key input for the inside program.
- The Mouse icon is replaced with the No-Mouse icon.
- A banner appears below the No-Mouse icon reading `Temporary mouse override until mouse-up.` followed by two buttons: **Make sticky** and **Cancel**.
- The override persists until the **next mouse-up event inside the terminal content area** (live region or scrollback) that is paired with a prior mouse-down in the same area. This includes plain clicks (a mouse-down/up pair that never crossed the drag threshold) as well as completed drags. The click on the No-Mouse icon itself, the banner's buttons, and any "orphan" mouse-up from a drag that started outside the terminal do **not** count as that mouse-up.
Expand All @@ -60,6 +62,8 @@ Activated by clicking the Mouse icon. While the temporary override is active:
- Clicking **Make sticky** in the banner converts the temporary override into a sticky one.
- The banner is dismissed.
- The No-Mouse icon remains visible with its "click to restore" hover text.
- Mouse and wheel events continue to be handled by the terminal rather than the
inside program.
- The override persists until the user clicks the No-Mouse icon, or until the inside program stops requesting mouse reporting.

### 2.3 Canceling the Temporary Override
Expand Down Expand Up @@ -88,6 +92,8 @@ Selection is available whenever the terminal is handling mouse events — that i
### 3.1 Initiating a Selection

- A click-and-drag in the terminal content area begins a selection. A small movement threshold (~4px) separates a plain click (which only shifts pane focus) from a drag (which begins a selection).
- On touch or pen surfaces, a primary pointer tap-and-drag follows the same
terminal selection path as mouse drag. Non-primary touch pointers are ignored.
- The selection is rendered by the terminal in a compositor layer **above** the cell grid, not by writing into the grid. This avoids conflicts with programs redrawing the screen.
- The selection rectangle is drawn as a single perimeter outline tracing the union of selected cells. Color is taken from `--vscode-focusBorder` with fallbacks to terminal foreground and selection background.

Expand Down
12 changes: 12 additions & 0 deletions docs/specs/theme.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,22 @@ it reads host-provided variables, materializes only missing Dormouse-consumed
variables on `body.style`, and removes stale materialized variables when the
host starts providing a real value.

Website routes may be hydrated as a full React Router document, so React can
reconcile the server `<body>` and remove render-time `body.style` and class
side effects. `applyTheme()` treats a same-theme call as a no-op only when the
expected inline `--vscode-*` variables and `vscode-light` / `vscode-dark` class
are still visible on `document.body`. ThemePicker also performs a browser
layout-effect restore after mount so website hydration cannot leave the picker
state saying a theme is active while xterm.js sees fallback colors.

`theme.css` declares the theme-dependent `--color-*` tokens on `body` because
`--vscode-*` variables also live there. Keep the parallel `@theme`
declarations so Tailwind can generate utility classes, but treat the body-level
declarations as the runtime source of truth.
Dynamic palette tokens (`--color-door-bg`, `--color-door-fg`, and
`--color-focus-ring`) also have body-level baseline bindings matching the
`@theme` declarations, so direct CSS-var consumers such as the mobile gesture
SVG render visibly before `useDynamicPalette()` publishes refined values.

`theme.css` must not contain hardcoded color defaults or `var(..., fallback)`
chains. Runtime hosts plus the shared resolver are responsible for providing
Expand Down
Loading
Loading