@dsh-tom/dsh-css 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ```powershell
8
8
  cd D:\product\dsh-css
9
- npm ci
9
+ npm run check
10
10
  npm login --registry=https://registry.npmjs.org/
11
11
  npm publish --access public --registry=https://registry.npmjs.org/
12
12
  ```
@@ -21,7 +21,7 @@ npm publish --access public --registry=https://registry.npmjs.org/
21
21
  打开 **插件 → 添加插件 → 安装第三方插件**,在“包名或地址”输入:
22
22
 
23
23
  ```text
24
- D:\product\dsh-css\dsh-tom-dsh-css-0.1.3.tgz
24
+ D:\product\dsh-css\dsh-tom-dsh-css-0.1.6.tgz
25
25
  ```
26
26
 
27
27
  安装并启用,然后完全退出并重新打开 dsh。
@@ -31,7 +31,7 @@ D:\product\dsh-css\dsh-tom-dsh-css-0.1.3.tgz
31
31
  包发布到 npmjs 后,先卸载已安装的旧版,再打开 **插件 → 添加插件 → 安装第三方插件**,输入:
32
32
 
33
33
  ```text
34
- @dsh-tom/dsh-css@0.1.3
34
+ @dsh-tom/dsh-css@0.1.6
35
35
  ```
36
36
 
37
37
  选择 **安装源 → npm 官方源**,安装并启用,然后完全退出并重新打开 dsh。
package/dist/client.js CHANGED
@@ -2,9 +2,295 @@
2
2
  window.__ModuleLoader__.load({
3
3
  id: "@dsh-tom/dsh-css",
4
4
  factory: (require) => {
5
- const { createElement } = require('react');
6
- const css = "/* 深色模式配色,保留内置文字、边框和语法高亮颜色。 */\nhtml body[data-ds-dark-theme] {\n /* 区域 1:左侧会话列表;Windows 标题栏在下方单独设置。原 RGB(27, 27, 28)。 */\n --dsw-specific-sidebar-fill: rgb(36, 37, 33) !important;\n\n /* 区域 2:会话标题、正文以及应用基础背景。原 RGB(21, 21, 23)。 */\n --dsw-alias-bg-base: rgb(39, 40, 34) !important;\n\n /* 区域 3:主输入框。原 RGB(44, 44, 46)。 */\n --dsw-specific-input-major: rgb(66, 67, 61) !important;\n\n /* 会话内代码块及使用同一语义变量的输出区域。原 RGB(27, 27, 28)。 */\n --dsw-alias-markdown-code-block: rgb(55, 56, 50) !important;\n}\n\n/* 标题栏及原生窗口按钮共享此颜色,会话列表继续使用原来的背景。 */\nhtml[data-windows-titlebar] body[data-ds-dark-theme] {\n --dsw-specific-sidebar-fill: rgb(32, 32, 32) !important;\n\n & .BynINW_sidebarCol {\n --dsw-specific-sidebar-fill: rgb(36, 37, 33) !important;\n }\n\n /* 主内容面板左上角改为直角。 */\n & .BynINW_centerCol {\n border-top-left-radius: 0;\n }\n}\n\n/* Windows 桌面版展开侧栏:左侧图标栏 + 右侧标题、会话列表。\n * 类名对应 DSH 0.2.1-rc.2;折叠侧栏继续使用宿主布局。\n * 复用品牌、新会话、面板和账户按钮,不改变原有点击事件。\n */\nhtml[data-windows-titlebar] body[data-ds-dark-theme] ._2H3hWW_root:not(._2H3hWW_collapsed) {\n --dsh-css-rail-width: 52px;\n display: grid;\n grid-template-columns: var(--dsh-css-rail-width) minmax(0, 1fr);\n grid-template-rows: 52px auto auto minmax(0, 1fr);\n padding: 0;\n position: relative;\n\n &::before {\n content: \"\";\n position: absolute;\n inset: 0 auto 0 0;\n width: var(--dsh-css-rail-width);\n background: rgb(32, 32, 32);\n pointer-events: none;\n }\n\n & ._2H3hWW_logoRow {\n grid-area: 1 / 2;\n height: 52px;\n margin: 0;\n padding: 0 16px;\n justify-content: flex-start;\n }\n\n & ._2H3hWW_logoRow::after {\n content: \"DeepSeek\";\n color: var(--dsw-alias-label-primary);\n font: 600 18px / 24px var(--dsw-font-family);\n letter-spacing: 0;\n }\n\n /* 原品牌按钮仍打开新会话,以首页图标呈现。 */\n & ._2H3hWW_brand {\n position: absolute;\n top: 8px;\n left: 8px;\n width: 36px;\n height: 36px;\n padding: 0;\n border-radius: 8px;\n justify-content: center;\n background: var(--dsw-alias-interactive-bg-hover);\n overflow: visible;\n }\n\n & ._2H3hWW_brandIdentity {\n display: none;\n }\n\n & ._2H3hWW_brand::before {\n content: \"\";\n width: 18px;\n height: 18px;\n background: currentColor;\n mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2 2 10v3h2v7a2 2 0 0 0 2 2h4v-7h4v7h4a2 2 0 0 0 2-2v-7h2v-3Z'/%3E%3C/svg%3E\") center / contain no-repeat;\n }\n\n &:has(._2H3hWW_panelActive) ._2H3hWW_brand {\n background: transparent;\n }\n\n & ._2H3hWW_newSession {\n grid-area: 2 / 2;\n align-self: start;\n z-index: 1;\n height: 36px;\n margin: 0 8px;\n padding: 0 8px;\n border: none;\n border-radius: 8px;\n background: transparent;\n font-weight: 400;\n justify-content: flex-start;\n }\n\n & ._2H3hWW_newSessionContent {\n width: 100%;\n justify-content: flex-start;\n gap: 8px;\n }\n\n & ._2H3hWW_newSessionContent > svg {\n display: none;\n }\n\n & ._2H3hWW_newSessionContent::before {\n content: \"\";\n flex: none;\n width: 14px;\n height: 14px;\n background: currentColor;\n mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 4H5a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-7M16 3a2.1 2.1 0 0 1 3 3L9 16l-4 1 1-4Z'/%3E%3C/svg%3E\") center / contain no-repeat;\n }\n\n & ._2H3hWW_panelList {\n grid-area: 2 / 1;\n position: relative;\n align-self: start;\n gap: 8px;\n margin: 0;\n padding: 0 8px;\n }\n\n & ._2H3hWW_panelRow {\n position: relative;\n justify-content: center;\n width: 36px;\n height: 36px;\n margin: 0;\n padding: 0;\n border-radius: 8px;\n color: var(--dsw-alias-label-tertiary);\n }\n\n & ._2H3hWW_panelTitle {\n display: none;\n }\n\n & ._2H3hWW_panelGlyph > svg {\n width: 18px;\n height: 18px;\n }\n\n & ._2H3hWW_panelActive {\n color: var(--dsw-alias-label-primary);\n }\n\n /* 列表与导航共用网格行,顶部留出新会话按钮的位置。 */\n & ._2H3hWW_regionArea {\n grid-area: 2 / 2 / -1 / 3;\n margin: 0;\n padding: 44px 0 0 12px;\n }\n\n & ._2H3hWW_footArea {\n grid-area: 4 / 1;\n position: relative;\n align-self: end;\n gap: 8px;\n padding: 8px;\n }\n\n & ._2H3hWW_footerActions:empty {\n display: none;\n }\n\n /* 左下角设置按钮沿用账户菜单,设置、登录等入口仍由宿主提供。 */\n & .ET65mq_trigger {\n position: relative;\n justify-content: center;\n width: 36px;\n height: 36px;\n padding: 0;\n border-radius: 8px;\n color: var(--dsw-alias-label-tertiary);\n }\n\n & .ET65mq_trigger > * {\n display: none;\n }\n\n & .ET65mq_trigger::before {\n content: \"\";\n width: 18px;\n height: 18px;\n background: currentColor;\n mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9.5 3-.5 2-2 1-2-.5-2 3.5L4.5 10v3L3 14l2 3.5 2-.5 2 1 .5 3h5l.5-3 2-1 2 .5 2-3.5-1.5-1v-3L21 9l-2-3.5-2 .5-2-1-.5-2Z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E\") center / contain no-repeat;\n }\n\n /* 同一元素也显示等待状态,只隐藏普通的相对时间。 */\n & .hIlkoa_sessionRow > .hIlkoa_time:not([aria-hidden=\"true\"]) {\n display: none;\n }\n\n & :is(._2H3hWW_brand, ._2H3hWW_newSession, .ET65mq_trigger):hover {\n background: var(--dsw-alias-interactive-bg-hover);\n }\n\n & :is(._2H3hWW_brand, ._2H3hWW_newSession, .ET65mq_trigger):focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: -2px;\n }\n\n /* 展开状态下宿主禁用了面板 Tooltip,为纯图标入口补上名称。 */\n & :is(._2H3hWW_panelRow, .ET65mq_trigger):is(:hover, :focus-visible)::after {\n content: attr(aria-label);\n position: absolute;\n z-index: 40;\n left: 44px;\n top: 50%;\n transform: translateY(-50%);\n padding: 4px 8px;\n border: 1px solid var(--dsw-alias-border-l3);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font: 12px / 20px var(--dsw-font-family);\n white-space: nowrap;\n pointer-events: none;\n }\n\n & .ET65mq_trigger:is(:hover, :focus-visible)::after {\n content: \"设置\";\n }\n}\n\n/* 悬浮卡片由宿主通过 portal 挂在侧栏外。 */\n.dsh-css-project {\n display: none;\n}\n\nhtml[data-windows-titlebar] body[data-ds-dark-theme] .dsh-css-project {\n display: flex;\n align-items: center;\n gap: 6px;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n line-height: 20px;\n\n & svg {\n flex: none;\n }\n\n & span {\n overflow-wrap: anywhere;\n }\n}\n";
7
- const inject = ['slots', 'sessions', 'workspaces', 'layout'];
5
+ const { createElement, useState, useEffect, useRef } = require('react');
6
+ const { Menu, Modal } = require('@deepseek-ai/dsh-client-ui-primitives');
7
+ const css = "/* 深色模式配色,保留内置文字、边框和语法高亮颜色。 */\nhtml body[data-ds-dark-theme] {\n /* 区域 1:左侧会话列表;Windows 标题栏在下方单独设置。原 RGB(27, 27, 28)。 */\n --dsw-specific-sidebar-fill: rgb(36, 37, 33) !important;\n\n /* 区域 2:会话标题、正文以及应用基础背景。原 RGB(21, 21, 23)。 */\n --dsw-alias-bg-base: rgb(39, 40, 34) !important;\n\n /* 区域 3:主输入框。原 RGB(44, 44, 46)。 */\n --dsw-specific-input-major: rgb(66, 67, 61) !important;\n\n /* 会话内代码块及使用同一语义变量的输出区域。原 RGB(27, 27, 28)。 */\n --dsw-alias-markdown-code-block: rgb(55, 56, 50) !important;\n}\n\n/* 标题栏及原生窗口按钮共享此颜色,会话列表继续使用原来的背景。 */\nhtml[data-windows-titlebar] body[data-ds-dark-theme] {\n --dsw-specific-sidebar-fill: rgb(32, 32, 32) !important;\n\n & .BynINW_sidebarCol {\n --dsw-specific-sidebar-fill: rgb(36, 37, 33) !important;\n }\n\n /* 主内容面板左上角改为直角。 */\n & .BynINW_centerCol {\n border-top-left-radius: 0;\n }\n}\n\n/* Windows 桌面版展开侧栏:左侧图标栏 + 右侧标题、会话列表。\n * 类名对应 DSH 0.2.1-rc.2;折叠侧栏继续使用宿主布局。\n * 复用品牌、新会话、面板和账户按钮,不改变原有点击事件。\n */\nhtml[data-windows-titlebar] body[data-ds-dark-theme] ._2H3hWW_root:not(._2H3hWW_collapsed) {\n --dsh-css-rail-width: 52px;\n display: grid;\n grid-template-columns: var(--dsh-css-rail-width) minmax(0, 1fr);\n grid-template-rows: 52px auto auto minmax(0, 1fr);\n padding: 0;\n position: relative;\n\n &::before {\n content: \"\";\n position: absolute;\n inset: 0 auto 0 0;\n width: var(--dsh-css-rail-width);\n background: rgb(32, 32, 32);\n pointer-events: none;\n }\n\n & ._2H3hWW_logoRow {\n grid-area: 1 / 2;\n height: 52px;\n margin: 0;\n padding: 0 16px;\n justify-content: flex-start;\n }\n\n & ._2H3hWW_logoRow::after {\n content: \"DeepSeek\";\n color: var(--dsw-alias-label-primary);\n font: 600 18px / 24px var(--dsw-font-family);\n letter-spacing: 0;\n }\n\n /* 原品牌按钮仍打开新会话,以首页图标呈现。 */\n & ._2H3hWW_brand {\n position: absolute;\n top: 8px;\n left: 8px;\n width: 36px;\n height: 36px;\n padding: 0;\n border-radius: 8px;\n justify-content: center;\n background: var(--dsw-alias-interactive-bg-hover);\n overflow: visible;\n }\n\n & ._2H3hWW_brandIdentity {\n display: none;\n }\n\n & ._2H3hWW_brand::before {\n content: \"\";\n width: 18px;\n height: 18px;\n background: currentColor;\n mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2 2 10v3h2v7a2 2 0 0 0 2 2h4v-7h4v7h4a2 2 0 0 0 2-2v-7h2v-3Z'/%3E%3C/svg%3E\") center / contain no-repeat;\n }\n\n &:has(._2H3hWW_panelActive) ._2H3hWW_brand {\n background: transparent;\n }\n\n & ._2H3hWW_newSession {\n grid-area: 2 / 2;\n align-self: start;\n z-index: 1;\n height: 36px;\n margin: 0 8px;\n padding: 0 8px;\n border: none;\n border-radius: 8px;\n background: transparent;\n font-weight: 400;\n justify-content: flex-start;\n }\n\n & ._2H3hWW_newSessionContent {\n width: 100%;\n justify-content: flex-start;\n gap: 8px;\n }\n\n & ._2H3hWW_newSessionContent > svg {\n display: none;\n }\n\n & ._2H3hWW_newSessionContent::before {\n content: \"\";\n flex: none;\n width: 14px;\n height: 14px;\n background: currentColor;\n mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 4H5a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-7M16 3a2.1 2.1 0 0 1 3 3L9 16l-4 1 1-4Z'/%3E%3C/svg%3E\") center / contain no-repeat;\n }\n\n & ._2H3hWW_panelList {\n grid-area: 2 / 1;\n position: relative;\n align-self: start;\n gap: 8px;\n margin: 0;\n padding: 0 8px;\n }\n\n & ._2H3hWW_panelRow {\n position: relative;\n justify-content: center;\n width: 36px;\n height: 36px;\n margin: 0;\n padding: 0;\n border-radius: 8px;\n color: var(--dsw-alias-label-tertiary);\n }\n\n & ._2H3hWW_panelTitle {\n display: none;\n }\n\n & ._2H3hWW_panelGlyph > svg {\n width: 18px;\n height: 18px;\n }\n\n & ._2H3hWW_panelActive {\n color: var(--dsw-alias-label-primary);\n }\n\n /* 列表与导航共用网格行,顶部留出新会话按钮的位置。 */\n & ._2H3hWW_regionArea {\n grid-area: 2 / 2 / -1 / 3;\n margin: 0;\n padding: 44px 0 0 12px;\n }\n\n /* 平铺会话标题与“会话”左对齐,状态图标放到右侧。 */\n & ._9lTDKa_flatList .hIlkoa_sessionRow {\n padding-inline-start: 4px;\n\n & > .hIlkoa_title {\n margin-left: 0;\n }\n\n & > .hIlkoa_slot {\n order: 1;\n }\n\n & > .hIlkoa_slot:empty {\n display: none;\n }\n }\n\n & ._2H3hWW_footArea {\n grid-area: 4 / 1;\n position: relative;\n align-self: end;\n gap: 8px;\n padding: 8px;\n }\n\n & ._2H3hWW_footerActions:empty {\n display: none;\n }\n\n /* 左下角设置按钮沿用账户菜单,设置、登录等入口仍由宿主提供。 */\n & .ET65mq_trigger {\n position: relative;\n justify-content: center;\n width: 36px;\n height: 36px;\n padding: 0;\n border-radius: 8px;\n color: var(--dsw-alias-label-tertiary);\n }\n\n & .ET65mq_trigger > * {\n display: none;\n }\n\n & .ET65mq_trigger::before {\n content: \"\";\n width: 18px;\n height: 18px;\n background: currentColor;\n mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9.5 3-.5 2-2 1-2-.5-2 3.5L4.5 10v3L3 14l2 3.5 2-.5 2 1 .5 3h5l.5-3 2-1 2 .5 2-3.5-1.5-1v-3L21 9l-2-3.5-2 .5-2-1-.5-2Z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E\") center / contain no-repeat;\n }\n\n /* 同一元素也显示等待状态,只隐藏普通的相对时间。 */\n & .hIlkoa_sessionRow > .hIlkoa_time:not([aria-hidden=\"true\"]) {\n display: none;\n }\n\n & :is(._2H3hWW_brand, ._2H3hWW_newSession, .ET65mq_trigger):hover {\n background: var(--dsw-alias-interactive-bg-hover);\n }\n\n & :is(._2H3hWW_brand, ._2H3hWW_newSession, .ET65mq_trigger):focus-visible {\n outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: -2px;\n }\n\n /* 展开状态下宿主禁用了面板 Tooltip,为纯图标入口补上名称。 */\n & :is(._2H3hWW_panelRow, .ET65mq_trigger):is(:hover, :focus-visible)::after {\n content: attr(aria-label);\n position: absolute;\n z-index: 40;\n left: 44px;\n top: 50%;\n transform: translateY(-50%);\n padding: 4px 8px;\n border: 1px solid var(--dsw-alias-border-l3);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font: 12px / 20px var(--dsw-font-family);\n white-space: nowrap;\n pointer-events: none;\n }\n\n & .ET65mq_trigger:is(:hover, :focus-visible)::after {\n content: \"设置\";\n }\n}\n\n/* 悬浮卡片由宿主通过 portal 挂在侧栏外。 */\n.dsh-css-project {\n display: none;\n}\n\nhtml[data-windows-titlebar] body[data-ds-dark-theme] .dsh-css-project {\n display: flex;\n align-items: center;\n gap: 6px;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n line-height: 20px;\n\n & svg {\n flex: none;\n }\n\n & span {\n overflow-wrap: anywhere;\n }\n}\n\n/* 项目页通过宿主 main 插槽挂载,菜单和对话框沿用宿主组件。 */\n.dsh-css-more-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.dsh-css-more-open {\n background: var(--dsw-alias-interactive-bg-hover) !important;\n}\n\nhtml[data-windows-titlebar] body[data-ds-dark-theme] ._2H3hWW_panelRow.dsh-css-more-open::after {\n display: none;\n}\n\n.dsh-css-projects-page {\n flex: 1;\n min-height: 0;\n overflow: auto;\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-base);\n font: 14px / 1.5 var(--dsw-font-family);\n}\n\n.dsh-css-projects-content {\n box-sizing: border-box;\n width: min(100%, 1408px);\n margin: 0 auto;\n padding: 32px;\n}\n\n.dsh-css-projects-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 20px;\n margin-bottom: 24px;\n\n & h1 {\n margin: 0;\n font-size: 24px;\n font-weight: 600;\n }\n}\n\n.dsh-css-projects-tools,\n.dsh-css-project-dialog-actions {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.dsh-css-projects-page button,\n.dsh-css-project-dialog-actions button {\n cursor: pointer;\n font: inherit;\n\n &:disabled { cursor: default; opacity: .45; }\n}\n\n.dsh-css-project-search {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 8px;\n width: 240px;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-border-l3);\n border-radius: 20px;\n color: var(--dsw-alias-label-tertiary);\n\n & input {\n width: 100%;\n min-width: 0;\n border: 0;\n outline: none;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n }\n\n &:focus-within { border-color: var(--dsw-alias-label-secondary); }\n}\n\n.dsh-css-project-create,\n.dsh-css-project-secondary {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n min-height: 34px;\n padding: 6px 16px;\n border: 1px solid transparent;\n border-radius: 20px;\n white-space: nowrap;\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.dsh-css-project-secondary {\n background: transparent;\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-border-l3);\n}\n\n.dsh-css-project-columns,\n.dsh-css-project-row {\n display: grid;\n grid-template-columns: minmax(0, 1fr) 100px 80px;\n align-items: center;\n gap: 16px;\n}\n\n.dsh-css-project-columns {\n min-height: 32px;\n border-bottom: 1px solid var(--dsw-alias-border-l3);\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n\n & button { border: 0; padding: 0; color: inherit; background: transparent; }\n}\n\n.dsh-css-project-row {\n position: relative;\n min-height: 70px;\n border-bottom: 1px solid var(--dsw-alias-border-l3);\n\n &::before {\n content: '';\n position: absolute;\n inset: 0 -12px;\n border-radius: 12px;\n pointer-events: none;\n }\n\n &:is(:hover, :focus-within, .is-open)::before { background: var(--dsw-alias-interactive-bg-hover); }\n & > * { position: relative; }\n &:is(:hover, :focus-within, .is-open) .dsh-css-project-chevron { opacity: 1; }\n}\n\n.dsh-css-project-name-cell { min-width: 0; }\n\n.dsh-css-project-name {\n display: flex;\n align-items: center;\n gap: 20px;\n width: 100%;\n min-height: 70px;\n padding: 8px;\n border: 0;\n background: transparent;\n color: inherit;\n text-align: left;\n\n & svg { flex: none; }\n & span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }\n}\n\n.dsh-css-project-chevron { opacity: 0; }\n.dsh-css-project-time { color: var(--dsw-alias-label-secondary); white-space: nowrap; }\n.dsh-css-project-actions { display: flex; gap: 4px; }\n\n.dsh-css-icon-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 32px;\n height: 32px;\n padding: 0;\n border: 0;\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n\n &:is(:hover, [aria-expanded='true']) { background: var(--dsw-alias-interactive-bg-hover); }\n}\n\n.dsh-css-projects-page button:focus-visible,\n.dsh-css-project-dialog-actions button:focus-visible {\n outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-label-secondary));\n outline-offset: 2px;\n}\n\n.dsh-css-project-empty { padding: 60px 0; text-align: center; color: var(--dsw-alias-label-tertiary); }\n.dsh-css-project-error { color: var(--dsw-alias-state-error-primary, #dc6767); overflow-wrap: anywhere; }\n.dsh-css-project-notice { color: var(--dsw-alias-label-secondary); }\n.dsh-css-project-field { display: flex; flex-direction: column; gap: 10px; }\n.dsh-css-project-field input {\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l3);\n border-radius: 8px;\n background: var(--dsw-alias-bg-base);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n}\n.dsh-css-project-path { color: var(--dsw-alias-label-tertiary); font-size: 12px; overflow-wrap: anywhere; }\n\n/* 项目弹窗使用 Codex 的深灰底、圆角和按钮配色。 */\n.dsh-css-project-remove-dialog,\n.dsh-css-project-edit-dialog {\n gap: 12px;\n padding-bottom: 20px;\n border-radius: 22px;\n box-shadow: none;\n\n & > :first-child > :first-child { padding: 20px 14px 4px 20px; }\n & h2 { font-size: 20px; line-height: 26px; font-weight: 600; overflow-wrap: anywhere; }\n & > :last-child { padding: 0 20px; }\n & .dsh-css-project-dialog-actions { gap: 12px; }\n & .dsh-css-project-dialog-actions button { min-height: 32px; padding: 5px 16px; border-radius: 10px; }\n & .dsh-css-project-secondary { border-color: transparent; }\n}\n\n.dsh-css-project-remove-dialog {\n width: min(420px, 100%);\n & .dsh-css-project-remove-content > p { padding: 0 20px; }\n & .dsh-css-project-remove-content > :last-child { margin-top: 0; padding: 0 20px; }\n}\n\n.dsh-css-project-edit-dialog {\n width: min(520px, 100%);\n gap: 20px;\n\n & .dsh-css-project-edit-content > :first-child { padding-bottom: 12px; }\n & .dsh-css-project-edit-content > :last-child { margin-top: 0; padding: 0 20px; }\n & .dsh-css-project-field { gap: 12px; }\n & .dsh-css-project-name-input {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-border-l3);\n border-radius: 12px;\n }\n & .dsh-css-project-name-input:focus-within { border-color: var(--dsw-alias-label-secondary); }\n & .dsh-css-project-name-input input {\n min-width: 0;\n width: 100%;\n height: 38px;\n padding: 0;\n border: 0;\n outline: 0;\n background: transparent;\n }\n & svg { flex: none; }\n & .dsh-css-project-source { display: flex; flex-direction: column; gap: 8px; }\n & .dsh-css-project-source-label { font-size: 13px; font-weight: 600; }\n & .dsh-css-project-path {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 10px;\n min-height: 48px;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l3);\n border-radius: 10px;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n }\n}\n\nbody[data-ds-dark-theme] :is(.dsh-css-project-remove-dialog, .dsh-css-project-edit-dialog) {\n background: rgb(55, 56, 51);\n\n & h2 { color: rgb(238, 238, 233); }\n & .dsh-css-project-remove-content > p,\n & .dsh-css-project-secondary { color: rgb(142, 143, 137); }\n & .dsh-css-project-secondary:hover:not(:disabled) { background: rgb(255 255 255 / 5%); }\n & .dsh-css-project-remove { color: rgb(240, 100, 96); background: rgb(76, 61, 56); }\n & .dsh-css-project-remove:hover:not(:disabled) { background: rgb(88, 65, 60); }\n & .dsh-css-project-name-input { background: rgb(58, 59, 53); border-color: rgb(80, 81, 74); }\n & .dsh-css-project-name-input:focus-within { border-color: rgb(148, 149, 138); }\n & .dsh-css-project-path { background: rgb(58, 59, 53); border-color: rgb(70, 71, 64); }\n & .dsh-css-project-path svg { color: rgb(142, 143, 137); }\n}\n\nbody[data-ds-dark-theme] .dsh-css-project-edit-dialog .dsh-css-project-create {\n background: rgb(248, 248, 243);\n color: rgb(55, 56, 51);\n}\n\nbody[data-ds-dark-theme] [role='presentation']:has(> :is(.dsh-css-project-remove-dialog, .dsh-css-project-edit-dialog)) {\n --dsw-alias-bg-mask-1: rgb(0 0 0 / 15%);\n}\n\n@media (max-width: 760px) {\n .dsh-css-projects-content { padding: 24px 20px; }\n .dsh-css-projects-header { align-items: stretch; flex-direction: column; }\n .dsh-css-project-search { flex: 1; width: auto; }\n .dsh-css-project-columns, .dsh-css-project-row { grid-template-columns: minmax(0, 1fr) 64px 68px; gap: 8px; }\n .dsh-css-project-name { gap: 10px; }\n}\n";
8
+ // React 和宿主的菜单、对话框由构建入口注入。
9
+ const PROJECTS_PANEL = 'dsh-css-projects';
10
+
11
+ function ProjectIcon({ name = 'folder', size = 16, ...props }) {
12
+ const paths = {
13
+ folder: 'M3 7V5a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Zm0 2h18',
14
+ more: 'M5 12h.01M12 12h.01M19 12h.01',
15
+ search: 'M21 21l-5-5M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0',
16
+ plus: 'M12 5v14M5 12h14',
17
+ edit: 'M12 4H5a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-7M16 3a2.1 2.1 0 0 1 3 3L9 16l-4 1 1-4Z',
18
+ archive: 'M3 3h18v4H3ZM5 7v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7M9 11h6',
19
+ close: 'm6 6 12 12M6 18 18 6',
20
+ chevron: 'm9 5 7 7-7 7',
21
+ };
22
+ return createElement('svg', {
23
+ width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
24
+ stroke: 'currentColor', strokeWidth: name === 'more' ? 3.5 : 1.6,
25
+ strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, ...props,
26
+ }, createElement('path', { d: paths[name] }));
27
+ }
28
+
29
+ function ProjectsNav({ size, openProjects }) {
30
+ const icon = useRef(null);
31
+ const anchor = useRef(null);
32
+ const [open, setOpen] = useState(false);
33
+ const opened = useRef(false);
34
+ opened.current = open;
35
+ // panellist 只提供图标插槽,外层按钮由宿主创建;接管点击以先打开菜单。
36
+ useEffect(() => {
37
+ const button = icon.current.closest('button');
38
+ anchor.current = button;
39
+ const toggle = event => {
40
+ event.stopPropagation();
41
+ setOpen(value => !value);
42
+ };
43
+ const keepAnchor = event => { if (opened.current) event.stopPropagation(); };
44
+ button.addEventListener('click', toggle);
45
+ button.addEventListener('pointerdown', keepAnchor);
46
+ button.setAttribute('aria-haspopup', 'menu');
47
+ return () => {
48
+ button.removeEventListener('click', toggle);
49
+ button.removeEventListener('pointerdown', keepAnchor);
50
+ button.removeAttribute('aria-haspopup');
51
+ button.removeAttribute('aria-expanded');
52
+ button.classList.remove('dsh-css-more-open');
53
+ };
54
+ }, []);
55
+ useEffect(() => {
56
+ anchor.current.setAttribute('aria-expanded', String(open));
57
+ anchor.current.classList.toggle('dsh-css-more-open', open);
58
+ }, [open]);
59
+ return createElement('span', { ref: icon, className: 'dsh-css-more-icon', onClick: event => event.stopPropagation() },
60
+ createElement(ProjectIcon, { name: 'more', size }),
61
+ createElement(Menu, {
62
+ open, anchor: null, portal: true, side: 'right', autoFocus: true,
63
+ getAnchorRect: () => anchor.current?.getBoundingClientRect() ?? null,
64
+ items: [{ id: 'projects', label: '项目', icon: createElement(ProjectIcon) }],
65
+ onClose: () => {
66
+ setOpen(false);
67
+ if (document.activeElement?.closest('[role="menu"]')) anchor.current?.focus();
68
+ },
69
+ onSelect: () => { setOpen(false); openProjects(); },
70
+ }));
71
+ }
72
+
73
+ function projectName(workspace) {
74
+ return workspace.title || workspace.path.split(/[\\/]/).filter(Boolean).at(-1) || workspace.path;
75
+ }
76
+
77
+ function projectUpdatedAt(workspace, sessions) {
78
+ return workspace.sessionIds.reduce((latest, id) => Math.max(latest, Number(sessions[id]?.updatedAt) || 0),
79
+ Date.parse(workspace.updatedAt || workspace.createdAt) || 0);
80
+ }
81
+
82
+ function projectTime(time, now) {
83
+ if (!time) return '—';
84
+ const minutes = Math.max(0, Math.floor((now - time) / 60000));
85
+ if (minutes < 1) return '刚刚';
86
+ if (minutes < 60) return `${minutes} 分`;
87
+ if (minutes < 1440) return `${Math.floor(minutes / 60)} 小时`;
88
+ if (minutes < 10080) return `${Math.floor(minutes / 1440)} 天`;
89
+ return `${Math.floor(minutes / 10080)} 周`;
90
+ }
91
+
92
+ function ProjectListRow({ workspace, updatedAt, now, busy, onOpen, onCreate, onAction }) {
93
+ const [open, setOpen] = useState(false);
94
+ const title = projectName(workspace);
95
+ return createElement('div', { className: `dsh-css-project-row${open ? ' is-open' : ''}`, role: 'row' },
96
+ createElement('div', { role: 'cell', className: 'dsh-css-project-name-cell' },
97
+ createElement('button', {
98
+ type: 'button', className: 'dsh-css-project-name', title: workspace.path,
99
+ disabled: busy, onClick: () => onOpen(workspace),
100
+ }, createElement(ProjectIcon), createElement('span', null, title),
101
+ createElement(ProjectIcon, { name: 'chevron', size: 12, className: 'dsh-css-project-chevron' }))),
102
+ createElement('div', { role: 'cell', className: 'dsh-css-project-time', title: updatedAt ? new Date(updatedAt).toLocaleString('zh-CN') : '' }, projectTime(updatedAt, now)),
103
+ createElement('div', { role: 'cell', className: 'dsh-css-project-actions' },
104
+ createElement(Menu, {
105
+ open, portal: true, side: 'bottom', align: 'end', autoFocus: true,
106
+ anchor: createElement('button', {
107
+ type: 'button', className: 'dsh-css-icon-button', disabled: busy,
108
+ 'aria-label': `${title}:更多操作`, 'aria-haspopup': 'menu', 'aria-expanded': open,
109
+ onClick: () => setOpen(value => !value),
110
+ }, createElement(ProjectIcon, { name: 'more' })),
111
+ items: [
112
+ { id: 'rename', label: '编辑项目', icon: createElement(ProjectIcon, { name: 'edit' }) },
113
+ { id: 'archive', label: '归档聊天', icon: createElement(ProjectIcon, { name: 'archive' }) },
114
+ { id: 'remove', label: '移除项目', icon: createElement(ProjectIcon, { name: 'close' }) },
115
+ ],
116
+ onClose: () => setOpen(false),
117
+ onSelect: action => { setOpen(false); onAction(action, workspace); },
118
+ }),
119
+ createElement('button', {
120
+ type: 'button', className: 'dsh-css-icon-button', disabled: busy,
121
+ 'aria-label': `${title}:新建聊天`, title: '新建聊天', onClick: () => onCreate(workspace),
122
+ }, createElement(ProjectIcon, { name: 'edit' }))));
123
+ }
124
+
125
+ function ProjectsPage({ useWorkspaces, useSessions, actions }) {
126
+ const workspaces = useWorkspaces(state => state);
127
+ const sessions = useSessions(state => state);
128
+ const [query, setQuery] = useState('');
129
+ const [ascending, setAscending] = useState(false);
130
+ const [now, setNow] = useState(Date.now);
131
+ const [busy, setBusy] = useState(false);
132
+ const pending = useRef(false);
133
+ const [error, setError] = useState('');
134
+ const [notice, setNotice] = useState('');
135
+ const [dialog, setDialog] = useState(null);
136
+ const [draft, setDraft] = useState('');
137
+ const ready = workspaces.phase === 'ready' && workspaces.state !== 'loading' && sessions.phase === 'ready';
138
+
139
+ useEffect(() => {
140
+ const timer = window.setInterval(() => setNow(Date.now()), 60000);
141
+ return () => window.clearInterval(timer);
142
+ }, []);
143
+
144
+ const run = async operation => {
145
+ if (pending.current) return;
146
+ pending.current = true;
147
+ setBusy(true);
148
+ setError('');
149
+ setNotice('');
150
+ try { await operation(); }
151
+ catch (reason) { setError(reason instanceof Error ? reason.message : String(reason)); }
152
+ finally { pending.current = false; setBusy(false); }
153
+ };
154
+ const requestAction = (kind, workspace) => {
155
+ setError('');
156
+ setNotice('');
157
+ setDraft(projectName(workspace));
158
+ setDialog({ kind, workspace });
159
+ };
160
+ const closeDialog = () => { if (!pending.current) { setDialog(null); setError(''); } };
161
+ const confirm = () => run(async () => {
162
+ const { kind, workspace } = dialog;
163
+ if (kind === 'rename') {
164
+ if (!draft.trim()) return;
165
+ await actions.rename(workspace.workspaceId, draft.trim());
166
+ } else if (kind === 'remove') {
167
+ await actions.remove(workspace.workspaceId);
168
+ } else {
169
+ const result = await actions.archive(workspace.workspaceId);
170
+ setNotice(result);
171
+ }
172
+ setDialog(null);
173
+ });
174
+ const needle = query.trim().toLocaleLowerCase();
175
+ const rows = workspaces.items.filter(workspace =>
176
+ `${projectName(workspace)}\n${workspace.path}`.toLocaleLowerCase().includes(needle))
177
+ .map(workspace => ({ workspace, updatedAt: projectUpdatedAt(workspace, sessions.byId) }))
178
+ .sort((a, b) => (ascending ? a.updatedAt - b.updatedAt : b.updatedAt - a.updatedAt) ||
179
+ projectName(a.workspace).localeCompare(projectName(b.workspace), 'zh-CN'));
180
+ const dialogTitle = dialog?.kind === 'rename' ? '编辑项目' : dialog?.kind === 'archive' ? '归档聊天' : '移除项目';
181
+ const removing = dialog?.kind === 'remove';
182
+ const editing = dialog?.kind === 'rename';
183
+
184
+ return createElement('section', { className: 'dsh-css-projects-page', 'aria-label': '项目', 'aria-busy': busy },
185
+ createElement('div', { className: 'dsh-css-projects-content' },
186
+ createElement('header', { className: 'dsh-css-projects-header' },
187
+ createElement('h1', null, '项目'),
188
+ createElement('div', { className: 'dsh-css-projects-tools' },
189
+ createElement('label', { className: 'dsh-css-project-search' },
190
+ createElement(ProjectIcon, { name: 'search', size: 14 }),
191
+ createElement('input', { type: 'search', value: query, placeholder: '搜索项目', 'aria-label': '搜索项目', onChange: event => setQuery(event.target.value) })),
192
+ createElement('button', {
193
+ type: 'button', className: 'dsh-css-project-create', disabled: busy || !ready,
194
+ onClick: () => run(async () => { if (await actions.create()) setNotice('项目已添加'); }),
195
+ }, createElement(ProjectIcon, { name: 'plus', size: 14 }), '创建'))),
196
+ error && !dialog && createElement('p', { className: 'dsh-css-project-error', role: 'alert' }, error),
197
+ notice && createElement('p', { className: 'dsh-css-project-notice', role: 'status' }, notice),
198
+ createElement('div', { role: 'table', 'aria-label': '项目列表', className: 'dsh-css-project-table' },
199
+ createElement('div', { role: 'row', className: 'dsh-css-project-columns' },
200
+ createElement('div', { role: 'columnheader' }, '名称'),
201
+ createElement('div', { role: 'columnheader', 'aria-sort': ascending ? 'ascending' : 'descending' },
202
+ createElement('button', { type: 'button', onClick: () => setAscending(value => !value) }, '已更新 ', ascending ? '↑' : '↓')),
203
+ createElement('div', { role: 'columnheader', 'aria-label': '操作' })),
204
+ ...rows.map(({ workspace, updatedAt }) => createElement(ProjectListRow, {
205
+ key: workspace.workspaceId, workspace, updatedAt, now, busy: busy || !ready,
206
+ onOpen: target => run(() => actions.open(target.workspaceId)),
207
+ onCreate: target => actions.startSession(target.workspaceId), onAction: requestAction,
208
+ }))),
209
+ rows.length === 0 && createElement('p', { className: 'dsh-css-project-empty', role: 'status' },
210
+ !ready ? '正在加载项目…' : needle ? '没有找到匹配的项目' : '还没有项目,点击“创建”选择项目文件夹。')),
211
+ createElement(Modal, {
212
+ open: dialog !== null, title: removing ? `移除 ${projectName(dialog.workspace)}?` : dialogTitle,
213
+ closeLabel: '关闭', onClose: closeDialog,
214
+ className: removing ? 'dsh-css-project-remove-dialog' : editing ? 'dsh-css-project-edit-dialog' : undefined,
215
+ contentClassName: removing ? 'dsh-css-project-remove-content' : editing ? 'dsh-css-project-edit-content' : undefined,
216
+ backdropBlur: !(removing || editing),
217
+ description: removing ? '这只会从应用中移除该项目。你电脑上的文件和现有聊天不会被删除。' : undefined,
218
+ footer: createElement('div', { className: 'dsh-css-project-dialog-actions' },
219
+ createElement('button', { type: 'button', className: 'dsh-css-project-secondary', disabled: busy, onClick: closeDialog }, '取消'),
220
+ createElement('button', {
221
+ type: 'button', className: `dsh-css-project-create${removing ? ' dsh-css-project-remove' : ''}`,
222
+ disabled: busy || (dialog?.kind === 'rename' && !draft.trim()), onClick: confirm,
223
+ }, busy ? '处理中…' : dialog?.kind === 'rename' ? '保存' : dialogTitle)),
224
+ }, editing
225
+ ? createElement('div', { className: 'dsh-css-project-field' },
226
+ createElement('label', { className: 'dsh-css-project-name-input' },
227
+ createElement(ProjectIcon), createElement('input', {
228
+ value: draft, disabled: busy, 'data-modal-autofocus': true,
229
+ 'aria-label': '项目名称',
230
+ onChange: event => setDraft(event.target.value),
231
+ onKeyDown: event => {
232
+ if (event.key === 'Enter' && !event.nativeEvent.isComposing) { event.preventDefault(); confirm(); }
233
+ },
234
+ })),
235
+ createElement('div', { className: 'dsh-css-project-source' },
236
+ createElement('span', { className: 'dsh-css-project-source-label' }, '源文件夹'),
237
+ createElement('div', { className: 'dsh-css-project-path', title: dialog.workspace.path },
238
+ createElement(ProjectIcon), createElement('span', null, dialog.workspace.path))))
239
+ : dialog?.kind === 'archive' ? createElement('p', null,
240
+ `归档“${projectName(dialog.workspace)}”中的聊天?运行中的聊天会保留,聊天记录不会删除。`) : null,
241
+ error && createElement('p', { className: 'dsh-css-project-error', role: 'alert' }, error)));
242
+ }
243
+
244
+ function registerProjects(ctx) {
245
+ const actions = {
246
+ async create() {
247
+ const picker = globalThis.__DSH_DIRECTORY_PICKER__;
248
+ const path = await (picker ? picker.pick() : ctx.uiWorkspace.pickDirectory());
249
+ if (path === null) return false;
250
+ await ctx.workspaces.create({ path });
251
+ return true;
252
+ },
253
+ rename: (id, title) => ctx.workspaces.rename(id, title),
254
+ remove: id => ctx.workspaces.delete(id),
255
+ startSession: id => ctx.uiWorkspace.startSession(id),
256
+ async open(id) {
257
+ const snapshot = ctx.workspaces.list.getSnapshot();
258
+ const workspace = snapshot.items.find(item => item.workspaceId === id);
259
+ if (!workspace) throw new Error('项目已被移除,请刷新后重试。');
260
+ const sessions = ctx.sessions.list.getSnapshot().byId;
261
+ const recent = workspace.sessionIds.filter(sessionId => sessions[sessionId] && !snapshot.archivedSessionIds.includes(sessionId))
262
+ .sort((a, b) => sessions[b].updatedAt - sessions[a].updatedAt)[0];
263
+ if (recent) ctx.uiWorkspace.openSession(recent);
264
+ else await ctx.uiWorkspace.openWorkspace(id);
265
+ },
266
+ async archive(id) {
267
+ const snapshot = ctx.workspaces.list.getSnapshot();
268
+ const workspace = snapshot.items.find(item => item.workspaceId === id);
269
+ if (!workspace) throw new Error('项目已被移除,请刷新后重试。');
270
+ const ids = workspace.sessionIds.filter(sessionId => !snapshot.archivedSessionIds.includes(sessionId));
271
+ let archived = 0;
272
+ const failures = [];
273
+ for (const sessionId of ids) {
274
+ try { await ctx.uiWorkspace.archiveSession(sessionId); archived++; }
275
+ catch (reason) {
276
+ failures.push(reason instanceof Error ? reason.message : String(reason));
277
+ }
278
+ }
279
+ if (failures.length) throw new Error(`已归档 ${archived} 个聊天,${failures.length} 个未归档:${failures[0]}`);
280
+ return ids.length ? `已归档 ${archived} 个聊天` : '没有需要归档的聊天';
281
+ },
282
+ };
283
+ ctx.slots.inject('main', () => ctx.slots.register({
284
+ name: 'main', key: PROJECTS_PANEL,
285
+ inject: () => ({ actions, hooks: { workspaces: ctx.workspaces.list, sessions: ctx.sessions.list } }),
286
+ }, ProjectsPage));
287
+ ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
288
+ name: 'sidebar.panellist', id: PROJECTS_PANEL, label: '更多', order: 100,
289
+ inject: () => ({ openProjects: () => ctx.layout.selectPanel(PROJECTS_PANEL) }),
290
+ }, ProjectsNav));
291
+ }
292
+
293
+ const inject = ['slots', 'sessions', 'workspaces', 'layout', 'uiWorkspace'];
8
294
 
9
295
  // createElement 和 css 由构建入口注入,React 复用宿主提供的模块。
10
296
  function ProjectHover({ sessionId, useWorkspaces, useSessions }) {
@@ -74,6 +360,7 @@ function apply(ctx) {
74
360
  });
75
361
 
76
362
  ctx.effect(() => applySidebarWidth(ctx.layout));
363
+ registerProjects(ctx);
77
364
  ctx.slots.inject('sidebar.session.row.hover', () => ctx.slots.register({
78
365
  name: 'sidebar.session.row.hover',
79
366
  id: 'dsh-css-project',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dsh-tom/dsh-css",
3
- "version": "0.1.3",
4
- "description": "为 DeepSeek Harness 深色界面调整侧边栏、会话、输入框和代码块的背景颜色。",
3
+ "version": "0.1.6",
4
+ "description": "为 DeepSeek Harness 调整深色界面,并增加项目管理列表。",
5
5
  "type": "module",
6
6
  "main": "./index.js",
7
7
  "exports": {
@@ -23,7 +23,8 @@
23
23
  },
24
24
  "scripts": {
25
25
  "build": "node scripts/build.mjs",
26
- "check": "node --check index.js && node --check src/client.js && node --check scripts/build.mjs && npm run build && node --check dist/client.js",
26
+ "test": "node --test test/*.test.mjs",
27
+ "check": "node --check index.js && node --check src/client.js && node --check src/projects.js && node --check scripts/build.mjs && npm run build && node --check dist/client.js && npm test",
27
28
  "prepack": "npm run check"
28
29
  }
29
30
  }
package/theme.css CHANGED
@@ -166,6 +166,23 @@ html[data-windows-titlebar] body[data-ds-dark-theme] ._2H3hWW_root:not(._2H3hWW_
166
166
  padding: 44px 0 0 12px;
167
167
  }
168
168
 
169
+ /* 平铺会话标题与“会话”左对齐,状态图标放到右侧。 */
170
+ & ._9lTDKa_flatList .hIlkoa_sessionRow {
171
+ padding-inline-start: 4px;
172
+
173
+ & > .hIlkoa_title {
174
+ margin-left: 0;
175
+ }
176
+
177
+ & > .hIlkoa_slot {
178
+ order: 1;
179
+ }
180
+
181
+ & > .hIlkoa_slot:empty {
182
+ display: none;
183
+ }
184
+ }
185
+
169
186
  & ._2H3hWW_footArea {
170
187
  grid-area: 4 / 1;
171
188
  position: relative;
@@ -259,3 +276,298 @@ html[data-windows-titlebar] body[data-ds-dark-theme] .dsh-css-project {
259
276
  overflow-wrap: anywhere;
260
277
  }
261
278
  }
279
+
280
+ /* 项目页通过宿主 main 插槽挂载,菜单和对话框沿用宿主组件。 */
281
+ .dsh-css-more-icon {
282
+ display: inline-flex;
283
+ align-items: center;
284
+ justify-content: center;
285
+ }
286
+
287
+ .dsh-css-more-open {
288
+ background: var(--dsw-alias-interactive-bg-hover) !important;
289
+ }
290
+
291
+ html[data-windows-titlebar] body[data-ds-dark-theme] ._2H3hWW_panelRow.dsh-css-more-open::after {
292
+ display: none;
293
+ }
294
+
295
+ .dsh-css-projects-page {
296
+ flex: 1;
297
+ min-height: 0;
298
+ overflow: auto;
299
+ color: var(--dsw-alias-label-primary);
300
+ background: var(--dsw-alias-bg-base);
301
+ font: 14px / 1.5 var(--dsw-font-family);
302
+ }
303
+
304
+ .dsh-css-projects-content {
305
+ box-sizing: border-box;
306
+ width: min(100%, 1408px);
307
+ margin: 0 auto;
308
+ padding: 32px;
309
+ }
310
+
311
+ .dsh-css-projects-header {
312
+ display: flex;
313
+ align-items: center;
314
+ justify-content: space-between;
315
+ gap: 20px;
316
+ margin-bottom: 24px;
317
+
318
+ & h1 {
319
+ margin: 0;
320
+ font-size: 24px;
321
+ font-weight: 600;
322
+ }
323
+ }
324
+
325
+ .dsh-css-projects-tools,
326
+ .dsh-css-project-dialog-actions {
327
+ display: flex;
328
+ align-items: center;
329
+ gap: 12px;
330
+ }
331
+
332
+ .dsh-css-projects-page button,
333
+ .dsh-css-project-dialog-actions button {
334
+ cursor: pointer;
335
+ font: inherit;
336
+
337
+ &:disabled { cursor: default; opacity: .45; }
338
+ }
339
+
340
+ .dsh-css-project-search {
341
+ box-sizing: border-box;
342
+ display: flex;
343
+ align-items: center;
344
+ gap: 8px;
345
+ width: 240px;
346
+ height: 34px;
347
+ padding: 0 12px;
348
+ border: 1px solid var(--dsw-alias-border-l3);
349
+ border-radius: 20px;
350
+ color: var(--dsw-alias-label-tertiary);
351
+
352
+ & input {
353
+ width: 100%;
354
+ min-width: 0;
355
+ border: 0;
356
+ outline: none;
357
+ background: transparent;
358
+ color: var(--dsw-alias-label-primary);
359
+ font: inherit;
360
+ }
361
+
362
+ &:focus-within { border-color: var(--dsw-alias-label-secondary); }
363
+ }
364
+
365
+ .dsh-css-project-create,
366
+ .dsh-css-project-secondary {
367
+ display: inline-flex;
368
+ align-items: center;
369
+ justify-content: center;
370
+ gap: 6px;
371
+ min-height: 34px;
372
+ padding: 6px 16px;
373
+ border: 1px solid transparent;
374
+ border-radius: 20px;
375
+ white-space: nowrap;
376
+ background: var(--dsw-alias-label-primary);
377
+ color: var(--dsw-alias-bg-base);
378
+ }
379
+
380
+ .dsh-css-project-secondary {
381
+ background: transparent;
382
+ color: var(--dsw-alias-label-primary);
383
+ border-color: var(--dsw-alias-border-l3);
384
+ }
385
+
386
+ .dsh-css-project-columns,
387
+ .dsh-css-project-row {
388
+ display: grid;
389
+ grid-template-columns: minmax(0, 1fr) 100px 80px;
390
+ align-items: center;
391
+ gap: 16px;
392
+ }
393
+
394
+ .dsh-css-project-columns {
395
+ min-height: 32px;
396
+ border-bottom: 1px solid var(--dsw-alias-border-l3);
397
+ color: var(--dsw-alias-label-tertiary);
398
+ font-size: 12px;
399
+
400
+ & button { border: 0; padding: 0; color: inherit; background: transparent; }
401
+ }
402
+
403
+ .dsh-css-project-row {
404
+ position: relative;
405
+ min-height: 70px;
406
+ border-bottom: 1px solid var(--dsw-alias-border-l3);
407
+
408
+ &::before {
409
+ content: '';
410
+ position: absolute;
411
+ inset: 0 -12px;
412
+ border-radius: 12px;
413
+ pointer-events: none;
414
+ }
415
+
416
+ &:is(:hover, :focus-within, .is-open)::before { background: var(--dsw-alias-interactive-bg-hover); }
417
+ & > * { position: relative; }
418
+ &:is(:hover, :focus-within, .is-open) .dsh-css-project-chevron { opacity: 1; }
419
+ }
420
+
421
+ .dsh-css-project-name-cell { min-width: 0; }
422
+
423
+ .dsh-css-project-name {
424
+ display: flex;
425
+ align-items: center;
426
+ gap: 20px;
427
+ width: 100%;
428
+ min-height: 70px;
429
+ padding: 8px;
430
+ border: 0;
431
+ background: transparent;
432
+ color: inherit;
433
+ text-align: left;
434
+
435
+ & svg { flex: none; }
436
+ & span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
437
+ }
438
+
439
+ .dsh-css-project-chevron { opacity: 0; }
440
+ .dsh-css-project-time { color: var(--dsw-alias-label-secondary); white-space: nowrap; }
441
+ .dsh-css-project-actions { display: flex; gap: 4px; }
442
+
443
+ .dsh-css-icon-button {
444
+ display: inline-flex;
445
+ align-items: center;
446
+ justify-content: center;
447
+ flex: none;
448
+ width: 32px;
449
+ height: 32px;
450
+ padding: 0;
451
+ border: 0;
452
+ border-radius: 8px;
453
+ background: transparent;
454
+ color: var(--dsw-alias-label-secondary);
455
+
456
+ &:is(:hover, [aria-expanded='true']) { background: var(--dsw-alias-interactive-bg-hover); }
457
+ }
458
+
459
+ .dsh-css-projects-page button:focus-visible,
460
+ .dsh-css-project-dialog-actions button:focus-visible {
461
+ outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-label-secondary));
462
+ outline-offset: 2px;
463
+ }
464
+
465
+ .dsh-css-project-empty { padding: 60px 0; text-align: center; color: var(--dsw-alias-label-tertiary); }
466
+ .dsh-css-project-error { color: var(--dsw-alias-state-error-primary, #dc6767); overflow-wrap: anywhere; }
467
+ .dsh-css-project-notice { color: var(--dsw-alias-label-secondary); }
468
+ .dsh-css-project-field { display: flex; flex-direction: column; gap: 10px; }
469
+ .dsh-css-project-field input {
470
+ padding: 10px 12px;
471
+ border: 1px solid var(--dsw-alias-border-l3);
472
+ border-radius: 8px;
473
+ background: var(--dsw-alias-bg-base);
474
+ color: var(--dsw-alias-label-primary);
475
+ font: inherit;
476
+ }
477
+ .dsh-css-project-path { color: var(--dsw-alias-label-tertiary); font-size: 12px; overflow-wrap: anywhere; }
478
+
479
+ /* 项目弹窗使用 Codex 的深灰底、圆角和按钮配色。 */
480
+ .dsh-css-project-remove-dialog,
481
+ .dsh-css-project-edit-dialog {
482
+ gap: 12px;
483
+ padding-bottom: 20px;
484
+ border-radius: 22px;
485
+ box-shadow: none;
486
+
487
+ & > :first-child > :first-child { padding: 20px 14px 4px 20px; }
488
+ & h2 { font-size: 20px; line-height: 26px; font-weight: 600; overflow-wrap: anywhere; }
489
+ & > :last-child { padding: 0 20px; }
490
+ & .dsh-css-project-dialog-actions { gap: 12px; }
491
+ & .dsh-css-project-dialog-actions button { min-height: 32px; padding: 5px 16px; border-radius: 10px; }
492
+ & .dsh-css-project-secondary { border-color: transparent; }
493
+ }
494
+
495
+ .dsh-css-project-remove-dialog {
496
+ width: min(420px, 100%);
497
+ & .dsh-css-project-remove-content > p { padding: 0 20px; }
498
+ & .dsh-css-project-remove-content > :last-child { margin-top: 0; padding: 0 20px; }
499
+ }
500
+
501
+ .dsh-css-project-edit-dialog {
502
+ width: min(520px, 100%);
503
+ gap: 20px;
504
+
505
+ & .dsh-css-project-edit-content > :first-child { padding-bottom: 12px; }
506
+ & .dsh-css-project-edit-content > :last-child { margin-top: 0; padding: 0 20px; }
507
+ & .dsh-css-project-field { gap: 12px; }
508
+ & .dsh-css-project-name-input {
509
+ display: flex;
510
+ align-items: center;
511
+ gap: 12px;
512
+ padding: 0 12px;
513
+ border: 1px solid var(--dsw-alias-border-l3);
514
+ border-radius: 12px;
515
+ }
516
+ & .dsh-css-project-name-input:focus-within { border-color: var(--dsw-alias-label-secondary); }
517
+ & .dsh-css-project-name-input input {
518
+ min-width: 0;
519
+ width: 100%;
520
+ height: 38px;
521
+ padding: 0;
522
+ border: 0;
523
+ outline: 0;
524
+ background: transparent;
525
+ }
526
+ & svg { flex: none; }
527
+ & .dsh-css-project-source { display: flex; flex-direction: column; gap: 8px; }
528
+ & .dsh-css-project-source-label { font-size: 13px; font-weight: 600; }
529
+ & .dsh-css-project-path {
530
+ box-sizing: border-box;
531
+ display: flex;
532
+ align-items: center;
533
+ gap: 10px;
534
+ min-height: 48px;
535
+ padding: 10px 12px;
536
+ border: 1px solid var(--dsw-alias-border-l3);
537
+ border-radius: 10px;
538
+ color: var(--dsw-alias-label-primary);
539
+ font-size: 13px;
540
+ }
541
+ }
542
+
543
+ body[data-ds-dark-theme] :is(.dsh-css-project-remove-dialog, .dsh-css-project-edit-dialog) {
544
+ background: rgb(55, 56, 51);
545
+
546
+ & h2 { color: rgb(238, 238, 233); }
547
+ & .dsh-css-project-remove-content > p,
548
+ & .dsh-css-project-secondary { color: rgb(142, 143, 137); }
549
+ & .dsh-css-project-secondary:hover:not(:disabled) { background: rgb(255 255 255 / 5%); }
550
+ & .dsh-css-project-remove { color: rgb(240, 100, 96); background: rgb(76, 61, 56); }
551
+ & .dsh-css-project-remove:hover:not(:disabled) { background: rgb(88, 65, 60); }
552
+ & .dsh-css-project-name-input { background: rgb(58, 59, 53); border-color: rgb(80, 81, 74); }
553
+ & .dsh-css-project-name-input:focus-within { border-color: rgb(148, 149, 138); }
554
+ & .dsh-css-project-path { background: rgb(58, 59, 53); border-color: rgb(70, 71, 64); }
555
+ & .dsh-css-project-path svg { color: rgb(142, 143, 137); }
556
+ }
557
+
558
+ body[data-ds-dark-theme] .dsh-css-project-edit-dialog .dsh-css-project-create {
559
+ background: rgb(248, 248, 243);
560
+ color: rgb(55, 56, 51);
561
+ }
562
+
563
+ body[data-ds-dark-theme] [role='presentation']:has(> :is(.dsh-css-project-remove-dialog, .dsh-css-project-edit-dialog)) {
564
+ --dsw-alias-bg-mask-1: rgb(0 0 0 / 15%);
565
+ }
566
+
567
+ @media (max-width: 760px) {
568
+ .dsh-css-projects-content { padding: 24px 20px; }
569
+ .dsh-css-projects-header { align-items: stretch; flex-direction: column; }
570
+ .dsh-css-project-search { flex: 1; width: auto; }
571
+ .dsh-css-project-columns, .dsh-css-project-row { grid-template-columns: minmax(0, 1fr) 64px 68px; gap: 8px; }
572
+ .dsh-css-project-name { gap: 10px; }
573
+ }