@objectivex666/dsh-settings-search 1.1.1 → 1.2.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
@@ -13,7 +13,9 @@
13
13
  ## Features
14
14
 
15
15
  - 🔍 **Live search** — a search box at the top of the settings panel's left navigation filters every registered settings page (`settings.section`) and general item (`settings.general.item`) as you type.
16
- - 🧭 **Navigation guidance** — clicking a result shows which left-navigation entry owns that setting, so you can jump straight to it.
16
+ - 📂 **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.
17
+ - 🧭 **Click-to-jump** — selecting a result clicks the matching left-nav entry to open its section; for tab options it also opens the tab and flashes the target row. A manual-path hint appears if automatic navigation is not possible.
18
+ - 🌱 **Progressive indexing** — self-drawing settings rows are harvested from the rendered DOM (via their `[data-slot]` anchors) the first time their page is visited, then stay searchable.
17
19
  - 🌐 **i18n** — built-in Chinese/English dictionaries that follow the DSH localization system and are easy to extend.
18
20
  - 🎨 **Theme-aware** — styled entirely with CSS variables, automatically following the DSH theme (light/dark).
19
21
  - ⚡ **Reactive** — subscribes to settings entries being added, removed, or changed, and refreshes results automatically.
package/README.md CHANGED
@@ -13,7 +13,9 @@
13
13
  ## 功能特性
14
14
 
15
15
  - 🔍 **实时搜索**:在设置面板左侧导航顶部嵌入搜索框,输入关键词立即过滤所有已注册的设置页(`settings.section`)与通用设置项(`settings.general.item`)。
16
- - 🧭 **导航引导**:点击搜索结果后,显示该设置所在导航项的标签,帮助用户快速定位。
16
+ - 📂 **页内选项搜索(v1.2.0)**:不只搜页面,还能直达每个设置页内部的具体选项——插件页的标签页(`settings.plugins.tab`)、Web UI 插件的卡片(`web-ui.plugin.item`)、通用设置行等,结果以「页面 › 选项」面包屑展示。
17
+ - 🧭 **点击即达**:选中结果后自动点击对应的左侧导航项打开所在分区;若是页内标签页会继续点开对应标签,并高亮闪烁目标选项行。无法自动跳转时显示手动路径提示。
18
+ - 🌱 **渐进索引**:自绘界面的设置行会在其所在页面首次被访问时从渲染 DOM 中收割标题并加入索引(此后始终可搜)。
17
19
  - 🌐 **多语言支持**:内置中英文国际化,可跟随 DSH 本地化系统扩展。
18
20
  - 🎨 **主题适配**:完全使用 CSS 变量,自动跟随 DSH 主题(亮/暗)。
19
21
  - ⚡ **动态响应**:实时订阅设置项的新增/删除/变更,搜索结果自动刷新。
package/lib/client.js CHANGED
@@ -7,11 +7,18 @@
7
7
  * and the client kernel adopts the exported `{ apply, inject }` as the
8
8
  * plugin's browser half.
9
9
  *
10
- * Behavior (identical to the dynamic-plugin source, `dsh-settings-search.js`):
11
- * embed a live search box at the top of the settings panel's left navigation
12
- * (`settings.header`, priority -1), filtering every registered settings page
13
- * (`settings.section`) and general item (`settings.general.item`), with a
14
- * navigation guide on selection.
10
+ * Behavior:
11
+ * - embeds a live search box at the top of the settings panel's left
12
+ * navigation (`settings.header`, priority -1);
13
+ * - indexes every settings PAGE (`settings.section`);
14
+ * - deep-indexes specific OPTIONS inside pages from machine-readable inner
15
+ * slots (`settings.plugins.tab`, `web-ui.plugin.item`,
16
+ * `settings.general.item`) — occupants that draw their own labels are read
17
+ * back from the rendered DOM via their `[data-slot]` anchors;
18
+ * - results show a breadcrumb path (页面 › 选项);
19
+ * - selecting a result really navigates: clicks the matching left-nav button
20
+ * (then the matching tab button), flashes the target row, and falls back to
21
+ * a breadcrumb hint when no DOM target exists.
15
22
  */
16
23
  window.__ModuleLoader__.load({
17
24
  id: '@objectivex666/dsh-settings-search',
@@ -21,28 +28,35 @@ window.__ModuleLoader__.load({
21
28
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
22
29
  const React = require('react')
23
30
 
24
- const NS = 'settings-search-v4'
31
+ const NS = 'settings-search-v5'
25
32
  const zh = {
26
33
  title: '设置',
27
34
  placeholder: '搜索设置…',
28
35
  empty: '没有匹配的设置',
29
36
  pages: '设置页',
30
- rows: '通用设置项',
31
- guide: '该设置位于左侧导航「{label}」,点击左侧导航项即可打开',
32
- page: '设置页',
33
- row: '设置项',
37
+ options: '页内选项',
38
+ page: '页面',
39
+ option: '选项',
40
+ jumpFail: '未能自动跳转,请手动打开:{path}',
34
41
  }
35
42
  const en = {
36
43
  title: 'Settings',
37
44
  placeholder: 'Search settings…',
38
45
  empty: 'No matching settings',
39
46
  pages: 'Settings pages',
40
- rows: 'General items',
41
- guide: 'This setting lives in "{label}" — open it from the left navigation',
47
+ options: 'In-page options',
42
48
  page: 'Page',
43
- row: 'Item',
49
+ option: 'Option',
50
+ jumpFail: 'Could not navigate automatically — open it here: {path}',
44
51
  }
45
52
 
53
+ /** Inner-row slots that represent specific options inside one page. */
54
+ const DEEP_SLOTS = [
55
+ { slot: 'settings.plugins.tab', parent: 'plugins' },
56
+ { slot: 'web-ui.plugin.item', parent: 'web-ui-plugins' },
57
+ { slot: 'settings.general.item', parent: 'general' },
58
+ ]
59
+
46
60
  const CSS = `
47
61
  .sss-wrap { position: relative; display: flex; flex-direction: column; gap: 8px; }
48
62
  .sss-title { font-size: 16px; font-weight: 600; line-height: 24px; color: var(--dsw-alias-label-primary); }
@@ -52,20 +66,24 @@ window.__ModuleLoader__.load({
52
66
  .sss-input::placeholder { color: var(--dsw-alias-label-dimmed); }
53
67
  .sss-clear { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; padding: 0; border: none; border-radius: 4px; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1; cursor: pointer; }
54
68
  .sss-clear:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
55
- .sss-pop { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 20; background: var(--dsw-alias-bg-overlay); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; box-shadow: var(--dsw-shadow-lv3); padding: 6px; max-height: 300px; overflow: auto; }
69
+ .sss-pop { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 20; background: var(--dsw-alias-bg-overlay); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; box-shadow: var(--dsw-shadow-lv3); padding: 6px; max-height: 320px; overflow: auto; }
56
70
  .sss-groups { display: flex; flex-direction: column; gap: 8px; }
57
71
  .sss-group-title { font-size: 11px; color: var(--dsw-alias-label-secondary); letter-spacing: .05em; padding: 2px 6px; }
58
72
  .sss-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
59
- .sss-item { display: flex; align-items: center; gap: 6px; padding: 7px 8px; border-radius: 7px; cursor: pointer; }
73
+ .sss-item { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: 7px; cursor: pointer; }
60
74
  .sss-item:hover { background: var(--dsw-alias-interactive-bg-hover); }
61
- .sss-item-label { flex: 1; min-width: 0; color: var(--dsw-alias-label-primary); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
62
- .sss-item-id { font-size: 10px; color: var(--dsw-alias-label-secondary); flex-shrink: 0; }
75
+ .sss-item-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
76
+ .sss-item-label { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 18px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
77
+ .sss-item-path { font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
78
+ .sss-item-kind { flex-shrink: 0; font-size: 10px; color: var(--dsw-alias-label-secondary); border: 1px solid var(--dsw-alias-border-l2); border-radius: 4px; padding: 1px 5px; }
63
79
  .sss-empty { margin: 0; padding: 10px 8px; color: var(--dsw-alias-label-secondary); font-size: 13px; }
64
80
  .sss-guide { padding: 7px 10px; border-radius: 8px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 18px; }
81
+ @keyframes sssFlashBg { 0% { background-color: var(--dsw-alias-interactive-bg-hover); } 100% { background-color: transparent; } }
82
+ .sss-flash { animation: sssFlashBg 1.8s ease-out; border-radius: 8px; }
65
83
  `
66
84
  let stylesInstalled = false
67
85
  function installStyles() {
68
- if (stylesInstalled || typeof document === 'undefined') return
86
+ if (stylesInstalled || typeof document === 'undefined') return undefined
69
87
  stylesInstalled = true
70
88
  const tag = document.createElement('style')
71
89
  tag.dataset.plugin = '@objectivex666/dsh-settings-search'
@@ -78,25 +96,195 @@ window.__ModuleLoader__.load({
78
96
  }
79
97
  }
80
98
 
81
- const resolveLabel = (entry) => {
82
- const raw = entry.options.label
83
- if (typeof raw === 'function') return String(raw())
84
- return raw == null ? '' : String(raw)
99
+ const callSafe = (fn) => {
100
+ try { return fn() } catch { return undefined }
101
+ }
102
+
103
+ /** Resolve a registrant-provided label option (string or thunk). */
104
+ const optionLabel = (raw) => {
105
+ const value = typeof raw === 'function' ? callSafe(raw) : raw
106
+ return typeof value === 'string' ? value : ''
107
+ }
108
+
109
+ function createModel(ctx) {
110
+ /** id → harvested visible title, per slot. */
111
+ const harvest = new Map()
112
+ let harvestSeq = 0
113
+
114
+ const entriesOf = (slot) => {
115
+ try { return ctx.slots.entries(slot) ?? [] } catch { return [] }
116
+ }
117
+ const sortEntries = (list) => list.slice().sort((a, b) => ((a.options?.order ?? 0) - (b.options?.order ?? 0)))
118
+
119
+ /** Best-effort static label from occupant options/locale namespace. */
120
+ const staticLabel = (entry, slotConfig) => {
121
+ const o = entry.options ?? {}
122
+ const explicit = optionLabel(o.label)
123
+ if (explicit !== '') return explicit
124
+ if (typeof o.locale === 'string') {
125
+ const t = callSafe(() => ctx.locale.bind(o.locale))
126
+ if (typeof t === 'function') {
127
+ for (const key of ['tab', 'title', 'card.title', 'name']) {
128
+ const value = callSafe(() => t(key))
129
+ if (typeof value === 'string' && value !== '' && value !== key) return value
130
+ }
131
+ }
132
+ }
133
+ void slotConfig
134
+ return ''
135
+ }
136
+
137
+ /**
138
+ * Read visible titles for self-drawing occupants out of the live DOM:
139
+ * the shell marks every rendered inner-slot occupant with
140
+ * `[data-slot="<name>"]`; when DOM order matches entry order we can
141
+ * pair each rendered card with its registration id.
142
+ */
143
+ const runHarvest = () => {
144
+ let grew = false
145
+ if (typeof document === 'undefined') return false
146
+ for (const cfg of DEEP_SLOTS) {
147
+ let nodes = []
148
+ try { nodes = [...document.querySelectorAll(`[data-slot="${cfg.slot}"]`)] } catch { continue }
149
+ const regs = sortEntries(entriesOf(cfg.slot))
150
+ if (nodes.length === 0 || nodes.length !== regs.length) continue
151
+ let map = harvest.get(cfg.slot)
152
+ if (map === undefined) { map = new Map(); harvest.set(cfg.slot, map) }
153
+ regs.forEach((entry, i) => {
154
+ const id = String(entry.options?.id ?? i)
155
+ if (map.has(id)) return
156
+ const title = titleFromNode(nodes[i])
157
+ if (title !== '') { map.set(id, title); grew = true }
158
+ })
159
+ }
160
+ if (grew) harvestSeq += 1
161
+ return grew
162
+ }
163
+
164
+ const titleFromNode = (node) => {
165
+ const pick = (el) => {
166
+ const text = (el.textContent ?? '').replace(/\s+/g, ' ').trim()
167
+ return text !== '' && text.length <= 60 ? text : ''
168
+ }
169
+ for (const sel of ['h1,h2,h3,h4,h5', '[class*="title"]', '[class*="heading"]', 'label', 'p']) {
170
+ const hit = node.querySelector(sel)
171
+ if (hit) { const text = pick(hit); if (text !== '') return text }
172
+ }
173
+ const first = node.firstElementChild
174
+ if (first) {
175
+ for (const child of first.children) { const text = pick(child); if (text !== '') return text }
176
+ }
177
+ return pick(node).split('·')[0].trim()
178
+ }
179
+
180
+ const readSections = () => sortEntries(entriesOf('settings.section'))
181
+ .filter((e) => e.options?.id !== 'settings-search')
182
+ .map((e) => ({
183
+ kind: 'page',
184
+ id: String(e.options?.id ?? ''),
185
+ order: e.options?.order ?? 0,
186
+ label: optionLabel(e.options?.label),
187
+ }))
188
+ .filter((e) => e.id !== '' && e.label !== '')
189
+
190
+ const readDeep = () => {
191
+ const sections = readSections()
192
+ const parentLabel = (parentId) =>
193
+ sections.find((s) => s.id === parentId)?.label ?? parentId
194
+ const out = []
195
+ for (const cfg of DEEP_SLOTS) {
196
+ const mapped = harvest.get(cfg.slot)
197
+ for (const entry of sortEntries(entriesOf(cfg.slot))) {
198
+ const id = String(entry.options?.id ?? '')
199
+ if (id === '') continue
200
+ const label = staticLabel(entry, cfg)
201
+ || (mapped ? mapped.get(id) ?? '' : '')
202
+ if (label === '') continue // unmapped, unharvested — invisible until it renders once
203
+ if (out.some((e) => e.key === cfg.slot + '#' + id)) continue
204
+ out.push({
205
+ kind: 'option',
206
+ key: cfg.slot + '#' + id,
207
+ slot: cfg.slot,
208
+ id,
209
+ order: entry.options?.order ?? 0,
210
+ label,
211
+ parentId: cfg.parent,
212
+ parentLabel: parentLabel(cfg.parent),
213
+ })
214
+ }
215
+ }
216
+ return out.sort((a, b) => a.parentLabel !== b.parentLabel
217
+ ? a.parentLabel.localeCompare(b.parentLabel)
218
+ : a.order - b.order)
219
+ }
220
+
221
+ return { readSections, readDeep, runHarvest, bump: () => harvestSeq }
222
+ }
223
+
224
+ /* ---------- DOM-level navigation into the settings shell ---------- */
225
+
226
+ const panelButtons = () => {
227
+ if (typeof document === 'undefined') return []
228
+ const root = document.querySelector('[role="dialog"]') ?? document.body
229
+ return [...root.querySelectorAll('button')]
230
+ }
231
+ const clickNavButton = (wantText) => {
232
+ const want = String(wantText ?? '').trim()
233
+ if (want === '') return false
234
+ const buttons = panelButtons()
235
+ const hit = buttons.find((b) => (b.textContent ?? '').trim() === want)
236
+ ?? buttons.find((b) => (b.textContent ?? '').trim().startsWith(want))
237
+ if (!hit) return false
238
+ hit.click()
239
+ return true
240
+ }
241
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
242
+
243
+ function createNavigator(ctx) {
244
+ /** Jump to a page result; falls back to showing a manual hint. */
245
+ const jumpToPage = async (item) => {
246
+ const ok = await Promise.resolve(clickNavButton(item.label))
247
+ return ok
248
+ }
249
+ /** Jump to an in-page option: open its page, select its tab, flash its row. */
250
+ const jumpToOption = async (item) => {
251
+ const opened = await Promise.resolve(clickNavButton(item.parentLabel))
252
+ await delay(160)
253
+ const label = item.label
254
+ const buttons = panelButtons()
255
+ const tab = buttons.find((b) => (b.textContent ?? '').trim() === label)
256
+ ?? buttons.find((b) => (b.getAttribute('role') ?? '') === 'tab' && (b.textContent ?? '').includes(label))
257
+ if (tab) { tab.click(); await delay(120) }
258
+ flashOptionRow(item)
259
+ return opened
260
+ }
261
+ /** Highlight the rendered row card for one indexed option. */
262
+ const flashOptionRow = (item) => {
263
+ try {
264
+ const nodes = [...document.querySelectorAll(`[data-slot="${item.slot}"]`)]
265
+ const target = nodes.find((n) => (n.textContent ?? '').includes(item.label)) ?? nodes[0]
266
+ if (!target) return
267
+ target.scrollIntoView({ block: 'nearest' })
268
+ target.classList.remove('sss-flash')
269
+ void target.offsetWidth // restart animation if re-flashed
270
+ target.classList.add('sss-flash')
271
+ setTimeout(() => target.classList.remove('sss-flash'), 1900)
272
+ } catch { /* visual nicety only */ }
273
+ }
274
+ void ctx
275
+ return { jumpToPage, jumpToOption }
85
276
  }
86
277
 
87
278
  function apply(ctx) {
88
279
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'settings-search: dictionaries')
89
280
  const t = ctx.locale.bind(NS)
90
- const removeStyles = installStyles()
91
- if (typeof removeStyles === 'function') ctx.effect(removeStyles, 'settings-search: styles')
281
+ ctx.effect(() => {
282
+ const remove = installStyles()
283
+ return () => { if (typeof remove === 'function') remove() }
284
+ }, 'settings-search: styles')
92
285
 
93
- const readSections = () => ctx.slots.entries('settings.section')
94
- .filter((e) => e.options.id !== 'settings-search')
95
- .map((e) => ({ id: e.options.id, order: e.options.order ?? 0, label: resolveLabel(e) }))
96
- .sort((a, b) => a.order - b.order)
97
- const readRows = () => ctx.slots.entries('settings.general.item')
98
- .map((e) => ({ id: e.options.id, order: e.options.order ?? 0, label: resolveLabel(e) }))
99
- .sort((a, b) => a.order - b.order)
286
+ const model = createModel(ctx)
287
+ const navigator = createNavigator(ctx)
100
288
 
101
289
  const SearchIcon = React.createElement('svg', {
102
290
  className: 'sss-icon',
@@ -119,36 +307,74 @@ window.__ModuleLoader__.load({
119
307
  function SettingsHeaderSearch() {
120
308
  const [query, setQuery] = React.useState('')
121
309
  const [open, setOpen] = React.useState(false)
122
- const [sections, setSections] = React.useState(readSections)
123
- const [rows, setRows] = React.useState(readRows)
124
- const [selected, setSelected] = React.useState(null)
310
+ const [tick, setTick] = React.useState(0)
311
+ const [, forceTick] = React.useReducer((x) => x + 1, 0)
312
+ void tick
313
+
125
314
  React.useEffect(() => {
126
- const refresh = () => {
127
- setSections(readSections())
128
- setRows(readRows())
129
- }
315
+ const watched = ['settings.section', ...DEEP_SLOTS.map((c) => c.slot)]
130
316
  const offs = [
131
- ctx.slots.subscribe('settings.section', refresh),
132
- ctx.slots.subscribe('settings.general.item', refresh),
133
- ctx.locale.subscribe(refresh),
317
+ ...watched.map((slot) => ctx.slots.subscribe(slot, forceTick)),
318
+ ctx.locale.subscribe(forceTick),
134
319
  ]
135
- return () => { for (const off of offs) off() }
320
+ // Grow the index whenever the panel body re-renders (visits new pages).
321
+ const observer = typeof MutationObserver === 'function'
322
+ ? new MutationObserver(() => {
323
+ if (model.runHarvest()) setTick((x) => x + 1)
324
+ })
325
+ : null
326
+ observer?.observe(document.body, { childList: true, subtree: true })
327
+ const warm = setTimeout(() => { model.runHarvest(); setTick((x) => x + 1) }, 800)
328
+ return () => {
329
+ for (const off of offs) off()
330
+ observer?.disconnect()
331
+ clearTimeout(warm)
332
+ }
136
333
  }, [])
137
334
 
138
335
  const q = query.trim().toLowerCase()
139
336
  const tokens = q.split(/\s+/).filter(Boolean)
140
- const match = (label, id) => tokens.every((tok) => (label + ' ' + id).toLowerCase().includes(tok))
141
- const shownSections = tokens.length ? sections.filter((s) => match(s.label, s.id)) : []
142
- const shownRows = tokens.length ? rows.filter((r) => match(r.label, r.id)) : []
143
- const none = tokens.length > 0 && shownSections.length === 0 && shownRows.length === 0
337
+ const matches = (haystack) => tokens.every((tok) => haystack.includes(tok))
338
+
339
+ const sections = model.readSections()
340
+ const deeps = q !== '' ? model.readDeep() : []
341
+ const shownPages = q !== ''
342
+ ? sections.filter((s) => matches(`${s.label} ${s.id}`.toLowerCase()))
343
+ : []
344
+ const shownOptions = q !== ''
345
+ ? deeps.filter((o) => matches(`${o.label} ${o.parentLabel} ${o.id} ${o.slot}`.toLowerCase()))
346
+ : []
347
+ const none = q !== '' && shownPages.length === 0 && shownOptions.length === 0
348
+
349
+ const [hint, setHint] = React.useState(null)
350
+ const pick = (item) => {
351
+ setOpen(false)
352
+ const jumped = item.kind === 'page'
353
+ ? navigator.jumpToPage(item)
354
+ : navigator.jumpToOption(item)
355
+ Promise.resolve(jumped).then((ok) => {
356
+ if (ok === false) setHint(item.kind === 'page' ? item.label : `${item.parentLabel} › ${item.label}`)
357
+ })
358
+ }
144
359
 
145
- const renderItem = (item, kind) => React.createElement('li', {
146
- key: item.id,
360
+ const renderPage = (item) => React.createElement('li', {
361
+ key: 'page:' + item.id,
147
362
  className: 'sss-item',
148
- onMouseDown: () => { setSelected(item); setOpen(false) },
363
+ onMouseDown: () => pick(item),
149
364
  }, [
150
- React.createElement('span', { className: 'sss-item-label' }, item.label || item.id),
151
- React.createElement('span', { className: 'sss-item-id' }, kind),
365
+ React.createElement('span', { className: 'sss-item-label' }, item.label),
366
+ React.createElement('span', { className: 'sss-item-kind' }, t('page')),
367
+ ])
368
+ const renderOption = (item) => React.createElement('li', {
369
+ key: item.key,
370
+ className: 'sss-item',
371
+ onMouseDown: () => pick(item),
372
+ }, [
373
+ React.createElement('span', { className: 'sss-item-col' }, [
374
+ React.createElement('span', { className: 'sss-item-label' }, item.label),
375
+ React.createElement('span', { className: 'sss-item-path' }, item.parentLabel),
376
+ ]),
377
+ React.createElement('span', { className: 'sss-item-kind' }, t('option')),
152
378
  ])
153
379
 
154
380
  return React.createElement('div', { className: 'sss-wrap' }, [
@@ -160,32 +386,32 @@ window.__ModuleLoader__.load({
160
386
  value: query,
161
387
  placeholder: t('placeholder'),
162
388
  spellCheck: false,
163
- onChange: (e) => { setQuery(e.target.value); setOpen(true) },
389
+ onChange: (e) => { setQuery(e.target.value); setHint(null); setOpen(true) },
164
390
  onFocus: () => setOpen(true),
165
391
  onBlur: () => setOpen(false),
166
392
  }),
167
393
  query !== '' ? React.createElement('button', {
168
394
  className: 'sss-clear',
169
395
  type: 'button',
170
- onClick: () => { setQuery(''); setSelected(null); setOpen(false) },
396
+ onClick: () => { setQuery(''); setHint(null); setOpen(false) },
171
397
  }, '✕') : null,
172
398
  ]),
173
- open && tokens.length > 0 ? React.createElement('div', { className: 'sss-pop' }, [
399
+ open && q !== '' ? React.createElement('div', { className: 'sss-pop' }, [
174
400
  none
175
401
  ? React.createElement('p', { className: 'sss-empty' }, t('empty'))
176
402
  : React.createElement('div', { className: 'sss-groups' }, [
177
- shownSections.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
403
+ shownPages.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
178
404
  React.createElement('div', { className: 'sss-group-title' }, t('pages')),
179
- React.createElement('ul', { className: 'sss-list' }, shownSections.map((s) => renderItem(s, t('page')))),
405
+ React.createElement('ul', { className: 'sss-list' }, shownPages.map(renderPage)),
180
406
  ]) : null,
181
- shownRows.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
182
- React.createElement('div', { className: 'sss-group-title' }, t('rows')),
183
- React.createElement('ul', { className: 'sss-list' }, shownRows.map((r) => renderItem(r, t('row')))),
407
+ shownOptions.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
408
+ React.createElement('div', { className: 'sss-group-title' }, t('options')),
409
+ React.createElement('ul', { className: 'sss-list' }, shownOptions.map(renderOption)),
184
410
  ]) : null,
185
411
  ]),
186
412
  ]) : null,
187
- selected !== null
188
- ? React.createElement('div', { className: 'sss-guide' }, t('guide').replace('{label}', selected.label || selected.id))
413
+ hint !== null
414
+ ? React.createElement('div', { className: 'sss-guide' }, t('jumpFail').replace('{path}', hint))
189
415
  : null,
190
416
  ])
191
417
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectivex666/dsh-settings-search",
3
- "version": "1.1.1",
3
+ "version": "1.2.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",