@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.
- package/LICENSE +38 -38
- package/README.i18n.yaml +6 -6
- package/README.md +66 -66
- package/README.zh.md +66 -66
- package/cordis.patch.yml +13 -13
- package/lib/client.js +27305 -270
- package/lib/client.js.map +1 -1
- package/lib/index.js +474 -42
- package/lib/tsconfig.client.tsbuildinfo +1 -1
- package/lib/tsconfig.host.tsbuildinfo +1 -1
- package/lib/types/client/file-source.d.ts +10 -0
- package/lib/types/client/file-source.d.ts.map +1 -1
- package/lib/types/client/fileType.d.ts +19 -4
- package/lib/types/client/fileType.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +3 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/mount.d.ts +1 -1
- package/lib/types/client/mount.d.ts.map +1 -1
- package/lib/types/client/preview/PreviewPanel.d.ts +2 -1
- package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -1
- package/lib/types/client/preview/PreviewToolbar.d.ts +3 -1
- package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -1
- package/lib/types/client/preview/content.d.ts +3 -1
- package/lib/types/client/preview/content.d.ts.map +1 -1
- package/lib/types/client/preview/office.d.ts +23 -0
- package/lib/types/client/preview/office.d.ts.map +1 -0
- package/lib/types/client/reference.d.ts +17 -0
- package/lib/types/client/reference.d.ts.map +1 -1
- package/lib/types/client/store.d.ts.map +1 -1
- package/lib/types/host/fs-service.d.ts.map +1 -1
- package/lib/types/host/office-preview.d.ts +13 -0
- package/lib/types/host/office-preview.d.ts.map +1 -0
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +19 -4
- package/src/client/FileManagerSettingsCard.tsx +117 -117
- package/src/client/PluginSettingsCard.tsx +337 -337
- package/src/client/chat/file-ref.ts +144 -144
- package/src/client/chat/mermaid-chat.tsx +102 -102
- package/src/client/chat/placeholder-hint.tsx +46 -46
- package/src/client/components/ExplorerPanel.tsx +534 -534
- package/src/client/components/FileIcon.tsx +46 -46
- package/src/client/components/ScmPanel.tsx +480 -480
- package/src/client/components/a11y.ts +20 -20
- package/src/client/components/icons.tsx +274 -274
- package/src/client/components/overlay.tsx +225 -225
- package/src/client/drag/DragFileInlay.tsx +89 -89
- package/src/client/drag/file-drag.ts +67 -67
- package/src/client/drag.ts +154 -154
- package/src/client/file-source.ts +34 -0
- package/src/client/fileType.ts +32 -5
- package/src/client/hooks/useResizableSplit.ts +91 -91
- package/src/client/hooks/useStore.ts +15 -15
- package/src/client/index.ts +42 -3
- package/src/client/layout.ts +523 -523
- package/src/client/locales.ts +6 -0
- package/src/client/maximize.ts +34 -34
- package/src/client/mount.tsx +2 -1
- package/src/client/persist.ts +194 -194
- package/src/client/picker/FilePickerModal.tsx +526 -526
- package/src/client/picker/file-picker.ts +86 -86
- package/src/client/preview/PreviewPanel.tsx +7 -1
- package/src/client/preview/PreviewTabs.tsx +159 -159
- package/src/client/preview/PreviewToolbar.tsx +3 -2
- package/src/client/preview/content.tsx +7 -2
- package/src/client/preview/markdown.ts +351 -351
- package/src/client/preview/mermaid.ts +274 -274
- package/src/client/preview/office.tsx +321 -0
- package/src/client/reference.ts +49 -0
- package/src/client/settings-card.module.css +316 -316
- package/src/client/settings-form.ts +451 -451
- package/src/client/store.ts +17 -12
- package/src/client/styles/drag.module.css +30 -30
- package/src/client/styles/explorer.module.css +371 -371
- package/src/client/styles/picker.module.css +344 -344
- package/src/client/styles/preview.module.css +3 -1
- package/src/client/styles/scm.module.css +380 -380
- package/src/client/styles/tokens.module.css +376 -371
- package/src/core/types.ts +162 -162
- package/src/host/fs-service.ts +7 -1
- package/src/host/gate.ts +73 -73
- package/src/host/git-runner.ts +116 -116
- package/src/host/git-service.ts +395 -395
- package/src/host/loopback.ts +63 -63
- package/src/host/office-preview.ts +399 -0
- package/src/host/poll-guard.ts +109 -109
- package/src/host/routes.ts +72 -1
- package/src/index.ts +3 -1
- package/src/mount-once.ts +48 -48
- package/lib/types/client/chat/AttachedFilesDock.d.ts +0 -51
- package/lib/types/client/chat/AttachedFilesDock.d.ts.map +0 -1
- package/lib/types/client/floating.d.ts +0 -27
- package/lib/types/client/floating.d.ts.map +0 -1
- package/lib/types/client/mention.d.ts +0 -44
- 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
|
+
}
|