@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) })
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user