Compare commits

...
Author SHA1 Message Date
Cursor Agentand陈大猫 83d9d0a85c feat: add custom CSS hooks for the selected top tab
Keep the default top-tab colors, and expose .top-tab[data-active] plus
--top-tab-active-bg/fg/border so Appearance custom CSS can recolor the
selected tab.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-10-20 03:02:57 +00:00
9 changed files with 379 additions and 84 deletions

No files matched your search

+18 -2
View File
@@ -413,7 +413,7 @@ export const enCoreMessages: Messages = {
'settings.appearance.themeColor.dark': 'Dark palette',
'settings.appearance.customCss': 'Custom CSS',
'settings.appearance.customCss.desc':
'Add custom CSS to personalize the app appearance. Changes apply immediately.',
'Add custom CSS to personalize the app appearance. Changes apply immediately. Selected top tabs (`.top-tab[data-active="true"]`) read `--top-tab-active-bg`, `--top-tab-active-fg`, and `--top-tab-active-border`. Leave them unset to keep the current look.',
'settings.appearance.customCss.help.ariaLabel': 'Custom CSS help',
'settings.appearance.customCss.help.title': 'Custom CSS guide',
'settings.appearance.customCss.help.body': `Major UI regions expose a \`data-section\` attribute you can target. Changes apply as soon as you edit the CSS.
@@ -440,6 +440,22 @@ Because many styles come from Tailwind utilities, you often need \`!important\`
- \`top-tabs-host-tree-toggle\`
- \`top-tabs-quick-switcher-toggle\`
Selected top tabs use the class \`top-tab\` and \`data-active="true"\`. Set these variables to recolor the selected tab. Leave them unset to keep the current look. They apply without \`!important\`.
- \`--top-tab-active-bg\` — selected fill (defaults to the current tab background)
- \`--top-tab-active-fg\` — selected label color (defaults to the current tab text)
- \`--top-tab-active-border\` — 1px inset outline (defaults to transparent, so there is no border)
The Vaults tab stays flat and does not use the fill.
\`\`\`css
.top-tab[data-active="true"] {
--top-tab-active-bg: #2563eb;
--top-tab-active-fg: #ffffff;
--top-tab-active-border: #1d4ed8;
}
\`\`\`
### Vault
- \`vault-view\`
@@ -533,7 +549,7 @@ Highlight the focused split pane:
\`\`\`
`,
'settings.appearance.customCss.placeholder':
'/* Examples — use !important to beat Tailwind utility specificity */\n\n/* Hide the host-list toggle in the top tab bar */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* Hide the plus button that opens the quick switcher */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* Border around the SFTP / side panel (does not linger after closing) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* Change the whole side panel background, not only the top tabs */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* Style selected SFTP file rows */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* Thicker split dividers */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* Highlight the focused split pane */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* Or use Settings → Terminal → Workspace Focus Indicator → Border on focused pane */',
'/* Examples — use !important to beat Tailwind utility specificity */\n\n/* Selected top tab. Leave these unset to keep the current look. */\n.top-tab[data-active="true"] {\n --top-tab-active-bg: #2563eb;\n --top-tab-active-fg: #ffffff;\n --top-tab-active-border: #1d4ed8;\n}\n\n/* Hide the host-list toggle in the top tab bar */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* Hide the plus button that opens the quick switcher */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* Border around the SFTP / side panel (does not linger after closing) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* Change the whole side panel background, not only the top tabs */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* Style selected SFTP file rows */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* Thicker split dividers */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* Highlight the focused split pane */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* Or use Settings → Terminal → Workspace Focus Indicator → Border on focused pane */',
'settings.appearance.language': 'Language',
'settings.appearance.language.desc': 'Choose the UI language',
'settings.appearance.uiFont': 'Interface Font',
+18 -2
View File
@@ -328,7 +328,7 @@ export const esCoreMessages: Messages = {
'settings.appearance.themeColor.dark': 'Paleta oscura',
'settings.appearance.customCss': 'CSS personalizado',
'settings.appearance.customCss.desc':
'Agrega CSS personalizado para personalizar la apariencia de la aplicación. Los cambios se aplican de inmediato.',
'Agrega CSS personalizado para personalizar la apariencia de la aplicación. Los cambios se aplican de inmediato. Las pestañas superiores seleccionadas (`.top-tab[data-active="true"]`) leen `--top-tab-active-bg`, `--top-tab-active-fg` y `--top-tab-active-border`. Si no las defines, se conserva el aspecto actual.',
'settings.appearance.customCss.help.ariaLabel': 'Ayuda de CSS personalizado',
'settings.appearance.customCss.help.title': 'Guía de CSS personalizado',
'settings.appearance.customCss.help.body': `Las principales regiones de la interfaz exponen un atributo \`data-section\` que puedes usar como destino. Los cambios se aplican en cuanto editas el CSS.
@@ -355,6 +355,22 @@ Como muchos estilos provienen de utilidades de Tailwind, a menudo necesitas \`!i
- \`top-tabs-host-tree-toggle\`
- \`top-tabs-quick-switcher-toggle\`
Las pestañas superiores seleccionadas usan la clase \`top-tab\` y \`data-active="true"\`. Define estas variables para cambiar el color de la pestaña seleccionada. Si no las defines, se conserva el aspecto actual. Se aplican sin \`!important\`.
- \`--top-tab-active-bg\` — relleno seleccionado (por defecto, el fondo actual de la pestaña)
- \`--top-tab-active-fg\` — color del texto seleccionado (por defecto, el texto actual)
- \`--top-tab-active-border\` — contorno interior de 1px (transparente por defecto, así que no hay borde)
La pestaña Vaults permanece plana y no usa el relleno.
\`\`\`css
.top-tab[data-active="true"] {
--top-tab-active-bg: #2563eb;
--top-tab-active-fg: #ffffff;
--top-tab-active-border: #1d4ed8;
}
\`\`\`
### Vault
- \`vault-view\`
@@ -448,7 +464,7 @@ Resalta el panel dividido enfocado:
\`\`\`
`,
'settings.appearance.customCss.placeholder':
'/* Ejemplos: usa !important para superar la especificidad de las utilidades de Tailwind */\n\n/* Oculta el botón de lista de hosts en la barra de pestañas superior */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* Oculta el botón + que abre el selector rápido */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* Borde alrededor del panel SFTP / lateral (no persiste tras cerrarlo) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* Cambia el fondo de todo el panel lateral, no solo las pestañas superiores */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* Estiliza las filas de archivos SFTP seleccionadas */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* Divisores de división más gruesos */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* Resalta el panel dividido enfocado */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* O usa Configuración → Terminal → Indicador de foco del espacio de trabajo → Borde en el panel enfocado */',
'/* Ejemplos: usa !important para superar la especificidad de las utilidades de Tailwind */\n\n/* Pestaña superior seleccionada. Si no defines estas variables, se conserva el aspecto actual. */\n.top-tab[data-active="true"] {\n --top-tab-active-bg: #2563eb;\n --top-tab-active-fg: #ffffff;\n --top-tab-active-border: #1d4ed8;\n}\n\n/* Oculta el botón de lista de hosts en la barra de pestañas superior */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* Oculta el botón + que abre el selector rápido */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* Borde alrededor del panel SFTP / lateral (no persiste tras cerrarlo) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* Cambia el fondo de todo el panel lateral, no solo las pestañas superiores */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* Estiliza las filas de archivos SFTP seleccionadas */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* Divisores de división más gruesos */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* Resalta el panel dividido enfocado */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* O usa Configuración → Terminal → Indicador de foco del espacio de trabajo → Borde en el panel enfocado */',
'settings.appearance.language': 'Idioma',
'settings.appearance.language.desc': 'Elige el idioma de la interfaz',
'settings.appearance.uiFont': 'Fuente de la interfaz',
+18 -2
View File
@@ -397,7 +397,7 @@ export const ruCoreMessages: Messages = {
'settings.appearance.themeColor.dark': 'Палитра тёмной темы',
'settings.appearance.customCss': 'Пользовательский CSS',
'settings.appearance.customCss.desc':
'Добавьте пользовательский CSS, чтобы настроить внешний вид. Изменения применяются сразу.',
'Добавьте пользовательский CSS, чтобы настроить внешний вид. Изменения применяются сразу. Выбранные верхние вкладки (`.top-tab[data-active="true"]`) читают `--top-tab-active-bg`, `--top-tab-active-fg` и `--top-tab-active-border`. Не задавайте их, чтобы сохранить текущий вид.',
'settings.appearance.customCss.help.ariaLabel': 'Справка по пользовательскому CSS',
'settings.appearance.customCss.help.title': 'Руководство по пользовательскому CSS',
'settings.appearance.customCss.help.body': `Основные области интерфейса помечены атрибутом \`data-section\` для CSS-селекторов. Изменения применяются сразу после правки.
@@ -423,6 +423,22 @@ export const ruCoreMessages: Messages = {
- \`top-tabs-host-tree-toggle\`
- \`top-tabs-quick-switcher-toggle\`
У выбранных верхних вкладок есть класс \`top-tab\` и атрибут \`data-active="true"\`. Задайте эти переменные, чтобы перекрасить выбранную вкладку. Не задавайте их, чтобы сохранить текущий вид. \`!important\` не нужен.
- \`--top-tab-active-bg\` — заливка выбранной вкладки (по умолчанию текущий фон вкладки)
- \`--top-tab-active-fg\` — цвет подписи (по умолчанию текущий цвет текста)
- \`--top-tab-active-border\` — внутренняя обводка 1px (по умолчанию прозрачная, то есть без рамки)
Вкладка Vaults остаётся плоской и не использует заливку.
\`\`\`css
.top-tab[data-active="true"] {
--top-tab-active-bg: #2563eb;
--top-tab-active-fg: #ffffff;
--top-tab-active-border: #1d4ed8;
}
\`\`\`
### Хранилище
- \`vault-view\`
@@ -502,7 +518,7 @@ export const ruCoreMessages: Messages = {
\`\`\`
`,
'settings.appearance.customCss.placeholder':
'/* Примеры — используйте !important, чтобы переопределить специфичность утилит Tailwind */\n\n/* Скрыть переключатель списка хостов в верхней панели вкладок */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* Скрыть кнопку плюса, открывающую быстрый переключатель */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* Рамка вокруг боковой панели SFTP (не остаётся после закрытия) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* Изменить фон всей боковой панели, а не только верхних вкладок */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* Настроить выбранные строки SFTP */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* Более заметные разделители сплита */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* Подсветка активной панели сплита */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* Или: Настройки → Терминал → Индикатор фокуса → Рамка вокруг активной панели */',
'/* Примеры — используйте !important, чтобы переопределить специфичность утилит Tailwind */\n\n/* Выбранная верхняя вкладка. Не задавайте переменные, чтобы сохранить текущий вид. */\n.top-tab[data-active="true"] {\n --top-tab-active-bg: #2563eb;\n --top-tab-active-fg: #ffffff;\n --top-tab-active-border: #1d4ed8;\n}\n\n/* Скрыть переключатель списка хостов в верхней панели вкладок */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* Скрыть кнопку плюса, открывающую быстрый переключатель */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* Рамка вокруг боковой панели SFTP (не остаётся после закрытия) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* Изменить фон всей боковой панели, а не только верхних вкладок */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* Настроить выбранные строки SFTP */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* Более заметные разделители сплита */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* Подсветка активной панели сплита */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* Или: Настройки → Терминал → Индикатор фокуса → Рамка вокруг активной панели */',
'settings.appearance.language': 'Язык',
'settings.appearance.language.desc': 'Выберите язык интерфейса',
'settings.appearance.uiFont': 'Шрифт интерфейса',
+18 -2
View File
@@ -417,7 +417,7 @@ export const zhCNCoreMessages: Messages = {
'settings.appearance.themeColor.dark': '深色主题',
'settings.appearance.customCss': '自定义 CSS',
'settings.appearance.customCss.desc':
'使用自定义 CSS 个性化界面,修改会立即生效。',
'使用自定义 CSS 个性化界面,修改会立即生效。选中的顶部标签(`.top-tab[data-active="true"]`)会读取 `--top-tab-active-bg`、`--top-tab-active-fg` 和 `--top-tab-active-border`。不设置则保持当前外观。',
'settings.appearance.customCss.help.ariaLabel': '自定义 CSS 说明',
'settings.appearance.customCss.help.title': '自定义 CSS 说明',
'settings.appearance.customCss.help.body': `主要 UI 区块都暴露了 \`data-section\` 属性,供你用 CSS 定位。编辑后会立即生效。
@@ -444,6 +444,22 @@ export const zhCNCoreMessages: Messages = {
- \`top-tabs-host-tree-toggle\`
- \`top-tabs-quick-switcher-toggle\`
选中的顶部标签带有 \`top-tab\` 类和 \`data-active="true"\`。设置下面的变量即可改变选中标签的颜色;不设置则保持当前外观。这些变量不需要 \`!important\`。
- \`--top-tab-active-bg\` — 选中标签的背景(默认是现在的标签背景)
- \`--top-tab-active-fg\` — 选中标签的文字颜色(默认是现在的文字颜色)
- \`--top-tab-active-border\` — 1px 内描边(默认透明,也就是没有边框)
Vaults 标签保持扁平,不使用这块背景。
\`\`\`css
.top-tab[data-active="true"] {
--top-tab-active-bg: #2563eb;
--top-tab-active-fg: #ffffff;
--top-tab-active-border: #1d4ed8;
}
\`\`\`
### 保险库
- \`vault-view\`
@@ -537,7 +553,7 @@ export const zhCNCoreMessages: Messages = {
\`\`\`
`,
'settings.appearance.customCss.placeholder':
'/* 示例 — 由于 Tailwind 优先级较高,需要使用 !important */\n\n/* 隐藏顶部标签栏里的主机列表开关 */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* 隐藏打开快速切换器的加号按钮 */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* SFTP / 操作侧栏边框(关闭侧栏后不会残留) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* 修改整个操作侧栏背景,而不只是顶部标签 */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* 修改选中的 SFTP 文件行 */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* 加粗分屏分割线 */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* 高亮当前聚焦的分屏 */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* 也可在 设置 → 终端 → 工作区聚焦指示 → 聚焦窗格显示边框 */',
'/* 示例 — 由于 Tailwind 优先级较高,需要使用 !important */\n\n/* 选中的顶部标签。不设置这些变量则保持当前外观。 */\n.top-tab[data-active="true"] {\n --top-tab-active-bg: #2563eb;\n --top-tab-active-fg: #ffffff;\n --top-tab-active-border: #1d4ed8;\n}\n\n/* 隐藏顶部标签栏里的主机列表开关 */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* 隐藏打开快速切换器的加号按钮 */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* SFTP / 操作侧栏边框(关闭侧栏后不会残留) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* 修改整个操作侧栏背景,而不只是顶部标签 */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* 修改选中的 SFTP 文件行 */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* 加粗分屏分割线 */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* 高亮当前聚焦的分屏 */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* 也可在 设置 → 终端 → 工作区聚焦指示 → 聚焦窗格显示边框 */',
'settings.appearance.language': '语言',
'settings.appearance.language.desc': '选择界面语言',
'settings.appearance.uiFont': '界面字体',
+18 -2
View File
@@ -419,7 +419,7 @@ export const zhTWCoreMessages: Messages = {
'settings.appearance.themeColor.dark': '深色主題',
'settings.appearance.customCss': '自訂 CSS',
'settings.appearance.customCss.desc':
'使用自訂 CSS 個人化介面,修改會立即生效。',
'使用自訂 CSS 個人化介面,修改會立即生效。選取的頂部分頁(`.top-tab[data-active="true"]`)會讀取 `--top-tab-active-bg`、`--top-tab-active-fg` 和 `--top-tab-active-border`。不設定則維持目前外觀。',
'settings.appearance.customCss.help.ariaLabel': '自訂 CSS 說明',
'settings.appearance.customCss.help.title': '自訂 CSS 說明',
'settings.appearance.customCss.help.body': `主要 UI 區塊都公開了 \`data-section\` 屬性,供你用 CSS 定位。編輯後會立即生效。
@@ -446,6 +446,22 @@ export const zhTWCoreMessages: Messages = {
- \`top-tabs-host-tree-toggle\`
- \`top-tabs-quick-switcher-toggle\`
選取的頂部分頁帶有 \`top-tab\` 類別和 \`data-active="true"\`。設定下列變數即可改變選取分頁的顏色;不設定則維持目前外觀。這些變數不需要 \`!important\`。
- \`--top-tab-active-bg\` — 選取分頁的背景(預設為目前的分頁背景)
- \`--top-tab-active-fg\` — 選取分頁的文字顏色(預設為目前的文字顏色)
- \`--top-tab-active-border\` — 1px 內描邊(預設透明,也就是沒有邊框)
Vaults 分頁維持扁平,不使用這塊背景。
\`\`\`css
.top-tab[data-active="true"] {
--top-tab-active-bg: #2563eb;
--top-tab-active-fg: #ffffff;
--top-tab-active-border: #1d4ed8;
}
\`\`\`
### 保險庫
- \`vault-view\`
@@ -539,7 +555,7 @@ export const zhTWCoreMessages: Messages = {
\`\`\`
`,
'settings.appearance.customCss.placeholder':
'/* 範例 — 由於 Tailwind 優先順序較高,需要使用 !important */\n\n/* 隱藏頂部分頁列裡的主機列表開關 */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* 隱藏開啟快速切換器的加號按鈕 */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* SFTP / 操作側欄邊框(關閉側欄後不會殘留) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* 修改整個操作側欄背景,而不只是頂部分頁 */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* 修改選取的 SFTP 檔案行 */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* 加粗分割分割線 */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* 標示目前聚焦的分割 */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* 也可在 設定 → 終端 → 工作區聚焦指示 → 聚焦窗格顯示邊框 */',
'/* 範例 — 由於 Tailwind 優先順序較高,需要使用 !important */\n\n/* 選取的頂部分頁。不設定這些變數則維持目前外觀。 */\n.top-tab[data-active="true"] {\n --top-tab-active-bg: #2563eb;\n --top-tab-active-fg: #ffffff;\n --top-tab-active-border: #1d4ed8;\n}\n\n/* 隱藏頂部分頁列裡的主機列表開關 */\n[data-section="top-tabs-host-tree-toggle"] {\n width: 0 !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* 隱藏開啟快速切換器的加號按鈕 */\n[data-section="top-tabs-quick-switcher-toggle"] {\n display: none !important;\n}\n\n/* SFTP / 操作側欄邊框(關閉側欄後不會殘留) */\n[data-section="terminal-side-panel"] {\n border: 2px solid #00c851 !important;\n border-radius: 6px !important;\n}\n\n/* 修改整個操作側欄背景,而不只是頂部分頁 */\n[data-section="terminal-side-panel"],\n[data-section="terminal-side-panel-tabs"],\n[data-section="terminal-side-panel-content"],\n[data-section="terminal-sftp-panel"],\n[data-section="terminal-sftp-pane"],\n[data-section="terminal-sftp-list"],\n[data-section="terminal-sftp-tree"],\n[data-section="terminal-sftp-transfer-queue"] {\n background-color: #1c384a !important;\n}\n\n/* 修改選取的 SFTP 檔案行 */\n[data-section="terminal-sftp-list-row"][data-selected="true"] {\n background-color: #00c851 !important;\n color: #001b10 !important;\n}\n\n/* 加粗分割分割線 */\n[data-section="terminal-split-resizer-bar"] {\n background-color: hsl(var(--primary)) !important;\n transform: scale(2) !important;\n}\n\n/* 標示目前聚焦的分割 */\n[data-section="terminal-split-pane"][data-focused="true"] {\n outline: 2px solid hsl(var(--primary)) !important;\n outline-offset: -2px;\n}\n\n/* 也可在 設定 → 終端 → 工作區聚焦指示 → 聚焦窗格顯示邊框 */',
'settings.appearance.language': '語言',
'settings.appearance.language.desc': '選擇介面語言',
'settings.appearance.uiFont': '介面字型',
+35 -74
View File
@@ -30,6 +30,13 @@ import { handleTabMiddleClickClose, handleTabMiddleMouseDown } from '../../lib/t
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger } from '../ui/context-menu';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
import { SessionTabContextMenuContent } from './SessionTabContextMenuContent';
import {
TOP_TAB_ITEM_CLASS,
clearInactiveTopTabHover,
paintInactiveTopTabHover,
resolveTopTabChromeStyle,
topTabActiveValue,
} from './topTabActiveStyle';
import { renderHostIconGlyph } from '../hostIconRenderer';
import type { PluginViewTab } from '../../application/state/pluginViewTabStore';
import { PluginContributionIcon } from '../plugins/PluginContributionIcon';
@@ -446,31 +453,20 @@ export const RootTopTab: React.FC<RootTopTabProps> = memo(({ tabId, label, icon,
data-tab-id={tabId}
data-tab-type="root"
data-state={isActive ? 'active' : 'inactive'}
data-active={topTabActiveValue(isActive)}
onClick={handleClick}
className={cn(
TOP_TAB_ITEM_CLASS,
"netcatty-tab relative h-7 overflow-hidden text-xs font-semibold cursor-pointer flex items-center app-no-drag transition-[padding,gap] duration-300 ease-out",
compact ? "px-2 gap-0" : "px-3 gap-2",
className,
)}
style={{
backgroundColor: isActive && !suppressActiveBg
? 'var(--top-tabs-active-bg, hsl(var(--background)))'
: 'transparent',
color: isActive
? 'var(--top-tabs-fg, hsl(var(--foreground)))'
: 'var(--top-tabs-muted, hsl(var(--muted-foreground)))',
}}
style={resolveTopTabChromeStyle(isActive, { suppressActiveBg })}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'color-mix(in srgb, var(--top-tabs-active-bg, hsl(var(--background))) 40%, transparent)';
e.currentTarget.style.color = 'var(--top-tabs-fg, hsl(var(--foreground)))';
}
if (!isActive) paintInactiveTopTabHover(e.currentTarget);
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = 'var(--top-tabs-muted, hsl(var(--muted-foreground)))';
}
if (!isActive) clearInactiveTopTabHover(e.currentTarget);
}}
>
{shortcutNumber != null ? <TabShortcutNumberBadge number={shortcutNumber} /> : icon}
@@ -529,6 +525,7 @@ export const PluginViewTopTab: React.FC<PluginViewTopTabProps> = memo(({
data-tab-id={tab.id}
data-tab-type="plugin-view"
data-state={isActive ? 'active' : 'inactive'}
data-active={topTabActiveValue(isActive)}
onClick={() => activeTabStore.setActiveTabId(tab.id)}
onMouseDown={handleTabMiddleMouseDown}
onAuxClick={(event) => handleTabMiddleClickClose(event, close)}
@@ -539,6 +536,7 @@ export const PluginViewTopTab: React.FC<PluginViewTopTabProps> = memo(({
onDragLeave={onTabDragLeave}
onDrop={(event) => onTabDrop(event, tab.id)}
className={cn(
TOP_TAB_ITEM_CLASS,
'netcatty-tab relative h-7 min-w-[140px] max-w-[240px] flex-shrink-0 cursor-pointer items-center justify-between gap-2 overflow-hidden rounded-t-md pl-3 pr-2 text-xs font-semibold app-no-drag',
'flex transition-transform duration-150',
isBeingDragged && isDraggingForReorder && 'scale-95 opacity-40',
@@ -546,8 +544,7 @@ export const PluginViewTopTab: React.FC<PluginViewTopTabProps> = memo(({
)}
style={{
...shiftStyle,
backgroundColor: isActive ? 'var(--top-tabs-active-bg, hsl(var(--background)))' : 'transparent',
color: isActive ? 'var(--top-tabs-fg, hsl(var(--foreground)))' : 'var(--top-tabs-muted, hsl(var(--muted-foreground)))',
...resolveTopTabChromeStyle(isActive),
}}
>
{showDropIndicatorBefore && isDraggingForReorder && <div className="absolute -left-0.5 bottom-1 top-1 w-0.5 rounded-full bg-primary" />}
@@ -649,6 +646,7 @@ export const EditorTopTab: React.FC<EditorTopTabProps> = memo(({
data-tab-id={tabId}
data-tab-type="editor"
data-state={isActive ? 'active' : 'inactive'}
data-active={topTabActiveValue(isActive)}
onClick={handleClick}
onMouseDown={handleTabMiddleMouseDown}
onAuxClick={(e) => handleTabMiddleClickClose(e, () => onRequestCloseEditorTab(editorTab.id))}
@@ -659,6 +657,7 @@ export const EditorTopTab: React.FC<EditorTopTabProps> = memo(({
onDragLeave={onTabDragLeave}
onDrop={(e) => onTabDrop(e, tabId)}
className={cn(
TOP_TAB_ITEM_CLASS,
"netcatty-tab relative h-7 pl-3 pr-2 min-w-[140px] max-w-[240px] rounded-t-md overflow-hidden text-xs font-semibold cursor-pointer flex items-center justify-between gap-2 app-no-drag flex-shrink-0",
"transition-transform duration-150",
isBeingDragged && isDraggingForReorder ? "opacity-40 scale-95" : "",
@@ -666,24 +665,13 @@ export const EditorTopTab: React.FC<EditorTopTabProps> = memo(({
)}
style={{
...shiftStyle,
backgroundColor: isActive
? 'var(--top-tabs-active-bg, hsl(var(--background)))'
: 'transparent',
color: isActive
? 'var(--top-tabs-fg, hsl(var(--foreground)))'
: 'var(--top-tabs-muted, hsl(var(--muted-foreground)))',
...resolveTopTabChromeStyle(isActive),
}}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'color-mix(in srgb, var(--top-tabs-active-bg, hsl(var(--background))) 40%, transparent)';
e.currentTarget.style.color = 'var(--top-tabs-fg, hsl(var(--foreground)))';
}
if (!isActive) paintInactiveTopTabHover(e.currentTarget);
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = 'var(--top-tabs-muted, hsl(var(--muted-foreground)))';
}
if (!isActive) clearInactiveTopTabHover(e.currentTarget);
}}
>
{showDropIndicatorBefore && isDraggingForReorder && (
@@ -833,6 +821,7 @@ export const SessionTopTab: React.FC<SessionTopTabProps> = memo(({
data-tab-id={session.id}
data-tab-type="session"
data-state={isActive ? 'active' : 'inactive'}
data-active={topTabActiveValue(isActive)}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
onMouseDown={handleTabMiddleMouseDown}
@@ -844,6 +833,7 @@ export const SessionTopTab: React.FC<SessionTopTabProps> = memo(({
onDragLeave={onTabDragLeave}
onDrop={(e) => onTabDrop(e, session.id)}
className={cn(
TOP_TAB_ITEM_CLASS,
"netcatty-tab relative h-7 pl-3 pr-2 min-w-[140px] max-w-[240px] rounded-t-md overflow-hidden text-xs font-semibold cursor-pointer flex items-center justify-between gap-2 app-no-drag flex-shrink-0",
"transition-transform duration-150",
isBeingDragged && isDraggingForReorder ? "opacity-40 scale-95" : "",
@@ -851,24 +841,13 @@ export const SessionTopTab: React.FC<SessionTopTabProps> = memo(({
)}
style={{
...shiftStyle,
backgroundColor: isActive
? 'var(--top-tabs-active-bg, hsl(var(--background)))'
: 'transparent',
color: isActive
? 'var(--top-tabs-fg, hsl(var(--foreground)))'
: 'var(--top-tabs-muted, hsl(var(--muted-foreground)))',
...resolveTopTabChromeStyle(isActive),
}}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'color-mix(in srgb, var(--top-tabs-active-bg, hsl(var(--background))) 40%, transparent)';
e.currentTarget.style.color = 'var(--top-tabs-fg, hsl(var(--foreground)))';
}
if (!isActive) paintInactiveTopTabHover(e.currentTarget);
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = 'var(--top-tabs-muted, hsl(var(--muted-foreground)))';
}
if (!isActive) clearInactiveTopTabHover(e.currentTarget);
}}
>
{showDropIndicatorBefore && isDraggingForReorder && (
@@ -1045,6 +1024,7 @@ export const WorkspaceTopTab: React.FC<WorkspaceTopTabProps> = memo(({
data-tab-id={workspace.id}
data-tab-type="workspace"
data-state={isActive ? 'active' : 'inactive'}
data-active={topTabActiveValue(isActive)}
data-host-drop-active={isHostDropTarget ? 'true' : 'false'}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
@@ -1057,6 +1037,7 @@ export const WorkspaceTopTab: React.FC<WorkspaceTopTabProps> = memo(({
onDragLeave={onTabDragLeave}
onDrop={(e) => onTabDrop(e, workspace.id)}
className={cn(
TOP_TAB_ITEM_CLASS,
"netcatty-tab relative h-7 pl-3 pr-2 min-w-[150px] max-w-[260px] rounded-t-md overflow-hidden text-xs font-semibold cursor-pointer flex items-center justify-between gap-2 app-no-drag flex-shrink-0",
"transition-transform duration-150",
isBeingDragged && isDraggingForReorder ? "opacity-40 scale-95" : "",
@@ -1065,24 +1046,13 @@ export const WorkspaceTopTab: React.FC<WorkspaceTopTabProps> = memo(({
)}
style={{
...shiftStyle,
backgroundColor: isActive
? 'var(--top-tabs-active-bg, hsl(var(--background)))'
: 'transparent',
color: isActive
? 'var(--top-tabs-fg, hsl(var(--foreground)))'
: 'var(--top-tabs-muted, hsl(var(--muted-foreground)))',
...resolveTopTabChromeStyle(isActive),
}}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'color-mix(in srgb, var(--top-tabs-active-bg, hsl(var(--background))) 40%, transparent)';
e.currentTarget.style.color = 'var(--top-tabs-fg, hsl(var(--foreground)))';
}
if (!isActive) paintInactiveTopTabHover(e.currentTarget);
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = 'var(--top-tabs-muted, hsl(var(--muted-foreground)))';
}
if (!isActive) clearInactiveTopTabHover(e.currentTarget);
}}
>
{showDropIndicatorBefore && isDraggingForReorder && (
@@ -1203,6 +1173,7 @@ export const LogViewTopTab: React.FC<LogViewTopTabProps> = memo(({
data-tab-id={logView.id}
data-tab-type="logView"
data-state={isActive ? 'active' : 'inactive'}
data-active={topTabActiveValue(isActive)}
onClick={handleClick}
onMouseDown={handleTabMiddleMouseDown}
onAuxClick={(e) => handleTabMiddleClickClose(e, () => onCloseLogView(logView.id))}
@@ -1213,6 +1184,7 @@ export const LogViewTopTab: React.FC<LogViewTopTabProps> = memo(({
onDragLeave={onTabDragLeave}
onDrop={(e) => onTabDrop(e, logView.id)}
className={cn(
TOP_TAB_ITEM_CLASS,
"netcatty-tab relative h-7 pl-3 pr-2 min-w-[140px] max-w-[240px] rounded-t-md overflow-hidden text-xs font-semibold cursor-pointer flex items-center justify-between gap-2 app-no-drag flex-shrink-0",
"transition-transform duration-150",
isBeingDragged && isDraggingForReorder ? "opacity-40 scale-95" : "",
@@ -1220,24 +1192,13 @@ export const LogViewTopTab: React.FC<LogViewTopTabProps> = memo(({
)}
style={{
...shiftStyle,
backgroundColor: isActive
? 'var(--top-tabs-active-bg, hsl(var(--background)))'
: 'transparent',
color: isActive
? 'var(--top-tabs-fg, hsl(var(--foreground)))'
: 'var(--top-tabs-muted, hsl(var(--muted-foreground)))',
...resolveTopTabChromeStyle(isActive),
}}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'color-mix(in srgb, var(--top-tabs-active-bg, hsl(var(--background))) 40%, transparent)';
e.currentTarget.style.color = 'var(--top-tabs-fg, hsl(var(--foreground)))';
}
if (!isActive) paintInactiveTopTabHover(e.currentTarget);
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = 'var(--top-tabs-muted, hsl(var(--muted-foreground)))';
}
if (!isActive) clearInactiveTopTabHover(e.currentTarget);
}}
>
{showDropIndicatorBefore && isDraggingForReorder && (
+73
View File
@@ -0,0 +1,73 @@
/**
* Stable hooks for the selected top tab.
*
* Defaults fall through to the existing chrome colors, so leaving the custom
* properties unset keeps today's look. Custom CSS can set them on
* `.top-tab[data-active="true"]` (or any ancestor) without !important.
* `--top-tab-active-border` is read by index.css as a 1px inset outline and
* defaults to transparent.
*/
export const TOP_TAB_ITEM_CLASS = 'top-tab';
/** Previous selected fill, kept as the fallback when the hook is unset. */
export const TOP_TAB_LEGACY_ACTIVE_BG = 'var(--top-tabs-active-bg, hsl(var(--background)))';
/** Previous selected label color, kept as the fallback when the hook is unset. */
export const TOP_TAB_LEGACY_ACTIVE_FG = 'var(--top-tabs-fg, hsl(var(--foreground)))';
export const TOP_TAB_ACTIVE_BG = `var(--top-tab-active-bg, ${TOP_TAB_LEGACY_ACTIVE_BG})`;
export const TOP_TAB_ACTIVE_FG = `var(--top-tab-active-fg, ${TOP_TAB_LEGACY_ACTIVE_FG})`;
export const TOP_TAB_ACTIVE_BORDER = 'var(--top-tab-active-border, transparent)';
export const TOP_TAB_INACTIVE_FG = 'var(--top-tabs-muted, hsl(var(--muted-foreground)))';
/** Same 40% mix as the previous hover fill. The inner color is the active-tab hook. */
export const TOP_TAB_HOVER_BG = `color-mix(in srgb, ${TOP_TAB_ACTIVE_BG} 40%, transparent)`;
export const TOP_TAB_ACTIVE_OUTLINE = `1px solid ${TOP_TAB_ACTIVE_BORDER}`;
export function topTabActiveValue(isActive: boolean): 'true' | 'false' {
return isActive ? 'true' : 'false';
}
export interface TopTabChromeStyle {
backgroundColor: string;
color: string;
}
export function resolveTopTabChromeStyle(
isActive: boolean,
options?: { suppressActiveBg?: boolean },
): TopTabChromeStyle {
if (!isActive) {
return {
backgroundColor: 'transparent',
color: TOP_TAB_INACTIVE_FG,
};
}
return {
backgroundColor: options?.suppressActiveBg ? 'transparent' : TOP_TAB_ACTIVE_BG,
color: TOP_TAB_ACTIVE_FG,
};
}
type TopTabStyleTarget = {
style: {
backgroundColor: string;
color: string;
};
};
export function paintInactiveTopTabHover(element: TopTabStyleTarget): void {
element.style.backgroundColor = TOP_TAB_HOVER_BG;
element.style.color = TOP_TAB_ACTIVE_FG;
}
export function clearInactiveTopTabHover(element: TopTabStyleTarget): void {
element.style.backgroundColor = 'transparent';
element.style.color = TOP_TAB_INACTIVE_FG;
}
+172
View File
@@ -0,0 +1,172 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { JSDOM } from 'jsdom';
import en from '../application/i18n/locales/en';
import es from '../application/i18n/locales/es';
import ru from '../application/i18n/locales/ru';
import zhCN from '../application/i18n/locales/zh-CN';
import zhTW from '../application/i18n/locales/zh-TW';
import {
TOP_TAB_ACTIVE_BG,
TOP_TAB_ACTIVE_BORDER,
TOP_TAB_ACTIVE_FG,
TOP_TAB_ACTIVE_OUTLINE,
TOP_TAB_HOVER_BG,
TOP_TAB_INACTIVE_FG,
TOP_TAB_ITEM_CLASS,
TOP_TAB_LEGACY_ACTIVE_BG,
TOP_TAB_LEGACY_ACTIVE_FG,
clearInactiveTopTabHover,
paintInactiveTopTabHover,
resolveTopTabChromeStyle,
topTabActiveValue,
} from './top-tabs/topTabActiveStyle';
const topTabItemsSource = readFileSync(
fileURLToPath(new URL('./top-tabs/TopTabItems.tsx', import.meta.url)),
'utf8',
);
const indexCss = readFileSync(fileURLToPath(new URL('../index.css', import.meta.url)), 'utf8');
const TAB_TYPES = ['root', 'plugin-view', 'editor', 'session', 'workspace', 'logView'] as const;
const LOCALES = [
{ name: 'en', messages: en },
{ name: 'zh-CN', messages: zhCN },
{ name: 'zh-TW', messages: zhTW },
{ name: 'es', messages: es },
{ name: 'ru', messages: ru },
];
const HOOKS = ['--top-tab-active-bg', '--top-tab-active-fg', '--top-tab-active-border'] as const;
test('active top tab colors fall back to the current chrome colors', () => {
assert.equal(TOP_TAB_ACTIVE_BG, `var(--top-tab-active-bg, ${TOP_TAB_LEGACY_ACTIVE_BG})`);
assert.equal(TOP_TAB_ACTIVE_FG, `var(--top-tab-active-fg, ${TOP_TAB_LEGACY_ACTIVE_FG})`);
assert.equal(TOP_TAB_ACTIVE_BORDER, 'var(--top-tab-active-border, transparent)');
assert.equal(TOP_TAB_LEGACY_ACTIVE_BG, 'var(--top-tabs-active-bg, hsl(var(--background)))');
assert.equal(TOP_TAB_LEGACY_ACTIVE_FG, 'var(--top-tabs-fg, hsl(var(--foreground)))');
assert.equal(
TOP_TAB_HOVER_BG,
`color-mix(in srgb, ${TOP_TAB_ACTIVE_BG} 40%, transparent)`,
);
const active = resolveTopTabChromeStyle(true);
assert.deepEqual(active, {
backgroundColor: TOP_TAB_ACTIVE_BG,
color: TOP_TAB_ACTIVE_FG,
});
const inactive = resolveTopTabChromeStyle(false);
assert.deepEqual(inactive, {
backgroundColor: 'transparent',
color: TOP_TAB_INACTIVE_FG,
});
assert.equal(TOP_TAB_INACTIVE_FG, 'var(--top-tabs-muted, hsl(var(--muted-foreground)))');
const vault = resolveTopTabChromeStyle(true, { suppressActiveBg: true });
assert.equal(vault.backgroundColor, 'transparent');
assert.equal(vault.color, TOP_TAB_ACTIVE_FG);
});
test('top tab active attribute is a stable true/false hook', () => {
assert.equal(topTabActiveValue(true), 'true');
assert.equal(topTabActiveValue(false), 'false');
assert.equal(TOP_TAB_ITEM_CLASS, 'top-tab');
});
test('inactive hover uses the active-tab hook and clears back to the flat tab', () => {
const element = { style: { backgroundColor: '', color: '' } };
paintInactiveTopTabHover(element);
assert.equal(element.style.backgroundColor, TOP_TAB_HOVER_BG);
assert.equal(element.style.color, TOP_TAB_ACTIVE_FG);
clearInactiveTopTabHover(element);
assert.equal(element.style.backgroundColor, 'transparent');
assert.equal(element.style.color, TOP_TAB_INACTIVE_FG);
});
test('every top tab kind exposes the active style hook', () => {
for (const type of TAB_TYPES) {
const marker = `data-tab-type="${type}"`;
const start = topTabItemsSource.indexOf(marker);
assert.notEqual(start, -1, marker);
const next = topTabItemsSource.indexOf('data-tab-type=', start + marker.length);
const slice = topTabItemsSource.slice(start, next === -1 ? undefined : next);
assert.match(slice, /data-active=\{topTabActiveValue\(isActive\)\}/);
assert.match(slice, /TOP_TAB_ITEM_CLASS/);
assert.match(slice, /resolveTopTabChromeStyle\(isActive/);
}
assert.equal(topTabItemsSource.split('data-active={topTabActiveValue(isActive)}').length - 1, TAB_TYPES.length);
assert.doesNotMatch(topTabItemsSource, /backgroundColor: isActive/);
});
test('selected top tab border hook defaults to a transparent inset outline', () => {
const start = indexCss.indexOf('[data-section="top-tabs"] .top-tab[data-active="true"]');
assert.notEqual(start, -1);
const rule = indexCss.slice(start, indexCss.indexOf('}', start));
assert.match(rule, /outline:\s*1px solid var\(--top-tab-active-border, transparent\)/);
assert.match(rule, /outline-offset:\s*-1px/);
assert.equal(TOP_TAB_ACTIVE_OUTLINE, '1px solid var(--top-tab-active-border, transparent)');
});
test('custom CSS sets the top tab hooks on the selected tab', () => {
const dom = new JSDOM(`<!doctype html><html><head></head><body>
<div data-section="top-tabs">
<div id="active" class="top-tab" data-active="true"></div>
<div id="idle" class="top-tab" data-active="false"></div>
</div>
</body></html>`);
const { document, getComputedStyle } = dom.window;
const active = document.getElementById('active')!;
const idle = document.getElementById('idle')!;
const activeStyle = resolveTopTabChromeStyle(true);
active.style.backgroundColor = activeStyle.backgroundColor;
active.style.color = activeStyle.color;
Object.assign(idle.style, resolveTopTabChromeStyle(false));
assert.equal(active.getAttribute('data-active'), 'true');
assert.equal(active.style.backgroundColor, TOP_TAB_ACTIVE_BG);
assert.equal(active.style.color, TOP_TAB_ACTIVE_FG);
assert.equal(idle.style.backgroundColor, 'transparent');
assert.equal(getComputedStyle(active).getPropertyValue('--top-tab-active-bg').trim(), '');
const custom = document.createElement('style');
custom.id = 'netcatty-custom-css';
custom.textContent = `
.top-tab[data-active="true"] {
--top-tab-active-bg: rgb(37, 99, 235);
--top-tab-active-fg: rgb(255, 255, 255);
--top-tab-active-border: rgb(29, 78, 216);
}
`;
document.head.appendChild(custom);
const styled = getComputedStyle(active);
const normalized = (value: string) => value.replace(/\s+/g, '');
assert.equal(normalized(styled.getPropertyValue('--top-tab-active-bg')), 'rgb(37,99,235)');
assert.equal(normalized(styled.getPropertyValue('--top-tab-active-fg')), 'rgb(255,255,255)');
assert.equal(normalized(styled.getPropertyValue('--top-tab-active-border')), 'rgb(29,78,216)');
assert.equal(getComputedStyle(idle).getPropertyValue('--top-tab-active-bg').trim(), '');
assert.match(active.style.backgroundColor, /var\(--top-tab-active-bg,/);
});
test('custom CSS help mentions the top tab hooks in every locale', () => {
for (const locale of LOCALES) {
const desc = locale.messages['settings.appearance.customCss.desc'];
const help = locale.messages['settings.appearance.customCss.help.body'];
const placeholder = locale.messages['settings.appearance.customCss.placeholder'];
assert.equal(typeof desc, 'string', locale.name);
assert.equal(typeof help, 'string', locale.name);
assert.equal(typeof placeholder, 'string', locale.name);
for (const hook of HOOKS) {
assert.ok(desc.includes(hook), `${locale.name} description missing ${hook}`);
assert.ok(help.includes(hook), `${locale.name} help missing ${hook}`);
assert.ok(placeholder.includes(hook), `${locale.name} placeholder missing ${hook}`);
}
assert.match(help, /\.top-tab\[data-active="true"\]/);
assert.match(placeholder, /\.top-tab\[data-active="true"\]/);
}
});
+9
View File
@@ -819,6 +819,15 @@ html[data-theme-transition] *::after {
-webkit-user-select: auto;
}
/* Selected top-tab hooks. Fill and text stay on the tab's inline style so the
unset custom properties fall through to the existing chrome colors. The
border is a 1px inset outline and stays invisible until
--top-tab-active-border is set (Settings → Appearance → Custom CSS). */
[data-section="top-tabs"] .top-tab[data-active="true"] {
outline: 1px solid var(--top-tab-active-border, transparent);
outline-offset: -1px;
}
/* Window controls: icons sit in the h-7 tab row; hover fills the full h-9 title bar */
[data-section="top-tabs"] .window-control-btn {
position: relative;