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.
- package/AGENTS.md +51 -0
- package/LICENSE +38 -0
- package/README.md +237 -0
- package/assets/home.png +0 -0
- package/assets/info.png +0 -0
- package/assets/pairing.png +0 -0
- package/assets/session.png +0 -0
- package/assets/sheet.png +0 -0
- package/cordis.patch.yml +17 -0
- package/cordis.patch.yml.example +16 -0
- package/docs/interface.md +200 -0
- package/docs/remote-access.en.md +335 -0
- package/docs/remote-access.md +422 -0
- package/dsh-push.mjs +213 -0
- package/lan-gate.mjs +78 -0
- package/lib/client.js +6615 -0
- package/lib/index.js +310 -0
- package/lib/index.js.map +1 -0
- package/lib/lan-gate-server.cjs +626 -0
- package/lib/types/client/MobileAttachButton.d.ts +25 -0
- package/lib/types/client/MobileAttachButton.d.ts.map +1 -0
- package/lib/types/client/MobileAttachChips.d.ts +32 -0
- package/lib/types/client/MobileAttachChips.d.ts.map +1 -0
- package/lib/types/client/MobileDrawerFooter.d.ts +21 -0
- package/lib/types/client/MobileDrawerFooter.d.ts.map +1 -0
- package/lib/types/client/MobileHome.d.ts +24 -0
- package/lib/types/client/MobileHome.d.ts.map +1 -0
- package/lib/types/client/MobileHomeChips.d.ts +76 -0
- package/lib/types/client/MobileHomeChips.d.ts.map +1 -0
- package/lib/types/client/MobileNavOverlay.d.ts +15 -0
- package/lib/types/client/MobileNavOverlay.d.ts.map +1 -0
- package/lib/types/client/MobileNavToggle.d.ts +18 -0
- package/lib/types/client/MobileNavToggle.d.ts.map +1 -0
- package/lib/types/client/MobileSessionHeader.d.ts +58 -0
- package/lib/types/client/MobileSessionHeader.d.ts.map +1 -0
- package/lib/types/client/MobileSessionInfo.d.ts +41 -0
- package/lib/types/client/MobileSessionInfo.d.ts.map +1 -0
- package/lib/types/client/attach-upload.d.ts +78 -0
- package/lib/types/client/attach-upload.d.ts.map +1 -0
- package/lib/types/client/chips-store.d.ts +13 -0
- package/lib/types/client/chips-store.d.ts.map +1 -0
- package/lib/types/client/debug.d.ts +9 -0
- package/lib/types/client/debug.d.ts.map +1 -0
- package/lib/types/client/effects/aionui-compat.d.ts +4 -0
- package/lib/types/client/effects/aionui-compat.d.ts.map +1 -0
- package/lib/types/client/effects/gestures.d.ts +7 -0
- package/lib/types/client/effects/gestures.d.ts.map +1 -0
- package/lib/types/client/effects/header-status.d.ts +15 -0
- package/lib/types/client/effects/header-status.d.ts.map +1 -0
- package/lib/types/client/effects/phone-chrome.d.ts +106 -0
- package/lib/types/client/effects/phone-chrome.d.ts.map +1 -0
- package/lib/types/client/effects/turn-fold.d.ts +25 -0
- package/lib/types/client/effects/turn-fold.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +18 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +58 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/nav-store.d.ts +73 -0
- package/lib/types/client/nav-store.d.ts.map +1 -0
- package/lib/types/client/session-dot.d.ts +12 -0
- package/lib/types/client/session-dot.d.ts.map +1 -0
- package/lib/types/client/styles/base.css.d.ts +2 -0
- package/lib/types/client/styles/base.css.d.ts.map +1 -0
- package/lib/types/client/styles/chips.css.d.ts +2 -0
- package/lib/types/client/styles/chips.css.d.ts.map +1 -0
- package/lib/types/client/styles/compat.css.d.ts +2 -0
- package/lib/types/client/styles/compat.css.d.ts.map +1 -0
- package/lib/types/client/styles/composer.css.d.ts +2 -0
- package/lib/types/client/styles/composer.css.d.ts.map +1 -0
- package/lib/types/client/styles/header.css.d.ts +2 -0
- package/lib/types/client/styles/header.css.d.ts.map +1 -0
- package/lib/types/client/styles/home.css.d.ts +2 -0
- package/lib/types/client/styles/home.css.d.ts.map +1 -0
- package/lib/types/client/styles/index.d.ts +15 -0
- package/lib/types/client/styles/index.d.ts.map +1 -0
- package/lib/types/client/styles/info.css.d.ts +2 -0
- package/lib/types/client/styles/info.css.d.ts.map +1 -0
- package/lib/types/client/styles/layout.css.d.ts +2 -0
- package/lib/types/client/styles/layout.css.d.ts.map +1 -0
- package/lib/types/client/styles/misc.css.d.ts +2 -0
- package/lib/types/client/styles/misc.css.d.ts.map +1 -0
- package/lib/types/client/styles/turn-fold.css.d.ts +2 -0
- package/lib/types/client/styles/turn-fold.css.d.ts.map +1 -0
- package/lib/types/index.d.ts +83 -0
- package/lib/types/index.d.ts.map +1 -0
- package/llms.txt +20 -0
- package/package.json +128 -0
- package/pwa/app.css +95 -0
- package/pwa/icons/icon-192.png +0 -0
- package/pwa/icons/icon-512.png +0 -0
- package/pwa/icons/icon-maskable-512.png +0 -0
- package/pwa/icons/icon-maskable.svg +1 -0
- package/pwa/icons/icon.svg +1 -0
- package/pwa/inject.js +190 -0
- package/pwa/manifest.json +29 -0
- package/pwa/offline.html +38 -0
- package/pwa/sw.js +176 -0
- package/pwa/touch-gestures.js +94 -0
- package/scripts/build-client.mjs +101 -0
- package/scripts/cdp-probe.mjs +64 -0
- package/scripts/check-attach-upload.mjs +99 -0
- package/scripts/check-sunk-viewport.mjs +33 -0
- package/scripts/check-upload-endpoint.mjs +168 -0
- package/src/client/MobileAttachButton.tsx +125 -0
- package/src/client/MobileAttachChips.tsx +85 -0
- package/src/client/MobileDrawerFooter.tsx +56 -0
- package/src/client/MobileHome.tsx +348 -0
- package/src/client/MobileHomeChips.tsx +426 -0
- package/src/client/MobileNavOverlay.tsx +279 -0
- package/src/client/MobileNavToggle.tsx +52 -0
- package/src/client/MobileSessionHeader.tsx +242 -0
- package/src/client/MobileSessionInfo.tsx +303 -0
- package/src/client/attach-upload.ts +140 -0
- package/src/client/chips-store.ts +41 -0
- package/src/client/debug.ts +253 -0
- package/src/client/effects/aionui-compat.ts +183 -0
- package/src/client/effects/gestures.ts +265 -0
- package/src/client/effects/header-status.ts +30 -0
- package/src/client/effects/phone-chrome.ts +265 -0
- package/src/client/effects/turn-fold.ts +258 -0
- package/src/client/index.tsx +227 -0
- package/src/client/locales.ts +109 -0
- package/src/client/nav-store.ts +78 -0
- package/src/client/session-dot.ts +17 -0
- package/src/client/styles/base.css.ts +167 -0
- package/src/client/styles/chips.css.ts +224 -0
- package/src/client/styles/compat.css.ts +681 -0
- package/src/client/styles/composer.css.ts +504 -0
- package/src/client/styles/header.css.ts +368 -0
- package/src/client/styles/home.css.ts +477 -0
- package/src/client/styles/index.ts +25 -0
- package/src/client/styles/info.css.ts +238 -0
- package/src/client/styles/layout.css.ts +501 -0
- package/src/client/styles/misc.css.ts +128 -0
- package/src/client/styles/turn-fold.css.ts +106 -0
- 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
|
+
}
|