Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8ae0fae
fix(ui): reveal custom feed tab pencil on hover
infin1t3 Aug 16, 2026
7a3bf96
fix(ui): make custom feed tab link fill the tab
infin1t3 Aug 16, 2026
c9d0e4e
fix(ui): stretch custom feed link across the whole tab
infin1t3 Aug 16, 2026
12addc0
Merge branch 'dev' into bug-ui/1898-custom-feeds-hover-interaction
secondl1ght Aug 17, 2026
846b46c
Merge branch 'dev' into bug-ui/1898-custom-feeds-hover-interaction
infin1t3 Aug 18, 2026
c97ee8a
Merge branch 'dev' into bug-ui/1898-custom-feeds-hover-interaction
infin1t3 Aug 20, 2026
484f34a
feat(ui): redesign feed tabs with reach label, icons, and mobile tab bar
infin1t3 Aug 20, 2026
4214d8d
fix(ui): apply code-review findings to feed tabs and icon stack
infin1t3 Aug 20, 2026
b5bfc81
Merge branch 'dev' into bug-ui/1898-custom-feeds-hover-interaction
infin1t3 Aug 20, 2026
4ee9f03
test(vrt): regenerate baselines 2026-08-20T09:21:51Z
github-actions[bot] Aug 20, 2026
c65b6c1
refactor(ui): rework dynamic icon store and icon picker internals
infin1t3 Aug 20, 2026
1b87186
fix(test): stabilize VRT runs against browser-mode startup races
infin1t3 Aug 20, 2026
3b02499
test(vrt): regenerate baselines 2026-08-20T12:55:34Z
github-actions[bot] Aug 20, 2026
caa220a
refactor(ui): use Container/Typography atoms and trim single-use type…
infin1t3 Aug 20, 2026
f335d6b
Merge remote-tracking branch 'origin/dev' into bug-ui/1898-custom-fee…
infin1t3 Aug 20, 2026
a9c1079
Merge branch 'dev' into bug-ui/1898-custom-feeds-hover-interaction
aldertnl Aug 24, 2026
dfd6786
fix(ui): ui fixes
aldertnl Aug 24, 2026
031fee1
test(vrt): regenerate baselines 2026-08-24T14:38:51Z
github-actions[bot] Aug 24, 2026
90686eb
Merge remote-tracking branch 'origin/dev' into bug-ui/1898-custom-fee…
infin1t3 Aug 25, 2026
a51996d
test(vrt): drop ineffective optimizeDeps entry, correct the paralleli…
infin1t3 Aug 25, 2026
50757ac
test(vrt): run the suite in parallel again
infin1t3 Aug 25, 2026
2b35834
refactor(ui): move lucide helpers into libs/lucide and fix review fin…
infin1t3 Aug 25, 2026
a24579a
Merge branch 'dev' into bug-ui/1898-custom-feeds-hover-interaction
infin1t3 Aug 25, 2026
adb3dae
refactor(core): move the tagged-as reach constant out of the filter c…
infin1t3 Aug 25, 2026
e8fa276
fix(ui): harden the icon path against peer-controlled names and load …
infin1t3 Aug 25, 2026
a78ae14
Merge branch 'dev' into bug-ui/1898-custom-feeds-hover-interaction
Taewa Aug 25, 2026
1412eaf
Merge branch 'dev' into bug-ui/1898-custom-feeds-hover-interaction
Taewa Aug 25, 2026
23bbeb4
perf(ui): load picker icons as one chunk and drop the dead mobile dra…
infin1t3 Aug 26, 2026
b7534d7
Merge branch 'dev' into bug-ui/1898-custom-feeds-hover-interaction
infin1t3 Aug 26, 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
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,19 @@ describe('CustomFeedDialog', () => {
expect(input).toBeDisabled();
});

it('uses the feed prop in edit mode instead of the route feed', () => {
mockUseCustomFeed.mockReturnValue(createMockFeed({ id: 'route-feed', name: 'Route Feed' }));

render(
<CustomFeedDialog mode="edit" feed={createMockFeed({ id: 'passed-feed', name: 'Passed Feed' })}>
<button>Edit Feed</button>
</CustomFeedDialog>,
);

expect(screen.getByTestId('feed-name-input')).toHaveValue('Passed Feed');
expect(screen.getByTestId('custom-feed-dialog-trigger')).toHaveAttribute('data-disabled', 'false');
});

it('renders feed name input as enabled in create mode', () => {
render(
<CustomFeedDialog mode="create">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { useCustomFeed } from '@/hooks/useCustomFeed/useCustomFeed';
import { useCustomFeedMutation } from '@/hooks/useCustomFeedMutation/useCustomFeedMutation';
import { UsersRound2 } from '@/icons';
import { getMaxStreamTags } from '@/libs/runtime-config/runtime-config';
import type { FeedModelSchema } from '@/models/feed/feed.schema';
import { TAGGED_AS_FILTER_KEY } from '@/molecules/Filters/FilterReach/FilterReach';
import { PostTag } from '@/molecules/PostTag/PostTag';
import { TagInput } from '@/molecules/TagInput/TagInput';
Expand All @@ -47,17 +48,19 @@ import { HOME_PROFILE_TAGS_MAX_SELECTED } from '@/stores/home/home.types';
type CustomFeedDialogProps = {
mode: 'create' | 'edit';
children: ReactNode;
feed?: FeedModelSchema;
};
type CustomFeedDialogContent = PubkyAppPostKind | 'ALL';
type CustomFeedReachValue = PubkyAppFeedReach | typeof TAGGED_AS_FILTER_KEY;

function isVisualCustomFeedContentSupported(content?: CustomFeedDialogContent): boolean {
return content === 'ALL' || content === PubkyAppPostKind.Image || content === PubkyAppPostKind.Video;
}
export const CustomFeedDialog = ({ mode, children }: CustomFeedDialogProps) => {
export const CustomFeedDialog = ({ mode, children, feed }: CustomFeedDialogProps) => {
const router = useRouter();
const { toast } = useToast();
const customFeed = useCustomFeed();
const routeFeed = useCustomFeed();
const customFeed = feed ?? routeFeed;
const { commitCreate, commitUpdate, commitDelete, loading } = useCustomFeedMutation();
const [open, setOpen] = useState(false);
const [name, setName] = useState('');
Expand Down
116 changes: 81 additions & 35 deletions src/components/organisms/FeedNavigation/FeedNavigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,7 @@ describe('FeedNavigation', () => {

// Home link should always be present
expect(screen.getByText('Home')).toBeInTheDocument();

// Create Feed button should always be present
expect(screen.getByText('Create Feed')).toBeInTheDocument();
expect(screen.getByTestId('custom-feed-dialog-create')).toHaveTextContent('Feed');
});

// ── Home feed link ──────────────────────────────────────────────────────
Expand Down Expand Up @@ -271,8 +269,7 @@ describe('FeedNavigation', () => {
const typographies = screen.getAllByTestId('typography');
const feedNames = typographies.map((t) => t.textContent);

// Home is first, then custom feeds, then Create Feed
expect(feedNames).toEqual(['Home', 'Alpha Feed', 'Beta Feed', 'Gamma Feed', 'Create Feed']);
expect(feedNames).toEqual(['Home', 'Alpha Feed', 'Beta Feed', 'Gamma Feed', 'Feed']);
});

// ── Active / Inactive custom feed styling ───────────────────────────────
Expand All @@ -283,10 +280,9 @@ describe('FeedNavigation', () => {

render(<FeedNavigation />);

const links = screen.getAllByTestId('link');
const activeLink = links.find((link) => link.getAttribute('href') === '/feed/feed-active');
expect(activeLink).toHaveClass('border-white');
const activeLink = screen.getAllByTestId('link').find((link) => link.getAttribute('href') === '/feed/feed-active');
expect(activeLink).toHaveClass('text-white');
expect(activeLink?.parentElement).toHaveClass('border-white');
});

it('applies muted styling to a custom feed when its route does not match pathname', () => {
Expand All @@ -295,36 +291,64 @@ describe('FeedNavigation', () => {

render(<FeedNavigation />);

const links = screen.getAllByTestId('link');
const inactiveLink = links.find((link) => link.getAttribute('href') === '/feed/feed-inactive');
expect(inactiveLink).toHaveClass('border-border');
expect(inactiveLink).toHaveClass('text-muted-foreground');
const inactiveLink = screen
.getAllByTestId('link')
.find((link) => link.getAttribute('href') === '/feed/feed-inactive');
expect(inactiveLink).toHaveClass('text-muted-foreground', 'group-hover:text-white');
expect(inactiveLink?.parentElement).toHaveClass('border-border');
expect(inactiveLink?.parentElement).not.toHaveClass('hover:text-white');
});

// ── Edit dialog for active custom feed ──────────────────────────────────
// ── Edit dialog for custom feeds ────────────────────────────────────────

it('wraps custom feed icon in edit dialog when feed is active', () => {
mockCustomFeeds = [createMockFeed({ id: 'feed-edit', name: 'Editable Feed' })];
it('wraps the right-side pencil in an edit dialog for every custom feed', () => {
mockCustomFeeds = [
createMockFeed({ id: 'feed-edit', name: 'Editable Feed' }),
createMockFeed({ id: 'feed-other', name: 'Other Feed' }),
];
mockUsePathname.mockReturnValue('/feed/feed-edit');

render(<FeedNavigation />);

const editDialog = screen.getByTestId('custom-feed-dialog-edit');
expect(editDialog).toBeInTheDocument();
const editDialogs = screen.getAllByTestId('custom-feed-dialog-edit');
expect(editDialogs).toHaveLength(2);

editDialogs.forEach((editDialog) => {
const editButton = editDialog.querySelector('[data-testid="button"]');
expect(editButton).toBeInTheDocument();
expect(editButton).toHaveClass(
'absolute',
'right-3',
'text-muted-foreground',
'lg:opacity-0',
'lg:group-hover:opacity-100',
);
expect(editButton?.querySelector('svg')).toHaveClass('size-3');
});
});

it('keeps the pencil outside the feed link so it does not navigate', () => {
mockCustomFeeds = [createMockFeed({ id: 'feed-edit', name: 'Editable Feed' })];
mockUsePathname.mockReturnValue('/feed/feed-edit');

// The edit dialog should contain a button with the pencil icon
const editButton = editDialog.querySelector('[data-testid="button"]');
expect(editButton).toBeInTheDocument();
render(<FeedNavigation />);

const feedLink = screen.getAllByTestId('link').find((link) => link.getAttribute('href') === '/feed/feed-edit');
expect(feedLink?.querySelector('.lucide-pencil')).not.toBeInTheDocument();
expect(screen.getByTestId('custom-feed-dialog-edit').querySelector('.lucide-pencil')).toBeInTheDocument();
});

it('does not show edit dialog for inactive custom feed', () => {
mockCustomFeeds = [createMockFeed({ id: 'feed-noedit', name: 'No Edit Feed' })];
it('whitens only the custom feed name on hover, not the pencil', () => {
mockCustomFeeds = [createMockFeed({ id: 'feed-1', name: 'Test Feed' })];
mockUsePathname.mockReturnValue('/home');

render(<FeedNavigation />);

// There should be a create dialog but no edit dialog
expect(screen.queryByTestId('custom-feed-dialog-edit')).not.toBeInTheDocument();
const feedLink = screen.getAllByTestId('link').find((link) => link.getAttribute('href') === '/feed/feed-1');
expect(feedLink).toHaveClass('group-hover:text-white');
expect(screen.getByTestId('custom-feed-dialog-edit').querySelector('[data-testid="button"]')).toHaveClass(
'text-muted-foreground',
);
});

it('does not show edit dialog for Home feed even when active', () => {
Expand All @@ -337,15 +361,15 @@ describe('FeedNavigation', () => {

// ── Create Feed button ──────────────────────────────────────────────────

it('renders Create Feed button inside a create dialog', () => {
it('renders Feed button inside a create dialog', () => {
render(<FeedNavigation />);

const createDialog = screen.getByTestId('custom-feed-dialog-create');
expect(createDialog).toBeInTheDocument();
expect(createDialog).toHaveTextContent('Create Feed');
expect(createDialog).toHaveTextContent('Feed');
});

it('renders Create Feed button with PlusCircle icon', () => {
it('renders Feed button with PlusCircle icon', () => {
render(<FeedNavigation />);

const createDialog = screen.getByTestId('custom-feed-dialog-create');
Expand All @@ -371,7 +395,7 @@ describe('FeedNavigation', () => {

render(<FeedNavigation />);

fireEvent.click(screen.getByText('Create Feed'));
fireEvent.click(screen.getByTestId('button'));

expect(mockRequireAuth).toHaveBeenCalledTimes(1);
});
Expand All @@ -384,9 +408,8 @@ describe('FeedNavigation', () => {

render(<FeedNavigation />);

// Should still render Home and Create Feed even when getList fails
expect(screen.getByText('Home')).toBeInTheDocument();
expect(screen.getByText('Create Feed')).toBeInTheDocument();
expect(screen.getByTestId('custom-feed-dialog-create')).toHaveTextContent('Feed');
});

// ── Container and layout ────────────────────────────────────────────────
Expand All @@ -399,16 +422,39 @@ describe('FeedNavigation', () => {
expect(container).toHaveClass('overflow-x-auto');
});

it('renders all links with min-w-40 and h-12 classes', () => {
it('renders tabs with Figma chrome classes', () => {
mockCustomFeeds = [createMockFeed({ id: 'feed-1', name: 'Test Feed' })];
render(<FeedNavigation />);

const links = screen.getAllByTestId('link');
links.forEach((link) => {
expect(link).toHaveClass('min-w-40');
expect(link).toHaveClass('min-h-12');
const homeLink = screen.getAllByTestId('link').find((link) => link.getAttribute('href') === '/home');
expect(homeLink).toHaveClass('min-h-12', 'w-full', 'min-w-40', 'lg:flex-1');

const customTab = screen
.getAllByTestId('link')
.find((link) => link.getAttribute('href') === '/feed/feed-1')?.parentElement;
expect(customTab).toHaveClass('min-h-12', 'w-full', 'min-w-40', 'lg:flex-1');

screen.getAllByTestId('typography').forEach((label) => {
expect(label).toHaveClass('text-sm', 'leading-5');
});
});

it('does not use a left-side pencil as the custom feed icon', () => {
mockCustomFeeds = [createMockFeed({ id: 'feed-1', name: 'Test Feed' })];
render(<FeedNavigation />);

const customLink = screen.getAllByTestId('link').find((link) => link.getAttribute('href') === '/feed/feed-1');
expect(customLink?.querySelector('svg')).not.toBeInTheDocument();
});

it('does not show a pencil on Home or Create Feed', () => {
render(<FeedNavigation />);

const homeLink = screen.getAllByTestId('link').find((link) => link.getAttribute('href') === '/home');
expect(homeLink?.querySelector('svg')).toHaveClass('lucide-house');
expect(homeLink?.querySelector('.lucide-pencil')).not.toBeInTheDocument();
expect(screen.getByTestId('custom-feed-dialog-create').querySelector('.lucide-pencil')).not.toBeInTheDocument();
});
});

// ---------------------------------------------------------------------------
Expand Down
Loading
Loading