Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 79 additions & 8 deletions client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ window.__ModuleLoader__.load({
// the bundle must bind React itself - otherwise every mobile component
// crashes at render time with "ReferenceError: React is not defined".
var React = require("react");
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name2 in all)
Expand All @@ -26,6 +28,14 @@ var __copyProps = (to, from, except, desc) => {
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// client/index.jsx
Expand Down Expand Up @@ -109,7 +119,66 @@ function redactStatus(s) {
}

// client/mobile/MobileNavToggle.tsx
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
var primitives = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);

// client/mobile/icon-compat.mjs
var MOBILE_ICON_CANDIDATES = {
panelLeft: [
"IconPanelLeftOutlineRegular",
"IconPanelLeftOutlineMedium",
"IconPanelLeftOutline16",
"IconPanelLeftOutline"
],
download: [
"IconDownloadOutlineRegular",
"IconDownloadOutlineMedium",
"IconDownloadOutline16",
"IconDownloadOutline"
],
folderOpen: [
"IconFolderOpenOutlineRegular",
"IconFolderOpenOutlineMedium",
"IconFolderOpenOutline16",
"IconFolderOpenOutline",
"IconFolderOpen"
]
};
var MOBILE_ICON_FALLBACK = {
panelLeft: "\u2630",
download: "\u2913",
folderOpen: "\u25A4"
};
function isRenderableIcon(value) {
if (typeof value === "function") return true;
return typeof value === "object" && value !== null;
}
function pickIcon(registry, names) {
if (registry === null || typeof registry !== "object") return null;
for (const name2 of names) {
const candidate = registry[name2];
if (isRenderableIcon(candidate)) return candidate;
}
return null;
}
function resolveMobileIcons(registry) {
const resolved = {};
for (const [key, names] of Object.entries(MOBILE_ICON_CANDIDATES)) {
resolved[key] = pickIcon(registry, names);
}
return resolved;
}

// client/mobile/MobileIcon.tsx
function MobileIcon({ icon, name: name2, size }) {
if (!isRenderableIcon(icon)) {
return /* @__PURE__ */ React.createElement("span", { "data-mobile-nav": "icon-fallback", "aria-hidden": "true", style: { fontSize: size, lineHeight: 1 } }, MOBILE_ICON_FALLBACK[name2]);
}
const Icon = icon;
return /* @__PURE__ */ React.createElement(Icon, { size });
}

// client/mobile/MobileNavToggle.tsx
var icons = resolveMobileIcons(primitives);
function MobileNavToggle({ toggleSidebar, t }) {
const toggleExplorer = () => {
const frame = document.querySelector('[data-mobile-nav="frame"]');
Expand All @@ -129,7 +198,7 @@ function MobileNavToggle({ toggleSidebar, t }) {
title: t("open"),
onClick: () => toggleSidebar()
},
/* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives.IconPanelLeftOutline16, { size: 16 })
/* @__PURE__ */ React.createElement(MobileIcon, { icon: icons.panelLeft, name: "panelLeft", size: 16 })
), /* @__PURE__ */ React.createElement(
"button",
{
Expand All @@ -139,13 +208,13 @@ function MobileNavToggle({ toggleSidebar, t }) {
title: t("files"),
onClick: toggleExplorer
},
/* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives.IconFolderOpenOutline16, { size: 16 })
/* @__PURE__ */ React.createElement(MobileIcon, { icon: icons.folderOpen, name: "folderOpen", size: 16 })
));
}

// client/mobile/MobileNavOverlay.tsx
var import_react = require("react");
var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
var primitives2 = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);

// client/mobile/nav-targets.mjs
var DRAWER_SELECTOR = '[data-mobile-nav="frame"] > :first-child';
Expand Down Expand Up @@ -181,6 +250,7 @@ function isOverlayTap(target) {
}

// client/mobile/MobileNavOverlay.tsx
var icons2 = resolveMobileIcons(primitives2);
var MOBILE_QUERY = "(max-width: 1023px)";
function useMobile() {
const [mobile, setMobile] = (0, import_react.useState)(() => window.matchMedia(MOBILE_QUERY).matches);
Expand Down Expand Up @@ -367,12 +437,13 @@ function MobileNavOverlay({ toggleSidebar, t }) {
title: t("open"),
onClick: () => toggleSidebar()
},
/* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives2.IconPanelLeftOutline16, { size: 18 })
/* @__PURE__ */ React.createElement(MobileIcon, { icon: icons2.panelLeft, name: "panelLeft", size: 18 })
));
}

// client/mobile/MobileDrawerFooter.tsx
var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
var primitives3 = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1);
var icons3 = resolveMobileIcons(primitives3);
function MobileDrawerFooter({ useSessions, downloadSessionLog, toggleSidebar, t }) {
const sessionId = useSessions((state) => state.current);
const openExplorer = () => {
Expand All @@ -388,7 +459,7 @@ function MobileDrawerFooter({ useSessions, downloadSessionLog, toggleSidebar, t
title: t("files"),
onClick: openExplorer
},
/* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives3.IconPanelLeftOutline16, { size: 14 }),
/* @__PURE__ */ React.createElement(MobileIcon, { icon: icons3.panelLeft, name: "panelLeft", size: 14 }),
/* @__PURE__ */ React.createElement("span", null, t("files"))
), /* @__PURE__ */ React.createElement(
"button",
Expand All @@ -402,7 +473,7 @@ function MobileDrawerFooter({ useSessions, downloadSessionLog, toggleSidebar, t
if (sessionId !== void 0) downloadSessionLog(sessionId);
}
},
/* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives3.IconDownloadOutline16, { size: 14 }),
/* @__PURE__ */ React.createElement(MobileIcon, { icon: icons3.download, name: "download", size: 14 }),
/* @__PURE__ */ React.createElement("span", null, t("sessionLog"))
));
}
Expand Down
11 changes: 8 additions & 3 deletions client/mobile/MobileDrawerFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import { IconDownloadOutline16, IconPanelLeftOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
import { NS } from './locales.ts'
import { MobileIcon } from './MobileIcon.tsx'
import { resolveMobileIcons } from './icon-compat.mjs'

/** 宿主图标在加载时解析一次:不同 DSH 版本的导出名不同(见 icon-compat.mjs)。 */
const icons = resolveMobileIcons(primitives)

/** Full props for the sidebar footer action entry. */
export interface MobileDrawerFooterProps extends PropsRuntime<'sidebar.footer.action'>, PropsLocale<typeof NS> {
Expand Down Expand Up @@ -35,7 +40,7 @@ export function MobileDrawerFooter({ useSessions, downloadSessionLog, toggleSide
title={t('files')}
onClick={openExplorer}
>
<IconPanelLeftOutline16 size={14} />
<MobileIcon icon={icons.panelLeft} name="panelLeft" size={14} />
<span>{t('files')}</span>
</button>
<button
Expand All @@ -48,7 +53,7 @@ export function MobileDrawerFooter({ useSessions, downloadSessionLog, toggleSide
if (sessionId !== undefined) downloadSessionLog(sessionId)
}}
>
<IconDownloadOutline16 size={14} />
<MobileIcon icon={icons.download} name="download" size={14} />
<span>{t('sessionLog')}</span>
</button>
</div>
Expand Down
31 changes: 31 additions & 0 deletions client/mobile/MobileIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { MOBILE_ICON_FALLBACK, isRenderableIcon } from './icon-compat.mjs'

/** Props for the version-tolerant icon slot. */
export interface MobileIconProps {
/** Icon component resolved from the host primitives namespace (null when absent). */
icon: unknown
/** Which fallback glyph to draw when the running DSH has no such export. */
name: 'panelLeft' | 'download' | 'folderOpen'
/** Square edge in px, forwarded to the host icon. */
size: number
}

/**
* Renders the resolved host icon, or a text glyph when this DSH build no longer
* exports that name. It must never hand `undefined` to createElement: that is
* exactly what crashed the whole slot entry (React #130) and left the mobile
* drawer dead on DSH 0.1.7-rc.2+ (see icon-compat.mjs for the version matrix).
*/
export function MobileIcon({ icon, name, size }: MobileIconProps) {
if (!isRenderableIcon(icon)) {
return (
<span data-mobile-nav="icon-fallback" aria-hidden="true" style={{ fontSize: size, lineHeight: 1 }}>
{MOBILE_ICON_FALLBACK[name]}
</span>
)
}
// The component type differs per DSH version, so it is only known at runtime;
// `never` as the return type keeps JSX typing happy without an `any` cast.
const Icon = icon as (props: { size?: number }) => never
return <Icon size={size} />
}
9 changes: 7 additions & 2 deletions client/mobile/MobileNavOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { useEffect, useLayoutEffect, useState } from 'react'
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import { IconPanelLeftOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
import { NS } from './locales.ts'
import { MobileIcon } from './MobileIcon.tsx'
import { resolveMobileIcons } from './icon-compat.mjs'

/** 宿主图标在加载时解析一次:不同 DSH 版本的导出名不同(见 icon-compat.mjs)。 */
const icons = resolveMobileIcons(primitives)
import {
DRAWER_SELECTOR,
TOGGLE_SELECTOR,
Expand Down Expand Up @@ -282,7 +287,7 @@ export function MobileNavOverlay({ toggleSidebar, t }: MobileNavOverlayProps) {
title={t('open')}
onClick={() => toggleSidebar()}
>
<IconPanelLeftOutline16 size={18} />
<MobileIcon icon={icons.panelLeft} name="panelLeft" size={18} />
</button>
)}
</>
Expand Down
11 changes: 8 additions & 3 deletions client/mobile/MobileNavToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import { IconFolderOpenOutline16, IconPanelLeftOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
import { NS } from './locales.ts'
import { MobileIcon } from './MobileIcon.tsx'
import { resolveMobileIcons } from './icon-compat.mjs'

/** 宿主图标在加载时解析一次:不同 DSH 版本的导出名不同(见 icon-compat.mjs)。 */
const icons = resolveMobileIcons(primitives)

/** Full props for the session-header directory toggle. */
export interface MobileNavToggleProps extends PropsRuntime<'conversation.session.header.actions'>, PropsLocale<typeof NS> {
Expand Down Expand Up @@ -36,7 +41,7 @@ export function MobileNavToggle({ toggleSidebar, t }: MobileNavToggleProps) {
title={t('open')}
onClick={() => toggleSidebar()}
>
<IconPanelLeftOutline16 size={16} />
<MobileIcon icon={icons.panelLeft} name="panelLeft" size={16} />
</button>
<button
type="button"
Expand All @@ -45,7 +50,7 @@ export function MobileNavToggle({ toggleSidebar, t }: MobileNavToggleProps) {
title={t('files')}
onClick={toggleExplorer}
>
<IconFolderOpenOutline16 size={16} />
<MobileIcon icon={icons.folderOpen} name="folderOpen" size={16} />
</button>
</>
)
Expand Down
90 changes: 90 additions & 0 deletions client/mobile/icon-compat.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// DSH primitives 图标导出的跨版本兼容层。
//
// 背景(2026-09 实测,dsh 0.1.7-rc.2 + dsh-pocket 2.10.6):
// 移动端组件原来直接具名导入
// IconPanelLeftOutline16 / IconDownloadOutline16 / IconFolderOpenOutline16,
// 而 @deepseek-ai/dsh-client-ui-primitives 在 0.1.7-rc 之后把图标导出改成了
// Icon<Name>OutlineRegular / Icon<Name>OutlineMedium(`16` 尺寸后缀被移除)。
// 于是这些导入在运行期变成 undefined —— 组件一渲染就抛 React #130
// ("Element type is invalid ... got: undefined"),slot 条目整条崩掉:
// 移动样式表照样注入,但 `data-mobile-nav="frame"` 标记永远挂不上,抽屉彻底失效。
// 手机上的表现:56px 侧栏图标条一直占位、正文被挤成一列(用户可见症状)。
//
// 这里做三件事:
// 1. 不再按具体名字静态导入 —— 新版名字在旧版不存在、旧版名字在新版不存在;
// 对真正的 ESM 来说,具名导入不存在的导出是**链接期**错误,比 undefined 更难查;
// 2. 用候选名列表在运行时解析,先后覆盖新旧两代命名;
// 3. 全都解析不到时返回 null(绝不返回 undefined),调用方用文字字形兜底 ——
// 在任何一代 DSH 上都不会再把整条 slot 打崩。
//
// 纯数据 + 纯函数、无 DOM 依赖,便于 `node --test` 直接引(test/icon-compat.test.js)。

/** 每个图标按「新名优先、旧名兜底」的候选顺序。 */
export const MOBILE_ICON_CANDIDATES = {
panelLeft: [
'IconPanelLeftOutlineRegular',
'IconPanelLeftOutlineMedium',
'IconPanelLeftOutline16',
'IconPanelLeftOutline',
],
download: [
'IconDownloadOutlineRegular',
'IconDownloadOutlineMedium',
'IconDownloadOutline16',
'IconDownloadOutline',
],
folderOpen: [
'IconFolderOpenOutlineRegular',
'IconFolderOpenOutlineMedium',
'IconFolderOpenOutline16',
'IconFolderOpenOutline',
'IconFolderOpen',
],
}

/** 解析不到图标时用的文字字形(保证按钮仍然可见、可点、可读)。 */
export const MOBILE_ICON_FALLBACK = {
panelLeft: '☰',
download: '⤓',
folderOpen: '▤',
}

/**
* 注册表里的某个导出能不能当 React 组件渲染。
* 组件可能是函数,也可能是对象(memo / forwardRef / class 组件),所以两者都收;
* 数字、字符串、null、undefined 一律不算 —— 它们正是打崩 slot 的元凶。
* @param value - 待判定的导出值。
* @returns 可渲染时为 true。
*/
export function isRenderableIcon(value) {
if (typeof value === 'function') return true
return typeof value === 'object' && value !== null
}

/**
* 按候选顺序取第一个可渲染的导出。
* @param registry - 宿主 primitives 命名空间(可能为 null/不是对象)。
* @param names - 候选导出名,按优先级排列。
* @returns 命中的组件;一个都没有时返回 null(**绝不返回 undefined**)。
*/
export function pickIcon(registry, names) {
if (registry === null || typeof registry !== 'object') return null
for (const name of names) {
const candidate = registry[name]
if (isRenderableIcon(candidate)) return candidate
}
return null
}

/**
* 把宿主 primitives 命名空间解析成移动端要用的三个图标。
* @param registry - 宿主 primitives 命名空间。
* @returns `{ panelLeft, download, folderOpen }`,缺的为 null(不是 undefined)。
*/
export function resolveMobileIcons(registry) {
const resolved = {}
for (const [key, names] of Object.entries(MOBILE_ICON_CANDIDATES)) {
resolved[key] = pickIcon(registry, names)
}
return resolved
}
Loading
Loading