Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 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
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
201 changes: 201 additions & 0 deletions packages/extension/src/newtab/ShortcutLinks/ShortcutImportFlow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import type { ReactElement } from 'react';
import React, { useEffect, useRef } from 'react';
import { useShortcuts } from '@dailydotdev/shared/src/features/shortcuts/contexts/ShortcutsProvider';
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 { MostVisitedSitesPermissionContent } from '@dailydotdev/shared/src/features/shortcuts/components/modals/MostVisitedSitesPermissionContent';
import { useSettingsContext } from '@dailydotdev/shared/src/contexts/SettingsContext';
import { useToastNotification } from '@dailydotdev/shared/src/hooks/useToastNotification';
import { MAX_SHORTCUTS } from '@dailydotdev/shared/src/features/shortcuts/types';

// Coordinates the "Import from browser" / "Import from bookmarks" flows for
// the new hub. Keeps the permission modals, picker modal, and silent-import
// paths in one place so the hub UI itself stays declarative.
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();

// Prevents running the same import more than once for a single click.
const handledRef = useRef<string | null>(null);

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

const capacity = Math.max(0, MAX_SHORTCUTS - (customLinks?.length ?? 0));

if (showImportSource === 'topSites') {
if (!hasCheckedTopSitesPermission || topSites === undefined) {
return;
}
if (handledRef.current === 'topSites') {
return;
}
handledRef.current = 'topSites';

if (topSites.length === 0) {
displayToast('No top sites yet. Visit some sites and try again.');
setShowImportSource?.(null);
return;
}
if (capacity === 0) {
displayToast(
`You already have ${MAX_SHORTCUTS} shortcuts. Remove some to import more.`,
);
setShowImportSource?.(null);
return;
}

// Always show the picker so the user sees exactly what gets imported,
// which source it comes from, and can deselect items before confirming.
// Previously we silently imported when items fit in capacity, which was
// confusing ("what just got added? where from?").
const items = topSites.map((s) => ({ url: s.url }));
openModal({
type: LazyModal.ImportPicker,
props: { source: 'topSites', items, returnTo: returnToAfterImport },
});
setShowImportSource?.(null);
return;
}

if (showImportSource === 'bookmarks') {
if (!hasCheckedBookmarksPermission || bookmarks === undefined) {
return;
}
if (handledRef.current === 'bookmarks') {
return;
}
handledRef.current = 'bookmarks';

if (bookmarks.length === 0) {
displayToast(
'Your bookmarks bar is empty. Add some bookmarks and try again.',
);
setShowImportSource?.(null);
return;
}
if (capacity === 0) {
displayToast(
`You already have ${MAX_SHORTCUTS} shortcuts. Remove some to import more.`,
);
setShowImportSource?.(null);
return;
}

const items = bookmarks.map((b) => ({ url: b.url, title: b.title }));
openModal({
type: LazyModal.ImportPicker,
props: { source: 'bookmarks', items, returnTo: returnToAfterImport },
});
setShowImportSource?.(null);
}
}, [
showImportSource,
topSites,
hasCheckedTopSitesPermission,
bookmarks,
hasCheckedBookmarksPermission,
customLinks,
displayToast,
openModal,
setShowImportSource,
returnToAfterImport,
]);

// Permission modals: shown when the user asked to import but the browser
// hasn't granted permission yet. Once granted, the provider refreshes
// `topSites` / `bookmarks` and the effect above finishes the import.
if (
showImportSource === 'topSites' &&
hasCheckedTopSitesPermission &&
topSites === undefined
) {
const onGrant = async () => {
const granted = await askTopSitesPermission();
if (!granted) {
setShowImportSource?.(null);
}
};
return (
<Modal
kind={Modal.Kind.FlexibleCenter}
size={Modal.Size.Medium}
isOpen
onRequestClose={() => setShowImportSource?.(null)}
>
<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>
);
}

if (
showImportSource === 'bookmarks' &&
hasCheckedBookmarksPermission &&
bookmarks === undefined
) {
const onGrant = async () => {
const granted = await askBookmarksPermission();
if (!granted) {
setShowImportSource?.(null);
}
};
return (
<Modal
kind={Modal.Kind.FlexibleCenter}
size={Modal.Size.Medium}
isOpen
onRequestClose={() => setShowImportSource?.(null)}
>
<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 onClick={onGrant} variant={ButtonVariant.Primary}>
Import bookmarks
</Button>
</Modal.Footer>
</Modal>
);
}

return null;
}
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
66 changes: 64 additions & 2 deletions packages/extension/src/newtab/ShortcutLinks/ShortcutLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +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 { useConditionalFeature } from '@dailydotdev/shared/src/hooks/useConditionalFeature';
import { featureShortcutsHub } from '@dailydotdev/shared/src/lib/featureManagement';
import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext';
import { useShortcutsManager } from '@dailydotdev/shared/src/features/shortcuts/hooks/useShortcutsManager';
import { useShortcutsMigration } from '@dailydotdev/shared/src/features/shortcuts/hooks/useShortcutsMigration';
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 {
const { openModal } = useLazyModal();
Expand Down Expand Up @@ -111,7 +118,7 @@ export default function ShortcutLinks({
{...{
onLinkClick,
onOptionsOpen,
shortcutLinks,
shortcutLinks: shortcutLinks ?? [],
shouldUseListFeedLayout,
toggleShowTopSites,
onReorder,
Expand All @@ -123,3 +130,58 @@ export default function ShortcutLinks({
</>
);
}

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

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

// Auto mode renders live top sites from the browser and ships its own
// permission CTA / empty state inside the hub, so an empty `customLinks`
// is not a signal to show onboarding. Only manual-mode users with zero
// curated shortcuts should see the "Choose your most visited sites" card.
const mode = flags?.shortcutsMode ?? 'manual';
const showOnboarding = mode === 'manual' && manager.shortcuts.length === 0;

if (showOnboarding) {
return (
<>
<ShortcutGetStarted
onTopSitesClick={toggleShowTopSites}
onCustomLinksClick={() =>
openModal({ type: LazyModal.ShortcutEdit, props: { mode: 'add' } })
}
/>
<ShortcutImportFlow />
</>
);
}

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

export default function ShortcutLinks(props: ShortcutLinksProps): ReactElement {
const { user } = useAuthContext();
const { value: hubEnabled } = useConditionalFeature({
feature: featureShortcutsHub,
shouldEvaluate: !!user,
});

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

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