@objectivex666/dsh-settings-search 1.3.0 → 1.4.1

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
@@ -16,6 +16,7 @@
16
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
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
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.
19
+ - 🛠 **Built-in settings page (v1.4.0)** — a "Settings Search" page in the left navigation shows the current version and checks npm for a newer release on demand; when one exists it hands you a copy-ready `dsh plugin update` command.
19
20
  - 🌐 **i18n** — built-in Chinese/English dictionaries that follow the DSH localization system and are easy to extend.
20
21
  - 🎨 **Theme-aware** — styled entirely with CSS variables, automatically following the DSH theme (light/dark).
21
22
  - ⚡ **Reactive** — subscribes to settings entries being added, removed, or changed, and refreshes results automatically.
@@ -79,7 +80,6 @@ lib/index.js Host half (no-op — makes the package a valid Cordis plugin
79
80
  lib/client.js Browser half (search UI, __ModuleLoader__ format)
80
81
  cordis.patch.yml Composition patch: inserts the settings-search plugin row
81
82
  scripts/check.mjs Structural self-checks (npm test)
82
- dsh-settings-search.js Dynamic-plugin source (optional: in-session cordis_define usage)
83
83
  ```
84
84
 
85
85
  ## Development
@@ -88,6 +88,7 @@ dsh-settings-search.js Dynamic-plugin source (optional: in-session cordis_defin
88
88
  npm test # validates the host half, the dsh.client manifest, and the client bundle handoff
89
89
  ```
90
90
 
91
- ## Dynamic-plugin usage (optional)
92
-
93
- Legacy usage: register the contents of `dsh-settings-search.js` as `code.client` through DSH's dynamic plugin system (`cordis_define`) — session-only and temporary. For anything persistent, use the profile-bundle install above.
91
+ > Historical note: up to v1.1 the repo shipped `dsh-settings-search.js` (an
92
+ > in-session `cordis_define` dynamic-plugin source). Because keeping it in
93
+ > sync with the shipping package kept drifting silently, it was removed in
94
+ > v1.4.1; retrieve it from git history (tag `v1.4.0`) if you need a reference.
package/README.md CHANGED
@@ -16,6 +16,7 @@
16
16
  - 📂 **页内选项搜索(v1.2.0)**:不只搜页面,还能直达每个设置页内部的具体选项——插件页的标签页(`settings.plugins.tab`)、Web UI 插件的卡片(`web-ui.plugin.item`)、通用设置行等,结果以「页面 › 选项」面包屑展示。
17
17
  - 🧭 **点击即达**:选中结果后自动点击对应的左侧导航项打开所在分区;若是页内标签页会继续点开对应标签,并高亮闪烁目标选项行。无法自动跳转时显示手动路径提示。
18
18
  - 🌱 **渐进索引**:自绘界面的设置行会在其所在页面首次被访问时从渲染 DOM 中收割标题并加入索引(此后始终可搜)。
19
+ - 🛠 **自带设置页(v1.4.0)**:左侧导航新增「设置搜索」页——展示当前版本,一键检查 npm 注册表上的最新版本;有更新时直接给出复制就用的 `dsh plugin update` 命令。
19
20
  - 🌐 **多语言支持**:内置中英文国际化,可跟随 DSH 本地化系统扩展。
20
21
  - 🎨 **主题适配**:完全使用 CSS 变量,自动跟随 DSH 主题(亮/暗)。
21
22
  - ⚡ **动态响应**:实时订阅设置项的新增/删除/变更,搜索结果自动刷新。
@@ -79,7 +80,6 @@ lib/index.js Host half(空实现,保证作为合法 Cordis 插件行
79
80
  lib/client.js Browser half(搜索框 UI,__ModuleLoader__ 格式)
80
81
  cordis.patch.yml 组合补丁:插入 settings-search 插件行
81
82
  scripts/check.mjs 结构自检(npm test)
82
- dsh-settings-search.js 动态插件源(可选:DSH 会话内 cordis_define 用法)
83
83
  ```
84
84
 
85
85
  ## 开发
@@ -88,8 +88,6 @@ dsh-settings-search.js 动态插件源(可选:DSH 会话内 cordis_define
88
88
  npm test # 校验 host 半、dsh.client 清单、client bundle handoff
89
89
  ```
90
90
 
91
- ## 动态插件用法(可选)
92
-
93
- 旧版用法:把 `dsh-settings-search.js` 的内容作为 `code.client` 通过 DSH 的
94
- 动态插件系统(`cordis_define`)注册,仅供会话内临时使用;命令行安装请使用
95
- 上文的 profile bundle 方式。
91
+ > 历史说明:v1.1 及之前仓库曾附带 `dsh-settings-search.js`(会话内
92
+ > `cordis_define` 动态插件源)。因与正式包代码长期双份维护漂移,已于 v1.4.1
93
+ > 移除;如需动态用法可从 git 历史(tag `v1.4.0`)取回参考。
package/lib/client.js CHANGED
@@ -29,6 +29,9 @@ window.__ModuleLoader__.load({
29
29
  const React = require('react')
30
30
 
31
31
  const NS = 'settings-search-v5'
32
+ /** Single source of truth is package.json — scripts/check.mjs asserts this matches. */
33
+ const PKG_VERSION = '1.4.1'
34
+ const NPM_NAME = '@objectivex666/dsh-settings-search'
32
35
  const zh = {
33
36
  title: '设置',
34
37
  placeholder: '搜索设置…',
@@ -38,6 +41,17 @@ window.__ModuleLoader__.load({
38
41
  page: '页面',
39
42
  option: '选项',
40
43
  jumpFail: '未能自动跳转,请手动打开:{path}',
44
+ updn: '设置搜索',
45
+ updDesc: '设置面板顶部搜索框由本插件提供。在此检查插件的版本与更新。',
46
+ updCurrent: '当前版本',
47
+ updLatestKnown: 'npm 最新版',
48
+ updCheck: '检查更新',
49
+ updChecking: '正在检查…',
50
+ updNewest: '已是最新版本 ✓',
51
+ updFound: '发现新版本 {ver},可执行以下命令更新后重启 DSH:',
52
+ updErr: '检查失败(网络或注册表不可达),请稍后重试。',
53
+ updCopy: '复制命令',
54
+ updCopied: '已复制 ✓',
41
55
  }
42
56
  const en = {
43
57
  title: 'Settings',
@@ -48,6 +62,17 @@ window.__ModuleLoader__.load({
48
62
  page: 'Page',
49
63
  option: 'Option',
50
64
  jumpFail: 'Could not navigate automatically — open it here: {path}',
65
+ updn: 'Settings Search',
66
+ updDesc: "The search box atop the settings panel comes from this plugin. Check its version and updates here.",
67
+ updCurrent: 'Current version',
68
+ updLatestKnown: 'npm latest',
69
+ updCheck: 'Check for updates',
70
+ updChecking: 'Checking…',
71
+ updNewest: 'You are on the latest version ✓',
72
+ updFound: 'Version {ver} available. Run the command below, then restart DSH:',
73
+ updErr: 'Check failed (network or registry unreachable). Try again later.',
74
+ updCopy: 'Copy command',
75
+ updCopied: 'Copied ✓',
51
76
  }
52
77
 
53
78
  /** Inner-row slots that represent specific options inside one page. */
@@ -80,6 +105,19 @@ window.__ModuleLoader__.load({
80
105
  .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
106
  @keyframes sssFlashBg { 0% { background-color: var(--dsw-alias-interactive-bg-hover); } 100% { background-color: transparent; } }
82
107
  .sss-flash { animation: sssFlashBg 1.8s ease-out; border-radius: 8px; }
108
+ .sss-upd { display: flex; flex-direction: column; gap: 12px; padding: 16px; max-width: 560px; }
109
+ .sss-upd-title { font-size: 15px; font-weight: 600; line-height: 22px; color: var(--dsw-alias-label-primary); margin: 0; }
110
+ .sss-upd-desc { margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
111
+ .sss-upd-card { display: flex; flex-direction: column; gap: 10px; padding: 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; background: var(--dsw-alias-bg-layer-1); }
112
+ .sss-upd-row { display: flex; align-items: center; gap: 8px; font-size: 13px; line-height: 20px; }
113
+ .sss-upd-key { flex-shrink: 0; width: 88px; color: var(--dsw-alias-label-secondary); }
114
+ .sss-upd-val { color: var(--dsw-alias-label-primary); }
115
+ .sss-upd-badge { flex-shrink: 0; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 18px; }
116
+ .sss-btn { height: 28px; padding: 0 12px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-size: 12px; cursor: pointer; }
117
+ .sss-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
118
+ .sss-btn:disabled { opacity: .55; cursor: default; }
119
+ .sss-cmd { user-select: all; font-family: ui-monospace, Consolas, 'Courier New', monospace; font-size: 11.5px; line-height: 20px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; padding: 4px 8px; word-break: break-all; }
120
+ .sss-upd-note { margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
83
121
  `
84
122
  let stylesInstalled = false
85
123
  function installStyles() {
@@ -463,6 +501,94 @@ window.__ModuleLoader__.load({
463
501
  ])
464
502
  }
465
503
 
504
+ const compareSemver = (a, b) => {
505
+ const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0)
506
+ const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0)
507
+ for (let i = 0; i < 3; i += 1) {
508
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0)
509
+ }
510
+ return 0
511
+ }
512
+
513
+ function UpdatePanel() {
514
+ const [state, setState] = React.useState('idle') // idle | loading | newest | outdated | error
515
+ const [latest, setLatest] = React.useState('')
516
+ const [copied, setCopied] = React.useState(false)
517
+
518
+ React.useEffect(() => {
519
+ if (!copied) return
520
+ const off = setTimeout(() => setCopied(false), 1600)
521
+ return () => clearTimeout(off)
522
+ }, [copied])
523
+
524
+ const check = async () => {
525
+ setState('loading')
526
+ setCopied(false)
527
+ try {
528
+ const res = await fetch(`https://registry.npmjs.org/${NPM_NAME}/latest?t=${Date.now()}`)
529
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
530
+ const data = await res.json()
531
+ const ver = typeof data?.version === 'string' ? data.version : ''
532
+ if (ver === '') throw new Error('no version in registry response')
533
+ setLatest(ver)
534
+ setState(compareSemver(ver, PKG_VERSION) > 0 ? 'outdated' : 'newest')
535
+ } catch {
536
+ setState('error')
537
+ }
538
+ }
539
+
540
+ const copyCmd = async () => {
541
+ const cmd = `dsh plugin update ${NPM_NAME}`
542
+ try { await navigator.clipboard.writeText(cmd); setCopied(true) } catch { /* user can copy manually */ }
543
+ }
544
+
545
+ const statusNode = {
546
+ idle: React.createElement('span', { className: 'sss-upd-val' }, '—'),
547
+ loading: React.createElement('span', { className: 'sss-upd-val' }, t('updChecking')),
548
+ newest: React.createElement('span', { className: 'sss-upd-val' }, t('updNewest')),
549
+ error: React.createElement('span', { className: 'sss-upd-val' }, t('updErr')),
550
+ outdated: latest === '' ? null : React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } }, [
551
+ React.createElement('p', { className: 'sss-upd-note' }, t('updFound').replace('{ver}', `v${latest}`)),
552
+ React.createElement('code', { className: 'sss-cmd' }, `dsh plugin update ${NPM_NAME}`),
553
+ React.createElement('div', null,
554
+ React.createElement('button', { className: 'sss-btn', type: 'button', onClick: () => { void copyCmd() } },
555
+ copied ? t('updCopied') : t('updCopy'))),
556
+ ]),
557
+ }
558
+
559
+ return React.createElement('div', { className: 'sss-upd' }, [
560
+ React.createElement('h3', { className: 'sss-upd-title' }, `${t('updn')} v${PKG_VERSION}`),
561
+ React.createElement('p', { className: 'sss-upd-desc' }, t('updDesc')),
562
+ React.createElement('div', { className: 'sss-upd-card' }, [
563
+ React.createElement('div', { className: 'sss-upd-row' }, [
564
+ React.createElement('span', { className: 'sss-upd-key' }, t('updCurrent')),
565
+ React.createElement('code', { className: 'sss-cmd' }, PKG_VERSION),
566
+ state === 'newest' ? React.createElement('span', { className: 'sss-upd-badge' }, 'npm ✓') : null,
567
+ ]),
568
+ state === 'outdated' || state === 'loading'
569
+ ? React.createElement('div', { className: 'sss-upd-row' }, [
570
+ React.createElement('span', { className: 'sss-upd-key' }, t('updLatestKnown')),
571
+ state === 'loading' ? React.createElement('span', { className: 'sss-upd-val' }, '…') : React.createElement('code', { className: 'sss-cmd' }, latest),
572
+ ])
573
+ : null,
574
+ state === 'outdated' || state === 'loading' || state === 'error' || state === 'idle' || state === 'newest'
575
+ ? (state === 'outdated' ? statusNode.outdated : statusNode[state])
576
+ : null,
577
+ React.createElement('div', { className: 'sss-upd-row' },
578
+ React.createElement('button', { className: 'sss-btn', type: 'button', disabled: state === 'loading', onClick: () => { void check() } },
579
+ state === 'loading' ? t('updChecking') : t('updCheck'))),
580
+ ]),
581
+ ])
582
+ }
583
+
584
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
585
+ name: 'settings.section',
586
+ id: 'settings-search',
587
+ order: 1600,
588
+ locale: NS,
589
+ label: () => t('updn'),
590
+ }, UpdatePanel))
591
+
466
592
  ctx.slots.inject('settings.header', () => ctx.slots.register({
467
593
  name: 'settings.header',
468
594
  priority: -1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectivex666/dsh-settings-search",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
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",
@@ -33,7 +33,6 @@
33
33
  "lib",
34
34
  "cordis.patch.yml",
35
35
  "scripts",
36
- "dsh-settings-search.js",
37
36
  "README.md",
38
37
  "README.en.md",
39
38
  "LICENSE"
package/scripts/check.mjs CHANGED
@@ -54,6 +54,12 @@ check(typeof client.apply === 'function', 'client half must export apply(ctx)')
54
54
  check(Array.isArray(client.inject) && client.inject.includes('slots') && client.inject.includes('locale'),
55
55
  'client half inject must include "slots" and "locale"')
56
56
 
57
+ // 4. embedded version constant matches the manifest (update panel displays it)
58
+ const versionMatch = /const PKG_VERSION = '([^']+)'/.exec(source)
59
+ check(versionMatch !== null, 'lib/client.js must define const PKG_VERSION')
60
+ check(versionMatch?.[1] === pkg.version,
61
+ `PKG_VERSION (${versionMatch?.[1]}) must equal package.json version (${pkg.version})`)
62
+
57
63
  if (failed > 0) {
58
64
  console.error(`${failed} check(s) failed`)
59
65
  process.exit(1)
@@ -1,179 +0,0 @@
1
- /**
2
- * settings-search 动态插件源文件(legacy,仅会话内临时使用)
3
- *
4
- * 说明:
5
- * - 这是 DSH 动态 Cordis 插件(Client half)的源码,通过 cordis_define 注册到运行中的
6
- * DSH 进程,不落盘。仅供会话内临时使用;命令行安装请改用本仓库的 profile bundle
7
- * 形态(`dsh plugin add`,见 README),其浏览器半位于 lib/client.js,与本文件等价。
8
- * - 功能:在设置面板左侧导航栏顶部(settings.header 槽,priority:-1 shadow)添加搜索框,
9
- * 输入关键词实时搜索所有已注册设置页(settings.section)与通用设置项
10
- * (settings.general.item),浮层展示结果,点击后显示左侧导航定位引导。
11
- * - 图标:SVG 放大镜(fill=currentColor),随主题文字颜色。
12
- * - 源码是 cordis_define 的 code.client 字段值:一个返回 Cordis Plugin 的纯 JS 函数体,
13
- * 无 TypeScript / JSX / import。
14
- *
15
- * 重新注册方式(会话内):
16
- * cordis_define({ kind: 'new', idPrefix: 'set' }, ...) 传入本文件的 code.client 内容。
17
- */
18
-
19
- return {
20
- inject: ['slots', 'locale'],
21
- apply(ctx) {
22
- const NS = 'settings-search-v4'
23
- ctx.effect(() => ctx.locale.register(NS, {
24
- zh: {
25
- title: '设置',
26
- placeholder: '搜索设置…',
27
- empty: '没有匹配的设置',
28
- pages: '设置页',
29
- rows: '通用设置项',
30
- guide: '该设置位于左侧导航「{label}」,点击左侧导航项即可打开',
31
- page: '设置页',
32
- row: '设置项',
33
- },
34
- en: {
35
- title: 'Settings',
36
- placeholder: 'Search settings…',
37
- empty: 'No matching settings',
38
- pages: 'Settings pages',
39
- rows: 'General items',
40
- guide: 'This setting lives in "{label}" — open it from the left navigation',
41
- page: 'Page',
42
- row: 'Item',
43
- },
44
- }), 'settings-search: dictionaries')
45
- const t = ctx.locale.bind(NS)
46
-
47
- styles.insert(`
48
- .sss-wrap { position: relative; display: flex; flex-direction: column; gap: 8px; }
49
- .sss-title { font-size: 16px; font-weight: 600; line-height: 24px; color: var(--dsw-alias-label-primary); }
50
- .sss-box { display: flex; align-items: center; gap: 6px; width: 100%; box-sizing: border-box; height: 32px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1); }
51
- .sss-icon { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; color: var(--dsw-alias-label-tertiary); }
52
- .sss-input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-primary); }
53
- .sss-input::placeholder { color: var(--dsw-alias-label-dimmed); }
54
- .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; }
55
- .sss-clear:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
56
- .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; }
57
- .sss-groups { display: flex; flex-direction: column; gap: 8px; }
58
- .sss-group-title { font-size: 11px; color: var(--dsw-alias-label-secondary); letter-spacing: .05em; padding: 2px 6px; }
59
- .sss-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
60
- .sss-item { display: flex; align-items: center; gap: 6px; padding: 7px 8px; border-radius: 7px; cursor: pointer; }
61
- .sss-item:hover { background: var(--dsw-alias-interactive-bg-hover); }
62
- .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; }
63
- .sss-item-id { font-size: 10px; color: var(--dsw-alias-label-secondary); flex-shrink: 0; }
64
- .sss-empty { margin: 0; padding: 10px 8px; color: var(--dsw-alias-label-secondary); font-size: 13px; }
65
- .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; }
66
- `)
67
-
68
- const resolveLabel = (entry) => {
69
- const raw = entry.options.label
70
- if (typeof raw === 'function') return String(raw())
71
- return raw == null ? '' : String(raw)
72
- }
73
- const readSections = () => ctx.slots.entries('settings.section')
74
- .filter((e) => e.options.id !== 'settings-search')
75
- .map((e) => ({ id: e.options.id, order: e.options.order ?? 0, label: resolveLabel(e) }))
76
- .sort((a, b) => a.order - b.order)
77
- const readRows = () => ctx.slots.entries('settings.general.item')
78
- .map((e) => ({ id: e.options.id, order: e.options.order ?? 0, label: resolveLabel(e) }))
79
- .sort((a, b) => a.order - b.order)
80
-
81
- const SearchIcon = React.createElement('svg', {
82
- className: 'sss-icon',
83
- viewBox: '0 0 16.5 16.5',
84
- width: 16,
85
- height: 16,
86
- xmlns: 'http://www.w3.org/2000/svg',
87
- 'aria-hidden': 'true',
88
- }, [
89
- React.createElement('path', {
90
- d: 'M11.894845 6.647401C11.894845 3.725463 9.534486 1.356779 6.623219 1.35657C3.711786 1.35657 1.351635 3.725338 1.351635 6.647401C1.351843 9.569296 3.711911 11.938273 6.623219 11.938273C9.534361 11.938064 11.894637 9.569171 11.894845 6.647401ZM13.245462 6.647401C13.245254 10.317935 10.280401 13.293613 6.623219 13.293821C2.965871 13.293821 0.000204 10.31806 0 6.647401C0 2.976574 2.965746 0 6.623219 0C10.280526 0.000205 13.245462 2.9767 13.245462 6.647401Z',
91
- fill: 'currentColor',
92
- }),
93
- React.createElement('path', {
94
- d: 'M16.000417 15.041079L15.044449 16.000433L11.530434 12.473588L12.486298 11.514234L16.000417 15.041079Z',
95
- fill: 'currentColor',
96
- }),
97
- ])
98
-
99
- function SettingsHeaderSearch() {
100
- const [query, setQuery] = React.useState('')
101
- const [open, setOpen] = React.useState(false)
102
- const [sections, setSections] = React.useState(readSections)
103
- const [rows, setRows] = React.useState(readRows)
104
- const [selected, setSelected] = React.useState(null)
105
- React.useEffect(() => {
106
- const refresh = () => {
107
- setSections(readSections())
108
- setRows(readRows())
109
- }
110
- const offs = [
111
- ctx.slots.subscribe('settings.section', refresh),
112
- ctx.slots.subscribe('settings.general.item', refresh),
113
- ctx.locale.subscribe(refresh),
114
- ]
115
- return () => { for (const off of offs) off() }
116
- }, [])
117
-
118
- const q = query.trim().toLowerCase()
119
- const tokens = q.split(/\s+/).filter(Boolean)
120
- const match = (label, id) => tokens.every((tok) => (label + ' ' + id).toLowerCase().includes(tok))
121
- const shownSections = tokens.length ? sections.filter((s) => match(s.label, s.id)) : []
122
- const shownRows = tokens.length ? rows.filter((r) => match(r.label, r.id)) : []
123
- const none = tokens.length > 0 && shownSections.length === 0 && shownRows.length === 0
124
-
125
- const renderItem = (item, kind) => React.createElement('li', {
126
- key: item.id,
127
- className: 'sss-item',
128
- onMouseDown: () => { setSelected(item); setOpen(false) },
129
- }, [
130
- React.createElement('span', { className: 'sss-item-label' }, item.label || item.id),
131
- React.createElement('span', { className: 'sss-item-id' }, kind),
132
- ])
133
-
134
- return React.createElement('div', { className: 'sss-wrap' }, [
135
- React.createElement('div', { className: 'sss-title' }, t('title')),
136
- React.createElement('div', { className: 'sss-box' }, [
137
- SearchIcon,
138
- React.createElement('input', {
139
- className: 'sss-input',
140
- value: query,
141
- placeholder: t('placeholder'),
142
- spellCheck: false,
143
- onChange: (e) => { setQuery(e.target.value); setOpen(true) },
144
- onFocus: () => setOpen(true),
145
- onBlur: () => setOpen(false),
146
- }),
147
- query !== '' ? React.createElement('button', {
148
- className: 'sss-clear',
149
- type: 'button',
150
- onClick: () => { setQuery(''); setSelected(null); setOpen(false) },
151
- }, '✕') : null,
152
- ]),
153
- open && tokens.length > 0 ? React.createElement('div', { className: 'sss-pop' }, [
154
- none
155
- ? React.createElement('p', { className: 'sss-empty' }, t('empty'))
156
- : React.createElement('div', { className: 'sss-groups' }, [
157
- shownSections.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
158
- React.createElement('div', { className: 'sss-group-title' }, t('pages')),
159
- React.createElement('ul', { className: 'sss-list' }, shownSections.map((s) => renderItem(s, t('page')))),
160
- ]) : null,
161
- shownRows.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
162
- React.createElement('div', { className: 'sss-group-title' }, t('rows')),
163
- React.createElement('ul', { className: 'sss-list' }, shownRows.map((r) => renderItem(r, t('row')))),
164
- ]) : null,
165
- ]),
166
- ]) : null,
167
- selected !== null
168
- ? React.createElement('div', { className: 'sss-guide' }, t('guide').replace('{label}', selected.label || selected.id))
169
- : null,
170
- ])
171
- }
172
-
173
- ctx.slots.inject('settings.header', () => ctx.slots.register({
174
- name: 'settings.header',
175
- priority: -1,
176
- locale: NS,
177
- }, SettingsHeaderSearch))
178
- },
179
- }