diff --git a/FEATURES.md b/FEATURES.md index ca2fbcb..4f0cc92 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 · 312 built (63 partial) · 381 tested · 202 feel right. 13 more are out of scope and not counted. +**Totals:** 472 spec’d · 312 built (63 partial) · 381 tested · 221 feel right. 13 more are out of scope and not counted. | Priority | Spec’d | Built | Tested | |---|---|---|---| @@ -209,7 +209,7 @@ Spec: `spec/split-view.md` | 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-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 | @@ -233,7 +233,7 @@ Spec: `spec/navigation.md` | NV-03 | Reload and hard reload | P0 | ✅ | ✅ e2e+unit | ✅ | Cmd/Ctrl+R, F5; hard reload Cmd+Shift+R, Ctrl+Shift+R, Shift+F5, Ctrl+F5; Cmd/middle-click Reload duplicates with history; keyboard focus goes to the page | | NV-04 | Stop loading | P1 | ✅ | ✅ e2e+unit | ✅ | Esc in the page while loading (overlays take it first; a page that uses Esc keeps it and goes on loading, D59), Cmd+. on macOS, double-click Reload, Stop (x) state; a typed address, Back/Forward or Reload shows where it is going at once and stays when stopped; the loading indicator goes. Known limit (keys fix check K6): a page's answer about an Esc that comes after Orbit stopped waiting for it (250 ms) answers the next Esc pressed while that page loads | | NV-05 | Find in page | P0 | ✅ | ✅ e2e+unit | ✅ | floating bar over the active page's top-right corner; finds as you type, n/m count, No matches; Enter/Cmd+G/F3 next, Shift+Enter/Shift+Cmd+G/Shift+F3 previous, buttons; Esc closes and keeps the match selected; per tab (hidden on switch, back with text and place); starts from the selection, else the last search; IME commits; keys typed for the bar before it has the keyboard (a new window's first moments, a busy bar) wait up to 5 s and then go into it in order, text as text (any character on the keyboard's own layout); it takes the keyboard when it shows, even later than that; shortcuts and IME composition are not held; Cmd+J jumps to the selection (macOS); Edit > Find menu; with a Peek open the bar finds in the Peek, over its card (PK-11); a Mini Window finds with a field in its top bar (MW-03). Known limits (chrome fix check 4, N3, by reading): keys held while a replay of held keys runs past 5 s (each held Backspace, Enter or Escape waits up to 1 s for the bar) are dropped, and keys typed after that can reach the bar ahead of the rest of the replay; keys held after a held Escape are dropped, where a ready bar would have let them reach the page once it closed; and a held character an auto-type tool maps onto the keyboard on the fly, which Chromium names "\u0000", is lost (chrome fix check 4) | -| NV-06 | Page zoom | P0 | ✅ | ✅ e2e+unit | – | Chromium steps 25-500% on Cmd+=/Cmd+-/Cmd+0 and Ctrl+wheel (Win/Linux), in a Peek (with a toast) and a Mini Window (no indicator there) too; per host in each profile (all its tabs), remembered across launches (written on quit), dropped with a deleted profile; indicator with Reset over the page for 1.5 s (longer while hovered), its number changing in place at each step while it shows; shown again, neither it nor the find bar first shows what it showed last time (their view draws itself empty before it leaves the window); pinch is visual zoom and Cmd+0 undoes it too | +| NV-06 | Page zoom | P0 | ✅ | ✅ e2e+unit | ✅ | Chromium steps 25-500% on Cmd+=/Cmd+-/Cmd+0 and Ctrl+wheel (Win/Linux), in a Peek (with a toast) and a Mini Window (no indicator there) too; per host in each profile (all its tabs), remembered across launches (written on quit), dropped with a deleted profile; indicator with Reset over the page for 1.5 s (longer while hovered), its number changing in place at each step while it shows; shown again, neither it nor the find bar first shows what it showed last time (their view draws itself empty before it leaves the window); pinch is visual zoom and Cmd+0 undoes it too | | NV-07 | Print | P1 | 🟡 partial | ✅ e2e+unit | – | ⌘P (Ctrl+P), File → Print… (macOS menu), the command bar's Print…, the page menu's Print…, ⌘P in a Peek or a Mini Window, and the page's own window.print() open Orbit's print dialog over that page only (Electron has no Chromium print preview): the page as it prints (the engine's PDF of it in the PDF viewer, made again when the layout changes) beside Destination (Save as PDF and the OS's printers), Pages (all, or ranges such as 1-5, 8, 11-13, with Chromium's messages for pages it can't print), Copies, Layout, Color, Headers and footers, Background graphics, and Print (Save for a PDF, which asks where) and Cancel; Esc closes it wherever its keyboard is (KS-19), Enter prints, Tab stays inside it, and the page gets the keyboard back (KS-16); it waits while its tab isn't shown and comes back with the keyboard, and goes when the page navigates; the page is left as it was (scroll, focus, fields); the next dialog starts from the last one's choices until Orbit quits (all pages and one copy each time). Differences: window.print() returns at once and the page's script goes on (Chromium's waits for the dialog), but what would take the page away waits for the dialog: its window.close() (a receipt written into a pop-up or a new tab, printed and closed at once) and a link, location or reload it starts go on once the dialog closes, as they would have (the same address and kind of history entry); a print() while a dialog that ⌘P or a menu opened shows returns at once and holds nothing, as Chromium's does (D76); missing: a form the page posts, or a move through history (history.back(), go()), right after print() goes at once and takes the dialog with it (the engine doesn't let a page's history move be stopped, and posting a form again would run the page's submit handlers twice); a frame's own window.print() opens the system's print dialog (frames have no preload, D40); missing: paper size, margins, scale, pages per sheet and two-sided choices (the preview and PDF are A4, or Letter where the region uses it; a printer uses its own paper); a page may call print() again as soon as its dialog closes. Tests print and save nothing (the one function that sends a job out records it, and the printers come from ORBIT_E2E_PRINTERS); only a person can check that a real printer prints what the preview shows, and Save as PDF's save dialog | | NV-08 | Clicking links: plain and modified clicks | P0 | 🟡 partial | ✅ e2e+unit | – | Cmd/middle-click background tab (while the sidebar is hidden a toast over the page, Opened in a new tab with Show, SB-13) and Shift+Cmd foreground tab at the top of Today; Shift-click a new window when Peek is off; the page itself opens these (a link click in an isolated world, with the gesture), so they are requested as its click is: Referer, Sec-Fetch-Site, no SameSite=Strict cookie across sites; Option/Alt-click split view instead of the engine download; a local file opens in a new tab, window, Mini Window or split view only for a local file page whose frames are all local too, whatever the engine lets through: Orbit can't tell which frame asked, so a frame of any other origin (a web page's, or an opaque one: sandboxed, data:) refuses Cmd-, Shift- and Cmd+Option-clicks, target=_blank and window.open, and a toast names the file; a plain click without a target still opens it (D49); Cmd+Option-click a Mini Window and Shift-click Peek through the peek lane; hover pill is SB-27; still requested as if typed (Sec-Fetch-Site none, Strict cookies sent), with the page as Referer: the page menu Open Link in New Tab and New Window, Option-click split view, a blocked pop-up Open; without a Referer too: Peek, Mini Window and routing-rule links (peek lane), a pop-up window own links, Open Link in Private Window (on purpose); a link in a frame from another origin is opened from the top page, without a Referer; a link (or text) dragged from a page onto the sidebar opens as a tab where it is dropped, as a tab row dragged there would (TB-02): the rows make room, a closed folder takes it in, the Favorites grid opens a place, a Space's icon takes it there; only a web (http, https) address opens or is saved there: other text is a search on the list or in a folder and nothing on the Favorites, and any other address (data:, view-source:, javascript:, a local file…) opens nothing (D87); left: a link dragged onto the page opens in a split (SV-08) | | NV-09 | Links with target=_blank and script-opened windows | P0 | ✅ | ✅ e2e+unit | – | target=_blank and window.open without size: selected tab at the top of Today with its opener; with size: a small window without sidebar titled with the host (Not secure marked), the opener can close it; pop-ups without a click blocked, shown in the address display with Open and Always Allow for the site; a click or a key in any frame of a page counts, one from another site's too (D77), and a pop-up window's own opens become tabs by the same rule; a web page, or a local page with a frame of another origin (web or opaque), gets no tab, window or pop-up of a local file, nor one offered there (D49); a toast names the file only where Orbit itself refuses it, for a local page with a frame of another origin: the engine refuses a web page's before Orbit's handler runs, with no toast; a tab that only downloads closes and reselects its opener | @@ -265,8 +265,8 @@ Spec: `spec/peek.md` | 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; the page is laid out once, at its size, and joins the card at rest: the card grows blank, and shows a still of the page while it moves or closes (none while the page shows a crash or a failed load: the card shows its own colour); 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-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; the page is laid out once, at its size, and joins the card at rest: the card grows blank, and shows a still of the page while it moves or closes (none while the page shows a crash or a failed load: the card shows its own colour); 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 | @@ -286,7 +286,7 @@ Spec: `spec/mini-window.md` | 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. The ready window Orbit keeps hidden draws itself empty until it opens, because a window shows the frame it drew last first: a link opens with its page's bar, never the blank lookup bar, and a blank window first shows early in its grow, not at full size. 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-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. The ready window Orbit keeps hidden draws itself empty until it opens, because a window shows the frame it drew last first: a link opens with its page's bar, never the blank lookup bar, and a blank window first shows early in its grow, not at full size. 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 | @@ -297,7 +297,7 @@ Spec: `spec/mini-window.md` | 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-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; closing and Open in… never leave the window on screen without its page or UI: where the window fades out its card shows a still of the page as the page leaves, if one is taken within 150 ms (macOS, Windows; not run here), elsewhere the window leaves the screen first, whole | +| 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; closing and Open in… never leave the window on screen without its page or UI: where the window fades out its card shows a still of the page as the page leaves, if one is taken within 150 ms (macOS, Windows; not run here), elsewhere the window leaves the screen first, whole | ## Picture-in-picture, sidebar media player and call controls (prefix MD) @@ -311,7 +311,7 @@ Spec: `spec/media.md` | 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 and save dialogs say so; so do Orbit's dialogs and screens over a page while they show: a page's alert, confirm, prompt and "Open Mail?", the print dialog, and the error, crash and "Page unresponsive" screens), 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, and with a page's confirm, the print dialog and a crash screen). 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-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 | @@ -329,7 +329,7 @@ Spec: `spec/archive.md` | ID | Feature | P | Built | Tested | Feels right | Notes | |---|---|---|---|---|---|---| | AR-01 | What goes into the Archive | P0 | ✅ | ✅ e2e+unit | ✅ | close, auto-archive, Clear, archive-below, pinned/folder/Space delete, Mini Window and Peek pages; entries carry tab, Space name, section, reason; back/forward history kept outside OrbitState (userData/archive, page state in memory only); private and empty tabs never archived; state keeps the newest ~200-300, the rest lives in the Archive store | -| AR-02 | Where the Archive lives and how to open it | P0 | ✅ | ✅ e2e | – | Library view over the page area (its own view above the tabs, below the command bar and Peek) from the bottom-bar button, ⌘⇧L (Open Library), ⌘T View Archive / Open Library and the macOS Archive menu; reopens to the last section; Esc, ⌘W or selecting another tab closes it and the page gets the keys back; section registry (*.library-section.tsx) for other lanes; closed, or made ahead of its first open, it draws nothing, so no half-drawn Library shows before it fades in, and only the section it opens on is marked; closing, its view leaves the window once it has drawn itself faded out (500 ms at the latest) | +| AR-02 | Where the Archive lives and how to open it | P0 | ✅ | ✅ e2e | ✅ | Library view over the page area (its own view above the tabs, below the command bar and Peek) from the bottom-bar button, ⌘⇧L (Open Library), ⌘T View Archive / Open Library and the macOS Archive menu; reopens to the last section; Esc, ⌘W or selecting another tab closes it and the page gets the keys back; section registry (*.library-section.tsx) for other lanes; closed, or made ahead of its first open, it draws nothing, so no half-drawn Library shows before it fades in, and only the section it opens on is marked; closing, its view leaves the window once it has drawn itself faded out (500 ms at the latest) | | AR-03 | Archive list layout | P0 | ✅ | ✅ e2e+unit | ✅ | newest first under Today / Yesterday / date headings; favicon, title, host, time and origin-Space badge; filters by source (all, sidebar, Mini Window), Space and how it closed; per profile (PR-05); virtualised and paged, fast at 10k entries. A row click previews (AR-06); ⌘-click and ⇧-click select | | AR-04 | Search the Archive | P0 | ✅ | ✅ e2e+unit | ✅ | search field filters on each keystroke (title and URL, case-insensitive, partial words, every word) using the Archive store's keys (history queryTerms/urlKeyOf); typing on narrows the last answer; Enter restores and shows the top or highlighted match; ⌘F focuses it. The command bar's Archived results search the whole Archive store of the profile, not only the recent entries kept in state | | AR-05 | Restore an archived tab | P0 | ✅ | ✅ e2e+unit | ✅ | Restore (hover button, Enter, row menu, several selected) puts tabs at the top of their Space's Today in order, with their tab id and back/forward history when the profile matches (never across profiles); routing rule > origin Space > current Space; toast with Go to Space when it lands elsewhere; command bar restores without a jump. A row click previews (AR-06) instead of restoring. Undoing Delete Folder after the Space changed profile also follows the new profile (W3) | @@ -436,23 +436,23 @@ Spec: `spec/boosts.md` | ID | Feature | P | Built | Tested | Feels right | Notes | |---|---|---|---|---|---|---| -| BO-01 | Create a site customization for the current site | P1 | ✅ | ✅ e2e+unit | – | New Tweak… from the command bar (also "customize", "customization", "site style"), the sidebar's + menu, the site controls panel, a paintbrush in the address field (shown on hover, in the accent colour once the site has a Tweak on) and the tab's menu (Tweak this Site…); none bound to a key. Opens the editor over the page with a new Tweak named after the host; a second one adds a second Tweak. A one-screen intro the first time only. Internal pages, error pages, a Peek and a Mini Window take none: the entries are greyed or gone, or say why. A Mini Window has no Tweak entry of its own; a key given to one there says why in the browser window, as a Mini Window shows no notes (with no browser window open, nothing shows). Nothing reloads | -| BO-02 | Editor panel: layout, live preview, close | P1 | ✅ | ✅ e2e+unit | – | a small view of its own over the page's top-right corner, sized to the panel, draggable by its title and kept inside the page; the page around it stays live. Every change to the look shows at once, while the page is still loading too, and is saved (JavaScript runs with BO-11's Run Now, or the next load); Esc, the close button and Done close it and give the page the keyboard back; the site controls' › and Edit Tweak reopen it as it was. Another page of the site keeps it open; another site or tab closes it; it steps aside for a Peek or full screen. No request names the site. Top to bottom: the name and its ▾ menu (BO-12), the colour area (BO-03 to BO-06), fonts (BO-07), size (BO-08), text case (BO-09), Hide elements (BO-10), the code area (BO-11, which widens the panel to the left), "Also apply to subdomains" (BO-17) and Done. Dragging the wheel recolours the page as fast as the page could itself (BO-03 has the numbers) | -| BO-03 | Colour wheel (theme tint) | P1 | ✅ | ✅ e2e+unit | – | a wheel with two linked handles: the big one anywhere inside (its angle the hue, its distance how strong), the small one on the rim (the accent: it turns the page's own colours by its distance from the main hue, and turns with the big one). One filter on the page root tints it: white becomes a light tint of the hue, black stays black, text keeps its contrast; images and video keep their colours (an inverse filter), the top layer is tinted too. Arrow keys move the focused handle (⇧ further). Measured with the frame sampler against the page changing the same colours itself, one a frame: the drag keeps up, and main takes an edit for every move (60 of 60, one a frame; an editor that took every other move fails). Under a virtual display with no GPU, 60 a second at 1024×768 (slowest frame 17 ms); at 1440×900 the page itself manages only about 35 a second there, and the drag matches it | -| BO-04 | Invert lightness ("make this site dark") | P1 | ✅ | ✅ e2e+unit | – | the lightbulb next to the wheel inverts lightness and keeps hues (white page dark, text light), with the tint on the inverted result; photos and video keep their own look; again restores it | -| BO-05 | Advanced colour sliders | P2 | ✅ | ✅ e2e+unit | – | the sliders button opens Contrast, Brightness and Original saturation, live as they move; each has a marked neutral middle (100%) that changes nothing, and a double-click puts it back there | -| BO-06 | Reset colours | P1 | ✅ | ✅ e2e+unit | – | the circle-with-a-cross button puts the wheel, invert and sliders back to neutral, so the page shows its own colours; fonts, size, case, hidden elements and code stay | +| BO-01 | Create a site customization for the current site | P1 | ✅ | ✅ e2e+unit | ✅ | New Tweak… from the command bar (also "customize", "customization", "site style"), the sidebar's + menu, the site controls panel, a paintbrush in the address field (shown on hover, in the accent colour once the site has a Tweak on) and the tab's menu (Tweak this Site…); none bound to a key. Opens the editor over the page with a new Tweak named after the host; a second one adds a second Tweak. A one-screen intro the first time only. Internal pages, error pages, a Peek and a Mini Window take none: the entries are greyed or gone, or say why. A Mini Window has no Tweak entry of its own; a key given to one there says why in the browser window, as a Mini Window shows no notes (with no browser window open, nothing shows). Nothing reloads | +| BO-02 | Editor panel: layout, live preview, close | P1 | ✅ | ✅ e2e+unit | ✅ | a small view of its own over the page's top-right corner, sized to the panel, draggable by its title and kept inside the page; the page around it stays live. Every change to the look shows at once, while the page is still loading too, and is saved (JavaScript runs with BO-11's Run Now, or the next load); Esc, the close button and Done close it and give the page the keyboard back; the site controls' › and Edit Tweak reopen it as it was. Another page of the site keeps it open; another site or tab closes it; it steps aside for a Peek or full screen. No request names the site. Top to bottom: the name and its ▾ menu (BO-12), the colour area (BO-03 to BO-06), fonts (BO-07), size (BO-08), text case (BO-09), Hide elements (BO-10), the code area (BO-11, which widens the panel to the left), "Also apply to subdomains" (BO-17) and Done. Dragging the wheel recolours the page as fast as the page could itself (BO-03 has the numbers) | +| BO-03 | Colour wheel (theme tint) | P1 | ✅ | ✅ e2e+unit | ✅ | a wheel with two linked handles: the big one anywhere inside (its angle the hue, its distance how strong), the small one on the rim (the accent: it turns the page's own colours by its distance from the main hue, and turns with the big one). One filter on the page root tints it: white becomes a light tint of the hue, black stays black, text keeps its contrast; images and video keep their colours (an inverse filter), the top layer is tinted too. Arrow keys move the focused handle (⇧ further). Measured with the frame sampler against the page changing the same colours itself, one a frame: the drag keeps up, and main takes an edit for every move (60 of 60, one a frame; an editor that took every other move fails). Under a virtual display with no GPU, 60 a second at 1024×768 (slowest frame 17 ms); at 1440×900 the page itself manages only about 35 a second there, and the drag matches it | +| BO-04 | Invert lightness ("make this site dark") | P1 | ✅ | ✅ e2e+unit | ✅ | the lightbulb next to the wheel inverts lightness and keeps hues (white page dark, text light), with the tint on the inverted result; photos and video keep their own look; again restores it | +| BO-05 | Advanced colour sliders | P2 | ✅ | ✅ e2e+unit | ✅ | the sliders button opens Contrast, Brightness and Original saturation, live as they move; each has a marked neutral middle (100%) that changes nothing, and a double-click puts it back there | +| BO-06 | Reset colours | P1 | ✅ | ✅ e2e+unit | ✅ | the circle-with-a-cross button puts the wheel, invert and sliders back to neutral, so the page shows its own colours; fonts, size, case, hidden elements and code stay | | BO-07 | Font override presets | P1 | 🟡 partial | ✅ e2e+unit | – | eight "Aa" presets, each in its own typeface: Sans, Serif, Classic, Humanist, Rounded, Mono, Typewriter and Hand. One applies to all the site's text but icon fonts, glyphs hidden from screen readers, code and math; the active one again gives the site its fonts back. The presets are font stacks of what the system has, so nothing is bundled and their look varies by system (Rounded and Hand fall back on Linux); shipping freely licensed fonts is open | -| BO-08 | Page size (zoom) control | P1 | ✅ | ✅ e2e+unit | – | − and + step the page through 90, 100, 110, 125 and 150%, saved with the Tweak, so every visit gets it; it multiplies with the tab's own zoom (⌘+ / ⌘−) and leaves that alone | -| BO-09 | Text case control | P2 | ✅ | ✅ e2e+unit | – | one button cycles As written, lowercase, UPPERCASE, Capitalized Words and back; fields, editable text and code keep their own case | +| BO-08 | Page size (zoom) control | P1 | ✅ | ✅ e2e+unit | ✅ | − and + step the page through 90, 100, 110, 125 and 150%, saved with the Tweak, so every visit gets it; it multiplies with the tab's own zoom (⌘+ / ⌘−) and leaves that alone | +| BO-09 | Text case control | P2 | ✅ | ✅ e2e+unit | ✅ | one button cycles As written, lowercase, UPPERCASE, Capitalized Words and back; fields, editable text and code keep their own case | | BO-10 | Hide-element mode | P1 | ✅ | ✅ e2e+unit | – | the lightning button: the element under the pointer gets an outline with its kind and size, and the page's links and buttons don't act: the picker stops each click and press on its way to them, except the page's own listeners on its window, which can still hear them first; and if the page takes the picker's layer away, the layer comes back (a page that keeps taking it ends the mode); a click picks. The panel then offers Hide this, or Hide all similar with a slider from narrow to broad, counting what it would hide, and how many of those show now where some don't ("5 (4 shown)"), and outlining those in the page. Gone at once and saved as plain CSS selectors, so hidden from the first frame on every page of the site where it matches. Done or Esc leaves the mode (Esc drops a pick first); another document ends it. On a page still loading, the mode starts when the load ends: a click on the button shows it starting at once, the button pressed and the panel saying it starts when the page has loaded, and the page acts as usual until then; Done or Esc, in the panel or the page, cancels the start (the page still gets its Esc), and another document ends it. The picker runs in an isolated world, never the page's own. Each hide has a restore marker in the panel (not over the page's corner): hovered it shows the element again, clicked it brings it back | -| BO-11 | Code editor: custom CSS and JavaScript per site | P1 | ✅ | ✅ e2e+unit | – | the { } button opens a code area with CSS and JavaScript tabs, empty to start. CSS applies as it is typed (about 90 ms from the keystroke to the page's frame, measured) and is saved as typed, sent before the panel closes; a reload keeps it. JavaScript runs in the page's own world once per load at document ready, from main; Run now runs it at once, Reload page loads the page again. The editor: monospace, syntax colouring (text only, never markup), line numbers, the platform's undo (⌘Z / Ctrl+Z through Orbit's key handling), Tab and ⇧Tab indent and outdent, Enter keeps the indentation. Errors, thrown or syntax, go to the page's console; no dialog, nothing in main. JavaScript from elsewhere stays off until Allow on this device, and editing it doesn't allow it. A JS badge in the site controls list and the code tab (the Library's list comes with BO-15) | -| BO-12 | Name, rename and reset all edits | P1 | ✅ | ✅ e2e+unit | – | the title is the Tweak's name with a ▾ menu: Rename… edits it in place (Enter keeps, Esc cancels), Reset all edits… asks first, then clears colours, font, size, case, hidden elements and code; the name and switches stay. The menu works from the keyboard; Esc closes it, not the panel | -| BO-13 | Shuffle (random style) | P2 | ✅ | ✅ e2e+unit | – | Shuffle in the name ▾ menu applies a random look live: any hue at a moderate strength, an accent a pleasing step away, sometimes inverted, one of the fonts, the site's size or a little larger, mostly the site's case. The menu stays open to shuffle again; hidden elements and code are never touched | -| BO-14 | Enable / disable a customization (per site) | P1 | ✅ | ✅ e2e+unit | – | the site controls panel lists the site's Tweaks by name with a paintbrush, coloured when on and grey when off; the name turns one off or on at once, without a reload, while the page is still loading too, each on its own, keeping all its settings; the › opens it in the editor. The address field's paintbrush shows in the accent colour while one is on and grey when all are off, and turns them all off or on. Off, a Tweak's styles leave the page at once; what its JavaScript already did stays until the page loads again | -| BO-15 | Manage all customizations (list view in the Library) | P1 | ✅ | ✅ e2e+unit | – | the Library's Tweaks section lists every Tweak on this device by site, a site's oldest first: its name, its site (where the name doesn't say it, and "and its subdomains"), a JS badge where it has JavaScript (struck through where that isn't allowed on this device) and a switch that turns it off or on at once. Right-click or ⋯ (the Library's own menu): Edit shows the site's tab in the Space (never a Peek's or a Mini Window's page), or opens the site, and opens the editor once the page can take it; a site that doesn't load stops the wait as its load fails, with a note saying so; Open Site shows that tab, else opens a new tab where the Tweak was made (scheme, host and port); Rename… in place; Delete… asks first, and its styles leave open pages at once (its JavaScript's effects with their next load). Keys: up and down, Enter edits, Space switches, F2 renames, Delete asks, Shift+F10 opens the menu. The command bar's View Tweaks opens it. With none, a short explanation and Tweak This Site for the page underneath | -| BO-16 | Global on/off switch for all customizations | P1 | ✅ | ✅ e2e+unit | – | Settings → Advanced: "Apply Tweaks on websites I visit", on by default. Off, every open page loses its Tweaks' styles at once, while still loading too, and the editor closes; what a Tweak's JavaScript already did stays until the page loads again, as the setting says; the address field's paintbrush and the + menu's and the tab menu's New Tweak go; the command bar's says Tweaks are off; the site controls panel and the Library say so, the Library with Turn On. Each Tweak keeps its own on or off for when they are back on. The organisation policy is P2 and not built | -| BO-17 | Scope: which pages a customization applies to | P1 | ✅ | ✅ e2e+unit | – | a Tweak is for its host's pages: any path, query and in-page navigation, not another host, the parent domain or a subdomain. "Also apply to subdomains" (off by default, at the panel's foot) brings in subdomains' open pages at once; it isn't offered for an address by number and is fixed while the page is on a subdomain it brings in. Every profile gets the same Tweaks; in a private window the CSS applies and the JavaScript runs only once "Also run in private windows" is ticked. Matching is a map lookup per label of the host | +| BO-11 | Code editor: custom CSS and JavaScript per site | P1 | ✅ | ✅ e2e+unit | ✅ | the { } button opens a code area with CSS and JavaScript tabs, empty to start. CSS applies as it is typed (about 90 ms from the keystroke to the page's frame, measured) and is saved as typed, sent before the panel closes; a reload keeps it. JavaScript runs in the page's own world once per load at document ready, from main; Run now runs it at once, Reload page loads the page again. The editor: monospace, syntax colouring (text only, never markup), line numbers, the platform's undo (⌘Z / Ctrl+Z through Orbit's key handling), Tab and ⇧Tab indent and outdent, Enter keeps the indentation. Errors, thrown or syntax, go to the page's console; no dialog, nothing in main. JavaScript from elsewhere stays off until Allow on this device, and editing it doesn't allow it. A JS badge in the site controls list and the code tab (the Library's list comes with BO-15) | +| BO-12 | Name, rename and reset all edits | P1 | ✅ | ✅ e2e+unit | ✅ | the title is the Tweak's name with a ▾ menu: Rename… edits it in place (Enter keeps, Esc cancels), Reset all edits… asks first, then clears colours, font, size, case, hidden elements and code; the name and switches stay. The menu works from the keyboard; Esc closes it, not the panel | +| BO-13 | Shuffle (random style) | P2 | ✅ | ✅ e2e+unit | ✅ | Shuffle in the name ▾ menu applies a random look live: any hue at a moderate strength, an accent a pleasing step away, sometimes inverted, one of the fonts, the site's size or a little larger, mostly the site's case. The menu stays open to shuffle again; hidden elements and code are never touched | +| BO-14 | Enable / disable a customization (per site) | P1 | ✅ | ✅ e2e+unit | ✅ | the site controls panel lists the site's Tweaks by name with a paintbrush, coloured when on and grey when off; the name turns one off or on at once, without a reload, while the page is still loading too, each on its own, keeping all its settings; the › opens it in the editor. The address field's paintbrush shows in the accent colour while one is on and grey when all are off, and turns them all off or on. Off, a Tweak's styles leave the page at once; what its JavaScript already did stays until the page loads again | +| BO-15 | Manage all customizations (list view in the Library) | P1 | ✅ | ✅ e2e+unit | ✅ | the Library's Tweaks section lists every Tweak on this device by site, a site's oldest first: its name, its site (where the name doesn't say it, and "and its subdomains"), a JS badge where it has JavaScript (struck through where that isn't allowed on this device) and a switch that turns it off or on at once. Right-click or ⋯ (the Library's own menu): Edit shows the site's tab in the Space (never a Peek's or a Mini Window's page), or opens the site, and opens the editor once the page can take it; a site that doesn't load stops the wait as its load fails, with a note saying so; Open Site shows that tab, else opens a new tab where the Tweak was made (scheme, host and port); Rename… in place; Delete… asks first, and its styles leave open pages at once (its JavaScript's effects with their next load). Keys: up and down, Enter edits, Space switches, F2 renames, Delete asks, Shift+F10 opens the menu. The command bar's View Tweaks opens it. With none, a short explanation and Tweak This Site for the page underneath | +| BO-16 | Global on/off switch for all customizations | P1 | ✅ | ✅ e2e+unit | ✅ | Settings → Advanced: "Apply Tweaks on websites I visit", on by default. Off, every open page loses its Tweaks' styles at once, while still loading too, and the editor closes; what a Tweak's JavaScript already did stays until the page loads again, as the setting says; the address field's paintbrush and the + menu's and the tab menu's New Tweak go; the command bar's says Tweaks are off; the site controls panel and the Library say so, the Library with Turn On. Each Tweak keeps its own on or off for when they are back on. The organisation policy is P2 and not built | +| BO-17 | Scope: which pages a customization applies to | P1 | ✅ | ✅ e2e+unit | ✅ | a Tweak is for its host's pages: any path, query and in-page navigation, not another host, the parent domain or a subdomain. "Also apply to subdomains" (off by default, at the panel's foot) brings in subdomains' open pages at once; it isn't offered for an address by number and is fixed while the page is on a subdomain it brings in. Every profile gets the same Tweaks; in a private window the CSS applies and the JavaScript runs only once "Also run in private windows" is ticked. Matching is a map lookup per label of the host | | BO-18 | How customizations apply on page load (timing, safety) | P1 | 🟡 partial | ✅ e2e+unit | – | the page preload asks main for the site's sheets before the page's first paint (D40) and puts them in as injected sheets (D85), which the page can't read or remove, and which its own layered !important rules can't outrank: no frame shows the page without its Tweak (frame-capture tests on a slow page's hard reload and first visit, on a page that sets its own adopted sheets in its head, and on one with a layered !important background). Main puts in its own copies at document ready, or at the first change before it, then tells the preload to take its copies out, by a message that carries only the document's number and that main never waits for. Survives reloads, back and forward, a reopened tab, a woken tab and a restart. JavaScript runs from main once per load at document ready, main frame only, where allowed (BO-11, BO-17), and checks the page's host itself as it runs: not again on an in-page navigation, never in a frame from another site; what it did stays until the next load. The Space colours come with BO-19 | | BO-19 | Use Space theme colours inside customization CSS | P2 | – | ✅ unit | – | | | BO-20 | Sharing customizations and the customization gallery | P2 | – | ✅ e2e | – | | diff --git a/docs/feature-status.json b/docs/feature-status.json index c9bd01d..f159fc6 100644 --- a/docs/feature-status.json +++ b/docs/feature-status.json @@ -61,7 +61,7 @@ "IM-11": {"scope": "out", "note": "Orbit has no store for addresses and cards to import into: the spec has no row for one (OT-12), and Electron has none (D52)"}, "ST-15": {"scope": "out", "note": "the settings of a password manager and an autofill store, which Orbit doesn't have (IM-08 and IM-11 are out of scope too, D52)"}, "AR-01": {"built": "yes", "feel": true, "note": "close, auto-archive, Clear, archive-below, pinned/folder/Space delete, Mini Window and Peek pages; entries carry tab, Space name, section, reason; back/forward history kept outside OrbitState (userData/archive, page state in memory only); private and empty tabs never archived; state keeps the newest ~200-300, the rest lives in the Archive store"}, - "AR-02": {"built": "yes", "note": "Library view over the page area (its own view above the tabs, below the command bar and Peek) from the bottom-bar button, ⌘⇧L (Open Library), ⌘T View Archive / Open Library and the macOS Archive menu; reopens to the last section; Esc, ⌘W or selecting another tab closes it and the page gets the keys back; section registry (*.library-section.tsx) for other lanes; closed, or made ahead of its first open, it draws nothing, so no half-drawn Library shows before it fades in, and only the section it opens on is marked; closing, its view leaves the window once it has drawn itself faded out (500 ms at the latest)"}, + "AR-02": {"built": "yes", "feel": true, "note": "Library view over the page area (its own view above the tabs, below the command bar and Peek) from the bottom-bar button, ⌘⇧L (Open Library), ⌘T View Archive / Open Library and the macOS Archive menu; reopens to the last section; Esc, ⌘W or selecting another tab closes it and the page gets the keys back; section registry (*.library-section.tsx) for other lanes; closed, or made ahead of its first open, it draws nothing, so no half-drawn Library shows before it fades in, and only the section it opens on is marked; closing, its view leaves the window once it has drawn itself faded out (500 ms at the latest)"}, "AR-03": {"built": "yes", "feel": true, "note": "newest first under Today / Yesterday / date headings; favicon, title, host, time and origin-Space badge; filters by source (all, sidebar, Mini Window), Space and how it closed; per profile (PR-05); virtualised and paged, fast at 10k entries. A row click previews (AR-06); ⌘-click and ⇧-click select"}, "AR-04": {"built": "yes", "feel": true, "note": "search field filters on each keystroke (title and URL, case-insensitive, partial words, every word) using the Archive store's keys (history queryTerms/urlKeyOf); typing on narrows the last answer; Enter restores and shows the top or highlighted match; ⌘F focuses it. The command bar's Archived results search the whole Archive store of the profile, not only the recent entries kept in state"}, "AR-05": {"built": "yes", "feel": true, "note": "Restore (hover button, Enter, row menu, several selected) puts tabs at the top of their Space's Today in order, with their tab id and back/forward history when the profile matches (never across profiles); routing rule > origin Space > current Space; toast with Go to Space when it lands elsewhere; command bar restores without a jump. A row click previews (AR-06) instead of restoring. Undoing Delete Folder after the Space changed profile also follows the new profile (W3)"}, @@ -103,7 +103,7 @@ "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-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", "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"}, @@ -195,7 +195,7 @@ "NV-03": {"built": "yes", "feel": true, "note": "Cmd/Ctrl+R, F5; hard reload Cmd+Shift+R, Ctrl+Shift+R, Shift+F5, Ctrl+F5; Cmd/middle-click Reload duplicates with history; keyboard focus goes to the page"}, "NV-04": {"built": "yes", "feel": true, "note": "Esc in the page while loading (overlays take it first; a page that uses Esc keeps it and goes on loading, D59), Cmd+. on macOS, double-click Reload, Stop (x) state; a typed address, Back/Forward or Reload shows where it is going at once and stays when stopped; the loading indicator goes. Known limit (keys fix check K6): a page's answer about an Esc that comes after Orbit stopped waiting for it (250 ms) answers the next Esc pressed while that page loads"}, "NV-05": {"built": "yes", "feel": true, "note": "floating bar over the active page's top-right corner; finds as you type, n/m count, No matches; Enter/Cmd+G/F3 next, Shift+Enter/Shift+Cmd+G/Shift+F3 previous, buttons; Esc closes and keeps the match selected; per tab (hidden on switch, back with text and place); starts from the selection, else the last search; IME commits; keys typed for the bar before it has the keyboard (a new window's first moments, a busy bar) wait up to 5 s and then go into it in order, text as text (any character on the keyboard's own layout); it takes the keyboard when it shows, even later than that; shortcuts and IME composition are not held; Cmd+J jumps to the selection (macOS); Edit > Find menu; with a Peek open the bar finds in the Peek, over its card (PK-11); a Mini Window finds with a field in its top bar (MW-03). Known limits (chrome fix check 4, N3, by reading): keys held while a replay of held keys runs past 5 s (each held Backspace, Enter or Escape waits up to 1 s for the bar) are dropped, and keys typed after that can reach the bar ahead of the rest of the replay; keys held after a held Escape are dropped, where a ready bar would have let them reach the page once it closed; and a held character an auto-type tool maps onto the keyboard on the fly, which Chromium names \"\\u0000\", is lost (chrome fix check 4)"}, - "NV-06": {"built": "yes", "note": "Chromium steps 25-500% on Cmd+=/Cmd+-/Cmd+0 and Ctrl+wheel (Win/Linux), in a Peek (with a toast) and a Mini Window (no indicator there) too; per host in each profile (all its tabs), remembered across launches (written on quit), dropped with a deleted profile; indicator with Reset over the page for 1.5 s (longer while hovered), its number changing in place at each step while it shows; shown again, neither it nor the find bar first shows what it showed last time (their view draws itself empty before it leaves the window); pinch is visual zoom and Cmd+0 undoes it too"}, + "NV-06": {"built": "yes", "feel": true, "note": "Chromium steps 25-500% on Cmd+=/Cmd+-/Cmd+0 and Ctrl+wheel (Win/Linux), in a Peek (with a toast) and a Mini Window (no indicator there) too; per host in each profile (all its tabs), remembered across launches (written on quit), dropped with a deleted profile; indicator with Reset over the page for 1.5 s (longer while hovered), its number changing in place at each step while it shows; shown again, neither it nor the find bar first shows what it showed last time (their view draws itself empty before it leaves the window); pinch is visual zoom and Cmd+0 undoes it too"}, "NV-07": {"built": "partial", "note": "⌘P (Ctrl+P), File → Print… (macOS menu), the command bar's Print…, the page menu's Print…, ⌘P in a Peek or a Mini Window, and the page's own window.print() open Orbit's print dialog over that page only (Electron has no Chromium print preview): the page as it prints (the engine's PDF of it in the PDF viewer, made again when the layout changes) beside Destination (Save as PDF and the OS's printers), Pages (all, or ranges such as 1-5, 8, 11-13, with Chromium's messages for pages it can't print), Copies, Layout, Color, Headers and footers, Background graphics, and Print (Save for a PDF, which asks where) and Cancel; Esc closes it wherever its keyboard is (KS-19), Enter prints, Tab stays inside it, and the page gets the keyboard back (KS-16); it waits while its tab isn't shown and comes back with the keyboard, and goes when the page navigates; the page is left as it was (scroll, focus, fields); the next dialog starts from the last one's choices until Orbit quits (all pages and one copy each time). Differences: window.print() returns at once and the page's script goes on (Chromium's waits for the dialog), but what would take the page away waits for the dialog: its window.close() (a receipt written into a pop-up or a new tab, printed and closed at once) and a link, location or reload it starts go on once the dialog closes, as they would have (the same address and kind of history entry); a print() while a dialog that ⌘P or a menu opened shows returns at once and holds nothing, as Chromium's does (D76); missing: a form the page posts, or a move through history (history.back(), go()), right after print() goes at once and takes the dialog with it (the engine doesn't let a page's history move be stopped, and posting a form again would run the page's submit handlers twice); a frame's own window.print() opens the system's print dialog (frames have no preload, D40); missing: paper size, margins, scale, pages per sheet and two-sided choices (the preview and PDF are A4, or Letter where the region uses it; a printer uses its own paper); a page may call print() again as soon as its dialog closes. Tests print and save nothing (the one function that sends a job out records it, and the printers come from ORBIT_E2E_PRINTERS); only a person can check that a real printer prints what the preview shows, and Save as PDF's save dialog"}, "NV-08": {"built": "partial", "note": "Cmd/middle-click background tab (while the sidebar is hidden a toast over the page, Opened in a new tab with Show, SB-13) and Shift+Cmd foreground tab at the top of Today; Shift-click a new window when Peek is off; the page itself opens these (a link click in an isolated world, with the gesture), so they are requested as its click is: Referer, Sec-Fetch-Site, no SameSite=Strict cookie across sites; Option/Alt-click split view instead of the engine download; a local file opens in a new tab, window, Mini Window or split view only for a local file page whose frames are all local too, whatever the engine lets through: Orbit can't tell which frame asked, so a frame of any other origin (a web page's, or an opaque one: sandboxed, data:) refuses Cmd-, Shift- and Cmd+Option-clicks, target=_blank and window.open, and a toast names the file; a plain click without a target still opens it (D49); Cmd+Option-click a Mini Window and Shift-click Peek through the peek lane; hover pill is SB-27; still requested as if typed (Sec-Fetch-Site none, Strict cookies sent), with the page as Referer: the page menu Open Link in New Tab and New Window, Option-click split view, a blocked pop-up Open; without a Referer too: Peek, Mini Window and routing-rule links (peek lane), a pop-up window own links, Open Link in Private Window (on purpose); a link in a frame from another origin is opened from the top page, without a Referer; a link (or text) dragged from a page onto the sidebar opens as a tab where it is dropped, as a tab row dragged there would (TB-02): the rows make room, a closed folder takes it in, the Favorites grid opens a place, a Space's icon takes it there; only a web (http, https) address opens or is saved there: other text is a search on the list or in a folder and nothing on the Favorites, and any other address (data:, view-source:, javascript:, a local file…) opens nothing (D87); left: a link dragged onto the page opens in a split (SV-08)"}, "NV-09": {"built": "yes", "note": "target=_blank and window.open without size: selected tab at the top of Today with its opener; with size: a small window without sidebar titled with the host (Not secure marked), the opener can close it; pop-ups without a click blocked, shown in the address display with Open and Always Allow for the site; a click or a key in any frame of a page counts, one from another site's too (D77), and a pop-up window's own opens become tabs by the same rule; a web page, or a local page with a frame of another origin (web or opaque), gets no tab, window or pop-up of a local file, nor one offered there (D49); a toast names the file only where Orbit itself refuses it, for a local page with a frame of another origin: the engine refuses a web page's before Orbit's handler runs, with no toast; a tab that only downloads closes and reselects its opener"}, @@ -225,7 +225,7 @@ "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 and save dialogs say so; so do Orbit's dialogs and screens over a page while they show: a page's alert, confirm, prompt and \"Open Mail?\", the print dialog, and the error, crash and \"Page unresponsive\" screens), 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, and with a page's confirm, the print dialog and a crash screen). 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-07": {"built": "yes", "feel": true, "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", "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"}, @@ -248,8 +248,8 @@ "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; the page is laid out once, at its size, and joins the card at rest: the card grows blank, and shows a still of the page while it moves or closes (none while the page shows a crash or a failed load: the card shows its own colour); 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", "feel": true, "note": "⌘O / button: card morphs to the frame, live page becomes a selected Today tab (routing rules apply)"}, + "PK-06": {"built": "yes", "feel": true, "note": "spring grow from the link, 18% dim (20% dark), 160 ms shrink to 0.96 and fade, reduced motion; the page is laid out once, at its size, and joins the card at rest: the card grows blank, and shows a still of the page while it moves or closes (none while the page shows a crash or a failed load: the card shows its own colour); 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", "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"}, @@ -263,7 +263,7 @@ "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. The ready window Orbit keeps hidden draws itself empty until it opens, because a window shows the frame it drew last first: a link opens with its page's bar, never the blank lookup bar, and a blank window first shows early in its grow, not at full size. 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-04": {"built": "yes", "feel": true, "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. The ready window Orbit keeps hidden draws itself empty until it opens, because a window shows the frame it drew last first: a link opens with its page's bar, never the blank lookup bar, and a blank window first shows early in its grow, not at full size. 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", "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"}, @@ -274,7 +274,7 @@ "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", "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; closing and Open in… never leave the window on screen without its page or UI: where the window fades out its card shows a still of the page as the page leaves, if one is taken within 150 ms (macOS, Windows; not run here), elsewhere the window leaves the screen first, whole"}, + "MW-15": {"built": "yes", "feel": true, "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; closing and Open in… never leave the window on screen without its page or UI: where the window fades out its card shows a still of the page as the page leaves, if one is taken within 150 ms (macOS, Windows; not run here), elsewhere the window leaves the screen first, whole"}, "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", "feel": true, "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"}, "DT-01": {"built": "yes", "feel": true, "note": "⌥⌘I / F12 (Ctrl+Shift+I / F12) open DevTools for the selected tab or focused split pane, docked inside its view, and close them, also from inside DevTools; a Peek's or Mini Window's page gets its own. ⌥⌘J (Ctrl+Shift+J) opens them on the Console; View → Developer menu (macOS) and the command bar. A key held down acts once. Copy, paste (after DevTools' own paste warning) and the device toolbar work; links DevTools open become tabs. Windows/Linux: while DevTools have the keyboard, only the DevTools keys reach Orbit"}, @@ -361,23 +361,23 @@ "FL-25": {"built": "yes", "note": "folder search and icon picker: drawn in the first frame, from 0.97 with the snappy timing (150 ms); close with the quick fade; keys go back to the sidebar at once"}, "FL-28": {"built": "yes", "note": "follows the OS setting, with a setting to force it on (AP-17); springs, slides and scales become short fades or instant changes, toasts included, also for what is already on screen when it turns on; a unit guard checks that every motion component sits under the config or reads the setting, and that every element with a layout animation, an entrance, an exit, a press or a moving animate asks useReducedMotion(), since motion reads the config only as an element mounts; drags still follow the pointer and settle with a short ease"}, "AP-17": {"built": "partial", "note": "one set of motion tokens for every surface (FL-01): ease-out curves of 90–270 ms and springs of 0.25–0.32 s; buttons and the × press to 0.9 (FL-08); the loading pill at the top centre of the card (FL-23); reduced motion from the OS setting or Orbit's own switch in Appearance, turning slides, flights and springs into short fades (FL-28). No shimmer when the app starts"}, - "BO-01": {"built": "yes", "note": "New Tweak… from the command bar (also \"customize\", \"customization\", \"site style\"), the sidebar's + menu, the site controls panel, a paintbrush in the address field (shown on hover, in the accent colour once the site has a Tweak on) and the tab's menu (Tweak this Site…); none bound to a key. Opens the editor over the page with a new Tweak named after the host; a second one adds a second Tweak. A one-screen intro the first time only. Internal pages, error pages, a Peek and a Mini Window take none: the entries are greyed or gone, or say why. A Mini Window has no Tweak entry of its own; a key given to one there says why in the browser window, as a Mini Window shows no notes (with no browser window open, nothing shows). Nothing reloads"}, - "BO-02": {"built": "yes", "note": "a small view of its own over the page's top-right corner, sized to the panel, draggable by its title and kept inside the page; the page around it stays live. Every change to the look shows at once, while the page is still loading too, and is saved (JavaScript runs with BO-11's Run Now, or the next load); Esc, the close button and Done close it and give the page the keyboard back; the site controls' › and Edit Tweak reopen it as it was. Another page of the site keeps it open; another site or tab closes it; it steps aside for a Peek or full screen. No request names the site. Top to bottom: the name and its ▾ menu (BO-12), the colour area (BO-03 to BO-06), fonts (BO-07), size (BO-08), text case (BO-09), Hide elements (BO-10), the code area (BO-11, which widens the panel to the left), \"Also apply to subdomains\" (BO-17) and Done. Dragging the wheel recolours the page as fast as the page could itself (BO-03 has the numbers)"}, - "BO-03": {"built": "yes", "note": "a wheel with two linked handles: the big one anywhere inside (its angle the hue, its distance how strong), the small one on the rim (the accent: it turns the page's own colours by its distance from the main hue, and turns with the big one). One filter on the page root tints it: white becomes a light tint of the hue, black stays black, text keeps its contrast; images and video keep their colours (an inverse filter), the top layer is tinted too. Arrow keys move the focused handle (⇧ further). Measured with the frame sampler against the page changing the same colours itself, one a frame: the drag keeps up, and main takes an edit for every move (60 of 60, one a frame; an editor that took every other move fails). Under a virtual display with no GPU, 60 a second at 1024×768 (slowest frame 17 ms); at 1440×900 the page itself manages only about 35 a second there, and the drag matches it"}, - "BO-04": {"built": "yes", "note": "the lightbulb next to the wheel inverts lightness and keeps hues (white page dark, text light), with the tint on the inverted result; photos and video keep their own look; again restores it"}, - "BO-05": {"built": "yes", "note": "the sliders button opens Contrast, Brightness and Original saturation, live as they move; each has a marked neutral middle (100%) that changes nothing, and a double-click puts it back there"}, - "BO-06": {"built": "yes", "note": "the circle-with-a-cross button puts the wheel, invert and sliders back to neutral, so the page shows its own colours; fonts, size, case, hidden elements and code stay"}, + "BO-01": {"built": "yes", "feel": true, "note": "New Tweak… from the command bar (also \"customize\", \"customization\", \"site style\"), the sidebar's + menu, the site controls panel, a paintbrush in the address field (shown on hover, in the accent colour once the site has a Tweak on) and the tab's menu (Tweak this Site…); none bound to a key. Opens the editor over the page with a new Tweak named after the host; a second one adds a second Tweak. A one-screen intro the first time only. Internal pages, error pages, a Peek and a Mini Window take none: the entries are greyed or gone, or say why. A Mini Window has no Tweak entry of its own; a key given to one there says why in the browser window, as a Mini Window shows no notes (with no browser window open, nothing shows). Nothing reloads"}, + "BO-02": {"built": "yes", "feel": true, "note": "a small view of its own over the page's top-right corner, sized to the panel, draggable by its title and kept inside the page; the page around it stays live. Every change to the look shows at once, while the page is still loading too, and is saved (JavaScript runs with BO-11's Run Now, or the next load); Esc, the close button and Done close it and give the page the keyboard back; the site controls' › and Edit Tweak reopen it as it was. Another page of the site keeps it open; another site or tab closes it; it steps aside for a Peek or full screen. No request names the site. Top to bottom: the name and its ▾ menu (BO-12), the colour area (BO-03 to BO-06), fonts (BO-07), size (BO-08), text case (BO-09), Hide elements (BO-10), the code area (BO-11, which widens the panel to the left), \"Also apply to subdomains\" (BO-17) and Done. Dragging the wheel recolours the page as fast as the page could itself (BO-03 has the numbers)"}, + "BO-03": {"built": "yes", "feel": true, "note": "a wheel with two linked handles: the big one anywhere inside (its angle the hue, its distance how strong), the small one on the rim (the accent: it turns the page's own colours by its distance from the main hue, and turns with the big one). One filter on the page root tints it: white becomes a light tint of the hue, black stays black, text keeps its contrast; images and video keep their colours (an inverse filter), the top layer is tinted too. Arrow keys move the focused handle (⇧ further). Measured with the frame sampler against the page changing the same colours itself, one a frame: the drag keeps up, and main takes an edit for every move (60 of 60, one a frame; an editor that took every other move fails). Under a virtual display with no GPU, 60 a second at 1024×768 (slowest frame 17 ms); at 1440×900 the page itself manages only about 35 a second there, and the drag matches it"}, + "BO-04": {"built": "yes", "feel": true, "note": "the lightbulb next to the wheel inverts lightness and keeps hues (white page dark, text light), with the tint on the inverted result; photos and video keep their own look; again restores it"}, + "BO-05": {"built": "yes", "feel": true, "note": "the sliders button opens Contrast, Brightness and Original saturation, live as they move; each has a marked neutral middle (100%) that changes nothing, and a double-click puts it back there"}, + "BO-06": {"built": "yes", "feel": true, "note": "the circle-with-a-cross button puts the wheel, invert and sliders back to neutral, so the page shows its own colours; fonts, size, case, hidden elements and code stay"}, "BO-07": {"built": "partial", "note": "eight \"Aa\" presets, each in its own typeface: Sans, Serif, Classic, Humanist, Rounded, Mono, Typewriter and Hand. One applies to all the site's text but icon fonts, glyphs hidden from screen readers, code and math; the active one again gives the site its fonts back. The presets are font stacks of what the system has, so nothing is bundled and their look varies by system (Rounded and Hand fall back on Linux); shipping freely licensed fonts is open"}, - "BO-08": {"built": "yes", "note": "− and + step the page through 90, 100, 110, 125 and 150%, saved with the Tweak, so every visit gets it; it multiplies with the tab's own zoom (⌘+ / ⌘−) and leaves that alone"}, - "BO-09": {"built": "yes", "note": "one button cycles As written, lowercase, UPPERCASE, Capitalized Words and back; fields, editable text and code keep their own case"}, + "BO-08": {"built": "yes", "feel": true, "note": "− and + step the page through 90, 100, 110, 125 and 150%, saved with the Tweak, so every visit gets it; it multiplies with the tab's own zoom (⌘+ / ⌘−) and leaves that alone"}, + "BO-09": {"built": "yes", "feel": true, "note": "one button cycles As written, lowercase, UPPERCASE, Capitalized Words and back; fields, editable text and code keep their own case"}, "BO-10": {"built": "yes", "note": "the lightning button: the element under the pointer gets an outline with its kind and size, and the page's links and buttons don't act: the picker stops each click and press on its way to them, except the page's own listeners on its window, which can still hear them first; and if the page takes the picker's layer away, the layer comes back (a page that keeps taking it ends the mode); a click picks. The panel then offers Hide this, or Hide all similar with a slider from narrow to broad, counting what it would hide, and how many of those show now where some don't (\"5 (4 shown)\"), and outlining those in the page. Gone at once and saved as plain CSS selectors, so hidden from the first frame on every page of the site where it matches. Done or Esc leaves the mode (Esc drops a pick first); another document ends it. On a page still loading, the mode starts when the load ends: a click on the button shows it starting at once, the button pressed and the panel saying it starts when the page has loaded, and the page acts as usual until then; Done or Esc, in the panel or the page, cancels the start (the page still gets its Esc), and another document ends it. The picker runs in an isolated world, never the page's own. Each hide has a restore marker in the panel (not over the page's corner): hovered it shows the element again, clicked it brings it back"}, - "BO-11": {"built": "yes", "note": "the { } button opens a code area with CSS and JavaScript tabs, empty to start. CSS applies as it is typed (about 90 ms from the keystroke to the page's frame, measured) and is saved as typed, sent before the panel closes; a reload keeps it. JavaScript runs in the page's own world once per load at document ready, from main; Run now runs it at once, Reload page loads the page again. The editor: monospace, syntax colouring (text only, never markup), line numbers, the platform's undo (⌘Z / Ctrl+Z through Orbit's key handling), Tab and ⇧Tab indent and outdent, Enter keeps the indentation. Errors, thrown or syntax, go to the page's console; no dialog, nothing in main. JavaScript from elsewhere stays off until Allow on this device, and editing it doesn't allow it. A JS badge in the site controls list and the code tab (the Library's list comes with BO-15)"}, - "BO-12": {"built": "yes", "note": "the title is the Tweak's name with a ▾ menu: Rename… edits it in place (Enter keeps, Esc cancels), Reset all edits… asks first, then clears colours, font, size, case, hidden elements and code; the name and switches stay. The menu works from the keyboard; Esc closes it, not the panel"}, - "BO-13": {"built": "yes", "note": "Shuffle in the name ▾ menu applies a random look live: any hue at a moderate strength, an accent a pleasing step away, sometimes inverted, one of the fonts, the site's size or a little larger, mostly the site's case. The menu stays open to shuffle again; hidden elements and code are never touched"}, - "BO-14": {"built": "yes", "note": "the site controls panel lists the site's Tweaks by name with a paintbrush, coloured when on and grey when off; the name turns one off or on at once, without a reload, while the page is still loading too, each on its own, keeping all its settings; the › opens it in the editor. The address field's paintbrush shows in the accent colour while one is on and grey when all are off, and turns them all off or on. Off, a Tweak's styles leave the page at once; what its JavaScript already did stays until the page loads again"}, - "BO-15": {"built": "yes", "note": "the Library's Tweaks section lists every Tweak on this device by site, a site's oldest first: its name, its site (where the name doesn't say it, and \"and its subdomains\"), a JS badge where it has JavaScript (struck through where that isn't allowed on this device) and a switch that turns it off or on at once. Right-click or ⋯ (the Library's own menu): Edit shows the site's tab in the Space (never a Peek's or a Mini Window's page), or opens the site, and opens the editor once the page can take it; a site that doesn't load stops the wait as its load fails, with a note saying so; Open Site shows that tab, else opens a new tab where the Tweak was made (scheme, host and port); Rename… in place; Delete… asks first, and its styles leave open pages at once (its JavaScript's effects with their next load). Keys: up and down, Enter edits, Space switches, F2 renames, Delete asks, Shift+F10 opens the menu. The command bar's View Tweaks opens it. With none, a short explanation and Tweak This Site for the page underneath"}, - "BO-16": {"built": "yes", "note": "Settings → Advanced: \"Apply Tweaks on websites I visit\", on by default. Off, every open page loses its Tweaks' styles at once, while still loading too, and the editor closes; what a Tweak's JavaScript already did stays until the page loads again, as the setting says; the address field's paintbrush and the + menu's and the tab menu's New Tweak go; the command bar's says Tweaks are off; the site controls panel and the Library say so, the Library with Turn On. Each Tweak keeps its own on or off for when they are back on. The organisation policy is P2 and not built"}, - "BO-17": {"built": "yes", "note": "a Tweak is for its host's pages: any path, query and in-page navigation, not another host, the parent domain or a subdomain. \"Also apply to subdomains\" (off by default, at the panel's foot) brings in subdomains' open pages at once; it isn't offered for an address by number and is fixed while the page is on a subdomain it brings in. Every profile gets the same Tweaks; in a private window the CSS applies and the JavaScript runs only once \"Also run in private windows\" is ticked. Matching is a map lookup per label of the host"}, + "BO-11": {"built": "yes", "feel": true, "note": "the { } button opens a code area with CSS and JavaScript tabs, empty to start. CSS applies as it is typed (about 90 ms from the keystroke to the page's frame, measured) and is saved as typed, sent before the panel closes; a reload keeps it. JavaScript runs in the page's own world once per load at document ready, from main; Run now runs it at once, Reload page loads the page again. The editor: monospace, syntax colouring (text only, never markup), line numbers, the platform's undo (⌘Z / Ctrl+Z through Orbit's key handling), Tab and ⇧Tab indent and outdent, Enter keeps the indentation. Errors, thrown or syntax, go to the page's console; no dialog, nothing in main. JavaScript from elsewhere stays off until Allow on this device, and editing it doesn't allow it. A JS badge in the site controls list and the code tab (the Library's list comes with BO-15)"}, + "BO-12": {"built": "yes", "feel": true, "note": "the title is the Tweak's name with a ▾ menu: Rename… edits it in place (Enter keeps, Esc cancels), Reset all edits… asks first, then clears colours, font, size, case, hidden elements and code; the name and switches stay. The menu works from the keyboard; Esc closes it, not the panel"}, + "BO-13": {"built": "yes", "feel": true, "note": "Shuffle in the name ▾ menu applies a random look live: any hue at a moderate strength, an accent a pleasing step away, sometimes inverted, one of the fonts, the site's size or a little larger, mostly the site's case. The menu stays open to shuffle again; hidden elements and code are never touched"}, + "BO-14": {"built": "yes", "feel": true, "note": "the site controls panel lists the site's Tweaks by name with a paintbrush, coloured when on and grey when off; the name turns one off or on at once, without a reload, while the page is still loading too, each on its own, keeping all its settings; the › opens it in the editor. The address field's paintbrush shows in the accent colour while one is on and grey when all are off, and turns them all off or on. Off, a Tweak's styles leave the page at once; what its JavaScript already did stays until the page loads again"}, + "BO-15": {"built": "yes", "feel": true, "note": "the Library's Tweaks section lists every Tweak on this device by site, a site's oldest first: its name, its site (where the name doesn't say it, and \"and its subdomains\"), a JS badge where it has JavaScript (struck through where that isn't allowed on this device) and a switch that turns it off or on at once. Right-click or ⋯ (the Library's own menu): Edit shows the site's tab in the Space (never a Peek's or a Mini Window's page), or opens the site, and opens the editor once the page can take it; a site that doesn't load stops the wait as its load fails, with a note saying so; Open Site shows that tab, else opens a new tab where the Tweak was made (scheme, host and port); Rename… in place; Delete… asks first, and its styles leave open pages at once (its JavaScript's effects with their next load). Keys: up and down, Enter edits, Space switches, F2 renames, Delete asks, Shift+F10 opens the menu. The command bar's View Tweaks opens it. With none, a short explanation and Tweak This Site for the page underneath"}, + "BO-16": {"built": "yes", "feel": true, "note": "Settings → Advanced: \"Apply Tweaks on websites I visit\", on by default. Off, every open page loses its Tweaks' styles at once, while still loading too, and the editor closes; what a Tweak's JavaScript already did stays until the page loads again, as the setting says; the address field's paintbrush and the + menu's and the tab menu's New Tweak go; the command bar's says Tweaks are off; the site controls panel and the Library say so, the Library with Turn On. Each Tweak keeps its own on or off for when they are back on. The organisation policy is P2 and not built"}, + "BO-17": {"built": "yes", "feel": true, "note": "a Tweak is for its host's pages: any path, query and in-page navigation, not another host, the parent domain or a subdomain. \"Also apply to subdomains\" (off by default, at the panel's foot) brings in subdomains' open pages at once; it isn't offered for an address by number and is fixed while the page is on a subdomain it brings in. Every profile gets the same Tweaks; in a private window the CSS applies and the JavaScript runs only once \"Also run in private windows\" is ticked. Matching is a map lookup per label of the host"}, "BO-18": {"built": "partial", "note": "the page preload asks main for the site's sheets before the page's first paint (D40) and puts them in as injected sheets (D85), which the page can't read or remove, and which its own layered !important rules can't outrank: no frame shows the page without its Tweak (frame-capture tests on a slow page's hard reload and first visit, on a page that sets its own adopted sheets in its head, and on one with a layered !important background). Main puts in its own copies at document ready, or at the first change before it, then tells the preload to take its copies out, by a message that carries only the document's number and that main never waits for. Survives reloads, back and forward, a reopened tab, a woken tab and a restart. JavaScript runs from main once per load at document ready, main frame only, where allowed (BO-11, BO-17), and checks the page's host itself as it runs: not again on an in-page navigation, never in a frame from another site; what it did stays until the next load. The Space colours come with BO-19"}, "BO-21": {"built": "partial", "note": "in the command bar, none bound to a key: New Tweak… (also \"customize\", \"customization\", \"site style\"), Edit Tweak (the site's one in the editor; with several, the site controls list them to pick from; with none, a note says so), Turn Off and Turn On Tweaks for This Site (with a note), View Tweaks (the Library's list, BO-15). In a Mini Window, View Tweaks opens the Library in the browser window (one that isn't private), and the others say there why its page takes no Tweak; from a Mini Window with no browser window open, they show nothing. Export Tweak… comes with BO-20"}, "FL-29": {"built": "partial", "note": "measured by the feel specs against each budget (docs/FEEL.md); press, ⌘W, ⌘S, Space switch (settles in ~290 ms), command bar and drag budgets met; still missed: p99 at two intervals in some animations under load (⌘S hide, long lists, a Peek opening), the hover sidebar and a Peek's close start in the second frame, and the first ⌘T or ⌘W after a launch can show in the second frame. The rubber band comes back in ~480 ms on FL-13's own spring: the 350 ms settle is for swipes that land on a Space (D70)"}, diff --git a/scripts/capture/record.mjs b/scripts/capture/record.mjs index f7c491e..ce52661 100644 --- a/scripts/capture/record.mjs +++ b/scripts/capture/record.mjs @@ -326,10 +326,14 @@ function startRecorder(file) { return { stop: () => new Promise((done) => { - ff.on('exit', done) + // ffmpeg is stopped if it hasn't finished in 5 s. The timer goes when it exits, so it keeps nothing up. + const timer = setTimeout(() => ff.kill('SIGINT'), 5000) + ff.on('exit', () => { + clearTimeout(timer) + done() + }) ff.stdin.write('q') ff.stdin.end() - setTimeout(() => ff.kill('SIGINT'), 5000) }), } } @@ -455,7 +459,15 @@ async function quitOrbit(app) { electronApp.quit() }) .catch(() => {}) - await Promise.race([exited, sleep(15_000)]) + // Up to 15 s for Orbit to exit. The timer goes when it does: a pending one kept the run up for all 15 s. + let timer + await Promise.race([ + exited, + new Promise((r) => { + timer = setTimeout(r, 15_000) + }), + ]) + clearTimeout(timer) await app.close() } diff --git a/scripts/capture/scenarios/bo.mjs b/scripts/capture/scenarios/bo.mjs new file mode 100644 index 0000000..c050278 --- /dev/null +++ b/scripts/capture/scenarios/bo.mjs @@ -0,0 +1,391 @@ +/** + * Tweaks (BO-01 to BO-14, BO-18, BO-21), about 50 s, for the "feels right" sign-off (D48). One local site: + * New Tweak… from the command bar and its intro; the wheel's big handle dragged round, then its rim handle + * (the page recolours as they move); invert; the advanced sliders; Reset; two fonts; the size; the text + * case through its four steps; Hide-element mode (Hide this, Hide all similar, a restore marker); a CSS rule + * typed into the code area; Done. Then a hard reload, the site controls' toggle off and on, the editor + * reopened from there, and View Tweaks. + * + * 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. + * Drags use the on-screen pointer and its button only (xdotool), as a person's hand would, and print + * `[check]` lines with the values they end on. + */ +import { execFile } from 'node:child_process' + +const SITE = 'Field notes' +let t0 = 0 +const mark = (name) => console.error(`[mark] ${name} ${Math.round(performance.now() - t0)}`) + +async function waitFor(t, test, ms = 8000) { + const end = Date.now() + ms + while (Date.now() < end) { + const v = await test().catch(() => null) + if (v) return v + await t.sleep(40) + } + throw new Error('timed out') +} + +/** The on-screen pointer, moved with xdotool. */ +const xPointer = (x, y) => + new Promise((r) => execFile('xdotool', ['mousemove', String(Math.round(x)), String(Math.round(y))], () => r())) + +/** The on-screen pointer's button, pressed (`mousedown`) or let go (`mouseup`), as a person's. */ +const xButton = (action) => new Promise((r) => execFile('xdotool', [action, '1'], () => r())) + +/** + * A view's page, found by its address, and where its corner is on the display: the window's content + * origin plus the view's place in it (views nest). + */ +async function viewOf(t, match) { + const page = await waitFor(t, async () => t.app.windows().find((p) => match(p.url()))) + const want = page.url() + const origin = await waitFor(t, () => + t.app.evaluate(({ BrowserWindow }, url) => { + const find = (view, ox, oy) => { + const b = view.getBounds() + const x = ox + b.x + const y = oy + b.y + if (view.webContents && view.webContents.getURL() === url) return b.width > 0 ? { x, y } : null + for (const child of view.children ?? []) { + const hit = find(child, x, y) + if (hit) return hit + } + return null + } + for (const w of BrowserWindow.getAllWindows()) { + const c = w.getContentBounds() + const hit = find(w.contentView, c.x, c.y) + if (hit) return hit + } + return null + }, want), + ) + return { page, x: origin.x, y: origin.y } +} + +/** The shell is the window's own page, at its content's corner. */ +async function shellOf(t) { + const c = await t.app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].getContentBounds()) + return { page: t.shell, x: c.x, y: c.y } +} +const overlayOf = (t) => viewOf(t, (u) => u.includes('view=overlay')) +const editorOf = (t) => viewOf(t, (u) => u.includes('view=tweak')) + +/** Moves a page's pointer and the on-screen one together, in eased steps. `on` is the page and its corner. */ +let at = { x: 700, y: 420 } +async function glide(on, x, y, steps = 16) { + const from = at + const to = { x: on.x + x, y: on.y + y } + for (let i = 1; i <= steps; i++) { + const k = 0.5 - 0.5 * Math.cos((Math.PI * i) / steps) + const sx = from.x + (to.x - from.x) * k + const sy = from.y + (to.y - from.y) * k + await on.page.mouse.move(sx - on.x, sy - on.y) + await xPointer(sx, sy) + } + at = to +} + +async function box(locator) { + const b = await locator.boundingBox() + if (!b) throw new Error('not on screen') + return { ...b, cx: b.x + b.width / 2, cy: b.y + b.height / 2 } +} + +/** The pointer comes to rest over a locator. */ +async function hover(on, locator, steps = 16) { + const b = await box(locator) + await glide(on, b.cx, b.cy, steps) +} + +/** A click as a person makes it: the pointer travels there first. */ +async function click(t, on, locator, steps = 16) { + const b = await box(locator) + await glide(on, b.cx, b.cy, steps) + await t.sleep(140) + await on.page.mouse.click(b.cx, b.cy) +} + +/** + * A drag with the on-screen pointer and its button only, through `points` (page coordinates of `on`), + * a step every `ms`. A native control's drag ends at the first move of a pointer with no button held. + */ +async function drag(t, on, points, ms = 16) { + await glide(on, points[0].x, points[0].y, 12) + await t.sleep(140) + await xButton('mousedown') + try { + for (const p of points.slice(1)) { + await xPointer(on.x + p.x, on.y + p.y) + await t.sleep(ms) + } + } finally { + await t.sleep(80) + await xButton('mouseup') + } + const last = points[points.length - 1] + at = { x: on.x + last.x, y: on.y + last.y } +} + +/** A range input's thumb dragged to `to` (0 to 100, along its track). Prints the value it ends on. */ +async function slide(t, on, input, to) { + const b = await box(input) + const [min, max, value] = await input.evaluate((el) => [Number(el.min), Number(el.max), Number(el.value)]) + const x = (v) => b.x + 8 + ((b.width - 16) * v) / 100 + const from = ((value - min) / (max - min)) * 100 + const points = [] + for (let i = 0; i <= 22; i++) { + const k = 0.5 - 0.5 * Math.cos((Math.PI * i) / 22) + points.push({ x: x(from + (to - from) * k), y: b.cy }) + } + await drag(t, on, points, 18) + const name = await input.getAttribute('data-testid') + console.error(`[check] ${name}: ${value} → ${await input.inputValue()} (asked ${to}% of ${min}–${max})`) +} + +/** Points on the editor's wheel, as the Tweaks tests find them: a hue (degrees from the top) and a strength (0 to 1). */ +async function wheelOf(view) { + const b = await box(view.getByTestId('tweak-wheel')) + const rad = (deg) => (deg * Math.PI) / 180 + const point = (deg, r) => ({ x: b.cx + r * Math.sin(rad(deg)), y: b.cy - r * Math.cos(rad(deg)) }) + return { + cx: b.cx, + cy: b.cy, + main: (hue, dist) => point(hue, (b.width / 2 - 16) * dist), + rim: (deg) => point(deg, b.width / 2 - 7), + } +} + +let tab = '' + +/** Unrecorded: the site's page in a tab of its own, loaded. */ +export async function setup(t) { + tab = await t.run('tabs.create', { url: t.fx.url(SITE) }) + await waitFor(t, async () => (await t.state()).tabs[tab]?.loading === false) + await t.sleep(1200) +} + +export default async function (t) { + t0 = performance.now() + const shell = await shellOf(t) + await t.sleep(600) + + // BO-01, BO-21: New Tweak… from the command bar, found by "customize". The editor opens over the page's + // top-right corner with its intro, once. + mark('new-tweak') + await t.key('Mod+T') + let overlay = await overlayOf(t) + await t.sleep(300) + await t.type(overlay.page, 'customize', 100) + await t.sleep(700) + const row = overlay.page.getByTestId('command-result').filter({ hasText: 'New Tweak…' }).first() + await click(t, overlay, row, 14) + let editor = await editorOf(t) + await waitFor(t, () => editor.page.getByTestId('tweak-intro').isVisible()) + await t.sleep(1600) + mark('intro-start') + await click(t, editor, editor.page.getByTestId('tweak-intro-start')) + await waitFor(t, () => editor.page.getByTestId('tweak-wheel').isVisible()) + editor = await editorOf(t) + await t.sleep(900) + + // BO-03: the big handle, from near the middle out to red, then round through yellow and green to blue. + // The page recolours as it moves. + mark('wheel') + let wheel = await wheelOf(editor.page) + const path = [wheel.main(0, 0.1)] + for (let i = 1; i <= 20; i++) path.push(wheel.main(0, 0.1 + (0.55 * i) / 20)) + for (let i = 1; i <= 90; i++) path.push(wheel.main((220 * i) / 90, 0.65)) + await drag(t, editor, path, 18) + await t.sleep(1200) + + // BO-03: the rim handle, a sixth of a turn round from where it sits. + mark('accent') + wheel = await wheelOf(editor.page) + const accent = await box(editor.page.getByTestId('tweak-wheel-accent')) + const from = (Math.atan2(accent.cx - wheel.cx, wheel.cy - accent.cy) * 180) / Math.PI + const turn = [] + for (let i = 0; i <= 40; i++) turn.push(wheel.rim(from + (60 * i) / 40)) + await drag(t, editor, turn, 18) + await t.sleep(1200) + + // BO-04: invert lightness, on and off. + mark('invert') + await click(t, editor, editor.page.getByTestId('tweak-invert')) + await t.sleep(1800) + await click(t, editor, editor.page.getByTestId('tweak-invert'), 8) + await t.sleep(900) + + // BO-05: the advanced sliders, contrast and the page's own saturation. + mark('sliders') + await click(t, editor, editor.page.getByTestId('tweak-sliders')) + await waitFor(t, () => editor.page.getByTestId('tweak-contrast').isVisible()) + editor = await editorOf(t) + await t.sleep(600) + await slide(t, editor, editor.page.getByTestId('tweak-contrast'), 80) + await t.sleep(500) + await slide(t, editor, editor.page.getByTestId('tweak-saturation'), 15) + await t.sleep(1000) + + // BO-06: Reset gives the site its own colours back. + mark('reset-colors') + await click(t, editor, editor.page.getByTestId('tweak-color-reset')) + await t.sleep(1400) + + // BO-07: two fonts, then the site's own again. + mark('font') + await click(t, editor, editor.page.getByTestId('tweak-font-serif')) + await t.sleep(1300) + await click(t, editor, editor.page.getByTestId('tweak-font-mono')) + await t.sleep(1300) + + // BO-08: the size, two steps up. + mark('size') + await click(t, editor, editor.page.getByTestId('tweak-size-up')) + await t.sleep(900) + await click(t, editor, editor.page.getByTestId('tweak-size-up'), 6) + await t.sleep(1300) + + // BO-09: the text case through its four steps, back to the page's own. + mark('case') + for (let i = 0; i < 4; i++) { + await click(t, editor, editor.page.getByTestId('tweak-case'), i === 0 ? 16 : 4) + await t.sleep(900) + } + + // BO-10: Hide-element mode. The outline follows the pointer over the page; a click picks; Hide this. + mark('hide') + await click(t, editor, editor.page.getByTestId('tweak-hide-button')) + await t.sleep(600) + const page = await viewOf(t, (u) => u.startsWith(t.fx.url(SITE))) + const card = (name) => page.page.locator('.card', { hasText: name }) + await hover(page, card('Overview'), 20) + await t.sleep(500) + await hover(page, card('Recent'), 14) + await t.sleep(500) + await hover(page, card('Ideas'), 18) + await t.sleep(400) + await page.page.mouse.click(at.x - page.x, at.y - page.y) + await waitFor(t, () => editor.page.getByTestId('tweak-hide-this').isVisible()) + await t.sleep(700) + mark('hide-this') + await click(t, editor, editor.page.getByTestId('tweak-hide-this')) + await t.sleep(1200) + + // BO-10: Hide all similar, first from a card's heading, which has no class of its own. + mark('hide-similar') + await hover(page, card('Starred').locator('h3'), 18) + await t.sleep(400) + await page.page.mouse.click(at.x - page.x, at.y - page.y) + await waitFor(t, () => editor.page.getByTestId('tweak-hide-similar').isVisible()) + await t.sleep(500) + await hover(editor, editor.page.getByTestId('tweak-hide-similar')) + await t.sleep(1600) + console.error( + `[check] Starred's title: Hide all similar ${(await editor.page.getByTestId('tweak-hide-similar').isEnabled()) ? 'on' : 'off'}, count ${await editor.page.getByTestId('tweak-hide-count').innerText()}`, + ) + await click(t, editor, editor.page.getByTestId('tweak-hide-cancel'), 4) + await t.sleep(700) + // A card's icon, which has a class: the panel counts the six, and Hide all similar takes them all. + mark('hide-similar-icon') + await hover(page, card('Recent').locator('.dot'), 18) + await t.sleep(400) + await page.page.mouse.click(at.x - page.x, at.y - page.y) + await waitFor(t, () => editor.page.getByTestId('tweak-hide-similar').isEnabled()) + await t.sleep(600) + console.error( + `[check] Recent's icon: count ${await editor.page.getByTestId('tweak-hide-count').innerText()}, slider ${(await editor.page.getByTestId('tweak-hide-breadth').isVisible()) ? 'shown' : 'not shown'}`, + ) + await hover(editor, editor.page.getByTestId('tweak-hide-similar')) + await t.sleep(1200) + await click(t, editor, editor.page.getByTestId('tweak-hide-similar'), 4) + await t.sleep(1400) + + // BO-10 (D86): a restore marker in the panel, hovered (the element shows for the moment), then clicked. + mark('restore') + const marker = editor.page.getByTestId('tweak-restore-marker').last() + await hover(editor, marker) + await t.sleep(1300) + await click(t, editor, marker, 4) + await t.sleep(1000) + await click(t, editor, editor.page.getByTestId('tweak-hide-done')) + await t.sleep(900) + + // BO-11: a CSS rule, typed; it applies as soon as it parses. + mark('code') + await click(t, editor, editor.page.getByTestId('tweak-code-button')) + await waitFor(t, () => editor.page.getByTestId('tweak-css-input').isVisible()) + editor = await editorOf(t) + await click(t, editor, editor.page.getByTestId('tweak-css-input')) + await t.type(editor.page, 'h1 { color: #b4235a; letter-spacing: 4px }', 60) + await t.sleep(1500) + + // BO-13: Shuffle, twice, from the name's ▾ menu, which stays open for another. + mark('shuffle') + await click(t, editor, editor.page.getByTestId('tweak-name')) + await waitFor(t, () => editor.page.getByTestId('tweak-menu-shuffle').isVisible()) + await t.sleep(400) + for (let i = 0; i < 2; i++) { + await click(t, editor, editor.page.getByTestId('tweak-menu-shuffle'), i === 0 ? 8 : 3) + await t.sleep(1700) + } + // BO-12: Rename…, from the same menu, typed over the selected name. + mark('rename') + await click(t, editor, editor.page.getByTestId('tweak-menu-rename'), 8) + await waitFor(t, () => editor.page.getByTestId('tweak-name-input').isVisible()) + await t.sleep(300) + await t.type(editor.page, 'Quiet reading', 80) + await editor.page.keyboard.press('Enter') + await t.sleep(1100) + + // BO-02: Done keeps every edit. + mark('done') + await click(t, editor, editor.page.getByTestId('tweak-done')) + await t.sleep(1400) + + // BO-18: a hard reload shows no frame without the Tweak. + mark('reload') + await t.key('Shift+Mod+R') + await t.sleep(2200) + + // BO-17: the Tweak is for the site's pages, so a link to another of them opens with it. + mark('other-page') + await click(t, page, page.page.getByRole('link', { name: 'Updates' })) + await waitFor(t, async () => { + const now = (await t.state()).tabs[tab] + return now?.url.endsWith('/Updates') && !now.loading + }) + await t.sleep(1800) + + // BO-14: the site controls list the site's Tweak; its name turns it off and on, with no reload. + mark('site-controls') + await click(t, shell, shell.page.getByTestId('site-controls-button')) + overlay = await overlayOf(t) + const toggle = overlay.page.getByTestId('site-controls').getByTestId('site-tweak-toggle').first() + await waitFor(t, () => toggle.isVisible()) + await t.sleep(700) + mark('toggle-off') + await click(t, overlay, toggle) + await t.sleep(1600) + mark('toggle-on') + await click(t, overlay, toggle, 4) + await t.sleep(1600) + + // BO-02: the chevron reopens the editor as it was; Esc closes it. + mark('reopen') + await click(t, overlay, overlay.page.getByTestId('site-controls').getByTestId('site-tweak-edit').first()) + editor = await editorOf(t) + await waitFor(t, () => editor.page.getByTestId('tweak-panel').isVisible()) + await t.sleep(1800) + mark('escape') + await editor.page.keyboard.press('Escape') + await t.sleep(1200) + + // BO-15, BO-21: View Tweaks opens the Library on its Tweaks section. + mark('library') + await t.action('tweak.view') + await t.sleep(2600) + mark('end') +} diff --git a/scripts/capture/scenarios/md2.mjs b/scripts/capture/scenarios/md2.mjs new file mode 100644 index 0000000..39fee82 --- /dev/null +++ b/scripts/capture/scenarios/md2.mjs @@ -0,0 +1,130 @@ +/** + * Picture-in-picture's other ways back (MD-01, MD-07; FL-29), about 16 s, for the "feels right" + * sign-off (D48). `md` shows selecting the tab and Back to tab, where the page the window showed stays + * until the video's page has drawn. Here no such page is left: ⌘W on a tab right above the video's tab, + * so that the video's tab comes next (TB-06); selecting it from an empty Space; and selecting its split + * from an empty Space. Its pane shows its blank card until the page has drawn (FL-29 allows that + * placeholder), and never a frame of its player layout. + * + * 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 +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') +})()` + +/** 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') +} + +/** The main window's selected tab, in the Space it shows. */ +const activeTab = async (t) => { + const s = await t.state() + const w = Object.values(s.windows).find((w) => w.kind === 'main') + return w?.activeTab[w.spaceId] ?? null +} + +let video = '' +let home = '' + +/** + * Unrecorded: a tab whose video plays. 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') + home = win.spaceId + // 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) + video = await t.run('tabs.create', { url: t.fx.url('Film club'), spaceId: home }) + await t.sleep(1200) + // 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) + + // A quiet tab opened right above the video's tab: the video moves into the player. ⌘W closes the quiet + // tab and selects the next one, the video's (TB-06), with no page left over its view. + mark('above') + const s = await t.state() + const today = s.spaces[home]?.today ?? [] + await t.run('tabs.create', { url: t.fx.url('Notes'), spaceId: home, index: today.indexOf(video) }) + await waitFor(t, () => player(t)) + await t.sleep(1500) + mark('close-above') + await t.key('Mod+W') + await waitFor(t, async () => !(await player(t))) + await t.sleep(1600) + + // An empty Space: the video moves into the player. Selecting its tab goes back to its Space. + mark('empty-space') + await t.run('spaces.create', { name: 'Empty' }) + await waitFor(t, () => player(t)) + await t.sleep(1500) + mark('back-from-empty') + await t.run('tabs.activate', { tabId: video }) + await waitFor(t, async () => !(await player(t)) && (await activeTab(t)) === video) + await t.sleep(1600) + + // The video's tab in a split with a quiet tab, then an empty Space, and back to the split. + mark('split') + const other = await t.run('tabs.create', { url: t.fx.url('Reading list'), spaceId: home, activate: false }) + await t.run('split.create', { tabIds: [video, other], focus: video }) + await t.sleep(1800) + mark('split-empty-space') + await t.run('spaces.create', { name: 'Empty 2' }) + await waitFor(t, () => player(t)) + await t.sleep(1500) + mark('back-to-split') + await t.run('tabs.activate', { tabId: video }) + await waitFor(t, async () => !(await player(t)) && (await activeTab(t)) === video) + await t.sleep(1600) + mark('end') +} diff --git a/scripts/capture/scenarios/sb2.mjs b/scripts/capture/scenarios/sb2.mjs index c4bf187..31fd22a 100644 --- a/scripts/capture/scenarios/sb2.mjs +++ b/scripts/capture/scenarios/sb2.mjs @@ -140,14 +140,17 @@ const NAMES = [ 'Yoga', ] -export default async function (t) { - t0 = performance.now() +// Made in setup, before the recording starts; the clip uses them. +let sp +const ids = {} + +/** + * A long Today, a page of files and two Favorites, each tab shown once so it has its title and icon (a tab + * never shown doesn't load). The newest is on top. + */ +export async function setup(t) { const s0 = await t.state() - const sp = s0.spaces[s0.spaceOrder[0]] - const [main] = await windowIds(t) - // A long Today, a page of files and two Favorites, each tab shown once so it has its title and icon (a - // tab never shown doesn't load), made before the clip has much to show. The newest is on top. - const ids = {} + sp = s0.spaces[s0.spaceOrder[0]] const shown = async (id, name) => { await t.run('tabs.activate', { tabId: id }) await waitFor(t, async () => { @@ -172,6 +175,11 @@ export default async function (t) { await shown(ids.Files, 'Field kit') ids.Docs = await t.run('tabs.create', { url: t.fx.url('Docs'), spaceId: sp.id, activate: false }) await shown(ids.Docs, 'Docs') +} + +export default async function (t) { + t0 = performance.now() + const [main] = await windowIds(t) await scrollRests(t) await glide(t, 760, 430, 4) await t.sleep(1500) diff --git a/scripts/capture/scenarios/sp.mjs b/scripts/capture/scenarios/sp.mjs index c5be4b7..12e6c50 100644 --- a/scripts/capture/scenarios/sp.mjs +++ b/scripts/capture/scenarios/sp.mjs @@ -71,13 +71,15 @@ async function restingOn(t, spaceId) { }) } -export default async function (t) { - t0 = performance.now() - const s0 = await t.state() - const home = s0.spaceOrder[0] +// Made in setup, before the recording starts; the clip uses them. +let home +let work +const tabs = {} + +/** Personal and Work, each tab shown once so it has its title and icon (a tab never shown doesn't load). */ +export async function setup(t) { + home = (await t.state()).spaceOrder[0] await t.run('spaces.update', { spaceId: home, name: 'Personal' }) - // Each tab is shown once, so it has its title and icon (a tab never shown doesn't load). - const tabs = {} const shown = async (id, name) => { await t.run('tabs.activate', { tabId: id }) await waitFor(t, async () => { @@ -85,7 +87,7 @@ export default async function (t) { return tab?.title === name && !tab.loading }) } - const work = await t.run('spaces.create', { name: 'Work', activate: false }) + work = await t.run('spaces.create', { name: 'Work', activate: false }) for (const n of ['Wiki', 'Tracker']) { tabs[n] = await t.run('tabs.create', { url: t.fx.url(n), spaceId: work, activate: false }) await shown(tabs[n], n) @@ -94,6 +96,10 @@ export default async function (t) { tabs[n] = await t.run('tabs.create', { url: t.fx.url(n), spaceId: home, activate: false }) await shown(tabs[n], n) } +} + +export default async function (t) { + t0 = performance.now() await restingOn(t, home) await glide(t, 700, 420, 4) await t.sleep(1500) diff --git a/scripts/capture/scenarios/sp2.mjs b/scripts/capture/scenarios/sp2.mjs index c7ebe4b..742f4ce 100644 --- a/scripts/capture/scenarios/sp2.mjs +++ b/scripts/capture/scenarios/sp2.mjs @@ -86,16 +86,20 @@ async function slide(t, on, input, to) { await glide(t, x(value), b.cy, 10, on) await t.sleep(120) await xButton('mousedown') - const from = at - const end = { x: on.x + x(to), y: on.y + b.cy } - for (let i = 1; i <= 22; i++) { - const k = 0.5 - 0.5 * Math.cos((Math.PI * i) / 22) - await xPointer(from.x + (end.x - from.x) * k, from.y + (end.y - from.y) * k) - await t.sleep(16) + // The button comes back up even if a step throws, so a failed run never leaves it held on the screen. + try { + const from = at + const end = { x: on.x + x(to), y: on.y + b.cy } + for (let i = 1; i <= 22; i++) { + const k = 0.5 - 0.5 * Math.cos((Math.PI * i) / 22) + await xPointer(from.x + (end.x - from.x) * k, from.y + (end.y - from.y) * k) + await t.sleep(16) + } + at = end + await t.sleep(100) + } finally { + await xButton('mouseup') } - at = end - await t.sleep(100) - await xButton('mouseup') const name = await input.getAttribute('data-testid') console.error(`[check] ${name}: ${value} → ${await input.inputValue()} (asked ${to})`) } @@ -181,17 +185,19 @@ const focusEvent = (t, kind) => for (const w of BrowserWindow.getAllWindows()) if (w.webContents.getURL().includes('view=shell')) w.emit(k) }, kind) -export default async function (t) { - t0 = performance.now() - const s0 = await t.state() - const home = s0.spaceOrder[0] - const [main] = await windowIds(t) - const overlay = await uiPage(t, 'overlay', main) - const onOverlay = { page: overlay, x: 0, y: 0 } +// Made in setup, before the recording starts; the clip uses them. +let home +let client +let work +const tabs = {} + +/** + * Two Favorites in the Default profile; Personal, then Client, then Work, each with tabs shown once so they + * have their titles and icons (a tab never shown doesn't load). + */ +export async function setup(t) { + home = (await t.state()).spaceOrder[0] await t.run('spaces.update', { spaceId: home, name: 'Personal' }) - // Two Favorites in the Default profile; Personal, then Client, then Work, each with tabs shown once so they - // have their titles and icons (a tab never shown doesn't load), made before the clip has much to show. - const tabs = {} const shown = async (id, name) => { await t.run('tabs.activate', { tabId: id }) await waitFor(t, async () => { @@ -199,8 +205,8 @@ export default async function (t) { return tab?.title === name && !tab.loading }) } - const client = await t.run('spaces.create', { name: 'Client', activate: false, afterSpaceId: home }) - const work = await t.run('spaces.create', { name: 'Work', activate: false, afterSpaceId: client }) + client = await t.run('spaces.create', { name: 'Client', activate: false, afterSpaceId: home }) + work = await t.run('spaces.create', { name: 'Work', activate: false, afterSpaceId: client }) for (const [n, space] of [ ['Wiki', work], ['Tracker', work], @@ -219,12 +225,19 @@ export default async function (t) { tabs[n] = await t.run('tabs.create', { url: t.fx.url(n), spaceId: home, activate: false }) await shown(tabs[n], n) } +} + +export default async function (t) { + t0 = performance.now() + const [main] = await windowIds(t) + const overlay = await uiPage(t, 'overlay', main) + const onOverlay = { page: overlay, x: 0, y: 0 } await restingOn(t, home) await glide(t, 700, 420, 4) await t.sleep(1500) - // SP-04, SB-06: hovering the title row shows the … button, which opens the Space menu; a right-click on - // another Space's icon opens its menu without leaving this Space. + // SP-04, SB-06: hovering the title row shows the … button, which opens the Space menu. SP-07: a + // right-click on another Space's icon in the switcher opens its menu without leaving this Space. mark('space-menu') await openSpaceMenu(t, overlay) for (const id of ['theme', 'icon', 'profile', 'rename', 'manage', 'delete']) { diff --git a/scripts/capture/scenarios/tb.mjs b/scripts/capture/scenarios/tb.mjs index 25d8b04..aeb76ee 100644 --- a/scripts/capture/scenarios/tb.mjs +++ b/scripts/capture/scenarios/tb.mjs @@ -101,12 +101,15 @@ const keyEvents = (t, events) => } }, events) -export default async function (t) { - t0 = performance.now() +// Made in setup, before the recording starts; the clip uses them. +const ids = {} + +/** + * Five Today tabs and two Favorites, each shown once so it has its title and icon (a tab never shown + * doesn't load). + */ +export async function setup(t) { const sp = await space(t) - // Five Today tabs and two Favorites, each shown once so it has its title and icon (a tab never shown - // doesn't load), made before the clip has much to show. - const ids = {} const shown = async (id, name) => { await t.run('tabs.activate', { tabId: id }) await waitFor(t, async () => { @@ -123,6 +126,10 @@ export default async function (t) { ids[n] = await t.run('tabs.create', { url: t.fx.url(n), spaceId: sp.id, activate: false }) await shown(ids[n], n) } +} + +export default async function (t) { + t0 = performance.now() await settled(t) await glide(t, 700, 420, 4) await t.sleep(1500) diff --git a/scripts/capture/scenarios/tb2.mjs b/scripts/capture/scenarios/tb2.mjs index 08da00f..5fb8a31 100644 --- a/scripts/capture/scenarios/tb2.mjs +++ b/scripts/capture/scenarios/tb2.mjs @@ -158,13 +158,18 @@ const newTabTitled = async (t, title, known) => { return Object.keys(s.tabs).find((id) => s.tabs[id].title === title && !known.has(id)) } -export default async function (t) { - t0 = performance.now() +// Made in setup, before the recording starts; the clip uses them. +let sp +let folder +const ids = {} + +/** + * Three Favorites, two pinned tabs, a pinned folder of three and six Today tabs, each shown once so it has + * its title and icon (a tab never shown doesn't load). + */ +export async function setup(t) { const s0 = await t.state() - const sp = s0.spaces[s0.spaceOrder[0]] - // Three Favorites, two pinned tabs, a pinned folder of three and six Today tabs, each shown once so it - // has its title and icon (a tab never shown doesn't load), made before the clip has much to show. - const ids = {} + sp = s0.spaces[s0.spaceOrder[0]] const shown = async (id, name) => { await t.run('tabs.activate', { tabId: id }) await waitFor(t, async () => { @@ -190,7 +195,7 @@ export default async function (t) { ids[n] = await t.run('tabs.create', { url: t.fx.url(n), spaceId: sp.id, activate: false }) await shown(ids[n], n) } - const folder = await t.run('folders.create', { + folder = await t.run('folders.create', { container: { kind: 'pinned', spaceId: sp.id }, name: 'Projects', items: [ids.Alpha, ids.Beta, ids.Gamma], @@ -199,6 +204,10 @@ export default async function (t) { ids[n] = await t.run('tabs.create', { url: t.fx.url(n), spaceId: sp.id, activate: false }) await shown(ids[n], n) } +} + +export default async function (t) { + t0 = performance.now() await settled(t) await glide(t, 700, 420, 4) await t.sleep(1500)