The bar's markup moved into the design system (BottomNav 0.35.4) and BottomBar became what only this app can know: which sections exist, what each one routes to, and that tapping Conversation on home brings the tucked-away composer back instead of navigating nowhere. What is new for the user: - a second level. Tap the section you are already in and its pages rise from behind the bar — Dashboards (activity, analytics, environment, graph), Files (memories, plans, goals, skills, agents, notifs) and Settings (shortcuts, tags, cron). Five slots per drawer; the rest wait in the hold-to-rearrange stash, and both orders persist server-side under the new "nav-sub" list. - swipe navigation. A left/right swipe on the content area walks the bar one section at a time, with the page following the finger; inside an expanded section it walks that section's pages and steps out past either end. Swipes that start in a horizontal scroller or on the bar itself are left alone. nav.ts grew SUB_SECTIONS (the second level, with the route prefixes each entry owns) and navKeyOf(), which resolves a path to the finest-grained bar destination — sectionOf() is unchanged and still owns the sections. Also the frontend's first test setup: vitest + testing-library + jsdom, 60 tests covering the bar (sections, active route, taps, the composer tap, drawers, swipes) and the route→section/sub-section mapping.
65 lines
2.2 KiB
TypeScript
65 lines
2.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { navKeyOf, sectionOf, SUB_SECTIONS } from "./nav";
|
|
|
|
describe("sectionOf", () => {
|
|
it.each([
|
|
["/", "conversation"],
|
|
["/conversation", "conversation"],
|
|
["/conversation/abc.jsonl", "conversation"],
|
|
["/conversations", "conversation"],
|
|
["/dashboards", "dashboards"],
|
|
["/dashboards/activity", "dashboards"],
|
|
["/graph", "dashboards"],
|
|
["/conversation/dashboard", "dashboards"],
|
|
["/files", "files"],
|
|
["/_/CLAUDE.md", "files"],
|
|
["/memories", "files"],
|
|
["/plans/2026-08-11", "files"],
|
|
["/goals", "files"],
|
|
["/skills", "files"],
|
|
["/agents/radar", "files"],
|
|
["/notifications", "files"],
|
|
["/projects", "projects"],
|
|
["/services/traefik", "projects"],
|
|
["/templates", "projects"],
|
|
["/settings", "settings"],
|
|
["/settings/cron", "settings"],
|
|
])("puts %s in %s", (path, section) => {
|
|
expect(sectionOf(path)).toBe(section);
|
|
});
|
|
});
|
|
|
|
describe("navKeyOf", () => {
|
|
it("falls back to the section when no sub-link owns the route", () => {
|
|
expect(navKeyOf("/")).toBe("conversation");
|
|
expect(navKeyOf("/files")).toBe("files");
|
|
expect(navKeyOf("/projects")).toBe("projects");
|
|
expect(navKeyOf("/dashboards")).toBe("dashboards");
|
|
});
|
|
|
|
it.each([
|
|
["/memories", "files/memories"],
|
|
["/memories/some-slug", "files/memories"],
|
|
["/plans", "files/plans"],
|
|
["/goals", "files/goals"],
|
|
["/skills", "files/skills"],
|
|
["/agents/radar", "files/agents"],
|
|
["/notifications", "files/notifs"],
|
|
["/dashboards/activity", "dashboards/activity"],
|
|
["/dashboards/conversations", "dashboards/conversations"],
|
|
["/dashboards/environnement", "dashboards/environnement"],
|
|
["/graph", "dashboards/graph"],
|
|
["/settings/shortcuts", "settings/shortcuts"],
|
|
["/settings/tags", "settings/tags"],
|
|
["/settings/cron", "settings/cron"],
|
|
])("resolves %s to %s", (path, key) => {
|
|
expect(navKeyOf(path)).toBe(key);
|
|
});
|
|
|
|
it("keeps every sub-link key namespaced under its own section", () => {
|
|
for (const [section, entries] of Object.entries(SUB_SECTIONS))
|
|
for (const e of entries) expect(e.key.startsWith(`${section}/`)).toBe(true);
|
|
});
|
|
});
|