@objectivex666/dsh-settings-search 1.9.0 → 1.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/README.en.md CHANGED
@@ -6,6 +6,7 @@
6
6
  [![license](https://img.shields.io/npm/l/@objectivex666/dsh-settings-search)](./LICENSE)
7
7
  [![node](https://img.shields.io/node/v/@objectivex666/dsh-settings-search)](https://nodejs.org)
8
8
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
9
+ [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/objectivex666/dsh-settings-search)
9
10
 
10
11
  **English** · [中文](./README.md)
11
12
 
@@ -14,7 +15,8 @@
14
15
  ## Features
15
16
 
16
17
  - 🔍 **Live search** — a search box in the settings panel filters every registered settings page (`settings.section`) and general item (`settings.general.item`) as you type.
17
- - 🧩 **Four search styles (v1.9.0)** — choose the classic sidebar, window top bar, popup search page, or a mixed window top bar plus popup page layout. The choice is saved in the current browser.
18
+ - 🧩 **Three search styles** — choose the classic sidebar, window top bar, or popup search page. The choice is saved in the current browser.
19
+ - ⌨️ **Search shortcut (v1.10.0)** — focus the search box with `Ctrl+F` or `⌘F` inside the settings panel. You can enable or disable it from the "Settings Search" page; disabling keeps the browser's default find behavior.
18
20
  - 🤝 **Import DSH model config (v1.9.0)** — import the Base URL and model name from DSH's current default model in the AI config section. For credential safety, the API key is never imported.
19
21
  - 🎯 **First-run onboarding (v1.9.0)** — a welcome modal appears only the first time the settings panel opens after installation. It introduces the four search styles, pinyin/intent/AI hints, click-to-jump, and auto-indexing; there are no "Don't show again" or manual reopen buttons.
20
22
  - 📂 **In-page option search (v1.2.0)** — search beyond pages and reach the specific options inside them: Plugins-page tabs (`settings.plugins.tab`), Web UI plugin cards (`web-ui.plugin.item`), general rows, and more, each shown with a "page › option" breadcrumb.
package/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  [![license](https://img.shields.io/npm/l/@objectivex666/dsh-settings-search)](./LICENSE)
7
7
  [![node](https://img.shields.io/node/v/@objectivex666/dsh-settings-search)](https://nodejs.org)
8
8
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
9
+ [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/objectivex666/dsh-settings-search)
9
10
 
10
11
  **中文** · [English](./README.en.md)
11
12
 
@@ -14,7 +15,8 @@
14
15
  ## 功能特性
15
16
 
16
17
  - 🔍 **实时搜索**:在设置面板中嵌入搜索框,输入关键词立即过滤所有已注册的设置页(`settings.section`)与通用设置项(`settings.general.item`)。
17
- - 🧩 **四种搜索样式(v1.9.0)**:可选择经典侧栏、窗口顶栏、弹出搜索页,或“窗口顶栏 + 弹出搜索页”混合模式;选择会保存在当前浏览器。
18
+ - 🧩 **三种搜索样式**:可选择经典侧栏、窗口顶栏或弹出搜索页;选择会保存在当前浏览器。
19
+ - ⌨️ **搜索快捷键(v1.10.0)**:在设置面板内按 `Ctrl+F` 或 `⌘F` 聚焦搜索框;可在「设置搜索」页启用或关闭,关闭后保留浏览器默认查找。
18
20
  - 🤝 **导入 DSH 模型配置(v1.9.0)**:在 AI 配置区一键读取 DSH 当前默认模型的 Base URL 与模型名;为避免凭据扩散,导入始终不包含 API Key。
19
21
  - 🎯 **新用户引导(v1.9.0)**:仅在安装后首次打开设置面板时弹出引导弹窗,介绍四种搜索样式、拼音/意图/AI 联想、点击即达与自动导入索引等核心功能;不再提供“不再显示”和手动重开按钮。
20
22
  - 📂 **页内选项搜索(v1.2.0)**:不只搜页面,还能直达每个设置页内部的具体选项——插件页的标签页(`settings.plugins.tab`)、Web UI 插件的卡片(`web-ui.plugin.item`)、通用设置行等,结果以「页面 › 选项」面包屑展示。
package/lib/client.js CHANGED
@@ -30,7 +30,7 @@ window.__ModuleLoader__.load({
30
30
 
31
31
  const NS = 'settings-search-v5'
32
32
  /** Single source of truth is package.json — scripts/check.mjs asserts this matches. */
33
- const PKG_VERSION = '1.9.0'
33
+ const PKG_VERSION = '1.10.0'
34
34
  const NPM_NAME = '@objectivex666/dsh-settings-search'
35
35
  const zh = {
36
36
  title: '设置',
@@ -41,8 +41,9 @@ window.__ModuleLoader__.load({
41
41
  styleClassic: '经典侧栏',
42
42
  styleTop: '窗口顶栏',
43
43
  stylePage: '弹出搜索页',
44
- styleMixed: '顶栏 + 搜索页',
45
44
  openSearch: '打开搜索',
45
+ shortcutTitle: 'Ctrl+F / ⌘F 快捷键',
46
+ shortcutHint: '在设置面板内按 Ctrl+F 或 ⌘F 聚焦搜索框;关闭后保留浏览器默认查找。',
46
47
  warming: '正在导入各分区选项…',
47
48
  empty: '没有匹配的设置',
48
49
  pages: '设置页',
@@ -128,7 +129,7 @@ window.__ModuleLoader__.load({
128
129
  wlTitle: '欢迎使用设置搜索',
129
130
  wlSubtitle: '为 DSH 设置面板提供即时搜索与导航,几秒钟就能找到任何设置。',
130
131
  wlFeature1Title: '实时搜索',
131
- wlFeature1Desc: '支持经典侧栏、窗口顶栏、弹出搜索页与顶栏加搜索页混合模式;输入关键词即可过滤设置。',
132
+ wlFeature1Desc: '支持经典侧栏、窗口顶栏与弹出搜索页;输入关键词即可过滤设置。',
132
133
  wlFeature2Title: '拼音与意图联想',
133
134
  wlFeature2Desc: '输入拼音首字母(如 sz)或直接描述需求(如「太亮」),也能联想设置项。',
134
135
  wlFeature3Title: 'AI 联想(可选)',
@@ -151,8 +152,9 @@ window.__ModuleLoader__.load({
151
152
  styleClassic: 'Classic sidebar',
152
153
  styleTop: 'Top bar',
153
154
  stylePage: 'Popup page',
154
- styleMixed: 'Top bar + popup',
155
155
  openSearch: 'Open search',
156
+ shortcutTitle: 'Ctrl+F / ⌘F shortcut',
157
+ shortcutHint: 'Focus the search box with Ctrl+F or ⌘F inside the settings panel; turn it off to keep the browser default.',
156
158
  warming: 'Importing section options…',
157
159
  empty: 'No matching settings',
158
160
  pages: 'Settings pages',
@@ -238,7 +240,7 @@ window.__ModuleLoader__.load({
238
240
  wlTitle: 'Welcome to Settings Search',
239
241
  wlSubtitle: 'Instant search and navigation for the DSH settings panel — find any setting in seconds.',
240
242
  wlFeature1Title: 'Instant search',
241
- wlFeature1Desc: 'Choose classic sidebar, top bar, popup page, or mixed top bar plus popup search; typing filters settings.',
243
+ wlFeature1Desc: 'Choose classic sidebar, top bar, or popup search page; typing filters settings.',
242
244
  wlFeature2Title: 'Pinyin & intent hints',
243
245
  wlFeature2Desc: 'Type pinyin initials (e.g. sz) or describe what you want to do to find settings.',
244
246
  wlFeature3Title: 'AI hints (optional)',
@@ -514,7 +516,9 @@ window.__ModuleLoader__.load({
514
516
  const WELCOME_EVENT = 'dsh-settings-search:welcome'
515
517
  const SEARCH_STYLE_KEY = 'dsh-settings-search:style'
516
518
  const SEARCH_STYLE_EVENT = 'dsh-settings-search:style-change'
517
- const SEARCH_STYLES = ['sidebar', 'top', 'page', 'mixed']
519
+ const SEARCH_SHORTCUT_KEY = 'dsh-settings-search:shortcut-enabled'
520
+ const SEARCH_SHORTCUT_EVENT = 'dsh-settings-search:shortcut-change'
521
+ const SEARCH_STYLES = ['sidebar', 'top', 'page']
518
522
  const readSearchStyle = () => {
519
523
  if (typeof localStorage === 'undefined') return 'sidebar'
520
524
  const style = localStorage.getItem(SEARCH_STYLE_KEY)
@@ -525,6 +529,15 @@ window.__ModuleLoader__.load({
525
529
  localStorage.setItem(SEARCH_STYLE_KEY, style)
526
530
  window.dispatchEvent(new CustomEvent(SEARCH_STYLE_EVENT, { detail: style }))
527
531
  }
532
+ const searchShortcutEnabled = () => {
533
+ if (typeof localStorage === 'undefined') return true
534
+ return localStorage.getItem(SEARCH_SHORTCUT_KEY) !== '0'
535
+ }
536
+ const setSearchShortcutEnabled = (enabled) => {
537
+ if (typeof localStorage === 'undefined') return
538
+ localStorage.setItem(SEARCH_SHORTCUT_KEY, enabled ? '1' : '0')
539
+ window.dispatchEvent(new CustomEvent(SEARCH_SHORTCUT_EVENT, { detail: enabled }))
540
+ }
528
541
  const welcomed = () => {
529
542
  if (typeof localStorage === 'undefined') return true
530
543
  return localStorage.getItem(ONBOARD_KEY) === '1'
@@ -630,6 +643,7 @@ window.__ModuleLoader__.load({
630
643
  method: 'POST',
631
644
  headers,
632
645
  signal,
646
+ redirect: 'error',
633
647
  body: JSON.stringify(body),
634
648
  })
635
649
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
@@ -716,6 +730,7 @@ window.__ModuleLoader__.load({
716
730
  const response = await fetch(requestUrl, {
717
731
  method: 'POST',
718
732
  headers,
733
+ redirect: 'error',
719
734
  body: JSON.stringify(body),
720
735
  })
721
736
  if (!response.ok) return { ok: false, code: testHttpCode(response.status), detail: `HTTP ${response.status}` }
@@ -795,7 +810,6 @@ window.__ModuleLoader__.load({
795
810
  .sss-page-results { flex: 1; min-height: 0; overflow: auto; padding: 8px; }
796
811
  .sss-page-results .sss-group { padding: 2px; }
797
812
  .sss-page-results .sss-item { padding: 10px 8px; border-radius: 8px; }
798
- .sss-mixed-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); font-size: 16px; line-height: 24px; }
799
813
  @keyframes sssFlashBg { 0% { background-color: var(--dsw-alias-interactive-bg-hover); } 100% { background-color: transparent; } }
800
814
  .sss-flash { animation: sssFlashBg 1.8s ease-out; border-radius: 8px; }
801
815
  .sss-vr { list-style: none; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-3); border-radius: 12px; max-width: 780px; width: 100%; box-sizing: border-box; transition: border-color .16s, background .16s; }
@@ -1225,9 +1239,16 @@ window.__ModuleLoader__.load({
1225
1239
 
1226
1240
  /* ---------- DOM-level navigation into the settings shell ---------- */
1227
1241
 
1228
- /** Root element that hosts the settings panel's nav buttons. */
1242
+ /**
1243
+ * Root element that hosts the settings panel's nav buttons. Derived from
1244
+ * the plugin's own mounted UI so an unrelated or plugin-owned dialog can
1245
+ * never become the click root; the first-dialog fallback only applies
1246
+ * when the plugin's anchors are not mounted.
1247
+ */
1229
1248
  const panelRoot = () => {
1230
1249
  if (typeof document === 'undefined') return null
1250
+ const anchor = document.querySelector('.sss-wrap, .sss-vr')
1251
+ if (anchor) return anchor.closest('[role="dialog"]') ?? document.body
1231
1252
  return document.querySelector('[role="dialog"]') ?? document.body
1232
1253
  }
1233
1254
  const panelButtons = () => {
@@ -1237,7 +1258,7 @@ window.__ModuleLoader__.load({
1237
1258
  const clickNavButton = (wantText) => {
1238
1259
  const want = String(wantText ?? '').trim()
1239
1260
  if (want === '') return false
1240
- const buttons = panelButtons()
1261
+ const buttons = panelButtons().filter((b) => /_navCell/.test(String(b.className || '')))
1241
1262
  const textOf = (b) => (b.textContent ?? '').trim()
1242
1263
  const hit = buttons.find((b) => textOf(b) === want)
1243
1264
  if (!hit) return false
@@ -1477,6 +1498,7 @@ window.__ModuleLoader__.load({
1477
1498
  const [open, setOpen] = React.useState(false)
1478
1499
  const [searchStyle, setSearchStyleState] = React.useState(readSearchStyle)
1479
1500
  const [pageOpen, setPageOpen] = React.useState(false)
1501
+ const [shortcutEnabled, setShortcutEnabled] = React.useState(searchShortcutEnabled)
1480
1502
  const [showWelcome, setShowWelcome] = React.useState(() => !welcomed())
1481
1503
  const [tick, setTick] = React.useState(0)
1482
1504
  const [warming, setWarming] = React.useState(false)
@@ -1555,6 +1577,21 @@ window.__ModuleLoader__.load({
1555
1577
  return () => window.removeEventListener(SEARCH_STYLE_EVENT, sync)
1556
1578
  }, [])
1557
1579
 
1580
+ // A style switch must not carry an open popup from the previous layout
1581
+ // into the new one. The host can keep this component mounted while the
1582
+ // settings choice changes, so reset transient view state explicitly.
1583
+ React.useEffect(() => {
1584
+ setOpen(false)
1585
+ setPageOpen(false)
1586
+ setHint(null)
1587
+ }, [searchStyle])
1588
+
1589
+ React.useEffect(() => {
1590
+ const sync = () => setShortcutEnabled(searchShortcutEnabled())
1591
+ window.addEventListener(SEARCH_SHORTCUT_EVENT, sync)
1592
+ return () => window.removeEventListener(SEARCH_SHORTCUT_EVENT, sync)
1593
+ }, [])
1594
+
1558
1595
  React.useEffect(() => {
1559
1596
  const sync = () => setAiEnabled(aiSearchEnabled())
1560
1597
  const onConfig = () => forceTick()
@@ -1670,8 +1707,7 @@ window.__ModuleLoader__.load({
1670
1707
  const popRef = React.useRef(null)
1671
1708
  const inputRef = React.useRef(null)
1672
1709
  const pageMode = searchStyle === 'page'
1673
- const mixedMode = searchStyle === 'mixed'
1674
- const topMode = searchStyle === 'top' || mixedMode
1710
+ const topMode = searchStyle === 'top'
1675
1711
  const activeIndex = Math.min(active, Math.max(results.length - 1, 0))
1676
1712
 
1677
1713
  React.useEffect(() => { setActive(0) }, [query])
@@ -1680,6 +1716,23 @@ window.__ModuleLoader__.load({
1680
1716
  const timer = setTimeout(() => inputRef.current?.focus(), 0)
1681
1717
  return () => clearTimeout(timer)
1682
1718
  }, [pageMode, pageOpen])
1719
+ React.useEffect(() => {
1720
+ if (!shortcutEnabled) return
1721
+ const onKeyDown = (event) => {
1722
+ if (!event.ctrlKey && !event.metaKey) return
1723
+ if (event.altKey || event.shiftKey || event.key !== 'f' && event.key !== 'F') return
1724
+ event.preventDefault()
1725
+ if (pageMode) {
1726
+ setPageOpen(true)
1727
+ return
1728
+ }
1729
+ setHint(null)
1730
+ setOpen(true)
1731
+ inputRef.current?.focus()
1732
+ }
1733
+ window.addEventListener('keydown', onKeyDown)
1734
+ return () => window.removeEventListener('keydown', onKeyDown)
1735
+ }, [pageMode, shortcutEnabled])
1683
1736
  React.useEffect(() => {
1684
1737
  const el = popRef.current?.querySelector('.sss-item.is-active')
1685
1738
  el?.scrollIntoView?.({ block: 'nearest' })
@@ -1812,12 +1865,12 @@ window.__ModuleLoader__.load({
1812
1865
  const item = results[activeIndex]
1813
1866
  if (item) { e.preventDefault(); pick(item) }
1814
1867
  } else if (e.key === 'Escape') {
1815
- if (pageMode || mixedMode) setPageOpen(false)
1868
+ if (pageMode) setPageOpen(false)
1816
1869
  else setOpen(false)
1817
1870
  }
1818
1871
  },
1819
- onChange: (e) => { setQuery(e.target.value); setHint(null); setOpen(true); if (mixedMode) setPageOpen(true) },
1820
- onFocus: () => { setOpen(true); if (mixedMode) setPageOpen(true) },
1872
+ onChange: (e) => { setQuery(e.target.value); setHint(null); setOpen(true) },
1873
+ onFocus: () => setOpen(true),
1821
1874
  onBlur: () => setOpen(false),
1822
1875
  })
1823
1876
  const clearButton = query !== '' ? React.createElement('button', {
@@ -1912,44 +1965,11 @@ window.__ModuleLoader__.load({
1912
1965
  clearButton,
1913
1966
  ]),
1914
1967
  warming ? React.createElement('p', { className: 'sss-warm-note' }, t('warming')) : null,
1915
- !mixedMode && open && q !== '' ? React.createElement('div', { className: 'sss-pop', ref: popRef }, [resultsNode]) : null,
1916
- !mixedMode && hint !== null
1968
+ open && q !== '' ? React.createElement('div', { className: 'sss-pop', ref: popRef }, [resultsNode]) : null,
1969
+ hint !== null
1917
1970
  ? React.createElement('div', { className: 'sss-guide' }, t('jumpFail').replace('{path}', hint))
1918
1971
  : null,
1919
1972
  ]),
1920
- mixedMode && pageOpen
1921
- ? React.createElement('div', {
1922
- className: 'sss-page-overlay',
1923
- onMouseDown: (event) => { if (event.target === event.currentTarget) setPageOpen(false) },
1924
- }, [
1925
- React.createElement('div', { className: 'sss-page' }, [
1926
- React.createElement('div', { className: 'sss-page-head' }, [
1927
- React.createElement('button', {
1928
- className: 'sss-icon-btn',
1929
- type: 'button',
1930
- 'aria-label': t('searchBtn'),
1931
- title: t('searchBtn'),
1932
- onClick: () => inputRef.current?.focus(),
1933
- }, [SearchIcon]),
1934
- React.createElement('span', { className: 'sss-page-input sss-mixed-title' }, t('pageSearchTitle')),
1935
- clearButton,
1936
- React.createElement('button', {
1937
- className: 'sss-page-close',
1938
- type: 'button',
1939
- 'aria-label': 'Close',
1940
- onClick: () => setPageOpen(false),
1941
- }, '✕'),
1942
- ]),
1943
- React.createElement('div', { className: 'sss-page-results' }, [
1944
- warming ? React.createElement('p', { className: 'sss-warm-note' }, t('warming')) : null,
1945
- q !== '' ? resultsNode : null,
1946
- hint !== null
1947
- ? React.createElement('div', { className: 'sss-guide' }, t('jumpFail').replace('{path}', hint))
1948
- : null,
1949
- ]),
1950
- ]),
1951
- ])
1952
- : null,
1953
1973
  ])
1954
1974
  }
1955
1975
 
@@ -1998,6 +2018,7 @@ window.__ModuleLoader__.load({
1998
2018
  function UpdatePanel() {
1999
2019
  const [open, setOpen] = React.useState(true)
2000
2020
  const [searchStyle, setSearchStyleState] = React.useState(readSearchStyle)
2021
+ const [shortcutOn, setShortcutOn] = React.useState(searchShortcutEnabled)
2001
2022
  const [state, setState] = React.useState('idle') // idle | loading | newest | outdated | error
2002
2023
  const [latest, setLatest] = React.useState('')
2003
2024
  const [copied, setCopied] = React.useState(false)
@@ -2035,6 +2056,12 @@ window.__ModuleLoader__.load({
2035
2056
  return () => window.removeEventListener(SEARCH_STYLE_EVENT, sync)
2036
2057
  }, [])
2037
2058
 
2059
+ React.useEffect(() => {
2060
+ const sync = () => setShortcutOn(searchShortcutEnabled())
2061
+ window.addEventListener(SEARCH_SHORTCUT_EVENT, sync)
2062
+ return () => window.removeEventListener(SEARCH_SHORTCUT_EVENT, sync)
2063
+ }, [])
2064
+
2038
2065
  useAutoReset(copied, () => { setCopied(false); setCopyFailed(false) })
2039
2066
  useAutoReset(aiSaved, () => setAiSaved(false))
2040
2067
 
@@ -2086,6 +2113,12 @@ window.__ModuleLoader__.load({
2086
2113
  setSearchStyleState(style)
2087
2114
  }
2088
2115
 
2116
+ const updateShortcutEnabled = (enabled) => {
2117
+ setSearchShortcutEnabled(enabled)
2118
+ setShortcutOn(enabled)
2119
+ log('info', enabled ? '启用搜索快捷键' : '禁用搜索快捷键', {})
2120
+ }
2121
+
2089
2122
  const requestClearAi = () => setConfirmClearAi(true)
2090
2123
  const cancelClearAi = () => setConfirmClearAi(false)
2091
2124
  const clearAiConfig = () => {
@@ -2268,11 +2301,6 @@ window.__ModuleLoader__.load({
2268
2301
  type: 'button',
2269
2302
  onClick: () => updateSearchStyle('page'),
2270
2303
  }, t('stylePage')),
2271
- React.createElement('button', {
2272
- className: 'sss-vr-seg' + (searchStyle === 'mixed' ? ' is-active' : ''),
2273
- type: 'button',
2274
- onClick: () => updateSearchStyle('mixed'),
2275
- }, t('styleMixed')),
2276
2304
  ]),
2277
2305
  ]),
2278
2306
  React.createElement('div', { className: 'sss-vr-field' }, [
@@ -2292,6 +2320,18 @@ window.__ModuleLoader__.load({
2292
2320
  React.createElement('p', { className: 'sss-vr-hint' }, t('aiDesc')),
2293
2321
  React.createElement('p', { className: 'sss-vr-hint' }, t('aiPrivacy')),
2294
2322
  ]),
2323
+ React.createElement('div', { className: 'sss-vr-field' }, [
2324
+ React.createElement('div', { className: 'sss-vr-field-head' }, [
2325
+ React.createElement('span', { className: 'sss-vr-label' }, t('shortcutTitle')),
2326
+ React.createElement('input', {
2327
+ className: 'sss-vr-check',
2328
+ type: 'checkbox',
2329
+ checked: shortcutOn,
2330
+ onChange: (event) => updateShortcutEnabled(event.target.checked),
2331
+ }),
2332
+ ]),
2333
+ React.createElement('p', { className: 'sss-vr-hint' }, t('shortcutHint')),
2334
+ ]),
2295
2335
  React.createElement('div', { className: 'sss-vr-field sss-vr-form' }, [
2296
2336
  React.createElement('div', { className: 'sss-vr-field-head' }, [
2297
2337
  React.createElement('label', { className: 'sss-vr-label' }, t('aiProvider')),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectivex666/dsh-settings-search",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "DSH settings panel search plugin – instantly filter setting sections and general items with navigation guidance. Installable via `dsh plugin add`.",
5
5
  "keywords": [
6
6
  "dsh",