dsh-zen-remote 1.0.0

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.
Files changed (136) hide show
  1. package/AGENTS.md +51 -0
  2. package/LICENSE +38 -0
  3. package/README.md +237 -0
  4. package/assets/home.png +0 -0
  5. package/assets/info.png +0 -0
  6. package/assets/pairing.png +0 -0
  7. package/assets/session.png +0 -0
  8. package/assets/sheet.png +0 -0
  9. package/cordis.patch.yml +17 -0
  10. package/cordis.patch.yml.example +16 -0
  11. package/docs/interface.md +200 -0
  12. package/docs/remote-access.en.md +335 -0
  13. package/docs/remote-access.md +422 -0
  14. package/dsh-push.mjs +213 -0
  15. package/lan-gate.mjs +78 -0
  16. package/lib/client.js +6615 -0
  17. package/lib/index.js +310 -0
  18. package/lib/index.js.map +1 -0
  19. package/lib/lan-gate-server.cjs +626 -0
  20. package/lib/types/client/MobileAttachButton.d.ts +25 -0
  21. package/lib/types/client/MobileAttachButton.d.ts.map +1 -0
  22. package/lib/types/client/MobileAttachChips.d.ts +32 -0
  23. package/lib/types/client/MobileAttachChips.d.ts.map +1 -0
  24. package/lib/types/client/MobileDrawerFooter.d.ts +21 -0
  25. package/lib/types/client/MobileDrawerFooter.d.ts.map +1 -0
  26. package/lib/types/client/MobileHome.d.ts +24 -0
  27. package/lib/types/client/MobileHome.d.ts.map +1 -0
  28. package/lib/types/client/MobileHomeChips.d.ts +76 -0
  29. package/lib/types/client/MobileHomeChips.d.ts.map +1 -0
  30. package/lib/types/client/MobileNavOverlay.d.ts +15 -0
  31. package/lib/types/client/MobileNavOverlay.d.ts.map +1 -0
  32. package/lib/types/client/MobileNavToggle.d.ts +18 -0
  33. package/lib/types/client/MobileNavToggle.d.ts.map +1 -0
  34. package/lib/types/client/MobileSessionHeader.d.ts +58 -0
  35. package/lib/types/client/MobileSessionHeader.d.ts.map +1 -0
  36. package/lib/types/client/MobileSessionInfo.d.ts +41 -0
  37. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -0
  38. package/lib/types/client/attach-upload.d.ts +78 -0
  39. package/lib/types/client/attach-upload.d.ts.map +1 -0
  40. package/lib/types/client/chips-store.d.ts +13 -0
  41. package/lib/types/client/chips-store.d.ts.map +1 -0
  42. package/lib/types/client/debug.d.ts +9 -0
  43. package/lib/types/client/debug.d.ts.map +1 -0
  44. package/lib/types/client/effects/aionui-compat.d.ts +4 -0
  45. package/lib/types/client/effects/aionui-compat.d.ts.map +1 -0
  46. package/lib/types/client/effects/gestures.d.ts +7 -0
  47. package/lib/types/client/effects/gestures.d.ts.map +1 -0
  48. package/lib/types/client/effects/header-status.d.ts +15 -0
  49. package/lib/types/client/effects/header-status.d.ts.map +1 -0
  50. package/lib/types/client/effects/phone-chrome.d.ts +106 -0
  51. package/lib/types/client/effects/phone-chrome.d.ts.map +1 -0
  52. package/lib/types/client/effects/turn-fold.d.ts +25 -0
  53. package/lib/types/client/effects/turn-fold.d.ts.map +1 -0
  54. package/lib/types/client/index.d.ts +18 -0
  55. package/lib/types/client/index.d.ts.map +1 -0
  56. package/lib/types/client/locales.d.ts +58 -0
  57. package/lib/types/client/locales.d.ts.map +1 -0
  58. package/lib/types/client/nav-store.d.ts +73 -0
  59. package/lib/types/client/nav-store.d.ts.map +1 -0
  60. package/lib/types/client/session-dot.d.ts +12 -0
  61. package/lib/types/client/session-dot.d.ts.map +1 -0
  62. package/lib/types/client/styles/base.css.d.ts +2 -0
  63. package/lib/types/client/styles/base.css.d.ts.map +1 -0
  64. package/lib/types/client/styles/chips.css.d.ts +2 -0
  65. package/lib/types/client/styles/chips.css.d.ts.map +1 -0
  66. package/lib/types/client/styles/compat.css.d.ts +2 -0
  67. package/lib/types/client/styles/compat.css.d.ts.map +1 -0
  68. package/lib/types/client/styles/composer.css.d.ts +2 -0
  69. package/lib/types/client/styles/composer.css.d.ts.map +1 -0
  70. package/lib/types/client/styles/header.css.d.ts +2 -0
  71. package/lib/types/client/styles/header.css.d.ts.map +1 -0
  72. package/lib/types/client/styles/home.css.d.ts +2 -0
  73. package/lib/types/client/styles/home.css.d.ts.map +1 -0
  74. package/lib/types/client/styles/index.d.ts +15 -0
  75. package/lib/types/client/styles/index.d.ts.map +1 -0
  76. package/lib/types/client/styles/info.css.d.ts +2 -0
  77. package/lib/types/client/styles/info.css.d.ts.map +1 -0
  78. package/lib/types/client/styles/layout.css.d.ts +2 -0
  79. package/lib/types/client/styles/layout.css.d.ts.map +1 -0
  80. package/lib/types/client/styles/misc.css.d.ts +2 -0
  81. package/lib/types/client/styles/misc.css.d.ts.map +1 -0
  82. package/lib/types/client/styles/turn-fold.css.d.ts +2 -0
  83. package/lib/types/client/styles/turn-fold.css.d.ts.map +1 -0
  84. package/lib/types/index.d.ts +83 -0
  85. package/lib/types/index.d.ts.map +1 -0
  86. package/llms.txt +20 -0
  87. package/package.json +128 -0
  88. package/pwa/app.css +95 -0
  89. package/pwa/icons/icon-192.png +0 -0
  90. package/pwa/icons/icon-512.png +0 -0
  91. package/pwa/icons/icon-maskable-512.png +0 -0
  92. package/pwa/icons/icon-maskable.svg +1 -0
  93. package/pwa/icons/icon.svg +1 -0
  94. package/pwa/inject.js +190 -0
  95. package/pwa/manifest.json +29 -0
  96. package/pwa/offline.html +38 -0
  97. package/pwa/sw.js +176 -0
  98. package/pwa/touch-gestures.js +94 -0
  99. package/scripts/build-client.mjs +101 -0
  100. package/scripts/cdp-probe.mjs +64 -0
  101. package/scripts/check-attach-upload.mjs +99 -0
  102. package/scripts/check-sunk-viewport.mjs +33 -0
  103. package/scripts/check-upload-endpoint.mjs +168 -0
  104. package/src/client/MobileAttachButton.tsx +125 -0
  105. package/src/client/MobileAttachChips.tsx +85 -0
  106. package/src/client/MobileDrawerFooter.tsx +56 -0
  107. package/src/client/MobileHome.tsx +348 -0
  108. package/src/client/MobileHomeChips.tsx +426 -0
  109. package/src/client/MobileNavOverlay.tsx +279 -0
  110. package/src/client/MobileNavToggle.tsx +52 -0
  111. package/src/client/MobileSessionHeader.tsx +242 -0
  112. package/src/client/MobileSessionInfo.tsx +303 -0
  113. package/src/client/attach-upload.ts +140 -0
  114. package/src/client/chips-store.ts +41 -0
  115. package/src/client/debug.ts +253 -0
  116. package/src/client/effects/aionui-compat.ts +183 -0
  117. package/src/client/effects/gestures.ts +265 -0
  118. package/src/client/effects/header-status.ts +30 -0
  119. package/src/client/effects/phone-chrome.ts +265 -0
  120. package/src/client/effects/turn-fold.ts +258 -0
  121. package/src/client/index.tsx +227 -0
  122. package/src/client/locales.ts +109 -0
  123. package/src/client/nav-store.ts +78 -0
  124. package/src/client/session-dot.ts +17 -0
  125. package/src/client/styles/base.css.ts +167 -0
  126. package/src/client/styles/chips.css.ts +224 -0
  127. package/src/client/styles/compat.css.ts +681 -0
  128. package/src/client/styles/composer.css.ts +504 -0
  129. package/src/client/styles/header.css.ts +368 -0
  130. package/src/client/styles/home.css.ts +477 -0
  131. package/src/client/styles/index.ts +25 -0
  132. package/src/client/styles/info.css.ts +238 -0
  133. package/src/client/styles/layout.css.ts +501 -0
  134. package/src/client/styles/misc.css.ts +128 -0
  135. package/src/client/styles/turn-fold.css.ts +106 -0
  136. package/src/index.ts +343 -0
@@ -0,0 +1,348 @@
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import type { PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots'
3
+ import {
4
+ IconChevronDownOutline14,
5
+ IconChevronLeftOutline14,
6
+ IconPlusOutline16,
7
+ IconSettingsOutline16,
8
+ StateDot,
9
+ } from '@deepseek-ai/dsh-client-ui-primitives'
10
+ import type { StateDotState } from '@deepseek-ai/dsh-client-ui-primitives'
11
+ import type { SessionId, SessionSummary, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
12
+ import { NS } from './locales.ts'
13
+ import { GO_HOME_EVENT } from './nav-store.ts'
14
+ import type { createNavStore } from './nav-store.ts'
15
+ import type { WorkspaceFilter } from './nav-store.ts'
16
+ import type { MobileNavKey } from './locales.ts'
17
+ import { dotState } from './session-dot.ts'
18
+ import { MobileHomeChips, MobileHomeChipsSheetBody } from './MobileHomeChips.tsx'
19
+
20
+ /** Full props for the phone home screen (shell.overlay entry). */
21
+ export type MobileHomeProps =
22
+ & PropsRuntime<'shell.overlay'>
23
+ & PropsStore<ReturnType<typeof createNavStore>>
24
+ & PropsLocale<typeof NS>
25
+ & {
26
+ /** Bound ctx.sessions.open(id). */
27
+ openSession: (id: SessionId) => void
28
+ /** Bound ctx.workspaces.startSession(workspaceId?). */
29
+ startSession: (workspaceId?: WorkspaceId) => void
30
+ /** Bound ctx.sessionLogDownload.download() — the session-log chip (S5). */
31
+ downloadSessionLog: (id: SessionId) => void
32
+ }
33
+
34
+ /** Phone breakpoint: below the tablet range, where the app-shell layout applies. */
35
+ const PHONE_QUERY = '(max-width: 767px)'
36
+
37
+
38
+ /** Live matchMedia hook for the phone breakpoint. */
39
+ function usePhone(): boolean {
40
+ const [phone, setPhone] = useState(() => window.matchMedia(PHONE_QUERY).matches)
41
+ useEffect(() => {
42
+ const query = window.matchMedia(PHONE_QUERY)
43
+ const onChange = (event: MediaQueryListEvent) => setPhone(event.matches)
44
+ query.addEventListener('change', onChange)
45
+ return () => query.removeEventListener('change', onChange)
46
+ }, [])
47
+ return phone
48
+ }
49
+
50
+ /**
51
+ * The site's own favicon, read at runtime from `document.head` (real-device
52
+ * round 2 feedback: a home-screen logo, without shipping any trademarked
53
+ * asset in this repo). A one-time lazy read is enough — the gateway/host
54
+ * writes this `<link>` before the client bundle ever runs (same "first
55
+ * frame" guarantee AGENTS.md documents for `viewport-fit=cover`), and
56
+ * favicons do not change at runtime in practice, so there is no case here
57
+ * that justifies a MutationObserver.
58
+ */
59
+ function useSiteIconHref(): string | undefined {
60
+ const [href] = useState(() => document.querySelector<HTMLLinkElement>('link[rel~="icon"]')?.href)
61
+ return href
62
+ }
63
+
64
+ // Timestamps use the browser's own locale data — no dictionary keys, correct
65
+ // plurals everywhere. Deliberately NOT `document.documentElement.lang`: the
66
+ // shell stamps zh-CN there while the UI copy follows the browser languages
67
+ // (measured 2026-08-17), so the html attribute would print Chinese times in
68
+ // an English UI.
69
+ const relative = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })
70
+ const shortDate = new Intl.DateTimeFormat(undefined, { month: 'numeric', day: 'numeric' })
71
+
72
+ /**
73
+ * Relative timestamp for a session row.
74
+ * @param at - epoch milliseconds.
75
+ * @returns "now" / "3 minutes ago" / "5/12", localized.
76
+ */
77
+ function relativeTime(at: number): string {
78
+ const elapsed = Date.now() - at
79
+ if (elapsed < 60_000) return relative.format(0, 'second')
80
+ if (elapsed < 3_600_000) return relative.format(-Math.floor(elapsed / 60_000), 'minute')
81
+ if (elapsed < 86_400_000) return relative.format(-Math.floor(elapsed / 3_600_000), 'hour')
82
+ if (elapsed < 604_800_000) return relative.format(-Math.floor(elapsed / 86_400_000), 'day')
83
+ return shortDate.format(at)
84
+ }
85
+
86
+ /**
87
+ * Card status subline (real-device round 2 follow-up, 2026-08-17): reuses
88
+ * `dotState`'s own state read (the same ongoing/warning/done semantics the
89
+ * dot already encodes) plus `agentPreset` from the same `SessionSummary`
90
+ * row — no new data source. Returns undefined when there is neither a
91
+ * state nor a preset to show, so the caller can skip the subline entirely
92
+ * rather than render an empty row.
93
+ */
94
+ function statusLine(row: SessionSummary, dot: StateDotState | undefined, t: (key: MobileNavKey) => string): string | undefined {
95
+ const state = dot === 'ongoing' ? t('homeStatusOngoing')
96
+ : dot === 'warning' ? t('homeStatusWarning')
97
+ : dot === 'done' ? t('homeStatusDone')
98
+ : undefined
99
+ if (state !== undefined && row.agentPreset !== undefined) return `${state} · ${row.agentPreset}`
100
+ return state ?? row.agentPreset
101
+ }
102
+
103
+ /**
104
+ * Phone home screen: the full-screen session list that owns the first level
105
+ * of the page stack. Renders nothing at or above 768px — the tablet drawer
106
+ * and the desktop layout stay exactly as they were.
107
+ *
108
+ * All data comes from the standard kit (`useSessions` / `useWorkspaces`) and
109
+ * all navigation from the injected official actions; nothing here reads the
110
+ * official DOM.
111
+ */
112
+ export function MobileHome({
113
+ useSessions,
114
+ useWorkspaces,
115
+ useStore,
116
+ actions,
117
+ openSession,
118
+ startSession,
119
+ downloadSessionLog,
120
+ t,
121
+ }: MobileHomeProps) {
122
+ const phone = usePhone()
123
+ const iconHref = useSiteIconHref()
124
+ const [iconBroken, setIconBroken] = useState(false)
125
+ const view = useStore((s) => s.view)
126
+ const pinned = useStore((s) => s.workspace)
127
+ // Whole snapshots: both stores keep unchanged rows identity-stable, and the
128
+ // list re-renders on any session change anyway (the running dots live here).
129
+ const sessions = useSessions((s) => s)
130
+ const workspaces = useWorkspaces((s) => s)
131
+ const [sheet, setSheet] = useState<'filter' | 'chips' | null>(null)
132
+
133
+ // Workspace of the current session — the untouched filter default.
134
+ const currentWorkspaceId = useMemo(() => {
135
+ const current = sessions.current
136
+ if (current === undefined) return undefined
137
+ return workspaces.items.find((item) => item.sessionIds.includes(current))?.workspaceId
138
+ }, [sessions.current, workspaces.items])
139
+
140
+ const selected: WorkspaceFilter = pinned ?? currentWorkspaceId ?? 'all'
141
+ const selectedWorkspace = selected === 'all'
142
+ ? undefined
143
+ : workspaces.items.find((item) => item.workspaceId === selected)
144
+
145
+ const rows = useMemo(() => {
146
+ const archived = new Set(workspaces.archivedSessionIds)
147
+ const scope = selectedWorkspace === undefined ? null : new Set(selectedWorkspace.sessionIds)
148
+ return sessions.ids
149
+ .flatMap((id) => {
150
+ const row = sessions.byId[id]
151
+ return row === undefined ? [] : [row]
152
+ })
153
+ // Blank sessions are the New Session placeholders the official sidebar
154
+ // hides too; subagents belong to their parent's session view.
155
+ .filter((row) => !row.blank && row.parentId === undefined && row.origin !== 'subagent')
156
+ .filter((row) => !archived.has(row.id))
157
+ .filter((row) => scope === null || scope.has(row.id))
158
+ .sort((a, b) => b.updatedAt - a.updatedAt)
159
+ }, [sessions.ids, sessions.byId, workspaces.archivedSessionIds, selectedWorkspace])
160
+
161
+ // The session header's back button (session scope) cannot hold this
162
+ // store directly — a handle mounts under exactly one scope, and this one
163
+ // is already root-scoped here (see nav-store.ts) — so it dispatches
164
+ // GO_HOME_EVENT instead and this, the store's actual owner, applies it.
165
+ useEffect(() => {
166
+ const onGoHome = (): void => actions.show('home')
167
+ window.addEventListener(GO_HOME_EVENT, onGoHome)
168
+ return () => window.removeEventListener(GO_HOME_EVENT, onGoHome)
169
+ }, [actions])
170
+
171
+ const enter = (start: () => void): void => {
172
+ start()
173
+ setSheet(null)
174
+ actions.show('session')
175
+ }
176
+
177
+ // Opens the OFFICIAL settings modal (dsh-client-ui-settings-general's
178
+ // SettingsRoot) by clicking its real trigger button — its "open" state is
179
+ // component-local React state with no public setter, same reasoning as
180
+ // MobileSessionHeader's Chat/Trajectory tab click. The trigger mounts
181
+ // inside the sidebar's `sidebar.settings` footer slot
182
+ // (`[class$="_settingsArea"]`, the sidebar shell's stable class suffix —
183
+ // see dsh-client-ui-sidebar's SidebarRoot), which is display:none on the
184
+ // phone breakpoint (home.css.ts): `.click()` still dispatches (bypasses
185
+ // hit-testing, S2.1 precedent) and flips `open`, but the resulting
186
+ // `aria-modal="true"` dialog would still paint nothing without the portal
187
+ // fix in styles/chips.css.ts (`:has([aria-modal="true"])` un-hides exactly
188
+ // the ancestor chain down to it, not the whole sidebar).
189
+ const openSettings = (): void => {
190
+ document.querySelector<HTMLButtonElement>('[class$="_settingsArea"] button[aria-haspopup="dialog"]')?.click()
191
+ }
192
+
193
+ if (!phone) return null
194
+
195
+ const title = selectedWorkspace?.title ?? t('allWorkspaces')
196
+
197
+ return (
198
+ <>
199
+ {/* Hero (new blank session) renders no conversation.session.header, so the
200
+ * header-slot back button doesn't exist there. This floating fallback is
201
+ * hidden by CSS whenever the real header back is mounted. */}
202
+ {view === 'session' && (
203
+ <button
204
+ type="button"
205
+ data-mobile-nav="hero-back"
206
+ aria-label={t('backToList')}
207
+ onClick={() => actions.show('home')}
208
+ >
209
+ <IconChevronLeftOutline14 size={20} />
210
+ </button>
211
+ )}
212
+ <div data-mobile-nav="home" data-view={view} aria-hidden={view === 'session'}>
213
+ <div data-mobile-nav="home-top">
214
+ {iconHref !== undefined && !iconBroken && (
215
+ <img
216
+ src={iconHref}
217
+ alt=""
218
+ aria-hidden="true"
219
+ data-mobile-nav="home-logo"
220
+ onError={() => setIconBroken(true)}
221
+ />
222
+ )}
223
+ <button
224
+ type="button"
225
+ data-mobile-nav="ws-switch"
226
+ aria-haspopup="menu"
227
+ onClick={() => setSheet('filter')}
228
+ >
229
+ <span>{title}</span>
230
+ <IconChevronDownOutline14 size={14} />
231
+ </button>
232
+ <button
233
+ type="button"
234
+ data-mobile-nav="home-settings"
235
+ aria-label={t('settings')}
236
+ title={t('settings')}
237
+ onClick={openSettings}
238
+ >
239
+ <IconSettingsOutline16 size={18} />
240
+ </button>
241
+ </div>
242
+
243
+ <MobileHomeChips
244
+ t={t}
245
+ sessionId={sessions.current}
246
+ downloadSessionLog={(id) => downloadSessionLog(id as SessionId)}
247
+ onCustomize={() => setSheet('chips')}
248
+ />
249
+
250
+ <ul data-mobile-nav="home-list">
251
+ {rows.map((row) => {
252
+ const dot = dotState(row)
253
+ const status = statusLine(row, dot, t)
254
+ const initial = row.displayTitle.trim().charAt(0).toUpperCase()
255
+ return (
256
+ <li key={row.id}>
257
+ <button
258
+ type="button"
259
+ data-mobile-nav="home-row"
260
+ data-current={row.id === sessions.current ? '' : undefined}
261
+ onClick={() => enter(() => openSession(row.id))}
262
+ >
263
+ <span data-mobile-nav="home-row-avatar" aria-hidden="true">
264
+ {dot !== undefined ? <StateDot state={dot} size={10} /> : initial}
265
+ </span>
266
+ <span data-mobile-nav="home-row-body">
267
+ <span data-mobile-nav="home-row-title">{row.displayTitle}</span>
268
+ {status !== undefined && (
269
+ <span data-mobile-nav="home-row-status">{status}</span>
270
+ )}
271
+ </span>
272
+ <time data-mobile-nav="home-row-time" dateTime={new Date(row.updatedAt).toISOString()}>
273
+ {relativeTime(row.updatedAt)}
274
+ </time>
275
+ </button>
276
+ </li>
277
+ )
278
+ })}
279
+ {rows.length === 0 && <li data-mobile-nav="home-empty">{t('noSessions')}</li>}
280
+ </ul>
281
+
282
+ <button
283
+ type="button"
284
+ data-mobile-nav="home-fab"
285
+ aria-label={t('newSession')}
286
+ title={t('newSession')}
287
+ onClick={() => enter(() => startSession(selectedWorkspace?.workspaceId))}
288
+ >
289
+ <IconPlusOutline16 size={18} />
290
+ <span>{t('newSession')}</span>
291
+ </button>
292
+
293
+ {sheet !== null && (
294
+ <div data-mobile-nav="home-sheet-layer">
295
+ <div
296
+ data-mobile-nav="home-sheet-mask"
297
+ role="button"
298
+ tabIndex={-1}
299
+ aria-label={t('close')}
300
+ onClick={() => setSheet(null)}
301
+ />
302
+ {/* Two sheets share this one container (chrome + S6 drag-to-close
303
+ * + mask-click-close, all keyed off data-mobile-nav="home-sheet"
304
+ * generically — effects/gestures.ts): the workspace switcher
305
+ * (role="menu", unchanged) and the S5 chip-customize toggle list
306
+ * (MobileHomeChipsSheetBody, no menu semantics — its rows are
307
+ * switches, not menu items that close the sheet on click). */}
308
+ <div data-mobile-nav="home-sheet" role={sheet === 'filter' ? 'menu' : undefined}>
309
+ {sheet === 'filter' && (
310
+ <>
311
+ <div data-mobile-nav="home-sheet-title">{t('switchWorkspace')}</div>
312
+ <button
313
+ type="button"
314
+ role="menuitem"
315
+ data-mobile-nav="home-sheet-item"
316
+ data-selected={selected === 'all' ? '' : undefined}
317
+ onClick={() => {
318
+ actions.filter('all')
319
+ setSheet(null)
320
+ }}
321
+ >
322
+ {t('allWorkspaces')}
323
+ </button>
324
+ {workspaces.items.map((item) => (
325
+ <button
326
+ key={item.workspaceId}
327
+ type="button"
328
+ role="menuitem"
329
+ data-mobile-nav="home-sheet-item"
330
+ data-selected={selected === item.workspaceId ? '' : undefined}
331
+ onClick={() => {
332
+ actions.filter(item.workspaceId)
333
+ setSheet(null)
334
+ }}
335
+ >
336
+ {item.title}
337
+ </button>
338
+ ))}
339
+ </>
340
+ )}
341
+ {sheet === 'chips' && <MobileHomeChipsSheetBody t={t} />}
342
+ </div>
343
+ </div>
344
+ )}
345
+ </div>
346
+ </>
347
+ )
348
+ }