Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f9440be
feat(shortcuts): redesign new-tab shortcuts hub behind feature flag
tsahimatsliah Apr 21, 2026
96aaf06
feat(shortcuts): allow uploading a custom icon image
tsahimatsliah Apr 21, 2026
9550fca
refactor(shortcuts): remove accent color picker from edit modal
tsahimatsliah Apr 21, 2026
9d1116e
feat(shortcuts): defensively cap hub to MAX_SHORTCUTS tiles
tsahimatsliah Apr 21, 2026
5f817ee
feat(icons): add DragIcon and use it for shortcut drag handles
tsahimatsliah Apr 21, 2026
e79a65f
feat(shortcuts): add mode selector and align hub UX with Chrome
tsahimatsliah Apr 21, 2026
5a6a391
refactor(shortcuts): redesign edit modal with icon-first layout
tsahimatsliah Apr 21, 2026
0909216
feat(shortcuts): redesign hub menu, import modal, and appearance options
tsahimatsliah Apr 21, 2026
f6bd290
fix(shortcuts): align hub dropdown with system DropdownMenu conventions
tsahimatsliah Apr 21, 2026
0cd654c
refactor(shortcuts): compact modals + realign hub dropdown with setti…
tsahimatsliah Apr 21, 2026
66ddda9
feat(shortcuts): polish modals, icons, and states across the hub
tsahimatsliah Apr 23, 2026
378bb46
fix(shortcuts): harden drag click-suppression across hub tiles
tsahimatsliah Apr 23, 2026
f856ea5
refactor(shortcuts): regroup manage modal, simplify import picker
tsahimatsliah Apr 23, 2026
14b1bf8
fix(shortcuts): unblock CI (lint, strict typecheck, tests)
tsahimatsliah Apr 23, 2026
e52fd42
refactor(shortcuts): simplify empty state and polish hub UX
tsahimatsliah Apr 23, 2026
1e626d9
Merge branch 'main' into feat/shortcuts-hub-redesign
tsahimatsliah Apr 23, 2026
f81b4e8
Merge branch 'main' into feat/shortcuts-hub-redesign
tsahimatsliah Apr 23, 2026
9ce8031
fix(shortcuts): stop tile drag from navigating the tab
tsahimatsliah Apr 23, 2026
bb785c1
fix(profile-menu): correct Section/common import paths
tsahimatsliah Apr 23, 2026
31e0323
fix(profile-menu): point SidebarSectionProps import at sidebar/sections
tsahimatsliah Apr 23, 2026
16c166b
Merge remote-tracking branch 'origin/main' into feat/shortcuts-hub-re…
tsahimatsliah Apr 23, 2026
f73489e
fix(sidebar): restore anonymous-user test by reshaping renderComponent
tsahimatsliah Apr 23, 2026
b3a0ed7
Merge branch 'main' into feat/shortcuts-hub-redesign
tsahimatsliah Apr 23, 2026
3e58c4c
fix(shortcuts): hide Connections section in auto mode
tsahimatsliah Apr 23, 2026
64670fd
fix(shortcuts): drop nested scroll on "Your shortcuts" list
tsahimatsliah Apr 23, 2026
681a876
refactor(shortcuts): extract drag-click guard + row-wide drop zone
tsahimatsliah Apr 23, 2026
8a47ace
fix(shortcuts): address review follow-ups on hub redesign
tsahimatsliah Apr 23, 2026
fe5b55a
fix(shortcuts): address PR review and unblock strict typecheck
tsahimatsliah Apr 23, 2026
61e3fc1
fix(shortcuts): restore lint-required directive and prettier format
tsahimatsliah Apr 23, 2026
7910ac0
Merge branch 'main' into feat/shortcuts-hub-redesign
tsahimatsliah Apr 23, 2026
e174311
fix(shortcuts): resolve lint (prettier + tailwind class order)
tsahimatsliah Apr 23, 2026
47019cf
Merge branch 'main' into feat/shortcuts-hub-redesign
rebelchris Apr 24, 2026
a5ff8be
fix: feedback round
rebelchris Apr 24, 2026
8abe1d6
fix: cleanup part 2
rebelchris Apr 24, 2026
8e98e83
fix: cleanup part 3
rebelchris Apr 24, 2026
b38198c
fix: cleanup part 4
rebelchris Apr 24, 2026
fb2425a
fix: cleanup part 5
rebelchris Apr 24, 2026
d7d504f
fix: cleanup part 6
rebelchris Apr 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/extension/src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"https://*.staging.daily.dev/"
],
"__firefox|dev__permissions": ["storage", "http://localhost/", "https://*.local.fylla.dev/"],
"optional_permissions": ["topSites", "declarativeNetRequestWithHostAccess"],
"__firefox__optional_permissions": ["topSites", "*://*/*"],
"optional_permissions": ["topSites", "bookmarks", "declarativeNetRequestWithHostAccess"],
"__firefox__optional_permissions": ["topSites", "bookmarks", "*://*/*"],
"__chrome|opera|edge__optional_host_permissions": [ "*://*/*"],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self';"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,18 @@ export const ShortcutGetStarted = ({
</div>
<div className="flex gap-4">
<Button
onClick={() => completeActionThenFire(onTopSitesClick)}
onClick={() => completeActionThenFire(onCustomLinksClick)}
size={ButtonSize.Medium}
variant={ButtonVariant.Float}
variant={ButtonVariant.Primary}
>
Skip for now
Add shortcuts
</Button>
<Button
onClick={() => completeActionThenFire(onCustomLinksClick)}
onClick={() => completeActionThenFire(onTopSitesClick)}
size={ButtonSize.Medium}
variant={ButtonVariant.Primary}
variant={ButtonVariant.Float}
>
Add shortcuts
Skip for now
</Button>
</div>
</div>
Expand Down
200 changes: 200 additions & 0 deletions packages/extension/src/newtab/ShortcutLinks/ShortcutImportFlow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import type { ReactElement } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { useShortcuts } from '@dailydotdev/shared/src/features/shortcuts/contexts/ShortcutsProvider';
import { MostVisitedSitesPermissionContent } from '@dailydotdev/shared/src/features/shortcuts/components/modals/MostVisitedSitesPermissionContent';
import { useLazyModal } from '@dailydotdev/shared/src/hooks/useLazyModal';
import { LazyModal } from '@dailydotdev/shared/src/components/modals/common/types';
import {
Button,
ButtonVariant,
} from '@dailydotdev/shared/src/components/buttons/Button';
import { Modal } from '@dailydotdev/shared/src/components/modals/common/Modal';
import { Justify } from '@dailydotdev/shared/src/components/utilities';
import {
Typography,
TypographyTag,
TypographyType,
} from '@dailydotdev/shared/src/components/typography/Typography';
import { useSettingsContext } from '@dailydotdev/shared/src/contexts/SettingsContext';
import { useToastNotification } from '@dailydotdev/shared/src/hooks/useToastNotification';
import {
type ImportSource,
MAX_SHORTCUTS,
} from '@dailydotdev/shared/src/features/shortcuts/types';

interface PermissionModalProps {
onGrant: () => Promise<void>;
onClose: () => void;
}

function TopSitesPermissionModal({
onGrant,
onClose,
}: PermissionModalProps): ReactElement {
return (
<Modal
kind={Modal.Kind.FlexibleCenter}
size={Modal.Size.Medium}
isOpen
onRequestClose={onClose}
>
<Modal.Header showCloseButton>
<Typography tag={TypographyTag.H3} type={TypographyType.Body} bold>
Show most visited sites
</Typography>
</Modal.Header>
<MostVisitedSitesPermissionContent
onGrant={onGrant}
ctaLabel="Import shortcuts"
/>
</Modal>
);
}

function BookmarksPermissionModal({
onGrant,
onClose,
}: PermissionModalProps): ReactElement {
return (
<Modal
kind={Modal.Kind.FlexibleCenter}
size={Modal.Size.Medium}
isOpen
onRequestClose={onClose}
>
<Modal.Header />
<Modal.Body>
<Modal.Title className="mb-4">Import your bookmarks bar</Modal.Title>
<Modal.Text className="text-center">
To import your bookmarks bar, your browser will ask for permission to
read bookmarks. We never sync your bookmarks to our servers.
</Modal.Text>
</Modal.Body>
<Modal.Footer justify={Justify.Center}>
<Button type="button" onClick={onGrant} variant={ButtonVariant.Primary}>
Import bookmarks
</Button>
</Modal.Footer>
</Modal>
);
}

export function ShortcutImportFlow(): ReactElement | null {
const {
showImportSource,
setShowImportSource,
returnToAfterImport,
topSites,
hasCheckedPermission: hasCheckedTopSitesPermission,
askTopSitesPermission,
bookmarks,
hasCheckedBookmarksPermission,
askBookmarksPermission,
} = useShortcuts();
const { customLinks } = useSettingsContext();
const { displayToast } = useToastNotification();
const { openModal } = useLazyModal();
const handledRef = useRef<ImportSource | null>(null);

const closeImportFlow = useCallback(
() => setShowImportSource?.(null),
[setShowImportSource],
);
const isTopSitesImport = showImportSource === 'topSites';
const hasCheckedPermission = isTopSitesImport
? hasCheckedTopSitesPermission
: hasCheckedBookmarksPermission;
const items = (
isTopSitesImport
? topSites?.map((site) => ({ url: site.url }))
: bookmarks?.map((bookmark) => ({
url: bookmark.url,
title: bookmark.title,
}))
) as Array<{ url: string; title?: string }> | undefined;
const askPermission = isTopSitesImport
? askTopSitesPermission
: askBookmarksPermission;
const emptyToast = isTopSitesImport
? 'No top sites yet. Visit some sites and try again.'
: 'Your bookmarks bar is empty. Add some bookmarks and try again.';

useEffect(() => {
if (!showImportSource) {
handledRef.current = null;
return;
}

if (!hasCheckedPermission || items === undefined) {
return;
}

if (handledRef.current === showImportSource) {
return;
}
handledRef.current = showImportSource;

const capacity = Math.max(0, MAX_SHORTCUTS - (customLinks?.length ?? 0));
if (items.length === 0) {
displayToast(emptyToast);
closeImportFlow();
return;
}

if (capacity === 0) {
displayToast(
`You already have ${MAX_SHORTCUTS} shortcuts. Remove some to import more.`,
);
closeImportFlow();
return;
}

openModal({
type: LazyModal.ImportPicker,
props: {
source: showImportSource,
items,
returnTo: returnToAfterImport,
},
});
closeImportFlow();
}, [
customLinks,
displayToast,
emptyToast,
hasCheckedPermission,
items,
closeImportFlow,
openModal,
returnToAfterImport,
showImportSource,
]);

if (!showImportSource) {
return null;
}

if (!hasCheckedPermission || items !== undefined) {
return null;
}

const handleGrant = async () => {
const granted = await askPermission();
if (!granted) {
closeImportFlow();
}
};

if (isTopSitesImport) {
return (
<TopSitesPermissionModal
onGrant={handleGrant}
onClose={closeImportFlow}
/>
);
}

return (
<BookmarksPermissionModal onGrant={handleGrant} onClose={closeImportFlow} />
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ jest.mock('@dailydotdev/shared/src/lib/boot', () => ({
getBootData: jest.fn(),
}));

// Pin these tests to the legacy code path. The shortcuts hub redesign is
// default-on in production; the suite below exercises the legacy UI that the
// hub is replacing behind the feature flag.
jest.mock('@dailydotdev/shared/src/hooks/useConditionalFeature', () => ({
useConditionalFeature: () => ({ value: false, isLoading: false }),
}));

jest.mock('webextension-polyfill', () => {
let providedPermission = false;

Expand Down
63 changes: 59 additions & 4 deletions packages/extension/src/newtab/ShortcutLinks/ShortcutLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,21 @@ import { useLazyModal } from '@dailydotdev/shared/src/hooks/useLazyModal';
import { LazyModal } from '@dailydotdev/shared/src/components/modals/common/types';
import { useShortcuts } from '@dailydotdev/shared/src/features/shortcuts/contexts/ShortcutsProvider';
import { useShortcutLinks } from '@dailydotdev/shared/src/features/shortcuts/hooks/useShortcutLinks';
import { useShortcutsManager } from '@dailydotdev/shared/src/features/shortcuts/hooks/useShortcutsManager';
import { useShortcutsMigration } from '@dailydotdev/shared/src/features/shortcuts/hooks/useShortcutsMigration';
import { useIsShortcutsHubEnabled } from '@dailydotdev/shared/src/features/shortcuts/hooks/useIsShortcutsHubEnabled';
import { ShortcutLinksList } from './ShortcutLinksList';
import { ShortcutGetStarted } from './ShortcutGetStarted';
import { ShortcutLinksHub } from './ShortcutLinksHub';
import { ShortcutImportFlow } from './ShortcutImportFlow';

interface ShortcutLinksProps {
shouldUseListFeedLayout: boolean;
}

export default function ShortcutLinks({
function LegacyShortcutLinks({
shouldUseListFeedLayout,
}: ShortcutLinksProps): ReactElement {
}: ShortcutLinksProps): ReactElement | null {
const { openModal } = useLazyModal();
const { showTopSites, toggleShowTopSites, updateCustomLinks } =
useSettingsContext();
Expand Down Expand Up @@ -95,7 +100,7 @@ export default function ShortcutLinks({
};

if (!showTopSites) {
return <></>;
return null;
}

return (
Expand All @@ -111,7 +116,7 @@ export default function ShortcutLinks({
{...{
onLinkClick,
onOptionsOpen,
shortcutLinks,
shortcutLinks: shortcutLinks ?? [],
shouldUseListFeedLayout,
toggleShowTopSites,
onReorder,
Expand All @@ -123,3 +128,53 @@ export default function ShortcutLinks({
</>
);
}

function NewShortcutLinks({
shouldUseListFeedLayout,
}: ShortcutLinksProps): ReactElement | null {
const { showTopSites, toggleShowTopSites, flags } = useSettingsContext();
const manager = useShortcutsManager();
const { openModal } = useLazyModal();
useShortcutsMigration();

if (!showTopSites) {
return null;
}

// Onboarding is only shown for manual-mode users with no shortcuts yet —
// auto mode handles its own empty state (permission CTA / no-history copy)
// inside the hub.
const mode = flags?.shortcutsMode ?? 'manual';
const showOnboarding = mode === 'manual' && manager.shortcuts.length === 0;

if (showOnboarding) {
return (
<>
<ShortcutGetStarted
onTopSitesClick={toggleShowTopSites}
onCustomLinksClick={() =>
openModal({ type: LazyModal.ShortcutsManage })
}
/>
<ShortcutImportFlow />
</>
);
}

return (
<>
<ShortcutLinksHub shouldUseListFeedLayout={shouldUseListFeedLayout} />
<ShortcutImportFlow />
</>
);
}

export default function ShortcutLinks(props: ShortcutLinksProps): ReactElement {
const hubEnabled = useIsShortcutsHubEnabled();

if (hubEnabled) {
return <NewShortcutLinks {...props} />;
}

return <LegacyShortcutLinks {...props} />;
}
Loading
Loading