Merge pull request #88 from omdsh-dev/feat/title-bar-compat
feat: 侧边栏新增位置兼容模式(v0.12.2)
This commit is contained in:
@@ -34,10 +34,11 @@
|
||||
|
||||
## 🆕 最近更新
|
||||
|
||||
<small>v0.12.1</small>
|
||||
<small>v0.12.2</small>
|
||||
|
||||
| 功能 | 说明 | 截图 |
|
||||
|---|---|---|
|
||||
| 📐 位置兼容模式 | 设置页新增「位置兼容模式」开关:为 Windows 右上角原生标题栏预留顶部空间,侧边栏按钮与内容整体下移(默认关闭);下移距离可在齿轮弹窗中自定义(0–120px) | |
|
||||
| 🔌 服务化基座 | 完整类型导出 + `version`/`features` 能力探测、状态订阅(`getSnapshot`/`subscribeState`)、tab 角标、`onOpen`/`onActivate`/`onClose` 生命周期回调、`updateTab`/`activateTab`/`openFile`、定向打开、`meta` 跨刷新持久化、插件自有设置(`pluginToggles`/`render`)、外链点击目标认领(`urlTarget`) | <a href="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0"><img width="800" alt="服务化基座截图" src="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0" /></a> |
|
||||
| ➕ 添加插件 | 设置页「推荐插件目录」+ 一键复制安装命令;内置 Office 预览迁至推荐插件 | <a href="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e"><img width="800" alt="添加插件截图" src="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e" /></a> |
|
||||
| 🖱️ 标签页滚轮 | 标签页栏支持鼠标滚轮横向滚动 | |
|
||||
@@ -66,10 +67,10 @@ irm https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/
|
||||
|
||||
```sh
|
||||
# macOS / Linux
|
||||
curl -fsSL https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.sh | bash -s 0.12.1 --restart
|
||||
curl -fsSL https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.sh | bash -s 0.12.2 --restart
|
||||
|
||||
# Windows PowerShell
|
||||
& ([scriptblock]::Create((irm 'https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.ps1'))) -Version 0.12.1 -Restart
|
||||
& ([scriptblock]::Create((irm 'https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.ps1'))) -Version 0.12.2 -Restart
|
||||
```
|
||||
|
||||
不确定的话,可先加 `--dry-run`(PowerShell 用 `-DryRun`)预览步骤再执行。
|
||||
@@ -95,7 +96,7 @@ minimumReleaseAgeExclude:
|
||||
- dsh-better-sidebar
|
||||
EOF
|
||||
|
||||
# ③ 安装并自动挂载(不带 @版本 = npm 的 latest;固定版本写 dsh-better-sidebar@0.12.1)
|
||||
# ③ 安装并自动挂载(不带 @版本 = npm 的 latest;固定版本写 dsh-better-sidebar@0.12.2)
|
||||
npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-better-sidebar
|
||||
```
|
||||
|
||||
@@ -126,7 +127,7 @@ npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-better-sideba
|
||||
3. 执行 `dsh plugin --profile web add dsh-better-sidebar`:登记依赖 → 识别包内 `dsh.bundle.patch` → 自动注册进 `dsh.profile.bundles` 挂载;
|
||||
4. 清理旧版残留的手动挂载行,避免「双挂载」(页面出现两个侧边栏)。
|
||||
|
||||
`curl | bash` / `irm | iex` 会执行远程代码——脚本已随仓库开源(`scripts/install.sh` / `scripts/install.ps1`),可先下载审阅。插件以 npm 包 `dsh-better-sidebar@0.12.1` 发布,通过 `dsh.bundle.patch`(随包的 `cordis.patch.yml`)由官方 CLI 自动挂载,**不修改 DSH 源码**。
|
||||
`curl | bash` / `irm | iex` 会执行远程代码——脚本已随仓库开源(`scripts/install.sh` / `scripts/install.ps1`),可先下载审阅。插件以 npm 包 `dsh-better-sidebar@0.12.2` 发布,通过 `dsh.bundle.patch`(随包的 `cordis.patch.yml`)由官方 CLI 自动挂载,**不修改 DSH 源码**。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -172,7 +173,7 @@ dsh plugin --profile web add dsh-better-sidebar
|
||||
5. 硬刷新浏览器(Cmd/Ctrl+Shift+R)即可看到效果(client 改动无需重启 DSH;host 半改动才需重启)
|
||||
```
|
||||
|
||||
更新:`git pull && pnpm install && pnpm build` → 硬刷新浏览器即可(client 改动热加载生效,无需重启 DSH;host 半改动才需重启)。切回 npm 通道时,把依赖改回 `"dsh-better-sidebar": "^0.12.1"` 再 `pnpm install`。
|
||||
更新:`git pull && pnpm install && pnpm build` → 硬刷新浏览器即可(client 改动热加载生效,无需重启 DSH;host 半改动才需重启)。切回 npm 通道时,把依赖改回 `"dsh-better-sidebar": "^0.12.2"` 再 `pnpm install`。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+8
-7
@@ -34,10 +34,11 @@
|
||||
|
||||
## 🆕 Recent Updates
|
||||
|
||||
<small>v0.12.1</small>
|
||||
<small>v0.12.2</small>
|
||||
|
||||
| Feature | Description | Screenshot |
|
||||
|---|---|---|
|
||||
| 📐 Position compat mode | New "Position compatibility mode" setting: reserves top space for the native Windows title bar (top-right) so the sidebar buttons and content sit below it (off by default); the shift distance is customizable in the gear popup (0–120px) | |
|
||||
| 🔌 Service API base | Complete type exports + `version`/`features` capability detection, state subscription (`getSnapshot`/`subscribeState`), tab `badge`, `onOpen`/`onActivate`/`onClose` lifecycle callbacks, `updateTab`/`activateTab`/`openFile`, targeted open, `meta` persisted across reloads, plugin-owned settings (`pluginToggles`/`render`), external-link claim (`urlTarget`) | <a href="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0"><img width="480" alt="Service API base screenshot" src="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0" /></a> |
|
||||
| ➕ Add Plugins | Recommended plugin catalog in settings + one-click copy install command; built-in Office preview moved to the recommended plugin | <a href="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e"><img width="480" alt="Add Plugins screenshot" src="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e" /></a> |
|
||||
| 🖱️ Tab-bar scroll | Mouse-wheel horizontal scrolling on the tab bar | |
|
||||
@@ -66,10 +67,10 @@ Then **hard-refresh the browser** (Cmd/Ctrl+Shift+R) to see the sidebar (DSH hot
|
||||
|
||||
```sh
|
||||
# macOS / Linux
|
||||
curl -fsSL https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.sh | bash -s 0.12.1 --restart
|
||||
curl -fsSL https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.sh | bash -s 0.12.2 --restart
|
||||
|
||||
# Windows PowerShell
|
||||
& ([scriptblock]::Create((irm 'https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.ps1'))) -Version 0.12.1 -Restart
|
||||
& ([scriptblock]::Create((irm 'https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.ps1'))) -Version 0.12.2 -Restart
|
||||
```
|
||||
|
||||
Not sure? Add `--dry-run` (`-DryRun` in PowerShell) to preview before running.
|
||||
@@ -95,7 +96,7 @@ minimumReleaseAgeExclude:
|
||||
- dsh-better-sidebar
|
||||
EOF
|
||||
|
||||
# ③ Install and auto-mount (no @version = npm's latest; pin with dsh-better-sidebar@0.12.1)
|
||||
# ③ Install and auto-mount (no @version = npm's latest; pin with dsh-better-sidebar@0.12.2)
|
||||
npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-better-sidebar
|
||||
```
|
||||
|
||||
@@ -126,7 +127,7 @@ The one-click script does four things, all idempotent (safe to re-run):
|
||||
3. Runs `dsh plugin --profile web add dsh-better-sidebar`: registers the dependency → detects `dsh.bundle.patch` → auto-appends the plugin to `dsh.profile.bundles`;
|
||||
4. Removes any leftover hand-written mount line to avoid double-mounting (two sidebars on the page).
|
||||
|
||||
`curl | bash` / `irm | iex` executes remote code — the scripts are open source in the repo (`scripts/install.sh` / `scripts/install.ps1`); download and review them first if you prefer. The plugin ships as npm package `dsh-better-sidebar@0.12.1` and mounts via `dsh.bundle.patch` (the shipped `cordis.patch.yml`), so the DSH source is never modified.
|
||||
`curl | bash` / `irm | iex` executes remote code — the scripts are open source in the repo (`scripts/install.sh` / `scripts/install.ps1`); download and review them first if you prefer. The plugin ships as npm package `dsh-better-sidebar@0.12.2` and mounts via `dsh.bundle.patch` (the shipped `cordis.patch.yml`), so the DSH source is never modified.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -137,7 +138,7 @@ The one-click script does four things, all idempotent (safe to re-run):
|
||||
dsh plugin --profile web add dsh-better-sidebar
|
||||
```
|
||||
|
||||
or re-run the one-click script; or bump the version in `~/.dsh/profiles/web/package.json` (e.g. `"^0.12.1"`) and run `pnpm install`. Then hard-refresh the browser (Cmd/Ctrl+Shift+R) — client changes do not need a DSH restart.
|
||||
or re-run the one-click script; or bump the version in `~/.dsh/profiles/web/package.json` (e.g. `"^0.12.2"`) and run `pnpm install`. Then hard-refresh the browser (Cmd/Ctrl+Shift+R) — client changes do not need a DSH restart.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -172,7 +173,7 @@ To debug local changes or track the dev branch, point the dependency at a local
|
||||
5. Restart DSH and hard-refresh
|
||||
```
|
||||
|
||||
Update: `git pull && pnpm install && pnpm build` → just hard-refresh the browser (client changes hot-reload; only host-half changes need a DSH restart). To switch back to the npm channel, restore `"dsh-better-sidebar": "^0.12.1"` and re-run `pnpm install`.
|
||||
Update: `git pull && pnpm install && pnpm build` → just hard-refresh the browser (client changes hot-reload; only host-half changes need a DSH restart). To switch back to the npm channel, restore `"dsh-better-sidebar": "^0.12.2"` and re-run `pnpm install`.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "dsh-external/dsh-better-sidebar",
|
||||
"version": "0.12.1",
|
||||
"version": "0.12.2",
|
||||
"main": "./lib/index.js",
|
||||
"description": "VSCode 风格右侧侧边栏:文件资源管理器 / 编辑器 / 终端 / Git / 浏览器,按会话隔离。暴露服务供其他插件注册侧边栏页面和文件预览器",
|
||||
"engines": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dsh-better-sidebar",
|
||||
"version": "0.12.1",
|
||||
"version": "0.12.2",
|
||||
"description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes a service for other plugins to register sidebar tabs and file viewers.",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
|
||||
@@ -126,7 +126,8 @@
|
||||
}
|
||||
|
||||
.cardMain:focus-visible,
|
||||
.cardGear:focus-visible {
|
||||
.cardGear:focus-visible,
|
||||
.rowGear:focus-visible {
|
||||
outline: 2px solid var(--dsw-alias-border-l4);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@@ -266,6 +267,29 @@
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
/* The gear icon button on a general settings ROW (the position-compat
|
||||
row's "customize the strip height" entry): the same 16px icon-button
|
||||
recipe as the card gear, sitting in the row's control area. */
|
||||
.rowGear {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
cursor: pointer;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.rowGear:hover {
|
||||
color: var(--dsw-alias-label-primary);
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
/* Settings-row recipe (the DSH General section rows): title/desc left,
|
||||
control right, hairline separators (stripped on the last row). */
|
||||
.row {
|
||||
|
||||
@@ -52,6 +52,8 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import {
|
||||
clampWidthPercent,
|
||||
TITLE_BAR_STRIP_MAX,
|
||||
TITLE_BAR_STRIP_MIN,
|
||||
WIDTH_PERCENT_MAX,
|
||||
WIDTH_PERCENT_MIN,
|
||||
type SidebarPrefs,
|
||||
@@ -380,6 +382,8 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
// Which feature's secondary settings popup is open (null = closed).
|
||||
const [settingsFor, setSettingsFor] = useState<TabDescriptor | FileViewerDescriptor | null>(null)
|
||||
// Whether the position-compat strip popup (the gear on the 常规 row) is open.
|
||||
const [stripSettingsOpen, setStripSettingsOpen] = useState(false)
|
||||
// Whether the "add plugin" modal (a dashed card at the end of the
|
||||
// 侧边栏内容 / 文件预览 grids) is open, and for which extension point
|
||||
// (null = closed).
|
||||
@@ -660,6 +664,36 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
||||
onChange={(next) => { applyPref({ interceptOpenPath: next }) }}
|
||||
/>
|
||||
</div>
|
||||
<div className={css.row}>
|
||||
<span className={css.rowText}>
|
||||
<span className={css.title}>{t('settingsTitleBarTitle')}</span>
|
||||
<span className={css.desc}>{t('settingsTitleBarDesc')}</span>
|
||||
</span>
|
||||
<span className={css.control}>
|
||||
{/*
|
||||
The position-compat row's gear (same popup pattern as the
|
||||
feature cards): opens a Modal with the strip-height number row.
|
||||
Hidden while the mode is off — its related setting is dormant
|
||||
then (the feature-card convention).
|
||||
*/}
|
||||
{prefs.titleBarCompat && (
|
||||
<button
|
||||
type="button"
|
||||
className={css.rowGear}
|
||||
aria-label={`${t('settingsTitleBarTitle')} ${t('settingsPopup')}`}
|
||||
title={t('settingsPopup')}
|
||||
onClick={() => { setStripSettingsOpen(true) }}
|
||||
>
|
||||
<IconSettingsOutline16 size={14} />
|
||||
</button>
|
||||
)}
|
||||
<Switch
|
||||
label={t('settingsTitleBarTitle')}
|
||||
checked={prefs.titleBarCompat}
|
||||
onChange={(next) => { applyPref({ titleBarCompat: next }) }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 侧边栏内容: one small card per registered tab type in a responsive
|
||||
@@ -783,6 +817,41 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* The position-compat strip popup (opened by the gear on the 常规
|
||||
row): one number row for the reserved strip height in px. Same
|
||||
Modal chrome and row machinery as the feature popups — mounted
|
||||
only while open (the Modal SSR rule above). */}
|
||||
{stripSettingsOpen && (
|
||||
<Modal
|
||||
open
|
||||
onClose={() => { setStripSettingsOpen(false) }}
|
||||
title={t('settingsTitleBarTitle')}
|
||||
description={t('settingsPopupDesc', { feature: t('settingsTitleBarTitle') })}
|
||||
closeLabel={t('close')}
|
||||
className={css.popupDialog}
|
||||
footer={(
|
||||
<button type="button" className={css.done} onClick={() => { setStripSettingsOpen(false) }}>
|
||||
{t('settingsDone')}
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
<FeatureSettingsRows
|
||||
toggles={[{
|
||||
key: 'titleBarStripPx',
|
||||
type: 'number',
|
||||
title: () => t('settingsTitleBarStripTitle'),
|
||||
desc: () => t('settingsTitleBarStripDesc'),
|
||||
min: TITLE_BAR_STRIP_MIN,
|
||||
max: TITLE_BAR_STRIP_MAX,
|
||||
unit: 'px',
|
||||
}]}
|
||||
prefs={prefs}
|
||||
onToggle={onToggleSetting}
|
||||
onCommit={onCommitSetting}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* The "add plugin" modal (opened by the dashed cards above): declares
|
||||
the extension point of the clicked kind, opens the GitHub topic,
|
||||
and lists the matching recommended plugin catalog with per-entry
|
||||
|
||||
@@ -167,6 +167,33 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
return () => { document.body.removeAttribute('data-dsh-sidebar-collapsed') }
|
||||
}, [collapsed])
|
||||
|
||||
// Position compatibility mode (titleBarCompat pref): Windows frameless
|
||||
// windows draw the native title bar (minimize/maximize/close) at the
|
||||
// window's top-right corner, OVER the web content. When the user enables
|
||||
// the pref, the body attribute lets sidebar.module.css drop the toggle
|
||||
// cluster below the strip and push the right panel's content below it.
|
||||
// The strip height is user-tunable (titleBarStripPx) and rides a CSS
|
||||
// variable so the rules stay declarative. The attribute rides the
|
||||
// snapshot's prefs, so flipping the setting re-renders and re-applies
|
||||
// immediately; the cleanup removes both on unmount/boundary swap so a
|
||||
// crashed sidebar never leaves them behind.
|
||||
const titleBarCompat = snapshot.prefs.titleBarCompat
|
||||
const titleBarStrip = snapshot.prefs.titleBarStripPx
|
||||
useEffect(() => {
|
||||
const root = document.documentElement
|
||||
if (titleBarCompat) {
|
||||
document.body.setAttribute('data-dsh-title-bar-compat', '')
|
||||
root.style.setProperty('--dsh-title-bar-strip', `${titleBarStrip}px`)
|
||||
} else {
|
||||
document.body.removeAttribute('data-dsh-title-bar-compat')
|
||||
root.style.removeProperty('--dsh-title-bar-strip')
|
||||
}
|
||||
return () => {
|
||||
document.body.removeAttribute('data-dsh-title-bar-compat')
|
||||
root.style.removeProperty('--dsh-title-bar-strip')
|
||||
}
|
||||
}, [titleBarCompat, titleBarStrip])
|
||||
|
||||
/**
|
||||
* Bottom-panel merge on narrow viewports: whenever a session is current
|
||||
* while narrow (mount, session switch, or a desktop→narrow transition),
|
||||
|
||||
@@ -116,6 +116,10 @@ export const zh = {
|
||||
settingsWidthSuffix: '%',
|
||||
settingsOpenPathTitle: '聊天区文件在侧边栏打开',
|
||||
settingsOpenPathDesc: '在聊天里点击文件链接(工具行、产物列表、文件提及)时,在侧边栏编辑器中打开,不再调用系统默认应用',
|
||||
settingsTitleBarTitle: '位置兼容模式',
|
||||
settingsTitleBarDesc: '为 Windows 右上角的原生标题栏预留空间:侧边栏按钮与侧边栏内容整体下移,避免被标题栏遮挡',
|
||||
settingsTitleBarStripTitle: '下移距离',
|
||||
settingsTitleBarStripDesc: '标题栏条带高度:侧边栏按钮与内容下移的像素数(0–120,默认 40)',
|
||||
settingsSaveFailed: '保存失败',
|
||||
settingsConflict: '设置已被其他窗口修改,请重试',
|
||||
binaryNoPreview: '此文件类型不支持预览',
|
||||
@@ -333,6 +337,10 @@ export const en: Record<keyof typeof zh, string> = {
|
||||
settingsWidthSuffix: '%',
|
||||
settingsOpenPathTitle: 'Open chat files in the sidebar',
|
||||
settingsOpenPathDesc: 'Open file links in the chat (tool rows, produced files, mentions) in the sidebar editor instead of the system default app',
|
||||
settingsTitleBarTitle: 'Position compatibility mode',
|
||||
settingsTitleBarDesc: 'Reserve space for the native Windows title bar at the top-right so the sidebar buttons and content sit below it instead of underneath',
|
||||
settingsTitleBarStripTitle: 'Shift distance',
|
||||
settingsTitleBarStripDesc: 'Title-bar strip height: how far the sidebar buttons and content move down in px (0–120, default 40)',
|
||||
settingsSaveFailed: 'Failed to save',
|
||||
settingsConflict: 'The setting changed in another window — please retry',
|
||||
binaryNoPreview: 'This file type cannot be previewed',
|
||||
|
||||
+8
-1
@@ -11,12 +11,13 @@
|
||||
import type { api } from './api.ts'
|
||||
import {
|
||||
clampTerminalFontSize,
|
||||
clampTitleBarStrip,
|
||||
clampWidthPercent,
|
||||
SIDEBAR_PREFS_DEFAULTS,
|
||||
type SidebarPrefs,
|
||||
} from '../prefs-shared.ts'
|
||||
|
||||
export { SIDEBAR_PREFS_DEFAULTS, clampTerminalFontSize, clampWidthPercent }
|
||||
export { SIDEBAR_PREFS_DEFAULTS, clampTerminalFontSize, clampTitleBarStrip, clampWidthPercent }
|
||||
export type { SidebarPrefs }
|
||||
|
||||
/** The settings wire face the preferences need (a subset of the plugin api). */
|
||||
@@ -59,6 +60,12 @@ export function parsePrefs(value: unknown): SidebarPrefs {
|
||||
interceptOpenPath: typeof record.interceptOpenPath === 'boolean'
|
||||
? record.interceptOpenPath
|
||||
: SIDEBAR_PREFS_DEFAULTS.interceptOpenPath,
|
||||
titleBarCompat: typeof record.titleBarCompat === 'boolean'
|
||||
? record.titleBarCompat
|
||||
: SIDEBAR_PREFS_DEFAULTS.titleBarCompat,
|
||||
titleBarStripPx: typeof record.titleBarStripPx === 'number' && Number.isFinite(record.titleBarStripPx)
|
||||
? clampTitleBarStrip(record.titleBarStripPx)
|
||||
: SIDEBAR_PREFS_DEFAULTS.titleBarStripPx,
|
||||
htmlViewerNoSandbox: typeof record.htmlViewerNoSandbox === 'boolean'
|
||||
? record.htmlViewerNoSandbox
|
||||
: SIDEBAR_PREFS_DEFAULTS.htmlViewerNoSandbox,
|
||||
|
||||
@@ -420,7 +420,7 @@ export function matchUrlTarget(tabs: readonly TabDescriptor[], url: URL): TabDes
|
||||
* The plugin version this service instance reports. Keep in lockstep with
|
||||
* `package.json`'s version — `tests/service.spec.ts` asserts the pair.
|
||||
*/
|
||||
export const SIDEBAR_SERVICE_VERSION = '0.12.1'
|
||||
export const SIDEBAR_SERVICE_VERSION = '0.12.2'
|
||||
|
||||
/**
|
||||
* Monotonic capability list consumers use to gate new API usage (features
|
||||
|
||||
@@ -209,6 +209,29 @@
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
/* ── Position compatibility mode (Windows native title bar) ────────────── */
|
||||
|
||||
/* Windows frameless / hidden-title-bar windows draw the native caption
|
||||
strip (minimize / maximize / close) at the window's top-right corner,
|
||||
OVER the web content. When the user enables the "position compatibility"
|
||||
pref, the sidebar yields the strip: the toggle cluster drops below it and
|
||||
the right panel's content starts below it — the panel's surface still
|
||||
spans the full height, so the caption buttons float over empty sidebar
|
||||
fill (the VSCode hidden-title-bar look). The strip height is user-tunable
|
||||
(`--dsh-title-bar-strip`, written by the Sidebar shell from
|
||||
`titleBarStripPx`; 40px fallback). The bottom panel is unaffected: it
|
||||
sits at the window's bottom, never under the top strip. */
|
||||
:global(body[data-dsh-title-bar-compat]) .toggleCluster {
|
||||
top: calc(var(--dsh-title-bar-strip, 40px) + 3px);
|
||||
}
|
||||
|
||||
:global(body[data-dsh-title-bar-compat]) .panel {
|
||||
/* The caption strip is drawn over the panel's own top edge: keep the
|
||||
surface full-height and push the content (the tab strip) below the
|
||||
strip instead. .panelResize is absolutely positioned and unaffected. */
|
||||
padding-top: var(--dsh-title-bar-strip, 40px);
|
||||
}
|
||||
|
||||
/* The shared corner (both panels open): the intersection of the right
|
||||
panel's left edge and the bottom panel's top edge. Horizontal drags
|
||||
resize the width, vertical drags the height — the two panels drag
|
||||
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
TERMINAL_FONT_SIZE_DEFAULT,
|
||||
TERMINAL_FONT_SIZE_MAX,
|
||||
TERMINAL_FONT_SIZE_MIN,
|
||||
TITLE_BAR_STRIP_DEFAULT,
|
||||
TITLE_BAR_STRIP_MAX,
|
||||
TITLE_BAR_STRIP_MIN,
|
||||
WIDTH_PERCENT_DEFAULT,
|
||||
WIDTH_PERCENT_MAX,
|
||||
WIDTH_PERCENT_MIN,
|
||||
@@ -24,6 +27,9 @@ export {
|
||||
TERMINAL_FONT_SIZE_DEFAULT,
|
||||
TERMINAL_FONT_SIZE_MAX,
|
||||
TERMINAL_FONT_SIZE_MIN,
|
||||
TITLE_BAR_STRIP_DEFAULT,
|
||||
TITLE_BAR_STRIP_MAX,
|
||||
TITLE_BAR_STRIP_MIN,
|
||||
WIDTH_PERCENT_DEFAULT,
|
||||
WIDTH_PERCENT_MAX,
|
||||
WIDTH_PERCENT_MIN,
|
||||
@@ -91,6 +97,8 @@ export const PrefsSchema: z<SidebarPrefs> = z.object({
|
||||
terminalFontFamily: z.string().default(''),
|
||||
terminalFontSize: z.number().step(1).min(TERMINAL_FONT_SIZE_MIN).max(TERMINAL_FONT_SIZE_MAX).default(TERMINAL_FONT_SIZE_DEFAULT),
|
||||
interceptOpenPath: z.boolean().default(true),
|
||||
titleBarCompat: z.boolean().default(false),
|
||||
titleBarStripPx: z.number().step(1).min(TITLE_BAR_STRIP_MIN).max(TITLE_BAR_STRIP_MAX).default(TITLE_BAR_STRIP_DEFAULT),
|
||||
htmlViewerNoSandbox: z.boolean().default(false),
|
||||
htmlViewerDefaultUnsafe: z.boolean().default(false),
|
||||
browserNoSandbox: z.boolean().default(false),
|
||||
|
||||
@@ -60,6 +60,21 @@ export interface SidebarPrefs {
|
||||
* own enable switch gates it too (both must be on for the takeover).
|
||||
*/
|
||||
interceptOpenPath: boolean
|
||||
/**
|
||||
* Position compatibility mode: reserves space at the top for the native
|
||||
* Windows title bar (drawn at the window's top-right corner over the web
|
||||
* content in frameless/hidden-title-bar windows). When on, the toggle
|
||||
* cluster drops below the strip and the right panel's content starts
|
||||
* below it. Off by default — the sidebar layout is untouched.
|
||||
*/
|
||||
titleBarCompat: boolean
|
||||
/**
|
||||
* The reserved top strip height in px when `titleBarCompat` is on
|
||||
* (0–120, default 40). Drives the `--dsh-title-bar-strip` CSS variable:
|
||||
* the toggle cluster drops `strip + 3px` and the right panel's content
|
||||
* starts `strip` px below its top edge.
|
||||
*/
|
||||
titleBarStripPx: number
|
||||
/**
|
||||
* Whether the HTML previewer drops its sandboxed iframe. Sandbox ON (the
|
||||
* default) renders previewed HTML in an opaque-origin iframe that cannot
|
||||
@@ -148,6 +163,11 @@ export const TERMINAL_FONT_SIZE_MIN = 9
|
||||
export const TERMINAL_FONT_SIZE_MAX = 32
|
||||
export const TERMINAL_FONT_SIZE_DEFAULT = 13
|
||||
|
||||
/** Range contract of {@link SidebarPrefs.titleBarStripPx}. */
|
||||
export const TITLE_BAR_STRIP_MIN = 0
|
||||
export const TITLE_BAR_STRIP_MAX = 120
|
||||
export const TITLE_BAR_STRIP_DEFAULT = 40
|
||||
|
||||
/** Fallback prefs used whenever the settings document is unreachable or malformed. */
|
||||
export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = {
|
||||
openByDefault: true,
|
||||
@@ -159,6 +179,8 @@ export const SIDEBAR_PREFS_DEFAULTS: SidebarPrefs = {
|
||||
terminalFontFamily: '',
|
||||
terminalFontSize: TERMINAL_FONT_SIZE_DEFAULT,
|
||||
interceptOpenPath: true,
|
||||
titleBarCompat: false,
|
||||
titleBarStripPx: TITLE_BAR_STRIP_DEFAULT,
|
||||
htmlViewerNoSandbox: false,
|
||||
htmlViewerDefaultUnsafe: false,
|
||||
browserNoSandbox: false,
|
||||
@@ -179,3 +201,8 @@ export function clampWidthPercent(value: number): number {
|
||||
export function clampTerminalFontSize(value: number): number {
|
||||
return Math.min(TERMINAL_FONT_SIZE_MAX, Math.max(TERMINAL_FONT_SIZE_MIN, Math.round(value)))
|
||||
}
|
||||
|
||||
/** Clamp one title-bar strip height into the contract range (shared by schema and client reads). */
|
||||
export function clampTitleBarStrip(value: number): number {
|
||||
return Math.min(TITLE_BAR_STRIP_MAX, Math.max(TITLE_BAR_STRIP_MIN, Math.round(value)))
|
||||
}
|
||||
|
||||
@@ -54,6 +54,10 @@ describe('dsh-better-sidebar plugin export shape', () => {
|
||||
// and 13px.
|
||||
expect(resolved.terminalFontFamily).toBe('')
|
||||
expect(resolved.terminalFontSize).toBe(13)
|
||||
// The position-compat mode defaults OFF (the normal layout is default),
|
||||
// with the strip defaulting to 40px.
|
||||
expect(resolved.titleBarCompat).toBe(false)
|
||||
expect(resolved.titleBarStripPx).toBe(40)
|
||||
// The enable-switch maps resolve to {} (everything on) for old documents.
|
||||
expect(resolved.tabsEnabled).toEqual({})
|
||||
expect(resolved.viewersEnabled).toEqual({})
|
||||
@@ -63,6 +67,6 @@ describe('dsh-better-sidebar plugin export shape', () => {
|
||||
const overridden = (PrefsSchema as unknown as {
|
||||
(input: Record<string, unknown> | undefined): Record<string, unknown>
|
||||
})({ openByDefault: false, defaultWidthPercent: 45 })
|
||||
expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
|
||||
expect(overridden).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -151,6 +151,28 @@ describe('SideCardSection declarative inventory', () => {
|
||||
const html = renderSection(store, service)
|
||||
expect(html).not.toContain('Feature settings')
|
||||
})
|
||||
|
||||
it('renders the position-compat mode general row: off by default, checked when the pref is on', () => {
|
||||
const { store, service } = mount()
|
||||
let html = renderSection(store, service)
|
||||
// The general row renders its title and description.
|
||||
expect(html).toContain('Position compatibility mode')
|
||||
expect(html).toContain('Reserve space for the native Windows title bar')
|
||||
// Three general rows now: openByDefault + interceptOpenPath checked,
|
||||
// the new titleBarCompat row UNCHECKED (default off) — the checked
|
||||
// checkbox count stays at 2 while the total checkbox count is 3.
|
||||
expect(html.match(/type="checkbox"/g)?.length).toBe(3)
|
||||
expect(html.match(/checked=""/g)?.length).toBe(2)
|
||||
// The row's gear (customize the shift distance) is dormant while the
|
||||
// mode is off — the feature-card convention.
|
||||
expect(html).not.toContain('Position compatibility mode Feature settings')
|
||||
|
||||
// When the pref is on, the new switch is checked and the gear appears.
|
||||
store.setPrefs({ ...store.getPrefs(), titleBarCompat: true })
|
||||
html = renderSection(store, service)
|
||||
expect(html.match(/checked=""/g)?.length).toBe(3)
|
||||
expect(html).toContain('aria-label="Position compatibility mode Feature settings"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('FeatureSettingsRows (the secondary settings popup body)', () => {
|
||||
@@ -229,6 +251,31 @@ describe('FeatureSettingsRows (the secondary settings popup body)', () => {
|
||||
expect(html).toContain('px')
|
||||
expect(html).not.toContain('type="checkbox"')
|
||||
})
|
||||
|
||||
it('renders the title-bar strip row: the pref value, the 0–120 bounds and the px suffix', () => {
|
||||
const html = renderToString(createElement(FeatureSettingsRows, {
|
||||
toggles: [{
|
||||
key: 'titleBarStripPx',
|
||||
type: 'number',
|
||||
title: () => 'Shift distance',
|
||||
desc: () => 'Title-bar strip height in px',
|
||||
min: 0,
|
||||
max: 120,
|
||||
unit: 'px',
|
||||
}],
|
||||
prefs: { ...prefs, titleBarStripPx: 64 },
|
||||
onToggle: () => {},
|
||||
onCommit: () => '64',
|
||||
}))
|
||||
expect(html).toContain('Shift distance')
|
||||
expect(html).toContain('Title-bar strip height in px')
|
||||
expect(html).toContain('type="number"')
|
||||
expect(html).toContain('value="64"')
|
||||
expect(html).toContain('min="0"')
|
||||
expect(html).toContain('max="120"')
|
||||
expect(html).toContain('px')
|
||||
expect(html).not.toContain('type="checkbox"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('mergePluginSetting (v0.12.0, codex review fix)', () => {
|
||||
|
||||
@@ -548,6 +548,8 @@ describe('side card settings routes', () => {
|
||||
terminalFontFamily: '',
|
||||
terminalFontSize: 13,
|
||||
interceptOpenPath: true,
|
||||
titleBarCompat: false,
|
||||
titleBarStripPx: 40,
|
||||
htmlViewerNoSandbox: false,
|
||||
htmlViewerDefaultUnsafe: false,
|
||||
browserNoSandbox: false,
|
||||
|
||||
+33
-4
@@ -1161,6 +1161,8 @@ describe('side card preferences', () => {
|
||||
terminalFontFamily: '',
|
||||
terminalFontSize: 13,
|
||||
interceptOpenPath: true,
|
||||
titleBarCompat: false,
|
||||
titleBarStripPx: 40,
|
||||
htmlViewerNoSandbox: false,
|
||||
htmlViewerDefaultUnsafe: false,
|
||||
browserNoSandbox: false,
|
||||
@@ -1185,6 +1187,8 @@ describe('side card preferences', () => {
|
||||
terminalFontFamily: '',
|
||||
terminalFontSize: 13,
|
||||
interceptOpenPath: true,
|
||||
titleBarCompat: false,
|
||||
titleBarStripPx: 40,
|
||||
htmlViewerNoSandbox: false,
|
||||
htmlViewerDefaultUnsafe: false,
|
||||
browserNoSandbox: false,
|
||||
@@ -1209,6 +1213,8 @@ describe('side card preferences', () => {
|
||||
terminalFontFamily: '',
|
||||
terminalFontSize: 13,
|
||||
interceptOpenPath: true,
|
||||
titleBarCompat: false,
|
||||
titleBarStripPx: 40,
|
||||
htmlViewerNoSandbox: false,
|
||||
htmlViewerDefaultUnsafe: false,
|
||||
browserNoSandbox: false,
|
||||
@@ -1245,6 +1251,29 @@ describe('side card preferences', () => {
|
||||
expect((await loadPrefs(wire({ interceptOpenPath: true }))).interceptOpenPath).toBe(true)
|
||||
})
|
||||
|
||||
it('defaults titleBarCompat to false; only an explicit true turns the position-compat mode on', async () => {
|
||||
// Absent or malformed → off (the normal layout is the default).
|
||||
expect((await loadPrefs(wire({}))).titleBarCompat).toBe(false)
|
||||
expect((await loadPrefs(wire({ titleBarCompat: 'yes' }))).titleBarCompat).toBe(false)
|
||||
expect((await loadPrefs(wire({ titleBarCompat: 1 }))).titleBarCompat).toBe(false)
|
||||
// Explicit booleans survive verbatim.
|
||||
expect((await loadPrefs(wire({ titleBarCompat: false }))).titleBarCompat).toBe(false)
|
||||
expect((await loadPrefs(wire({ titleBarCompat: true }))).titleBarCompat).toBe(true)
|
||||
})
|
||||
|
||||
it('defaults titleBarStripPx to 40 and clamps stored values into the contract range', async () => {
|
||||
// Absent or malformed → 40 (the strip default).
|
||||
expect((await loadPrefs(wire({}))).titleBarStripPx).toBe(40)
|
||||
expect((await loadPrefs(wire({ titleBarStripPx: 'yes' }))).titleBarStripPx).toBe(40)
|
||||
// Out-of-range numbers clamp into 0–120.
|
||||
expect((await loadPrefs(wire({ titleBarStripPx: -5 }))).titleBarStripPx).toBe(0)
|
||||
expect((await loadPrefs(wire({ titleBarStripPx: 200 }))).titleBarStripPx).toBe(120)
|
||||
expect((await loadPrefs(wire({ titleBarStripPx: 47.6 }))).titleBarStripPx).toBe(48)
|
||||
// In-range values survive verbatim.
|
||||
expect((await loadPrefs(wire({ titleBarStripPx: 0 }))).titleBarStripPx).toBe(0)
|
||||
expect((await loadPrefs(wire({ titleBarStripPx: 64 }))).titleBarStripPx).toBe(64)
|
||||
})
|
||||
|
||||
it('defaults the link-takeover protocol flags: http on, https off, master on', async () => {
|
||||
// Absent or malformed → the per-protocol defaults.
|
||||
expect((await loadPrefs(wire({}))).browserInterceptLinks).toBe(true)
|
||||
@@ -1294,9 +1323,9 @@ describe('side card preferences', () => {
|
||||
const store = createSidebarStore()
|
||||
// Node environment: no window → the width falls back to PANEL_DEFAULT,
|
||||
// while the open flag still follows the preference.
|
||||
store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
|
||||
store.setPrefs({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
|
||||
store.setSession('fresh-session')
|
||||
expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
|
||||
expect(store.getPrefs()).toEqual({ openByDefault: false, defaultWidthPercent: 45, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
|
||||
const snapshot = store.getSnapshot()
|
||||
expect(snapshot.sessionId).toBe('fresh-session')
|
||||
expect(snapshot.state?.panelOpen).toBe(false)
|
||||
@@ -1332,7 +1361,7 @@ describe('side card preferences', () => {
|
||||
|
||||
it('skips the default explorer tab when the explorer type is disabled', () => {
|
||||
const store = createSidebarStore()
|
||||
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: { explorer: false }, viewersEnabled: {}, pluginSettings: {} })
|
||||
store.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: { explorer: false }, viewersEnabled: {}, pluginSettings: {} })
|
||||
store.setSession('no-explorer')
|
||||
const state = store.getSnapshot().state!
|
||||
const tabs = allLeaves(state.splits).flatMap(leaf => leaf.tabs)
|
||||
@@ -1340,7 +1369,7 @@ describe('side card preferences', () => {
|
||||
expect(state.splits.kind).toBe('leaf')
|
||||
// Re-enabling seeds the explorer tab again.
|
||||
const openStore = createSidebarStore()
|
||||
openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
|
||||
openStore.setPrefs({ openByDefault: true, defaultWidthPercent: 30, autoOpenSubagent: true, autoOpenJobs: true, agentTerminalTools: false, bottomPanelAutoTerminal: true, terminalFontFamily: '', terminalFontSize: 13, interceptOpenPath: true, titleBarCompat: false, titleBarStripPx: 40, htmlViewerNoSandbox: false, htmlViewerDefaultUnsafe: false, browserNoSandbox: false, browserInterceptLinks: true, browserInterceptHttp: true, browserInterceptHttps: false, tabsEnabled: {}, viewersEnabled: {}, pluginSettings: {} })
|
||||
openStore.setSession('with-explorer')
|
||||
const openTabs = allLeaves(openStore.getSnapshot().state!.splits).flatMap(leaf => leaf.tabs)
|
||||
expect(openTabs.map(tab => tab.type)).toEqual(['explorer'])
|
||||
|
||||
Reference in New Issue
Block a user