# Chrome extension

Surface. Prototype: previews/ExtensionView.html

The Chrome extension, prototyped: the toolbar button (or the page's right-click menu) opens Visualli in Chrome's **side panel** beside the page you're reading — the same visual browsing as the MCP card, no vault or chat — with *Open in Visualli* to continue the journey.
- **Two ways in:** the toolbar button (tooltip "Visualize this page", shortcut Alt+Shift+V; clicking it again closes the panel) and the page's right-click menu (**Visualize this page**). No selection mapping — the whole page is always the source.
- **Why the side panel:** `chrome.sidePanel` works on every supported Chrome (114+, opening on click from 116) and stays put as you scroll. Chrome draws the panel's header (icon, *Visualli*, pin, ✕) and lets people resize it; our content starts below that header, so `ExtensionPanel` is rendered without its own brand mark or close button.
- **The panel** (`ExtensionPanel`): the map's title, the source (favicon, domain, reading time) and **Open in Visualli** (primary — saves the map to the vault and opens the app). Body: *Reading thedailyreader.example…* with the growing tree while ideas bloom in, then the `VisualMap`; *Couldn't read this page* with *Try again*.
- Inside the map everything browsing does works: peek, terms, Go deeper (sheet layout at this width), Step inside, Map it. Content is read-only here (no edit, no moving) — arranging and editing continue in the app.
- Try the narrow width (320px, Chrome's minimum) — the header and map still fit. The browser chrome and the news site in this prototype are deliberately generic and fictitious.


## Features

- VI-001 Visualize this page (toolbar, right-click) · Open in Visualli: UX ready
- VI-002 Map: bloom, step inside, depth trail: UX ready
- VI-003 Touch: tap to peek, tap again to step inside: N/A (Chrome on desktop uses the mouse; the map switches to touch by itself on touchscreens)
- VI-004 SemanticPeek (an idea's summary): UX ready
- VI-005 SemanticAnchor (term definitions): UX ready
- VI-006 Go deeper (facets for an idea or term): UX ready
- VI-007 Map it (inside this map / new Visualli): UX ready
- VI-008 Movable ideas and Reset layout: N/A (Page maps are previews; arrange them after Open in Visualli)
- VI-009 Edit mode (ideas, connectors, groups): N/A (Read-only beside the page; edit after Open in Visualli)
- VI-010 Sources panel: N/A (The page beside the map is the source)
- VI-011 Export: single-page PDF, multi-page PDF, .visualli file (no share link): N/A (Open in Visualli, then export from the app)
- VI-012 Chat: Composer and receipts: N/A (Continue the conversation after Open in Visualli)
- VI-013 Vault (library of Visuallis): N/A (Open in Visualli saves the map to the vault)
- VI-014 Comfort & appearance: N/A (Uses the comfort settings saved in the person's Visualli account (synced))
- VI-015 Tips in the chat stack (e.g. "Layout saved. Want to edit the content too?"): N/A (Nothing is moved or edited here, so no tips are triggered)

## Build it

The Visualli extension does one thing: **visualize the page you're reading** in Chrome's side panel, with *Open in Visualli* to continue there. Component: `ExtensionPanel`.

### The experience

1. **Toolbar button** — the tree mark. Tooltip "Visualize this page", keyboard shortcut **Alt+Shift+V** (`commands`). Clicking opens the side panel for the current tab; clicking again closes it.
   **Right-click** — one item in the page's context menu (`contexts: ['page']`): **Visualize this page**, with the tree icon. Same result as the button. There is no "visualize selection" — the whole page is always the source.
2. **Reading** — `ExtensionPanel status="reading"`: the growing tree and *Reading ‹domain›…*; ideas bloom in as they're found.
3. **The map** — `status="ready"`: a read-only `VisualMap` with everything browsing does (peek, terms, Step inside, Go deeper, Map it). No edit, no moving, no vault, no composer — like the MCP card.
4. **Open in Visualli** — the one primary action: saves the map to the user's vault and opens it in the app (new tab), where chat, editing and sources continue.
5. **Can't read** — `status="error"`: *Couldn't read this page* (login, paywall, too little text) and *Try again*.

### Why the side panel (and not split view)

- `chrome.sidePanel` is the stable, documented surface: available from **Chrome 114**, `sidePanel.open()` and `setPanelBehavior({ openPanelOnActionClick: true })` from **116**. That covers effectively every Chrome install still receiving updates; newer versions add a resizable panel and keep it per tab.
- Chrome's split view is a user feature with no extension API to open it, so it can't be the primary path. If a user drags the panel into a split, the same `ExtensionPanel` simply gets more room.
- Chrome draws the panel's header (icon, *Visualli*, pin, ✕). Render `ExtensionPanel` **without** `logoSrc` and `onClose` there, so the brand and close aren't doubled. Minimum width is 320px; the header and map are designed to fit.
- **Older Chrome (< 114) or browsers without `sidePanel`:** feature-detect `chrome.sidePanel`; if it's missing, the button opens the same panel page in a popup window (`chrome.windows.create({ type: 'popup', width: 440 })`) placed beside the current window. Same component, same flow.

```js
// background.js (MV3 service worker)
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({ id: 'visualize-page', title: 'Visualize this page', contexts: ['page'] });
});

async function openPopup(tab) {
  const win = await chrome.windows.getCurrent();
  chrome.windows.create({ url: `panel.html?tab=${tab.id}`, type: 'popup', width: 440, height: win.height, left: win.left + win.width - 440, top: win.top });
}

if (chrome.sidePanel?.setPanelBehavior) {
  chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
} else {
  chrome.action.onClicked.addListener(openPopup);
}

chrome.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId !== 'visualize-page') return;
  // sidePanel.open() must run synchronously inside the click (user gesture) — Chrome 116+.
  if (chrome.sidePanel?.open) chrome.sidePanel.open({ tabId: tab.id });
  else openPopup(tab);
});
```

Permissions: `sidePanel`, `contextMenus`, `activeTab`, `scripting` (read the page's text on click — no broad host permissions), `storage`.

### Rules

- **No remote code.** MV3 forbids loading scripts from a CDN. Bundle React and the components into the extension.
- **Ship fonts and CSS locally.** Copy `assets/fonts/` and `tokens/dist/tokens.css` into the extension and reference them with `chrome.runtime.getURL()`. Google Fonts may be blocked by the extension's CSP; the fallbacks in the tokens must still work.
- **The side panel is your own document:** load the tokens normally and set `data-theme` on `<html>`. Nothing is injected into the pages people read; the content script only extracts text when the button is clicked.
- **Respect comfort settings** from the user's Visualli account when available; otherwise follow `prefers-color-scheme` and `prefers-reduced-motion`.
- **Copy:** "Visualize this page" (tooltip and menu item), "Reading ‹domain›…", "Open in Visualli", "Couldn't read this page", "Try again". No emoji, no "AI".
