Merge pull request #114 from XuYouo/main

fix(ui): 优化侧边卡片设置页图标与布局
This commit is contained in:
時芽ときめ
2026-08-16 16:24:28 +08:00
committed by GitHub
parent 9b50f5fb64
commit 835d1538e0
5 changed files with 155 additions and 14 deletions
+16 -14
View File
@@ -24,12 +24,11 @@
flex-direction: column;
gap: 14px;
width: 100%;
/* Tall inventories (7 tab cards + 9 viewer cards + general rows) must
never be clipped by the settings shell: fill the shell's options area
and scroll inside the section when the content outgrows the panel. */
height: 100%;
min-height: 0;
overflow-y: auto;
max-width: 760px;
/* The DSH settings shell's `.options` area owns vertical scrolling. Keep
this page a plain content column like Plugins → Plugin list so its
scrollbar stays at the shell edge instead of being inset by the options
area's right padding. */
}
/* One-line section intro (the DSH section heading+intro recipe). */
@@ -417,10 +416,13 @@
width: min(460px, 100%);
}
/* Popup rows: the same settings-row recipe with switch controls. */
/* Popup rows: each setting gets the same outer chrome as a plugin-list
PluginCard (l2 hairline, 12px radius, layer-3 fill). The row's title,
description and control geometry stay unchanged inside that frame. */
.popupRows {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
@@ -429,16 +431,16 @@
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 2px;
border-bottom: 1px solid var(--dsw-alias-border-l2);
min-width: 0;
padding: 14px 16px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 12px;
background: var(--dsw-alias-bg-layer-3);
transition: border-color 0.16s, background 0.16s;
}
.popupRow:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.popupRows > :last-child {
border-bottom: none;
border-color: var(--dsw-alias-label-dimmed);
}
/* The popup's Done button: the settings primary-button recipe
+11
View File
@@ -20,6 +20,7 @@ import { RenderBoundary } from './RenderBoundary.tsx'
import { registerOpenPathInterception, registerTurnTailInterception } from './intercept.tsx'
import { registerLinkInterception } from './link-intercept.ts'
import { registerImeGuard } from './ime-guard.ts'
import { registerSettingsNavIcon } from './settings-nav-icon.ts'
import { loadPrefs } from './prefs.ts'
import { SideCardSection } from './SideCardSection.tsx'
import { api } from './api.ts'
@@ -213,6 +214,16 @@ export function apply(ctx: Context): void {
'dsh-better-sidebar: IME composition guard',
)
// DSH 0.1.x does not yet carry an icon through the settings.section
// registration contract: its shell renders a generic gear for every
// external section. Mark only this plugin's localized nav row so
// layout.css can paint the requested Side card SVG; the disposer clears
// the marker for HMR / plugin disable.
ctx.effect(
() => registerSettingsNavIcon(() => t('settingsNav')),
'dsh-better-sidebar: settings navigation icon',
)
// The "Side card" settings section: appears in the DSH Settings shell
// once the shell's declaration is on the ledger (slots.inject waits for
// it); the section reads/writes the prefs through the plugin's own
+19
View File
@@ -51,6 +51,25 @@ body[data-dsh-sidebar-dragging] #root > div[data-slot="root"] > div > div:nth-ch
transition: none;
}
/* DSH 0.1.x gives external settings sections a generic gear and exposes no
icon field in the settings.section contract. settings-nav-icon.ts marks
only this plugin's localized row; render the requested Lucide
gallery-horizontal-end SVG as a currentColor mask so it follows the native
nav hover/active colors without changing the shell's 16px icon rhythm. */
[data-dsh-better-sidebar-settings-nav] > svg:first-child {
display: none;
}
[data-dsh-better-sidebar-settings-nav]::before {
content: '';
flex: none;
width: 16px;
height: 16px;
background: currentColor;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 7v10'/%3E%3Cpath d='M6 5v14'/%3E%3Crect width='12' height='18' x='10' y='3' rx='2'/%3E%3C/svg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 7v10'/%3E%3Cpath d='M6 5v14'/%3E%3Crect width='12' height='18' x='10' y='3' rx='2'/%3E%3C/svg%3E") center / contain no-repeat;
}
@media (prefers-reduced-motion: reduce) {
#root,
#root > div[data-slot="root"] > div > div:nth-child(2) {
+45
View File
@@ -0,0 +1,45 @@
/**
* Mark this plugin's row in the DSH settings navigation so its bundled CSS
* can replace the shell's fallback gear with the Side card glyph.
*
* DSH 0.1.x projects only `id`, `order`, and `label` from a
* `settings.section` registration, then chooses icons inside the settings
* shell from a closed list of built-in ids. Until that public contract grows
* an icon field, the plugin identifies only its own localized row after the
* dialog mounts. The marker owns no shell structure and is removed on fiber
* disposal, so the adaptation remains HMR-safe.
*/
export const SETTINGS_NAV_MARKER = 'data-dsh-better-sidebar-settings-nav'
/**
* Keep the marker on the settings-nav button whose visible text is this
* plugin's current localized section label.
* @param label - locale-aware label resolver used by the section registration.
* @returns disposer that disconnects observation and removes owned markers.
*/
export function registerSettingsNavIcon(label: () => string): () => void {
let disposed = false
const sync = (): void => {
if (disposed) return
const currentLabel = label().trim()
const buttons = document.querySelectorAll<HTMLButtonElement>('[role="dialog"] nav button')
for (const button of buttons) {
const matches = currentLabel.length > 0 && button.textContent?.trim() === currentLabel
if (matches) button.setAttribute(SETTINGS_NAV_MARKER, '')
else button.removeAttribute(SETTINGS_NAV_MARKER)
}
}
sync()
const observer = new MutationObserver(sync)
observer.observe(document.body, { childList: true, subtree: true, characterData: true })
return () => {
disposed = true
observer.disconnect()
document.querySelectorAll(`[${SETTINGS_NAV_MARKER}]`)
.forEach(element => { element.removeAttribute(SETTINGS_NAV_MARKER) })
}
}
+64
View File
@@ -0,0 +1,64 @@
// @vitest-environment jsdom
/** Settings-navigation icon marker tests (including delayed dialog mount). */
import { afterEach, describe, expect, it } from 'vitest'
import { registerSettingsNavIcon, SETTINGS_NAV_MARKER } from '../src/client/settings-nav-icon.ts'
function navButton(label: string): HTMLButtonElement {
const button = document.createElement('button')
button.innerHTML = `<svg data-fallback="gear"></svg><span>${label}</span>`
return button
}
function dialogWith(...buttons: HTMLButtonElement[]): HTMLElement {
const dialog = document.createElement('div')
dialog.setAttribute('role', 'dialog')
const nav = document.createElement('nav')
nav.append(...buttons)
dialog.append(nav)
return dialog
}
async function mutationTick(): Promise<void> {
await new Promise<void>(resolve => { setTimeout(resolve, 0) })
}
afterEach(() => {
document.body.innerHTML = ''
})
describe('registerSettingsNavIcon', () => {
it('marks only the matching settings row and preserves the shell icon DOM', () => {
const general = navButton('General')
const sideCard = navButton('Side card')
document.body.append(dialogWith(general, sideCard))
const dispose = registerSettingsNavIcon(() => 'Side card')
expect(general.hasAttribute(SETTINGS_NAV_MARKER)).toBe(false)
expect(sideCard.hasAttribute(SETTINGS_NAV_MARKER)).toBe(true)
expect(sideCard.querySelector('[data-fallback="gear"]')).not.toBeNull()
dispose()
expect(sideCard.hasAttribute(SETTINGS_NAV_MARKER)).toBe(false)
})
it('marks a dialog mounted later and follows a localized label change', async () => {
let label = 'Side card'
const dispose = registerSettingsNavIcon(() => label)
const english = navButton('Side card')
const chinese = navButton('侧边卡片')
const dialog = dialogWith(english, chinese)
document.body.append(dialog)
await mutationTick()
expect(english.hasAttribute(SETTINGS_NAV_MARKER)).toBe(true)
expect(chinese.hasAttribute(SETTINGS_NAV_MARKER)).toBe(false)
label = '侧边卡片'
chinese.querySelector('span')!.textContent = '侧边卡片 '
await mutationTick()
expect(english.hasAttribute(SETTINGS_NAV_MARKER)).toBe(false)
expect(chinese.hasAttribute(SETTINGS_NAV_MARKER)).toBe(true)
dispose()
})
})