dsh-tiddlywiki 0.16.21 → 0.16.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +311 -271
- package/docs/seed-initialization.md +47 -22
- package/lib/client.bundle.js +26 -26
- package/lib/client.js +26 -26
- package/lib/index.js +395 -17
- package/lib/index.js.map +1 -1
- package/package.json +86 -86
- package/src/client/better-sidebar-tab.ts +136 -0
- package/src/client/index.ts +30 -0
- package/src/client/panel.ts +5 -4
- package/src/client/rightbar-tab.ts +20 -299
- package/src/client/settings-page.ts +1 -0
- package/src/client/tw-frame.ts +334 -0
- package/src/client/ui-config.ts +5 -2
- package/src/host/config.ts +8 -0
- package/src/host/routes.ts +2 -0
- package/src/host/seed-home.ts +96 -92
- package/src/host/seed-notes.ts +8 -1
- package/src/host/seed-starter-docs.ts +251 -0
- package/src/host/seed-ui-styles.ts +74 -0
- package/src/host/seeds.ts +71 -25
- package/src/index.ts +7 -4
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared TiddlyWiki iframe machinery for DSH side surfaces (v0.16.23).
|
|
3
|
+
*
|
|
4
|
+
* The center-column panel, the native right-sidebar tab (rightbar-tab.ts) and
|
|
5
|
+
* the DSH Better Sidebar tab (better-sidebar-tab.ts) all embed the SAME-ORIGIN
|
|
6
|
+
* TW proxy (`/dsh-tiddlywiki/tw/`) in an iframe and share the same lifecycle:
|
|
7
|
+
* lazy-load on first show, `/status` polling with restart/error states, DSH
|
|
8
|
+
* theme sync, reload-on-FAB-event, and tiddler-hash navigation. This module
|
|
9
|
+
* owns that machinery plus the two small shared bits of state the surfaces
|
|
10
|
+
* mirror (the tab chip label from `ui.tabLabel` and the tab icon).
|
|
11
|
+
*
|
|
12
|
+
* Link routing: each mounted TW surface registers its live frame controller
|
|
13
|
+
* here; panel.ts asks `openTiddlerInLiveTab` first, so a wiki link lands in a
|
|
14
|
+
* visible side TW tab (rightbar / better-sidebar) before falling back to the
|
|
15
|
+
* center overlay. Mutual exclusion (one TW client at a time) rides the
|
|
16
|
+
* `dsh-panel-activate` protocol — each surface dispatches its own panel name
|
|
17
|
+
* on becoming visible and closes itself when another name activates.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-tiddlywiki/client/tw-frame
|
|
20
|
+
*/
|
|
21
|
+
import * as React from 'react'
|
|
22
|
+
import { STATUS_ENDPOINT } from './endpoints.ts'
|
|
23
|
+
import { attachThemeSync, setThemeSyncConfig } from './theme-sync.ts'
|
|
24
|
+
|
|
25
|
+
/** Cross-plugin activation event; detail is the activating panel name. */
|
|
26
|
+
export const ACTIVATE_EVENT = 'dsh-panel-activate'
|
|
27
|
+
/** The "知识库" FAB's reload event; side frames reload with the center one. */
|
|
28
|
+
export const PANEL_RELOAD_EVENT = 'dsh-tw-panel-reload'
|
|
29
|
+
|
|
30
|
+
const RESTART_ENDPOINT = '/dsh-tiddlywiki/restart'
|
|
31
|
+
|
|
32
|
+
/** Tab chip / + menu / guide copy default (label refreshed from `/status` ui.tabLabel). */
|
|
33
|
+
let tabLabel = '知识库'
|
|
34
|
+
|
|
35
|
+
/** Update the shared surface label from the live config (ui.tabLabel). */
|
|
36
|
+
export function setTabLabel(label: string): void {
|
|
37
|
+
const trimmed = typeof label === 'string' ? label.trim() : ''
|
|
38
|
+
if (trimmed.length > 0) tabLabel = trimmed
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Current shared surface label (ui.tabLabel, default 「知识库」). */
|
|
42
|
+
export function getTabLabel(): string {
|
|
43
|
+
return tabLabel
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface StatusPayload {
|
|
47
|
+
ok?: boolean
|
|
48
|
+
status: string
|
|
49
|
+
url?: string
|
|
50
|
+
/** Same-origin TW proxy path (e.g. /dsh-tiddlywiki/tw/); the iframe base. */
|
|
51
|
+
twProxy?: string
|
|
52
|
+
wikiPath?: string
|
|
53
|
+
error?: string
|
|
54
|
+
ui?: { followDshTheme?: boolean; darkPalette?: string; tabLabel?: string }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function fetchStatus(): Promise<StatusPayload | null> {
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(8_000) })
|
|
60
|
+
if (!res.ok) return null
|
|
61
|
+
return (await res.json()) as StatusPayload
|
|
62
|
+
} catch {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function requestRestart(): Promise<boolean> {
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(RESTART_ENDPOINT, { method: 'POST', signal: AbortSignal.timeout(8_000) })
|
|
70
|
+
return res.ok
|
|
71
|
+
} catch {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The shared surface glyph: a wiki page with a TiddlyWiki-style "T". */
|
|
77
|
+
export function TwTabIcon({ size = 16, className }: { size?: number; className?: string }): React.ReactElement {
|
|
78
|
+
return React.createElement(
|
|
79
|
+
'svg',
|
|
80
|
+
{ width: size, height: size, className, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
|
|
81
|
+
React.createElement('path', { d: 'M4 2.5h8a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1z' }),
|
|
82
|
+
React.createElement('path', { d: 'M6 6h4M6 8.5h2.5' }),
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The plain-DOM TW frame controller owned by one surface body. */
|
|
87
|
+
export interface TwFrameController {
|
|
88
|
+
/** Reflect the surface's visibility; loads lazily on the first show. */
|
|
89
|
+
setVisible(visible: boolean): void
|
|
90
|
+
isVisible(): boolean
|
|
91
|
+
/** Open a tiddler by title; false when this controller cannot serve it. */
|
|
92
|
+
openTiddler(title: string): boolean
|
|
93
|
+
dispose(): void
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Live visible-capable TW frames; a controller joins on creation, leaves on dispose. */
|
|
97
|
+
const liveFrames = new Set<TwFrameController>()
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Ask every live side TW surface to open a tiddler; true when one served it.
|
|
101
|
+
* A controller answers false while hidden or disposed, so the center overlay
|
|
102
|
+
* only opens when no visible side TW tab can take the link.
|
|
103
|
+
*/
|
|
104
|
+
export function openTiddlerInLiveTab(title: string): boolean {
|
|
105
|
+
for (const controller of liveFrames) {
|
|
106
|
+
if (controller.openTiddler(title)) return true
|
|
107
|
+
}
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Refresh the shared surface label from `/status` (ui.tabLabel) so a surface
|
|
113
|
+
* that mounts before the first status call already shows the right name.
|
|
114
|
+
*/
|
|
115
|
+
export function warmTabLabel(): void {
|
|
116
|
+
void fetchStatus().then((payload) => {
|
|
117
|
+
if (payload?.ui !== undefined) setTabLabel(payload.ui.tabLabel ?? tabLabel)
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a TW frame controller inside `host`. The frame loads lazily on the
|
|
123
|
+
* first `setVisible(true)`, polls `/status` (starting/restart/error states),
|
|
124
|
+
* follows the DSH theme, reloads on the FAB's reload event, and navigates to
|
|
125
|
+
* `#<title>` hashes once the frame is ready. `signal` (the tab's abort) tears
|
|
126
|
+
* the whole controller down. Joins the live-frame registry on creation and
|
|
127
|
+
* leaves it on dispose.
|
|
128
|
+
*/
|
|
129
|
+
export function createTwFrameController(host: HTMLElement, signal: AbortSignal): TwFrameController {
|
|
130
|
+
let visible = false
|
|
131
|
+
let started = false
|
|
132
|
+
let disposed = false
|
|
133
|
+
let refreshTimer: number | undefined
|
|
134
|
+
let refreshAttempts = 0
|
|
135
|
+
let frameLoaded = false
|
|
136
|
+
let pendingHash: string | null = null
|
|
137
|
+
let themeSyncDispose: (() => void) | undefined
|
|
138
|
+
|
|
139
|
+
const view = document.createElement('div')
|
|
140
|
+
view.className = 'dsh-tw-rightbar-view'
|
|
141
|
+
|
|
142
|
+
const frameArea = document.createElement('div')
|
|
143
|
+
frameArea.className = 'dsh-tw-rightbar-frame-wrap'
|
|
144
|
+
const frame = document.createElement('iframe')
|
|
145
|
+
frame.className = 'dsh-tw-rightbar-frame'
|
|
146
|
+
frame.title = 'TiddlyWiki'
|
|
147
|
+
frame.hidden = true
|
|
148
|
+
frameArea.append(frame)
|
|
149
|
+
|
|
150
|
+
const errorArea = document.createElement('div')
|
|
151
|
+
errorArea.className = 'dsh-tw-rightbar-error'
|
|
152
|
+
errorArea.hidden = true
|
|
153
|
+
|
|
154
|
+
view.append(frameArea, errorArea)
|
|
155
|
+
host.append(view)
|
|
156
|
+
|
|
157
|
+
frame.addEventListener('load', () => {
|
|
158
|
+
frameLoaded = true
|
|
159
|
+
applyPendingHash()
|
|
160
|
+
})
|
|
161
|
+
themeSyncDispose = attachThemeSync(frame)
|
|
162
|
+
|
|
163
|
+
const showError = (message: string): void => {
|
|
164
|
+
frame.hidden = true
|
|
165
|
+
errorArea.hidden = false
|
|
166
|
+
errorArea.replaceChildren()
|
|
167
|
+
const p = document.createElement('div')
|
|
168
|
+
p.textContent = 'TiddlyWiki 服务不可用'
|
|
169
|
+
const code = document.createElement('code')
|
|
170
|
+
code.textContent = message
|
|
171
|
+
const retry = document.createElement('button')
|
|
172
|
+
retry.type = 'button'
|
|
173
|
+
retry.textContent = '重试'
|
|
174
|
+
retry.addEventListener('click', () => {
|
|
175
|
+
retry.disabled = true
|
|
176
|
+
retry.textContent = '重启中…'
|
|
177
|
+
void requestRestart().finally(() => { void doRefresh() })
|
|
178
|
+
})
|
|
179
|
+
errorArea.append(p, code, retry)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const showStarting = (): void => {
|
|
183
|
+
frame.hidden = true
|
|
184
|
+
errorArea.hidden = false
|
|
185
|
+
errorArea.replaceChildren()
|
|
186
|
+
const p = document.createElement('div')
|
|
187
|
+
p.textContent = 'TiddlyWiki 服务正在启动…'
|
|
188
|
+
errorArea.append(p)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const showFrame = (url: string): void => {
|
|
192
|
+
errorArea.hidden = true
|
|
193
|
+
// Only reveal the frame while the surface is on screen.
|
|
194
|
+
frame.hidden = !visible
|
|
195
|
+
// Set the src only when the url changed, so an editor never loses unsaved
|
|
196
|
+
// state on a status refresh.
|
|
197
|
+
if (frame.dataset.loaded !== url) {
|
|
198
|
+
frame.dataset.loaded = url
|
|
199
|
+
frameLoaded = false
|
|
200
|
+
frame.src = url
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Apply a pending tiddler-hash once the frame is ready (see panel.ts). */
|
|
205
|
+
const applyPendingHash = (): void => {
|
|
206
|
+
if (pendingHash === null || !frameLoaded) return
|
|
207
|
+
const hash = pendingHash
|
|
208
|
+
const win = frame.contentWindow
|
|
209
|
+
if (win === null) {
|
|
210
|
+
fallbackLoad(hash)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
const tryOnce = (attempt: number): void => {
|
|
214
|
+
if (disposed) return
|
|
215
|
+
if (pendingHash !== hash) return
|
|
216
|
+
const frameTw = win as { $tw?: unknown }
|
|
217
|
+
if (typeof frameTw.$tw !== 'object' || frameTw.$tw === null) {
|
|
218
|
+
if (attempt < 40) {
|
|
219
|
+
window.setTimeout(() => tryOnce(attempt + 1), 150)
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
fallbackLoad(hash)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
pendingHash = null
|
|
226
|
+
try {
|
|
227
|
+
if (win.location.hash !== hash) win.location.hash = hash
|
|
228
|
+
} catch {
|
|
229
|
+
fallbackLoad(hash)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
tryOnce(0)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const fallbackLoad = (hash: string): void => {
|
|
236
|
+
if (disposed) return
|
|
237
|
+
if (pendingHash === hash) pendingHash = null
|
|
238
|
+
const base = frame.src.split('#')[0]
|
|
239
|
+
if (frame.src !== `${base}${hash}`) frame.src = `${base}${hash}`
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const doRefresh = async (): Promise<void> => {
|
|
243
|
+
if (refreshTimer !== undefined) {
|
|
244
|
+
window.clearTimeout(refreshTimer)
|
|
245
|
+
refreshTimer = undefined
|
|
246
|
+
}
|
|
247
|
+
const payload = await fetchStatus()
|
|
248
|
+
if (disposed) return
|
|
249
|
+
if (payload === null) {
|
|
250
|
+
showError('无法访问 /dsh-tiddlywiki/status')
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
if (payload.ui !== undefined) {
|
|
254
|
+
setTabLabel(payload.ui.tabLabel ?? tabLabel)
|
|
255
|
+
setThemeSyncConfig({
|
|
256
|
+
enabled: payload.ui.followDshTheme !== false,
|
|
257
|
+
darkPalette: payload.ui.darkPalette,
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
if (payload.status === 'running') {
|
|
261
|
+
refreshAttempts = 0
|
|
262
|
+
if (typeof payload.twProxy === 'string') {
|
|
263
|
+
showFrame(new URL(payload.twProxy, window.location.origin).href)
|
|
264
|
+
} else if (typeof payload.url === 'string') {
|
|
265
|
+
showFrame(payload.url)
|
|
266
|
+
} else {
|
|
267
|
+
showError('服务未返回编辑器地址')
|
|
268
|
+
}
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
if (payload.status === 'starting') {
|
|
272
|
+
showStarting()
|
|
273
|
+
if (refreshAttempts < 30) {
|
|
274
|
+
refreshAttempts++
|
|
275
|
+
refreshTimer = window.setTimeout(() => { void doRefresh() }, 1_500)
|
|
276
|
+
}
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
refreshAttempts = 0
|
|
280
|
+
showError(payload.error ?? `服务状态:${payload.status}`)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const onReloadRequest = (): void => {
|
|
284
|
+
if (!frame.hidden) frame.src = frame.src
|
|
285
|
+
}
|
|
286
|
+
document.addEventListener(PANEL_RELOAD_EVENT, onReloadRequest)
|
|
287
|
+
|
|
288
|
+
const onAbort = (): void => controller.dispose()
|
|
289
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
290
|
+
|
|
291
|
+
const controller: TwFrameController = {
|
|
292
|
+
setVisible(next: boolean): void {
|
|
293
|
+
visible = next
|
|
294
|
+
if (!started) {
|
|
295
|
+
// Nothing loaded yet: stay hidden until the first show kicks the load.
|
|
296
|
+
frame.hidden = true
|
|
297
|
+
view.dataset.visible = next ? '1' : '0'
|
|
298
|
+
if (next) {
|
|
299
|
+
started = true
|
|
300
|
+
void doRefresh()
|
|
301
|
+
}
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
frame.hidden = !next
|
|
305
|
+
view.dataset.visible = next ? '1' : '0'
|
|
306
|
+
},
|
|
307
|
+
isVisible(): boolean {
|
|
308
|
+
return visible
|
|
309
|
+
},
|
|
310
|
+
openTiddler(title: string): boolean {
|
|
311
|
+
if (disposed || !visible) return false
|
|
312
|
+
pendingHash = `#${encodeURIComponent(title)}`
|
|
313
|
+
if (!started) {
|
|
314
|
+
started = true
|
|
315
|
+
void doRefresh()
|
|
316
|
+
} else {
|
|
317
|
+
applyPendingHash()
|
|
318
|
+
}
|
|
319
|
+
return true
|
|
320
|
+
},
|
|
321
|
+
dispose(): void {
|
|
322
|
+
if (disposed) return
|
|
323
|
+
disposed = true
|
|
324
|
+
liveFrames.delete(controller)
|
|
325
|
+
document.removeEventListener(PANEL_RELOAD_EVENT, onReloadRequest)
|
|
326
|
+
signal.removeEventListener('abort', onAbort)
|
|
327
|
+
if (refreshTimer !== undefined) window.clearTimeout(refreshTimer)
|
|
328
|
+
themeSyncDispose?.()
|
|
329
|
+
view.remove()
|
|
330
|
+
},
|
|
331
|
+
}
|
|
332
|
+
liveFrames.add(controller)
|
|
333
|
+
return controller
|
|
334
|
+
}
|
package/src/client/ui-config.ts
CHANGED
|
@@ -26,9 +26,11 @@ export interface UiConfig {
|
|
|
26
26
|
showSessionTab: boolean
|
|
27
27
|
/** 是否在 DSH 右侧边栏提供 TiddlyWiki 入口/Tab(默认 true)。 */
|
|
28
28
|
showRightbarTab: boolean
|
|
29
|
+
/** 是否在 DSH Better Sidebar 侧边栏注册 TiddlyWiki tab(默认 true)。 */
|
|
30
|
+
showBetterSidebarTab: boolean
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
const FALLBACK: UiConfig = { showQuickNoteDock: true, quickNoteMode: 'native', sidebarLabel: 'TiddlyWiki', tabLabel: '知识库', showSessionTab: true, showRightbarTab: true }
|
|
33
|
+
const FALLBACK: UiConfig = { showQuickNoteDock: true, quickNoteMode: 'native', sidebarLabel: 'TiddlyWiki', tabLabel: '知识库', showSessionTab: true, showRightbarTab: true, showBetterSidebarTab: true }
|
|
32
34
|
|
|
33
35
|
const CACHE_TTL_MS = 15_000
|
|
34
36
|
let cache: { at: number; value: UiConfig } | undefined
|
|
@@ -43,7 +45,7 @@ export async function fetchUiConfig(opts: { force?: boolean } = {}): Promise<UiC
|
|
|
43
45
|
try {
|
|
44
46
|
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(5_000) })
|
|
45
47
|
if (!res.ok) return FALLBACK
|
|
46
|
-
const p = (await res.json()) as { ui?: { showQuickNoteDock?: boolean; quickNoteMode?: 'native' | 'card'; sidebarLabel?: string; tabLabel?: string; showSessionTab?: boolean; showRightbarTab?: boolean } }
|
|
48
|
+
const p = (await res.json()) as { ui?: { showQuickNoteDock?: boolean; quickNoteMode?: 'native' | 'card'; sidebarLabel?: string; tabLabel?: string; showSessionTab?: boolean; showRightbarTab?: boolean; showBetterSidebarTab?: boolean } }
|
|
47
49
|
const value: UiConfig = {
|
|
48
50
|
showQuickNoteDock: p.ui?.showQuickNoteDock !== false,
|
|
49
51
|
quickNoteMode: p.ui?.quickNoteMode === 'card' ? 'card' : 'native',
|
|
@@ -55,6 +57,7 @@ export async function fetchUiConfig(opts: { force?: boolean } = {}): Promise<UiC
|
|
|
55
57
|
: '知识库',
|
|
56
58
|
showSessionTab: p.ui?.showSessionTab !== false,
|
|
57
59
|
showRightbarTab: p.ui?.showRightbarTab !== false,
|
|
60
|
+
showBetterSidebarTab: p.ui?.showBetterSidebarTab !== false,
|
|
58
61
|
}
|
|
59
62
|
cache = { at: Date.now(), value }
|
|
60
63
|
return value
|
package/src/host/config.ts
CHANGED
|
@@ -88,6 +88,14 @@ export interface PluginConfigShape {
|
|
|
88
88
|
* DSH 无 rightbar 时该配置不生效(客户端自动跳过)。
|
|
89
89
|
*/
|
|
90
90
|
showRightbarTab?: boolean
|
|
91
|
+
/**
|
|
92
|
+
* 是否在 DSH Better Sidebar(dsh-better-sidebar 插件)侧边栏提供
|
|
93
|
+
* TiddlyWiki tab(默认 true):通过其 client 服务 `ctx.betterSidebar`
|
|
94
|
+
* 注册 tab 类型,+ 菜单可打开完整 TW 编辑器(与聊天并排)。未安装
|
|
95
|
+
* Better Sidebar 时该配置不生效(客户端自动跳过);该插件自己的设置页
|
|
96
|
+
* 也为本 tab 提供独立的启用开关。
|
|
97
|
+
*/
|
|
98
|
+
showBetterSidebarTab?: boolean
|
|
91
99
|
}
|
|
92
100
|
uiLanguage?: string
|
|
93
101
|
[key: string]: unknown
|
package/src/host/routes.ts
CHANGED
|
@@ -153,6 +153,8 @@ export interface UiDefaultsPublic {
|
|
|
153
153
|
showSessionTab: boolean
|
|
154
154
|
/** 是否在 DSH 右侧边栏提供 TiddlyWiki 入口/Tab(默认 true)。 */
|
|
155
155
|
showRightbarTab: boolean
|
|
156
|
+
/** 是否在 DSH Better Sidebar 侧边栏注册 TiddlyWiki tab(默认 true)。 */
|
|
157
|
+
showBetterSidebarTab: boolean
|
|
156
158
|
}
|
|
157
159
|
|
|
158
160
|
export interface RouteDeps {
|