Move pond settings to a TopBar gear, pin the trash link to the sidebar bottom (M10 follow-up)
All checks were successful
CD / Build and push images (push) Successful in 1m8s
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m11s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 3m42s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m31s
CI / Import/export fidelity gate (push) Successful in 47s

- owners get a Settings icon next to the pond name while a pond route
  is active (same lucide set, localized aria-label/tooltip via the
  existing labels:link key); gone on non-pond routes and for non-owners
- the trash stays a text link but pins to the sidebar's bottom
  (.sidebar is a flex column now; .sidebar__footer uses margin-top:auto)
- trash.spec: delete flows go through the #101 overflow menu (was
  missed in 65f30a5 — the pack is not part of CI)
- markdown.spec: replace the wait for the 'saved' status removed in #36
  with polling the export endpoint (pre-existing local failure, same
  category as the known image.spec one)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
This commit is contained in:
Claude Fable 5 2026-07-12 06:07:15 +02:00
parent e740ea6c01
commit 33121cd73d
5 changed files with 61 additions and 17 deletions

View File

@ -117,7 +117,15 @@ test('page menu downloads the page as Markdown matching its content', async ({ b
await enterEditMode(page);
await page.locator('.ProseMirror').click();
await page.keyboard.type('export me please');
await expect(page.getByRole('status')).toHaveText(/saved|gespeichert/i, { timeout: 10000 });
// There is no "saved" status since #36 (collab autosaves continuously) —
// poll the export endpoint until the typed content has been persisted.
await expect
.poll(
async () => (await context.request.get(`/api/v1/pages/${pageId}/export/markdown`)).text(),
{ timeout: 10000 },
)
.toBe('export me please');
const exported = await context.request.get(`/api/v1/pages/${pageId}/export/markdown`);
expect(exported.headers()['content-disposition']).toContain(`${pageSlug}.md`);

View File

@ -43,7 +43,9 @@ test('deleting a page hides it from the sidebar and shows a trash hint on direct
await page.goto(`/p/${pondSlug}/${pageSlug}`);
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
await page.getByRole('button', { name: /move to trash|papierkorb verschieben/i }).click();
// Delete sits behind the TopBar overflow menu since #101.
await page.getByRole('button', { name: /more actions|weitere aktionen/i }).click();
await page.getByRole('menuitem', { name: /move to trash|papierkorb verschieben/i }).click();
// Deleting navigates away; going back to the same URL now 404s with a hint.
await page.goto(`/p/${pondSlug}/${pageSlug}`);
@ -64,7 +66,9 @@ test('restoring a page from the trash brings it back', async ({ browser }) => {
await page.goto(`/p/${pondSlug}/${pageSlug}`);
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
await page.getByRole('button', { name: /move to trash|papierkorb verschieben/i }).click();
// Delete sits behind the TopBar overflow menu since #101.
await page.getByRole('button', { name: /more actions|weitere aktionen/i }).click();
await page.getByRole('menuitem', { name: /move to trash|papierkorb verschieben/i }).click();
await page.goto(`/p/${pondSlug}/trash`);
const item = trashItem(page, title);
@ -87,7 +91,9 @@ test('purging a page from the trash removes it for good', async ({ browser }) =>
await page.goto(`/p/${pondSlug}/${pageSlug}`);
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
await page.getByRole('button', { name: /move to trash|papierkorb verschieben/i }).click();
// Delete sits behind the TopBar overflow menu since #101.
await page.getByRole('button', { name: /more actions|weitere aktionen/i }).click();
await page.getByRole('menuitem', { name: /move to trash|papierkorb verschieben/i }).click();
await page.goto(`/p/${pondSlug}/trash`);
const item = trashItem(page, title);

View File

@ -126,16 +126,6 @@ export function Sidebar({ collapsed }: SidebarProps): React.JSX.Element {
))}
</select>
)}
{isOwner && (
<Link to={`/p/${pondSlug}/settings`} className="linklike sidebar__settings-link">
{tLabels('link')}
</Link>
)}
{isOwner && (
<Link to={`/p/${pondSlug}/trash`} className="linklike sidebar__trash-link">
{t('editor:trash.link')}
</Link>
)}
</div>
{flat.length > 0 && (
@ -282,6 +272,16 @@ export function Sidebar({ collapsed }: SidebarProps): React.JSX.Element {
<p className="visually-hidden sidebar__announce" role="status" aria-live="polite">
{announcement}
</p>
{/* The trash stays a text link, pinned to the sidebar's bottom
(M10 follow-up; pond settings moved to the TopBar gear icon). */}
{isOwner && (
<div className="sidebar__footer">
<Link to={`/p/${pondSlug}/trash`} className="linklike sidebar__trash-link">
{t('editor:trash.link')}
</Link>
</div>
)}
</>
)}
</nav>

View File

@ -1,13 +1,17 @@
import { Menu, Search } from 'lucide-react';
import type { PondView } from '@dorfteich/shared';
import { useQuery } from '@tanstack/react-query';
import { Menu, Search, Settings } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../auth/auth-context';
import { apiGet } from '../lib/api';
import { SearchPalette } from '../search/SearchPalette';
import { NotificationsBell } from '../notifications/NotificationsBell';
import { usePageActionsSlot } from './page-actions';
import { PondSwitcher } from './PondSwitcher';
import { useCurrentPondRoute } from './use-pond-route';
/** True when focus is in a field where "/" should type, not open search. */
function isTypingTarget(target: EventTarget | null): boolean {
@ -28,6 +32,15 @@ export function TopBar({ sidebarCollapsed, onToggleSidebar }: TopBarProps): Reac
const [menuOpen, setMenuOpen] = useState(false);
const [searchOpen, setSearchOpen] = useState(false);
const { setElement: setPageActionsElement, setPresenceElement } = usePageActionsSlot();
// Pond-settings shortcut next to the pond name (M10 follow-up): owners get
// a gear icon while a pond route is active. Shares the sidebar's query key.
const { pondSlug } = useCurrentPondRoute();
const pond = useQuery({
queryKey: ['pond', pondSlug],
queryFn: () => apiGet<PondView>(`/ponds/${pondSlug}`),
enabled: Boolean(user && pondSlug),
});
const isPondOwner = Boolean(user && pond.data && user.id === pond.data.ownerId);
async function handleLogout(): Promise<void> {
setMenuOpen(false);
@ -63,6 +76,16 @@ export function TopBar({ sidebarCollapsed, onToggleSidebar }: TopBarProps): Reac
Dorfteich
</Link>
{user && <PondSwitcher />}
{isPondOwner && pondSlug && (
<Link
to={`/p/${pondSlug}/settings`}
className="icon-button topbar__pond-settings"
aria-label={t('labels:link')}
title={t('labels:link')}
>
<Settings aria-hidden />
</Link>
)}
<span className="topbar__spacer" />
{/* Page-scoped slots, rendered only for signed-in users: live presence
(#102) and the page's icon actions (#101) portal in while a page

View File

@ -88,6 +88,9 @@ button {
.sidebar {
width: var(--sidebar-width);
flex-shrink: 0;
/* Column flex so the trash footer can pin to the bottom (margin-top: auto). */
display: flex;
flex-direction: column;
border-right: 1px solid var(--color-border);
background: var(--color-bg-subtle);
padding: var(--space-4);
@ -176,6 +179,9 @@ button {
.sidebar__new-page {
font-size: 0.9rem;
/* The sidebar is a flex column now; keep the stretched button reading
like the left-aligned link it used to be. */
text-align: left;
}
.sidebar__new-page-form {
@ -1447,8 +1453,9 @@ button {
cursor: pointer;
}
.sidebar__settings-link {
font-size: 0.85rem;
.sidebar__footer {
margin-top: auto;
padding-top: var(--space-4);
}
/* Pond settings page + label manager. */