@lijian-ui/dsh-file-manager 0.2.3 → 0.2.4

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 (96) hide show
  1. package/LICENSE +38 -38
  2. package/README.i18n.yaml +6 -6
  3. package/README.md +66 -66
  4. package/README.zh.md +66 -66
  5. package/cordis.patch.yml +13 -13
  6. package/lib/client.js +27305 -270
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +474 -42
  9. package/lib/tsconfig.client.tsbuildinfo +1 -1
  10. package/lib/tsconfig.host.tsbuildinfo +1 -1
  11. package/lib/types/client/file-source.d.ts +10 -0
  12. package/lib/types/client/file-source.d.ts.map +1 -1
  13. package/lib/types/client/fileType.d.ts +19 -4
  14. package/lib/types/client/fileType.d.ts.map +1 -1
  15. package/lib/types/client/index.d.ts.map +1 -1
  16. package/lib/types/client/locales.d.ts +3 -0
  17. package/lib/types/client/locales.d.ts.map +1 -1
  18. package/lib/types/client/mount.d.ts +1 -1
  19. package/lib/types/client/mount.d.ts.map +1 -1
  20. package/lib/types/client/preview/PreviewPanel.d.ts +2 -1
  21. package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -1
  22. package/lib/types/client/preview/PreviewToolbar.d.ts +3 -1
  23. package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -1
  24. package/lib/types/client/preview/content.d.ts +3 -1
  25. package/lib/types/client/preview/content.d.ts.map +1 -1
  26. package/lib/types/client/preview/office.d.ts +23 -0
  27. package/lib/types/client/preview/office.d.ts.map +1 -0
  28. package/lib/types/client/reference.d.ts +17 -0
  29. package/lib/types/client/reference.d.ts.map +1 -1
  30. package/lib/types/client/store.d.ts.map +1 -1
  31. package/lib/types/host/fs-service.d.ts.map +1 -1
  32. package/lib/types/host/office-preview.d.ts +13 -0
  33. package/lib/types/host/office-preview.d.ts.map +1 -0
  34. package/lib/types/host/routes.d.ts.map +1 -1
  35. package/lib/types/index.d.ts.map +1 -1
  36. package/package.json +19 -4
  37. package/src/client/FileManagerSettingsCard.tsx +117 -117
  38. package/src/client/PluginSettingsCard.tsx +337 -337
  39. package/src/client/chat/file-ref.ts +144 -144
  40. package/src/client/chat/mermaid-chat.tsx +102 -102
  41. package/src/client/chat/placeholder-hint.tsx +46 -46
  42. package/src/client/components/ExplorerPanel.tsx +534 -534
  43. package/src/client/components/FileIcon.tsx +46 -46
  44. package/src/client/components/ScmPanel.tsx +480 -480
  45. package/src/client/components/a11y.ts +20 -20
  46. package/src/client/components/icons.tsx +274 -274
  47. package/src/client/components/overlay.tsx +225 -225
  48. package/src/client/drag/DragFileInlay.tsx +89 -89
  49. package/src/client/drag/file-drag.ts +67 -67
  50. package/src/client/drag.ts +154 -154
  51. package/src/client/file-source.ts +34 -0
  52. package/src/client/fileType.ts +32 -5
  53. package/src/client/hooks/useResizableSplit.ts +91 -91
  54. package/src/client/hooks/useStore.ts +15 -15
  55. package/src/client/index.ts +42 -3
  56. package/src/client/layout.ts +523 -523
  57. package/src/client/locales.ts +6 -0
  58. package/src/client/maximize.ts +34 -34
  59. package/src/client/mount.tsx +2 -1
  60. package/src/client/persist.ts +194 -194
  61. package/src/client/picker/FilePickerModal.tsx +526 -526
  62. package/src/client/picker/file-picker.ts +86 -86
  63. package/src/client/preview/PreviewPanel.tsx +7 -1
  64. package/src/client/preview/PreviewTabs.tsx +159 -159
  65. package/src/client/preview/PreviewToolbar.tsx +3 -2
  66. package/src/client/preview/content.tsx +7 -2
  67. package/src/client/preview/markdown.ts +351 -351
  68. package/src/client/preview/mermaid.ts +274 -274
  69. package/src/client/preview/office.tsx +321 -0
  70. package/src/client/reference.ts +49 -0
  71. package/src/client/settings-card.module.css +316 -316
  72. package/src/client/settings-form.ts +451 -451
  73. package/src/client/store.ts +17 -12
  74. package/src/client/styles/drag.module.css +30 -30
  75. package/src/client/styles/explorer.module.css +371 -371
  76. package/src/client/styles/picker.module.css +344 -344
  77. package/src/client/styles/preview.module.css +3 -1
  78. package/src/client/styles/scm.module.css +380 -380
  79. package/src/client/styles/tokens.module.css +376 -371
  80. package/src/core/types.ts +162 -162
  81. package/src/host/fs-service.ts +7 -1
  82. package/src/host/gate.ts +73 -73
  83. package/src/host/git-runner.ts +116 -116
  84. package/src/host/git-service.ts +395 -395
  85. package/src/host/loopback.ts +63 -63
  86. package/src/host/office-preview.ts +399 -0
  87. package/src/host/poll-guard.ts +109 -109
  88. package/src/host/routes.ts +72 -1
  89. package/src/index.ts +3 -1
  90. package/src/mount-once.ts +48 -48
  91. package/lib/types/client/chat/AttachedFilesDock.d.ts +0 -51
  92. package/lib/types/client/chat/AttachedFilesDock.d.ts.map +0 -1
  93. package/lib/types/client/floating.d.ts +0 -27
  94. package/lib/types/client/floating.d.ts.map +0 -1
  95. package/lib/types/client/mention.d.ts +0 -44
  96. package/lib/types/client/mention.d.ts.map +0 -1
@@ -1,337 +1,337 @@
1
- // Generated by scripts/sync-shared.mjs from shared/client/settings/PluginSettingsCard.tsx. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
2
- /**
3
- * Family-shared chrome for plugin settings cards: a disclosure header naming
4
- * the plugin and what its settings govern, the controls inside, and the save
5
- * that writes them. Renders nothing while the namespace is unavailable — a
6
- * deployment that does not compose the owning plugin should show no trace of
7
- * it. Inlined into each consumer's client bundle; mirrors the official
8
- * ui-plugin-config PluginCard in a self-contained slice.
9
- */
10
-
11
- import { useState, type ReactNode } from 'react'
12
- import type { CardShell } from './settings-form.ts'
13
- import css from './settings-card.module.css'
14
-
15
- /** Copy keys the card chrome itself reads; every consumer locale carries this shared vocabulary. */
16
- export const CARD_COPY_KEYS = [
17
- 'settings.collapse',
18
- 'settings.expand',
19
- 'settings.notExposed',
20
- 'settings.unsaved',
21
- 'settings.readOnly',
22
- 'settings.saveFailed',
23
- 'settings.discard',
24
- 'settings.save',
25
- 'settings.saving',
26
- ] as const
27
-
28
- /** Copy key the card chrome itself reads. */
29
- export type CardCopyKey = (typeof CARD_COPY_KEYS)[number]
30
-
31
- /** Key domain for the plugin's own copy (inferred per consumer). */
32
- export type SettingsCardKey<TKey extends string = string> = TKey | CardCopyKey
33
-
34
- /** Card chrome shared by every plugin settings card. */
35
- export interface PluginSettingsCardProps<TKey extends string = string> {
36
- /** Locale reader for this card's copy. */
37
- t: (key: SettingsCardKey<TKey>, params?: Record<string, string | number>) => string
38
- /** Locale key of the plugin's name. */
39
- titleKey: TKey
40
- /** Locale key of the line describing what this plugin's settings govern. */
41
- descriptionKey: TKey
42
- /** The card's form state: availability, writability, and what a save would do. */
43
- state: CardShell
44
- /** Write every staged edit. */
45
- onSave: () => void
46
- /** Drop every staged edit. */
47
- onDiscard: () => void
48
- /**
49
- * Render the controls expanded on arrival (still collapsible). Defaults to
50
- * true: promoted first-level sections show their settings immediately.
51
- */
52
- defaultOpen?: boolean
53
- /**
54
- * Render without the collapse affordance: a static header and an always
55
- * visible body. Used by first-level settings sections whose nav entry
56
- * already provides the selection.
57
- */
58
- alwaysOpen?: boolean
59
- /** The plugin's controls. */
60
- children: ReactNode
61
- }
62
-
63
- /**
64
- * Render one plugin settings card.
65
- * @param props - the plugin's copy keys, its form state, and its controls.
66
- * @returns the card, or nothing while the namespace is still loading.
67
- */
68
- export function PluginSettingsCard<TKey extends string = string>(props: PluginSettingsCardProps<TKey>) {
69
- const [open, setOpen] = useState(props.defaultOpen ?? true)
70
- const { state, alwaysOpen } = props
71
- if (!state.available) return null
72
- const title = props.t(props.titleKey)
73
- const description = props.t(props.descriptionKey)
74
- const blocked = !state.dirty || state.invalid || state.saving
75
- const expanded = alwaysOpen === true || open
76
- const cardClass = expanded ? `${css.cardOpen} ${css.card}` : css.card
77
- // With alwaysOpen the nav entry already provides the selection, so the
78
- // header is a static title row instead of a disclosure button.
79
- const header = alwaysOpen === true
80
- ? (
81
- <div className={css.headerStatic}>
82
- <span className={css.headText}>
83
- <span className={css.name} title={title}>{title}</span>
84
- <span className={css.description} title={description}>{description}</span>
85
- </span>
86
- {state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
87
- </div>
88
- )
89
- : (
90
- <button
91
- type="button"
92
- className={css.header}
93
- aria-expanded={open}
94
- aria-label={`${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`}
95
- onClick={() => { setOpen(!open) }}
96
- >
97
- <span className={css.headText}>
98
- <span className={css.name} title={title}>{title}</span>
99
- <span className={css.description} title={description}>{description}</span>
100
- </span>
101
- {state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
102
- <svg
103
- width="14"
104
- height="14"
105
- viewBox="0 0 14 14"
106
- fill="none"
107
- xmlns="http://www.w3.org/2000/svg"
108
- className={open ? `${css.chevron} ${css.chevronOpen}` : css.chevron}
109
- >
110
- <path
111
- d="M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z"
112
- fill="currentColor"
113
- />
114
- </svg>
115
- </button>
116
- )
117
- // The namespace exists but the Host does not serve it to this client (the
118
- // official settings allowlist omits third-party namespaces): show a card
119
- // that explains the gap instead of vanishing, so a missing card never
120
- // reads as a missing plugin.
121
- if (!state.exposed) {
122
- return (
123
- <li className={cardClass}>
124
- {header}
125
- {expanded
126
- ? (
127
- <div className={css.body}>
128
- <p className={css.notExposed} role="status">{props.t('settings.notExposed')}</p>
129
- </div>
130
- )
131
- : null}
132
- </li>
133
- )
134
- }
135
- return (
136
- <li className={cardClass}>
137
- {header}
138
- {expanded
139
- ? (
140
- <div className={css.body}>
141
- {!state.writable ? <p className={css.readOnly} role="status">{props.t('settings.readOnly')}</p> : null}
142
- {props.children}
143
- <div className={css.footer}>
144
- {state.failed
145
- ? (
146
- <p className={css.failed} role="status">
147
- {props.t('settings.saveFailed')}{state.failedReason ? ' - ' + state.failedReason : ''}
148
- </p>
149
- )
150
- : null}
151
- <button
152
- type="button"
153
- className={css.discard}
154
- disabled={!state.dirty || state.saving}
155
- onClick={props.onDiscard}
156
- >
157
- {props.t('settings.discard')}
158
- </button>
159
- <button
160
- type="button"
161
- className={css.save}
162
- disabled={blocked}
163
- onClick={props.onSave}
164
- >
165
- {props.t(!state.saving ? 'settings.save' : 'settings.saving')}
166
- </button>
167
- </div>
168
- </div>
169
- )
170
- : null}
171
- </li>
172
- )
173
- }
174
-
175
- /** Props every field control needs regardless of its value type. */
176
- export interface FieldProps {
177
- /** Stable id associating the label with its control. */
178
- id: string
179
- /** Visible label. */
180
- label: string
181
- /** One-line explanation rendered under the control. */
182
- hint: string
183
- /** Draft text this control renders. */
184
- text: string
185
- /** True when saving would leave a user-layer entry for this field. */
186
- overridden: boolean
187
- /** True when the draft is not a value this field accepts. */
188
- invalid: boolean
189
- /** Copy for the overridden badge. */
190
- overriddenLabel: string
191
- /** Copy for the reset control. */
192
- resetLabel: string
193
- /** Copy shown in place of the hint while the draft is invalid. */
194
- invalidLabel: string
195
- /** Disables every control (read-only document, or an unavailable namespace). */
196
- disabled: boolean
197
- /** Stage draft text. */
198
- onEdit: (text: string) => void
199
- /** Stage a clear so the field re-inherits the composition layer. */
200
- onReset: () => void
201
- }
202
-
203
- /** A staged value field. `numeric` only hints the keypad: which drafts a field accepts is decided by its spec. */
204
- export function ValueField(props: FieldProps & {
205
- /** Hints a numeric keypad without narrowing what the control accepts. */
206
- numeric?: boolean
207
- /** Placeholder shown while the draft is empty. */
208
- placeholder?: string
209
- }) {
210
- return (
211
- <div className={css.field}>
212
- <div className={css.head}>
213
- <label className={css.label} htmlFor={props.id}>{props.label}</label>
214
- {props.overridden
215
- ? (
216
- <span className={css.badges}>
217
- <span className={css.badge}>{props.overriddenLabel}</span>
218
- <button
219
- type="button"
220
- className={css.reset}
221
- disabled={props.disabled}
222
- onClick={props.onReset}
223
- >
224
- {props.resetLabel}
225
- </button>
226
- </span>
227
- )
228
- : null}
229
- </div>
230
- <input
231
- id={props.id}
232
- className={props.invalid ? css.inputInvalid : css.input}
233
- type="text"
234
- {...props.numeric === true ? { inputMode: 'numeric' as const } : {}}
235
- {...props.invalid ? { 'aria-invalid': true } : {}}
236
- value={props.text}
237
- placeholder={props.placeholder ?? ''}
238
- disabled={props.disabled}
239
- onChange={(event) => { props.onEdit(event.target.value) }}
240
- />
241
- <p className={props.invalid ? css.invalid : css.hint}>
242
- {props.invalid ? props.invalidLabel : props.hint}
243
- </p>
244
- </div>
245
- )
246
- }
247
-
248
- /** A staged boolean field: 继承 / 开 / 关. */
249
- export function BooleanField(props: FieldProps & {
250
- /** Copy for the inherit option. */
251
- inheritLabel: string
252
- /** Copy for the on option. */
253
- onLabel: string
254
- /** Copy for the off option. */
255
- offLabel: string
256
- }) {
257
- return (
258
- <div className={css.field}>
259
- <div className={css.head}>
260
- <label className={css.label} htmlFor={props.id}>{props.label}</label>
261
- {props.overridden
262
- ? (
263
- <span className={css.badges}>
264
- <span className={css.badge}>{props.overriddenLabel}</span>
265
- <button
266
- type="button"
267
- className={css.reset}
268
- disabled={props.disabled}
269
- onClick={props.onReset}
270
- >
271
- {props.resetLabel}
272
- </button>
273
- </span>
274
- )
275
- : null}
276
- </div>
277
- <select
278
- id={props.id}
279
- className={css.select}
280
- value={props.text}
281
- disabled={props.disabled}
282
- onChange={(event) => { props.onEdit(event.target.value) }}
283
- >
284
- <option value="">{props.inheritLabel}</option>
285
- <option value="true">{props.onLabel}</option>
286
- <option value="false">{props.offLabel}</option>
287
- </select>
288
- <p className={css.hint}>{props.hint}</p>
289
- </div>
290
- )
291
- }
292
-
293
- /** A staged enumerated field rendered as a select. */
294
- export function ChoiceField(props: FieldProps & {
295
- /** Copy for the inherit option (draft text is the empty string). */
296
- inheritLabel: string
297
- /** Choices rendered in order; `value` is the draft/stored text. */
298
- choices: ReadonlyArray<{ value: string; label: string }>
299
- }) {
300
- return (
301
- <div className={css.field}>
302
- <div className={css.head}>
303
- <label className={css.label} htmlFor={props.id}>{props.label}</label>
304
- {props.overridden
305
- ? (
306
- <span className={css.badges}>
307
- <span className={css.badge}>{props.overriddenLabel}</span>
308
- <button
309
- type="button"
310
- className={css.reset}
311
- disabled={props.disabled}
312
- onClick={props.onReset}
313
- >
314
- {props.resetLabel}
315
- </button>
316
- </span>
317
- )
318
- : null}
319
- </div>
320
- <select
321
- id={props.id}
322
- className={css.select}
323
- value={props.text}
324
- disabled={props.disabled}
325
- onChange={(event) => { props.onEdit(event.target.value) }}
326
- >
327
- <option value="">{props.inheritLabel}</option>
328
- {props.choices.map(choice => (
329
- <option key={choice.value} value={choice.value}>{choice.label}</option>
330
- ))}
331
- </select>
332
- <p className={props.invalid ? css.invalid : css.hint}>
333
- {props.invalid ? props.invalidLabel : props.hint}
334
- </p>
335
- </div>
336
- )
337
- }
1
+ // Generated by scripts/sync-shared.mjs from shared/client/settings/PluginSettingsCard.tsx. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
2
+ /**
3
+ * Family-shared chrome for plugin settings cards: a disclosure header naming
4
+ * the plugin and what its settings govern, the controls inside, and the save
5
+ * that writes them. Renders nothing while the namespace is unavailable — a
6
+ * deployment that does not compose the owning plugin should show no trace of
7
+ * it. Inlined into each consumer's client bundle; mirrors the official
8
+ * ui-plugin-config PluginCard in a self-contained slice.
9
+ */
10
+
11
+ import { useState, type ReactNode } from 'react'
12
+ import type { CardShell } from './settings-form.ts'
13
+ import css from './settings-card.module.css'
14
+
15
+ /** Copy keys the card chrome itself reads; every consumer locale carries this shared vocabulary. */
16
+ export const CARD_COPY_KEYS = [
17
+ 'settings.collapse',
18
+ 'settings.expand',
19
+ 'settings.notExposed',
20
+ 'settings.unsaved',
21
+ 'settings.readOnly',
22
+ 'settings.saveFailed',
23
+ 'settings.discard',
24
+ 'settings.save',
25
+ 'settings.saving',
26
+ ] as const
27
+
28
+ /** Copy key the card chrome itself reads. */
29
+ export type CardCopyKey = (typeof CARD_COPY_KEYS)[number]
30
+
31
+ /** Key domain for the plugin's own copy (inferred per consumer). */
32
+ export type SettingsCardKey<TKey extends string = string> = TKey | CardCopyKey
33
+
34
+ /** Card chrome shared by every plugin settings card. */
35
+ export interface PluginSettingsCardProps<TKey extends string = string> {
36
+ /** Locale reader for this card's copy. */
37
+ t: (key: SettingsCardKey<TKey>, params?: Record<string, string | number>) => string
38
+ /** Locale key of the plugin's name. */
39
+ titleKey: TKey
40
+ /** Locale key of the line describing what this plugin's settings govern. */
41
+ descriptionKey: TKey
42
+ /** The card's form state: availability, writability, and what a save would do. */
43
+ state: CardShell
44
+ /** Write every staged edit. */
45
+ onSave: () => void
46
+ /** Drop every staged edit. */
47
+ onDiscard: () => void
48
+ /**
49
+ * Render the controls expanded on arrival (still collapsible). Defaults to
50
+ * true: promoted first-level sections show their settings immediately.
51
+ */
52
+ defaultOpen?: boolean
53
+ /**
54
+ * Render without the collapse affordance: a static header and an always
55
+ * visible body. Used by first-level settings sections whose nav entry
56
+ * already provides the selection.
57
+ */
58
+ alwaysOpen?: boolean
59
+ /** The plugin's controls. */
60
+ children: ReactNode
61
+ }
62
+
63
+ /**
64
+ * Render one plugin settings card.
65
+ * @param props - the plugin's copy keys, its form state, and its controls.
66
+ * @returns the card, or nothing while the namespace is still loading.
67
+ */
68
+ export function PluginSettingsCard<TKey extends string = string>(props: PluginSettingsCardProps<TKey>) {
69
+ const [open, setOpen] = useState(props.defaultOpen ?? true)
70
+ const { state, alwaysOpen } = props
71
+ if (!state.available) return null
72
+ const title = props.t(props.titleKey)
73
+ const description = props.t(props.descriptionKey)
74
+ const blocked = !state.dirty || state.invalid || state.saving
75
+ const expanded = alwaysOpen === true || open
76
+ const cardClass = expanded ? `${css.cardOpen} ${css.card}` : css.card
77
+ // With alwaysOpen the nav entry already provides the selection, so the
78
+ // header is a static title row instead of a disclosure button.
79
+ const header = alwaysOpen === true
80
+ ? (
81
+ <div className={css.headerStatic}>
82
+ <span className={css.headText}>
83
+ <span className={css.name} title={title}>{title}</span>
84
+ <span className={css.description} title={description}>{description}</span>
85
+ </span>
86
+ {state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
87
+ </div>
88
+ )
89
+ : (
90
+ <button
91
+ type="button"
92
+ className={css.header}
93
+ aria-expanded={open}
94
+ aria-label={`${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`}
95
+ onClick={() => { setOpen(!open) }}
96
+ >
97
+ <span className={css.headText}>
98
+ <span className={css.name} title={title}>{title}</span>
99
+ <span className={css.description} title={description}>{description}</span>
100
+ </span>
101
+ {state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
102
+ <svg
103
+ width="14"
104
+ height="14"
105
+ viewBox="0 0 14 14"
106
+ fill="none"
107
+ xmlns="http://www.w3.org/2000/svg"
108
+ className={open ? `${css.chevron} ${css.chevronOpen}` : css.chevron}
109
+ >
110
+ <path
111
+ d="M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z"
112
+ fill="currentColor"
113
+ />
114
+ </svg>
115
+ </button>
116
+ )
117
+ // The namespace exists but the Host does not serve it to this client (the
118
+ // official settings allowlist omits third-party namespaces): show a card
119
+ // that explains the gap instead of vanishing, so a missing card never
120
+ // reads as a missing plugin.
121
+ if (!state.exposed) {
122
+ return (
123
+ <li className={cardClass}>
124
+ {header}
125
+ {expanded
126
+ ? (
127
+ <div className={css.body}>
128
+ <p className={css.notExposed} role="status">{props.t('settings.notExposed')}</p>
129
+ </div>
130
+ )
131
+ : null}
132
+ </li>
133
+ )
134
+ }
135
+ return (
136
+ <li className={cardClass}>
137
+ {header}
138
+ {expanded
139
+ ? (
140
+ <div className={css.body}>
141
+ {!state.writable ? <p className={css.readOnly} role="status">{props.t('settings.readOnly')}</p> : null}
142
+ {props.children}
143
+ <div className={css.footer}>
144
+ {state.failed
145
+ ? (
146
+ <p className={css.failed} role="status">
147
+ {props.t('settings.saveFailed')}{state.failedReason ? ' - ' + state.failedReason : ''}
148
+ </p>
149
+ )
150
+ : null}
151
+ <button
152
+ type="button"
153
+ className={css.discard}
154
+ disabled={!state.dirty || state.saving}
155
+ onClick={props.onDiscard}
156
+ >
157
+ {props.t('settings.discard')}
158
+ </button>
159
+ <button
160
+ type="button"
161
+ className={css.save}
162
+ disabled={blocked}
163
+ onClick={props.onSave}
164
+ >
165
+ {props.t(!state.saving ? 'settings.save' : 'settings.saving')}
166
+ </button>
167
+ </div>
168
+ </div>
169
+ )
170
+ : null}
171
+ </li>
172
+ )
173
+ }
174
+
175
+ /** Props every field control needs regardless of its value type. */
176
+ export interface FieldProps {
177
+ /** Stable id associating the label with its control. */
178
+ id: string
179
+ /** Visible label. */
180
+ label: string
181
+ /** One-line explanation rendered under the control. */
182
+ hint: string
183
+ /** Draft text this control renders. */
184
+ text: string
185
+ /** True when saving would leave a user-layer entry for this field. */
186
+ overridden: boolean
187
+ /** True when the draft is not a value this field accepts. */
188
+ invalid: boolean
189
+ /** Copy for the overridden badge. */
190
+ overriddenLabel: string
191
+ /** Copy for the reset control. */
192
+ resetLabel: string
193
+ /** Copy shown in place of the hint while the draft is invalid. */
194
+ invalidLabel: string
195
+ /** Disables every control (read-only document, or an unavailable namespace). */
196
+ disabled: boolean
197
+ /** Stage draft text. */
198
+ onEdit: (text: string) => void
199
+ /** Stage a clear so the field re-inherits the composition layer. */
200
+ onReset: () => void
201
+ }
202
+
203
+ /** A staged value field. `numeric` only hints the keypad: which drafts a field accepts is decided by its spec. */
204
+ export function ValueField(props: FieldProps & {
205
+ /** Hints a numeric keypad without narrowing what the control accepts. */
206
+ numeric?: boolean
207
+ /** Placeholder shown while the draft is empty. */
208
+ placeholder?: string
209
+ }) {
210
+ return (
211
+ <div className={css.field}>
212
+ <div className={css.head}>
213
+ <label className={css.label} htmlFor={props.id}>{props.label}</label>
214
+ {props.overridden
215
+ ? (
216
+ <span className={css.badges}>
217
+ <span className={css.badge}>{props.overriddenLabel}</span>
218
+ <button
219
+ type="button"
220
+ className={css.reset}
221
+ disabled={props.disabled}
222
+ onClick={props.onReset}
223
+ >
224
+ {props.resetLabel}
225
+ </button>
226
+ </span>
227
+ )
228
+ : null}
229
+ </div>
230
+ <input
231
+ id={props.id}
232
+ className={props.invalid ? css.inputInvalid : css.input}
233
+ type="text"
234
+ {...props.numeric === true ? { inputMode: 'numeric' as const } : {}}
235
+ {...props.invalid ? { 'aria-invalid': true } : {}}
236
+ value={props.text}
237
+ placeholder={props.placeholder ?? ''}
238
+ disabled={props.disabled}
239
+ onChange={(event) => { props.onEdit(event.target.value) }}
240
+ />
241
+ <p className={props.invalid ? css.invalid : css.hint}>
242
+ {props.invalid ? props.invalidLabel : props.hint}
243
+ </p>
244
+ </div>
245
+ )
246
+ }
247
+
248
+ /** A staged boolean field: 继承 / 开 / 关. */
249
+ export function BooleanField(props: FieldProps & {
250
+ /** Copy for the inherit option. */
251
+ inheritLabel: string
252
+ /** Copy for the on option. */
253
+ onLabel: string
254
+ /** Copy for the off option. */
255
+ offLabel: string
256
+ }) {
257
+ return (
258
+ <div className={css.field}>
259
+ <div className={css.head}>
260
+ <label className={css.label} htmlFor={props.id}>{props.label}</label>
261
+ {props.overridden
262
+ ? (
263
+ <span className={css.badges}>
264
+ <span className={css.badge}>{props.overriddenLabel}</span>
265
+ <button
266
+ type="button"
267
+ className={css.reset}
268
+ disabled={props.disabled}
269
+ onClick={props.onReset}
270
+ >
271
+ {props.resetLabel}
272
+ </button>
273
+ </span>
274
+ )
275
+ : null}
276
+ </div>
277
+ <select
278
+ id={props.id}
279
+ className={css.select}
280
+ value={props.text}
281
+ disabled={props.disabled}
282
+ onChange={(event) => { props.onEdit(event.target.value) }}
283
+ >
284
+ <option value="">{props.inheritLabel}</option>
285
+ <option value="true">{props.onLabel}</option>
286
+ <option value="false">{props.offLabel}</option>
287
+ </select>
288
+ <p className={css.hint}>{props.hint}</p>
289
+ </div>
290
+ )
291
+ }
292
+
293
+ /** A staged enumerated field rendered as a select. */
294
+ export function ChoiceField(props: FieldProps & {
295
+ /** Copy for the inherit option (draft text is the empty string). */
296
+ inheritLabel: string
297
+ /** Choices rendered in order; `value` is the draft/stored text. */
298
+ choices: ReadonlyArray<{ value: string; label: string }>
299
+ }) {
300
+ return (
301
+ <div className={css.field}>
302
+ <div className={css.head}>
303
+ <label className={css.label} htmlFor={props.id}>{props.label}</label>
304
+ {props.overridden
305
+ ? (
306
+ <span className={css.badges}>
307
+ <span className={css.badge}>{props.overriddenLabel}</span>
308
+ <button
309
+ type="button"
310
+ className={css.reset}
311
+ disabled={props.disabled}
312
+ onClick={props.onReset}
313
+ >
314
+ {props.resetLabel}
315
+ </button>
316
+ </span>
317
+ )
318
+ : null}
319
+ </div>
320
+ <select
321
+ id={props.id}
322
+ className={css.select}
323
+ value={props.text}
324
+ disabled={props.disabled}
325
+ onChange={(event) => { props.onEdit(event.target.value) }}
326
+ >
327
+ <option value="">{props.inheritLabel}</option>
328
+ {props.choices.map(choice => (
329
+ <option key={choice.value} value={choice.value}>{choice.label}</option>
330
+ ))}
331
+ </select>
332
+ <p className={props.invalid ? css.invalid : css.hint}>
333
+ {props.invalid ? props.invalidLabel : props.hint}
334
+ </p>
335
+ </div>
336
+ )
337
+ }