Skip to content

feat(shortcuts): redesign new-tab shortcuts hub#5903

Open
tsahimatsliah wants to merge 31 commits intomainfrom
feat/shortcuts-hub-redesign
Open

feat(shortcuts): redesign new-tab shortcuts hub#5903
tsahimatsliah wants to merge 31 commits intomainfrom
feat/shortcuts-hub-redesign

Conversation

@tsahimatsliah
Copy link
Copy Markdown
Member

@tsahimatsliah tsahimatsliah commented Apr 21, 2026

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.

  • New hub UI (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).
  • Rich shortcut model with custom name, optional custom icon URL, and accent color fallback when no favicon is available (persisted via SettingsFlags.shortcutMeta).
  • Manage modal with capacity counter (N/12), inline reorder, edit/remove, empty state with three CTAs (Add / Most visited / Bookmarks), and revoke-permission buttons.
  • Edit modal with live tile preview, validated URL/name/icon fields, and a color palette.
  • Import flow (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.
  • One-time auto-migration (useShortcutsMigration) seeds existing top-sites users so they don't land on an empty hub.
  • Undo toast (6s) on remove, keyboard DnD support, aria-live reorder announcements, focus rings, motion-reduce-aware transitions.
  • bookmarks added to manifest optional_permissions (Chrome/Edge/Opera + Firefox).

Rollout

Ships behind the shortcuts_hub GrowthBook feature flag (featureShortcutsHub). Legacy code path is untouched and kept as the fallback. Existing ShortcutLinks.spec.tsx pins useConditionalFeature to false so the legacy UI stays covered.

Notable fixes (late in the branch)

  • useBrowserBookmarks now initialises bookmarks to undefined and 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.
  • ShortcutImportFlow was 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 resets showImportSource.
  • DnD click-through on tiles is guarded by a shared useDragClickGuard hook (document-level capture-phase click swallower, armed on drag start/end), plus per-tile didPointerTravel/isDragging checks. 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.
  • Drag-to-add accepts drops anywhere on the toolbar (not just the + tile), and the hover halo only lights up for text/uri-list drags so highlighting a selection of plain text no longer triggers a false-positive drop zone.
  • canonicalShortcutUrl now also strips leading www. so example.com and www.example.com dedup 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).
  • Lint clean on changed files.
  • With flag off: legacy UI unchanged on newtab.daily.dev and extension new tab (top-sites row still capped at 8 tiles).
  • With flag on: hub renders, add/edit/remove/reorder all round-trip to settings.
  • Import from browser: first click prompts for topSites permission; granting opens the picker with up to MAX_SHORTCUTS items; denying closes cleanly.
  • Import from bookmarks: first click prompts for bookmarks permission; granting opens the picker with the bookmarks bar; empty bookmarks bar shows a toast.
  • Remove shows undo toast that restores the shortcut and its meta within 6s.
  • Reorder via drag-and-drop persists; clicking a tile after a drag never navigates mid-drag (tested with long drags that exit the toolbar bounds).
  • Dragging a bookmarks-bar link onto any point of the row imports it; dragging selected text does not light up the drop halo.
  • Reduced-motion users don't see the lift/hover transforms.
  • Existing users with customLinks see them preserved; existing top-sites-only users get a one-time migration into customLinks.

Made with Cursor

Preview domain

https://feat-shortcuts-hub-redesign.preview.app.daily.dev

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
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Apr 23, 2026 1:34pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
storybook Ignored Ignored Apr 23, 2026 1:34pm

Request Review

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
tsahimatsliah and others added 2 commits April 23, 2026 12:33
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
@tsahimatsliah
Copy link
Copy Markdown
Member Author

Quick note on CI state for reviewers:

Recent pushes on this branch:

  • 9ce8031fix(shortcuts): stop tile drag from navigating the tab (DOM-level draggable={false} on anchors + favicons, plus capture-phase onDragStartCapture backstop on the extension hub and webapp shortcuts toolbars — so dragging a tile sideways no longer triggers Chrome's drop-URL-to-navigate behavior)
  • 16c166b — merge origin/main to pick up 63f752532's MainSection rewrite, which replaced my earlier import-path fix commits

`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
@tsahimatsliah
Copy link
Copy Markdown
Member Author

Update: CI is fully green now.

Traced the Sidebar.spec.tsxshould require login before opening following for anonymous users failure to its root cause — an ES default-parameter gotcha in the spec's own renderComponent:

user: LoggedUser | undefined = defaultUser

The previous iteration of the test passed null to bypass the default; ee36f20e7 switched those call sites to undefined, which triggers the default, which silently logged the test user back in. That made !user false inside SidebarItem, so showLogin was never wired up and never called.

Reshaped renderComponent into an options bag with an explicit isAnonymous discriminant so "no user" can't be erased by a passed-in undefined. All 11 Sidebar.spec tests pass; only the test fixture changed — product code untouched.

Closed #5910 as fixed.

Final CI: build, build_extension, test_extension (10/10 shortcut tests), test_shared, test_webapp, lint_shared, typecheck_strict_changed, Vercel storybook, preview-domain + Jira + CLA — all green. Vercel daily-webapp is still deploying.

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
tsahimatsliah and others added 3 commits April 23, 2026 16:19
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant