Skip to content

Commit 398e180

Browse files
feat: show current page title in mobile header bar (#1136)
Co-authored-by: Ona <no-reply@ona.com>
1 parent 51e5df1 commit 398e180

13 files changed

Lines changed: 340 additions & 11 deletions

.agents/architecture.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ src/
449449
│ │ ├── page-search.tsx # Full-text search input + results dropdown (debounced, 300ms)
450450
│ │ ├── favorites-section.tsx # Per-user favorites list + useFavorite hook for toggle
451451
│ │ ├── focus-mode-hint.tsx # Floating hint shown when sidebar is hidden in focus mode
452+
│ │ ├── mobile-header-title.tsx # Observes document.title and displays current page title in mobile header
452453
│ │ ├── page-tree.tsx # Orchestrator: data fetching, state, delete dialog (uses extracted sub-modules)
453454
│ │ ├── page-tree-item.tsx # Single tree node rendering + context menu
454455
│ │ ├── page-tree-drag-layer.ts # usePageTreeDrag hook: drag-and-drop state + handlers

.agents/quality.md

Lines changed: 6 additions & 5 deletions
Large diffs are not rendered by default.

e2e/mobile-header-title.spec.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { test, expect } from "./fixtures/auth";
2+
3+
const MOBILE_VIEWPORT = { width: 375, height: 667 };
4+
const DESKTOP_VIEWPORT = { width: 1280, height: 720 };
5+
6+
test.describe("Mobile header page title", () => {
7+
test("shows workspace name on workspace home at mobile viewport", async ({
8+
authenticatedPage: page,
9+
}) => {
10+
await page.setViewportSize(MOBILE_VIEWPORT);
11+
await page.goto("/");
12+
13+
// Wait for the app shell to load
14+
const toggleButton = page.getByTestId("as-sidebar-toggle");
15+
await expect(toggleButton).toBeVisible({ timeout: 15_000 });
16+
17+
// The mobile header title should display the workspace name
18+
const titleEl = page.getByTestId("mobile-header-title");
19+
await expect(titleEl).toBeVisible({ timeout: 10_000 });
20+
21+
// The title should be non-empty (workspace name)
22+
const titleText = await titleEl.textContent();
23+
expect(titleText).toBeTruthy();
24+
expect(titleText!.trim().length).toBeGreaterThan(0);
25+
});
26+
27+
test("updates title when navigating to a page via sidebar", async ({
28+
authenticatedPage: page,
29+
}) => {
30+
// Start at desktop to get workspace slug, then switch to mobile
31+
await page.goto("/");
32+
await page.waitForURL((url) => !url.pathname.includes("/sign-in"), {
33+
timeout: 15_000,
34+
});
35+
36+
// Get workspace slug from URL
37+
const currentUrl = new URL(page.url());
38+
const pathParts = currentUrl.pathname.split("/").filter(Boolean);
39+
const workspaceSlug = pathParts[0];
40+
if (!workspaceSlug) {
41+
test.skip(true, "Could not determine workspace slug");
42+
return;
43+
}
44+
45+
// Switch to mobile viewport
46+
await page.setViewportSize(MOBILE_VIEWPORT);
47+
48+
// Open sidebar
49+
const toggleButton = page.getByTestId("as-sidebar-toggle");
50+
await expect(toggleButton).toBeVisible({ timeout: 10_000 });
51+
await toggleButton.click();
52+
53+
const sheetContent = page.locator('[data-slot="sheet-content"]');
54+
await expect(sheetContent).toBeVisible({ timeout: 5_000 });
55+
56+
// Wait for the page tree to load
57+
const treeItem = sheetContent.locator('[role="treeitem"]').first();
58+
try {
59+
await expect(treeItem).toBeVisible({ timeout: 10_000 });
60+
} catch {
61+
// No pages — create one via sidebar
62+
const newPageBtn = sheetContent.getByTestId("sb-new-page-btn");
63+
if ((await newPageBtn.count()) > 0) {
64+
await newPageBtn.click();
65+
await page.waitForURL(
66+
(url) => url.pathname.split("/").filter(Boolean).length >= 2,
67+
{ timeout: 15_000 },
68+
);
69+
// After navigation, the mobile header title should be visible
70+
const titleEl = page.getByTestId("mobile-header-title");
71+
await expect(titleEl).toBeVisible({ timeout: 10_000 });
72+
return;
73+
}
74+
test.skip(true, "No pages and no new-page button available");
75+
return;
76+
}
77+
78+
// Record URL before clicking
79+
const urlBefore = page.url();
80+
81+
// Click to navigate
82+
await treeItem.click();
83+
84+
// Wait for URL to change
85+
await page.waitForURL((url) => url.href !== urlBefore, {
86+
timeout: 15_000,
87+
});
88+
89+
// The mobile header title should update to show the page title
90+
const titleEl = page.getByTestId("mobile-header-title");
91+
await expect(titleEl).toBeVisible({ timeout: 10_000 });
92+
93+
// Title should be non-empty
94+
const titleText = await titleEl.textContent();
95+
expect(titleText).toBeTruthy();
96+
expect(titleText!.trim().length).toBeGreaterThan(0);
97+
});
98+
99+
test("shows Settings on settings page", async ({
100+
authenticatedPage: page,
101+
}) => {
102+
await page.goto("/");
103+
await page.waitForURL((url) => !url.pathname.includes("/sign-in"), {
104+
timeout: 15_000,
105+
});
106+
107+
// Get workspace slug from URL
108+
const currentUrl = new URL(page.url());
109+
const pathParts = currentUrl.pathname.split("/").filter(Boolean);
110+
const workspaceSlug = pathParts[0];
111+
if (!workspaceSlug) {
112+
test.skip(true, "Could not determine workspace slug");
113+
return;
114+
}
115+
116+
await page.setViewportSize(MOBILE_VIEWPORT);
117+
118+
// Navigate to settings
119+
await page.goto(`/${workspaceSlug}/settings`);
120+
121+
// Wait for the settings page to load
122+
const titleEl = page.getByTestId("mobile-header-title");
123+
await expect(titleEl).toBeVisible({ timeout: 15_000 });
124+
125+
// Title should contain "Settings"
126+
await expect(titleEl).toContainText("Settings", { timeout: 10_000 });
127+
});
128+
129+
test("header is hidden on desktop viewport", async ({
130+
authenticatedPage: page,
131+
}) => {
132+
await page.setViewportSize(DESKTOP_VIEWPORT);
133+
await page.goto("/");
134+
135+
await page.waitForURL((url) => !url.pathname.includes("/sign-in"), {
136+
timeout: 15_000,
137+
});
138+
139+
// The entire mobile header (including the title) should be hidden on desktop
140+
// because the header has md:hidden class
141+
const mobileHeader = page.locator("header").filter({
142+
has: page.getByTestId("as-sidebar-toggle"),
143+
});
144+
145+
await expect(mobileHeader).toBeHidden({ timeout: 5_000 });
146+
});
147+
148+
test("title element has truncation styles", async ({
149+
authenticatedPage: page,
150+
}) => {
151+
await page.setViewportSize(MOBILE_VIEWPORT);
152+
await page.goto("/");
153+
154+
const toggleButton = page.getByTestId("as-sidebar-toggle");
155+
await expect(toggleButton).toBeVisible({ timeout: 15_000 });
156+
157+
const titleEl = page.getByTestId("mobile-header-title");
158+
await expect(titleEl).toBeVisible({ timeout: 10_000 });
159+
160+
// Verify the element has overflow hidden via the truncate class
161+
const overflow = await titleEl.evaluate(
162+
(el) => window.getComputedStyle(el).overflow,
163+
);
164+
expect(overflow).toBe("hidden");
165+
166+
const textOverflow = await titleEl.evaluate(
167+
(el) => window.getComputedStyle(el).textOverflow,
168+
);
169+
expect(textOverflow).toBe("ellipsis");
170+
});
171+
});
20.1 KB
Loading
4.99 KB
Loading
7.95 KB
Loading
7.02 KB
Loading
8.2 KB
Loading
7.25 KB
Loading

src/components/sidebar/app-shell.stories.tsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,26 +134,54 @@ export const SidebarCollapsed: Story = {
134134
),
135135
};
136136

137-
/** Mobile layout — sidebar as overlay sheet. */
137+
/** Mobile layout — sidebar as overlay sheet with page title in header. */
138138
export const MobileLayout: Story = {
139139
render: () => (
140140
<div className="h-[500px] w-[375px] overflow-hidden bg-background">
141141
<header className="flex h-10 items-center gap-2 border-b border-overlay-border px-4">
142142
<button
143143
type="button"
144-
className="flex h-8 w-8 items-center justify-center rounded-sm text-muted-foreground hover:bg-overlay-hover"
144+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-overlay-hover"
145145
aria-label="Toggle sidebar"
146146
>
147147
<PanelLeft className="h-4 w-4" />
148148
</button>
149-
<span className="text-sm font-medium text-foreground">
150-
My Workspace
149+
<span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
150+
Getting Started
151+
</span>
152+
</header>
153+
<main className="flex-1 overflow-y-auto p-4">
154+
<h1 className="text-xl font-bold text-foreground">Getting Started</h1>
155+
<p className="mt-4 text-sm text-foreground">
156+
Mobile layout with sidebar toggle and page title in the header.
157+
</p>
158+
</main>
159+
</div>
160+
),
161+
};
162+
163+
/** Mobile layout — long title truncated with ellipsis. */
164+
export const MobileLongTitle: Story = {
165+
render: () => (
166+
<div className="h-[500px] w-[375px] overflow-hidden bg-background">
167+
<header className="flex h-10 items-center gap-2 border-b border-overlay-border px-4">
168+
<button
169+
type="button"
170+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-overlay-hover"
171+
aria-label="Toggle sidebar"
172+
>
173+
<PanelLeft className="h-4 w-4" />
174+
</button>
175+
<span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
176+
This Is a Very Long Page Title That Should Be Truncated
151177
</span>
152178
</header>
153179
<main className="flex-1 overflow-y-auto p-4">
154-
<h1 className="text-xl font-bold text-foreground">Page Title</h1>
180+
<h1 className="text-xl font-bold text-foreground">
181+
This Is a Very Long Page Title That Should Be Truncated
182+
</h1>
155183
<p className="mt-4 text-sm text-foreground">
156-
Mobile layout with sidebar toggle in the header.
184+
The header title truncates with ellipsis when it overflows.
157185
</p>
158186
</main>
159187
</div>

0 commit comments

Comments
 (0)