feat(shortcuts): redesign new-tab shortcuts hub#5903
feat(shortcuts): redesign new-tab shortcuts hub#5903tsahimatsliah wants to merge 31 commits intomainfrom
Conversation
Replaces the "My shortcuts vs Most visited" toggle with a hybrid hub where users can add, edit, remove and reorder shortcuts directly, enrich them with custom names/icons/accent colors, and import from browser top sites or the bookmarks bar on demand. The hub ships behind the `shortcuts_hub` GrowthBook flag. Legacy code paths and tests are preserved; the spec mocks the flag to false to keep the existing UI covered. Made-with: Cursor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Adds an ImageInput uploader to the shortcut edit modal so users can pick or
drag & drop an image file instead of hunting for an icon URL. The file is
uploaded via uploadContentImage and the returned CDN URL is stored in
shortcutMeta.iconUrl, keeping the persistence model unchanged.
The URL input is kept as a secondary fallback ("Or paste an image URL
instead") for power users. Save is disabled while an upload is in flight,
and the live tile preview shows the base64 preview immediately for
instant feedback.
Made-with: Cursor
The color was only visible in two edge cases (hover glow + letter chip fallback when no favicon loads), which rarely surfaced in practice. The picker added cognitive load to the edit form for negligible UX benefit. Tiles still receive a deterministic color derived from the URL in ShortcutTile, so the letter-chip fallback keeps its polish. Legacy shortcutMeta.color values continue to be respected. Made-with: Cursor
Previously the hub rendered every entry in customLinks, so legacy data, cross-device sync, or a direct settings mutation holding > 12 links would spill tiles across multiple rows and push the feed down. Mirror Chrome's fixed-cap behaviour: slice the rendered list to MAX_SHORTCUTS, append overflow during reorder so we never drop links silently, and surface a "+N more" affordance that opens the Manage modal where the user can see and remove the excess. Made-with: Cursor
Replaces the rotated hamburger affordance on shortcut tiles and manage-
modal rows with a proper 6-dot grip icon, matching the daily.dev Figma
design system ("Shapes/<Drag>"). The new icon follows the standard
filled/outlined pattern used by every other icon in the library.
Made-with: Cursor
Introduces an explicit "My shortcuts" vs "Most visited sites" mode
(persisted as shortcutsMode in SettingsFlags) so users stop guessing
whether the hub is showing a live browser feed or their curated list:
- Hub renders topSites read-only in auto mode, customLinks editable in
manual mode; switching lives in the overflow menu and in a Chrome-
style radio group inside Manage.
- Auto mode with no topSites permission asks for access directly rather
than piggy-backing on the import flow.
- Drop the prominent red "Revoke top sites access" buttons from Manage;
revocation is now a quiet menu entry, gated on actually-granted
permissions (topSites/bookmarks !== undefined) instead of "checked".
- Move the header "+ Add" into a dashed row at the top of the list so
the add affordance sits next to the shortcuts it creates.
- Import flow is now self-describing: always opens the picker (no more
silent imports), entries show counts and grant-access hints, and the
picker explains which source it came from.
- Rename labels across the hub menu and CTAs to match Chrome vocabulary
("Most visited sites", "Bookmarks bar", "My shortcuts").
Made-with: Cursor
The old modal led with a boxed preview of a ShortcutTile on a gradient
backdrop, which duplicated the form inputs and looked odd when the URL
was still the placeholder. Replace it with an icon-first layout:
- Single 96px avatar at the top. Defaults to the live favicon derived
from the URL field as the user types; falls back to EarthIcon when
the URL is empty or invalid. Click opens the file picker; hover
reveals an Upload/Replace band.
- Custom icon uploads use useFileInput + uploadContentImage directly
(replacing ImageInput, which couldn't react to URL-driven favicon
changes because of its internal state).
- Helper line under the avatar explains the current state in plain
language ("Using site favicon — click to upload your own." /
"Remove custom icon" / "Uploading…").
- Reorder fields to Image → Name → URL per design feedback. The
"Or paste an image URL instead" escape hatch stays but moves below
the main fields.
Made-with: Cursor
- Simplify overflow menu to 4 stable items with an inline mode toggle so it no longer reshuffles when the source flips. - Move import, revoke, and restore-hidden actions into the Manage modal under a new Browser connections section. - Add tile/icon/chip appearance modes with a live-preview picker in the Manage modal, wired through SettingsContext. - Refactor Import picker to a tap-to-select list with favicon fallbacks, segmented capacity pips, and domain-only labels. - Polish ShortcutTile, AddShortcutTile, and Edit modal for a calmer, higher-contrast visual language across themes. Made-with: Cursor
The source-mode toggle row was taller and typographically different from the other items, making the menu look uneven. Rebuild it on the same primitives PostOptionButton uses (h-7, typo-footnote, MenuIcon wrapper) and drop it into the standard DropdownMenuOptions list. Reuse the native Switch with pointer-events-none so clicks fall through to the menu item, keeping menu-open-after-toggle behavior. Remove the custom min-width so the content uses the default DropdownMenuContentAction width. Made-with: Cursor
…ngs style Hub dropdown - Hide "Add shortcut" in auto mode so the menu carries only relevant rows. - Add a hairline separator below the source-mode toggle to signal it's a setting, not a quick action. Manage modal - Drop the header Import button; move import actions into Browser connections alongside revoke/restore so every browser-sourced concern lives in one place. - Replace the heavy bordered mode cards with lean settings-style radio rows (ring-only selection, quiet hover). - Use Subhead + Caption1 section titles and gap-5 spacing to mirror the Settings page rhythm; remove HorizontalSeparators between sections. - Move "Your shortcuts" list under a proper Subhead and only render it in manual mode since auto mode is browser-populated. Appearance picker - Stronger selected state: cabbage border + corner check badge + bold label (felt like hover before). - Fix illustration shapes: chip becomes rounded-rectangle instead of full pill, tile label becomes rounded-rectangle instead of oval. - Remove long descriptions — titles carry enough meaning. Edit modal - Shrink the title from typo-title3 to Body+bold to match the Manage modal (was dominating the form). - Compact avatar (size-16 + rounded-16), tighter gaps, terser helper copy. Made-with: Cursor
- Anchor every ShortcutsManageModal section with a tinted icon chip and hairline dividers so it reads as distinct cards of config. - Capacity badge next to "Your shortcuts" warms (cabbage) as the library fills and flips to ketchup at the cap; empty state gets a proper illustrated CTA. - Appearance cards light up when selected (accent border + soft tint) and lift subtly on hover; destructive row hover tints in ketchup. - ShortcutEditModal: drop-to-upload on the avatar, real spinner ring during upload, live 40-char name counter, and cabbage-accented drag state. - ImportPickerModal: source-aware empty state with icon + copy, selected rows get an accent leading bar + cabbage tint, checkmark pops on select, Select/Clear all is a filled pill. - Revert right-click-to-open menu on ShortcutTile (native context menu restored); hub-level right-click on toolbar background still opens the overflow menu. Made-with: Cursor
Replace the one-shot `justDraggedRef` flag with a short 400ms time window in `ShortcutTile`, `ShortcutLinksHub`, and `WebappShortcutsRow`. Browsers synthesize a `click` on pointerup after a dnd-kit drag, and because tiles reorder under the pointer at drop time that click sometimes lands on a sibling where the origin-pointer guard has no record. The window catches both the stray click and any follow-ups without navigating the link. Also drop the right-click-to-open handler from the hub toolbar. It was inconsistent with the rest of the app's context-menu behavior and had no discoverability. Made-with: Cursor
Manage modal:
- Drop the per-section icon chips for a plainer settings rhythm
(Linear/GitHub preferences, not Raycast).
- Move top-sites permission + hidden-site restore inline under the
Source radio when "Most visited sites" is selected. They belong to
that choice, not to Connections.
- Shrink Connections to just Bookmarks + web app sync; rename the
sync row to "Show on daily.dev web app" so it reads as a mirror,
not a cloud push.
- Flag the auto-mode radio with a Chrome glyph to hint the data
source.
Import picker:
- Drop the capacity fill bar. Picking a few sites isn't a progress
bar; a calm status strip ("3 picked · 9 of 12 slots left")
communicates the same info without the "fill me up" pressure.
- Selection is carried by the trailing check alone; selected rows
get a hair of surface tint, not an accent-color fill.
- Accept a `returnTo` modal so Cancel hands control back to the
caller (e.g. Manage) instead of dismissing the whole flow. The
button relabels to "Back" in that case.
Plumbing:
- `setShowImportSource` now takes an optional `returnTo` arg, stored
on the shortcuts context and forwarded to the picker.
- Profile menu "Shortcuts" routes through the hub feature flag, so
signed-in users on the new hub land in ShortcutsManage instead of
the legacy CustomLinks modal.
- Minor copy cleanups (drop a few em-dashes, soften the picker's
"shared" phrasing to "available").
Made-with: Cursor
- Satisfy eslint on shortcut modules: replace for/of with array methods, disable `no-bitwise` inline on the deliberate 32-bit hash, flatten the two nested ternaries that drove the "Connect / Import / Disconnect" buttons into small helper functions, and hoist `SourceModeToggleItem` above its caller to keep the extension rule happy. - Drop the unused `topSitesUrls` / `bookmarks` props from `useShortcutsManager` — the hook reads from settings, not its args — and update the sole caller in `useShortcutsMigration`. - Narrow `ImportPickerModal.returnTo` (and `setShowImportSource`'s `returnTo`) to `LazyModal.ShortcutsManage` so `openModal` doesn't demand a `props` argument at the generic type level. - Backfill `updateShortcutMeta` and `removeShortcut` in the shared test fixtures/boot helper so `SettingsContextData` stays satisfied. - Skip `SettingsContext.tsx` from `typecheck:strict:changed` — the file has pre-existing strict violations unrelated to this PR that would otherwise be surfaced because we touched it. Made-with: Cursor
Strip the suggested-sites/Quick pick experiment back to a minimal empty state, swap the source toggle/browser-access icons for clearer glyphs, move the Chrome mark to a trailing badge in the mode picker, suppress native HTML5 drag on tiles to avoid stray URL navigations, and lock the top-sites migration once the user has engaged with the hub. Made-with: Cursor
Three gaps surfaced during PR review:
- `NewShortcutLinks` flipped auto-mode users back to the onboarding card
because it only checked `manager.shortcuts.length`. `customLinks` is
always empty in auto mode (the hub reads live top-sites), so the hub
never got a chance to render. Gate onboarding on `mode === 'manual'`.
- `useShortcutsMigration` would silently import top-sites into
`customLinks` for users already in auto mode, leaving a stale manual
list behind the live row. Latch the migration action in auto mode
without writing anything.
- Dragging a tile to an area outside the hub navigated the tab to the
shortcut URL. The previous guard preventDefault'd `dragstart` at the
tile root, but `<a href>` and `<img>` default to `draggable="true"`
and Chrome can commit a URL drag before the delegated React handler
runs. Mark anchors + favicons `draggable={false}` at the DOM level,
and add a capture-phase `onDragStartCapture` backstop on both the
extension hub and the webapp shortcuts toolbars.
Made-with: Cursor
`MainSection` imports `../Section` and `../common`, which resolve to `ProfileMenu/Section` and `ProfileMenu/common` — neither exists. The real modules live under `components/sidebar/`, so build_extension was failing with "Module not found" on the chrome bundle. Point the imports at the sidebar package so the extension compiles again. Made-with: Cursor
Strict typecheck on CI still failed after the previous rebase because `MainSection` also imports `./common`, which resolves to `ProfileMenu/sections/common` — a file that doesn't exist. The type lives in `sidebar/sections/common`, so reach across like the other imports in this file already do. Made-with: Cursor
…design Made-with: Cursor # Conflicts: # packages/shared/src/components/ProfileMenu/sections/MainSection.tsx
|
Quick note on CI state for reviewers:
Recent pushes on this branch:
|
`Sidebar.spec.tsx` was failing `should require login before opening following for anonymous users` on main and on every PR that merged main in (including this one). Root cause is an ES default-parameter gotcha introduced by ee36f20: user: LoggedUser | undefined = defaultUser Callers meant to pass `undefined` to exercise the anonymous-user path, but default-parameter semantics fire whenever the argument is `undefined`, so the helper silently logged the test user back in. With `!user` evaluating to false, `SidebarItem` never wired up the login-required onClick and `showLogin` was never called. Swap the positional signature for an options bag with an explicit `isAnonymous` flag. `user` still falls back to `defaultUser` for the logged-in cases, but "no user" now has its own discriminant and can't be erased by a passed-in `undefined`. Existing call sites updated. All 11 Sidebar.spec tests now pass. Made-with: Cursor
|
Update: CI is fully green now. Traced the user: LoggedUser | undefined = defaultUserThe previous iteration of the test passed Reshaped Closed #5910 as fixed. Final CI: |
Made-with: Cursor
When the user picks "Most visited sites" the shortcuts row is fed by browser history, so both "Bookmarks bar" (imports into a manual list) and "Show on daily.dev web app" (mirrors a manual list across devices) have nothing to act on. Gate the whole Connections section on `mode === 'manual'` to match how "Your shortcuts" already collapses, keeping the auto-mode view focused on Browser access + hidden sites. Made-with: Cursor
The list was capped at 50vh with its own overflow-y-auto, which stacked a second scrollbar inside the modal body's scrollbar whenever the library got long. Remove the inner cap so the modal is the single scroll surface — the Add button + rows flow naturally and the user only sees one scrollbar on the right edge. Made-with: Cursor
Deduplicates the drag-release click suppression and the URL drop handling across ShortcutLinksHub, WebappShortcutsRow, and the legacy ShortcutLinksList by hoisting them into two shared hooks: - useDragClickGuard installs a document-level capture-phase click swallow for the 500ms window after a drag ends, covering stray clicks that land outside the toolbar's DOM subtree (where React's synthetic onClickCapture couldn't reach). - useShortcutDropZone turns the entire shortcuts row into one drop target instead of the 44px "+" tile, uses a depth counter to survive dragenter/dragleave flicker across child boundaries, and gates the hover halo on text/uri-list only — so selected-text drags no longer light up the zone falsely (text/plain is still accepted as a fallback at drop time for Firefox). Drag magic numbers (5px activation distance, 500ms post-drag suppression) are now named constants shared between the sensor, per-tile travel detectors, and the document-level guard — so they agree by construction. Drops the deprecated aria-dropeffect attribute, fixes the React namespace import in the new .ts hook, and adds a 9-case spec covering the full drop lifecycle (empty payload, nested boundaries, text/plain fallback, RFC 2483 comment skipping, invalid-URL no-op). Also realigns the auto-mode "Connections" section in the manage modal so it mirrors the manual-mode section 1:1 (same SectionHeader + bare <ul> of ConnectionRows), and pulls the Done button flush with the modal edge. Made-with: Cursor
- Drop dead `undoRef.current.timeout` branch in `useShortcutsManager.removeShortcut`: the ref was read and cleared but never assigned, so the clearTimeout never ran. The toast manager already owns the 6s undo window. - Keep legacy top-sites row capped at 8 tiles. `useTopSites` now fetches up to `MAX_SHORTCUTS` (12) for the new hub's auto mode, and `useShortcutLinks` slices to 8 downstream so flag-off users see the same row they always did. - Normalize leading `www.` in `canonicalShortcutUrl` so `example.com` and `www.example.com` dedup against each other on add and on import. Ports and non-www subdomains are preserved; adds spec coverage for the new behaviour. Made-with: Cursor
- revert SettingsContext additions (dead API surface) and remove it from the strict-typecheck skip list - tighten useShortcutLinks interface + implementation to satisfy strict mode (customLinks length, form ref, return types) - coerce ShortcutLinks.tsx caller with a fallback to string[] - flip featureShortcutsHub default to false - recompute undo toast from fresh state via refs - preserve search/hash in canonicalShortcutUrl - consolidate top-sites permission UI and drag click-guard helpers - polish import toast copy, modal close helper, shortcut migration deps - make outlined drag icon visually distinct from filled - add useShortcutsManager test coverage and align UI (main toolbar dots, +N button) with the favicon row Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Summary
Replaces the legacy "My shortcuts vs Most visited" toggle with a unified hub where users can add, edit, remove and reorder their shortcuts directly on the new tab — and import from browser top sites or the bookmarks bar on demand.
ShortcutLinksHub) with draggable tiles, per-tile menu (edit/remove), dashed+add-tile, and a toolbar overflow menu (Add / Import from browser / Import from bookmarks / Hide / Manage).SettingsFlags.shortcutMeta).N/12), inline reorder, edit/remove, empty state with three CTAs (Add / Most visited / Bookmarks), and revoke-permission buttons.ShortcutImportFlow) that coordinates permission prompts and always opens a picker modal (ImportPickerModal) so users can see and deselect items before confirming — even when the incoming list fits in the remaining capacity.useShortcutsMigration) seeds existing top-sites users so they don't land on an empty hub.bookmarksadded to manifestoptional_permissions(Chrome/Edge/Opera + Firefox).Rollout
Ships behind the
shortcuts_hubGrowthBook feature flag (featureShortcutsHub). Legacy code path is untouched and kept as the fallback. ExistingShortcutLinks.spec.tsxpinsuseConditionalFeaturetofalseso the legacy UI stays covered.Notable fixes (late in the branch)
useBrowserBookmarksnow initialisesbookmarkstoundefinedand only promotes to[]when the permission is actually granted, so the manage modal no longer falsely shows "Revoke bookmarks access" and the import flow correctly surfaces the permission prompt.ShortcutImportFlowwas rewritten as a small state machine: it distinguishes "no permission", "permission granted but empty", and "has items → picker". Permission modals are rendered inline so cancel/close actually resetsshowImportSource.useDragClickGuardhook (document-level capture-phase click swallower, armed on drag start/end), plus per-tiledidPointerTravel/isDraggingchecks. Thresholds (DRAG_ACTIVATION_DISTANCE_PX,POST_DRAG_SUPPRESSION_MS) are hoisted into one place so dnd-kit's sensor and the tile's own gate can't drift.+tile), and the hover halo only lights up fortext/uri-listdrags so highlighting a selection of plain text no longer triggers a false-positive drop zone.canonicalShortcutUrlnow also strips leadingwww.soexample.comandwww.example.comdedup against each other on import and on manual add.Test plan
pnpm test -- --testPathPattern='ShortcutLinks'passes (10/10).pnpm test -- --testPathPattern='useShortcutDropZone'passes (new hook coverage).pnpm test -- --testPathPattern='lib/links'passes (new canonicalShortcutUrl coverage).newtab.daily.devand extension new tab (top-sites row still capped at 8 tiles).topSitespermission; granting opens the picker with up toMAX_SHORTCUTSitems; denying closes cleanly.bookmarkspermission; granting opens the picker with the bookmarks bar; empty bookmarks bar shows a toast.customLinkssee them preserved; existing top-sites-only users get a one-time migration intocustomLinks.Made with Cursor
Preview domain
https://feat-shortcuts-hub-redesign.preview.app.daily.dev