diff --git a/client/src/components/Collab/CollabLinks.test.tsx b/client/src/components/Collab/CollabLinks.test.tsx
index f134d6819..4b2d7e7c3 100644
--- a/client/src/components/Collab/CollabLinks.test.tsx
+++ b/client/src/components/Collab/CollabLinks.test.tsx
@@ -63,7 +63,8 @@ describe('CollabLinks', () => {
);
render();
expect(await screen.findByText('Ferry timetable')).toBeInTheDocument();
- expect(screen.getByText('https://ferries.example/timetable')).toBeInTheDocument();
+ // The chip shows the host, not the whole address, and links to the address itself.
+ expect(screen.getByText('ferries.example')).toBeInTheDocument();
expect(screen.getByRole('link', { name: /ferry timetable/i })).toHaveAttribute('href', 'https://ferries.example/timetable');
});
@@ -144,17 +145,19 @@ describe('CollabLinks', () => {
await waitFor(() => expect(put).toEqual({ title: 'Ferry timetable 2026', url: 'https://ferries.example/2026' }));
await waitFor(() => expect(screen.queryByLabelText(/link title|collab\.links\.titlePlaceholder/i)).not.toBeInTheDocument());
expect(await screen.findByText('Ferry timetable 2026')).toBeInTheDocument();
- expect(screen.getByText('https://ferries.example/2026')).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: /ferry timetable 2026/i })).toHaveAttribute('href', 'https://ferries.example/2026');
});
- it('FE-COMP-LINKS-010: the external-link glyph opens the address in a new tab', async () => {
+ it('FE-COMP-LINKS-010: the chip is the link and opens the address in a new tab', async () => {
server.use(
http.get('/api/trips/1/collab/links', () => HttpResponse.json({ links: [buildLink()] })),
);
render();
- const open = await screen.findByRole('link', { name: /open link|collab\.links\.open/i });
+ const open = await screen.findByRole('link', { name: /ferry timetable/i });
expect(open).toHaveAttribute('href', 'https://ferries.example/timetable');
expect(open).toHaveAttribute('target', '_blank');
+ // The host stands beside the title; the whole address would not fit a chip.
+ expect(screen.getByText('ferries.example')).toBeInTheDocument();
});
it('FE-COMP-LINKS-008: a viewer without edit rights gets no add button', async () => {
diff --git a/client/src/components/Collab/CollabLinks.tsx b/client/src/components/Collab/CollabLinks.tsx
index 43f285c67..be716b693 100644
--- a/client/src/components/Collab/CollabLinks.tsx
+++ b/client/src/components/Collab/CollabLinks.tsx
@@ -18,12 +18,20 @@ interface CollabLink {
pinned: boolean | number
}
-function LinkIcon({ url, title }: { url: string; title: string }) {
+function LinkIcon({ url }: { url: string }) {
const [failed, setFailed] = useState(false)
let favicon = ''
try { favicon = new URL('/favicon.ico', url).href } catch { /* not a parseable url, fall through to the glyph */ }
- if (!favicon || failed) return
- return setFailed(true)} style={{ width: 28, height: 28, flex: '0 0 28px', borderRadius: 7, objectFit: 'contain', background: 'var(--bg-secondary)' }} />
+ return (
+
+ {!favicon || failed ? : setFailed(true)} />}
+
+ )
+}
+
+/** The part of the address worth a chip's width: the host, without a leading www. */
+function hostOf(url: string): string {
+ try { return new URL(url).hostname.replace(/^www\./, '') } catch { return url }
}
/**
@@ -211,35 +219,36 @@ export default function CollabLinks({ tripId }: { tripId: number }) {
)}
- {/* Content */}
-
+ {/* Content: one chip per link, flowing like tags (see .collab-link-chip). The chip
+ is the link, with the host beside the title; the actions unfold from its tail. */}
+
+
+
+
+ {link.title}
+ {hostOf(link.url)}
+
+
- {/* The glyph used to be decoration next to a row that was already a link,
- which read as a button that did nothing (#2414). */}
-
-
-
- {canEdit && <>
-
-
-
- >}
+ {canEdit && (
+
+
+
+
+
+ )}