From e59840a1c36bab91c47cb5b4780e4507581ad01d Mon Sep 17 00:00:00 2001 From: Siya Date: Sun, 11 Oct 2026 14:12:10 +0000 Subject: [PATCH 1/2] feel: sign off 74 rows of the command bar, split view, Peek, Mini Window and picture-in-picture (D48, D79) The lead's first "feels right" sign-off. Each row was checked against feel.md: the feel specs re-measured at low load (load 1.6-5.5, three rounds) and one 60 fps screen recording per area, read frame by frame. The record, with the rows held back and their numbers, is reviews/feel-signoff-1.md in the project's context folder. - docs/feature-status.json: "feel": true on 74 built rows (CB 20 and FL-16, FL-17; SV 19; PK 13; MW 12; MD 8). FEATURES.md regenerated: 74 feel right. - scripts/capture/scenarios: cb, sv, pk, mw and md, each printing a [mark] line per step so frames around it can be cut from the clip. - scripts/capture/record.mjs: --fps (60 to judge motion frame by frame) and --port (where the fixture server starts looking), so a capture stays in its own port range. Co-Authored-By: Claude Opus 5.5 --- FEATURES.md | 150 ++++++++++++------------ docs/feature-status.json | 148 ++++++++++++------------ scripts/capture/record.mjs | 14 ++- scripts/capture/scenarios/cb.mjs | 75 ++++++++++++ scripts/capture/scenarios/md.mjs | 132 +++++++++++++++++++++ scripts/capture/scenarios/mw.mjs | 115 +++++++++++++++++++ scripts/capture/scenarios/pk.mjs | 190 +++++++++++++++++++++++++++++++ scripts/capture/scenarios/sv.mjs | 87 ++++++++++++++ 8 files changed, 759 insertions(+), 152 deletions(-) create mode 100644 scripts/capture/scenarios/cb.mjs create mode 100644 scripts/capture/scenarios/md.mjs create mode 100644 scripts/capture/scenarios/mw.mjs create mode 100644 scripts/capture/scenarios/pk.mjs create mode 100644 scripts/capture/scenarios/sv.mjs diff --git a/FEATURES.md b/FEATURES.md index 0019dfd9..34ca3cee 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -4,7 +4,7 @@ Every row is one behaviour from the clean-room spec. Status per ID: **spec’d** (every row) → **built** → **tested** (a unit or e2e test names the ID) → **feels right** (lead sign-off after a hands-on review against `feel.md`). -**Totals:** 472 spec’d · 267 built (77 partial) · 349 tested · 0 feel right. 13 more are out of scope and not counted. +**Totals:** 472 spec’d · 267 built (77 partial) · 349 tested · 74 feel right. 13 more are out of scope and not counted. | Priority | Spec’d | Built | Tested | |---|---|---|---| @@ -165,29 +165,29 @@ Spec: `spec/commandbar.md` | ID | Feature | P | Built | Tested | Feels right | Notes | |---|---|---|---|---|---|---| -| CB-01 | One bar replaces the omnibox and the new-tab page | P0 | ✅ | ✅ e2e | – | no new-tab page: ⌘T and + New Tab open the bar; a tab exists only once a row is chosen | -| CB-02 | Open in new-tab mode (⌘T) | P0 | ✅ | ✅ e2e | – | ⌘T/Ctrl+T, + New Tab row, macOS File menu; second ⌘T closes | -| CB-03 | Open in edit-current-URL mode (⌘L) | P0 | ✅ | ✅ e2e+unit | – | ⌘L/Ctrl+L/Alt+D: full URL selected, current-page row (Enter reloads), navigates the current tab | -| CB-04 | Position, size and look: a floating centred panel | P0 | ✅ | ✅ e2e | – | centred in the window (D11b), input ~36% down, 660 px wide, ≤9 rows sized to fit, no scroll bar, Space accent, dark | +| CB-01 | One bar replaces the omnibox and the new-tab page | P0 | ✅ | ✅ e2e | ✅ | no new-tab page: ⌘T and + New Tab open the bar; a tab exists only once a row is chosen | +| CB-02 | Open in new-tab mode (⌘T) | P0 | ✅ | ✅ e2e | ✅ | ⌘T/Ctrl+T, + New Tab row, macOS File menu; second ⌘T closes | +| CB-03 | Open in edit-current-URL mode (⌘L) | P0 | ✅ | ✅ e2e+unit | ✅ | ⌘L/Ctrl+L/Alt+D: full URL selected, current-page row (Enter reloads), navigates the current tab | +| CB-04 | Position, size and look: a floating centred panel | P0 | ✅ | ✅ e2e | ✅ | centred in the window (D11b), input ~36% down, 660 px wide, ≤9 rows sized to fit, no scroll bar, Space accent, dark | | CB-05 | What it searches (sources) | P0 | 🟡 partial | ✅ e2e+unit | – | tabs, pinned/Favorites, folders, archive, per-profile history, typed URL + autocomplete, past searches, opt-in engine suggestions, actions, Spaces, and Settings: every section of the settings lane's registry (and common settings) is a row labelled Settings that opens Settings there (ST-02's actions; a unit test keeps each registered section in the bar); extensions by name wait for the extensions lane (EX-06) | -| CB-06 | Result kinds and their row labels | P0 | ✅ | ✅ e2e+unit | – | kind label on every row, split favicons, other-Space name, shortcuts, hints, middle-truncated URLs | -| CB-07 | Ranking and grouping | P0 | ✅ | ✅ e2e+unit | – | one ranked list (src/main/commandbar/rank.ts, tunable), best-guess top row, dedupe tab > archived > history, stable row ids | -| CB-08 | Typed URLs and inline autocompletion | P0 | ✅ | ✅ e2e+unit | – | inline domain/URL completion from history and tabs; Backspace drops it, →/End accept; scheme added | -| CB-09 | Switch to an open tab instead of duplicating | P0 | ✅ | ✅ e2e+unit | – | switches to the tab (and its Space) instead of duplicating; exact URL match tops the list; ⌥/Alt+Enter opens a copy | -| CB-10 | Keyboard navigation inside the bar | P0 | ✅ | ✅ e2e | – | ↑/↓ (⌃N/⌃P on macOS), Enter, hover, aria-activedescendant; ⌘⌥⌫ / Shift+Delete removes history rows | -| CB-11 | Site search with Tab (search a site directly) | P0 | ✅ | ✅ e2e+unit | – | keyword or site-name prefix + Tab → chip; Backspace/Esc leaves; starter list in settings.siteSearch (editing UI is ST-06) | -| CB-12 | Paste and go | P1 | ✅ | ✅ e2e | – | Paste and go row first in the empty state; ⌘⌥V / Ctrl+Alt+V is the tabs lane's tab.pasteAsNewTab | +| CB-06 | Result kinds and their row labels | P0 | ✅ | ✅ e2e+unit | ✅ | kind label on every row, split favicons, other-Space name, shortcuts, hints, middle-truncated URLs | +| CB-07 | Ranking and grouping | P0 | ✅ | ✅ e2e+unit | ✅ | one ranked list (src/main/commandbar/rank.ts, tunable), best-guess top row, dedupe tab > archived > history, stable row ids | +| CB-08 | Typed URLs and inline autocompletion | P0 | ✅ | ✅ e2e+unit | ✅ | inline domain/URL completion from history and tabs; Backspace drops it, →/End accept; scheme added | +| CB-09 | Switch to an open tab instead of duplicating | P0 | ✅ | ✅ e2e+unit | ✅ | switches to the tab (and its Space) instead of duplicating; exact URL match tops the list; ⌥/Alt+Enter opens a copy | +| CB-10 | Keyboard navigation inside the bar | P0 | ✅ | ✅ e2e | ✅ | ↑/↓ (⌃N/⌃P on macOS), Enter, hover, aria-activedescendant; ⌘⌥⌫ / Shift+Delete removes history rows | +| CB-11 | Site search with Tab (search a site directly) | P0 | ✅ | ✅ e2e+unit | ✅ | keyword or site-name prefix + Tab → chip; Backspace/Esc leaves; starter list in settings.siteSearch (editing UI is ST-06) | +| CB-12 | Paste and go | P1 | ✅ | ✅ e2e | ✅ | Paste and go row first in the empty state; ⌘⌥V / Ctrl+Alt+V is the tabs lane's tab.pasteAsNewTab | | CB-13 | Actions (browser commands by name) | P0 | 🟡 partial | ✅ e2e+unit | – | every registered action by name/synonym/initials, with the key the live keymap gives it (KS-18); Move to / Pin to [Space]; Open [Folder]; tab-only actions hidden on empty Spaces; a unit test checks each catalogue name Orbit has finds its action first, including the added Archive Tabs Below (TD-05), Close Split (SV-19; the whole split, Close Split Pane stays SV-17's) and the names Copy URLs, Change Theme and Zoom Reset. Not built, with their lanes: Show/Hide Toolbar (SB-17, look), New Blank Window (TB-17) and Manage Spaces (SP-13, finish-tabs), Enter/Exit Full Screen (NV-20), Save Page As (NV-21), Capture Full Page (DT-09) and Reader Mode (NV-23) (pages), Developer Mode for this site (DT-06, finish-downloads), Getting started (OB-10, finish-settings), Manage extensions and Extension: [name] (EX-06, extensions), New Note (notes), New Canvas and Capture (canvases), New Tweak and View Tweaks (BO-21, tweaks); Share is a later phase; Translate (NV-22, D45) and Manage passwords (D52) are out of scope | -| CB-14 | Actions-only mode (Tab on an empty field) | P1 | ✅ | ✅ e2e+unit | – | Tab on an empty field or '/ ' at the start → Actions chip; Esc/Backspace leaves | -| CB-15 | Default search engine fallback | P0 | ✅ | ✅ e2e+unit | – | default engine search row and destination (q=a+b); engine suggestions opt-in (settings.searchSuggestions, off by default), non-blocking with a 1.5 s timeout | -| CB-16 | Open the top web result directly (Shift+Enter) | P2 | ✅ | ✅ e2e+unit | – | ⇧Enter opens the first result (DuckDuckGo backslash prefix) when settings.searchTopResult is on (default off); otherwise like Enter | -| CB-17 | Remove a suggestion | P1 | ✅ | ✅ e2e+unit | – | hover × and ⌘⌥⌫ / Shift+Delete delete history and past-search rows | -| CB-18 | Empty state (bar opened with nothing typed) | P0 | ✅ | ✅ e2e+unit | – | recent tabs, frequent sites, paste row; starter actions/sites in a fresh profile (not pre-selected: Enter on the untouched field only runs Paste and go or ⌘L's current page); current page in ⌘L mode | -| CB-19 | Space and profile awareness | P0 | ✅ | ✅ e2e+unit | – | results use the window's current Space/profile and re-run on Space switch; new tabs go to the current Space | -| CB-20 | Sidebar stays interactive while the bar is open | P1 | ✅ | ✅ e2e | – | a left click over the docked or the hover-revealed sidebar closes the bar and reaches the sidebar where it is drawn (one click selects a tab); right and middle clicks only close it; ⌘T with the revealed sidebar out opens the bar over it and the sidebar stays until the bar closes; the window's edge doesn't bring the sidebar in while the bar is open (SB-12), so its appearance never dismisses the bar | -| CB-21 | Closing the bar and what happens to typed text | P0 | ✅ | ✅ e2e | – | Esc, ⌘T/⌘L again, click outside, window blur; text discarded; focus returns to the page, also when the window comes back after a background close | +| CB-14 | Actions-only mode (Tab on an empty field) | P1 | ✅ | ✅ e2e+unit | ✅ | Tab on an empty field or '/ ' at the start → Actions chip; Esc/Backspace leaves | +| CB-15 | Default search engine fallback | P0 | ✅ | ✅ e2e+unit | ✅ | default engine search row and destination (q=a+b); engine suggestions opt-in (settings.searchSuggestions, off by default), non-blocking with a 1.5 s timeout | +| CB-16 | Open the top web result directly (Shift+Enter) | P2 | ✅ | ✅ e2e+unit | ✅ | ⇧Enter opens the first result (DuckDuckGo backslash prefix) when settings.searchTopResult is on (default off); otherwise like Enter | +| CB-17 | Remove a suggestion | P1 | ✅ | ✅ e2e+unit | ✅ | hover × and ⌘⌥⌫ / Shift+Delete delete history and past-search rows | +| CB-18 | Empty state (bar opened with nothing typed) | P0 | ✅ | ✅ e2e+unit | ✅ | recent tabs, frequent sites, paste row; starter actions/sites in a fresh profile (not pre-selected: Enter on the untouched field only runs Paste and go or ⌘L's current page); current page in ⌘L mode | +| CB-19 | Space and profile awareness | P0 | ✅ | ✅ e2e+unit | ✅ | results use the window's current Space/profile and re-run on Space switch; new tabs go to the current Space | +| CB-20 | Sidebar stays interactive while the bar is open | P1 | ✅ | ✅ e2e | ✅ | a left click over the docked or the hover-revealed sidebar closes the bar and reaches the sidebar where it is drawn (one click selects a tab); right and middle clicks only close it; ⌘T with the revealed sidebar out opens the bar over it and the sidebar stays until the bar closes; the window's edge doesn't bring the sidebar in while the bar is open (SB-12), so its appearance never dismisses the bar | +| CB-21 | Closing the bar and what happens to typed text | P0 | ✅ | ✅ e2e | ✅ | Esc, ⌘T/⌘L again, click outside, window blur; text discarded; focus returns to the page, also when the window comes back after a background close | | CB-22 | Feel: open/close animation, latency and live updates | P0 | ✅ | ✅ e2e+unit | – | spring scale-fade entrance, quicker exit, reduced-motion fade; type-ahead and Enter buffered; IME-safe; per-keystroke latency measured | -| CB-23 | The bar in other contexts (split pane, private window, Mini Window) | P1 | ✅ | ✅ e2e | – | split pane: the bar that fills a new pane (⌃⇧=, Add Split View) takes ⌘W, ⌘T, ⌘L and Esc first: each closes it and the empty pane goes; a URL or an open tab fills the pane; ⌘L on a pane edits that pane (SV-02, SV-15). Private (non-persistent) profiles: no history or archive rows, and their tabs, folders and Spaces never show in normal windows. Mini Window: the blank window's lookup bar and the address field over its page (⌘L or a click on the host) are the bar's compact variant: the same ranking, inline completion, site search and engine suggestions, from its profile's history and the typed text, with no open tabs, archive, Spaces, folders, actions or Settings sections (they act on the browser); a choice loads in that Mini Window. Over a page the list covers a still image of the page (the page's own view steps aside until the bar closes); ⌘W and ⌘L close the bar first, Esc too, and Enter with nothing to load gives the page the keyboard; ⌘T opens the main window's bar. ⌘L on an empty Space acts like ⌘T | +| CB-23 | The bar in other contexts (split pane, private window, Mini Window) | P1 | ✅ | ✅ e2e | ✅ | split pane: the bar that fills a new pane (⌃⇧=, Add Split View) takes ⌘W, ⌘T, ⌘L and Esc first: each closes it and the empty pane goes; a URL or an open tab fills the pane; ⌘L on a pane edits that pane (SV-02, SV-15). Private (non-persistent) profiles: no history or archive rows, and their tabs, folders and Spaces never show in normal windows. Mini Window: the blank window's lookup bar and the address field over its page (⌘L or a click on the host) are the bar's compact variant: the same ranking, inline completion, site search and engine suggestions, from its profile's history and the typed text, with no open tabs, archive, Spaces, folders, actions or Settings sections (they act on the browser); a choice loads in that Mini Window. Over a page the list covers a still image of the page (the page's own view steps aside until the bar closes); ⌘W and ⌘L close the bar first, Esc too, and Enter with nothing to load gives the page the keyboard; ⌘T opens the main window's bar. ⌘L on an empty Space acts like ⌘T | ## Split View (prefix SV) @@ -195,31 +195,31 @@ Spec: `spec/split-view.md` | ID | Feature | P | Built | Tested | Feels right | Notes | |---|---|---|---|---|---|---| -| SV-01 | What a split view is | P0 | ✅ | ✅ e2e+unit | – | 2-4 live panes shown together; the split is one sidebar item | +| SV-01 | What a split view is | P0 | ✅ | ✅ e2e+unit | ✅ | 2-4 live panes shown together; the split is one sidebar item | | SV-02 | Create with the keyboard shortcut | P0 | ✅ | ✅ e2e+unit | – | Ctrl+Shift+= adds an empty pane and opens the command bar; a URL or open tab fills it, Escape removes it; refused in an empty window | | SV-03 | Create from the command bar | P0 | ✅ | ✅ e2e | – | Add Split Right/Left/Above/Below and New Split View actions; the command bar stays open for the new pane | -| SV-04 | Create by dragging a tab onto the page | P0 | ✅ | ✅ e2e+unit | – | drag a tab row over the page: theme-coloured drop indicator, the page makes room, release adds the pane | -| SV-05 | Create by dropping a tab onto another tab row | P0 | ✅ | ✅ e2e+unit | – | resting a dragged tab on a tab or split row's middle band (~400 ms) highlights that half; dropping makes or extends a split there, undoable with Cmd/Ctrl+Z; quick drags reorder as usual | -| SV-06 | Create from a multi-selection | P0 | ✅ | ✅ e2e+unit | – | Open in Split View in the tab menu for a 2-4 tab multi-selection (sidebar order); more than 4 is disabled with the hint | +| SV-04 | Create by dragging a tab onto the page | P0 | ✅ | ✅ e2e+unit | ✅ | drag a tab row over the page: theme-coloured drop indicator, the page makes room, release adds the pane | +| SV-05 | Create by dropping a tab onto another tab row | P0 | ✅ | ✅ e2e+unit | ✅ | resting a dragged tab on a tab or split row's middle band (~400 ms) highlights that half; dropping makes or extends a split there, undoable with Cmd/Ctrl+Z; quick drags reorder as usual | +| SV-06 | Create from a multi-selection | P0 | ✅ | ✅ e2e+unit | ✅ | Open in Split View in the tab menu for a 2-4 tab multi-selection (sidebar order); more than 4 is disabled with the hint | | SV-07 | Create from the "+" menu or the window's split button | P1 | ✅ | ✅ e2e+unit | – | the sidebar's + menu has New Split View (as ⌃⇧=: a pane beside the current tab with the bar ready; the choice fills it); on Windows and Linux a split button beside the window controls opens Add Split Right, Left, Above and Below, each showing its key, and is the first to give way when the sidebar is narrow; macOS has none (traffic lights). No right-edge hover target (decision) | | SV-08 | Open a link into a split | P1 | 🟡 partial | ✅ e2e+unit | – | Open Link in Split View in the page's link menu; Option/Alt-click a link opens it beside the page (the engine download is cancelled; Download Linked File stays in the menu); a link dragged out of a page that rests on a pane's page (or on Orbit's surface in the frame) for 250 ms shows the drop indicator with SV-04's zones, and the drop opens it in a new pane there; the page's own drop targets and text fields keep the drag, a drag that stays near the link does nothing, a full split takes none, http(s) only, main frame only; from a Peek, PK-08. Not built: a file dropped onto the window (inferred bullet): the drop gives no file address without a preload change (D40), and D58 (3) sends dropped addresses through D49 | -| SV-09 | Option-click a tab row | P1 | ✅ | ✅ e2e+unit | – | Alt-click a tab row joins it (the selected tab duplicates beside itself); Alt-click a split row separates | +| SV-09 | Option-click a tab row | P1 | ✅ | ✅ e2e+unit | ✅ | Alt-click a tab row joins it (the selected tab duplicates beside itself); Alt-click a split row separates | | SV-10 | Notes and canvases in a split | P2 | – | – | – | | -| SV-11 | Layouts and flipping orientation | P0 | ✅ | ✅ e2e+unit | – | row/column trees, 3-4 pane arrangements from where panes are added, flip transposes keeping ratios | -| SV-12 | Pane chrome: the hover strip | P0 | ✅ | ✅ e2e | – | hover strip view with close and split-controls menu (flip, add split x4, move pane, separate); no per-pane toolbar yet | -| SV-13 | Resize panes with the dividers | P0 | ✅ | ✅ e2e+unit | – | 1:1 divider drags, 240x160 px minimums, snapping to 1/3, 1/2, 2/3, double-click equalizes, sizes saved | -| SV-14 | The focused pane | P0 | ✅ | ✅ e2e | – | clicking a page focuses its pane; outline in the gap; address, Cmd+L and page commands follow the focused pane | -| SV-15 | Change what a pane shows | P0 | ✅ | ✅ e2e | – | Cmd+L on a pane changes only that pane; choosing an open Today tab swaps it in | -| SV-16 | Keyboard navigation between panes | P1 | ✅ | ✅ e2e | – | Ctrl+Shift+1-4, Ctrl+Shift+[ / ], Ctrl+Shift+- | -| SV-17 | Close one pane | P0 | ✅ | ✅ e2e+unit | – | strip close, Ctrl+Shift+-, Cmd/Ctrl+W (pinned and Favorite splits too) close one pane in one update; the others keep their arrangement; reopening restores its exact spot | -| SV-18 | Separate a split into tabs | P0 | ✅ | ✅ e2e | – | Separate Tabs from the split menu, pane menu and Alt-click | -| SV-19 | Close the whole split | P0 | ✅ | ✅ e2e+unit | – | the row's close closes the split as one unit (pinned unloads); one reopen brings the whole split back (layout, sizes, name, place). In the Archive each pane keeps its own row, marked as a split pane; restoring either brings the split back as one unit at the top of Today | -| SV-20 | Swap and move panes | P2 | ✅ | ✅ e2e+unit | – | drag a pane by its strip grip: centre swaps, an edge moves it there (live preview); Move Pane Left/Right/Up/Down in the pane menu | -| SV-21 | The split row in the sidebar | P0 | ✅ | ✅ e2e+unit | – | one row with a favicon+title segment per pane in reading order, segment click focuses; each segment shows its pane's audio and recording indicators (TB-15, MD-11), and a named row shows them after the name; a custom name and/or icon (picked from the split's menu, Change Icon…) replaces the segments: the icon stands in for the favicons, the pane titles stay unless named; the command bar lists an open split as one row (title: the name or the pane titles, every pane's favicon, label Split, hint Switch to split), found by any pane or its name, choosing it shows the split with the best-matching pane focused (tie: the pane used last); the panes' pages aren't offered again from history or the Archive, and the split on screen isn't offered at all, like the current tab | +| SV-11 | Layouts and flipping orientation | P0 | ✅ | ✅ e2e+unit | ✅ | row/column trees, 3-4 pane arrangements from where panes are added, flip transposes keeping ratios | +| SV-12 | Pane chrome: the hover strip | P0 | ✅ | ✅ e2e | ✅ | hover strip view with close and split-controls menu (flip, add split x4, move pane, separate); no per-pane toolbar yet | +| SV-13 | Resize panes with the dividers | P0 | ✅ | ✅ e2e+unit | ✅ | 1:1 divider drags, 240x160 px minimums, snapping to 1/3, 1/2, 2/3, double-click equalizes, sizes saved | +| SV-14 | The focused pane | P0 | ✅ | ✅ e2e | ✅ | clicking a page focuses its pane; outline in the gap; address, Cmd+L and page commands follow the focused pane | +| SV-15 | Change what a pane shows | P0 | ✅ | ✅ e2e | ✅ | Cmd+L on a pane changes only that pane; choosing an open Today tab swaps it in | +| SV-16 | Keyboard navigation between panes | P1 | ✅ | ✅ e2e | ✅ | Ctrl+Shift+1-4, Ctrl+Shift+[ / ], Ctrl+Shift+- | +| SV-17 | Close one pane | P0 | ✅ | ✅ e2e+unit | ✅ | strip close, Ctrl+Shift+-, Cmd/Ctrl+W (pinned and Favorite splits too) close one pane in one update; the others keep their arrangement; reopening restores its exact spot | +| SV-18 | Separate a split into tabs | P0 | ✅ | ✅ e2e | ✅ | Separate Tabs from the split menu, pane menu and Alt-click | +| SV-19 | Close the whole split | P0 | ✅ | ✅ e2e+unit | ✅ | the row's close closes the split as one unit (pinned unloads); one reopen brings the whole split back (layout, sizes, name, place). In the Archive each pane keeps its own row, marked as a split pane; restoring either brings the split back as one unit at the top of Today | +| SV-20 | Swap and move panes | P2 | ✅ | ✅ e2e+unit | ✅ | drag a pane by its strip grip: centre swaps, an edge moves it there (live preview); Move Pane Left/Right/Up/Down in the pane menu | +| SV-21 | The split row in the sidebar | P0 | ✅ | ✅ e2e+unit | ✅ | one row with a favicon+title segment per pane in reading order, segment click focuses; each segment shows its pane's audio and recording indicators (TB-15, MD-11), and a named row shows them after the name; a custom name and/or icon (picked from the split's menu, Change Icon…) replaces the segments: the icon stands in for the favicons, the pane titles stay unless named; the command bar lists an open split as one row (title: the name or the pane titles, every pane's favicon, label Split, hint Switch to split), found by any pane or its name, choosing it shows the split with the best-matching pane focused (tie: the pane used last); the panes' pages aren't offered again from history or the Archive, and the split on screen isn't offered at all, like the current tab | | SV-22 | The split as one unit: context menu | P1 | 🟡 partial | ✅ e2e+unit | – | split menu: rename, Change Icon… (emoji, removable), per-pane submenus (focus, mute, copy link, move out, close), flip, equalize, duplicate, pin/unpin, Add to Favorites, move to Space, Archive Tabs Below (in Today; greyed out on the last item), separate, close; Share is the spec's later phase and isn't built | -| SV-23 | Pinned and Favorite splits | P1 | ✅ | ✅ e2e+unit | – | pinning keeps panes, sizes and orientation; drops onto pinned tabs stay pinned; each pane keeps its own pinned URL, and the split's menu (Reset to Pinned URLs) or a double-click on its Favorite tile brings every pane back (Reset Tab on one pane still resets that pane); a link to another site in either pane of a pinned or Favorite split opens a Peek over the split from that pane; Favorite splits show a combined tile (with the away dot when a pane wandered off), or the split's custom icon, and drag like tiles; a Favorite split keeps its focused pane across Spaces | -| SV-24 | Splits persist | P0 | ✅ | ✅ e2e+unit | – | panes, order, orientation, sizes and focused pane survive relaunch; a Today split closed or auto-archived as one unit restores from the Archive as one unit, with its layout and sizes | -| SV-25 | Limits and edge cases | P1 | ✅ | ✅ e2e+unit | – | fifth pane refused with a toast on every route; no split from an empty window; HTML full screen (core) fills the window, hides the strip and returns to the split | +| SV-23 | Pinned and Favorite splits | P1 | ✅ | ✅ e2e+unit | ✅ | pinning keeps panes, sizes and orientation; drops onto pinned tabs stay pinned; each pane keeps its own pinned URL, and the split's menu (Reset to Pinned URLs) or a double-click on its Favorite tile brings every pane back (Reset Tab on one pane still resets that pane); a link to another site in either pane of a pinned or Favorite split opens a Peek over the split from that pane; Favorite splits show a combined tile (with the away dot when a pane wandered off), or the split's custom icon, and drag like tiles; a Favorite split keeps its focused pane across Spaces | +| SV-24 | Splits persist | P0 | ✅ | ✅ e2e+unit | ✅ | panes, order, orientation, sizes and focused pane survive relaunch; a Today split closed or auto-archived as one unit restores from the Archive as one unit, with its layout and sizes | +| SV-25 | Limits and edge cases | P1 | ✅ | ✅ e2e+unit | ✅ | fifth pane refused with a toast on every route; no split from an empty window; HTML full screen (core) fills the window, hides the strip and returns to the split | | SV-26 | Feel: create, resize, close | P1 | 🟡 partial | ✅ e2e+unit | – | springs for create/close/flip, closing card fades, 1:1 resize, drop indicator fades in and moves between halves at once (FL-18), frame colour until loaded, split row entrance; a pane added while a pane closes, or while a neighbour still grows back, grows in from its edge beside the neighbour (PR #19 review F10); the sidebar's row merge (two rows into one, and back on separate) isn't built: it's left to the finish-tabs lane | ## Page navigation, windows, links and page-level controls (prefix NV) @@ -260,22 +260,22 @@ Spec: `spec/peek.md` | ID | Feature | P | Built | Tested | Feels right | Notes | |---|---|---|---|---|---|---| -| PK-01 | What Peek is | P0 | ✅ | ✅ e2e+unit | – | floating card over the source tab; one Peek per window; links inside navigate it | -| PK-02 | Which links open in Peek: other-site links in pinned tabs and Favorites | P0 | ✅ | ✅ e2e+unit | – | other-site (eTLD+1) link clicks, Enter on a focused link, and target=_blank in pinned tabs/Favorites; same-site navigates. Only a navigation to the clicked link counts, so form posts, script navigations and auto-posted sign-ins stay in the tab | -| PK-03 | Shift-click opens any link in Peek | P1 | ✅ | ✅ e2e+unit | – | Shift-click in any tab; link menu 'Open Link in Peek' | -| PK-04 | Exceptions: meeting links and link-routing rules | P1 | ✅ | ✅ e2e+unit | – | meeting links open a tab; routing rules (settings.linkRules) win; rules UI is the settings lane's | -| PK-05 | The Peek card's look | P0 | ✅ | ✅ e2e+unit | – | inset card, shadow, dimmed page area, Close / Open as Tab / Split buttons with shortcut tooltips; no toolbar (AP-07 not built) | +| PK-01 | What Peek is | P0 | ✅ | ✅ e2e+unit | ✅ | floating card over the source tab; one Peek per window; links inside navigate it | +| PK-02 | Which links open in Peek: other-site links in pinned tabs and Favorites | P0 | ✅ | ✅ e2e+unit | ✅ | other-site (eTLD+1) link clicks, Enter on a focused link, and target=_blank in pinned tabs/Favorites; same-site navigates. Only a navigation to the clicked link counts, so form posts, script navigations and auto-posted sign-ins stay in the tab | +| PK-03 | Shift-click opens any link in Peek | P1 | ✅ | ✅ e2e+unit | ✅ | Shift-click in any tab; link menu 'Open Link in Peek' | +| PK-04 | Exceptions: meeting links and link-routing rules | P1 | ✅ | ✅ e2e+unit | ✅ | meeting links open a tab; routing rules (settings.linkRules) win; rules UI is the settings lane's | +| PK-05 | The Peek card's look | P0 | ✅ | ✅ e2e+unit | ✅ | inset card, shadow, dimmed page area, Close / Open as Tab / Split buttons with shortcut tooltips; no toolbar (AP-07 not built) | | PK-06 | Open and close animation, and the dismiss gesture | P1 | ✅ | ✅ e2e+unit | – | spring grow from the link, 18% dim (20% dark), 160 ms shrink to 0.96 and fade, reduced motion; drag the top handle or swipe down beside the card: follows the input, closes past 25% or 500 px/s, else springs back | -| PK-07 | Expand a Peek into a full tab | P0 | ✅ | ✅ e2e+unit | – | ⌘O / button: card morphs to the frame, live page becomes a selected Today tab (routing rules apply) | -| PK-08 | Open a Peek in Split View | P1 | ✅ | ✅ e2e | – | the card slides to the right half, then split.addTab makes the source and the live Peek page one split in the source's place (a pinned source gives a pinned split), the page focused; from a full split the Peek stays open and the SV-25 toast shows | -| PK-09 | Close a Peek | P0 | ✅ | ✅ e2e+unit | – | Esc (after fullscreen), ⌘W, ×, click outside; archived as from 'peek' | -| PK-10 | Reopen a Peek closed by mistake | P1 | ✅ | ✅ e2e | – | ⌘Z right after closing, or ⌘⇧T, reopens on the same page with history | +| PK-07 | Expand a Peek into a full tab | P0 | ✅ | ✅ e2e+unit | ✅ | ⌘O / button: card morphs to the frame, live page becomes a selected Today tab (routing rules apply) | +| PK-08 | Open a Peek in Split View | P1 | ✅ | ✅ e2e | ✅ | the card slides to the right half, then split.addTab makes the source and the live Peek page one split in the source's place (a pinned source gives a pinned split), the page focused; from a full split the Peek stays open and the SV-25 toast shows | +| PK-09 | Close a Peek | P0 | ✅ | ✅ e2e+unit | ✅ | Esc (after fullscreen), ⌘W, ×, click outside; archived as from 'peek' | +| PK-10 | Reopen a Peek closed by mistake | P1 | ✅ | ✅ e2e | ✅ | ⌘Z right after closing, or ⌘⇧T, reopens on the same page with history | | PK-11 | Navigating and acting inside a Peek | P1 | 🟡 partial | ✅ e2e+unit | – | links, forms, back/forward, reload, zoom, copy URL and copy URL as Markdown, print, devtools and find in page act on the Peek: ⌘F, ⌘G and F3 bring the find bar over the Peek card's top-right corner, searching the Peek's page with its own search, and Esc gives the Peek the keyboard back; the tab under it keeps its own search; the sidebar address shows the Peek host; ⌘L makes it a tab and edits it; downloads go to the normal list (DL-16). Not built: extensions in Peek pages come with EX-11 | -| PK-12 | Peek settings | P1 | ✅ | ✅ e2e+unit | – | Settings → Links has both toggles ("Open links from pinned tabs and Favorites in Peek", "Shift-click opens links in Peek"), on by default on every platform; off, a pinned tab's or Favorite's other-site link opens as a Today tab and ⇧-click opens a new window. Tests: settings.spec ST-09/MW-11 (toggles, defaults), peek.spec PK-12, nav-links.spec NV-08 ⇧-click, link-routing unit PK-12 | -| PK-13 | Peek and the sidebar | P0 | ✅ | ✅ e2e | – | no sidebar row; sidebar stays usable; selecting another tab, Space or window dismisses (reopenable) | +| PK-12 | Peek settings | P1 | ✅ | ✅ e2e+unit | ✅ | Settings → Links has both toggles ("Open links from pinned tabs and Favorites in Peek", "Shift-click opens links in Peek"), on by default on every platform; off, a pinned tab's or Favorite's other-site link opens as a Today tab and ⇧-click opens a new window. Tests: settings.spec ST-09/MW-11 (toggles, defaults), peek.spec PK-12, nav-links.spec NV-08 ⇧-click, link-routing unit PK-12 | +| PK-13 | Peek and the sidebar | P0 | ✅ | ✅ e2e | ✅ | no sidebar row; sidebar stays usable; selecting another tab, Space or window dismisses (reopenable) | | PK-14 | Canvases, notes and Library items open in Peek | P2 | – | – | – | | -| PK-15 | Peek is not the hover preview | P2 | ✅ | ✅ e2e | – | no hover-to-Peek trigger: hovering links or pinned rows never opens a Peek (hover cards are TB-21) | -| PK-16 | Peek and the Mini Window are different | P1 | ✅ | ✅ e2e+unit | – | Mini Window links never Peek; target=_blank stays in the Mini Window | +| PK-15 | Peek is not the hover preview | P2 | ✅ | ✅ e2e | ✅ | no hover-to-Peek trigger: hovering links or pinned rows never opens a Peek (hover cards are TB-21) | +| PK-16 | Peek and the Mini Window are different | P1 | ✅ | ✅ e2e+unit | ✅ | Mini Window links never Peek; target=_blank stays in the Mini Window | ## Mini Window (prefix MW) @@ -283,20 +283,20 @@ Spec: `spec/mini-window.md` | ID | Feature | P | Built | Tested | Feels right | Notes | |---|---|---|---|---|---|---| -| MW-01 | What the Mini Window is | P0 | ✅ | ✅ e2e | – | separate small window, one floating tab, main window untouched | -| MW-02 | Links from other apps open in a Mini Window by default | P0 | ✅ | ✅ e2e | – | open-url / second-instance / argv links open a Mini Window; same URL resurfaces it; routing rules first | -| MW-03 | Look and anatomy | P0 | ✅ | ✅ e2e | – | slim top bar: controls, favicon+host (click/⌘L opens the compact command bar on the page's address, CB-23), Open in…, profile pill; inset page card; neutral theme. ⌘F opens a find field in the top bar beside the host (NV-05): finds as you type, n/m count, No matches, Enter/⌘G/F3 next and ⇧ for previous, buttons; Esc closes it, keeps the match selected and gives the page the keyboard; keys typed before the field has the keyboard are not held (as they are for the main window's bar) | +| MW-01 | What the Mini Window is | P0 | ✅ | ✅ e2e | ✅ | separate small window, one floating tab, main window untouched | +| MW-02 | Links from other apps open in a Mini Window by default | P0 | ✅ | ✅ e2e | ✅ | open-url / second-instance / argv links open a Mini Window; same URL resurfaces it; routing rules first | +| MW-03 | Look and anatomy | P0 | ✅ | ✅ e2e | ✅ | slim top bar: controls, favicon+host (click/⌘L opens the compact command bar on the page's address, CB-23), Open in…, profile pill; inset page card; neutral theme. ⌘F opens a find field in the top bar beside the host (NV-05): finds as you type, n/m count, No matches, Enter/⌘G/F3 next and ⇧ for previous, buttons; Esc closes it, keeps the match selected and gives the page the keyboard; keys typed before the field has the keyboard are not held (as they are for the main window's bar) | | MW-04 | Opening animation and window placement | P1 | ✅ | ✅ e2e+unit | – | a quick scale-and-fade instead of the OS's animation: the window's UI grows from 92% to full size in 200 ms (the --ease-out curve) in its own layer while the window fades in (macOS, Windows; Linux can't fade a window, so there the UI fades in too). The OS window is not resized, which would lay the page out every frame: the page's native view, loading all along, sits out the grow and joins on the card at full size when it ends. Reduced motion shows it at once. Opens at the last-used size and place, each further window 24 px down and right; on macOS it shows above a full-screen Orbit window, on the current desktop. Only a person can check the desktop and full-screen parts, on a Mac | -| MW-05 | Move the page into a Space ("Open in…") | P0 | ✅ | ✅ e2e | – | ⌘O to the most recent Space, Open in… / ⌘⌥O (⌘⇧O) native Space menu; live page, no reload | -| MW-06 | Open a blank Mini Window for a quick lookup | P0 | ✅ | ✅ e2e+unit | – | ⌘⌥N opens a pre-rendered blank window with a focused lookup bar, the command bar's compact variant (CB-23): history and typed-text rows, inline completion, site search, engine suggestions; the choice loads here; Esc empties it, Esc on the empty bar or ⌘W closes the window. System-wide on macOS only (outside e2e); on Windows/Linux Ctrl+Alt+N works inside Orbit, so other apps (and AltGr+N) keep the key | -| MW-07 | Open a link or a tab in a Mini Window on purpose | P1 | ✅ | ✅ e2e+unit | – | ⌘⌥-click a link, link menu Open Link in Mini Window, or ⌘⌥-click a sidebar row: Today rows move live, pinned/Favorite rows open a copy | -| MW-08 | Several Mini Windows at once | P1 | ✅ | ✅ e2e | – | any number; Show/Hide All action and macOS dock menu | -| MW-09 | Closing, auto-archive and the Archive | P0 | ✅ | ✅ e2e+unit | – | close and idle auto-close (settings.miniWindowArchiveAfterMs, 6 h, 0 = never; not while audible) archive with from 'mini' | -| MW-10 | Profiles | P1 | ✅ | ✅ e2e+unit | – | profile pill/menu reopens the page in another profile; remembered per site | -| MW-11 | Turning the Mini Window off | P1 | ✅ | ✅ e2e | – | settings.miniWindowForExternalLinks off: tabs in the most recent or chosen Space (externalLinksSpace) | +| MW-05 | Move the page into a Space ("Open in…") | P0 | ✅ | ✅ e2e | ✅ | ⌘O to the most recent Space, Open in… / ⌘⌥O (⌘⇧O) native Space menu; live page, no reload | +| MW-06 | Open a blank Mini Window for a quick lookup | P0 | ✅ | ✅ e2e+unit | ✅ | ⌘⌥N opens a pre-rendered blank window with a focused lookup bar, the command bar's compact variant (CB-23): history and typed-text rows, inline completion, site search, engine suggestions; the choice loads here; Esc empties it, Esc on the empty bar or ⌘W closes the window. System-wide on macOS only (outside e2e); on Windows/Linux Ctrl+Alt+N works inside Orbit, so other apps (and AltGr+N) keep the key | +| MW-07 | Open a link or a tab in a Mini Window on purpose | P1 | ✅ | ✅ e2e+unit | ✅ | ⌘⌥-click a link, link menu Open Link in Mini Window, or ⌘⌥-click a sidebar row: Today rows move live, pinned/Favorite rows open a copy | +| MW-08 | Several Mini Windows at once | P1 | ✅ | ✅ e2e | ✅ | any number; Show/Hide All action and macOS dock menu | +| MW-09 | Closing, auto-archive and the Archive | P0 | ✅ | ✅ e2e+unit | ✅ | close and idle auto-close (settings.miniWindowArchiveAfterMs, 6 h, 0 = never; not while audible) archive with from 'mini' | +| MW-10 | Profiles | P1 | ✅ | ✅ e2e+unit | ✅ | profile pill/menu reopens the page in another profile; remembered per site | +| MW-11 | Turning the Mini Window off | P1 | ✅ | ✅ e2e | ✅ | settings.miniWindowForExternalLinks off: tabs in the most recent or chosen Space (externalLinksSpace) | | MW-12 | What works inside a Mini Window | P2 | 🟡 partial | ✅ e2e | – | a video's full screen happens in the Mini Window: the window fills the screen with the page, which stays there, and the card comes back on exit (tested); copy URL, devtools, downloads through the profile's session. Not built: extensions (Orbit has no extension support yet, EX-11), captures saved to a canvas (no Canvases yet) and sharing (Orbit has no share feature) | -| MW-13 | Menu-bar / tray button | P2 | ✅ | ✅ e2e+unit | – | off by default; Settings > General > Mini Window turns it on: Orbit's mark as a template glyph in the macOS menu bar, the app's icon in the Windows and Linux tray (the drawn mark where a build has no icon file); a click opens a blank Mini Window with its lookup bar focused (macOS: right-click for its menu); its menu has New Mini Window and Show/Hide All Mini Windows, New Mini Window first for Linux trays that show only the menu (tested through the button's click and menu). GNOME shows tray icons only with an AppIndicator extension; on Windows and Linux Orbit still quits with its last window, button or not | -| MW-14 | Platforms | P1 | ✅ | ✅ e2e+unit | – | all platforms with Ctrl+Alt+N / Ctrl+O / Ctrl+Alt+O, in-app outside macOS (D23), with the tray button (MW-13) as the way in from other apps; New Mini Window and Show/Hide All in the macOS dock menu, the Windows jump list, Linux launcher actions and the tray menu. Launcher actions: a packaged Linux folder build writes ~/.local/share/applications/.desktop at start-up (under $XDG_DATA_HOME when that is set; is Electron's desktop name), with no opt-out for now, and keeps it pointing at where Orbit runs from. It never touches an entry a package installed, one the user wrote, or one it can't read (checked on a packaged build: both actions reach the running Orbit). Only a person can check the Windows jump list | +| MW-13 | Menu-bar / tray button | P2 | ✅ | ✅ e2e+unit | ✅ | off by default; Settings > General > Mini Window turns it on: Orbit's mark as a template glyph in the macOS menu bar, the app's icon in the Windows and Linux tray (the drawn mark where a build has no icon file); a click opens a blank Mini Window with its lookup bar focused (macOS: right-click for its menu); its menu has New Mini Window and Show/Hide All Mini Windows, New Mini Window first for Linux trays that show only the menu (tested through the button's click and menu). GNOME shows tray icons only with an AppIndicator extension; on Windows and Linux Orbit still quits with its last window, button or not | +| MW-14 | Platforms | P1 | ✅ | ✅ e2e+unit | ✅ | all platforms with Ctrl+Alt+N / Ctrl+O / Ctrl+Alt+O, in-app outside macOS (D23), with the tray button (MW-13) as the way in from other apps; New Mini Window and Show/Hide All in the macOS dock menu, the Windows jump list, Linux launcher actions and the tray menu. Launcher actions: a packaged Linux folder build writes ~/.local/share/applications/.desktop at start-up (under $XDG_DATA_HOME when that is set; is Electron's desktop name), with no opt-out for now, and keeps it pointing at where Orbit runs from. It never touches an entry a package installed, one the user wrote, or one it can't read (checked on a packaged build: both actions reach the running Orbit). Only a person can check the Windows jump list | | MW-15 | Feel | P1 | ✅ | ✅ e2e | – | warm spare window takes keys at once, page loads before the window shows; opens with MW-04's 200 ms grow and fade; Open in… shrinks to 92% and fades (140 ms, macOS/Windows) as the row enters Today | ## Picture-in-picture, sidebar media player and call controls (prefix MD) @@ -307,16 +307,16 @@ Spec: `spec/media.md` |---|---|---|---|---|---|---| | MD-01 | Automatic picture-in-picture when leaving a video tab | P0 | ✅ | ✅ e2e+unit | – | Orbit's own floating player: leaving a tab whose main video plays (tab, Space or split change; the largest playing video, in the page or in any of its frames, cross-site ones included) moves it there within ~300 ms, still playing; not when paused, muted or turned off (MD-08); selecting the tab again puts it back in the page (or its frame) with no blank frame, at any load (a frame's page does it from its own size, before its first frame back is drawn); moving between a split's panes doesn't float it. A frame inside a shadow tree can't be found from its page: its video plays on in the tab and in the sidebar controller instead. Known limit (D71): a page's own picture-in-picture button inside a frame returns to the tab without raising the window; the tab is selected only when its window already has focus, in the Space it shows, and once for each frame document (D74). Known limit (D74): a page can end its own picture-in-picture once with exitPictureInPicture() and bring the window forward, as the engine's Back to tab button does; moving it to another of its videos brings nothing forward | | MD-02 | Open picture-in-picture manually | P1 | 🟡 partial | ✅ e2e | – | Picture in Picture in a video's page right-click menu, the Picture in Picture action and the tray's PiP button; while the tab stays selected the engine's own PiP window shows the video (Orbit's player needs the tab's view), so its controls are the engine's until the tab is left and Orbit's player takes over | -| MD-03 | The floating player's look and placement | P0 | ✅ | ✅ e2e+unit | – | frameless, always-on-top window, 320x180 for 16:9 (keeps the video's shape), bottom-right 16 px inside the work area; rounded corners (a window shape where the system has none) and shadow; video only until hovered, then controls over a dark gradient; the same player on every platform | -| MD-04 | Floating player controls | P0 | ✅ | ✅ e2e+unit | – | on hover: play/pause, +/-15 s, progress with elapsed and total time (seekable; LIVE badge for streams), Back to tab (selects it in its window and Space), x (keeps playing in the sidebar), ... menu (playback speed, turn off today / always for the site), previous/next for Media Session pages; double-click goes back to the tab | -| MD-05 | Keyboard and media keys for the floating player | P1 | ✅ | ✅ e2e | – | with the player focused (clicked) Space plays and pauses and the arrow keys seek 5 s, its buttons 15 s (tested); hardware media keys reach the playing video through Chromium's media key handling, which Orbit turns on: only a person can check them (no media keys under Xvfb) | -| MD-06 | Move, resize and zoom the floating player | P1 | ✅ | ✅ e2e+unit | – | drag or two-finger hover-scroll moves it and it glides to the corner the release or fling heads for; corner drag resizes keeping the shape, at least 200 px wide (tall videos too) and at most half the screen width and the screen's height, with rubber-band resistance; Cmd/Ctrl+scroll or pinch zooms (crops) inside the window; corner and size remembered. While an Orbit window shows an alert or dialog (the engine disables the window meanwhile, macOS shows a sheet, and Orbit's own file pickers say so), the player keeps clear of where it shows, up to 900 x 620 centered on the window (from its top on macOS): it glides to the nearest clear corner at its size, or goes out of sight, still playing, when no corner is clear, and comes back when the dialog closes unless it was moved meanwhile; its remembered place doesn't change (tested with an alert). A web page's own file picker on Linux isn't seen: the engine doesn't disable the window for it | +| MD-03 | The floating player's look and placement | P0 | ✅ | ✅ e2e+unit | ✅ | frameless, always-on-top window, 320x180 for 16:9 (keeps the video's shape), bottom-right 16 px inside the work area; rounded corners (a window shape where the system has none) and shadow; video only until hovered, then controls over a dark gradient; the same player on every platform | +| MD-04 | Floating player controls | P0 | ✅ | ✅ e2e+unit | ✅ | on hover: play/pause, +/-15 s, progress with elapsed and total time (seekable; LIVE badge for streams), Back to tab (selects it in its window and Space), x (keeps playing in the sidebar), ... menu (playback speed, turn off today / always for the site), previous/next for Media Session pages; double-click goes back to the tab | +| MD-05 | Keyboard and media keys for the floating player | P1 | ✅ | ✅ e2e | ✅ | with the player focused (clicked) Space plays and pauses and the arrow keys seek 5 s, its buttons 15 s (tested); hardware media keys reach the playing video through Chromium's media key handling, which Orbit turns on: only a person can check them (no media keys under Xvfb) | +| MD-06 | Move, resize and zoom the floating player | P1 | ✅ | ✅ e2e+unit | ✅ | drag or two-finger hover-scroll moves it and it glides to the corner the release or fling heads for; corner drag resizes keeping the shape, at least 200 px wide (tall videos too) and at most half the screen width and the screen's height, with rubber-band resistance; Cmd/Ctrl+scroll or pinch zooms (crops) inside the window; corner and size remembered. While an Orbit window shows an alert or dialog (the engine disables the window meanwhile, macOS shows a sheet, and Orbit's own file pickers say so), the player keeps clear of where it shows, up to 900 x 620 centered on the window (from its top on macOS): it glides to the nearest clear corner at its size, or goes out of sight, still playing, when no corner is clear, and comes back when the dialog closes unless it was moved meanwhile; its remembered place doesn't change (tested with an alert). A web page's own file picker on Linux isn't seen: the engine doesn't disable the window for it | | MD-07 | Ending picture-in-picture | P0 | ✅ | ✅ e2e | – | returning (select, double-click, Back to tab) ends it with the video playing in the page; x closes it and the media keeps playing in the sidebar controls; closing or unloading the tab stops media and player; navigating the tab or the video ending closes it, without replay; a picture-in-picture the engine closes because another one opened (in another pane, tab or window, by a page or by Orbit) isn't Back to tab: nothing is selected and no window comes forward (D75) | -| MD-08 | Turning automatic picture-in-picture off | P0 | ✅ | ✅ e2e+unit | – | site controls panel (NV-16) Picture in Picture switch, on by default: off is Always turn off for this site, on undoes it and Turn off for today, which it shows too; player ... menu Turn Off for Today / Always Turn Off for This Site (also in the tray controller menu); global switch Settings > Advanced (ST-10) and the Picture in Picture action; turning off keeps playback (sidebar controls); muted tabs never trigger | -| MD-09 | Sidebar media controls for background audio | P0 | ✅ | ✅ e2e+unit | – | controller above the bottom bar: the Media Session's artwork (loaded with the tab's session, kept small) or the favicon, Media Session title/artist or tab title (marquee), play/pause, next, previous where supported, x pauses and hides; click jumps to the tab (switching Space); quiet audio counts; hidden while its tab is selected; media in frames (embedded players, cross-site too) has the same controls | -| MD-10 | Several players: the media tray | P1 | ✅ | ✅ e2e+unit | – | one controller per background tab, newest first, appears and disappears by itself, works across windows; a video's controller has Picture in Picture (Back to PiP): the player grows out of the controller while the controller gives way | +| MD-08 | Turning automatic picture-in-picture off | P0 | ✅ | ✅ e2e+unit | ✅ | site controls panel (NV-16) Picture in Picture switch, on by default: off is Always turn off for this site, on undoes it and Turn off for today, which it shows too; player ... menu Turn Off for Today / Always Turn Off for This Site (also in the tray controller menu); global switch Settings > Advanced (ST-10) and the Picture in Picture action; turning off keeps playback (sidebar controls); muted tabs never trigger | +| MD-09 | Sidebar media controls for background audio | P0 | ✅ | ✅ e2e+unit | ✅ | controller above the bottom bar: the Media Session's artwork (loaded with the tab's session, kept small) or the favicon, Media Session title/artist or tab title (marquee), play/pause, next, previous where supported, x pauses and hides; click jumps to the tab (switching Space); quiet audio counts; hidden while its tab is selected; media in frames (embedded players, cross-site too) has the same controls | +| MD-10 | Several players: the media tray | P1 | ✅ | ✅ e2e+unit | ✅ | one controller per background tab, newest first, appears and disappears by itself, works across windows; a video's controller has Picture in Picture (Back to PiP): the player grows out of the controller while the controller gives way | | MD-11 | Audio and recording indicators, mute | P0 | 🟡 partial | ✅ e2e+unit | – | pulsing camera/microphone indicator on tab rows (tracks the page shows); a distinct blue screen-share indicator, lit while either source says a share is live (D69): main's watcher, put in the asking frame (any frame) when Orbit's screen chooser grants, or the page preload's count of the main frame's live shared tracks (covers macOS 15's system picker); only chooser grants keep a tab awake and unarchived; known limit: a page that wants to hide its share can (e.g. clone a track with a same-origin frame's built-ins and stop the one Orbit follows), the OS's own indicator is the backstop; a frame's share the macOS 15 picker grants doesn't show; a capture that fails after the chooser granted it shows until the page leaves; Peek and Mini Window shares have no row; per-pane Mute/Unmute in the split row's menu (split lane's menu, tested here: only that pane goes silent); no switcher indicators (TB-05, finish-tabs lane; TabMedia.mic/camera/screen are ready) | -| MD-12 | Media and the tab lifecycle | P0 | ✅ | ✅ e2e+unit | – | playing (muted too), PiP and camera/mic tabs never sleep, auto-archive, Clear or Archive Below; closing stops media, PiP and controller; isAnyMediaPlaying() is ready for the updates lane | +| MD-12 | Media and the tab lifecycle | P0 | ✅ | ✅ e2e+unit | ✅ | playing (muted too), PiP and camera/mic tabs never sleep, auto-archive, Clear or Archive Below; closing stops media, PiP and controller; isAnyMediaPlaying() is ready for the updates lane | | MD-13 | Video calls: automatic call picture-in-picture | P1 | 🟡 partial | ✅ e2e | – | leaving a call tab floats the largest participant video (never the self view) in Orbit's player with microphone/camera toggles and Back to call tab; moves and resizes like any video (a pinch zooms, as for any video). Not built: leave call (the site's Media Session hang-up handler lives in the page's own script world, which Orbit reaches only through a page patch, a lead decision under D40), a chat toggle (no web API exposes a site's chat), and the site's own document picture-in-picture view (also only through the page's Media Session) | | MD-14 | Call controls in the sidebar | P1 | 🟡 partial | ✅ e2e+unit | – | a background call tab (or a closed call window) gets a sidebar call controller: microphone and camera toggles (track.enabled on the tracks the page shows) and jump back. Not built: a call is seen only when its page shows its own camera (a self view); an audio-only call isn't, because the browser process can't see what a page captures and watching getUserMedia takes a page patch (a lead decision under D40) | | MD-15 | Switch which tab is being screen-shared | P2 | – | – | – | not possible as specified: Orbit's screen chooser shares a screen or a window, never a single tab, and the engine can't change what a running share captures (switching the shared tab is the Chrome browser's own feature, not in Electron); swapping the call page's video track would take a page patch, a lead decision under D40 | @@ -388,8 +388,8 @@ Spec: `spec/feel.md` | FL-13 | Space switching: swipe physics, settle and rubber-band | P0 | ✅ | ✅ e2e+unit | – | strip of pages, 1:1 tracking, commit thresholds, settle/rubber-band springs, one-page slides for clicks and keys, reduced motion cross-fade; the rubber band keeps the spring FL-13 names (response 0.3 s, ratio 1.0) and comes back within 0.5 px about 480 ms after the last event: FL-29's 350 ms is for swipes that land on a Space (D70) | | FL-14 | Theme-colour transition between Spaces | P0 | ✅ | ✅ e2e+unit | – | theme blends with the gesture/spring in OKLab; 200 ms preset blends; light/dark cross-fade | | FL-15 | Button press micro-animations | P1 | ✅ | ✅ e2e | – | every icon button presses to 0.94 at once and springs back (pop), the Library button too; Back/Forward nudge, Reload's glyph turns once, the + scales to 0.92; restarts on repeat; none when disabled; no spring with reduced motion | -| FL-16 | Command bar open and close | P0 | ✅ | ✅ e2e | – | motion spring (0.97→1, 4 px drop), 100 ms exit (80 ms after choosing a row) while focus returns at once; fade only with reduced motion; centred in the window | -| FL-17 | Command bar typing and result updates | P0 | ✅ | ✅ e2e+unit | – | per-keystroke local results, identity-keyed memoized rows, instant highlight; engine suggestions fill free rows without moving the highlight | +| FL-16 | Command bar open and close | P0 | ✅ | ✅ e2e | ✅ | motion spring (0.97→1, 4 px drop), 100 ms exit (80 ms after choosing a row) while focus returns at once; fade only with reduced motion; centred in the window | +| FL-17 | Command bar typing and result updates | P0 | ✅ | ✅ e2e+unit | ✅ | per-keystroke local results, identity-keyed memoized rows, instant highlight; engine suggestions fill free rows without moving the highlight | | FL-18 | Split view create and resize | P1 | 🟡 partial | ✅ e2e | – | a tab dragged over the page: the zone fades in over 100 ms on the half the new pane takes and moves between halves at once; the new pane grows in from its side with the standard spring; the divider follows the pointer 1:1; flip and close travel with the spring (docs/FEEL.md); a tab dragged onto another tab row highlights the half (SV-05) but has no bubble preview or wobble on drop | | FL-19 | Link preview open, expand, close | P1 | ✅ | ✅ e2e+unit | – | open: a spring tuned to FL-19's 280–320 ms (response 0.275 s, ratio 0.92; the standard token FL-19 names takes longer over a card's travel, D70), within 2 px in ~320 ms with no overshoot, 18% dim; expand meets the frame in ~250 ms, then the row enters; split slides to the right half; close 160 ms to 0.96 + fade, the page behind takes clicks again 173–194 ms after Escape (best of up to three rounds, at the edge of 200 ms); drag/swipe dismiss; ⌘Z/⌘⇧T replay the open | | FL-20 | Favorites tiles | P1 | ✅ | ✅ e2e | – | a new Favorite pops in from 0.85 with the pop spring while the other tiles re-flow with the snappy spring; a removed one shrinks to 0.85 and fades where it was while the grid closes up | diff --git a/docs/feature-status.json b/docs/feature-status.json index b1781876..fe265917 100644 --- a/docs/feature-status.json +++ b/docs/feature-status.json @@ -88,54 +88,54 @@ "FV-07": {"built": "yes", "note": "Favorite menu (icon, pinned URL, reset, move, mute, copy, close), title tooltip"}, "FV-08": {"built": "yes", "note": "Favorites sleep only after 4x the sleep period"}, "FV-10": {"built": "yes", "note": "see TB-04"}, - "SV-01": {"built": "yes", "note": "2-4 live panes shown together; the split is one sidebar item"}, + "SV-01": {"built": "yes", "feel": true, "note": "2-4 live panes shown together; the split is one sidebar item"}, "SV-02": {"built": "yes", "note": "Ctrl+Shift+= adds an empty pane and opens the command bar; a URL or open tab fills it, Escape removes it; refused in an empty window"}, "SV-03": {"built": "yes", "note": "Add Split Right/Left/Above/Below and New Split View actions; the command bar stays open for the new pane"}, - "SV-04": {"built": "yes", "note": "drag a tab row over the page: theme-coloured drop indicator, the page makes room, release adds the pane"}, - "SV-05": {"built": "yes", "note": "resting a dragged tab on a tab or split row's middle band (~400 ms) highlights that half; dropping makes or extends a split there, undoable with Cmd/Ctrl+Z; quick drags reorder as usual"}, - "SV-06": {"built": "yes", "note": "Open in Split View in the tab menu for a 2-4 tab multi-selection (sidebar order); more than 4 is disabled with the hint"}, + "SV-04": {"built": "yes", "feel": true, "note": "drag a tab row over the page: theme-coloured drop indicator, the page makes room, release adds the pane"}, + "SV-05": {"built": "yes", "feel": true, "note": "resting a dragged tab on a tab or split row's middle band (~400 ms) highlights that half; dropping makes or extends a split there, undoable with Cmd/Ctrl+Z; quick drags reorder as usual"}, + "SV-06": {"built": "yes", "feel": true, "note": "Open in Split View in the tab menu for a 2-4 tab multi-selection (sidebar order); more than 4 is disabled with the hint"}, "SV-07": {"built": "yes", "note": "the sidebar's + menu has New Split View (as ⌃⇧=: a pane beside the current tab with the bar ready; the choice fills it); on Windows and Linux a split button beside the window controls opens Add Split Right, Left, Above and Below, each showing its key, and is the first to give way when the sidebar is narrow; macOS has none (traffic lights). No right-edge hover target (decision)"}, "SV-08": {"built": "partial", "note": "Open Link in Split View in the page's link menu; Option/Alt-click a link opens it beside the page (the engine download is cancelled; Download Linked File stays in the menu); a link dragged out of a page that rests on a pane's page (or on Orbit's surface in the frame) for 250 ms shows the drop indicator with SV-04's zones, and the drop opens it in a new pane there; the page's own drop targets and text fields keep the drag, a drag that stays near the link does nothing, a full split takes none, http(s) only, main frame only; from a Peek, PK-08. Not built: a file dropped onto the window (inferred bullet): the drop gives no file address without a preload change (D40), and D58 (3) sends dropped addresses through D49"}, - "SV-09": {"built": "yes", "note": "Alt-click a tab row joins it (the selected tab duplicates beside itself); Alt-click a split row separates"}, - "SV-11": {"built": "yes", "note": "row/column trees, 3-4 pane arrangements from where panes are added, flip transposes keeping ratios"}, - "SV-12": {"built": "yes", "note": "hover strip view with close and split-controls menu (flip, add split x4, move pane, separate); no per-pane toolbar yet"}, - "SV-13": {"built": "yes", "note": "1:1 divider drags, 240x160 px minimums, snapping to 1/3, 1/2, 2/3, double-click equalizes, sizes saved"}, - "SV-14": {"built": "yes", "note": "clicking a page focuses its pane; outline in the gap; address, Cmd+L and page commands follow the focused pane"}, - "SV-15": {"built": "yes", "note": "Cmd+L on a pane changes only that pane; choosing an open Today tab swaps it in"}, - "SV-16": {"built": "yes", "note": "Ctrl+Shift+1-4, Ctrl+Shift+[ / ], Ctrl+Shift+-"}, - "SV-17": {"built": "yes", "note": "strip close, Ctrl+Shift+-, Cmd/Ctrl+W (pinned and Favorite splits too) close one pane in one update; the others keep their arrangement; reopening restores its exact spot"}, - "SV-18": {"built": "yes", "note": "Separate Tabs from the split menu, pane menu and Alt-click"}, - "SV-19": {"built": "yes", "note": "the row's close closes the split as one unit (pinned unloads); one reopen brings the whole split back (layout, sizes, name, place). In the Archive each pane keeps its own row, marked as a split pane; restoring either brings the split back as one unit at the top of Today"}, - "SV-20": {"built": "yes", "note": "drag a pane by its strip grip: centre swaps, an edge moves it there (live preview); Move Pane Left/Right/Up/Down in the pane menu"}, - "SV-21": {"built": "yes", "note": "one row with a favicon+title segment per pane in reading order, segment click focuses; each segment shows its pane's audio and recording indicators (TB-15, MD-11), and a named row shows them after the name; a custom name and/or icon (picked from the split's menu, Change Icon…) replaces the segments: the icon stands in for the favicons, the pane titles stay unless named; the command bar lists an open split as one row (title: the name or the pane titles, every pane's favicon, label Split, hint Switch to split), found by any pane or its name, choosing it shows the split with the best-matching pane focused (tie: the pane used last); the panes' pages aren't offered again from history or the Archive, and the split on screen isn't offered at all, like the current tab"}, + "SV-09": {"built": "yes", "feel": true, "note": "Alt-click a tab row joins it (the selected tab duplicates beside itself); Alt-click a split row separates"}, + "SV-11": {"built": "yes", "feel": true, "note": "row/column trees, 3-4 pane arrangements from where panes are added, flip transposes keeping ratios"}, + "SV-12": {"built": "yes", "feel": true, "note": "hover strip view with close and split-controls menu (flip, add split x4, move pane, separate); no per-pane toolbar yet"}, + "SV-13": {"built": "yes", "feel": true, "note": "1:1 divider drags, 240x160 px minimums, snapping to 1/3, 1/2, 2/3, double-click equalizes, sizes saved"}, + "SV-14": {"built": "yes", "feel": true, "note": "clicking a page focuses its pane; outline in the gap; address, Cmd+L and page commands follow the focused pane"}, + "SV-15": {"built": "yes", "feel": true, "note": "Cmd+L on a pane changes only that pane; choosing an open Today tab swaps it in"}, + "SV-16": {"built": "yes", "feel": true, "note": "Ctrl+Shift+1-4, Ctrl+Shift+[ / ], Ctrl+Shift+-"}, + "SV-17": {"built": "yes", "feel": true, "note": "strip close, Ctrl+Shift+-, Cmd/Ctrl+W (pinned and Favorite splits too) close one pane in one update; the others keep their arrangement; reopening restores its exact spot"}, + "SV-18": {"built": "yes", "feel": true, "note": "Separate Tabs from the split menu, pane menu and Alt-click"}, + "SV-19": {"built": "yes", "feel": true, "note": "the row's close closes the split as one unit (pinned unloads); one reopen brings the whole split back (layout, sizes, name, place). In the Archive each pane keeps its own row, marked as a split pane; restoring either brings the split back as one unit at the top of Today"}, + "SV-20": {"built": "yes", "feel": true, "note": "drag a pane by its strip grip: centre swaps, an edge moves it there (live preview); Move Pane Left/Right/Up/Down in the pane menu"}, + "SV-21": {"built": "yes", "feel": true, "note": "one row with a favicon+title segment per pane in reading order, segment click focuses; each segment shows its pane's audio and recording indicators (TB-15, MD-11), and a named row shows them after the name; a custom name and/or icon (picked from the split's menu, Change Icon…) replaces the segments: the icon stands in for the favicons, the pane titles stay unless named; the command bar lists an open split as one row (title: the name or the pane titles, every pane's favicon, label Split, hint Switch to split), found by any pane or its name, choosing it shows the split with the best-matching pane focused (tie: the pane used last); the panes' pages aren't offered again from history or the Archive, and the split on screen isn't offered at all, like the current tab"}, "SV-22": {"built": "partial", "note": "split menu: rename, Change Icon… (emoji, removable), per-pane submenus (focus, mute, copy link, move out, close), flip, equalize, duplicate, pin/unpin, Add to Favorites, move to Space, Archive Tabs Below (in Today; greyed out on the last item), separate, close; Share is the spec's later phase and isn't built"}, - "SV-23": {"built": "yes", "note": "pinning keeps panes, sizes and orientation; drops onto pinned tabs stay pinned; each pane keeps its own pinned URL, and the split's menu (Reset to Pinned URLs) or a double-click on its Favorite tile brings every pane back (Reset Tab on one pane still resets that pane); a link to another site in either pane of a pinned or Favorite split opens a Peek over the split from that pane; Favorite splits show a combined tile (with the away dot when a pane wandered off), or the split's custom icon, and drag like tiles; a Favorite split keeps its focused pane across Spaces"}, - "SV-24": {"built": "yes", "note": "panes, order, orientation, sizes and focused pane survive relaunch; a Today split closed or auto-archived as one unit restores from the Archive as one unit, with its layout and sizes"}, - "SV-25": {"built": "yes", "note": "fifth pane refused with a toast on every route; no split from an empty window; HTML full screen (core) fills the window, hides the strip and returns to the split"}, + "SV-23": {"built": "yes", "feel": true, "note": "pinning keeps panes, sizes and orientation; drops onto pinned tabs stay pinned; each pane keeps its own pinned URL, and the split's menu (Reset to Pinned URLs) or a double-click on its Favorite tile brings every pane back (Reset Tab on one pane still resets that pane); a link to another site in either pane of a pinned or Favorite split opens a Peek over the split from that pane; Favorite splits show a combined tile (with the away dot when a pane wandered off), or the split's custom icon, and drag like tiles; a Favorite split keeps its focused pane across Spaces"}, + "SV-24": {"built": "yes", "feel": true, "note": "panes, order, orientation, sizes and focused pane survive relaunch; a Today split closed or auto-archived as one unit restores from the Archive as one unit, with its layout and sizes"}, + "SV-25": {"built": "yes", "feel": true, "note": "fifth pane refused with a toast on every route; no split from an empty window; HTML full screen (core) fills the window, hides the strip and returns to the split"}, "SV-26": {"built": "partial", "note": "springs for create/close/flip, closing card fades, 1:1 resize, drop indicator fades in and moves between halves at once (FL-18), frame colour until loaded, split row entrance; a pane added while a pane closes, or while a neighbour still grows back, grows in from its edge beside the neighbour (PR #19 review F10); the sidebar's row merge (two rows into one, and back on separate) isn't built: it's left to the finish-tabs lane"}, - "CB-01": {"built": "yes", "note": "no new-tab page: ⌘T and + New Tab open the bar; a tab exists only once a row is chosen"}, - "CB-02": {"built": "yes", "note": "⌘T/Ctrl+T, + New Tab row, macOS File menu; second ⌘T closes"}, - "CB-03": {"built": "yes", "note": "⌘L/Ctrl+L/Alt+D: full URL selected, current-page row (Enter reloads), navigates the current tab"}, - "CB-04": {"built": "yes", "note": "centred in the window (D11b), input ~36% down, 660 px wide, ≤9 rows sized to fit, no scroll bar, Space accent, dark"}, + "CB-01": {"built": "yes", "feel": true, "note": "no new-tab page: ⌘T and + New Tab open the bar; a tab exists only once a row is chosen"}, + "CB-02": {"built": "yes", "feel": true, "note": "⌘T/Ctrl+T, + New Tab row, macOS File menu; second ⌘T closes"}, + "CB-03": {"built": "yes", "feel": true, "note": "⌘L/Ctrl+L/Alt+D: full URL selected, current-page row (Enter reloads), navigates the current tab"}, + "CB-04": {"built": "yes", "feel": true, "note": "centred in the window (D11b), input ~36% down, 660 px wide, ≤9 rows sized to fit, no scroll bar, Space accent, dark"}, "CB-05": {"built": "partial", "note": "tabs, pinned/Favorites, folders, archive, per-profile history, typed URL + autocomplete, past searches, opt-in engine suggestions, actions, Spaces, and Settings: every section of the settings lane's registry (and common settings) is a row labelled Settings that opens Settings there (ST-02's actions; a unit test keeps each registered section in the bar); extensions by name wait for the extensions lane (EX-06)"}, - "CB-06": {"built": "yes", "note": "kind label on every row, split favicons, other-Space name, shortcuts, hints, middle-truncated URLs"}, - "CB-07": {"built": "yes", "note": "one ranked list (src/main/commandbar/rank.ts, tunable), best-guess top row, dedupe tab > archived > history, stable row ids"}, - "CB-08": {"built": "yes", "note": "inline domain/URL completion from history and tabs; Backspace drops it, →/End accept; scheme added"}, - "CB-09": {"built": "yes", "note": "switches to the tab (and its Space) instead of duplicating; exact URL match tops the list; ⌥/Alt+Enter opens a copy"}, - "CB-10": {"built": "yes", "note": "↑/↓ (⌃N/⌃P on macOS), Enter, hover, aria-activedescendant; ⌘⌥⌫ / Shift+Delete removes history rows"}, - "CB-11": {"built": "yes", "note": "keyword or site-name prefix + Tab → chip; Backspace/Esc leaves; starter list in settings.siteSearch (editing UI is ST-06)"}, - "CB-12": {"built": "yes", "note": "Paste and go row first in the empty state; ⌘⌥V / Ctrl+Alt+V is the tabs lane's tab.pasteAsNewTab"}, + "CB-06": {"built": "yes", "feel": true, "note": "kind label on every row, split favicons, other-Space name, shortcuts, hints, middle-truncated URLs"}, + "CB-07": {"built": "yes", "feel": true, "note": "one ranked list (src/main/commandbar/rank.ts, tunable), best-guess top row, dedupe tab > archived > history, stable row ids"}, + "CB-08": {"built": "yes", "feel": true, "note": "inline domain/URL completion from history and tabs; Backspace drops it, →/End accept; scheme added"}, + "CB-09": {"built": "yes", "feel": true, "note": "switches to the tab (and its Space) instead of duplicating; exact URL match tops the list; ⌥/Alt+Enter opens a copy"}, + "CB-10": {"built": "yes", "feel": true, "note": "↑/↓ (⌃N/⌃P on macOS), Enter, hover, aria-activedescendant; ⌘⌥⌫ / Shift+Delete removes history rows"}, + "CB-11": {"built": "yes", "feel": true, "note": "keyword or site-name prefix + Tab → chip; Backspace/Esc leaves; starter list in settings.siteSearch (editing UI is ST-06)"}, + "CB-12": {"built": "yes", "feel": true, "note": "Paste and go row first in the empty state; ⌘⌥V / Ctrl+Alt+V is the tabs lane's tab.pasteAsNewTab"}, "CB-13": {"built": "partial", "note": "every registered action by name/synonym/initials, with the key the live keymap gives it (KS-18); Move to / Pin to [Space]; Open [Folder]; tab-only actions hidden on empty Spaces; a unit test checks each catalogue name Orbit has finds its action first, including the added Archive Tabs Below (TD-05), Close Split (SV-19; the whole split, Close Split Pane stays SV-17's) and the names Copy URLs, Change Theme and Zoom Reset. Not built, with their lanes: Show/Hide Toolbar (SB-17, look), New Blank Window (TB-17) and Manage Spaces (SP-13, finish-tabs), Enter/Exit Full Screen (NV-20), Save Page As (NV-21), Capture Full Page (DT-09) and Reader Mode (NV-23) (pages), Developer Mode for this site (DT-06, finish-downloads), Getting started (OB-10, finish-settings), Manage extensions and Extension: [name] (EX-06, extensions), New Note (notes), New Canvas and Capture (canvases), New Tweak and View Tweaks (BO-21, tweaks); Share is a later phase; Translate (NV-22, D45) and Manage passwords (D52) are out of scope"}, - "CB-14": {"built": "yes", "note": "Tab on an empty field or '/ ' at the start → Actions chip; Esc/Backspace leaves"}, - "CB-15": {"built": "yes", "note": "default engine search row and destination (q=a+b); engine suggestions opt-in (settings.searchSuggestions, off by default), non-blocking with a 1.5 s timeout"}, - "CB-16": {"built": "yes", "note": "⇧Enter opens the first result (DuckDuckGo backslash prefix) when settings.searchTopResult is on (default off); otherwise like Enter"}, - "CB-17": {"built": "yes", "note": "hover × and ⌘⌥⌫ / Shift+Delete delete history and past-search rows"}, - "CB-18": {"built": "yes", "note": "recent tabs, frequent sites, paste row; starter actions/sites in a fresh profile (not pre-selected: Enter on the untouched field only runs Paste and go or ⌘L's current page); current page in ⌘L mode"}, - "CB-19": {"built": "yes", "note": "results use the window's current Space/profile and re-run on Space switch; new tabs go to the current Space"}, - "CB-20": {"built": "yes", "note": "a left click over the docked or the hover-revealed sidebar closes the bar and reaches the sidebar where it is drawn (one click selects a tab); right and middle clicks only close it; ⌘T with the revealed sidebar out opens the bar over it and the sidebar stays until the bar closes; the window's edge doesn't bring the sidebar in while the bar is open (SB-12), so its appearance never dismisses the bar"}, - "CB-21": {"built": "yes", "note": "Esc, ⌘T/⌘L again, click outside, window blur; text discarded; focus returns to the page, also when the window comes back after a background close"}, + "CB-14": {"built": "yes", "feel": true, "note": "Tab on an empty field or '/ ' at the start → Actions chip; Esc/Backspace leaves"}, + "CB-15": {"built": "yes", "feel": true, "note": "default engine search row and destination (q=a+b); engine suggestions opt-in (settings.searchSuggestions, off by default), non-blocking with a 1.5 s timeout"}, + "CB-16": {"built": "yes", "feel": true, "note": "⇧Enter opens the first result (DuckDuckGo backslash prefix) when settings.searchTopResult is on (default off); otherwise like Enter"}, + "CB-17": {"built": "yes", "feel": true, "note": "hover × and ⌘⌥⌫ / Shift+Delete delete history and past-search rows"}, + "CB-18": {"built": "yes", "feel": true, "note": "recent tabs, frequent sites, paste row; starter actions/sites in a fresh profile (not pre-selected: Enter on the untouched field only runs Paste and go or ⌘L's current page); current page in ⌘L mode"}, + "CB-19": {"built": "yes", "feel": true, "note": "results use the window's current Space/profile and re-run on Space switch; new tabs go to the current Space"}, + "CB-20": {"built": "yes", "feel": true, "note": "a left click over the docked or the hover-revealed sidebar closes the bar and reaches the sidebar where it is drawn (one click selects a tab); right and middle clicks only close it; ⌘T with the revealed sidebar out opens the bar over it and the sidebar stays until the bar closes; the window's edge doesn't bring the sidebar in while the bar is open (SB-12), so its appearance never dismisses the bar"}, + "CB-21": {"built": "yes", "feel": true, "note": "Esc, ⌘T/⌘L again, click outside, window blur; text discarded; focus returns to the page, also when the window comes back after a background close"}, "CB-22": {"built": "yes", "note": "spring scale-fade entrance, quicker exit, reduced-motion fade; type-ahead and Enter buffered; IME-safe; per-keystroke latency measured"}, - "CB-23": {"built": "yes", "note": "split pane: the bar that fills a new pane (⌃⇧=, Add Split View) takes ⌘W, ⌘T, ⌘L and Esc first: each closes it and the empty pane goes; a URL or an open tab fills the pane; ⌘L on a pane edits that pane (SV-02, SV-15). Private (non-persistent) profiles: no history or archive rows, and their tabs, folders and Spaces never show in normal windows. Mini Window: the blank window's lookup bar and the address field over its page (⌘L or a click on the host) are the bar's compact variant: the same ranking, inline completion, site search and engine suggestions, from its profile's history and the typed text, with no open tabs, archive, Spaces, folders, actions or Settings sections (they act on the browser); a choice loads in that Mini Window. Over a page the list covers a still image of the page (the page's own view steps aside until the bar closes); ⌘W and ⌘L close the bar first, Esc too, and Enter with nothing to load gives the page the keyboard; ⌘T opens the main window's bar. ⌘L on an empty Space acts like ⌘T"}, + "CB-23": {"built": "yes", "feel": true, "note": "split pane: the bar that fills a new pane (⌃⇧=, Add Split View) takes ⌘W, ⌘T, ⌘L and Esc first: each closes it and the empty pane goes; a URL or an open tab fills the pane; ⌘L on a pane edits that pane (SV-02, SV-15). Private (non-persistent) profiles: no history or archive rows, and their tabs, folders and Spaces never show in normal windows. Mini Window: the blank window's lookup bar and the address field over its page (⌘L or a click on the host) are the bar's compact variant: the same ranking, inline completion, site search and engine suggestions, from its profile's history and the typed text, with no open tabs, archive, Spaces, folders, actions or Settings sections (they act on the browser); a choice loads in that Mini Window. Over a page the list covers a still image of the page (the page's own view steps aside until the bar closes); ⌘W and ⌘L close the bar first, Esc too, and Enter with nothing to load gives the page the keyboard; ⌘T opens the main window's bar. ⌘L on an empty Space acts like ⌘T"}, "KS-04": {"built": "yes", "note": "⌘T/⌘L/Alt+D, Esc, ⌘T/⌘L again, ↑/↓ (⌃N/⌃P), Enter, Tab, ⌫, ⇧Enter (option), ⌘⌥⌫ / Shift+Delete, ⌘⌥V"}, "KS-07": {"built": "yes", "note": "Space shortcuts wired through the action table"}, "KS-19": {"built": "partial", "note": "the open overlay takes its keys first: command bar (⌘T/⌘L/⌘W/Esc), find bar (Enter/⇧Enter/Esc, NV-05), Peek (Esc, ⌘O, ⌘W, PK-09; Esc closes the Peek and the tab under it keeps loading), the recent-tab switcher (Tab/⇧Tab/`, W, letting go of ⌃), permission prompt and site controls (Esc), the sidebar with keyboard focus (arrows, Enter, Esc back to the page, SB-22); then Esc on a loading page goes to the page first: the page's preload tells main whether the page used it, and Orbit stops the load only if it didn't, waiting 250 ms at most (no answer counts as left alone; D59, D60); only the main frame of a tab's page answers, and only for an Esc pressed there. Not built: leaving full screen with Esc held or pressed twice (NV-20), the print dialog (NV-07)"}, @@ -149,8 +149,8 @@ "KS-06": {"built": "yes", "note": "⌘1–⌘8 / Ctrl+1–8 (Favorites counted first unless turned off), ⌘9 / Ctrl+9 the last item or, set so, the ninth, ⌘⌥↓/↑ / Ctrl+Alt+↓/↑ and the ⌘⇧] / ⌘⇧[ (Ctrl+PgDn / Ctrl+PgUp) aliases, all tested from a page; while ⌃ is held, Tab and ⇧Tab step through recent tabs and ` steps back too, on every platform and only while the switcher is up (D60), W closes the highlighted tab (the next one in the switcher takes its place) and letting go settles; the switcher has no on-screen list yet (TB-05)"}, "FL-13": {"built": "yes", "note": "strip of pages, 1:1 tracking, commit thresholds, settle/rubber-band springs, one-page slides for clicks and keys, reduced motion cross-fade; the rubber band keeps the spring FL-13 names (response 0.3 s, ratio 1.0) and comes back within 0.5 px about 480 ms after the last event: FL-29's 350 ms is for swipes that land on a Space (D70)"}, "FL-14": {"built": "yes", "note": "theme blends with the gesture/spring in OKLab; 200 ms preset blends; light/dark cross-fade"}, - "FL-16": {"built": "yes", "note": "motion spring (0.97→1, 4 px drop), 100 ms exit (80 ms after choosing a row) while focus returns at once; fade only with reduced motion; centred in the window"}, - "FL-17": {"built": "yes", "note": "per-keystroke local results, identity-keyed memoized rows, instant highlight; engine suggestions fill free rows without moving the highlight"}, + "FL-16": {"built": "yes", "feel": true, "note": "motion spring (0.97→1, 4 px drop), 100 ms exit (80 ms after choosing a row) while focus returns at once; fade only with reduced motion; centred in the window"}, + "FL-17": {"built": "yes", "feel": true, "note": "per-keystroke local results, identity-keyed memoized rows, instant highlight; engine suggestions fill free rows without moving the highlight"}, "SP-01": {"built": "yes", "note": "Spaces own name, icon, theme, pinned and Today; Favorites belong to the profile"}, "SP-02": {"built": "partial", "note": "flow in the sidebar (name+icon, profile, theme with light/dark renditions), inserted after the active Space, matching placeholder; the hidden-sidebar toast has no Show action yet"}, "SP-03": {"built": "yes", "note": "inline rename; Escape cancels; empty name restores the placeholder"}, @@ -210,16 +210,16 @@ "AP-05": {"built": "yes", "note": "private windows are near-black whatever the theme (frame, sidebar, window background) with hover and selection tuned for black; Private label with icon in the sidebar, not over the page; no Space switcher or Favorites"}, "MD-01": {"built": "yes", "note": "Orbit's own floating player: leaving a tab whose main video plays (tab, Space or split change; the largest playing video, in the page or in any of its frames, cross-site ones included) moves it there within ~300 ms, still playing; not when paused, muted or turned off (MD-08); selecting the tab again puts it back in the page (or its frame) with no blank frame, at any load (a frame's page does it from its own size, before its first frame back is drawn); moving between a split's panes doesn't float it. A frame inside a shadow tree can't be found from its page: its video plays on in the tab and in the sidebar controller instead. Known limit (D71): a page's own picture-in-picture button inside a frame returns to the tab without raising the window; the tab is selected only when its window already has focus, in the Space it shows, and once for each frame document (D74). Known limit (D74): a page can end its own picture-in-picture once with exitPictureInPicture() and bring the window forward, as the engine's Back to tab button does; moving it to another of its videos brings nothing forward"}, "MD-02": {"built": "partial", "note": "Picture in Picture in a video's page right-click menu, the Picture in Picture action and the tray's PiP button; while the tab stays selected the engine's own PiP window shows the video (Orbit's player needs the tab's view), so its controls are the engine's until the tab is left and Orbit's player takes over"}, - "MD-03": {"built": "yes", "note": "frameless, always-on-top window, 320x180 for 16:9 (keeps the video's shape), bottom-right 16 px inside the work area; rounded corners (a window shape where the system has none) and shadow; video only until hovered, then controls over a dark gradient; the same player on every platform"}, - "MD-04": {"built": "yes", "note": "on hover: play/pause, +/-15 s, progress with elapsed and total time (seekable; LIVE badge for streams), Back to tab (selects it in its window and Space), x (keeps playing in the sidebar), ... menu (playback speed, turn off today / always for the site), previous/next for Media Session pages; double-click goes back to the tab"}, - "MD-05": {"built": "yes", "note": "with the player focused (clicked) Space plays and pauses and the arrow keys seek 5 s, its buttons 15 s (tested); hardware media keys reach the playing video through Chromium's media key handling, which Orbit turns on: only a person can check them (no media keys under Xvfb)"}, - "MD-06": {"built": "yes", "note": "drag or two-finger hover-scroll moves it and it glides to the corner the release or fling heads for; corner drag resizes keeping the shape, at least 200 px wide (tall videos too) and at most half the screen width and the screen's height, with rubber-band resistance; Cmd/Ctrl+scroll or pinch zooms (crops) inside the window; corner and size remembered. While an Orbit window shows an alert or dialog (the engine disables the window meanwhile, macOS shows a sheet, and Orbit's own file pickers say so), the player keeps clear of where it shows, up to 900 x 620 centered on the window (from its top on macOS): it glides to the nearest clear corner at its size, or goes out of sight, still playing, when no corner is clear, and comes back when the dialog closes unless it was moved meanwhile; its remembered place doesn't change (tested with an alert). A web page's own file picker on Linux isn't seen: the engine doesn't disable the window for it"}, + "MD-03": {"built": "yes", "feel": true, "note": "frameless, always-on-top window, 320x180 for 16:9 (keeps the video's shape), bottom-right 16 px inside the work area; rounded corners (a window shape where the system has none) and shadow; video only until hovered, then controls over a dark gradient; the same player on every platform"}, + "MD-04": {"built": "yes", "feel": true, "note": "on hover: play/pause, +/-15 s, progress with elapsed and total time (seekable; LIVE badge for streams), Back to tab (selects it in its window and Space), x (keeps playing in the sidebar), ... menu (playback speed, turn off today / always for the site), previous/next for Media Session pages; double-click goes back to the tab"}, + "MD-05": {"built": "yes", "feel": true, "note": "with the player focused (clicked) Space plays and pauses and the arrow keys seek 5 s, its buttons 15 s (tested); hardware media keys reach the playing video through Chromium's media key handling, which Orbit turns on: only a person can check them (no media keys under Xvfb)"}, + "MD-06": {"built": "yes", "feel": true, "note": "drag or two-finger hover-scroll moves it and it glides to the corner the release or fling heads for; corner drag resizes keeping the shape, at least 200 px wide (tall videos too) and at most half the screen width and the screen's height, with rubber-band resistance; Cmd/Ctrl+scroll or pinch zooms (crops) inside the window; corner and size remembered. While an Orbit window shows an alert or dialog (the engine disables the window meanwhile, macOS shows a sheet, and Orbit's own file pickers say so), the player keeps clear of where it shows, up to 900 x 620 centered on the window (from its top on macOS): it glides to the nearest clear corner at its size, or goes out of sight, still playing, when no corner is clear, and comes back when the dialog closes unless it was moved meanwhile; its remembered place doesn't change (tested with an alert). A web page's own file picker on Linux isn't seen: the engine doesn't disable the window for it"}, "MD-07": {"built": "yes", "note": "returning (select, double-click, Back to tab) ends it with the video playing in the page; x closes it and the media keeps playing in the sidebar controls; closing or unloading the tab stops media and player; navigating the tab or the video ending closes it, without replay; a picture-in-picture the engine closes because another one opened (in another pane, tab or window, by a page or by Orbit) isn't Back to tab: nothing is selected and no window comes forward (D75)"}, - "MD-08": {"built": "yes", "note": "site controls panel (NV-16) Picture in Picture switch, on by default: off is Always turn off for this site, on undoes it and Turn off for today, which it shows too; player ... menu Turn Off for Today / Always Turn Off for This Site (also in the tray controller menu); global switch Settings > Advanced (ST-10) and the Picture in Picture action; turning off keeps playback (sidebar controls); muted tabs never trigger"}, - "MD-09": {"built": "yes", "note": "controller above the bottom bar: the Media Session's artwork (loaded with the tab's session, kept small) or the favicon, Media Session title/artist or tab title (marquee), play/pause, next, previous where supported, x pauses and hides; click jumps to the tab (switching Space); quiet audio counts; hidden while its tab is selected; media in frames (embedded players, cross-site too) has the same controls"}, - "MD-10": {"built": "yes", "note": "one controller per background tab, newest first, appears and disappears by itself, works across windows; a video's controller has Picture in Picture (Back to PiP): the player grows out of the controller while the controller gives way"}, + "MD-08": {"built": "yes", "feel": true, "note": "site controls panel (NV-16) Picture in Picture switch, on by default: off is Always turn off for this site, on undoes it and Turn off for today, which it shows too; player ... menu Turn Off for Today / Always Turn Off for This Site (also in the tray controller menu); global switch Settings > Advanced (ST-10) and the Picture in Picture action; turning off keeps playback (sidebar controls); muted tabs never trigger"}, + "MD-09": {"built": "yes", "feel": true, "note": "controller above the bottom bar: the Media Session's artwork (loaded with the tab's session, kept small) or the favicon, Media Session title/artist or tab title (marquee), play/pause, next, previous where supported, x pauses and hides; click jumps to the tab (switching Space); quiet audio counts; hidden while its tab is selected; media in frames (embedded players, cross-site too) has the same controls"}, + "MD-10": {"built": "yes", "feel": true, "note": "one controller per background tab, newest first, appears and disappears by itself, works across windows; a video's controller has Picture in Picture (Back to PiP): the player grows out of the controller while the controller gives way"}, "MD-11": {"built": "partial", "note": "pulsing camera/microphone indicator on tab rows (tracks the page shows); a distinct blue screen-share indicator, lit while either source says a share is live (D69): main's watcher, put in the asking frame (any frame) when Orbit's screen chooser grants, or the page preload's count of the main frame's live shared tracks (covers macOS 15's system picker); only chooser grants keep a tab awake and unarchived; known limit: a page that wants to hide its share can (e.g. clone a track with a same-origin frame's built-ins and stop the one Orbit follows), the OS's own indicator is the backstop; a frame's share the macOS 15 picker grants doesn't show; a capture that fails after the chooser granted it shows until the page leaves; Peek and Mini Window shares have no row; per-pane Mute/Unmute in the split row's menu (split lane's menu, tested here: only that pane goes silent); no switcher indicators (TB-05, finish-tabs lane; TabMedia.mic/camera/screen are ready)"}, - "MD-12": {"built": "yes", "note": "playing (muted too), PiP and camera/mic tabs never sleep, auto-archive, Clear or Archive Below; closing stops media, PiP and controller; isAnyMediaPlaying() is ready for the updates lane"}, + "MD-12": {"built": "yes", "feel": true, "note": "playing (muted too), PiP and camera/mic tabs never sleep, auto-archive, Clear or Archive Below; closing stops media, PiP and controller; isAnyMediaPlaying() is ready for the updates lane"}, "MD-13": {"built": "partial", "note": "leaving a call tab floats the largest participant video (never the self view) in Orbit's player with microphone/camera toggles and Back to call tab; moves and resizes like any video (a pinch zooms, as for any video). Not built: leave call (the site's Media Session hang-up handler lives in the page's own script world, which Orbit reaches only through a page patch, a lead decision under D40), a chat toggle (no web API exposes a site's chat), and the site's own document picture-in-picture view (also only through the page's Media Session)"}, "MD-14": {"built": "partial", "note": "a background call tab (or a closed call window) gets a sidebar call controller: microphone and camera toggles (track.enabled on the tracks the page shows) and jump back. Not built: a call is seen only when its page shows its own camera (a self view); an audio-only call isn't, because the browser process can't see what a page captures and watching getUserMedia takes a page patch (a lead decision under D40)"}, "MD-15": {"built": "not started", "note": "not possible as specified: Orbit's screen chooser shares a screen or a window, never a single tab, and the engine can't change what a running share captures (switching the shared tab is the Chrome browser's own feature, not in Electron); swapping the call page's video track would take a page patch, a lead decision under D40"}, @@ -232,37 +232,37 @@ "KS-15": {"built": "yes", "note": "macOS: the menu bar keeps the system’s items and keys: Hide Orbit ⌘H, Hide Others ⌥⌘H and Quit ⌘Q in the Orbit menu, the Window menu (Minimize ⌘M, ⌘` between Orbit windows) and the Help menu with its search (⌘?). No action can take these keys, nor Alt+F4, Alt+Tab or Alt+F on Windows and Linux, and Settings → Shortcuts says what keeps them (KS-03). Windows and Linux: Alt+F4 closes the window and, on the last one, quits; F1 opens Help (OB-13) and can’t be changed there. An Exit item comes with the Windows/Linux app menu (SB-26, not built). The menu template and keymap are unit-tested; not run on a Mac"}, "KS-20": {"built": "partial", "note": "private window: the same keys, but the actions that need Spaces, pinned tabs, Favorites, folders or the Archive (⌘D, ⌃1–9 / Alt+1–9, ⌘⌥← / ⌘⌥→, ⌘⇧K Clear, Favorite, New Folder, View Archive, every Space action) do nothing there, from a key, a menu or the command bar, and a toast says “Not available in a private window”; its sidebar has no Clear button; tab keys work. Mini Window: ⌘W closes it, ⌘O / ⌘⌥O move or offer to move its page (KS-10), ⌘L edits its address with the keyboard in the field and Esc gives the keyboard back to its page, ⌘R, zoom and print act on its page, sidebar and Space keys do nothing; ⌘Y / Ctrl+H History, the Library and Help → Keyboard Shortcuts open in the browser window behind it. Full screen: every key works with the sidebar hidden; ⌘1–9 and ⌘⌥↑ / ⌘⌥↓ switch tabs without bringing it out. Not built: ⌘F in a Mini Window (it has no find bar, Mini Window lane), the blank window (TB-17), extension keys in private windows (EX-06, EX-10)"}, "SB-10": {"built": "partial", "note": "media tray and call controls (media lane), above the bottom bar, pushing the list up; download progress and the update banner belong to other lanes"}, - "PK-01": {"built": "yes", "note": "floating card over the source tab; one Peek per window; links inside navigate it"}, - "PK-02": {"built": "yes", "note": "other-site (eTLD+1) link clicks, Enter on a focused link, and target=_blank in pinned tabs/Favorites; same-site navigates. Only a navigation to the clicked link counts, so form posts, script navigations and auto-posted sign-ins stay in the tab"}, - "PK-03": {"built": "yes", "note": "Shift-click in any tab; link menu 'Open Link in Peek'"}, - "PK-04": {"built": "yes", "note": "meeting links open a tab; routing rules (settings.linkRules) win; rules UI is the settings lane's"}, - "PK-05": {"built": "yes", "note": "inset card, shadow, dimmed page area, Close / Open as Tab / Split buttons with shortcut tooltips; no toolbar (AP-07 not built)"}, + "PK-01": {"built": "yes", "feel": true, "note": "floating card over the source tab; one Peek per window; links inside navigate it"}, + "PK-02": {"built": "yes", "feel": true, "note": "other-site (eTLD+1) link clicks, Enter on a focused link, and target=_blank in pinned tabs/Favorites; same-site navigates. Only a navigation to the clicked link counts, so form posts, script navigations and auto-posted sign-ins stay in the tab"}, + "PK-03": {"built": "yes", "feel": true, "note": "Shift-click in any tab; link menu 'Open Link in Peek'"}, + "PK-04": {"built": "yes", "feel": true, "note": "meeting links open a tab; routing rules (settings.linkRules) win; rules UI is the settings lane's"}, + "PK-05": {"built": "yes", "feel": true, "note": "inset card, shadow, dimmed page area, Close / Open as Tab / Split buttons with shortcut tooltips; no toolbar (AP-07 not built)"}, "PK-06": {"built": "yes", "note": "spring grow from the link, 18% dim (20% dark), 160 ms shrink to 0.96 and fade, reduced motion; drag the top handle or swipe down beside the card: follows the input, closes past 25% or 500 px/s, else springs back"}, - "PK-07": {"built": "yes", "note": "⌘O / button: card morphs to the frame, live page becomes a selected Today tab (routing rules apply)"}, - "PK-08": {"built": "yes", "note": "the card slides to the right half, then split.addTab makes the source and the live Peek page one split in the source's place (a pinned source gives a pinned split), the page focused; from a full split the Peek stays open and the SV-25 toast shows"}, - "PK-09": {"built": "yes", "note": "Esc (after fullscreen), ⌘W, ×, click outside; archived as from 'peek'"}, - "PK-10": {"built": "yes", "note": "⌘Z right after closing, or ⌘⇧T, reopens on the same page with history"}, + "PK-07": {"built": "yes", "feel": true, "note": "⌘O / button: card morphs to the frame, live page becomes a selected Today tab (routing rules apply)"}, + "PK-08": {"built": "yes", "feel": true, "note": "the card slides to the right half, then split.addTab makes the source and the live Peek page one split in the source's place (a pinned source gives a pinned split), the page focused; from a full split the Peek stays open and the SV-25 toast shows"}, + "PK-09": {"built": "yes", "feel": true, "note": "Esc (after fullscreen), ⌘W, ×, click outside; archived as from 'peek'"}, + "PK-10": {"built": "yes", "feel": true, "note": "⌘Z right after closing, or ⌘⇧T, reopens on the same page with history"}, "PK-11": {"built": "partial", "note": "links, forms, back/forward, reload, zoom, copy URL and copy URL as Markdown, print, devtools and find in page act on the Peek: ⌘F, ⌘G and F3 bring the find bar over the Peek card's top-right corner, searching the Peek's page with its own search, and Esc gives the Peek the keyboard back; the tab under it keeps its own search; the sidebar address shows the Peek host; ⌘L makes it a tab and edits it; downloads go to the normal list (DL-16). Not built: extensions in Peek pages come with EX-11"}, - "PK-12": {"built": "yes", "note": "Settings → Links has both toggles (\"Open links from pinned tabs and Favorites in Peek\", \"Shift-click opens links in Peek\"), on by default on every platform; off, a pinned tab's or Favorite's other-site link opens as a Today tab and ⇧-click opens a new window. Tests: settings.spec ST-09/MW-11 (toggles, defaults), peek.spec PK-12, nav-links.spec NV-08 ⇧-click, link-routing unit PK-12"}, - "PK-13": {"built": "yes", "note": "no sidebar row; sidebar stays usable; selecting another tab, Space or window dismisses (reopenable)"}, - "PK-15": {"built": "yes", "note": "no hover-to-Peek trigger: hovering links or pinned rows never opens a Peek (hover cards are TB-21)"}, - "PK-16": {"built": "yes", "note": "Mini Window links never Peek; target=_blank stays in the Mini Window"}, + "PK-12": {"built": "yes", "feel": true, "note": "Settings → Links has both toggles (\"Open links from pinned tabs and Favorites in Peek\", \"Shift-click opens links in Peek\"), on by default on every platform; off, a pinned tab's or Favorite's other-site link opens as a Today tab and ⇧-click opens a new window. Tests: settings.spec ST-09/MW-11 (toggles, defaults), peek.spec PK-12, nav-links.spec NV-08 ⇧-click, link-routing unit PK-12"}, + "PK-13": {"built": "yes", "feel": true, "note": "no sidebar row; sidebar stays usable; selecting another tab, Space or window dismisses (reopenable)"}, + "PK-15": {"built": "yes", "feel": true, "note": "no hover-to-Peek trigger: hovering links or pinned rows never opens a Peek (hover cards are TB-21)"}, + "PK-16": {"built": "yes", "feel": true, "note": "Mini Window links never Peek; target=_blank stays in the Mini Window"}, "FL-19": {"built": "yes", "note": "open: a spring tuned to FL-19's 280–320 ms (response 0.275 s, ratio 0.92; the standard token FL-19 names takes longer over a card's travel, D70), within 2 px in ~320 ms with no overshoot, 18% dim; expand meets the frame in ~250 ms, then the row enters; split slides to the right half; close 160 ms to 0.96 + fade, the page behind takes clicks again 173–194 ms after Escape (best of up to three rounds, at the edge of 200 ms); drag/swipe dismiss; ⌘Z/⌘⇧T replay the open"}, "FL-24": {"built": "yes", "note": "Library opens with a 220 ms fade and 0.98→1 scale (opacity only with reduced motion) and fades out; sections cross-fade in 90 ms; search answers within 50 ms; restored rows fade and the rows below rise; long lists virtualised; a previewed entry opens and closes with the Peek's motion (FL-19)"}, - "MW-01": {"built": "yes", "note": "separate small window, one floating tab, main window untouched"}, - "MW-02": {"built": "yes", "note": "open-url / second-instance / argv links open a Mini Window; same URL resurfaces it; routing rules first"}, - "MW-03": {"built": "yes", "note": "slim top bar: controls, favicon+host (click/⌘L opens the compact command bar on the page's address, CB-23), Open in…, profile pill; inset page card; neutral theme. ⌘F opens a find field in the top bar beside the host (NV-05): finds as you type, n/m count, No matches, Enter/⌘G/F3 next and ⇧ for previous, buttons; Esc closes it, keeps the match selected and gives the page the keyboard; keys typed before the field has the keyboard are not held (as they are for the main window's bar)"}, + "MW-01": {"built": "yes", "feel": true, "note": "separate small window, one floating tab, main window untouched"}, + "MW-02": {"built": "yes", "feel": true, "note": "open-url / second-instance / argv links open a Mini Window; same URL resurfaces it; routing rules first"}, + "MW-03": {"built": "yes", "feel": true, "note": "slim top bar: controls, favicon+host (click/⌘L opens the compact command bar on the page's address, CB-23), Open in…, profile pill; inset page card; neutral theme. ⌘F opens a find field in the top bar beside the host (NV-05): finds as you type, n/m count, No matches, Enter/⌘G/F3 next and ⇧ for previous, buttons; Esc closes it, keeps the match selected and gives the page the keyboard; keys typed before the field has the keyboard are not held (as they are for the main window's bar)"}, "MW-04": {"built": "yes", "note": "a quick scale-and-fade instead of the OS's animation: the window's UI grows from 92% to full size in 200 ms (the --ease-out curve) in its own layer while the window fades in (macOS, Windows; Linux can't fade a window, so there the UI fades in too). The OS window is not resized, which would lay the page out every frame: the page's native view, loading all along, sits out the grow and joins on the card at full size when it ends. Reduced motion shows it at once. Opens at the last-used size and place, each further window 24 px down and right; on macOS it shows above a full-screen Orbit window, on the current desktop. Only a person can check the desktop and full-screen parts, on a Mac"}, - "MW-05": {"built": "yes", "note": "⌘O to the most recent Space, Open in… / ⌘⌥O (⌘⇧O) native Space menu; live page, no reload"}, - "MW-06": {"built": "yes", "note": "⌘⌥N opens a pre-rendered blank window with a focused lookup bar, the command bar's compact variant (CB-23): history and typed-text rows, inline completion, site search, engine suggestions; the choice loads here; Esc empties it, Esc on the empty bar or ⌘W closes the window. System-wide on macOS only (outside e2e); on Windows/Linux Ctrl+Alt+N works inside Orbit, so other apps (and AltGr+N) keep the key"}, - "MW-07": {"built": "yes", "note": "⌘⌥-click a link, link menu Open Link in Mini Window, or ⌘⌥-click a sidebar row: Today rows move live, pinned/Favorite rows open a copy"}, - "MW-08": {"built": "yes", "note": "any number; Show/Hide All action and macOS dock menu"}, - "MW-09": {"built": "yes", "note": "close and idle auto-close (settings.miniWindowArchiveAfterMs, 6 h, 0 = never; not while audible) archive with from 'mini'"}, - "MW-10": {"built": "yes", "note": "profile pill/menu reopens the page in another profile; remembered per site"}, - "MW-11": {"built": "yes", "note": "settings.miniWindowForExternalLinks off: tabs in the most recent or chosen Space (externalLinksSpace)"}, + "MW-05": {"built": "yes", "feel": true, "note": "⌘O to the most recent Space, Open in… / ⌘⌥O (⌘⇧O) native Space menu; live page, no reload"}, + "MW-06": {"built": "yes", "feel": true, "note": "⌘⌥N opens a pre-rendered blank window with a focused lookup bar, the command bar's compact variant (CB-23): history and typed-text rows, inline completion, site search, engine suggestions; the choice loads here; Esc empties it, Esc on the empty bar or ⌘W closes the window. System-wide on macOS only (outside e2e); on Windows/Linux Ctrl+Alt+N works inside Orbit, so other apps (and AltGr+N) keep the key"}, + "MW-07": {"built": "yes", "feel": true, "note": "⌘⌥-click a link, link menu Open Link in Mini Window, or ⌘⌥-click a sidebar row: Today rows move live, pinned/Favorite rows open a copy"}, + "MW-08": {"built": "yes", "feel": true, "note": "any number; Show/Hide All action and macOS dock menu"}, + "MW-09": {"built": "yes", "feel": true, "note": "close and idle auto-close (settings.miniWindowArchiveAfterMs, 6 h, 0 = never; not while audible) archive with from 'mini'"}, + "MW-10": {"built": "yes", "feel": true, "note": "profile pill/menu reopens the page in another profile; remembered per site"}, + "MW-11": {"built": "yes", "feel": true, "note": "settings.miniWindowForExternalLinks off: tabs in the most recent or chosen Space (externalLinksSpace)"}, "MW-12": {"built": "partial", "note": "a video's full screen happens in the Mini Window: the window fills the screen with the page, which stays there, and the card comes back on exit (tested); copy URL, devtools, downloads through the profile's session. Not built: extensions (Orbit has no extension support yet, EX-11), captures saved to a canvas (no Canvases yet) and sharing (Orbit has no share feature)"}, - "MW-13": {"built": "yes", "note": "off by default; Settings > General > Mini Window turns it on: Orbit's mark as a template glyph in the macOS menu bar, the app's icon in the Windows and Linux tray (the drawn mark where a build has no icon file); a click opens a blank Mini Window with its lookup bar focused (macOS: right-click for its menu); its menu has New Mini Window and Show/Hide All Mini Windows, New Mini Window first for Linux trays that show only the menu (tested through the button's click and menu). GNOME shows tray icons only with an AppIndicator extension; on Windows and Linux Orbit still quits with its last window, button or not"}, - "MW-14": {"built": "yes", "note": "all platforms with Ctrl+Alt+N / Ctrl+O / Ctrl+Alt+O, in-app outside macOS (D23), with the tray button (MW-13) as the way in from other apps; New Mini Window and Show/Hide All in the macOS dock menu, the Windows jump list, Linux launcher actions and the tray menu. Launcher actions: a packaged Linux folder build writes ~/.local/share/applications/.desktop at start-up (under $XDG_DATA_HOME when that is set; is Electron's desktop name), with no opt-out for now, and keeps it pointing at where Orbit runs from. It never touches an entry a package installed, one the user wrote, or one it can't read (checked on a packaged build: both actions reach the running Orbit). Only a person can check the Windows jump list"}, + "MW-13": {"built": "yes", "feel": true, "note": "off by default; Settings > General > Mini Window turns it on: Orbit's mark as a template glyph in the macOS menu bar, the app's icon in the Windows and Linux tray (the drawn mark where a build has no icon file); a click opens a blank Mini Window with its lookup bar focused (macOS: right-click for its menu); its menu has New Mini Window and Show/Hide All Mini Windows, New Mini Window first for Linux trays that show only the menu (tested through the button's click and menu). GNOME shows tray icons only with an AppIndicator extension; on Windows and Linux Orbit still quits with its last window, button or not"}, + "MW-14": {"built": "yes", "feel": true, "note": "all platforms with Ctrl+Alt+N / Ctrl+O / Ctrl+Alt+O, in-app outside macOS (D23), with the tray button (MW-13) as the way in from other apps; New Mini Window and Show/Hide All in the macOS dock menu, the Windows jump list, Linux launcher actions and the tray menu. Launcher actions: a packaged Linux folder build writes ~/.local/share/applications/.desktop at start-up (under $XDG_DATA_HOME when that is set; is Electron's desktop name), with no opt-out for now, and keeps it pointing at where Orbit runs from. It never touches an entry a package installed, one the user wrote, or one it can't read (checked on a packaged build: both actions reach the running Orbit). Only a person can check the Windows jump list"}, "MW-15": {"built": "yes", "note": "warm spare window takes keys at once, page loads before the window shows; opens with MW-04's 200 ms grow and fade; Open in… shrinks to 92% and fades (140 ms, macOS/Windows) as the row enters Today"}, "DL-09": {"built": "partial", "note": "Library container (archive lane): section list on the left with the Archive, History (AR-10) and Backups (AR-11) sections; other lanes add theirs through the registry. Reopens to the last section; Esc/⌘W close and the previous tab comes back. Its view is made once Orbit is idle after launch, so even the first open is quick (about 70 ms at 10,000 entries; later opens 10–50 ms). Downloads lane: the Media and Downloads sections (just before Downloads, Media); right after a download, a closed Library opens on Downloads next time; their items open in a Peek over the Library (⌘/Ctrl-click: a tab). Missing, all owned by other lanes: Canvases & Notes (notes lane), Spaces (finish-tabs, SP-13) and Site customizations (tweaks, BO-15)"}, "DL-01": {"built": "yes", "note": "per-profile sessions save to the profile's folder (OS Downloads), name (1) on collisions, .crdownload until done; a blank tab opened for a download closes; outlives its page; quitting asks while downloads run"}, diff --git a/scripts/capture/record.mjs b/scripts/capture/record.mjs index 4a9f4074..9c26d539 100644 --- a/scripts/capture/record.mjs +++ b/scripts/capture/record.mjs @@ -6,6 +6,10 @@ * * Needs an ffmpeg with x11grab (set ORBIT_FFMPEG to its path) and a display: * xvfb-run -a -s "-screen 0 1440x900x24" node scripts/capture/record.mjs [--out clips/.mp4] [--shots dir] + * [--fps 30] [--port 3380] + * + * --fps sets the recording's frame rate (60 to judge motion frame by frame). --port is where the fixture + * server starts looking for a free port, so a capture can stay inside its own range. * * Scenarios live in scripts/capture/scenarios/.mjs and export * `default async function (t)`; see helpers in `makeToolkit` below. @@ -25,13 +29,17 @@ const opt = (name, fallback) => { return i >= 0 ? argv[i + 1] : fallback } if (!scenario) { - console.error('usage: record.mjs [--out file.mp4] [--shots dir] [--size 1440x900]') + console.error( + 'usage: record.mjs [--out file.mp4] [--shots dir] [--size 1440x900] [--fps 30] [--port 3380]', + ) process.exit(2) } const scenarioName = basename(scenario, '.mjs') const OUT = resolve(opt('out', `clips/${scenarioName}.mp4`)) const SHOTS = opt('shots') ? resolve(opt('shots')) : undefined const [W, H] = opt('size', '1440x900').split('x').map(Number) +const FPS = opt('fps', '30') +const PORT = Number(opt('port', '3380')) const FFMPEG = process.env.ORBIT_FFMPEG ?? 'ffmpeg' const DISPLAY = process.env.DISPLAY if (!DISPLAY) throw new Error('No DISPLAY: run under xvfb-run') @@ -85,7 +93,7 @@ main{padding:40px;max-width:1100px}h1{font-size:40px;margin:8px 0 6px;letter-spa
${esc(name)}

${esc(name)}

A local demo page for Orbit screen captures, served from 127.0.0.1.

${cards}
`) }) - let port = 3380 + let port = PORT await new Promise((ok, fail) => { server.on('error', (err) => err.code === 'EADDRINUSE' && port < 3399 ? server.listen(++port, '127.0.0.1') : fail(err), @@ -108,7 +116,7 @@ function startRecorder(file) { '-draw_mouse', '1', '-framerate', - '30', + FPS, '-video_size', `${W}x${H}`, '-i', diff --git a/scripts/capture/scenarios/cb.mjs b/scripts/capture/scenarios/cb.mjs new file mode 100644 index 00000000..f9ada82a --- /dev/null +++ b/scripts/capture/scenarios/cb.mjs @@ -0,0 +1,75 @@ +/** + * The command bar (CB-02, CB-03, CB-09, CB-10, CB-13, CB-22; FL-16, FL-17), about 12 s, for the "feels right" + * sign-off (D48). ⌘T opens the bar; typing narrows the results as each key lands; ↓ moves the + * highlight; Enter switches to an open tab; ⌘L edits the current page's address; Escape closes; + * an action runs by name. + * + * Each step prints a `[mark] ` line on stderr: its time from the start of the recording, + * so frames around it can be cut from the clip. + */ + +let t0 = 0 +const mark = (name) => console.error(`[mark] ${name} ${Math.round(performance.now() - t0)}`) + +/** Unrecorded: a Space with a few open tabs, each visited once so it has its title and icon. */ +export async function setup(t) { + const s = await t.state() + const win = Object.values(s.windows).find((w) => w.kind === 'main') + const tabs = [] + for (const name of ['Recipes', 'Reading list', 'Budget', 'Trip planning', 'Team wiki']) { + tabs.push(await t.run('tabs.create', { url: t.fx.url(name), spaceId: win.spaceId, activate: false })) + } + for (const tabId of tabs) { + await t.run('tabs.activate', { tabId }) + await t.sleep(450) + } +} + +export default async function (t) { + t0 = performance.now() + await t.sleep(600) + + // CB-02, CB-22, FL-16: ⌘T shows the bar in the first frame, fading and scaling in. + mark('open') + await t.key('Mod+T') + let overlay = await t.overlay() + await t.sleep(700) + + // CB-22, FL-17: each key is echoed in its frame, and the results follow; CB-10: ↓ and ↑ move the highlight. + mark('type') + await t.type(overlay, 'rec', 110) + await t.sleep(700) + mark('down') + await overlay.keyboard.press('ArrowDown') + await t.sleep(500) + await overlay.keyboard.press('ArrowUp') + await t.sleep(600) + + // CB-09: Enter on an open tab switches to it; the bar hides at once. + mark('enter') + await overlay.keyboard.press('Enter') + await t.sleep(1400) + + // CB-03: ⌘L opens the bar on the current page's address, selected. + mark('cmd-l') + await t.key('Mod+L') + overlay = await t.overlay() + await t.sleep(1200) + + // FL-16: Escape fades and shrinks it out. + mark('escape') + await overlay.keyboard.press('Escape') + await t.sleep(1200) + + // CB-13: an action by name. + mark('action') + await t.key('Mod+T') + overlay = await t.overlay() + await t.sleep(400) + await t.type(overlay, 'new spa', 110) + await t.sleep(1100) + mark('action-escape') + await overlay.keyboard.press('Escape') + await t.sleep(1200) + mark('end') +} diff --git a/scripts/capture/scenarios/md.mjs b/scripts/capture/scenarios/md.mjs new file mode 100644 index 00000000..6d1cbd0d --- /dev/null +++ b/scripts/capture/scenarios/md.mjs @@ -0,0 +1,132 @@ +/** + * Picture-in-picture (MD-01, MD-03, MD-04, MD-07, MD-16; FL-32), about 14 s, for the "feels right" + * sign-off (D48). Leaving a tab whose video plays moves the video into the floating player at the + * bottom-right; the pointer over it shows its controls; selecting the tab again puts the video back in + * the page, still playing; "Back to tab" plays the reverse motion. + * + * Each step prints a `[mark] ` line on stderr: its time from the start of the recording, + * so frames around it can be cut from the clip. + */ +import { execFile } from 'node:child_process' + +let t0 = 0 +const mark = (name) => console.error(`[mark] ${name} ${Math.round(performance.now() - t0)}`) + +/** A playing video drawn from a canvas (a colour that changes every 40 ms), with a quiet tone: no file, no network. */ +const VIDEO = `(() => { + const box = document.createElement('div') + box.style.cssText = 'margin:0 0 28px' + box.innerHTML = '' + document.querySelector('.lead').after(box) + const c = document.getElementById('c'), g = c.getContext('2d'); let n = 0 + setInterval(() => { + g.fillStyle = 'hsl(' + (n * 3 % 360) + ',65%,52%)'; g.fillRect(0, 0, 640, 360) + g.fillStyle = '#fff'; g.font = 'bold 64px system-ui'; g.fillText(String(n++), 40, 300) + }, 40) + const stream = c.captureStream(25) + const ac = new AudioContext(), osc = ac.createOscillator(), gain = ac.createGain(); gain.gain.value = 0.03 + const dest = ac.createMediaStreamDestination(); osc.connect(gain).connect(dest); osc.start() + stream.addTrack(dest.stream.getAudioTracks()[0]) + const v = document.getElementById('v'); v.srcObject = stream + return v.play().then(() => 'playing') +})()` + +/** Moves the on-screen pointer (display coordinates) in eased steps. */ +let at = null +async function pointerTo(x, y, steps = 16) { + const from = at ?? { x, y } + for (let i = 1; i <= steps; i++) { + const k = 0.5 - 0.5 * Math.cos((Math.PI * i) / steps) + await new Promise((r) => + execFile( + 'xdotool', + ['mousemove', String(Math.round(from.x + (x - from.x) * k)), String(Math.round(from.y + (y - from.y) * k))], + () => r(), + ), + ) + } + at = { x, y } +} + +/** The floating player's window bounds once it shows a video and has settled, or null while it isn't showing. */ +const player = (t) => + t.app.evaluate(() => { + const info = globalThis.__orbitMedia.manager.player.info() + return info.visible && info.holdsView ? info.bounds : null + }) + +async function waitFor(t, test, ms = 8000) { + const end = Date.now() + ms + while (Date.now() < end) { + const v = await test() + if (v) return v + await t.sleep(40) + } + throw new Error('timed out') +} + +let video = '' +let notes = '' + +/** + * Unrecorded: a tab whose video plays, and a second tab to leave it for. The main window leaves the + * screen's bottom-right corner free: a display with no window manager keeps no window on top, so the + * player shows only where no other window covers it. + */ +export async function setup(t) { + const s = await t.state() + const win = Object.values(s.windows).find((w) => w.kind === 'main') + await t.app.evaluate( + (_e, id) => globalThis.__orbit.ctx.windows.get(id)?.setBounds({ x: 0, y: 0, width: 1000, height: 640 }), + win.id, + ) + await t.sleep(400) + notes = await t.run('tabs.create', { url: t.fx.url('Notes'), spaceId: win.spaceId, activate: false }) + video = await t.run('tabs.create', { url: t.fx.url('Film club'), spaceId: win.spaceId, activate: false }) + await t.run('tabs.activate', { tabId: notes }) + await t.sleep(450) + await t.run('tabs.activate', { tabId: video }) + await t.sleep(900) + // As if the person pressed play: the script runs with a user gesture, so the sound may start. + const state = await t.app.evaluate( + (_e, [id, code]) => globalThis.__orbit.ctx.tabs.get(id).webContents.executeJavaScript(code, true), + [video, VIDEO], + ) + if (state !== 'playing') throw new Error(`the video didn't start: ${state}`) + await t.sleep(1200) +} + +export default async function (t) { + t0 = performance.now() + await t.sleep(600) + + // MD-01, MD-16: leaving the tab moves its playing video into the floating player, bottom-right. + mark('leave') + await t.run('tabs.activate', { tabId: notes }) + const box = await waitFor(t, () => player(t)) + await t.sleep(1800) + + // MD-04: the pointer over the player shows its controls. + mark('hover') + await pointerTo(box.x + box.width / 2, box.y + box.height / 2, 20) + await t.sleep(1400) + await pointerTo(box.x - 260, box.y - 220, 16) + await t.sleep(600) + + // MD-01: selecting the tab again puts the video back in the page, still playing, without a blank frame. + mark('back') + await t.run('tabs.activate', { tabId: video }) + await waitFor(t, async () => !(await player(t))) + await t.sleep(1500) + + // MD-07, MD-16: "Back to tab" from the player plays the reverse motion toward the page. + mark('leave-2') + await t.run('tabs.activate', { tabId: notes }) + await waitFor(t, () => player(t)) + await t.sleep(1500) + mark('pip-back') + await t.run('media.pipBack', {}) + await waitFor(t, async () => !(await player(t))) + await t.sleep(1500) + mark('end') +} diff --git a/scripts/capture/scenarios/mw.mjs b/scripts/capture/scenarios/mw.mjs new file mode 100644 index 00000000..c61eb03a --- /dev/null +++ b/scripts/capture/scenarios/mw.mjs @@ -0,0 +1,115 @@ +/** + * The Mini Window (MW-02, MW-04 to MW-06, MW-09, MW-15), about 16 s, for the "feels right" sign-off + * (D48). A link from another app opens a Mini Window with a quick scale-and-fade; "Open in…" moves its + * page into the main window, where its row slides into the sidebar; ⌘⌥N opens a blank one whose lookup + * bar takes the keys typed at once; closing it fades it out. + * + * Each step prints a `[mark] ` line on stderr: its time from the start of the recording, + * so frames around it can be cut from the clip. + */ + +let t0 = 0 +const mark = (name) => console.error(`[mark] ${name} ${Math.round(performance.now() - t0)}`) + +/** The Mini Windows open now, with whether each one shows. */ +const minis = (t) => + t.app.evaluate(({ BrowserWindow }) => { + const ctx = globalThis.__orbit.ctx + return Object.values(ctx.store.state.windows) + .filter((w) => w.kind === 'mini') + .map((w) => { + const win = BrowserWindow.getAllWindows().find( + (b) => !b.isDestroyed() && ctx.windows.isUi(b.webContents) && ctx.windows.windowIdFor(b.webContents) === w.id, + ) + return { id: w.id, visible: !!win?.isVisible() } + }) + }) + +async function waitFor(t, test, ms = 8000) { + const end = Date.now() + ms + while (Date.now() < end) { + const v = await test() + if (v) return v + await t.sleep(40) + } + throw new Error('timed out') +} + +/** A Mini Window's own UI page (its top bar and lookup bar). */ +async function miniUi(t, windowId) { + return waitFor(t, async () => { + for (const p of t.app.windows()) { + if (!p.url().includes('view=mini')) continue + const id = await p.evaluate(() => window.orbit.windowId).catch(() => '') + if (id === windowId) return p + } + return null + }) +} + +/** Runs a command from a Mini Window's UI, as its buttons do, without waiting on it (it may close the window). */ +async function fromMini(t, windowId, name, args) { + const ui = await miniUi(t, windowId) + await ui.evaluate(([n, a]) => void window.orbit.invoke(n, a), [name, args]).catch(() => {}) +} + +let spaceId = '' + +/** Unrecorded: two tabs in the main window, and a blank Mini Window kept warm as Orbit does after launch. */ +export async function setup(t) { + const s = await t.state() + const win = Object.values(s.windows).find((w) => w.kind === 'main') + spaceId = win.spaceId + const tabs = [] + for (const name of ['Inbox', 'Calendar']) { + tabs.push(await t.run('tabs.create', { url: t.fx.url(name), spaceId, activate: false })) + } + for (const tabId of tabs) { + await t.run('tabs.activate', { tabId }) + await t.sleep(450) + } + await t.run('mini.prepare', {}) + await t.sleep(1500) +} + +export default async function (t) { + t0 = performance.now() + await t.sleep(600) + + // MW-02, MW-04: a link from another app opens a Mini Window with a quick scale-and-fade. + mark('open-link') + const shared = t.fx.url('Shared doc').replace('127.0.0.1', 'localhost') + await t.app.evaluate(({ app }, u) => app.emit('open-url', { preventDefault() {} }, u), shared) + const [first] = await waitFor(t, async () => { + const list = await minis(t) + return list.length === 1 && list[0].visible ? list : null + }) + await t.sleep(1800) + + // MW-05, MW-15: "Open in…" moves the page into the main window; its row slides into the sidebar. + mark('open-in') + await fromMini(t, first.id, 'mini.moveToSpace', { spaceId }) + await waitFor(t, async () => (await minis(t)).length === 0) + await t.sleep(1500) + + // MW-06, MW-15: ⌘⌥N opens a blank Mini Window; every key typed at once lands in its lookup bar. + mark('blank') + await t.key('Mod+Alt+N') + const [blank] = await waitFor(t, async () => { + const list = await minis(t) + return list.length === 1 ? list : null + }) + const ui = await miniUi(t, blank.id) + await t.type(ui, t.fx.url('Lookup').replace('http://', ''), 45) + await t.sleep(400) + mark('lookup-enter') + await ui.keyboard.press('Enter') + await t.sleep(1800) + + // MW-09, MW-15: closing fades the window out. + mark('close') + await fromMini(t, blank.id, 'mini.control', { op: 'close' }) + await waitFor(t, async () => (await minis(t)).every((m) => !m.visible)) + await t.sleep(1200) + mark('end') +} diff --git a/scripts/capture/scenarios/pk.mjs b/scripts/capture/scenarios/pk.mjs new file mode 100644 index 00000000..3095d488 --- /dev/null +++ b/scripts/capture/scenarios/pk.mjs @@ -0,0 +1,190 @@ +/** + * Peek (PK-02, PK-05 to PK-07, PK-09, PK-10; FL-19), about 25 s, for the "feels right" sign-off (D48). + * A link to another site in a pinned tab opens a Peek that grows out of the link; Escape closes it; + * ⌘Z brings it back; a short two-finger swipe down moves the card with the fingers and lets it spring + * back, and a long one dismisses it; a last Peek expands into a full tab with ⌘O. + * + * Each step prints a `[mark] ` line on stderr: its time from the start of the recording, + * so frames around it can be cut from the clip. + */ +import { execFile } from 'node:child_process' + +const SOURCE = 'Journal' +let t0 = 0 +const mark = (name) => console.error(`[mark] ${name} ${Math.round(performance.now() - t0)}`) + +/** A tab's page view: its bounds in the window's content, and the content's place on the display. */ +const viewOf = (t, tabId) => + t.app.evaluate(({ BrowserWindow }, id) => { + const view = globalThis.__orbit.ctx.tabs.get(id) + const win = BrowserWindow.getAllWindows()[0] + return { bounds: view?.getBounds() ?? null, origin: win.getContentBounds() } + }, tabId) + +/** The overlay view's place in the window's content (it draws the Peek's card). */ +const overlayOrigin = (t) => + t.app.evaluate(({ BrowserWindow }) => { + const win = BrowserWindow.getAllWindows()[0] + const view = win.contentView.children.find((v) => v.webContents?.getURL().includes('view=overlay')) + return { bounds: view?.getBounds() ?? { x: 0, y: 0 }, origin: win.getContentBounds() } + }) + +/** Moves the on-screen pointer (display coordinates) in eased steps. */ +let at = null +async function pointerTo(x, y, steps = 16) { + const from = at ?? { x, y } + for (let i = 1; i <= steps; i++) { + const k = 0.5 - 0.5 * Math.cos((Math.PI * i) / steps) + await new Promise((r) => + execFile( + 'xdotool', + ['mousemove', String(Math.round(from.x + (x - from.x) * k)), String(Math.round(from.y + (y - from.y) * k))], + () => r(), + ), + ) + } + at = { x, y } +} + +/** The Peek showing in the main window, or '' once there is none. */ +async function peekTab(t) { + const s = await t.state() + const win = Object.values(s.windows).find((w) => w.kind === 'main') + return win?.overlay?.kind === 'peek' ? win.overlay.tabId : '' +} + +async function waitFor(t, test, ms = 5000) { + const end = Date.now() + ms + while (Date.now() < end) { + const v = await test() + if (v) return v + await t.sleep(40) + } + throw new Error('timed out') +} + +/** Presses keys in a tab's page through the browser's input handling, where Orbit's shortcuts listen. */ +const press = (t, tabId, accel) => + t.app.evaluate( + (_e, [id, keys, mac]) => { + const contents = globalThis.__orbit.ctx.tabs.get(id)?.webContents + const parts = keys.split('+') + const keyCode = parts.pop() + const modifiers = parts.map((m) => (m === 'Mod' ? (mac ? 'meta' : 'control') : m.toLowerCase())) + contents.focus() + contents.sendInputEvent({ type: 'keyDown', keyCode, modifiers }) + contents.sendInputEvent({ type: 'keyUp', keyCode, modifiers }) + }, + [tabId, accel, process.platform === 'darwin'], + ) + +/** Clicks a point of a tab's page through the browser's input handling. */ +const clickIn = (t, tabId, x, y) => + t.app.evaluate( + (_e, [id, px, py]) => { + const contents = globalThis.__orbit.ctx.tabs.get(id)?.webContents + contents.sendInputEvent({ type: 'mouseMove', x: px, y: py }) + contents.sendInputEvent({ type: 'mouseDown', x: px, y: py, button: 'left', clickCount: 1 }) + contents.sendInputEvent({ type: 'mouseUp', x: px, y: py, button: 'left', clickCount: 1 }) + }, + [tabId, x, y], + ) + +/** The centre of an element of a tab's page, in page coordinates. */ +const centreOf = (t, tabId, selector) => + t.app.evaluate( + (_e, [id, sel]) => + globalThis.__orbit.ctx.tabs + .get(id) + .webContents.executeJavaScript( + `(() => { const r = document.querySelector(${JSON.stringify(sel)}).getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 } })()`, + ), + [tabId, selector], + ) + +let source = '' + +/** Unrecorded: a pinned tab whose page has two links to another site (localhost and 127.0.0.1 are two sites). */ +export async function setup(t) { + const s = await t.state() + const win = Object.values(s.windows).find((w) => w.kind === 'main') + source = await t.run('tabs.create', { url: t.fx.url(SOURCE), container: { kind: 'pinned', spaceId: win.spaceId } }) + await t.run('tabs.activate', { tabId: source }) + await t.sleep(900) + const other = t.fx.url('Field notes').replace('127.0.0.1', 'localhost') + const later = t.fx.url('Travel guide').replace('127.0.0.1', 'localhost') + await t.app.evaluate( + (_e, [id, a, b]) => + globalThis.__orbit.ctx.tabs.get(id).webContents.executeJavaScript(`(() => { + const p = document.createElement('p') + p.style.cssText = 'font-size:20px;margin:0 0 24px' + p.innerHTML = 'Read the field notes on another site, or the travel guide.' + document.querySelector('.lead').after(p) + })()`), + [source, other, later], + ) + await t.sleep(400) +} + +async function clickLink(t, selector) { + const { bounds, origin } = await viewOf(t, source) + const c = await centreOf(t, source, selector) + await pointerTo(origin.x + bounds.x + c.x, origin.y + bounds.y + c.y) + await t.sleep(250) + await clickIn(t, source, c.x, c.y) +} + +export default async function (t) { + t0 = performance.now() + // PK-02, FL-19: the card grows out of the clicked link. + mark('open') + await clickLink(t, '#other') + const peek = await waitFor(t, () => peekTab(t)) + await t.sleep(1600) + + // PK-09, FL-19: Escape shrinks and fades the card out. + mark('escape') + await press(t, peek, 'Escape') + await waitFor(t, async () => !(await peekTab(t))) + await t.sleep(1000) + + // PK-10: ⌘Z brings the Peek back with the open motion. + mark('reopen') + await press(t, source, 'Mod+Z') + await waitFor(t, () => peekTab(t)) + await t.sleep(1500) + + // PK-06, FL-19: a short two-finger swipe down beside the card moves it with the fingers and springs + // back; a long one dismisses it. Fingers moving down scroll content up, so the wheel deltas are negative. + const ov = await t.overlay() + const { bounds, origin } = await overlayOrigin(t) + const card = await ov.getByTestId('peek-card').boundingBox() + const sx = Math.max(8, Math.round(card.x / 2)) + const sy = Math.round(card.y + card.height / 2) + await pointerTo(origin.x + bounds.x + sx, origin.y + bounds.y + sy) + await ov.mouse.move(sx, sy) + const swipe = async (total, steps) => { + for (let i = 0; i < steps; i++) { + await ov.mouse.wheel(0, -total / steps) + await t.sleep(16) + } + } + mark('swipe-short') + await swipe(card.height * 0.12, 8) + await t.sleep(1400) + mark('swipe-long') + await swipe(card.height * 0.4, 14) + await waitFor(t, async () => !(await peekTab(t))) + await t.sleep(1200) + + // PK-07, FL-19: ⌘O grows the card to the page card's edges; it becomes a tab, and its row inserts. + mark('open-2') + await clickLink(t, '#later') + const second = await waitFor(t, () => peekTab(t)) + await t.sleep(1500) + mark('expand') + await press(t, second, 'Mod+O') + await waitFor(t, async () => !(await peekTab(t))) + await t.sleep(1800) + mark('end') +} diff --git a/scripts/capture/scenarios/sv.mjs b/scripts/capture/scenarios/sv.mjs new file mode 100644 index 00000000..842fca71 --- /dev/null +++ b/scripts/capture/scenarios/sv.mjs @@ -0,0 +1,87 @@ +/** + * Split view (SV-02, SV-11, SV-13, SV-17, SV-19, SV-26; FL-18), about 17 s, for the "feels right" sign-off (D48). + * A pane added beside the page grows in with the command bar to fill it; a second pane goes below; + * the divider follows the pointer; the layout flips; panes close and the others grow back. + * + * Each step prints a `[mark] ` line on stderr: its time from the start of the recording, + * so frames around it can be cut from the clip. + */ + +let t0 = 0 +const mark = (name) => console.error(`[mark] ${name} ${Math.round(performance.now() - t0)}`) + +/** Unrecorded: a Space with a few open tabs, each visited once so it has its title and icon. */ +export async function setup(t) { + const s = await t.state() + const win = Object.values(s.windows).find((w) => w.kind === 'main') + const tabs = [] + for (const name of ['Design review', 'Metrics', 'Standup notes', 'Changelog']) { + tabs.push(await t.run('tabs.create', { url: t.fx.url(name), spaceId: win.spaceId, activate: false })) + } + for (const tabId of tabs) { + await t.run('tabs.activate', { tabId }) + await t.sleep(450) + } +} + +/** Fills the new pane: the command bar opens with it, and Enter on a match puts that tab there. */ +async function fill(t, text) { + const overlay = await t.overlay() + await t.sleep(500) + await t.type(overlay, text, 100) + await t.sleep(600) + await overlay.keyboard.press('Enter') +} + +export default async function (t) { + t0 = performance.now() + await t.sleep(600) + + // SV-02, FL-18: a pane added on the right grows in; the page makes room with it, and an open tab fills it. + mark('add-right') + await t.action('split.addRight') + await fill(t, 'metr') + await t.sleep(1500) + + // SV-02, SV-11: a third pane below the focused one. + mark('add-below') + await t.action('split.addBelow') + await fill(t, 'stand') + await t.sleep(1500) + + // SV-13, FL-18: the divider follows the pointer 1:1. + const divider = t.shell.getByTestId('split-divider').first() + await t.point(divider) + const box = await divider.boundingBox() + if (box) { + const x = box.x + box.width / 2 + const y = box.y + box.height / 2 + mark('resize') + await t.shell.mouse.down() + for (let i = 1; i <= 20; i++) { + await t.shell.mouse.move(x - 8 * i, y) + await t.sleep(16) + } + for (let i = 1; i <= 20; i++) { + await t.shell.mouse.move(x - 160 + 8 * i, y) + await t.sleep(16) + } + await t.shell.mouse.up() + } + await t.sleep(1200) + + // SV-11, SV-26, FL-18: flip the orientation; the panes move to their new places. + mark('flip') + await t.action('split.flip') + await t.sleep(1500) + + // SV-17, SV-26, FL-18: close the focused pane; the others grow back into its space. Then SV-19: close the + // whole split. + mark('close-pane') + await t.action('split.close') + await t.sleep(1500) + mark('close-split') + await t.action('split.closeSplit') + await t.sleep(1500) + mark('end') +} From 492c21ed10e5682bd483ac055d62f15b870b0fed Mon Sep 17 00:00:00 2001 From: Siya Date: Sun, 11 Oct 2026 15:05:33 +0000 Subject: [PATCH 2/2] capture: sv names SV-03 and runs split.add; marks say when they count from; md resizes from its own task; --port walks 9 ports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #28's review: step 1 of the sv scenario runs ⌃⇧='s action (split.add) and step 2, Add Split Below, is SV-03. A [mark] time counts from the scenario's start, 0.7 s after the recording's. md's window resize runs from a task of its own, as record.mjs's does. The fixture server looks at most 9 ports above --port. Co-Authored-By: Claude Opus 5.5 --- scripts/capture/record.mjs | 4 ++-- scripts/capture/scenarios/cb.mjs | 4 ++-- scripts/capture/scenarios/md.mjs | 14 ++++++++------ scripts/capture/scenarios/mw.mjs | 4 ++-- scripts/capture/scenarios/pk.mjs | 4 ++-- scripts/capture/scenarios/sv.mjs | 12 ++++++------ 6 files changed, 22 insertions(+), 20 deletions(-) diff --git a/scripts/capture/record.mjs b/scripts/capture/record.mjs index 9c26d539..04f9eb8e 100644 --- a/scripts/capture/record.mjs +++ b/scripts/capture/record.mjs @@ -9,7 +9,7 @@ * [--fps 30] [--port 3380] * * --fps sets the recording's frame rate (60 to judge motion frame by frame). --port is where the fixture - * server starts looking for a free port, so a capture can stay inside its own range. + * server starts looking for a free port, up to 9 above it, so a capture stays inside its own range. * * Scenarios live in scripts/capture/scenarios/.mjs and export * `default async function (t)`; see helpers in `makeToolkit` below. @@ -96,7 +96,7 @@ main{padding:40px;max-width:1100px}h1{font-size:40px;margin:8px 0 6px;letter-spa let port = PORT await new Promise((ok, fail) => { server.on('error', (err) => - err.code === 'EADDRINUSE' && port < 3399 ? server.listen(++port, '127.0.0.1') : fail(err), + err.code === 'EADDRINUSE' && port < Math.min(PORT + 9, 3399) ? server.listen(++port, '127.0.0.1') : fail(err), ) server.listen(port, '127.0.0.1', ok) }) diff --git a/scripts/capture/scenarios/cb.mjs b/scripts/capture/scenarios/cb.mjs index f9ada82a..24c128e9 100644 --- a/scripts/capture/scenarios/cb.mjs +++ b/scripts/capture/scenarios/cb.mjs @@ -4,8 +4,8 @@ * highlight; Enter switches to an open tab; ⌘L edits the current page's address; Escape closes; * an action runs by name. * - * Each step prints a `[mark] ` line on stderr: its time from the start of the recording, - * so frames around it can be cut from the clip. + * Each step prints a `[mark] ` line on stderr: its time since the scenario began. + * record.mjs begins it 0.7 s after the recording starts, so a step shows about 0.7 s later in the clip. */ let t0 = 0 diff --git a/scripts/capture/scenarios/md.mjs b/scripts/capture/scenarios/md.mjs index 6d1cbd0d..a0508a69 100644 --- a/scripts/capture/scenarios/md.mjs +++ b/scripts/capture/scenarios/md.mjs @@ -4,8 +4,8 @@ * bottom-right; the pointer over it shows its controls; selecting the tab again puts the video back in * the page, still playing; "Back to tab" plays the reverse motion. * - * Each step prints a `[mark] ` line on stderr: its time from the start of the recording, - * so frames around it can be cut from the clip. + * Each step prints a `[mark] ` line on stderr: its time since the scenario began. + * record.mjs begins it 0.7 s after the recording starts, so a step shows about 0.7 s later in the clip. */ import { execFile } from 'node:child_process' @@ -76,10 +76,12 @@ let notes = '' export async function setup(t) { const s = await t.state() const win = Object.values(s.windows).find((w) => w.kind === 'main') - await t.app.evaluate( - (_e, id) => globalThis.__orbit.ctx.windows.get(id)?.setBounds({ x: 0, y: 0, width: 1000, height: 640 }), - win.id, - ) + // From a task of its own, as record.mjs resizes: the inspector runs an evaluate by interrupt, and Orbit's + // resize handler would run part-way through other main-process code. + await t.app.evaluate(async (_e, id) => { + await new Promise((r) => setImmediate(r)) + globalThis.__orbit.ctx.windows.get(id)?.setBounds({ x: 0, y: 0, width: 1000, height: 640 }) + }, win.id) await t.sleep(400) notes = await t.run('tabs.create', { url: t.fx.url('Notes'), spaceId: win.spaceId, activate: false }) video = await t.run('tabs.create', { url: t.fx.url('Film club'), spaceId: win.spaceId, activate: false }) diff --git a/scripts/capture/scenarios/mw.mjs b/scripts/capture/scenarios/mw.mjs index c61eb03a..fb425ff2 100644 --- a/scripts/capture/scenarios/mw.mjs +++ b/scripts/capture/scenarios/mw.mjs @@ -4,8 +4,8 @@ * page into the main window, where its row slides into the sidebar; ⌘⌥N opens a blank one whose lookup * bar takes the keys typed at once; closing it fades it out. * - * Each step prints a `[mark] ` line on stderr: its time from the start of the recording, - * so frames around it can be cut from the clip. + * Each step prints a `[mark] ` line on stderr: its time since the scenario began. + * record.mjs begins it 0.7 s after the recording starts, so a step shows about 0.7 s later in the clip. */ let t0 = 0 diff --git a/scripts/capture/scenarios/pk.mjs b/scripts/capture/scenarios/pk.mjs index 3095d488..96b2c539 100644 --- a/scripts/capture/scenarios/pk.mjs +++ b/scripts/capture/scenarios/pk.mjs @@ -4,8 +4,8 @@ * ⌘Z brings it back; a short two-finger swipe down moves the card with the fingers and lets it spring * back, and a long one dismisses it; a last Peek expands into a full tab with ⌘O. * - * Each step prints a `[mark] ` line on stderr: its time from the start of the recording, - * so frames around it can be cut from the clip. + * Each step prints a `[mark] ` line on stderr: its time since the scenario began. + * record.mjs begins it 0.7 s after the recording starts, so a step shows about 0.7 s later in the clip. */ import { execFile } from 'node:child_process' diff --git a/scripts/capture/scenarios/sv.mjs b/scripts/capture/scenarios/sv.mjs index 842fca71..d4e8cd05 100644 --- a/scripts/capture/scenarios/sv.mjs +++ b/scripts/capture/scenarios/sv.mjs @@ -1,10 +1,10 @@ /** - * Split view (SV-02, SV-11, SV-13, SV-17, SV-19, SV-26; FL-18), about 17 s, for the "feels right" sign-off (D48). + * Split view (SV-02, SV-03, SV-11, SV-13, SV-17, SV-19, SV-26; FL-18), about 17 s, for the "feels right" sign-off (D48). * A pane added beside the page grows in with the command bar to fill it; a second pane goes below; * the divider follows the pointer; the layout flips; panes close and the others grow back. * - * Each step prints a `[mark] ` line on stderr: its time from the start of the recording, - * so frames around it can be cut from the clip. + * Each step prints a `[mark] ` line on stderr: its time since the scenario began. + * record.mjs begins it 0.7 s after the recording starts, so a step shows about 0.7 s later in the clip. */ let t0 = 0 @@ -37,13 +37,13 @@ export default async function (t) { t0 = performance.now() await t.sleep(600) - // SV-02, FL-18: a pane added on the right grows in; the page makes room with it, and an open tab fills it. + // SV-02, FL-18: ⌃⇧= adds a pane on the right; it grows in, the page makes room with it, and an open tab fills it. mark('add-right') - await t.action('split.addRight') + await t.action('split.add') await fill(t, 'metr') await t.sleep(1500) - // SV-02, SV-11: a third pane below the focused one. + // SV-03, SV-11: Add Split Below puts a third pane below the focused one. mark('add-below') await t.action('split.addBelow') await fill(t, 'stand')