@khorsheed/dsh-mobile 0.1.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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +132 -0
- package/README.zh.md +132 -0
- package/cordis.patch.yml +3 -0
- package/examples/https-ingress.mjs +84 -0
- package/examples/safari-stream-compat.mjs +98 -0
- package/lib/client.js +6138 -0
- package/lib/index.js +172 -0
- package/lib/invariant.js +20 -0
- package/lib/types/client/ConnectPhone.d.ts +9 -0
- package/lib/types/client/ConnectPhone.js +83 -0
- package/lib/types/client/DirectoryFlow.d.ts +14 -0
- package/lib/types/client/DirectoryFlow.js +75 -0
- package/lib/types/client/MobileChrome.d.ts +15 -0
- package/lib/types/client/MobileChrome.js +124 -0
- package/lib/types/client/MobileIcon.d.ts +6 -0
- package/lib/types/client/MobileIcon.js +16 -0
- package/lib/types/client/MobileLibrary.d.ts +13 -0
- package/lib/types/client/MobileLibrary.js +73 -0
- package/lib/types/client/MobileQueue.d.ts +9 -0
- package/lib/types/client/MobileQueue.js +90 -0
- package/lib/types/client/MobileRooms.d.ts +13 -0
- package/lib/types/client/MobileRooms.js +80 -0
- package/lib/types/client/MobileSeats.d.ts +4 -0
- package/lib/types/client/MobileSeats.js +5 -0
- package/lib/types/client/MobileTools.d.ts +7 -0
- package/lib/types/client/MobileTools.js +34 -0
- package/lib/types/client/SessionRename.d.ts +10 -0
- package/lib/types/client/SessionRename.js +43 -0
- package/lib/types/client/SubmissionFocus.d.ts +26 -0
- package/lib/types/client/SubmissionFocus.js +74 -0
- package/lib/types/client/composerActions.d.ts +24 -0
- package/lib/types/client/composerActions.js +66 -0
- package/lib/types/client/composerFocus.d.ts +15 -0
- package/lib/types/client/composerFocus.js +41 -0
- package/lib/types/client/directory.d.ts +17 -0
- package/lib/types/client/directory.js +54 -0
- package/lib/types/client/foreground.d.ts +4 -0
- package/lib/types/client/foreground.js +47 -0
- package/lib/types/client/gestures.d.ts +4 -0
- package/lib/types/client/gestures.js +118 -0
- package/lib/types/client/index.d.ts +20 -0
- package/lib/types/client/index.js +144 -0
- package/lib/types/client/locales.d.ts +145 -0
- package/lib/types/client/locales.js +80 -0
- package/lib/types/client/messageMenu.d.ts +19 -0
- package/lib/types/client/messageMenu.js +99 -0
- package/lib/types/client/native.d.ts +6 -0
- package/lib/types/client/native.js +15 -0
- package/lib/types/client/navigation.d.ts +41 -0
- package/lib/types/client/navigation.js +61 -0
- package/lib/types/client/pickers.d.ts +17 -0
- package/lib/types/client/pickers.js +109 -0
- package/lib/types/client/presentation.d.ts +38 -0
- package/lib/types/client/presentation.js +146 -0
- package/lib/types/client/queue.d.ts +47 -0
- package/lib/types/client/queue.js +26 -0
- package/lib/types/client/roomActions.d.ts +6 -0
- package/lib/types/client/roomActions.js +41 -0
- package/lib/types/client/rooms.d.ts +44 -0
- package/lib/types/client/rooms.js +75 -0
- package/lib/types/client/styles.d.ts +4 -0
- package/lib/types/client/styles.js +434 -0
- package/lib/types/client/surface.d.ts +17 -0
- package/lib/types/client/surface.js +112 -0
- package/lib/types/client/viewport.d.ts +13 -0
- package/lib/types/client/viewport.js +42 -0
- package/lib/types/connect.d.ts +7 -0
- package/lib/types/connect.js +61 -0
- package/lib/types/directories.d.ts +5 -0
- package/lib/types/directories.js +39 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/index.js +21 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +17 -0
- package/lib/types/protocol.d.ts +30 -0
- package/lib/types/protocol.js +17 -0
- package/locale/en.json +1 -0
- package/locale/zh.json +1 -0
- package/package.json +102 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Copy for controls owned by mobile; all conversation copy stays with its owner. */
|
|
2
|
+
export const NS = 'mobile';
|
|
3
|
+
export const en = {
|
|
4
|
+
connectPhone: "Connect phone",
|
|
5
|
+
connectIntro: "Continue your conversations in DSH Mobile.",
|
|
6
|
+
connectQR: "Scan with DSH Mobile to sign in",
|
|
7
|
+
connectGenerate: "Show login QR code",
|
|
8
|
+
connectHide: "Hide QR code",
|
|
9
|
+
connectConceal: "Hidden automatically after 2 minutes or when you leave this page.",
|
|
10
|
+
connectStepOpen: "Open DSH Mobile on your iPhone.",
|
|
11
|
+
connectStepScan: "Go to Connection Settings → Scan to connect.",
|
|
12
|
+
connectStepConfirm: "Scan this code, check the host address, then confirm the connection.",
|
|
13
|
+
connectPrivacy: "This QR code grants access to this host. Keep it private. Hiding the code does not revoke the official login link; it remains valid until the host process restarts.",
|
|
14
|
+
connectNetwork: "Keep the computer and its HTTPS entry online. Scanning reconnects the App; it cannot repair a disconnected tunnel.",
|
|
15
|
+
connectSetup: "The operator must configure a reachable HTTPS origin using mobile.publicOrigin or DSH_MOBILE_PUBLIC_ORIGIN, and add its hostname to the host trusted hosts.",
|
|
16
|
+
connectUntrusted: "The configured address is not trusted by this host. Update --trusted-host and restart the host before generating a code.",
|
|
17
|
+
connectUnsupported: "This host version cannot provide official login links. Update the supported host/plugin combination.",
|
|
18
|
+
connectError: "Could not load the connection information. Check the connection and retry.",
|
|
19
|
+
connectLoading: "Loading connection information…",
|
|
20
|
+
directoryManual: 'Enter a path', directoryTitle: 'Choose working directory', directoryHint: 'Select a folder on your computer.', directoryComputer: 'Browse computer', directoryNoSaved: 'No saved workspaces yet. Browse your computer to choose one.', directoryGo: 'Go', directoryParent: 'Parent folder', directoryHidden: 'Hidden folders', directoryLoading: 'Loading folders…', directoryError: 'Could not read this folder. Check the path, permissions and connection.', directoryRetry: 'Retry', directorySearch: 'Filter folders', directoryEmpty: 'No subfolders.', directoryTruncated: 'This listing is limited. Enter a full path to open another folder.', directorySelect: 'Use this directory',
|
|
21
|
+
renameSession: 'Rename session', sessionTitle: 'Session title', renameUnavailable: 'Could not rename this conversation. Please try again.',
|
|
22
|
+
roomActionUnavailable: 'Open Members to manage this room; its shortcut is unavailable.', memberFailed: 'Task failed',
|
|
23
|
+
currentConversation: 'Current conversation',
|
|
24
|
+
loadingAgents: 'Checking available agents…', noAgents: 'No local agents are available on this host.', loginAgents: 'Sign in to an agent on the computer before inviting it.',
|
|
25
|
+
queued: 'Queued messages', queueError: 'Could not update the queue. Please try again.', editQueued: 'Edit', removeQueued: 'Remove', sendNow: 'Send now', sendingQueued: 'Waiting for host admission…',
|
|
26
|
+
members: 'Members', membersHint: 'Open a member conversation, or adjust its role.', inviteMember: 'Invite member', memberSettings: 'Member settings', duplicateMember: 'This member name is already in use.', invalidMember: 'Use a name without spaces or @.', roomError: 'Could not complete this request. Check the connection and try again.', mainMember: 'Main agent', openMemberChat: 'Open conversation', awaitingMember: 'Waiting for a first task', agentProvider: 'Agent', chooseProvider: 'Choose an available agent', needsLogin: 'Sign in on computer', memberName: 'Name', memberRole: 'Role instructions', memberRoleHint: 'What should this member focus on?', inviteHint: 'Invitation adds the member. Send an @message in the room to start a task.', roleChangeHint: 'Role changes apply to subsequent dispatches.', advanced: 'Advanced', inheritWorkspace: 'Inherit room workspace', saving: 'Saving…', save: 'Save', stopMemberConfirm: 'Stop this member’s current task?', removeMemberConfirm: 'Remove this member from the room? Its conversation history is retained.', confirm: 'Confirm', stopMember: 'Stop task', removeMember: 'Remove member', backMembers: 'Back to members', memberUnit: 'members',
|
|
27
|
+
inputTools: 'Add and configure', closeTools: 'Close input tools', attachments: 'Add attachment', commands: 'Commands', permissions: 'Permissions',
|
|
28
|
+
welcome: 'What would you like to do?', welcomeHint: 'Continue an idea, or start a new task.',
|
|
29
|
+
menu: 'Conversations', close: 'Close navigation', returnToConversation: 'Return to full conversation', title: 'Your workspace, with you',
|
|
30
|
+
settings: 'Mobile settings', back: 'Back', display: 'Display',
|
|
31
|
+
automatic: 'Follow screen size', mobile: 'Mobile', desktop: 'Desktop',
|
|
32
|
+
restoreMobile: 'Return to mobile layout',
|
|
33
|
+
appearance: 'Layout applies only to this browser.',
|
|
34
|
+
connection: 'Connection', reconnect: 'Reconnect', connected: 'Connected',
|
|
35
|
+
connecting: 'Connecting…', disconnected: 'Connection interrupted',
|
|
36
|
+
server: 'Host', privacy: 'Models, tools and files run on your host.',
|
|
37
|
+
done: 'Done', brand: 'DSH',
|
|
38
|
+
grouping: 'Conversation view', byTime: 'Time', byWorkspace: 'Workspace', today: 'Today', yesterday: 'Yesterday', earlier: 'Earlier', unassignedWorkspace: 'No workspace', scan: 'Scan to connect', conversation: 'Conversation', options: 'Conversation options', connectionSettings: 'Connection settings',
|
|
39
|
+
libraryEyebrow: 'YOUR WORKSPACE', libraryTitle: 'Pick up where you left off.', librarySubtitle: 'A thought, a conversation, a little progress.', newSession: 'New conversation', searchTitles: 'Search', searchResults: 'Results', recent: 'Recent conversations', workspaces: 'Workspaces', loadingSessions: 'Loading conversations…', noMatches: 'No matching conversations.', noSessions: 'Your conversations will appear here.', navigationError: 'Could not open this conversation. Please try again.', running: 'Working', completed: 'Completed', libraryFooter: 'A little closer, wherever you are.',
|
|
40
|
+
workspace: 'Open workspace', hostPath: 'Computer directory', hostPathHelp: 'Enter an existing absolute directory on your computer. This does not select a folder on your phone.', openWorkspace: 'Open', cancel: 'Cancel',
|
|
41
|
+
};
|
|
42
|
+
export const zh = {
|
|
43
|
+
connectPhone: "连接手机",
|
|
44
|
+
connectIntro: "在 DSH Mobile 中继续你的会话。",
|
|
45
|
+
connectQR: "使用 DSH Mobile 扫码登录",
|
|
46
|
+
connectGenerate: "显示登录二维码",
|
|
47
|
+
connectHide: "隐藏二维码",
|
|
48
|
+
connectConceal: "展示 2 分钟后或离开此页面时自动隐藏。",
|
|
49
|
+
connectStepOpen: "在 iPhone 上打开 DSH Mobile。",
|
|
50
|
+
connectStepScan: "进入「连接设置 → 扫码连接电脑」。",
|
|
51
|
+
connectStepConfirm: "扫描二维码,核对主机地址后确认连接。",
|
|
52
|
+
connectPrivacy: "二维码可授予这台主机的访问权限,请勿分享。隐藏二维码不会撤销官方登录链接;链接在宿主进程重启前仍然有效。",
|
|
53
|
+
connectNetwork: "请保持电脑和 HTTPS 入口在线。扫码用于重新连接 App,无法修复已经断开的隧道。",
|
|
54
|
+
connectSetup: "请由部署者通过 mobile.publicOrigin 或 DSH_MOBILE_PUBLIC_ORIGIN 配置手机可达的 HTTPS 地址,并将域名加入宿主信任列表。",
|
|
55
|
+
connectUntrusted: "宿主尚未信任配置的地址。请更新 --trusted-host 并重启宿主后再生成二维码。",
|
|
56
|
+
connectUnsupported: "此宿主版本不能提供官方登录链接,请更新到支持的宿主和插件组合。",
|
|
57
|
+
connectError: "暂时无法读取连接信息,请检查连接后重试。",
|
|
58
|
+
connectLoading: "正在读取连接信息…",
|
|
59
|
+
directoryManual: '输入路径', directoryTitle: '选择工作目录', directoryHint: '选择电脑上的文件夹。', directoryComputer: '浏览电脑', directoryNoSaved: '暂无工作区,可以浏览电脑选择文件夹。', directoryGo: '前往', directoryParent: '上一级', directoryHidden: '隐藏目录', directoryLoading: '正在读取目录…', directoryError: '暂时无法读取此目录,请检查路径、权限和连接。', directoryRetry: '重试', directorySearch: '筛选文件夹', directoryEmpty: '没有子目录。', directoryTruncated: '目录较多,仅显示部分内容;可输入完整路径前往。', directorySelect: '使用此目录',
|
|
60
|
+
renameSession: '重命名会话', sessionTitle: '会话标题', renameUnavailable: '暂时无法修改标题,请重试。',
|
|
61
|
+
roomActionUnavailable: '快捷入口暂不可用,请进入成员页管理。', memberFailed: '任务失败',
|
|
62
|
+
currentConversation: '当前会话',
|
|
63
|
+
loadingAgents: '正在检查可用代理…', noAgents: '这台电脑暂时没有可用的本地代理。', loginAgents: '请先在电脑上登录代理,再邀请加入。',
|
|
64
|
+
queued: '排队消息', queueError: '队列更新失败,请重试。', editQueued: '编辑', removeQueued: '移除', sendNow: '立即发送', sendingQueued: '等待主机确认…',
|
|
65
|
+
members: '成员', membersHint: '进入成员对话,或调整成员的分工。', inviteMember: '邀请成员', memberSettings: '成员设置', duplicateMember: '这个成员名称已被使用。', invalidMember: '成员名称不能包含空格或 @。', roomError: '操作未完成,请检查连接后重试。', mainMember: '主代理', openMemberChat: '进入对话', awaitingMember: '等待首个任务', agentProvider: '代理', chooseProvider: '选择可用代理', needsLogin: '需在电脑登录', memberName: '名称', memberRole: '角色指令', memberRoleHint: '这位成员主要负责什么?', inviteHint: '邀请仅添加成员。在群聊中发送 @消息后才开始任务。', roleChangeHint: '角色修改从后续任务开始生效。', advanced: '高级信息', inheritWorkspace: '跟随群聊工作区', saving: '正在保存…', save: '保存', stopMemberConfirm: '停止这位成员当前的任务?', removeMemberConfirm: '将这位成员移出群聊?已有对话记录会保留。', confirm: '确认', stopMember: '停止任务', removeMember: '移出成员', backMembers: '返回成员列表', memberUnit: '位成员',
|
|
66
|
+
inputTools: '添加与设置', closeTools: '关闭输入工具', attachments: '添加附件', commands: '指令', permissions: '会话权限',
|
|
67
|
+
welcome: '今天想做点什么?', welcomeHint: '继续一个想法,或开始新的任务。',
|
|
68
|
+
menu: '会话', close: '关闭会话导航', returnToConversation: '展开完整聊天', title: '工作空间,随身同行',
|
|
69
|
+
settings: '移动端设置', back: '返回', display: '界面',
|
|
70
|
+
automatic: '跟随屏幕尺寸', mobile: '移动布局', desktop: '桌面布局',
|
|
71
|
+
restoreMobile: '返回移动布局',
|
|
72
|
+
appearance: '布局设置仅对当前客户端生效。',
|
|
73
|
+
connection: '连接', reconnect: '重新连接', connected: '已连接',
|
|
74
|
+
connecting: '正在连接…', disconnected: '连接已中断',
|
|
75
|
+
server: '主机', privacy: '模型、工具与文件操作由电脑执行。',
|
|
76
|
+
done: '完成', brand: 'DSH',
|
|
77
|
+
grouping: '会话查看方式', byTime: '时间', byWorkspace: '工作区', today: '今天', yesterday: '昨天', earlier: '更早', unassignedWorkspace: '未指定工作区', scan: '扫码连接电脑', conversation: '会话', options: '会话选项', connectionSettings: '连接设置',
|
|
78
|
+
libraryEyebrow: '随身工作空间', libraryTitle: '继续你的想法。', librarySubtitle: '从一段对话,开始新的进展。', newSession: '新建会话', searchTitles: '搜索', searchResults: '搜索结果', recent: '最近会话', workspaces: '工作区', loadingSessions: '正在加载会话…', noMatches: '没有匹配的会话。', noSessions: '新的对话,会出现在这里。', navigationError: '暂时无法打开这段会话,请重试。', running: '进行中', completed: '已完成', libraryFooter: '让每一个想法,向前一步。',
|
|
79
|
+
workspace: '打开工作区', hostPath: '电脑目录', hostPathHelp: '输入电脑上已有目录的完整路径。这里选择的是电脑工作区,不是手机文件夹。', openWorkspace: '打开', cancel: '取消',
|
|
80
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** A touch presentation of the rendered message's own actions. The source
|
|
2
|
+
* button stays mounted; its plugin still owns edits, confirmations and errors. */
|
|
3
|
+
export declare class MessageMenu {
|
|
4
|
+
private doc;
|
|
5
|
+
private timer;
|
|
6
|
+
private point;
|
|
7
|
+
private menu;
|
|
8
|
+
constructor(doc: Document);
|
|
9
|
+
private row;
|
|
10
|
+
private actions;
|
|
11
|
+
private down;
|
|
12
|
+
private move;
|
|
13
|
+
private cancel;
|
|
14
|
+
private context;
|
|
15
|
+
private close;
|
|
16
|
+
private open;
|
|
17
|
+
dispose(): void;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=messageMenu.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** A touch presentation of the rendered message's own actions. The source
|
|
2
|
+
* button stays mounted; its plugin still owns edits, confirmations and errors. */
|
|
3
|
+
export class MessageMenu {
|
|
4
|
+
doc;
|
|
5
|
+
timer;
|
|
6
|
+
point;
|
|
7
|
+
menu;
|
|
8
|
+
constructor(doc) {
|
|
9
|
+
this.doc = doc;
|
|
10
|
+
doc.addEventListener('pointerdown', this.down, true);
|
|
11
|
+
doc.addEventListener('pointermove', this.move, true);
|
|
12
|
+
doc.addEventListener('pointerup', this.cancel, true);
|
|
13
|
+
doc.addEventListener('pointercancel', this.cancel, true);
|
|
14
|
+
doc.addEventListener('scroll', this.cancel, true);
|
|
15
|
+
doc.addEventListener('contextmenu', this.context, true);
|
|
16
|
+
}
|
|
17
|
+
row(target) {
|
|
18
|
+
if (!this.doc.documentElement.hasAttribute('data-dsh-mobile') || !(target instanceof Element) || target.closest('button,a,input,textarea,[contenteditable],pre,code,[role=dialog]'))
|
|
19
|
+
return;
|
|
20
|
+
const user = target.closest('[data-conversation-scroll] [data-time-hover-root]');
|
|
21
|
+
if (user)
|
|
22
|
+
return user;
|
|
23
|
+
const assistant = target.closest('[data-chat-flow-kind="assistant-step"][data-chat-turn]'), turn = assistant?.getAttribute('data-chat-turn');
|
|
24
|
+
if (turn && /^\d+$/.test(turn))
|
|
25
|
+
return assistant?.closest('[data-conversation-scroll]')?.querySelector(`[data-turn-tail="${turn}"]`) ?? undefined;
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
actions(row) {
|
|
29
|
+
// An action bar's immediate buttons have accessible labels. Never harvest
|
|
30
|
+
// controls inside the text, tool calls, JSON viewers or nested messages.
|
|
31
|
+
return [...row.querySelectorAll(row.hasAttribute('data-turn-tail') ? 'button[aria-label]' : ':scope > div > button[aria-label], :scope > div > span > button[aria-label]')]
|
|
32
|
+
.filter(b => /^(复制|已复制|编辑|撤回|点赞|点踩|不喜欢|分支|好的回答|有问题的回答|在新对话中分支|Copy|Copied|Edit|Withdraw|Like|Dislike|Branch)/i.test(b.getAttribute('aria-label') ?? ''));
|
|
33
|
+
}
|
|
34
|
+
down = (e) => {
|
|
35
|
+
this.cancel();
|
|
36
|
+
const row = this.row(e.target);
|
|
37
|
+
if (!row || e.pointerType === 'mouse' || !this.actions(row).length)
|
|
38
|
+
return;
|
|
39
|
+
this.point = { x: e.clientX, y: e.clientY, row };
|
|
40
|
+
this.timer = setTimeout(() => { if (this.point)
|
|
41
|
+
this.open(row, e.clientX, e.clientY); }, 500);
|
|
42
|
+
};
|
|
43
|
+
move = (e) => { if (this.point && Math.hypot(e.clientX - this.point.x, e.clientY - this.point.y) > 10)
|
|
44
|
+
this.cancel(); };
|
|
45
|
+
cancel = () => { clearTimeout(this.timer); this.timer = undefined; this.point = undefined; };
|
|
46
|
+
context = (e) => {
|
|
47
|
+
const row = this.row(e.target);
|
|
48
|
+
if (row && this.actions(row).length) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
this.open(row, e.clientX, e.clientY);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
close = () => { this.menu?.close(); this.menu?.remove(); this.menu = undefined; };
|
|
54
|
+
open(row, x, y) {
|
|
55
|
+
this.cancel();
|
|
56
|
+
this.close();
|
|
57
|
+
const actions = this.actions(row);
|
|
58
|
+
if (!actions.length)
|
|
59
|
+
return;
|
|
60
|
+
const menu = this.doc.createElement('dialog');
|
|
61
|
+
this.menu = menu;
|
|
62
|
+
menu.dataset.mobileMessageMenu = '';
|
|
63
|
+
menu.setAttribute('aria-label', this.doc.documentElement.lang.startsWith('zh') ? '消息操作' : 'Message actions');
|
|
64
|
+
for (const target of actions) {
|
|
65
|
+
const button = this.doc.createElement('button');
|
|
66
|
+
button.type = 'button';
|
|
67
|
+
button.disabled = target.disabled;
|
|
68
|
+
const icon = target.querySelector('svg')?.cloneNode(true);
|
|
69
|
+
if (icon)
|
|
70
|
+
button.append(icon);
|
|
71
|
+
button.append(this.doc.createTextNode(target.getAttribute('aria-label')));
|
|
72
|
+
button.onclick = () => { this.close(); if (target.isConnected && !target.disabled && this.doc.documentElement.hasAttribute('data-dsh-mobile'))
|
|
73
|
+
target.click(); };
|
|
74
|
+
menu.append(button);
|
|
75
|
+
}
|
|
76
|
+
menu.addEventListener('cancel', this.close);
|
|
77
|
+
menu.addEventListener('click', e => { if (e.target === menu) {
|
|
78
|
+
const r = menu.getBoundingClientRect();
|
|
79
|
+
if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom)
|
|
80
|
+
this.close();
|
|
81
|
+
} });
|
|
82
|
+
this.doc.body.append(menu);
|
|
83
|
+
menu.showModal();
|
|
84
|
+
const bounds = menu.getBoundingClientRect(), viewport = this.doc.defaultView?.visualViewport;
|
|
85
|
+
const width = viewport?.width ?? this.doc.documentElement.clientWidth, height = viewport?.height ?? this.doc.documentElement.clientHeight;
|
|
86
|
+
menu.style.left = `${Math.max(12, Math.min(x - bounds.width / 2, width - bounds.width - 12))}px`;
|
|
87
|
+
menu.style.top = `${Math.max(12, Math.min(y + 12, height - bounds.height - 12))}px`;
|
|
88
|
+
}
|
|
89
|
+
dispose() {
|
|
90
|
+
this.cancel();
|
|
91
|
+
this.close();
|
|
92
|
+
this.doc.removeEventListener('pointerdown', this.down, true);
|
|
93
|
+
this.doc.removeEventListener('pointermove', this.move, true);
|
|
94
|
+
this.doc.removeEventListener('pointerup', this.cancel, true);
|
|
95
|
+
this.doc.removeEventListener('pointercancel', this.cancel, true);
|
|
96
|
+
this.doc.removeEventListener('scroll', this.cancel, true);
|
|
97
|
+
this.doc.removeEventListener('contextmenu', this.context, true);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type NativeAction = 'settings' | 'scan';
|
|
2
|
+
/** Additive bridge features: older shells and ordinary browsers retain Web controls. */
|
|
3
|
+
export declare function hasNativeAction(action: NativeAction): boolean;
|
|
4
|
+
export declare function requestNativeAction(action: NativeAction): void;
|
|
5
|
+
export declare function reportChrome(visible: boolean): void;
|
|
6
|
+
//# sourceMappingURL=native.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BRIDGE_VERSION } from '../protocol.js';
|
|
2
|
+
/** Additive bridge features: older shells and ordinary browsers retain Web controls. */
|
|
3
|
+
export function hasNativeAction(action) {
|
|
4
|
+
return window.__DSH_MOBILE_SHELL__?.bridgeVersion === BRIDGE_VERSION
|
|
5
|
+
&& window.__DSH_MOBILE_SHELL__.capabilities?.includes(action) === true
|
|
6
|
+
&& !!window.webkit?.messageHandlers?.dshMobile;
|
|
7
|
+
}
|
|
8
|
+
export function requestNativeAction(action) {
|
|
9
|
+
if (hasNativeAction(action))
|
|
10
|
+
window.webkit?.messageHandlers?.dshMobile?.postMessage({ type: action, bridgeVersion: BRIDGE_VERSION });
|
|
11
|
+
}
|
|
12
|
+
export function reportChrome(visible) {
|
|
13
|
+
if (hasNativeAction('settings'))
|
|
14
|
+
window.webkit?.messageHandlers?.dshMobile?.postMessage({ type: 'chrome', bridgeVersion: BRIDGE_VERSION, visible });
|
|
15
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ISessions, SessionListState, SessionSummary } from '@deepseek-ai/dsh-api-session-controller/client';
|
|
2
|
+
import type { IWorkspaces } from '@deepseek-ai/dsh-api-workspace-controller/client';
|
|
3
|
+
import type { UiWorkspace } from '@deepseek-ai/dsh-client-ui-workspace/client';
|
|
4
|
+
type SessionId = SessionSummary['id'];
|
|
5
|
+
/**
|
|
6
|
+
* The session the main view shows, read off both host lines: alpha.2 deleted
|
|
7
|
+
* the list's `current` for the per-row `retainedBy.mainView` reference count;
|
|
8
|
+
* 0.1.5 carries only `current`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function mainSessionId(list: SessionListState): SessionId | undefined;
|
|
11
|
+
export interface NavigationCapabilities {
|
|
12
|
+
sessions: ISessions;
|
|
13
|
+
workspaces: IWorkspaces;
|
|
14
|
+
workspace: UiWorkspace;
|
|
15
|
+
}
|
|
16
|
+
export type LibraryGrouping = 'time' | 'workspace';
|
|
17
|
+
export declare const GROUPING_KEY = "dsh.mobile.grouping";
|
|
18
|
+
export interface SessionGroup {
|
|
19
|
+
key: string;
|
|
20
|
+
label: string;
|
|
21
|
+
rows: SessionSummary[];
|
|
22
|
+
}
|
|
23
|
+
/** Group an ordered projection without joining different directories with the same basename. */
|
|
24
|
+
export declare function groupSessions(rows: SessionSummary[], mode: LibraryGrouping, labels: {
|
|
25
|
+
today: string;
|
|
26
|
+
yesterday: string;
|
|
27
|
+
earlier: string;
|
|
28
|
+
workspace: string;
|
|
29
|
+
}, now?: Date): SessionGroup[];
|
|
30
|
+
/** Optional official services can disappear without removing basic mobile presentation. */
|
|
31
|
+
export declare class MobileNavigation {
|
|
32
|
+
private value;
|
|
33
|
+
private listeners;
|
|
34
|
+
readonly getSnapshot: () => NavigationCapabilities | undefined;
|
|
35
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
36
|
+
set(value: NavigationCapabilities | undefined): void;
|
|
37
|
+
}
|
|
38
|
+
/** The Host owns metadata and membership; this projection only orders visible ordinary sessions. */
|
|
39
|
+
export declare function recentSessions(state: SessionListState, archived: readonly string[], query: string): SessionSummary[];
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session the main view shows, read off both host lines: alpha.2 deleted
|
|
3
|
+
* the list's `current` for the per-row `retainedBy.mainView` reference count;
|
|
4
|
+
* 0.1.5 carries only `current`.
|
|
5
|
+
*/
|
|
6
|
+
export function mainSessionId(list) {
|
|
7
|
+
return Object.values(list.byId).find(summary => (summary.retainedBy?.mainView ?? 0) > 0)?.id
|
|
8
|
+
?? list.current;
|
|
9
|
+
}
|
|
10
|
+
export const GROUPING_KEY = 'dsh.mobile.grouping';
|
|
11
|
+
/** Group an ordered projection without joining different directories with the same basename. */
|
|
12
|
+
export function groupSessions(rows, mode, labels, now = new Date()) {
|
|
13
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
14
|
+
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1).getTime();
|
|
15
|
+
const groups = new Map();
|
|
16
|
+
for (const row of rows) {
|
|
17
|
+
const key = mode === 'workspace' ? row.cwd || '' : row.updatedAt >= today ? 'today' : row.updatedAt >= yesterday ? 'yesterday' : 'earlier';
|
|
18
|
+
let group = groups.get(key);
|
|
19
|
+
if (!group) {
|
|
20
|
+
const label = mode === 'workspace' ? key.split(/[\\/]/).filter(Boolean).at(-1) || labels.workspace : labels[key];
|
|
21
|
+
group = { key, label, rows: [] };
|
|
22
|
+
groups.set(key, group);
|
|
23
|
+
}
|
|
24
|
+
group.rows.push(row);
|
|
25
|
+
}
|
|
26
|
+
if (mode === 'workspace') {
|
|
27
|
+
const counts = new Map();
|
|
28
|
+
for (const group of groups.values())
|
|
29
|
+
counts.set(group.label, (counts.get(group.label) ?? 0) + 1);
|
|
30
|
+
for (const group of groups.values())
|
|
31
|
+
if ((counts.get(group.label) ?? 0) > 1 && group.key)
|
|
32
|
+
group.label = group.key;
|
|
33
|
+
}
|
|
34
|
+
return [...groups.values()];
|
|
35
|
+
}
|
|
36
|
+
/** Optional official services can disappear without removing basic mobile presentation. */
|
|
37
|
+
export class MobileNavigation {
|
|
38
|
+
value;
|
|
39
|
+
listeners = new Set();
|
|
40
|
+
getSnapshot = () => this.value;
|
|
41
|
+
subscribe = (listener) => { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; };
|
|
42
|
+
set(value) {
|
|
43
|
+
this.value = value;
|
|
44
|
+
for (const listener of this.listeners)
|
|
45
|
+
listener();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** The Host owns metadata and membership; this projection only orders visible ordinary sessions. */
|
|
49
|
+
export function recentSessions(state, archived, query) {
|
|
50
|
+
const hidden = new Set(archived);
|
|
51
|
+
const term = query.trim().toLocaleLowerCase();
|
|
52
|
+
const current = mainSessionId(state);
|
|
53
|
+
return state.ids.flatMap(id => {
|
|
54
|
+
const row = state.byId[id];
|
|
55
|
+
if (!row || hidden.has(id) || row.origin === 'subagent' || (row.blank && id !== current))
|
|
56
|
+
return [];
|
|
57
|
+
if (term && !`${row.title ?? ''} ${row.displayTitle} ${row.cwd ?? ''}`.toLocaleLowerCase().includes(term))
|
|
58
|
+
return [];
|
|
59
|
+
return [row];
|
|
60
|
+
}).sort((a, b) => b.updatedAt - a.updatedAt || a.id.localeCompare(b.id));
|
|
61
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Adapt only the next menu opened by a checked official hero trigger.
|
|
2
|
+
* Choices, disabled state, selection, refusal and dismissal remain with its owner. */
|
|
3
|
+
export declare class MobilePickers {
|
|
4
|
+
private readonly doc;
|
|
5
|
+
private pending;
|
|
6
|
+
private menu;
|
|
7
|
+
private chrome;
|
|
8
|
+
private shade;
|
|
9
|
+
private hidden;
|
|
10
|
+
private separators;
|
|
11
|
+
constructor(doc: Document);
|
|
12
|
+
private readonly click;
|
|
13
|
+
sync(active: boolean): void;
|
|
14
|
+
private clear;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=pickers.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** Adapt only the next menu opened by a checked official hero trigger.
|
|
2
|
+
* Choices, disabled state, selection, refusal and dismissal remain with its owner. */
|
|
3
|
+
export class MobilePickers {
|
|
4
|
+
doc;
|
|
5
|
+
pending;
|
|
6
|
+
menu;
|
|
7
|
+
chrome;
|
|
8
|
+
shade;
|
|
9
|
+
hidden = [];
|
|
10
|
+
separators = [];
|
|
11
|
+
constructor(doc) {
|
|
12
|
+
this.doc = doc;
|
|
13
|
+
doc.addEventListener('click', this.click, true);
|
|
14
|
+
}
|
|
15
|
+
click = (event) => {
|
|
16
|
+
if (!this.doc.documentElement.hasAttribute('data-dsh-mobile'))
|
|
17
|
+
return;
|
|
18
|
+
const trigger = event.target.closest('button[aria-haspopup="menu"]');
|
|
19
|
+
if (!trigger || trigger.getAttribute('aria-expanded') === 'true')
|
|
20
|
+
return;
|
|
21
|
+
const preset = trigger.closest('[data-slot="conversation.hero.agentPreset"]');
|
|
22
|
+
const workspace = trigger.closest('[data-phase="hero"]') && trigger.querySelector('svg') && trigger.parentElement?.querySelector(':scope > [data-slot="conversation.hero.workspace"]');
|
|
23
|
+
if (!preset && !workspace)
|
|
24
|
+
return;
|
|
25
|
+
this.pending = { trigger, kind: preset ? 'preset' : 'workspace', before: new Set(this.doc.querySelectorAll('[role=menu]')), until: Date.now() + 1000 };
|
|
26
|
+
};
|
|
27
|
+
sync(active) {
|
|
28
|
+
if (!active) {
|
|
29
|
+
this.clear();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (this.menu && !this.menu.isConnected)
|
|
33
|
+
this.clear();
|
|
34
|
+
const pending = this.pending;
|
|
35
|
+
if (!pending)
|
|
36
|
+
return;
|
|
37
|
+
if (Date.now() > pending.until || !pending.trigger.isConnected) {
|
|
38
|
+
this.pending = undefined;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const menu = Array.from(this.doc.querySelectorAll('[role=menu]')).find(el => !pending.before.has(el) && !el.parentElement?.closest('[role=menu]'));
|
|
42
|
+
if (!menu)
|
|
43
|
+
return;
|
|
44
|
+
this.pending = undefined;
|
|
45
|
+
this.menu = menu;
|
|
46
|
+
menu.dataset.mobilePicker = pending.kind;
|
|
47
|
+
if (pending.kind === 'preset')
|
|
48
|
+
for (const item of Array.from(menu.querySelectorAll('[role=menuitem]')).slice(1)) {
|
|
49
|
+
if (!item.hasAttribute('data-mobile-picker-divider')) {
|
|
50
|
+
item.setAttribute('data-mobile-picker-divider', '');
|
|
51
|
+
this.separators.push(item);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const zh = /^zh/i.test(this.doc.documentElement.lang || navigator.language);
|
|
55
|
+
const header = this.doc.createElement('div');
|
|
56
|
+
header.dataset.mobilePickerChrome = '';
|
|
57
|
+
const title = this.doc.createElement('strong');
|
|
58
|
+
title.textContent = pending.kind === 'workspace' ? (zh ? '选择工作区' : 'Choose workspace') : (zh ? '选择模式' : 'Choose mode');
|
|
59
|
+
const close = this.doc.createElement('button');
|
|
60
|
+
close.textContent = zh ? '完成' : 'Done';
|
|
61
|
+
close.onclick = () => pending.trigger.click();
|
|
62
|
+
header.append(title, close);
|
|
63
|
+
if (pending.kind === 'workspace') {
|
|
64
|
+
const input = this.doc.createElement('input');
|
|
65
|
+
input.type = 'search';
|
|
66
|
+
input.placeholder = zh ? '搜索工作区' : 'Search workspaces';
|
|
67
|
+
input.setAttribute('aria-label', input.placeholder);
|
|
68
|
+
input.oninput = () => {
|
|
69
|
+
for (const el of this.hidden)
|
|
70
|
+
el.hidden = false;
|
|
71
|
+
this.hidden = [];
|
|
72
|
+
for (const item of menu.querySelectorAll('[role=menuitem]')) {
|
|
73
|
+
// Keep the add-workspace action available when no existing directory matches.
|
|
74
|
+
if (item.parentElement?.parentElement === menu.lastElementChild && menu.children.length > 2)
|
|
75
|
+
continue;
|
|
76
|
+
if (!item.textContent?.toLocaleLowerCase().includes(input.value.toLocaleLowerCase()) && !item.hidden) {
|
|
77
|
+
item.hidden = true;
|
|
78
|
+
this.hidden.push(item);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
header.append(input);
|
|
83
|
+
}
|
|
84
|
+
menu.prepend(header);
|
|
85
|
+
this.chrome = header;
|
|
86
|
+
const shade = this.doc.createElement('div');
|
|
87
|
+
shade.dataset.mobilePickerShade = '';
|
|
88
|
+
shade.setAttribute('aria-hidden', 'true');
|
|
89
|
+
// The official outside-pointer handler closes its menu, including focus restoration.
|
|
90
|
+
menu.before(shade);
|
|
91
|
+
this.shade = shade;
|
|
92
|
+
}
|
|
93
|
+
clear() {
|
|
94
|
+
this.menu?.removeAttribute('data-mobile-picker');
|
|
95
|
+
this.chrome?.remove();
|
|
96
|
+
this.shade?.remove();
|
|
97
|
+
for (const el of this.hidden)
|
|
98
|
+
el.hidden = false;
|
|
99
|
+
for (const el of this.separators)
|
|
100
|
+
el.removeAttribute('data-mobile-picker-divider');
|
|
101
|
+
this.separators = [];
|
|
102
|
+
this.hidden = [];
|
|
103
|
+
this.menu = undefined;
|
|
104
|
+
this.chrome = undefined;
|
|
105
|
+
this.shade = undefined;
|
|
106
|
+
this.pending = undefined;
|
|
107
|
+
}
|
|
108
|
+
dispose() { this.doc.removeEventListener('click', this.click, true); this.clear(); }
|
|
109
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { MobileSurface } from './surface.ts';
|
|
2
|
+
export type DisplayMode = 'auto' | 'mobile' | 'desktop';
|
|
3
|
+
export interface MobileSnapshot {
|
|
4
|
+
mode: DisplayMode;
|
|
5
|
+
active: boolean;
|
|
6
|
+
drawer: boolean;
|
|
7
|
+
supported: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Owns only this document's mobile effects. It never writes Host or sibling state. */
|
|
10
|
+
export declare class MobilePresentation {
|
|
11
|
+
private readonly win;
|
|
12
|
+
private readonly shell;
|
|
13
|
+
private readonly listeners;
|
|
14
|
+
private readonly media;
|
|
15
|
+
private readonly observer;
|
|
16
|
+
private readonly style;
|
|
17
|
+
private frame;
|
|
18
|
+
private snapshot;
|
|
19
|
+
private disposed;
|
|
20
|
+
private pending;
|
|
21
|
+
private readonly composerFocus;
|
|
22
|
+
private readonly pickers;
|
|
23
|
+
private readonly viewport;
|
|
24
|
+
readonly surface: MobileSurface;
|
|
25
|
+
constructor(win: Window, shell: boolean);
|
|
26
|
+
readonly getSnapshot: () => MobileSnapshot;
|
|
27
|
+
readonly prepareNavigation: () => void;
|
|
28
|
+
readonly subscribe: (listener: () => void) => (() => void);
|
|
29
|
+
readonly setMode: (mode: DisplayMode) => void;
|
|
30
|
+
private readonly display;
|
|
31
|
+
private readonly pointer;
|
|
32
|
+
private readonly keyboard;
|
|
33
|
+
private readonly schedule;
|
|
34
|
+
private reconcile;
|
|
35
|
+
/** Release only owned presentation resources; leave shared streams and drafts intact. */
|
|
36
|
+
dispose(): void;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=presentation.d.ts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { MobilePickers } from './pickers.js';
|
|
2
|
+
import { MobileViewport } from './viewport.js';
|
|
3
|
+
import { MOBILE_CSS } from './styles.js';
|
|
4
|
+
import { MobileSurface } from './surface.js';
|
|
5
|
+
import { ComposerFocus } from './composerFocus.js';
|
|
6
|
+
const STORAGE_KEY = 'dsh.mobile.display';
|
|
7
|
+
/** Owns only this document's mobile effects. It never writes Host or sibling state. */
|
|
8
|
+
export class MobilePresentation {
|
|
9
|
+
win;
|
|
10
|
+
shell;
|
|
11
|
+
listeners = new Set();
|
|
12
|
+
media;
|
|
13
|
+
observer;
|
|
14
|
+
style;
|
|
15
|
+
frame;
|
|
16
|
+
snapshot;
|
|
17
|
+
disposed = false;
|
|
18
|
+
pending = 0;
|
|
19
|
+
composerFocus;
|
|
20
|
+
pickers;
|
|
21
|
+
viewport;
|
|
22
|
+
surface;
|
|
23
|
+
constructor(win, shell) {
|
|
24
|
+
this.win = win;
|
|
25
|
+
this.shell = shell;
|
|
26
|
+
const query = new URL(win.location.href).searchParams.get('mobile');
|
|
27
|
+
let saved = null;
|
|
28
|
+
try {
|
|
29
|
+
saved = win.localStorage.getItem(STORAGE_KEY);
|
|
30
|
+
}
|
|
31
|
+
catch { /* Storage may be disabled; preferences remain document-local. */ }
|
|
32
|
+
const mode = query === '1' ? 'mobile' : query === '0' ? 'desktop'
|
|
33
|
+
: saved === 'mobile' || saved === 'desktop' ? saved : 'auto';
|
|
34
|
+
this.snapshot = { mode, active: false, drawer: false, supported: false };
|
|
35
|
+
this.surface = new MobileSurface(win.document);
|
|
36
|
+
this.viewport = new MobileViewport(win);
|
|
37
|
+
this.pickers = new MobilePickers(win.document);
|
|
38
|
+
this.composerFocus = new ComposerFocus(win.document, () => this.snapshot.active);
|
|
39
|
+
this.media = win.matchMedia('(max-width: 760px) and (pointer: coarse)');
|
|
40
|
+
this.style = win.document.createElement('style');
|
|
41
|
+
this.style.dataset.mobileOwned = 'styles';
|
|
42
|
+
this.style.textContent = MOBILE_CSS;
|
|
43
|
+
win.document.head.append(this.style);
|
|
44
|
+
this.observer = new MutationObserver(this.schedule);
|
|
45
|
+
this.observer.observe(win.document.body, { subtree: true, childList: true, attributes: true, attributeFilter: ['data-sidebar-collapsed', 'data-phase', 'aria-selected', 'aria-expanded', 'data-ds-dark-theme', 'src'], characterData: true });
|
|
46
|
+
this.media.addEventListener('change', this.schedule);
|
|
47
|
+
win.addEventListener('resize', this.schedule);
|
|
48
|
+
win.addEventListener('dsh-mobile-display', this.display);
|
|
49
|
+
win.document.addEventListener('pointerdown', this.pointer, true);
|
|
50
|
+
win.document.addEventListener('keydown', this.keyboard, true);
|
|
51
|
+
win.visualViewport?.addEventListener('resize', this.schedule);
|
|
52
|
+
win.visualViewport?.addEventListener('scroll', this.schedule);
|
|
53
|
+
this.reconcile();
|
|
54
|
+
}
|
|
55
|
+
getSnapshot = () => this.snapshot;
|
|
56
|
+
prepareNavigation = () => { this.composerFocus.arm(); };
|
|
57
|
+
subscribe = (listener) => {
|
|
58
|
+
this.listeners.add(listener);
|
|
59
|
+
return () => { this.listeners.delete(listener); };
|
|
60
|
+
};
|
|
61
|
+
setMode = (mode) => {
|
|
62
|
+
this.snapshot = { ...this.snapshot, mode };
|
|
63
|
+
try {
|
|
64
|
+
this.win.localStorage.setItem(STORAGE_KEY, mode);
|
|
65
|
+
}
|
|
66
|
+
catch { /* Private storage denial does not block the UI. */ }
|
|
67
|
+
this.reconcile(true);
|
|
68
|
+
};
|
|
69
|
+
display = (event) => {
|
|
70
|
+
const mode = event.detail?.mode;
|
|
71
|
+
if (mode === 'auto' || mode === 'mobile' || mode === 'desktop')
|
|
72
|
+
this.setMode(mode);
|
|
73
|
+
};
|
|
74
|
+
pointer = () => { if (this.snapshot.active)
|
|
75
|
+
this.win.document.documentElement.dataset.mobileInput = 'pointer'; };
|
|
76
|
+
keyboard = (event) => { if (this.snapshot.active && event.key === 'Tab')
|
|
77
|
+
this.win.document.documentElement.dataset.mobileInput = 'keyboard'; };
|
|
78
|
+
schedule = () => {
|
|
79
|
+
if (this.disposed || this.pending)
|
|
80
|
+
return;
|
|
81
|
+
this.pending = this.win.requestAnimationFrame(() => { this.pending = 0; this.reconcile(); });
|
|
82
|
+
};
|
|
83
|
+
reconcile(force = false) {
|
|
84
|
+
if (this.disposed)
|
|
85
|
+
return;
|
|
86
|
+
const doc = this.win.document;
|
|
87
|
+
// Root priority alone cannot transfer child-slot render authority. Keep
|
|
88
|
+
// the official frame and require its observed rc1 anchor arrangement.
|
|
89
|
+
const candidate = doc.querySelector('[data-slot="root"] > div:has(> [data-shell-overlay])');
|
|
90
|
+
const frame = candidate?.querySelector('[data-slot="main"]') && candidate.querySelector('[data-slot="sidebar"]') ? candidate : undefined;
|
|
91
|
+
if (this.frame !== frame)
|
|
92
|
+
this.frame?.removeAttribute('data-mobile-frame');
|
|
93
|
+
this.frame = frame;
|
|
94
|
+
const active = !!frame && (this.snapshot.mode === 'mobile' || (this.snapshot.mode === 'auto' && (this.shell || this.media.matches)));
|
|
95
|
+
doc.documentElement.toggleAttribute('data-dsh-mobile', active);
|
|
96
|
+
frame?.toggleAttribute('data-mobile-frame', active);
|
|
97
|
+
if (active)
|
|
98
|
+
doc.documentElement.style.setProperty('--mobile-height', `${Math.round(this.win.visualViewport?.height ?? this.win.innerHeight)}px`);
|
|
99
|
+
else
|
|
100
|
+
doc.documentElement.style.removeProperty('--mobile-height');
|
|
101
|
+
if (active)
|
|
102
|
+
doc.documentElement.style.setProperty('--mobile-top', `${Math.round(this.win.visualViewport?.offsetTop ?? 0)}px`);
|
|
103
|
+
else
|
|
104
|
+
doc.documentElement.style.removeProperty('--mobile-top');
|
|
105
|
+
doc.documentElement.toggleAttribute('data-mobile-native-insets', active && this.shell);
|
|
106
|
+
if (active && !doc.documentElement.dataset.mobileInput)
|
|
107
|
+
doc.documentElement.dataset.mobileInput = 'pointer';
|
|
108
|
+
if (!active)
|
|
109
|
+
doc.documentElement.removeAttribute('data-mobile-input');
|
|
110
|
+
this.surface.sync(active ? frame : undefined);
|
|
111
|
+
this.viewport.sync(active ? frame : undefined);
|
|
112
|
+
this.pickers.sync(active);
|
|
113
|
+
const drawer = active && !frame?.hasAttribute('data-sidebar-collapsed');
|
|
114
|
+
if (force || active !== this.snapshot.active || drawer !== this.snapshot.drawer || !!frame !== this.snapshot.supported) {
|
|
115
|
+
this.snapshot = { ...this.snapshot, active, drawer, supported: !!frame };
|
|
116
|
+
for (const listener of this.listeners)
|
|
117
|
+
listener();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Release only owned presentation resources; leave shared streams and drafts intact. */
|
|
121
|
+
dispose() {
|
|
122
|
+
this.disposed = true;
|
|
123
|
+
this.observer.disconnect();
|
|
124
|
+
this.composerFocus.dispose();
|
|
125
|
+
this.surface.dispose();
|
|
126
|
+
this.viewport.dispose();
|
|
127
|
+
this.pickers.dispose();
|
|
128
|
+
this.win.removeEventListener('dsh-mobile-display', this.display);
|
|
129
|
+
this.win.document.removeEventListener('pointerdown', this.pointer, true);
|
|
130
|
+
this.win.document.removeEventListener('keydown', this.keyboard, true);
|
|
131
|
+
this.win.document.documentElement.removeAttribute('data-mobile-input');
|
|
132
|
+
this.win.document.documentElement.removeAttribute('data-mobile-native-insets');
|
|
133
|
+
this.media.removeEventListener('change', this.schedule);
|
|
134
|
+
this.win.removeEventListener('resize', this.schedule);
|
|
135
|
+
this.win.visualViewport?.removeEventListener('resize', this.schedule);
|
|
136
|
+
this.win.visualViewport?.removeEventListener('scroll', this.schedule);
|
|
137
|
+
if (this.pending)
|
|
138
|
+
this.win.cancelAnimationFrame(this.pending);
|
|
139
|
+
this.frame?.removeAttribute('data-mobile-frame');
|
|
140
|
+
this.win.document.documentElement.removeAttribute('data-dsh-mobile');
|
|
141
|
+
this.win.document.documentElement.style.removeProperty('--mobile-height');
|
|
142
|
+
this.win.document.documentElement.style.removeProperty('--mobile-top');
|
|
143
|
+
this.style.remove();
|
|
144
|
+
this.listeners.clear();
|
|
145
|
+
}
|
|
146
|
+
}
|