@lijian-ui/dsh-file-manager 0.1.20

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.
Files changed (169) hide show
  1. package/LICENSE +38 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +66 -0
  4. package/README.zh.md +66 -0
  5. package/cordis.patch.yml +13 -0
  6. package/lib/assets/mermaid.min.js +3587 -0
  7. package/lib/client.js +7621 -0
  8. package/lib/client.js.map +1 -0
  9. package/lib/index.js +3207 -0
  10. package/lib/tsconfig.client.tsbuildinfo +1 -0
  11. package/lib/tsconfig.host.tsbuildinfo +1 -0
  12. package/lib/types/client/FileManagerSettingsCard.d.ts +56 -0
  13. package/lib/types/client/FileManagerSettingsCard.d.ts.map +1 -0
  14. package/lib/types/client/PluginSettingsCard.d.ts +104 -0
  15. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
  16. package/lib/types/client/api.d.ts +75 -0
  17. package/lib/types/client/api.d.ts.map +1 -0
  18. package/lib/types/client/chat/AttachedFilesDock.d.ts +51 -0
  19. package/lib/types/client/chat/AttachedFilesDock.d.ts.map +1 -0
  20. package/lib/types/client/chat/file-ref.d.ts +35 -0
  21. package/lib/types/client/chat/file-ref.d.ts.map +1 -0
  22. package/lib/types/client/chat/mermaid-chat.d.ts +30 -0
  23. package/lib/types/client/chat/mermaid-chat.d.ts.map +1 -0
  24. package/lib/types/client/chat/placeholder-hint.d.ts +16 -0
  25. package/lib/types/client/chat/placeholder-hint.d.ts.map +1 -0
  26. package/lib/types/client/components/ExplorerPanel.d.ts +26 -0
  27. package/lib/types/client/components/ExplorerPanel.d.ts.map +1 -0
  28. package/lib/types/client/components/FileIcon.d.ts +15 -0
  29. package/lib/types/client/components/FileIcon.d.ts.map +1 -0
  30. package/lib/types/client/components/ScmPanel.d.ts +23 -0
  31. package/lib/types/client/components/ScmPanel.d.ts.map +1 -0
  32. package/lib/types/client/components/a11y.d.ts +12 -0
  33. package/lib/types/client/components/a11y.d.ts.map +1 -0
  34. package/lib/types/client/components/icons.d.ts +41 -0
  35. package/lib/types/client/components/icons.d.ts.map +1 -0
  36. package/lib/types/client/components/overlay.d.ts +45 -0
  37. package/lib/types/client/components/overlay.d.ts.map +1 -0
  38. package/lib/types/client/drag/DragFileInlay.d.ts +31 -0
  39. package/lib/types/client/drag/DragFileInlay.d.ts.map +1 -0
  40. package/lib/types/client/drag/file-drag.d.ts +47 -0
  41. package/lib/types/client/drag/file-drag.d.ts.map +1 -0
  42. package/lib/types/client/drag.d.ts +31 -0
  43. package/lib/types/client/drag.d.ts.map +1 -0
  44. package/lib/types/client/file-source.d.ts +29 -0
  45. package/lib/types/client/file-source.d.ts.map +1 -0
  46. package/lib/types/client/fileType.d.ts +31 -0
  47. package/lib/types/client/fileType.d.ts.map +1 -0
  48. package/lib/types/client/floating.d.ts +27 -0
  49. package/lib/types/client/floating.d.ts.map +1 -0
  50. package/lib/types/client/hooks/useResizableSplit.d.ts +37 -0
  51. package/lib/types/client/hooks/useResizableSplit.d.ts.map +1 -0
  52. package/lib/types/client/hooks/useStore.d.ts +10 -0
  53. package/lib/types/client/hooks/useStore.d.ts.map +1 -0
  54. package/lib/types/client/index.d.ts +57 -0
  55. package/lib/types/client/index.d.ts.map +1 -0
  56. package/lib/types/client/layout.d.ts +104 -0
  57. package/lib/types/client/layout.d.ts.map +1 -0
  58. package/lib/types/client/locales.d.ts +125 -0
  59. package/lib/types/client/locales.d.ts.map +1 -0
  60. package/lib/types/client/maximize.d.ts +24 -0
  61. package/lib/types/client/maximize.d.ts.map +1 -0
  62. package/lib/types/client/mention.d.ts +44 -0
  63. package/lib/types/client/mention.d.ts.map +1 -0
  64. package/lib/types/client/mount.d.ts +22 -0
  65. package/lib/types/client/mount.d.ts.map +1 -0
  66. package/lib/types/client/persist.d.ts +55 -0
  67. package/lib/types/client/persist.d.ts.map +1 -0
  68. package/lib/types/client/picker/FilePickerModal.d.ts +47 -0
  69. package/lib/types/client/picker/FilePickerModal.d.ts.map +1 -0
  70. package/lib/types/client/picker/file-picker.d.ts +62 -0
  71. package/lib/types/client/picker/file-picker.d.ts.map +1 -0
  72. package/lib/types/client/preview/PreviewPanel.d.ts +17 -0
  73. package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -0
  74. package/lib/types/client/preview/PreviewTabs.d.ts +30 -0
  75. package/lib/types/client/preview/PreviewTabs.d.ts.map +1 -0
  76. package/lib/types/client/preview/PreviewToolbar.d.ts +37 -0
  77. package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -0
  78. package/lib/types/client/preview/content.d.ts +43 -0
  79. package/lib/types/client/preview/content.d.ts.map +1 -0
  80. package/lib/types/client/preview/markdown.d.ts +60 -0
  81. package/lib/types/client/preview/markdown.d.ts.map +1 -0
  82. package/lib/types/client/preview/mermaid.d.ts +78 -0
  83. package/lib/types/client/preview/mermaid.d.ts.map +1 -0
  84. package/lib/types/client/preview/selection.d.ts +26 -0
  85. package/lib/types/client/preview/selection.d.ts.map +1 -0
  86. package/lib/types/client/reference.d.ts +75 -0
  87. package/lib/types/client/reference.d.ts.map +1 -0
  88. package/lib/types/client/settings-form.d.ts +206 -0
  89. package/lib/types/client/settings-form.d.ts.map +1 -0
  90. package/lib/types/client/store.d.ts +279 -0
  91. package/lib/types/client/store.d.ts.map +1 -0
  92. package/lib/types/core/types.d.ts +132 -0
  93. package/lib/types/core/types.d.ts.map +1 -0
  94. package/lib/types/host/fs-service.d.ts +96 -0
  95. package/lib/types/host/fs-service.d.ts.map +1 -0
  96. package/lib/types/host/gate.d.ts +45 -0
  97. package/lib/types/host/gate.d.ts.map +1 -0
  98. package/lib/types/host/git-runner.d.ts +80 -0
  99. package/lib/types/host/git-runner.d.ts.map +1 -0
  100. package/lib/types/host/git-service.d.ts +130 -0
  101. package/lib/types/host/git-service.d.ts.map +1 -0
  102. package/lib/types/host/loopback.d.ts +25 -0
  103. package/lib/types/host/loopback.d.ts.map +1 -0
  104. package/lib/types/host/poll-guard.d.ts +55 -0
  105. package/lib/types/host/poll-guard.d.ts.map +1 -0
  106. package/lib/types/host/routes.d.ts +54 -0
  107. package/lib/types/host/routes.d.ts.map +1 -0
  108. package/lib/types/index.d.ts +35 -0
  109. package/lib/types/index.d.ts.map +1 -0
  110. package/lib/types/mount-once.d.ts +25 -0
  111. package/lib/types/mount-once.d.ts.map +1 -0
  112. package/package.json +85 -0
  113. package/src/client/FileManagerSettingsCard.tsx +117 -0
  114. package/src/client/PluginSettingsCard.tsx +337 -0
  115. package/src/client/api.ts +141 -0
  116. package/src/client/chat/file-ref.ts +144 -0
  117. package/src/client/chat/mermaid-chat.tsx +102 -0
  118. package/src/client/chat/placeholder-hint.tsx +46 -0
  119. package/src/client/components/ExplorerPanel.tsx +534 -0
  120. package/src/client/components/FileIcon.tsx +46 -0
  121. package/src/client/components/ScmPanel.tsx +480 -0
  122. package/src/client/components/a11y.ts +20 -0
  123. package/src/client/components/icons.tsx +274 -0
  124. package/src/client/components/overlay.tsx +225 -0
  125. package/src/client/css-modules.d.ts +5 -0
  126. package/src/client/drag/DragFileInlay.tsx +89 -0
  127. package/src/client/drag/file-drag.ts +67 -0
  128. package/src/client/drag.ts +154 -0
  129. package/src/client/file-source.ts +58 -0
  130. package/src/client/fileType.ts +129 -0
  131. package/src/client/floating.ts +63 -0
  132. package/src/client/hooks/useResizableSplit.ts +91 -0
  133. package/src/client/hooks/useStore.ts +15 -0
  134. package/src/client/index.ts +389 -0
  135. package/src/client/layout.ts +546 -0
  136. package/src/client/locales.ts +247 -0
  137. package/src/client/maximize.ts +34 -0
  138. package/src/client/mount.tsx +74 -0
  139. package/src/client/persist.ts +194 -0
  140. package/src/client/picker/FilePickerModal.tsx +506 -0
  141. package/src/client/picker/file-picker.ts +86 -0
  142. package/src/client/preview/PreviewPanel.tsx +233 -0
  143. package/src/client/preview/PreviewTabs.tsx +159 -0
  144. package/src/client/preview/PreviewToolbar.tsx +160 -0
  145. package/src/client/preview/content.tsx +617 -0
  146. package/src/client/preview/markdown.ts +351 -0
  147. package/src/client/preview/mermaid.ts +274 -0
  148. package/src/client/preview/selection.ts +82 -0
  149. package/src/client/reference.ts +161 -0
  150. package/src/client/settings-card.module.css +316 -0
  151. package/src/client/settings-form.ts +451 -0
  152. package/src/client/store.ts +1290 -0
  153. package/src/client/styles/chip.module.css +38 -0
  154. package/src/client/styles/drag.module.css +30 -0
  155. package/src/client/styles/explorer.module.css +371 -0
  156. package/src/client/styles/picker.module.css +344 -0
  157. package/src/client/styles/preview.module.css +904 -0
  158. package/src/client/styles/scm.module.css +380 -0
  159. package/src/client/styles/tokens.module.css +480 -0
  160. package/src/core/types.ts +162 -0
  161. package/src/host/fs-service.ts +513 -0
  162. package/src/host/gate.ts +74 -0
  163. package/src/host/git-runner.ts +116 -0
  164. package/src/host/git-service.ts +395 -0
  165. package/src/host/loopback.ts +63 -0
  166. package/src/host/poll-guard.ts +109 -0
  167. package/src/host/routes.ts +695 -0
  168. package/src/index.ts +97 -0
  169. package/src/mount-once.ts +48 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Chat file-reference click handling (issue #314): the transcript renders
3
+ * workspace file paths as inline code, and clicking one should locate the
4
+ * file in the Explorer (switch to the Files tab, expand the ancestor chain,
5
+ * select the node) and open it in the Preview panel — directories are
6
+ * reveal-only. A document-level click listener (wired in the client apply)
7
+ * recognizes a conservative subset: a single-line `code` element whose text
8
+ * is a workspace-relative path or an absolute path under the project root.
9
+ * Links (`a`), multi-line fences, URLs, escaped (`..`) paths and anything
10
+ * inside the panels' own subtrees keep their existing behavior.
11
+ * @module dsh-filemgr/client/chat/file-ref
12
+ */
13
+
14
+ import type { PanelApi } from '../api.ts'
15
+ import type { PanelStores } from '../store.ts'
16
+ import { parentRel } from '../fileType.ts'
17
+
18
+ /** Longest candidate text (arbitrary safety cap against huge code spans). */
19
+ const MAX_REF_LENGTH = 512
20
+
21
+ /** Normalize both separators to '/'. */
22
+ function normalizeSlashes(value: string): string {
23
+ return value.replace(/\\/g, '/')
24
+ }
25
+
26
+ /**
27
+ * Interpret a code span's text as a workspace path. Returns the workspace
28
+ * RELATIVE path ('' = the root itself), or null when the text is not a
29
+ * recognizable in-workspace path. Never resolves above the root: `..`
30
+ * segments, absolute outside paths, URLs and whitespace/multi-line text all
31
+ * fall back to null so the transcript keeps its normal behavior.
32
+ */
33
+ export function pathFromText(text: string, root: string): string | null {
34
+ const raw = text.trim()
35
+ if (raw === '' || raw.length > MAX_REF_LENGTH) return null
36
+ if (/[\r\n]/.test(raw)) return null
37
+ if (raw.includes('://')) return null
38
+ const normalized = normalizeSlashes(raw)
39
+ const rootNorm = normalizeSlashes(root).replace(/\/+$/, '')
40
+ if (rootNorm === '') return null
41
+
42
+ // Absolute path inside the workspace. Posix paths start with '/'
43
+ // (case-sensitive prefix match); Windows drive paths carry a drive
44
+ // letter (case-insensitive prefix match with a separator boundary).
45
+ if (normalized.startsWith('/')) {
46
+ if (normalized === rootNorm) return ''
47
+ if (!normalized.startsWith(rootNorm + '/')) return null
48
+ const rel = normalized.slice(rootNorm.length).replace(/^\/+/, '')
49
+ return validRelative(rel) ? rel : null
50
+ }
51
+ if (/^[a-zA-Z]:/.test(normalized)) {
52
+ const lower = normalized.toLowerCase()
53
+ const lowerRoot = rootNorm.toLowerCase()
54
+ if (lower === lowerRoot) return ''
55
+ if (!lower.startsWith(lowerRoot + '/')) return null
56
+ const rel = normalized.slice(rootNorm.length).replace(/^\/+/, '')
57
+ return validRelative(rel) ? rel : null
58
+ }
59
+
60
+ // Workspace-relative path ('.' prefix stripped).
61
+ const rel = normalized.startsWith('./') ? normalized.slice(2) : normalized
62
+ return validRelative(rel) ? rel : null
63
+ }
64
+
65
+ /** Validate a relative path: no escapes, no whitespace, looks path-like. */
66
+ function validRelative(rel: string): boolean {
67
+ if (rel === '' || rel.length > MAX_REF_LENGTH) return false
68
+ if (/\s/.test(rel)) return false
69
+ const segments = rel.split('/')
70
+ if (segments.length < 2) {
71
+ // A bare name is accepted only when it looks like a file (dotted), so
72
+ // ordinary words in code spans never trigger a locate.
73
+ return /^[^/]*\.[^/]+$/.test(rel)
74
+ }
75
+ for (const segment of segments) {
76
+ if (segment === '' || segment === '.' || segment === '..') return false
77
+ }
78
+ return true
79
+ }
80
+
81
+ /** The `code` element a click targets, when it is a candidate file ref. */
82
+ export function fileRefElement(target: EventTarget | null): HTMLElement | null {
83
+ if (!(target instanceof Element)) return null
84
+ const code = target.closest('code')
85
+ if (code === null) return null
86
+ // Links keep their own navigation; the panels' own code blocks (preview
87
+ // editors, tree rows) never trigger the transcript locate.
88
+ if (code.closest('a') !== null) return null
89
+ if (code.closest('.filemgr-root') !== null) return null
90
+ if (code.closest('[data-filemgr-preview-col], [data-filemgr-explorer-col]') !== null) return null
91
+ return code
92
+ }
93
+
94
+ /**
95
+ * Locate a workspace-relative path from the transcript: Files tab + expand
96
+ * ancestors + select; directories stay reveal-only, files also open in the
97
+ * Preview panel (dedup focuses the existing tab). The parent listing is
98
+ * consulted to classify the node; unknown paths keep the reveal and never
99
+ * issue a preview request.
100
+ */
101
+ export async function locateFileRef(stores: PanelStores, api: PanelApi, rel: string): Promise<void> {
102
+ const explorer = stores.explorer
103
+ explorer.setActiveTab('files')
104
+ if (rel !== '') explorer.reveal(rel)
105
+
106
+ const root = stores.layout.getSnapshot().root
107
+ if (root === '' || rel === '') return
108
+
109
+ const name = rel.split('/').pop() ?? ''
110
+ const parent = parentRel(rel)
111
+ let entry = explorer.getSnapshot().dirs[parent]?.find((item) => item.name === name)
112
+ if (entry === undefined) {
113
+ const result = await api.list(root, parent)
114
+ if (result.ok) entry = result.value.entries.find((item) => item.name === name)
115
+ }
116
+ // Known directories: reveal-only (their children list is the tree's job).
117
+ // Known files: open in the preview panel. Unknown: no preview request.
118
+ if (entry !== undefined && !entry.isDir) {
119
+ stores.preview.openFile(root, rel)
120
+ }
121
+ }
122
+
123
+ /** Document-level click handler: locate recognized chat file references. */
124
+ export function handleFileRefClick(stores: PanelStores, api: PanelApi, event: MouseEvent): void {
125
+ if (event.defaultPrevented || event.button !== 0) return
126
+ const code = fileRefElement(event.target)
127
+ if (code === null) return
128
+ const root = stores.layout.getSnapshot().root
129
+ if (root === '') return
130
+ const rel = pathFromText(code.textContent ?? '', root)
131
+ if (rel === null) return
132
+ // The explorer must be visible for the reveal to be seen: un-collapse the
133
+ // column (persisted like the chevron toggle does).
134
+ const layout = stores.layout.getSnapshot()
135
+ if (layout.explorerCollapsed) {
136
+ stores.layout.update((prev) => ({ ...prev, explorerCollapsed: false }))
137
+ try {
138
+ localStorage.setItem(`project-panel-collapse:${root}`, 'expanded')
139
+ } catch {
140
+ // best-effort
141
+ }
142
+ }
143
+ void locateFileRef(stores, api, rel)
144
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Chat-transcript mermaid enhancement: the core conversation renderer emits
3
+ * fenced code as `pre > code.language-mermaid`, and the shell has no slot
4
+ * for message-body post-processing — so this component rides the
5
+ * conversation input dock as a zero-render sentinel and observes the
6
+ * document for mermaid blocks the transcript mounts. Blocks inside the
7
+ * preview panel's own subtree are excluded (each surface owns its blocks).
8
+ *
9
+ * Streaming awareness: an assistant message re-renders continuously, so a
10
+ * diagram fence is often incomplete mid-stream. Renders that fail restore
11
+ * the block and the next mutation retries it — once the fence closes the
12
+ * diagram lands. Mutations are debounced to one rAF so long transcripts do
13
+ * not re-scan the whole document: each batch is mapped to the minimal
14
+ * mutated subtrees and scoped per-frame while the first scheduled pass scans
15
+ * the body once. The observer is disconnected on unmount.
16
+ * @module dsh-filemgr/client/chat/mermaid-chat
17
+ */
18
+
19
+ import { useEffect } from 'react'
20
+ import type { JSX } from 'react'
21
+ import { DATA_MD_SCOPE, enhanceMermaidBlocks, mermaidTheme, rethemeMermaidBlocks, shellIsDark, watchShellTheme } from '../preview/mermaid.ts'
22
+ import previewCss from '../styles/preview.module.css'
23
+
24
+ /**
25
+ * Map a mutation batch to the minimal scan scopes that may contain new
26
+ * mermaid fences. Each record contributes its target and its added nodes
27
+ * (an added element directly; otherwise that node's parentElement), deduped
28
+ * by identity. Disconnected nodes and removed-only records yield nothing —
29
+ * removal never introduces a fence. Pure (DOM-read only) so tests can drive
30
+ * it in jsdom.
31
+ */
32
+ export function enhanceScopesFor(records: MutationRecord[]): Element[] {
33
+ const scopes = new Set<Element>()
34
+ for (const record of records) {
35
+ // Removal-only batches do not introduce a fence: without additions the
36
+ // target scan is wasted, so only records carrying added nodes contribute.
37
+ if (record.addedNodes.length === 0) continue
38
+ if (record.target instanceof Element && record.target.isConnected) {
39
+ scopes.add(record.target)
40
+ }
41
+ for (const node of record.addedNodes) {
42
+ const element = node instanceof Element ? node : node.parentElement
43
+ if (element !== null && element.isConnected) scopes.add(element)
44
+ }
45
+ }
46
+ return Array.from(scopes)
47
+ }
48
+
49
+ /** Hidden sentinel: renders nothing, owns the transcript observer. */
50
+ export function MermaidChatEnhancer(): JSX.Element | null {
51
+ useEffect(() => {
52
+ let scheduled = false
53
+ let pendingFrame = 0
54
+ let firstPass = true
55
+ let pendingRecords: MutationRecord[] = []
56
+ const run = (): void => {
57
+ scheduled = false
58
+ const records = pendingRecords
59
+ pendingRecords = []
60
+ const scopes = enhanceScopesFor(records)
61
+ if (firstPass) {
62
+ // First scheduled pass only: scan the whole document exactly once.
63
+ // Later batches (even removal-only ones that yield no scopes) never
64
+ // fall back to a full-body scan.
65
+ firstPass = false
66
+ void enhanceMermaidBlocks(document.body, {
67
+ className: previewCss.mermaidBlock,
68
+ theme: mermaidTheme(shellIsDark()),
69
+ skip: (pre) => pre.closest(`[${DATA_MD_SCOPE}]`) !== null,
70
+ })
71
+ return
72
+ }
73
+ for (const scope of scopes) {
74
+ void enhanceMermaidBlocks(scope, {
75
+ className: previewCss.mermaidBlock,
76
+ theme: mermaidTheme(shellIsDark()),
77
+ skip: (pre) => pre.closest(`[${DATA_MD_SCOPE}]`) !== null,
78
+ })
79
+ }
80
+ }
81
+ const schedule = (): void => {
82
+ if (scheduled) return
83
+ scheduled = true
84
+ pendingFrame = requestAnimationFrame(run)
85
+ }
86
+ const observer = new MutationObserver((records) => {
87
+ pendingRecords = pendingRecords.concat(records)
88
+ schedule()
89
+ })
90
+ observer.observe(document.body, { childList: true, subtree: true })
91
+ schedule()
92
+ const disposeTheme = watchShellTheme((isDark) => {
93
+ void rethemeMermaidBlocks(document.body, { theme: mermaidTheme(isDark) })
94
+ })
95
+ return () => {
96
+ observer.disconnect()
97
+ disposeTheme()
98
+ cancelAnimationFrame(pendingFrame)
99
+ }
100
+ }, [])
101
+ return null
102
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Composer placeholder hint: the shell owns the input-bar placeholder
3
+ * ("给智能体发消息") and exposes no override seam (the composer bar is a
4
+ * single slot owned by ui-conversation, and its locale namespace rejects
5
+ * duplicate registration), so this zero-render sentinel — riding the same
6
+ * `conversation.input.dock` band as the mermaid enhancer — rewrites the
7
+ * placeholder attribute in the DOM. It re-applies on any placeholder
8
+ * attribute change or node insert, which covers React re-renders that
9
+ * reset the attribute to the shell default. English UI is left untouched
10
+ * (exact-match on the Chinese string only).
11
+ * @module dsh-filemgr/client/chat/placeholder-hint
12
+ */
13
+
14
+ import { useEffect } from 'react'
15
+ import type { JSX } from 'react'
16
+
17
+ const OLD_PLACEHOLDER = '给智能体发消息'
18
+ const NEW_PLACEHOLDER = '给智能体发消息。@引用项目文件,/ 调用技能与指令'
19
+
20
+ /** Rewrite every composer input whose placeholder matches the shell default. */
21
+ function apply(): void {
22
+ for (const el of document.querySelectorAll<HTMLTextAreaElement | HTMLInputElement>('textarea, input[type="text"]')) {
23
+ if (el.placeholder === OLD_PLACEHOLDER) el.placeholder = NEW_PLACEHOLDER
24
+ }
25
+ }
26
+
27
+ /** Hidden sentinel: renders nothing, owns the placeholder rewriting observer. */
28
+ export function PlaceholderHint(): JSX.Element | null {
29
+ useEffect(() => {
30
+ apply()
31
+ const observer = new MutationObserver((records) => {
32
+ const relevant = records.some(
33
+ (record) => record.type === 'attributes' || record.addedNodes.length > 0,
34
+ )
35
+ if (relevant) apply()
36
+ })
37
+ observer.observe(document.body, {
38
+ attributes: true,
39
+ attributeFilter: ['placeholder'],
40
+ childList: true,
41
+ subtree: true,
42
+ })
43
+ return () => { observer.disconnect() }
44
+ }, [])
45
+ return null
46
+ }