Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 21 additions & 1 deletion packages/e2e/tests/home-recovery.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,11 @@ test('web app root reopens the last Space and people panel links back to overvie
await page.goto(`/${first.project.slug}`);
await expect(page.getByTestId('project-name')).toHaveText('First Space');

// The Spaces overview is reached from the people panel, not a header button.
// The People panel keeps account and Space-switching actions tucked away
// until they are needed.
await page.getByTestId('presence-bar').click();
await expect(page.getByTestId('member-list-panel')).toBeVisible();
await page.getByTestId('account-toggle-button').click();
// Other known Spaces are listed for quick switching.
await expect(page.getByTestId('panel-space-link')).toContainText(['Second Space']);
await page.getByTestId('panel-space-link').click();
Expand All @@ -149,8 +151,26 @@ test('web app root reopens the last Space and people panel links back to overvie
await expect(page.getByTestId('project-name')).toHaveText('Second Space');

await page.getByTestId('presence-bar').click();
await page.getByTestId('account-toggle-button').click();
await page.getByTestId('spaces-overview-link').click();
await expect(page).toHaveURL('/spaces');
await expect(page.getByTestId('known-spaces')).toBeVisible();
await expect(page.getByTestId('known-space-link')).toContainText(['First Space', 'Second Space']);
});

// The panel route above still works, but it is no longer the only way out: the
// header carries a plain link so leaving a Space costs one tap, not three.
test('the Space header links straight back to the Spaces overview', async ({ page }) => {
const { project, token, member } = await createProjectViaApi('Header Space', 'Owner');

await page.goto('/');
await seedIdentity(page, project.slug, token, member.id, 'Header Space');

await page.goto(`/${project.slug}`);
await expect(page.getByTestId('project-name')).toHaveText('Header Space');

await page.getByTestId('header-spaces-overview-link').click();
await expect(page).toHaveURL('/spaces');
await expect(page.getByTestId('known-spaces')).toBeVisible();
await expect(page.getByTestId('known-space-link')).toContainText(['Header Space']);
});
3 changes: 3 additions & 0 deletions packages/e2e/tests/join-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ test('leaving your last Space clears the device-wide saved email', async ({ page
await page.goto(`/${project.slug}`);
await page.getByTestId('presence-bar').click();
await expect(page.getByTestId('member-list-panel')).toBeVisible();
await page.getByTestId('account-toggle-button').click();
await expect(page.getByTestId('forget-plainspace-email-button')).toBeVisible();

await page.getByTestId('leave-space-button').click();
Expand All @@ -202,6 +203,7 @@ test('signing out revokes this browser session without leaving the Space', async
await page.goto(`/${project.slug}`);
await page.getByTestId('presence-bar').click();
await expect(page.getByTestId('member-list-panel')).toBeVisible();
await page.getByTestId('account-toggle-button').click();
await page.getByTestId('sign-out-button').click();

await expect(page).toHaveURL(/\/$/);
Expand Down Expand Up @@ -233,6 +235,7 @@ test('leaving one of several Spaces keeps the saved email', async ({ page }) =>
await page.goto(`/${spaceA.project.slug}`);
await page.getByTestId('presence-bar').click();
await expect(page.getByTestId('member-list-panel')).toBeVisible();
await page.getByTestId('account-toggle-button').click();

await page.getByTestId('leave-space-button').click();
await page.getByTestId('confirm-dialog-confirm').click();
Expand Down
1 change: 1 addition & 0 deletions packages/e2e/tests/recover-login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ test('people panel lists a Space the verified email belongs to without a local t
await expect(page.getByTestId('project-name')).toHaveText('Home Space');
await page.getByTestId('presence-bar').click();
await expect(page.getByTestId('member-list-panel')).toBeVisible();
await page.getByTestId('account-toggle-button').click();
await expect(page.getByTestId('panel-space-link')).toContainText(['Faraway Space']);

// A server-discovered Space (no local token) deep-links into open-by-email so
Expand Down
1 change: 1 addition & 0 deletions packages/e2e/tests/reminders.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ test.describe('reminders', () => {

await page.getByTestId('presence-bar').click();
await expect(page.getByTestId('member-list-panel')).toBeVisible();
await page.getByTestId('account-toggle-button').click();
await page.getByTestId('leave-space-button').click();
await page.getByTestId('confirm-dialog-confirm').click();

Expand Down
53 changes: 52 additions & 1 deletion packages/web/src/components/layout/Header.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,32 @@
flex-direction: column;
}

.backLink {
display: inline-flex;
align-items: center;
align-self: flex-start;
gap: var(--space-xs);
margin-bottom: var(--space-xs);
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
font-family: var(--font-sans);
font-size: var(--text-xs);
font-weight: 600;
letter-spacing: var(--tracking-wide);
line-height: var(--leading-snug);
text-decoration: none;
text-transform: uppercase;
}

.backLink:hover {
color: var(--color-accent-hover);
}

.backLink:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}

.name {
font-family: var(--font-serif);
font-size: clamp(1.5rem, 0.5rem + 4vw, 3.5rem);
Expand All @@ -66,6 +92,10 @@
letter-spacing: var(--tracking-tight);
}

.header.scrolled .backLink {
margin-bottom: 0;
}

.purpose {
font-family: var(--font-serif);
font-style: italic;
Expand Down Expand Up @@ -182,7 +212,10 @@
border-radius: 999px;
border: 1px dashed var(--color-accent);
background: transparent;
color: var(--color-accent);
/* Label sits on the page background, so it takes the darker (light theme) /
lighter (dark theme) accent for contrast; the border keeps the plain
accent so the dashed outline stays quiet next to it. */
color: var(--color-accent-hover);
font-size: var(--text-sm);
font-weight: 500;
cursor: pointer;
Expand Down Expand Up @@ -289,6 +322,24 @@
padding-bottom: 12px;
}

/* Phones can't spend a whole line on a back-link: in flow it made the header
21% taller, pushing content below the fold on a 851px viewport. Out of flow
it rides in the padding .inner already reserves, so the header keeps the
exact height it had before the link existed. Hit height is unchanged from
the in-flow version; the negative inline margin widens it a little. */
.titleBlock {
position: relative;
}

.backLink {
position: absolute;
bottom: 100%;
left: 0;
margin-bottom: 1px;
padding-inline: 6px;
margin-left: -6px;
}

.name {
display: -webkit-box;
overflow: hidden;
Expand Down
78 changes: 78 additions & 0 deletions packages/web/src/components/layout/Header.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen } from '@solidjs/testing-library';
import type { JSX } from 'solid-js';
import type { Member, Project } from '@plainspace/shared';

vi.mock('@solidjs/router', () => ({
A: (props: {
href: string;
children?: JSX.Element;
class?: string;
'aria-label'?: string;
'data-testid'?: string;
}) => (
<a
href={props.href}
class={props.class}
aria-label={props['aria-label']}
data-testid={props['data-testid']}
>
{props.children}
</a>
),
}));

vi.mock('../members/MemberList', () => ({ default: () => null }));

import Header from './Header';

const project = {
id: 'project-1',
slug: 'weekend',
name: 'Weekend away',
purpose: 'Plan the trip',
sharingMode: 'open',
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
} as Project;

const member = {
id: 'member-1',
projectId: 'project-1',
displayName: 'Maya',
color: '#123456',
avatarIndex: 0,
email: null,
emailVerified: false,
isCreator: true,
role: 'admin',
tosVersion: null,
tosAcceptedAt: null,
joinedAt: '2026-01-01T00:00:00.000Z',
} as Member;

describe('Header', () => {
beforeEach(() => {
// Only `.matches` is read (the scroll handler's mobile check).
vi.stubGlobal('matchMedia', vi.fn().mockReturnValue({ matches: false }));
});

afterEach(() => vi.unstubAllGlobals());

it('offers a dedicated route back to the Spaces overview', () => {
render(() => (
<Header
project={project}
members={[member]}
presence={[]}
slug="weekend"
myId={member.id}
myRole="admin"
isCreator
/>
));

const overview = screen.getByRole('link', { name: /spaces overview/i });
expect(overview.getAttribute('href')).toBe('/spaces');
});
});
10 changes: 10 additions & 0 deletions packages/web/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { For, createSignal, onCleanup, onMount, Show } from 'solid-js';
import { A } from '@solidjs/router';
import type { Project, Member } from '@plainspace/shared';
import { copyJoinLink } from '../../lib/join-link';
import MemberList from '../members/MemberList';
Expand Down Expand Up @@ -76,6 +77,15 @@ export default function Header(props: HeaderProps) {
>
<div class={styles.inner}>
<div class={styles.titleBlock}>
<A
href="/spaces"
class={styles.backLink}
aria-label="Spaces overview"
data-testid="header-spaces-overview-link"
>
<span aria-hidden="true">←</span>
<span>Spaces</span>
</A>
<h1 class={styles.name} data-testid="project-name">
{props.project.name}
</h1>
Expand Down
32 changes: 7 additions & 25 deletions packages/web/src/components/members/MemberList.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,25 +59,18 @@
border-bottom: 0;
}

.sectionTitle {
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--color-text-secondary);
margin: 0 0 var(--space-sm);
}

.onlineCount {
font-weight: 600;
color: var(--color-online-text);
}

.sectionHeader {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-sm);
.subsectionTitle {
margin: 0 0 var(--space-sm);
color: var(--color-text-secondary);
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
}

.list {
Expand Down Expand Up @@ -179,17 +172,6 @@
border: 1px solid var(--color-border);
}

.advancedBody {
display: flex;
flex-direction: column;
gap: var(--space-md);
margin-top: var(--space-md);
}

.advancedBody[hidden] {
display: none;
}

.helpText {
font-size: var(--text-xs);
color: var(--color-text-secondary);
Expand Down
Loading
Loading