Skip to content

Commit 205a8a0

Browse files
Your Namecursoragent
andcommitted
Real URL routes per lane: /w/<lane> optional catch-all, shallow history navigation with popstate sync, sidebar items become real links (middle/ctrl-click friendly)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 0b35a41 commit 205a8a0

4 files changed

Lines changed: 95 additions & 10 deletions

File tree

app/about/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default function AboutPage() {
2929
<span className="mx-2" aria-hidden>·</span>
3030
<Link href="/auth" className="text-med-brand hover:underline">Sign in</Link>
3131
<span className="mx-2" aria-hidden>·</span>
32-
<span>Aletheia Research OS · v3</span>
32+
<span>Aletheia Research OS · v4</span>
3333
</footer>
3434
</main>
3535
</div>

app/w/[[...lane]]/page.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"use client";
2+
3+
import dynamic from "next/dynamic";
4+
import { useParams } from "next/navigation";
5+
import { DisclaimerGate } from "@/components/DisclaimerGate";
6+
7+
const WorkspaceApp = dynamic(() => import("@/components/WorkspaceApp"), {
8+
ssr: false,
9+
loading: () => (
10+
<div className="min-h-screen bg-med-bg flex items-center justify-center">
11+
<p className="text-sm text-med-sub">Loading workspace…</p>
12+
</div>
13+
),
14+
});
15+
16+
/**
17+
* Real URL routes for every workspace lane: /w/<lane> (e.g. /w/methods,
18+
* /w/journal-finder). The workspace itself stays a single mounted app —
19+
* lane changes are shallow history updates, so drafts and scroll state
20+
* survive navigation while URLs stay shareable and bookmarkable.
21+
*/
22+
export default function WorkspaceLanePage() {
23+
const params = useParams<{ lane?: string[] }>();
24+
const lane = Array.isArray(params?.lane) ? params.lane[0] : undefined;
25+
return (
26+
<DisclaimerGate>
27+
<WorkspaceApp initialLane={lane} />
28+
</DisclaimerGate>
29+
);
30+
}

components/LifecycleNavigation.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,39 @@ export type LifecycleKey =
5151
| "announcements"
5252
| "about";
5353

54+
/** Every routable lane key — used to validate /w/<lane> URLs. */
55+
export const LANE_KEYS: LifecycleKey[] = [
56+
"overview",
57+
"launch",
58+
"protocol",
59+
"type",
60+
"title",
61+
"literature",
62+
"introduction",
63+
"methods",
64+
"results",
65+
"discussion",
66+
"conclusion",
67+
"references",
68+
"appendix",
69+
"journal-finder",
70+
"pipeline",
71+
"submission",
72+
"club",
73+
"review",
74+
"library",
75+
"toolkit",
76+
"skills",
77+
"impact-studio",
78+
"export",
79+
"announcements",
80+
"about",
81+
];
82+
83+
export function isLaneKey(k: string | undefined | null): k is LifecycleKey {
84+
return !!k && (LANE_KEYS as string[]).includes(k);
85+
}
86+
5487
type PhaseName =
5588
| "Platform"
5689
| "Pre-Research"
@@ -207,15 +240,18 @@ export function LifecycleNavigation({
207240
{items.map((item) => {
208241
const isActive = active === item.key;
209242
return (
210-
<button
243+
<a
211244
key={item.key}
212-
type="button"
213-
onClick={() => {
245+
href={`/w/${item.key}`}
246+
onClick={(e) => {
247+
// Real link (middle/ctrl-click works); left-click stays in-app.
248+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
249+
e.preventDefault();
214250
trackFeature(`panel:${item.key}`);
215251
onSelect(item.key);
216252
}}
217253
aria-current={isActive ? "page" : undefined}
218-
className={`relative w-full text-left rounded-lg pl-3.5 pr-2.5 py-2 text-[13px] transition ${
254+
className={`relative block w-full text-left rounded-lg pl-3.5 pr-2.5 py-2 text-[13px] transition ${
219255
isActive
220256
? `${theme.activeClass} font-semibold`
221257
: "text-[var(--mc-ink-700)] hover:bg-slate-50"
@@ -228,7 +264,7 @@ export function LifecycleNavigation({
228264
/>
229265
)}
230266
{item.label}
231-
</button>
267+
</a>
232268
);
233269
})}
234270
</div>

components/WorkspaceApp.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"use client";
22

33
import dynamic from "next/dynamic";
4-
import { useEffect, useState } from "react";
5-
import { LifecycleNavigation, type LifecycleKey } from "@/components/LifecycleNavigation";
4+
import { useCallback, useEffect, useState } from "react";
5+
import { LifecycleNavigation, isLaneKey, type LifecycleKey } from "@/components/LifecycleNavigation";
66
import { WorkspaceHeader } from "@/components/WorkspaceHeader";
77
import { ResearchLaunch } from "@/components/ResearchLaunch";
88
import { FounderContact } from "@/components/FounderContact";
@@ -121,9 +121,28 @@ const LibraryNavigator = dynamic(
121121
{ loading: RouteSkeleton, ssr: false },
122122
);
123123

124-
export default function WorkspaceApp() {
124+
export default function WorkspaceApp({ initialLane }: { initialLane?: string }) {
125125
const { project, setProject, update, ready, autosave } = useProject();
126-
const [active, setActive] = useState<LifecycleKey>("launch");
126+
const [active, setActiveInternal] = useState<LifecycleKey>(
127+
isLaneKey(initialLane) ? initialLane : "launch",
128+
);
129+
130+
/** Lane change = shallow URL update, so every lane has a real, shareable URL. */
131+
const setActive = useCallback((k: LifecycleKey) => {
132+
setActiveInternal(k);
133+
if (typeof window !== "undefined" && window.location.pathname !== `/w/${k}`) {
134+
window.history.pushState(null, "", `/w/${k}`);
135+
}
136+
}, []);
137+
138+
useEffect(() => {
139+
const onPop = () => {
140+
const m = window.location.pathname.match(/^\/w\/([\w-]+)/);
141+
setActiveInternal(m && isLaneKey(m[1]) ? (m[1] as LifecycleKey) : "launch");
142+
};
143+
window.addEventListener("popstate", onPop);
144+
return () => window.removeEventListener("popstate", onPop);
145+
}, []);
127146

128147
useEffect(() => {
129148
if (!ready) return;

0 commit comments

Comments
 (0)