Add folder placement cards to the popup - #81
Merged
Merged
Conversation
A saved link is now presented as a stack of placements: the My Links card always on top carrying the personal tags, plus one card per shared folder, each with its own folder-scoped tag autocomplete (existing tags or created by name). A dashed "Also add to a shared folder" affordance appends cards from the accessible-folders list, and reopening an already-saved page pre-fills the cards from the lookup's placements. One explicit save sends everything atomically: POST /urls with the placements array for a new link, PATCH /user_bookmarks/:id with the full desired state for an existing one - so removing a card removes that folder placement on save. Requires linkbucket.app to serve the placements API (linkbucket/linkbucket#2991); deploy that first per RELEASING.md. Tom Select handling in tags.js becomes a factory so every card owns an instance; the My Links singleton API is unchanged on top of it.
The heading was plain text, so nothing in the popup carried the actual Linkbucket logotype. The wordmark from the web app replaces the h1 text (kept as the image's alt), making the popup recognizably official at a glance; the skateboarding-bucket illustration stays and becomes decorative for screen readers.
Clicking the add-folder affordance with no addable folders (none exist, all are placed, or the fetch failed) silently did nothing. The button now reads "No folders to add" and stays disabled until a card removal makes a folder addable again.
Closed
The card stack implemented exploration 2b; the chosen design is 2a: destination first, tags second. Step 1 lists every accessible folder as a checkbox row under a locked, always-checked My Links (with a search filter once the list grows); step 2 shows one tag row per checked destination, each row's autocomplete scoped to its own folder. Checking a folder adds its row, unchecking removes it - and on an already-saved link, unchecking removes that placement on save, so the checklist is also the removal UI. Reopening pre-checks the folders the link lives in and notes "Already in your bucket". Same save payload and API as before; folders now load on popup open instead of on demand. My Links stays locked because the backend always saves it - making it uncheckable (folder-only save) would be a small API addition if wanted later.
Reverts the folder-first rework after a hands-on comparison of both directions: the cards keep each folder's tag field inside that folder's card so tags can't land in the wrong scope, the popup costs nothing when folders aren't used (no folder list rendered or fetched on open), a reopened link shows exactly the placements that exist, and removing a placement is a deliberate click on the card instead of an easy-to-miss unchecked box. This reverts commit dd425e0.
The lookup hit was deliberately silent: the only tell was pre-filled tags, and the button copy only changed after saving. With placement cards now pre-filled too, say it outright so the popup reads as "edit what exists" rather than "save a duplicate".
The popup kept its original proportions - 470px wide, 52px inputs, 19px labels - which read oversized next to the exploration mocks. Adopt their scale and palette instead: 400px wide, 38-40px fields, 13px inputs, soft warm borders, deep-indigo ink on the cream background, and fully round tag pills. The existing buttons stay (peach pill submit, underlined key link), only trimmed to the new scale. The header swaps the large skateboarding illustration for a compact official lockup: the bucket mark from the app favicon beside the wordmark.
The winning mockup from the design round: My Links loses its heavy
solid pill for a plain bold name with a checkbox - checked by default,
and unchecking greys the card, hides its tag field (selections survive
a re-check), and saves to folders only via the new my_links API flag.
Save is blocked when nothing is selected at all.
The readonly URL field becomes a page box: the tab's title bold with
the URL beneath, the URL standing in when a page has no title - no new
permissions, the title comes with tabs.query. The add affordance stops
implying folders are shared ("+ Add to folder", then "+ Add to another
folder" once one card exists), and the save button is just "Save".
The compact pass overshot: 13-14px type in 38-40px fields with 10-14px gaps read as cramped. Settle halfway between that and the original proportions - 15px base, 14px inputs, 44px fields, 42px tag controls, 18px section rhythm, and a 420px popup - keeping the compact layout's character without the squeeze.
Chromium draws the text caret at the input's line-box height, so the type-sized line-height left a half-height caret beside the visually taller tag pills. A pixel line-height matching the pills fixes the caret without touching the typed text size.
The line under the actions claimed permanent popup height for a message that only applied to reopened links; the already-saved signal will get a better home. The result area now collapses when idle and appears only for transient save feedback.
The upward list was a leftover from the single-field layout, where the tag field sat at the popup's bottom edge with nothing beneath it. In the card layout it buried the header and clipped at the popup's top. Every field now has the add-folder and actions rows below it - 129px in the tightest state (My Links only) and ~90px more per folder card - so the list opens downward and, on each open, fills the space between the field and the popup's bottom edge, quantized to whole rows with a three-row floor matching that tightest state.
The native select was the one control left outside the popup's design language: OS-drawn menu, chevron pinned to the edge, no search. The picker now mounts the same Tom Select as the tag fields in single mode, so every dropdown shares one look and the folder list is searchable. Unlike the tag fields, the picker sits at the popup's bottom with no room below, so while choosing the body extends to fit the list (the popup window resizes to content) and snaps back on close. The dropdown room-sizing helper moved to tags.js for both controls to share.
The picker's inner search input still inherited the generic form-input chrome (44px height, padding), puffing the single-mode control up past the tag fields - the neutralizing override was multi-only, and the single control's min-height sat 2px above the tag controls' rendered height. Both now land at 42px. Picking the first folder flashed a taller window for a frame: the card was added immediately while the picker teardown was deferred a tick, so card + picker + reserved height stacked briefly, then shrank. Later picks didn't flash only because the popup was already near Chrome's height cap. Card add now happens in the same deferred tick as the teardown, so the popup reflows once.
Whether a link was already saved was invisible: the status line at the bottom was removed, leaving pre-filled tags as the only tell. Per the explorations' already-saved treatment, a reopened link now opens with a soft-green check circle and "Saved N days ago" (from the lookup's new saved_at) between the brand and the page box, and the submit button reads "Save changes" instead of "Save". New links show neither.
"Saving..." and the success message rendered in the result area below the actions, so every save grew the popup window by a row. The whole lifecycle now lives in the button - Saving..., Saved with a check, then back to its idle label - in a fixed-size box so the popup height never moves. The result area below is for errors only, where the extra row is worth it.
The popup painted immediately in its new-link state and mutated as the async work landed: the title, the saved header, and the folder cards each popped into place, shifting everything below. The form now stays hidden until the lookup has composed the final layout and fades in once, complete. A 400ms cap keeps a slow network from holding the popup blank - then late data pops in, the rare case instead of every open.
Four review catches: a rejection in the popup's async init left the form permanently hidden (the reveal now proceeds regardless); removing a folder card mid-save re-enabled the button through the selection guard, which now respects a busy flag; folder-only saves sent the hidden My Links field's tags, creating user tags that attached to nothing; and API errors displayed as raw JSON instead of their error message. The save button label also had two sources of truth - one refresh path owns it now.
Comments that restated the adjacent code are gone; the keepers carry a constraint and got shorter. The setValues comment now says what it actually guards: setValue silently drops ids with no registered option. Also from review: addFolderCard was exported with no external caller, the dropdown row height existed as a bare 38 in three places (now a shared constant), and a small accessibility pass - the save button announces its label changes, the My Links checkbox has an accessible name, and the decorative check glyph is hidden from screen readers.
Reopening a folder-only link and removing its last card left a disabled Save with no explanation - the user's intent (take this link out of the folder) had nowhere to go. On a saved link, deselecting every place now flips the button to "Move to trash": the existing full-state PATCH trashes the personal save and every placement, and the popup then becomes a fresh save form so a change of heart re-saves via POST. A new link with nothing selected keeps the disabled button - there is nothing to do. The selection callbacks now refresh the label too, not just the disabled state.
There was a problem hiding this comment.
Pull request overview
Adds folder placement cards and atomic placement editing to the popup, alongside refreshed saved-link feedback and styling.
Changes:
- Adds folder-scoped cards, tags, and searchable folder selection.
- Supports editing, trashing, and folder-only saves.
- Updates tests, documentation, branding, and release versions.
Reviewed changes
Copilot reviewed 15 out of 18 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/popup/popup.js |
Coordinates placement and save flows. |
src/popup/placements.js |
Implements folder placement cards. |
src/popup/tags.js |
Generalizes tag controls. |
src/popup/utils.js |
Adds folder, payload, date, and error helpers. |
src/popup/api.js |
Adds folder retrieval. |
src/popup/popup.html |
Introduces the card-based interface. |
src/popup/popup.css |
Restyles the popup and new controls. |
tests/utils.test.js |
Tests new utility helpers. |
tests/api.test.js |
Tests folder retrieval. |
README.md |
Documents folder support. |
CONTRIBUTING.md |
Updates the source layout. |
CHANGELOG.md |
Records the 0.5.0 features. |
package.json |
Bumps the package version. |
manifest.chrome.json |
Bumps the Chrome version. |
manifest.firefox.json |
Bumps the Firefox version. |
assets/images/skate-bucky.svg |
Removes the previous illustration. |
assets/images/lb-logo.svg |
Adds the new wordmark. |
assets/images/bucket-mark.svg |
Adds the new bucket mark. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Two Copilot catches on the POST/PATCH boundary. A successful first save left the popup in create mode, so follow-up edits re-POSTed - and POST is additive, meaning removing a just-added card silently didn't remove it. After a POST the popup now re-runs the lookup and switches to update mode (deferred saves stay additive; no bookmark exists yet). And the reveal cap could show the form while the lookup was still pending, letting a fast save POST an already-saved link - Save now stays gated until init settles.
fetchFolders answered [] for failures and real empty lists alike, so a transient error read "No folders to add" and dead-ended the button until the popup reopened. Failures now resolve to null and the button offers "Couldn't load folders. Try again", still clickable; only a genuinely empty list keeps the dead-end label.
Elapsed 24h periods called a 23:55 save "today" at 00:05. The day count now compares local calendar dates, so today and yesterday flip at the user's midnight and stay put across DST.
This was referenced Aug 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #40
What does this PR do?
Adds folder support to the popup as placement cards, per the design explorations (2b structure, refined over several rounds against dev):
my_linksAPI flag). Unchecking greys the card; selections survive a re-check.saved_at), pre-filled cards, and a "Save changes" button. The popup reveals only after lookup composes the layout, so nothing pops into place.POST /urls(additive) for new links,PATCH /user_bookmarks/:idfor existing — removing a card removes that placement (to folder trash), unchecking My Links trashes the personal save. Deselecting everything on a saved link turns the button into "Move to trash"; a new link with nothing selected keeps Save disabled.Version 0.4.2 → 0.5.0 (minor: new feature + UI change; store screenshots need refreshing per RELEASING.md). Branch history note: the UI was first built as exploration 2b, reworked to 2a for comparison, and returned to 2b — the net diff is the final flow.
Depends on linkbucket/linkbucket#2991 (folders/placements API incl.
my_linksandsaved_at). Deploy the backend first; older extension builds are unaffected (every new key is optional).How to test
dist/.Checklist
npm testpasses (55)npm run lintpassesnpm run format:checkpassesnpm run buildsucceeds for both targets (addons-linter: 0 errors)🤖 Generated with Claude Code