dsh-better-sidebar 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/README_EN.md +139 -0
- package/lib/client-docx.js +7649 -0
- package/lib/client-editor.js +32677 -0
- package/lib/client-pptx.js +63438 -0
- package/lib/client-registry.js +8021 -0
- package/lib/client-terminal.js +8060 -0
- package/lib/client-xlsx.js +359755 -0
- package/lib/client.js +8021 -0
- package/lib/index.js +2604 -0
- package/lib/invariant.js +22 -0
- package/lib/types/agent-pty.d.ts +220 -0
- package/lib/types/browser-probe.d.ts +15 -0
- package/lib/types/bundle-route.d.ts +13 -0
- package/lib/types/client/BrowserView.d.ts +22 -0
- package/lib/types/client/DiffTab.d.ts +6 -0
- package/lib/types/client/DiffView.d.ts +51 -0
- package/lib/types/client/EditorHost.d.ts +10 -0
- package/lib/types/client/ExplorerView.d.ts +9 -0
- package/lib/types/client/GitView.d.ts +8 -0
- package/lib/types/client/OrphanedTab.d.ts +11 -0
- package/lib/types/client/PdfView.d.ts +6 -0
- package/lib/types/client/PptxView.d.ts +6 -0
- package/lib/types/client/SandboxStatusBar.d.ts +10 -0
- package/lib/types/client/SideCardSection.d.ts +27 -0
- package/lib/types/client/Sidebar.d.ts +6 -0
- package/lib/types/client/SubagentView.d.ts +16 -0
- package/lib/types/client/TabBar.d.ts +38 -0
- package/lib/types/client/TerminalView.d.ts +8 -0
- package/lib/types/client/TextEditor.d.ts +10 -0
- package/lib/types/client/api.d.ts +170 -0
- package/lib/types/client/binary-download.d.ts +12 -0
- package/lib/types/client/breakpoints.d.ts +12 -0
- package/lib/types/client/browser.d.ts +50 -0
- package/lib/types/client/builtins/index.d.ts +17 -0
- package/lib/types/client/builtins/tabs.d.ts +6 -0
- package/lib/types/client/builtins/viewers.d.ts +3 -0
- package/lib/types/client/chunk-loader.d.ts +74 -0
- package/lib/types/client/chunks/docx.d.ts +8 -0
- package/lib/types/client/chunks/editor.d.ts +9 -0
- package/lib/types/client/chunks/pptx.d.ts +8 -0
- package/lib/types/client/chunks/terminal.d.ts +9 -0
- package/lib/types/client/chunks/xlsx.d.ts +9 -0
- package/lib/types/client/cm-themes.d.ts +24 -0
- package/lib/types/client/conversation-draft.d.ts +14 -0
- package/lib/types/client/docx-view.d.ts +8 -0
- package/lib/types/client/editor-load.d.ts +66 -0
- package/lib/types/client/icons.d.ts +47 -0
- package/lib/types/client/image-types.d.ts +3 -0
- package/lib/types/client/ime-guard.d.ts +36 -0
- package/lib/types/client/index.d.ts +10 -0
- package/lib/types/client/intercept.d.ts +20 -0
- package/lib/types/client/lang.d.ts +12 -0
- package/lib/types/client/lazy-chunk.d.ts +25 -0
- package/lib/types/client/link-intercept.d.ts +35 -0
- package/lib/types/client/locales.d.ts +237 -0
- package/lib/types/client/office-shared.d.ts +33 -0
- package/lib/types/client/office-types.d.ts +36 -0
- package/lib/types/client/office-view.d.ts +12 -0
- package/lib/types/client/openpath-intercept.d.ts +41 -0
- package/lib/types/client/paths.d.ts +19 -0
- package/lib/types/client/pdf-types.d.ts +2 -0
- package/lib/types/client/prefs.d.ts +30 -0
- package/lib/types/client/produced-files.d.ts +27 -0
- package/lib/types/client/selection-payload.d.ts +27 -0
- package/lib/types/client/service.d.ts +214 -0
- package/lib/types/client/split-pane.d.ts +29 -0
- package/lib/types/client/state.d.ts +322 -0
- package/lib/types/client/subagent-activity.d.ts +35 -0
- package/lib/types/client/subagent-detect.d.ts +47 -0
- package/lib/types/client/subagent-jobs.d.ts +63 -0
- package/lib/types/client/theme.d.ts +28 -0
- package/lib/types/client/xlsx-to-univer.d.ts +33 -0
- package/lib/types/client/xlsx-view.d.ts +10 -0
- package/lib/types/config.d.ts +41 -0
- package/lib/types/context-types.d.ts +431 -0
- package/lib/types/fs-tree.d.ts +41 -0
- package/lib/types/git.d.ts +75 -0
- package/lib/types/html-route.d.ts +48 -0
- package/lib/types/index.d.ts +39 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/jobs-routes.d.ts +45 -0
- package/lib/types/prefs-shared.d.ts +106 -0
- package/lib/types/pty-manager.d.ts +76 -0
- package/lib/types/tools.d.ts +28 -0
- package/lib/types/trust-fence.d.ts +24 -0
- package/lib/types/wire.d.ts +38 -0
- package/package.json +168 -0
- package/src/agent-pty.ts +519 -0
- package/src/browser-probe.ts +26 -0
- package/src/bundle-route.ts +130 -0
- package/src/client/BrowserView.tsx +253 -0
- package/src/client/DiffTab.tsx +110 -0
- package/src/client/DiffView.tsx +266 -0
- package/src/client/EditorHost.tsx +101 -0
- package/src/client/ExplorerView.tsx +283 -0
- package/src/client/GitView.tsx +540 -0
- package/src/client/OrphanedTab.tsx +27 -0
- package/src/client/PdfView.tsx +110 -0
- package/src/client/PptxView.tsx +131 -0
- package/src/client/SandboxStatusBar.tsx +60 -0
- package/src/client/SideCardSection.module.css +271 -0
- package/src/client/SideCardSection.tsx +426 -0
- package/src/client/Sidebar.tsx +861 -0
- package/src/client/SubagentView.module.css +547 -0
- package/src/client/SubagentView.tsx +866 -0
- package/src/client/TabBar.tsx +188 -0
- package/src/client/TerminalView.tsx +233 -0
- package/src/client/TextEditor.tsx +382 -0
- package/src/client/api.ts +215 -0
- package/src/client/binary-download.tsx +23 -0
- package/src/client/breakpoints.ts +51 -0
- package/src/client/browser.ts +119 -0
- package/src/client/builtins/index.ts +33 -0
- package/src/client/builtins/tabs.tsx +211 -0
- package/src/client/builtins/viewers.tsx +151 -0
- package/src/client/chunk-loader.ts +196 -0
- package/src/client/chunks/docx.tsx +8 -0
- package/src/client/chunks/editor.tsx +9 -0
- package/src/client/chunks/pptx.tsx +8 -0
- package/src/client/chunks/terminal.tsx +9 -0
- package/src/client/chunks/xlsx.tsx +9 -0
- package/src/client/cm-themes.ts +129 -0
- package/src/client/conversation-draft.ts +29 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/docx-view.tsx +113 -0
- package/src/client/editor-load.ts +92 -0
- package/src/client/icons.tsx +137 -0
- package/src/client/image-types.ts +8 -0
- package/src/client/ime-guard.ts +47 -0
- package/src/client/index.tsx +238 -0
- package/src/client/intercept.tsx +94 -0
- package/src/client/lang.ts +95 -0
- package/src/client/layout.css +54 -0
- package/src/client/lazy-chunk.tsx +89 -0
- package/src/client/link-intercept.ts +66 -0
- package/src/client/locales.ts +476 -0
- package/src/client/office-shared.tsx +40 -0
- package/src/client/office-types.ts +48 -0
- package/src/client/office-view.tsx +12 -0
- package/src/client/openpath-intercept.ts +62 -0
- package/src/client/paths.ts +28 -0
- package/src/client/pdf-types.ts +4 -0
- package/src/client/prefs.ts +100 -0
- package/src/client/produced-files.ts +87 -0
- package/src/client/selection-payload.ts +86 -0
- package/src/client/service.ts +434 -0
- package/src/client/sidebar.module.css +1917 -0
- package/src/client/split-pane.tsx +292 -0
- package/src/client/state.ts +1048 -0
- package/src/client/subagent-activity.ts +67 -0
- package/src/client/subagent-detect.ts +125 -0
- package/src/client/subagent-jobs.ts +161 -0
- package/src/client/theme.ts +44 -0
- package/src/client/xlsx-to-univer.ts +260 -0
- package/src/client/xlsx-view.tsx +116 -0
- package/src/config.ts +95 -0
- package/src/context-types.ts +445 -0
- package/src/fs-tree.ts +115 -0
- package/src/git.ts +236 -0
- package/src/html-route.ts +70 -0
- package/src/index.ts +897 -0
- package/src/invariant.ts +32 -0
- package/src/jobs-routes.ts +257 -0
- package/src/prefs-shared.ts +127 -0
- package/src/pty-manager.ts +199 -0
- package/src/tools.ts +477 -0
- package/src/trust-fence.ts +77 -0
- package/src/wire.ts +94 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Side card" settings section: the user-facing preferences for the sidebar
|
|
3
|
+
* panel, rendered natively in the DSH Settings shell (nav label "Side card").
|
|
4
|
+
*
|
|
5
|
+
* The section is DECLARATIVE — it renders the enable/disable inventory from
|
|
6
|
+
* the sidebar service's registries instead of hardcoding rows:
|
|
7
|
+
* - 常规: new conversations open the panel by default (a toggle card), and
|
|
8
|
+
* the default panel width as a percent of the window (number input row).
|
|
9
|
+
* - 侧边栏内容: one SMALL CARD per REGISTERED tab type (built-ins and
|
|
10
|
+
* external plugins alike), laid out in a responsive grid that wraps
|
|
11
|
+
* several cards per row — icon + title + type id, clicked to toggle the
|
|
12
|
+
* switch persisted in `prefs.tabsEnabled[id]`.
|
|
13
|
+
* - 文件预览: one SMALL CARD per REGISTERED file viewer — icon + title +
|
|
14
|
+
* the extensions it covers, clicked to toggle `prefs.viewersEnabled[id]`.
|
|
15
|
+
*
|
|
16
|
+
* A card's on/off state is its VISUAL STATE: enabled = highlighted (brand
|
|
17
|
+
* border + tinted fill + a check badge pinned to the card's far right),
|
|
18
|
+
* disabled = neutral and dimmed. Features that declare `settings.toggles`
|
|
19
|
+
* carry a gear corner button that opens a native Modal with the related
|
|
20
|
+
* settings as native checkbox rows (e.g. the Subagent page's "auto-open
|
|
21
|
+
* when a subagent appears", the Terminal page's model terminal tools).
|
|
22
|
+
*
|
|
23
|
+
* Writes ride the plugin's own fenced settings route (the host calls the
|
|
24
|
+
* settings seam in-process — the DSH settings RPC domain does not serve
|
|
25
|
+
* third-party namespaces to configuration clients); the shared SidebarStore
|
|
26
|
+
* is refreshed on success so the very next brand-new session seeds from the
|
|
27
|
+
* new values and the sidebar's consumption points (the + menu, derived
|
|
28
|
+
* flows) re-render immediately. Any failure reverts the optimistic UI and
|
|
29
|
+
* shows the wire error inline — a broken settings surface never crashes the
|
|
30
|
+
* shell.
|
|
31
|
+
*/
|
|
32
|
+
import { Fragment, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
33
|
+
import {
|
|
34
|
+
IconCheckOutline16,
|
|
35
|
+
IconCodeOutline16,
|
|
36
|
+
IconPanelLeftOutline16,
|
|
37
|
+
IconSettingsOutline16,
|
|
38
|
+
Input,
|
|
39
|
+
Modal,
|
|
40
|
+
} from '@deepseek-ai/dsh-client-ui-primitives'
|
|
41
|
+
import clsx from 'clsx'
|
|
42
|
+
// Type-only: pulls the settings shell's SlotMap merges ('settings.section').
|
|
43
|
+
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
44
|
+
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
45
|
+
import {
|
|
46
|
+
clampWidthPercent,
|
|
47
|
+
WIDTH_PERCENT_MAX,
|
|
48
|
+
WIDTH_PERCENT_MIN,
|
|
49
|
+
type SidebarPrefs,
|
|
50
|
+
} from '../prefs-shared.ts'
|
|
51
|
+
import { api } from './api.ts'
|
|
52
|
+
import { parsePrefs } from './prefs.ts'
|
|
53
|
+
import { t } from './locales.ts'
|
|
54
|
+
import type { SidebarStore } from './state.ts'
|
|
55
|
+
import type {
|
|
56
|
+
BetterSidebarService,
|
|
57
|
+
FileViewerDescriptor,
|
|
58
|
+
SidebarSettingToggle,
|
|
59
|
+
TabDescriptor,
|
|
60
|
+
} from './service.ts'
|
|
61
|
+
import css from './SideCardSection.module.css'
|
|
62
|
+
|
|
63
|
+
/** Injected business face: the shared store (prefs cache) + the sidebar service (registries). */
|
|
64
|
+
export interface SideCardSectionInjected {
|
|
65
|
+
store: SidebarStore
|
|
66
|
+
service: BetterSidebarService
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Full section props: the runtime share plus the injected face. */
|
|
70
|
+
export type SideCardSectionProps = PropsRuntime<'settings.section'> & SideCardSectionInjected
|
|
71
|
+
|
|
72
|
+
/** Map one wire failure to the inline message (the conflict gets friendly copy). */
|
|
73
|
+
function messageOf(error: unknown): string {
|
|
74
|
+
if (error instanceof Error && 'code' in error && (error as { code?: unknown }).code === 'settings-conflict') {
|
|
75
|
+
return `${t('settingsSaveFailed')} ${t('settingsConflict')}`
|
|
76
|
+
}
|
|
77
|
+
return `${t('settingsSaveFailed')} ${error instanceof Error ? error.message : String(error)}`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Resolve an i18n-friendly string-or-function value. */
|
|
81
|
+
function textOf(value: string | (() => string) | undefined): string {
|
|
82
|
+
if (value === undefined) return ''
|
|
83
|
+
return typeof value === 'function' ? value() : value
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Resolve a descriptor icon (ReactNode or size function). */
|
|
87
|
+
function iconOf(icon: ReactNode | ((size: number) => ReactNode) | undefined, size: number): ReactNode {
|
|
88
|
+
if (icon === undefined) return null
|
|
89
|
+
return typeof icon === 'function' ? icon(size) : icon
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Tab inventory order: hidden types (editor/diff) last, then + menu order. */
|
|
93
|
+
function tabOrder(a: TabDescriptor, b: TabDescriptor): number {
|
|
94
|
+
if (a.hidden !== b.hidden) return a.hidden === true ? 1 : -1
|
|
95
|
+
return (a.order ?? 100) - (b.order ?? 100)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Viewer inventory order: priority desc (the catch-all `code` comes last). */
|
|
99
|
+
function viewerOrder(a: FileViewerDescriptor, b: FileViewerDescriptor): number {
|
|
100
|
+
return (b.priority ?? 0) - (a.priority ?? 0)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Read one boolean pref by declarative key (missing = false). */
|
|
104
|
+
function prefBool(prefs: SidebarPrefs, key: string): boolean {
|
|
105
|
+
return (prefs as unknown as Record<string, boolean>)[key] === true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The body of a feature's secondary settings popup: one native checkbox row
|
|
110
|
+
* per declared toggle. Extracted so the rows are testable without opening
|
|
111
|
+
* the Modal (the Modal portal renders only while open).
|
|
112
|
+
*/
|
|
113
|
+
export function FeatureSettingsRows(props: {
|
|
114
|
+
toggles: readonly SidebarSettingToggle[]
|
|
115
|
+
prefs: SidebarPrefs
|
|
116
|
+
onToggle: (toggle: SidebarSettingToggle, next: boolean) => void
|
|
117
|
+
}) {
|
|
118
|
+
const { toggles, prefs, onToggle } = props
|
|
119
|
+
return (
|
|
120
|
+
<div className={css.popupRows}>
|
|
121
|
+
{toggles.map(toggle => {
|
|
122
|
+
const title = textOf(toggle.title)
|
|
123
|
+
return (
|
|
124
|
+
<label key={toggle.key} className={css.popupRow}>
|
|
125
|
+
<span className={css.rowText}>
|
|
126
|
+
<span className={css.title}>{title}</span>
|
|
127
|
+
{textOf(toggle.desc) !== '' && <span className={css.desc}>{textOf(toggle.desc)}</span>}
|
|
128
|
+
</span>
|
|
129
|
+
<input
|
|
130
|
+
type="checkbox"
|
|
131
|
+
className={css.toggle}
|
|
132
|
+
checked={prefBool(prefs, toggle.key)}
|
|
133
|
+
aria-label={title}
|
|
134
|
+
onChange={event => { onToggle(toggle, event.currentTarget.checked) }}
|
|
135
|
+
/>
|
|
136
|
+
</label>
|
|
137
|
+
)
|
|
138
|
+
})}
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Render the Side card preferences section.
|
|
145
|
+
* @param props - composed slot props (runtime share + injected store/service).
|
|
146
|
+
* @returns the section element tree.
|
|
147
|
+
*/
|
|
148
|
+
export function SideCardSection({ store, service }: SideCardSectionProps) {
|
|
149
|
+
const [prefs, setPrefs] = useState<SidebarPrefs>(() => store.getPrefs())
|
|
150
|
+
const [widthDraft, setWidthDraft] = useState<string>(String(store.getPrefs().defaultWidthPercent))
|
|
151
|
+
const [error, setError] = useState<string | null>(null)
|
|
152
|
+
// Which feature's secondary settings popup is open (null = closed).
|
|
153
|
+
const [settingsFor, setSettingsFor] = useState<TabDescriptor | null>(null)
|
|
154
|
+
|
|
155
|
+
// The declarative inventory: the registered tab types and file viewers.
|
|
156
|
+
// Local state + service.subscribe (registry changes are rare — plugin
|
|
157
|
+
// load/unload — so a plain effect is enough; no external-store ceremony).
|
|
158
|
+
const [tabs, setTabs] = useState<TabDescriptor[]>(() => [...service.getTabs()].sort(tabOrder))
|
|
159
|
+
const [viewers, setViewers] = useState<FileViewerDescriptor[]>(() => [...service.getFileViewers()].sort(viewerOrder))
|
|
160
|
+
useEffect(() => service.subscribe(() => {
|
|
161
|
+
setTabs([...service.getTabs()].sort(tabOrder))
|
|
162
|
+
setViewers([...service.getFileViewers()].sort(viewerOrder))
|
|
163
|
+
}), [service])
|
|
164
|
+
|
|
165
|
+
// The settings document revision (guards concurrent writes). A ref: commits
|
|
166
|
+
// read the freshest value at execution time, no re-render needed.
|
|
167
|
+
const revisionRef = useRef<number | undefined>(undefined)
|
|
168
|
+
// Whether the user already wrote since mount: the mount read must not
|
|
169
|
+
// clobber a newer optimistic edit (the window is milliseconds, but a slow
|
|
170
|
+
// route must never silently revert a just-made change).
|
|
171
|
+
const dirtyRef = useRef(false)
|
|
172
|
+
// Serialize commits: a queued write must observe the previous write's
|
|
173
|
+
// revision; a failed write must not poison the queue for later ones.
|
|
174
|
+
const inFlightRef = useRef<Promise<unknown>>(Promise.resolve())
|
|
175
|
+
|
|
176
|
+
// Sync the persisted document once on mount: the revision and the current
|
|
177
|
+
// values (another tab may have changed them since the store hydrated).
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
let cancelled = false
|
|
180
|
+
void api.settingsGet().then((view) => {
|
|
181
|
+
if (cancelled) return
|
|
182
|
+
revisionRef.current = view.revision
|
|
183
|
+
if (dirtyRef.current) return
|
|
184
|
+
const next = parsePrefs(view.value)
|
|
185
|
+
setPrefs(next)
|
|
186
|
+
setWidthDraft(String(next.defaultWidthPercent))
|
|
187
|
+
}).catch(() => { /* the store's defaults stay authoritative */ })
|
|
188
|
+
return () => { cancelled = true }
|
|
189
|
+
}, [])
|
|
190
|
+
|
|
191
|
+
/** Persist one patch through the settings route (serialized, revision-guarded). */
|
|
192
|
+
const commit = (patch: Record<string, unknown>): Promise<{ ok: boolean; prefs: SidebarPrefs }> => {
|
|
193
|
+
dirtyRef.current = true
|
|
194
|
+
const run = inFlightRef.current.then(async () => {
|
|
195
|
+
const view = await api.settingsUpdate(
|
|
196
|
+
{ ...patch },
|
|
197
|
+
revisionRef.current,
|
|
198
|
+
)
|
|
199
|
+
const next = parsePrefs(view.value)
|
|
200
|
+
revisionRef.current = view.revision
|
|
201
|
+
store.setPrefs(next)
|
|
202
|
+
return next
|
|
203
|
+
})
|
|
204
|
+
// A failed commit must not poison the queue: later writes still run.
|
|
205
|
+
inFlightRef.current = run.then(() => undefined, () => undefined)
|
|
206
|
+
return run.then(
|
|
207
|
+
(next) => ({ ok: true, prefs: next }),
|
|
208
|
+
(caught) => {
|
|
209
|
+
setError(messageOf(caught))
|
|
210
|
+
return { ok: false, prefs }
|
|
211
|
+
},
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Settle one commit: success adopts the server values, failure reverts. */
|
|
216
|
+
const applyOutcome = (previous: SidebarPrefs, outcome: { ok: boolean; prefs: SidebarPrefs }): void => {
|
|
217
|
+
const settled = outcome.ok ? outcome.prefs : previous
|
|
218
|
+
setPrefs(settled)
|
|
219
|
+
setWidthDraft(String(settled.defaultWidthPercent))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Optimistically flip one boolean pref, then commit (revert on failure). */
|
|
223
|
+
const togglePref = (patch: Record<string, unknown>): void => {
|
|
224
|
+
const previous = prefs
|
|
225
|
+
setPrefs({ ...previous, ...patch } as SidebarPrefs)
|
|
226
|
+
setError(null)
|
|
227
|
+
void commit(patch).then(outcome => applyOutcome(previous, outcome))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const onToggle = (next: boolean): void => {
|
|
231
|
+
togglePref({ openByDefault: next })
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Flip one per-tab enable switch (merge into the tabsEnabled map). */
|
|
235
|
+
const onToggleTab = (id: string, next: boolean): void => {
|
|
236
|
+
togglePref({ tabsEnabled: { ...prefs.tabsEnabled, [id]: next } })
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Flip one per-viewer enable switch (merge into the viewersEnabled map). */
|
|
240
|
+
const onToggleViewer = (id: string, next: boolean): void => {
|
|
241
|
+
togglePref({ viewersEnabled: { ...prefs.viewersEnabled, [id]: next } })
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Flip one declaratively-declared toggle (a SidebarPrefs boolean field). */
|
|
245
|
+
const onToggleSetting = (toggle: SidebarSettingToggle, next: boolean): void => {
|
|
246
|
+
togglePref({ [toggle.key]: next })
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const commitWidth = (): void => {
|
|
250
|
+
const parsed = Number(widthDraft)
|
|
251
|
+
if (!Number.isFinite(parsed)) {
|
|
252
|
+
setWidthDraft(String(prefs.defaultWidthPercent))
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
const clamped = clampWidthPercent(parsed)
|
|
256
|
+
const previous = prefs
|
|
257
|
+
setPrefs({ ...previous, defaultWidthPercent: clamped })
|
|
258
|
+
setWidthDraft(String(clamped))
|
|
259
|
+
setError(null)
|
|
260
|
+
void commit({ defaultWidthPercent: clamped }).then(outcome => applyOutcome(previous, outcome))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* One SMALL toggle card for the responsive inventory grid: the card's main
|
|
265
|
+
* area is the switch (click to flips, visual state IS the state), the
|
|
266
|
+
* check badge sits at the far right, and a feature that declares related
|
|
267
|
+
* settings carries a gear corner button opening its settings popup.
|
|
268
|
+
*/
|
|
269
|
+
const renderCard = (props: {
|
|
270
|
+
title: string
|
|
271
|
+
desc: string
|
|
272
|
+
icon?: ReactNode
|
|
273
|
+
enabled: boolean
|
|
274
|
+
onToggle: (next: boolean) => void
|
|
275
|
+
/** A feature with declared related settings shows the gear corner button. */
|
|
276
|
+
onOpenSettings?: () => void
|
|
277
|
+
}) => {
|
|
278
|
+
const hasSettings = props.onOpenSettings !== undefined
|
|
279
|
+
return (
|
|
280
|
+
<div
|
|
281
|
+
className={clsx(css.card, props.enabled && css.cardOn, hasSettings && css.cardWithGear)}
|
|
282
|
+
>
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
className={css.cardMain}
|
|
286
|
+
aria-pressed={props.enabled}
|
|
287
|
+
title={props.desc}
|
|
288
|
+
onClick={() => { props.onToggle(!props.enabled) }}
|
|
289
|
+
>
|
|
290
|
+
<span className={css.cardTop}>
|
|
291
|
+
{props.icon !== null && props.icon !== undefined && (
|
|
292
|
+
<span className={css.cardIcon}>{props.icon}</span>
|
|
293
|
+
)}
|
|
294
|
+
<span className={css.cardTitle}>{props.title}</span>
|
|
295
|
+
{props.enabled && (
|
|
296
|
+
<span className={css.cardCheck}>
|
|
297
|
+
<IconCheckOutline16 size={14} />
|
|
298
|
+
</span>
|
|
299
|
+
)}
|
|
300
|
+
</span>
|
|
301
|
+
<span className={css.cardDesc}>{props.desc}</span>
|
|
302
|
+
</button>
|
|
303
|
+
{hasSettings && (
|
|
304
|
+
<button
|
|
305
|
+
type="button"
|
|
306
|
+
className={css.cardGear}
|
|
307
|
+
aria-label={`${props.title} ${t('settingsPopup')}`}
|
|
308
|
+
title={t('settingsPopup')}
|
|
309
|
+
onClick={props.onOpenSettings}
|
|
310
|
+
>
|
|
311
|
+
<IconSettingsOutline16 size={14} />
|
|
312
|
+
</button>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div className={css.section}>
|
|
320
|
+
<div className={css.sectionHeading}>{t('settingsGeneralTitle')}</div>
|
|
321
|
+
{renderCard({
|
|
322
|
+
title: t('settingsOpenTitle'),
|
|
323
|
+
desc: t('settingsOpenDesc'),
|
|
324
|
+
icon: <IconPanelLeftOutline16 size={16} />,
|
|
325
|
+
enabled: prefs.openByDefault,
|
|
326
|
+
onToggle,
|
|
327
|
+
})}
|
|
328
|
+
<div className={css.row}>
|
|
329
|
+
<span className={css.rowText}>
|
|
330
|
+
<span className={css.title}>{t('settingsWidthTitle')}</span>
|
|
331
|
+
<span className={css.desc}>{t('settingsWidthDesc')}</span>
|
|
332
|
+
</span>
|
|
333
|
+
<span className={css.control}>
|
|
334
|
+
<Input
|
|
335
|
+
type="number"
|
|
336
|
+
className={css.percentInput}
|
|
337
|
+
value={widthDraft}
|
|
338
|
+
min={WIDTH_PERCENT_MIN}
|
|
339
|
+
max={WIDTH_PERCENT_MAX}
|
|
340
|
+
step={1}
|
|
341
|
+
aria-label={t('settingsWidthTitle')}
|
|
342
|
+
onChange={event => { setWidthDraft(event.currentTarget.value) }}
|
|
343
|
+
onBlur={commitWidth}
|
|
344
|
+
onKeyDown={event => {
|
|
345
|
+
if (event.key === 'Enter') event.currentTarget.blur()
|
|
346
|
+
}}
|
|
347
|
+
/>
|
|
348
|
+
<span className={css.suffix}>{t('settingsWidthSuffix')}</span>
|
|
349
|
+
</span>
|
|
350
|
+
</div>
|
|
351
|
+
{renderCard({
|
|
352
|
+
title: t('settingsOpenPathTitle'),
|
|
353
|
+
desc: t('settingsOpenPathDesc'),
|
|
354
|
+
icon: <IconCodeOutline16 size={16} />,
|
|
355
|
+
enabled: prefs.interceptOpenPath,
|
|
356
|
+
onToggle: (next) => { togglePref({ interceptOpenPath: next }) },
|
|
357
|
+
})}
|
|
358
|
+
|
|
359
|
+
{/* 侧边栏内容: one small card per registered tab type in a responsive
|
|
360
|
+
grid; features declaring `settings.toggles` open their settings in
|
|
361
|
+
the popup (gear corner button) instead of nested inline rows. */}
|
|
362
|
+
<div className={css.sectionHeading}>{t('settingsTabsTitle')}</div>
|
|
363
|
+
<div className={css.grid}>
|
|
364
|
+
{tabs.map(tab => (
|
|
365
|
+
<Fragment key={tab.id}>
|
|
366
|
+
{renderCard({
|
|
367
|
+
title: textOf(tab.title),
|
|
368
|
+
desc: tab.id,
|
|
369
|
+
icon: iconOf(tab.icon, 16),
|
|
370
|
+
enabled: prefs.tabsEnabled[tab.id] !== false,
|
|
371
|
+
onToggle: (next) => { onToggleTab(tab.id, next) },
|
|
372
|
+
// The settings gear only while the feature is enabled: its
|
|
373
|
+
// related settings are dormant while the feature is off.
|
|
374
|
+
onOpenSettings: prefs.tabsEnabled[tab.id] !== false
|
|
375
|
+
&& (tab.settings?.toggles?.length ?? 0) > 0
|
|
376
|
+
? () => { setSettingsFor(tab) }
|
|
377
|
+
: undefined,
|
|
378
|
+
})}
|
|
379
|
+
</Fragment>
|
|
380
|
+
))}
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
{/* 文件预览: one small card per registered file viewer. */}
|
|
384
|
+
<div className={css.sectionHeading}>{t('settingsViewersTitle')}</div>
|
|
385
|
+
<div className={css.grid}>
|
|
386
|
+
{viewers.map(viewer => (
|
|
387
|
+
<Fragment key={viewer.id}>
|
|
388
|
+
{renderCard({
|
|
389
|
+
title: textOf(viewer.title) || viewer.id,
|
|
390
|
+
desc: viewer.exts.length === 0 ? t('settingsViewerCatchAll') : viewer.exts.join(' · '),
|
|
391
|
+
icon: iconOf(viewer.icon, 16),
|
|
392
|
+
enabled: prefs.viewersEnabled[viewer.id] !== false,
|
|
393
|
+
onToggle: (next) => { onToggleViewer(viewer.id, next) },
|
|
394
|
+
})}
|
|
395
|
+
</Fragment>
|
|
396
|
+
))}
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
{/* The secondary settings popup: a feature's declared related settings
|
|
400
|
+
as native checkbox rows (Modal chrome is the app's own). Mounted
|
|
401
|
+
only while a feature is open — the Modal primitive runs hooks
|
|
402
|
+
unconditionally, so a closed-but-mounted Modal would break SSR
|
|
403
|
+
(and the renderToString spec) under the test dual-react split. */}
|
|
404
|
+
{settingsFor !== null && (
|
|
405
|
+
<Modal
|
|
406
|
+
open
|
|
407
|
+
onClose={() => { setSettingsFor(null) }}
|
|
408
|
+
title={textOf(settingsFor.title)}
|
|
409
|
+
closeLabel={t('close')}
|
|
410
|
+
>
|
|
411
|
+
<FeatureSettingsRows
|
|
412
|
+
toggles={settingsFor.settings?.toggles ?? []}
|
|
413
|
+
prefs={prefs}
|
|
414
|
+
onToggle={onToggleSetting}
|
|
415
|
+
/>
|
|
416
|
+
</Modal>
|
|
417
|
+
)}
|
|
418
|
+
|
|
419
|
+
{error !== null && (
|
|
420
|
+
<div className={css.error} role="alert">
|
|
421
|
+
{error}
|
|
422
|
+
</div>
|
|
423
|
+
)}
|
|
424
|
+
</div>
|
|
425
|
+
)
|
|
426
|
+
}
|