forked from nexu-io/open-design
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrouter.ts
More file actions
160 lines (153 loc) · 6 KB
/
router.ts
File metadata and controls
160 lines (153 loc) · 6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// Tiny URL router. We avoid pulling in react-router for two reasons:
// the surface area we need is small (three routes, plain pushState), and
// we want a single source of truth for "what file is open" — encoding
// that in the URL is the simplest way to make it deep-linkable.
import { useEffect, useState } from 'react';
// Entry-shell sub-views. The home/project landing renders one of three
// columns and each sub-view now owns a top-level path so the browser
// back/forward buttons work, deep links are shareable, and per-tab
// state isn't trapped behind a `useState` boundary.
export type EntryHomeView =
| 'home'
| 'onboarding'
| 'projects'
| 'tasks'
| 'plugins'
| 'design-systems'
| 'integrations';
export type Route =
| { kind: 'home'; view: EntryHomeView }
| { kind: 'design-system-create' }
| { kind: 'design-system-detail'; designSystemId: string }
| {
kind: 'project';
projectId: string;
/**
* Deep-link to a specific conversation inside the project. When
* present, the project view picks this conversation as the active
* one instead of defaulting to `list[0]`. Falls back to the
* default picker when the routed conversation no longer exists.
* Added for issue #1505 (Routines history → specific conversation).
*/
conversationId?: string | null;
fileName: string | null;
}
| { kind: 'marketplace' }
| { kind: 'marketplace-detail'; pluginId: string };
export function parseRoute(pathname: string): Route {
const parts = pathname.replace(/\/+$/, '').split('/').filter(Boolean);
if (parts.length === 0) return { kind: 'home', view: 'home' };
if (parts[0] === 'onboarding') {
return { kind: 'home', view: 'onboarding' };
}
if (parts[0] === 'projects') {
if (parts[1]) {
const projectId = decodeURIComponent(parts[1]);
// /projects/:id/conversations/:cid[/files/...]
if (parts[2] === 'conversations' && parts[3]) {
const conversationId = decodeURIComponent(parts[3]);
if (parts[4] === 'files' && parts[5]) {
return {
kind: 'project',
projectId,
conversationId,
fileName: decodeURIComponent(parts.slice(5).join('/')),
};
}
return { kind: 'project', projectId, conversationId, fileName: null };
}
// /projects/:id/files/...
if (parts[2] === 'files' && parts[3]) {
return {
kind: 'project',
projectId,
conversationId: null,
fileName: decodeURIComponent(parts.slice(3).join('/')),
};
}
return { kind: 'project', projectId, conversationId: null, fileName: null };
}
return { kind: 'home', view: 'projects' };
}
if (parts[0] === 'design-systems') {
if (parts[1] === 'create') {
return { kind: 'design-system-create' };
}
if (parts[1]) {
return { kind: 'design-system-detail', designSystemId: decodeURIComponent(parts[1]) };
}
return { kind: 'home', view: 'design-systems' };
}
if (parts[0] === 'automations' || parts[0] === 'tasks') {
return { kind: 'home', view: 'tasks' };
}
if (parts[0] === 'plugins' && !parts[1]) {
return { kind: 'home', view: 'plugins' };
}
if (parts[0] === 'integrations') {
return { kind: 'home', view: 'integrations' };
}
// Phase 2B / spec §11.6 — marketplace deep UI routes. Two paths:
// /marketplace → catalog grid (MarketplaceView)
// /marketplace/<pluginId> → detail page (PluginDetailView)
// Aliases to /plugins remain reserved for the public site (spec §13);
// in-app we keep /marketplace canonical.
if (parts[0] === 'marketplace' || parts[0] === 'plugins') {
if (parts[1]) {
return { kind: 'marketplace-detail', pluginId: decodeURIComponent(parts[1]) };
}
return { kind: 'marketplace' };
}
return { kind: 'home', view: 'home' };
}
export function buildPath(route: Route): string {
if (route.kind === 'home') {
if (route.view === 'onboarding') return '/onboarding';
if (route.view === 'projects') return '/projects';
if (route.view === 'tasks') return '/automations';
if (route.view === 'plugins') return '/plugins';
if (route.view === 'design-systems') return '/design-systems';
if (route.view === 'integrations') return '/integrations';
return '/';
}
if (route.kind === 'marketplace') return '/marketplace';
if (route.kind === 'marketplace-detail') return `/marketplace/${encodeURIComponent(route.pluginId)}`;
if (route.kind === 'design-system-create') return '/design-systems/create';
if (route.kind === 'design-system-detail') {
return `/design-systems/${encodeURIComponent(route.designSystemId)}`;
}
const id = encodeURIComponent(route.projectId);
const file = route.fileName
? route.fileName.split('/').map((s) => encodeURIComponent(s)).join('/')
: null;
if (route.conversationId) {
const cid = encodeURIComponent(route.conversationId);
return file
? `/projects/${id}/conversations/${cid}/files/${file}`
: `/projects/${id}/conversations/${cid}`;
}
return file ? `/projects/${id}/files/${file}` : `/projects/${id}`;
}
// Centralized navigation. Components call this instead of mutating
// `window.location` directly so we can fan the change out to any
// `useRoute()` subscriber via a custom event.
export function navigate(route: Route, opts: { replace?: boolean } = {}): void {
const target = buildPath(route);
const current = window.location.pathname;
if (target === current) return;
if (opts.replace) {
window.history.replaceState(null, '', target);
} else {
window.history.pushState(null, '', target);
}
window.dispatchEvent(new PopStateEvent('popstate'));
}
export function useRoute(): Route {
const [route, setRoute] = useState<Route>(() => parseRoute(window.location.pathname));
useEffect(() => {
const onPop = () => setRoute(parseRoute(window.location.pathname));
window.addEventListener('popstate', onPop);
return () => window.removeEventListener('popstate', onPop);
}, []);
return route;
}