@objectivex666/dsh-settings-search 1.1.2 → 1.3.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,10 +96,230 @@ 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
+ * Two shell shapes exist for `[data-slot="<name>"]`:
140
+ * - per-occupant anchors: one element each, count === regs → pair ids;
141
+ * - a single WRAPPER around all rows (e.g. settings.general.item's
142
+ * stacked section): harvest each direct child as one row, keyed by
143
+ * render order ("#i") since id pairing is impossible.
144
+ */
145
+ const runHarvest = () => {
146
+ let grew = false
147
+ if (typeof document === 'undefined') return false
148
+ let map
149
+ for (const cfg of DEEP_SLOTS) {
150
+ let containers = []
151
+ try { containers = [...document.querySelectorAll(`[data-slot="${cfg.slot}"]`)] } catch { continue }
152
+ const regs = sortEntries(entriesOf(cfg.slot))
153
+ if (regs.length === 0) continue
154
+ let nodes = []
155
+ if (containers.length === 1 && containers[0].childElementCount > 0) {
156
+ nodes = [...containers[0].children]
157
+ } else {
158
+ nodes = containers.filter((n) => n.childElementCount >= 0)
159
+ }
160
+ if (nodes.length === 0) continue
161
+ map = harvest.get(cfg.slot)
162
+ if (map === undefined) { map = new Map(); harvest.set(cfg.slot, map) }
163
+ if (nodes.length !== regs.length) {
164
+ // wrapper shape: index-keyed loose harvest of every rendered row
165
+ nodes.forEach((node, i) => {
166
+ const key = '#' + i
167
+ if (map.has(key)) return
168
+ const title = titleFromNode(node)
169
+ if (title === '') return
170
+ const text = (node.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 160)
171
+ map.set(key, { title, idx: i, text }); grew = true
172
+ })
173
+ continue
174
+ }
175
+ regs.forEach((entry, i) => {
176
+ const id = String(entry.options?.id ?? i)
177
+ if (map.has(id)) return
178
+ const title = titleFromNode(nodes[i])
179
+ if (title === '') return
180
+ const text = (nodes[i].textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 160)
181
+ map.set(id, { title, idx: i, text }); grew = true
182
+ })
183
+ }
184
+ if (grew) harvestSeq += 1
185
+ return grew
186
+ }
187
+
188
+ const titleFromNode = (node) => {
189
+ const pick = (el) => {
190
+ const text = (el.textContent ?? '').replace(/\s+/g, ' ').trim()
191
+ return text !== '' && text.length <= 60 ? text : ''
192
+ }
193
+ for (const sel of ['h1,h2,h3,h4,h5', '[class*="title"]', '[class*="heading"]', 'label', 'p']) {
194
+ const hit = node.querySelector(sel)
195
+ if (hit) { const text = pick(hit); if (text !== '') return text }
196
+ }
197
+ const first = node.firstElementChild
198
+ if (first) {
199
+ for (const child of first.children) { const text = pick(child); if (text !== '') return text }
200
+ }
201
+ return pick(node).split('·')[0].trim()
202
+ }
203
+
204
+ const readSections = () => sortEntries(entriesOf('settings.section'))
205
+ .filter((e) => e.options?.id !== 'settings-search')
206
+ .map((e) => ({
207
+ kind: 'page',
208
+ id: String(e.options?.id ?? ''),
209
+ order: e.options?.order ?? 0,
210
+ label: optionLabel(e.options?.label),
211
+ }))
212
+ .filter((e) => e.id !== '' && e.label !== '')
213
+
214
+ const readDeep = () => {
215
+ const sections = readSections()
216
+ const parentLabel = (parentId) =>
217
+ sections.find((s) => s.id === parentId)?.label ?? parentId
218
+ const out = []
219
+ for (const cfg of DEEP_SLOTS) {
220
+ const mapped = harvest.get(cfg.slot)
221
+ sortEntries(entriesOf(cfg.slot)).forEach((entry, i) => {
222
+ const id = String(entry.options?.id ?? '')
223
+ if (id === '') return
224
+ // static label first, then id-paired harvest, then loose index-keyed harvest
225
+ const hitId = mapped?.get(id)
226
+ const hitIdx = mapped?.get('#' + i)
227
+ const hitObj = typeof hitId === 'object' ? hitId : (typeof hitIdx === 'object' ? hitIdx : undefined)
228
+ const label = staticLabel(entry, cfg) || hitObj?.title || ''
229
+ if (label === '') return // unmapped, unharvested — invisible until it renders once
230
+ const key = cfg.slot + '#' + id
231
+ if (out.some((e) => e.key === key)) return
232
+ out.push({
233
+ kind: 'option',
234
+ key,
235
+ slot: cfg.slot,
236
+ id,
237
+ order: entry.options?.order ?? 0,
238
+ label,
239
+ hay: label + ' ' + (hitObj?.text ?? ''),
240
+ rowIndex: hitObj?.idx ?? i,
241
+ parentId: cfg.parent,
242
+ parentLabel: parentLabel(cfg.parent),
243
+ })
244
+ })
245
+ }
246
+ return out.sort((a, b) => a.parentLabel !== b.parentLabel
247
+ ? a.parentLabel.localeCompare(b.parentLabel)
248
+ : a.order - b.order)
249
+ }
250
+
251
+ return { readSections, readDeep, runHarvest, bump: () => harvestSeq }
252
+ }
253
+
254
+ /* ---------- DOM-level navigation into the settings shell ---------- */
255
+
256
+ const panelButtons = () => {
257
+ if (typeof document === 'undefined') return []
258
+ const root = document.querySelector('[role="dialog"]') ?? document.body
259
+ return [...root.querySelectorAll('button')]
260
+ }
261
+ const clickNavButton = (wantText) => {
262
+ const want = String(wantText ?? '').trim()
263
+ if (want === '') return false
264
+ const buttons = panelButtons()
265
+ const textOf = (b) => (b.textContent ?? '').trim()
266
+ const hit = buttons.find((b) => textOf(b) === want)
267
+ ?? buttons.find((b) => textOf(b).startsWith(want))
268
+ ?? buttons.find((b) => want.length >= 2 && textOf(b) !== '' && (textOf(b).includes(want) || want.includes(textOf(b))))
269
+ if (!hit) return false
270
+ hit.click()
271
+ return true
272
+ }
273
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
274
+
275
+ function createNavigator(ctx) {
276
+ /** Jump to a page result; falls back to showing a manual hint. */
277
+ const jumpToPage = async (item) => {
278
+ const ok = await Promise.resolve(clickNavButton(item.label))
279
+ return ok
280
+ }
281
+ /** Jump to an in-page option: open its page, select its tab, flash its row. */
282
+ const jumpToOption = async (item) => {
283
+ const opened = await Promise.resolve(clickNavButton(item.parentLabel))
284
+ await delay(160)
285
+ const label = item.label
286
+ const buttons = panelButtons()
287
+ const tab = buttons.find((b) => (b.textContent ?? '').trim() === label)
288
+ ?? buttons.find((b) => (b.getAttribute('role') ?? '') === 'tab' && (b.textContent ?? '').includes(label))
289
+ if (tab) { tab.click(); await delay(120) }
290
+ flashOptionRow(item)
291
+ return opened
292
+ }
293
+ /** Highlight the rendered row card for one indexed option. */
294
+ const flashOptionRow = (item) => {
295
+ try {
296
+ let target = null
297
+ if (typeof item.rowIndex === 'number') {
298
+ // wrapper shape: pick the exact rendered row by index
299
+ const container = document.querySelector(`[data-slot="${item.slot}"]`)
300
+ target = container?.children?.[item.rowIndex] ?? null
301
+ if (target && !(target.textContent ?? '').includes(item.label)) {
302
+ // index drift — fall through to textual match below
303
+ target = null
304
+ }
305
+ }
306
+ if (!target) {
307
+ const nodes = [...document.querySelectorAll(`[data-slot="${item.slot}"]`)]
308
+ const rows = nodes.length === 1 && nodes[0].childElementCount > 0
309
+ ? [...nodes[0].children]
310
+ : nodes
311
+ target = rows.find((n) => (n.textContent ?? '').includes(item.label)) ?? null
312
+ }
313
+ if (!target) return
314
+ target.scrollIntoView({ block: 'nearest' })
315
+ target.classList.remove('sss-flash')
316
+ void target.offsetWidth // restart animation if re-flashed
317
+ target.classList.add('sss-flash')
318
+ setTimeout(() => target.classList.remove('sss-flash'), 1900)
319
+ } catch { /* visual nicety only */ }
320
+ }
321
+ void ctx
322
+ return { jumpToPage, jumpToOption }
85
323
  }
86
324
 
87
325
  function apply(ctx) {
@@ -92,13 +330,8 @@ window.__ModuleLoader__.load({
92
330
  return () => { if (typeof remove === 'function') remove() }
93
331
  }, 'settings-search: styles')
94
332
 
95
- const readSections = () => ctx.slots.entries('settings.section')
96
- .filter((e) => e.options.id !== 'settings-search')
97
- .map((e) => ({ id: e.options.id, order: e.options.order ?? 0, label: resolveLabel(e) }))
98
- .sort((a, b) => a.order - b.order)
99
- const readRows = () => ctx.slots.entries('settings.general.item')
100
- .map((e) => ({ id: e.options.id, order: e.options.order ?? 0, label: resolveLabel(e) }))
101
- .sort((a, b) => a.order - b.order)
333
+ const model = createModel(ctx)
334
+ const navigator = createNavigator(ctx)
102
335
 
103
336
  const SearchIcon = React.createElement('svg', {
104
337
  className: 'sss-icon',
@@ -121,36 +354,74 @@ window.__ModuleLoader__.load({
121
354
  function SettingsHeaderSearch() {
122
355
  const [query, setQuery] = React.useState('')
123
356
  const [open, setOpen] = React.useState(false)
124
- const [sections, setSections] = React.useState(readSections)
125
- const [rows, setRows] = React.useState(readRows)
126
- const [selected, setSelected] = React.useState(null)
357
+ const [tick, setTick] = React.useState(0)
358
+ const [, forceTick] = React.useReducer((x) => x + 1, 0)
359
+ void tick
360
+
127
361
  React.useEffect(() => {
128
- const refresh = () => {
129
- setSections(readSections())
130
- setRows(readRows())
131
- }
362
+ const watched = ['settings.section', ...DEEP_SLOTS.map((c) => c.slot)]
132
363
  const offs = [
133
- ctx.slots.subscribe('settings.section', refresh),
134
- ctx.slots.subscribe('settings.general.item', refresh),
135
- ctx.locale.subscribe(refresh),
364
+ ...watched.map((slot) => ctx.slots.subscribe(slot, forceTick)),
365
+ ctx.locale.subscribe(forceTick),
136
366
  ]
137
- return () => { for (const off of offs) off() }
367
+ // Grow the index whenever the panel body re-renders (visits new pages).
368
+ const observer = typeof MutationObserver === 'function'
369
+ ? new MutationObserver(() => {
370
+ if (model.runHarvest()) setTick((x) => x + 1)
371
+ })
372
+ : null
373
+ observer?.observe(document.body, { childList: true, subtree: true })
374
+ const warm = setTimeout(() => { model.runHarvest(); setTick((x) => x + 1) }, 800)
375
+ return () => {
376
+ for (const off of offs) off()
377
+ observer?.disconnect()
378
+ clearTimeout(warm)
379
+ }
138
380
  }, [])
139
381
 
140
382
  const q = query.trim().toLowerCase()
141
383
  const tokens = q.split(/\s+/).filter(Boolean)
142
- const match = (label, id) => tokens.every((tok) => (label + ' ' + id).toLowerCase().includes(tok))
143
- const shownSections = tokens.length ? sections.filter((s) => match(s.label, s.id)) : []
144
- const shownRows = tokens.length ? rows.filter((r) => match(r.label, r.id)) : []
145
- const none = tokens.length > 0 && shownSections.length === 0 && shownRows.length === 0
384
+ const matches = (haystack) => tokens.every((tok) => haystack.includes(tok))
385
+
386
+ const sections = model.readSections()
387
+ const deeps = q !== '' ? model.readDeep() : []
388
+ const shownPages = q !== ''
389
+ ? sections.filter((s) => matches(`${s.label} ${s.id}`.toLowerCase()))
390
+ : []
391
+ const shownOptions = q !== ''
392
+ ? deeps.filter((o) => matches(`${o.hay} ${o.parentLabel} ${o.id} ${o.slot}`.toLowerCase()))
393
+ : []
394
+ const none = q !== '' && shownPages.length === 0 && shownOptions.length === 0
146
395
 
147
- const renderItem = (item, kind) => React.createElement('li', {
148
- key: item.id,
396
+ const [hint, setHint] = React.useState(null)
397
+ const pick = (item) => {
398
+ setOpen(false)
399
+ const jumped = item.kind === 'page'
400
+ ? navigator.jumpToPage(item)
401
+ : navigator.jumpToOption(item)
402
+ Promise.resolve(jumped).then((ok) => {
403
+ if (ok === false) setHint(item.kind === 'page' ? item.label : `${item.parentLabel} › ${item.label}`)
404
+ })
405
+ }
406
+
407
+ const renderPage = (item) => React.createElement('li', {
408
+ key: 'page:' + item.id,
149
409
  className: 'sss-item',
150
- onMouseDown: () => { setSelected(item); setOpen(false) },
410
+ onMouseDown: () => pick(item),
151
411
  }, [
152
- React.createElement('span', { className: 'sss-item-label' }, item.label || item.id),
153
- React.createElement('span', { className: 'sss-item-id' }, kind),
412
+ React.createElement('span', { className: 'sss-item-label' }, item.label),
413
+ React.createElement('span', { className: 'sss-item-kind' }, t('page')),
414
+ ])
415
+ const renderOption = (item) => React.createElement('li', {
416
+ key: item.key,
417
+ className: 'sss-item',
418
+ onMouseDown: () => pick(item),
419
+ }, [
420
+ React.createElement('span', { className: 'sss-item-col' }, [
421
+ React.createElement('span', { className: 'sss-item-label' }, item.label),
422
+ React.createElement('span', { className: 'sss-item-path' }, item.parentLabel),
423
+ ]),
424
+ React.createElement('span', { className: 'sss-item-kind' }, t('option')),
154
425
  ])
155
426
 
156
427
  return React.createElement('div', { className: 'sss-wrap' }, [
@@ -162,32 +433,32 @@ window.__ModuleLoader__.load({
162
433
  value: query,
163
434
  placeholder: t('placeholder'),
164
435
  spellCheck: false,
165
- onChange: (e) => { setQuery(e.target.value); setOpen(true) },
436
+ onChange: (e) => { setQuery(e.target.value); setHint(null); setOpen(true) },
166
437
  onFocus: () => setOpen(true),
167
438
  onBlur: () => setOpen(false),
168
439
  }),
169
440
  query !== '' ? React.createElement('button', {
170
441
  className: 'sss-clear',
171
442
  type: 'button',
172
- onClick: () => { setQuery(''); setSelected(null); setOpen(false) },
443
+ onClick: () => { setQuery(''); setHint(null); setOpen(false) },
173
444
  }, '✕') : null,
174
445
  ]),
175
- open && tokens.length > 0 ? React.createElement('div', { className: 'sss-pop' }, [
446
+ open && q !== '' ? React.createElement('div', { className: 'sss-pop' }, [
176
447
  none
177
448
  ? React.createElement('p', { className: 'sss-empty' }, t('empty'))
178
449
  : React.createElement('div', { className: 'sss-groups' }, [
179
- shownSections.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
450
+ shownPages.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
180
451
  React.createElement('div', { className: 'sss-group-title' }, t('pages')),
181
- React.createElement('ul', { className: 'sss-list' }, shownSections.map((s) => renderItem(s, t('page')))),
452
+ React.createElement('ul', { className: 'sss-list' }, shownPages.map(renderPage)),
182
453
  ]) : null,
183
- shownRows.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
184
- React.createElement('div', { className: 'sss-group-title' }, t('rows')),
185
- React.createElement('ul', { className: 'sss-list' }, shownRows.map((r) => renderItem(r, t('row')))),
454
+ shownOptions.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
455
+ React.createElement('div', { className: 'sss-group-title' }, t('options')),
456
+ React.createElement('ul', { className: 'sss-list' }, shownOptions.map(renderOption)),
186
457
  ]) : null,
187
458
  ]),
188
459
  ]) : null,
189
- selected !== null
190
- ? React.createElement('div', { className: 'sss-guide' }, t('guide').replace('{label}', selected.label || selected.id))
460
+ hint !== null
461
+ ? React.createElement('div', { className: 'sss-guide' }, t('jumpFail').replace('{path}', hint))
191
462
  : null,
192
463
  ])
193
464
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectivex666/dsh-settings-search",
3
- "version": "1.1.2",
3
+ "version": "1.3.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",