A new tab page built from your bookmarks bar. Every top-level folder becomes a full-width row, and inside it each bookmark is a mini card: site logo on the left, title and domain on two lines to its right. Links from nested subfolders are pulled into their folder's row.
The Mono theme, with sample bookmarks. Bookmarks lying loose on the bar get a row of their own, the count sits at the right end of every header, and the dashed bar at the bottom creates a folder.
Titles are cleaned for display: 169 · Inbox — Yandex.Mail shows up as Inbox. The browser's
own bookmarks are never rewritten.
Cards can be dragged with the mouse, and folders can be renamed, reordered, created and deleted right on the page — see below.
The extension makes no network requests at all: no analytics, no favicons fetched from third parties.
Chrome and Firefox, Manifest V3.
Five visual languages. The switch is an iOS-style segmented control pinned to the top right
corner: five labelled segments Mono · One UI · Clay · Dark · Grid, the active one lit by a
sliding pill, any theme one click away. Left and right arrows walk the row from the keyboard and
stop at both ends.
The choice is kept in localStorage and applied before the first paint, so a new tab never
flashes the wrong theme — and the extension needs no storage permission.
| Theme | Look |
|---|---|
| Monochrome (default) | black canvas, white hairlines, the hovered row inverts |
| One UI | light, white cards, generous radii, blue accent |
| Clay | pale lilac, soft volumes with a double highlight |
| One UI Dark | the same language at night, blue accent on near-black |
| Night Grid | a measuring grid behind translucent rows, monospace |
A card can be dragged inside its row to reorder it, or into another row to move the bookmark to a different folder. A line marks the insertion point and the target folder is outlined. Dropping onto empty space in a row appends the bookmark to that folder.
This rewrites the browser's real bookmarks, not just the picture on screen: bookmarks.move
does the work. Hence the undo — Cmd+Z (or Ctrl+Z) puts the bookmark back where it was. The
undo stack lives until the tab is reloaded.
One rule: dropping onto a card means taking its place. The point inside the card does not matter, neither does the direction — the neighbour simply yields its position and shifts aside. So a move two slots forward lands exactly two slots forward, and a round trip returns the bookmark where it started.
If a bookmark lived in a nested subfolder, it lands wherever its new neighbour lives — the layout flattens subfolders, and this is the only rule that never yanks a bookmark out of its subfolder unexpectedly.
After a move the neighbours glide to their new places (FLIP: only transform and opacity are
animated) and the arriving card flashes briefly, so it is obvious where it landed. With the
system "reduce motion" setting on, the animation is skipped.
A cross appears at the right edge of the card under the pointer and deletes the bookmark on one click, with no question asked. It keeps its place whether it shows or not, so a title is cut at the same point either way and nothing shifts under the cursor.
Nothing is destroyed silently: the message at the bottom offers Undo, and Cmd+Z does the same. Undo cannot resurrect a deleted bookmark, so it builds an identical one at the position the old one held — same folder, same slot, same title and address, new internal id.
Four actions, all of them changing the real bookmarks and all of them undoable with Cmd+Z:
| Action | How |
|---|---|
| Rename | click the folder name; Enter commits, Escape restores |
| Reorder | drag the row itself — anywhere outside a card counts as the handle |
| Create | the wide bar under the list; the new folder opens its name field at once |
| Delete | the bin in the header, which appears when the pointer is over the row |
Deleting never destroys links: they move into the last folder, and a message at the bottom says how many went where. Deleting the last folder hands them to the one above it, which then becomes last. A new folder appears at the end of the bar, so a folder created and then thought better of leaves its bookmarks next door rather than at the top of the screen.
A folder is only removed once it is empty, so if there is nowhere to hand the links over — a single folder on the whole bar — nothing is deleted and the message says why. The same goes for a folder holding a subfolder: rows show links only, so the message names what is in the way.
The message carries an Undo button for eight seconds, and Cmd+Z does the same afterwards. Undoing a deletion recreates the folder at its old position and moves the links back into it; the name and the contents match, but the folder's internal id is a new one.
The row that holds bookmarks lying loose on the bar is not a real folder: it cannot be renamed, dragged or deleted.
In Chrome they come from the browser's own cache through the _favicon API — hence the
favicon permission in manifest.chrome.json. Not a single network request: the icons already
live in the profile.
Firefox has no such API, so it shows the first letter of the domain on a plate instead. The
detection reads the manifest (src/favicon.ts) rather than sniffing the user agent. If Chrome
fails to hand back an icon, onerror swaps in the same letter.
Permissions: bookmarks, plus favicon in the Chrome build.
npm install
npm test # pure logic: layout + avatar + title + theme + dnd + folders
npm run build # -> dist/chrome
npm run build:firefox # -> dist/firefox
npm run build:all # both
npm run dev # rebuild dist/chrome on every saveEach browser gets its own folder, because each needs its own manifest: Firefox refuses the
favicon permission outright, and Chrome has no use for gecko.id. One shared folder meant the
last build silently won.
npm run dev only rebuilds dist/chrome. To see the changes, hit "Reload" on the extension card
in chrome://extensions and open a new tab.
npm run buildchrome://extensions→ enable Developer mode- Load unpacked → pick the
dist/chromefolder - Open a new tab
Do not rename or move the project folder afterwards: the id of an unpacked extension is derived from the absolute path. A different path means a different id, and the browser treats it as another extension.
Temporarily, until the browser restarts:
npm run build:firefoxabout:debugging#/runtime/this-firefox→ Load Temporary Add-on- Pick
dist/firefox/manifest.json
The add-on disappears when Firefox restarts: that is what a temporary add-on is.
npm run build:all && npm run packProduces build/purrtab-chrome.zip and build/purrtab-firefox.zip by zipping each build folder
as it stands. pack never touches a manifest, so neither archive can end up with the other
browser's one.
src/
types.ts bookmark tree, column and theme types
browser.ts the only place that knows chrome.* from browser.*
bookmarks.ts finds the bookmarks bar among the roots
layout.ts tree -> columns (pure function, covered by tests)
title.ts cleans a bookmark title (pure function, covered by tests)
avatar.ts domain -> letter and colour (pure function, covered by tests)
favicon.ts favicon address from the browser cache, or null
theme.ts five themes, their order and the remembered choice
dnd.ts drop target maths (pure functions) and drag handlers
folders.ts folder maths: delete plan, receiver, folder drop target (pure functions)
flip.ts cards gliding to their new places after a reorder
render.ts columns -> DOM
toast.ts the message at the bottom, with an undo button when there is something to undo
scripts/
make-icons.mjs dependency-free PNG icon generator
manifest.mjs picks the manifest per browser
pack.mjs builds the zip archives
Two manifests, not one: Chrome warns about the unknown browser_specific_settings key, and
Firefox without gecko.id loses its stable identifier when signed.
Search, a settings screen, Top Sites, expanding nested folders as a tree, deleting a folder together with its links, deleting several bookmarks at once, reacting to bookmark changes made in another window.
