@morlay/dsh-client-ui-primitives 0.0.2-alpha.7 → 0.0.2-alpha.8
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.md +29 -3
- package/dist/client.cjs +1062 -406
- package/dist/client.d.cts +328 -3
- package/dist/client.d.mts +328 -3
- package/package.json +5 -4
- package/src/client/index.ts +1 -0
- package/src/client/settings-form/SettingsForm.styles.ts +57 -0
- package/src/client/settings-form/SettingsForm.tsx +72 -0
- package/src/client/settings-form/fields.styles.ts +157 -0
- package/src/client/settings-form/fields.tsx +193 -0
- package/src/client/settings-form/form-model.ts +421 -0
- package/src/client/settings-form/index.ts +19 -0
- package/src/client/theme.generated.ts +748 -731
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// 搬自 vendor/deepseek-harness/packages/client/ui-primitives/src/settings-form/form-model.ts
|
|
2
|
+
// (上游 `@deepseek-ai/dsh-client-ui-primitives`,见本包 .agents/adrs 里的搬迁记录);逻辑未改,
|
|
3
|
+
// 只有 catch 的未用参数按 lint 口径写成可选绑定。改这里之前先看上游是否也改了同一处。
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The staged form model behind a plugin's settings page.
|
|
7
|
+
*
|
|
8
|
+
* A card stages what the user types and writes it only when they save. Each
|
|
9
|
+
* settings write is a durable, revision-fenced document mutation, so a control
|
|
10
|
+
* that committed as it settled turned one edit into a write the user never
|
|
11
|
+
* asked for and could not preview; staged text makes what is on screen exactly
|
|
12
|
+
* what a save would store.
|
|
13
|
+
*
|
|
14
|
+
* A field shows its effective value — the user layer over the composition
|
|
15
|
+
* layer over the schema default — and whether the user layer carries it. That
|
|
16
|
+
* presence, not a value comparison, is what marks a field overridden: an
|
|
17
|
+
* override equal to the composition default is still an override.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createSnapshotStore, type SnapshotStore } from "@deepseek-ai/dsh-client-store";
|
|
21
|
+
|
|
22
|
+
/** What the model reads of one Host entry's form. */
|
|
23
|
+
export interface SettingsFormScopeSnapshot<T> {
|
|
24
|
+
/** `ready` while the Host serves the entry to this client; the form renders nothing otherwise. */
|
|
25
|
+
status: "loading" | "ready" | "unavailable";
|
|
26
|
+
/** Last accepted schema-resolved section; undefined before the first acceptance. */
|
|
27
|
+
value: T | undefined;
|
|
28
|
+
/** Composition layer the value resolves over: what a field reverts to once cleared. */
|
|
29
|
+
base: unknown;
|
|
30
|
+
/** Raw user layer as stored; a field's PRESENCE here is what marks it overridden. */
|
|
31
|
+
user: unknown;
|
|
32
|
+
/** Whether the Host document accepts writes. */
|
|
33
|
+
writable: boolean;
|
|
34
|
+
/** Revision the snapshot was read at; a save fences its mutation with the revision its drafts started from. */
|
|
35
|
+
revision: number | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One path edit a save sends, as the shared configuration form's `mutate` accepts it. */
|
|
39
|
+
export type SettingsFormPathOp =
|
|
40
|
+
| { op: "set"; path: readonly string[]; value: unknown }
|
|
41
|
+
| { op: "unset"; path: readonly string[] };
|
|
42
|
+
|
|
43
|
+
/** The entry form the model stages over: the reads and the atomic write of the form `ui-settings` shares per Host entry. */
|
|
44
|
+
export interface SettingsFormScope<T> {
|
|
45
|
+
/** @returns the current sync snapshot. */
|
|
46
|
+
getSnapshot(): SettingsFormScopeSnapshot<T>;
|
|
47
|
+
/**
|
|
48
|
+
* Observe snapshot replacements.
|
|
49
|
+
* @param listener - invoked after each snapshot change.
|
|
50
|
+
* @returns the disposer removing this listener.
|
|
51
|
+
*/
|
|
52
|
+
subscribe(listener: () => void): () => void;
|
|
53
|
+
/**
|
|
54
|
+
* Apply ordered field edits in one revision-fenced write.
|
|
55
|
+
* @param ops - the edits, in staging order.
|
|
56
|
+
* @param expectedRevision - the revision the drafts were staged against, when known.
|
|
57
|
+
* @returns true for Host acceptance, false for refusal, after any recovery read.
|
|
58
|
+
*/
|
|
59
|
+
mutate(ops: readonly SettingsFormPathOp[], expectedRevision?: number): Promise<boolean>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The write one field's staged text performs when the card is saved. */
|
|
63
|
+
export type SettingsFieldWrite = { kind: "set"; value: unknown } | { kind: "clear" };
|
|
64
|
+
|
|
65
|
+
/** How one section field converts between its stored value and its draft text. */
|
|
66
|
+
export interface SettingsFieldSpec {
|
|
67
|
+
/** Field name inside the namespace section. */
|
|
68
|
+
field: string;
|
|
69
|
+
/** Render a stored value as draft text; the empty string when the section carries none. */
|
|
70
|
+
format: (value: unknown) => string;
|
|
71
|
+
/**
|
|
72
|
+
* The write this draft text stages, or undefined when the text is not a
|
|
73
|
+
* value this field accepts — which blocks the save rather than discarding it.
|
|
74
|
+
*/
|
|
75
|
+
parse: (text: string) => SettingsFieldWrite | undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A control whose value is written outside the settings section. A credential
|
|
80
|
+
* literal never rides a response, so its draft has nothing to seed from: it is
|
|
81
|
+
* blank until typed, and a blank draft writes nothing.
|
|
82
|
+
*/
|
|
83
|
+
export interface SettingsSecretSpec {
|
|
84
|
+
/** Field name addressing this control inside the card's form. */
|
|
85
|
+
field: string;
|
|
86
|
+
/** Write the staged text; resolves to whether the Host accepted it. */
|
|
87
|
+
write: (text: string) => Promise<boolean>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One field as a card's control renders it. */
|
|
91
|
+
export interface SettingsFieldState {
|
|
92
|
+
/** Draft text the control renders. */
|
|
93
|
+
text: string;
|
|
94
|
+
/**
|
|
95
|
+
* Whether saving would leave a user-layer entry for this field. A staged
|
|
96
|
+
* edit answers for itself, so the badge previews the save rather than
|
|
97
|
+
* reporting a state the pending edit already contradicts.
|
|
98
|
+
*/
|
|
99
|
+
overridden: boolean;
|
|
100
|
+
/** Whether the draft is not a value this field accepts, which blocks saving. */
|
|
101
|
+
invalid: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Form state every plugin card shares. */
|
|
105
|
+
export interface SettingsFormShell {
|
|
106
|
+
/** False while the namespace is not served to this client; the card renders nothing. */
|
|
107
|
+
available: boolean;
|
|
108
|
+
/** Whether the Host document accepts writes. */
|
|
109
|
+
writable: boolean;
|
|
110
|
+
/** Whether the form holds edits that a save would write. */
|
|
111
|
+
dirty: boolean;
|
|
112
|
+
/** Whether any staged draft is invalid, which blocks the save. */
|
|
113
|
+
invalid: boolean;
|
|
114
|
+
/** Whether a save is crossing the wire. */
|
|
115
|
+
saving: boolean;
|
|
116
|
+
/** Whether the last save did not land as staged; cleared by the next edit or save. */
|
|
117
|
+
failed: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The write actions every plugin card's slot entry injects. */
|
|
121
|
+
export interface SettingsFormActions {
|
|
122
|
+
/** Stage draft text for one field. */
|
|
123
|
+
edit: (field: string, text: string) => void;
|
|
124
|
+
/** Stage a clear, so saving lets the field re-inherit the composition layer. */
|
|
125
|
+
resetField: (field: string) => void;
|
|
126
|
+
/** Write every staged edit, then re-seed from what the Host accepted. */
|
|
127
|
+
save: () => void;
|
|
128
|
+
/** Drop every staged edit. */
|
|
129
|
+
discard: () => void;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** One field's staged edit. */
|
|
133
|
+
interface StagedEdit {
|
|
134
|
+
/** Draft text the control renders. */
|
|
135
|
+
text: string;
|
|
136
|
+
/** True when this edit clears the field whatever text it shows. */
|
|
137
|
+
clear: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** One staged edit resolved into the write a save performs. */
|
|
141
|
+
interface PlannedWrite {
|
|
142
|
+
/** Field this entry writes. */
|
|
143
|
+
field: string;
|
|
144
|
+
/**
|
|
145
|
+
* Perform the write and report whether the Host holds the staged value
|
|
146
|
+
* afterwards; undefined when the draft is not a value the field accepts.
|
|
147
|
+
*/
|
|
148
|
+
run?: () => Promise<boolean>;
|
|
149
|
+
op?: SettingsFormPathOp;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A whole-number field. An empty draft clears the field; any other draft that
|
|
154
|
+
* is not a finite number blocks the save.
|
|
155
|
+
* @param field - field name inside the namespace section.
|
|
156
|
+
* @returns the field's conversion spec.
|
|
157
|
+
*/
|
|
158
|
+
export function settingsNumberField(field: string): SettingsFieldSpec {
|
|
159
|
+
return {
|
|
160
|
+
field,
|
|
161
|
+
// A section that carries no number for this field renders empty rather
|
|
162
|
+
// than as a value nobody chose.
|
|
163
|
+
format: (value) => (typeof value === "number" ? String(value) : ""),
|
|
164
|
+
parse: (text) => {
|
|
165
|
+
const trimmed = text.trim();
|
|
166
|
+
if (trimmed === "") return { kind: "clear" };
|
|
167
|
+
const parsed = Number(trimmed);
|
|
168
|
+
return Number.isFinite(parsed) ? { kind: "set", value: parsed } : undefined;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A free-text field. An empty draft clears the field, so emptying the control
|
|
175
|
+
* and saving is the same gesture as resetting it.
|
|
176
|
+
* @param field - field name inside the namespace section.
|
|
177
|
+
* @returns the field's conversion spec.
|
|
178
|
+
*/
|
|
179
|
+
export function settingsTextField(field: string): SettingsFieldSpec {
|
|
180
|
+
return {
|
|
181
|
+
field,
|
|
182
|
+
format: (value) => (typeof value === "string" ? value : ""),
|
|
183
|
+
parse: (text) => {
|
|
184
|
+
const trimmed = text.trim();
|
|
185
|
+
return trimmed === "" ? { kind: "clear" } : { kind: "set", value: trimmed };
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Stages one card's edits over one settings namespace and writes them on save.
|
|
192
|
+
*
|
|
193
|
+
* The form publishes through a snapshot store because slot components read
|
|
194
|
+
* through a snapshot selector, while both the scope and the local drafts
|
|
195
|
+
* change underneath; every projection is rebuilt from the two together.
|
|
196
|
+
*/
|
|
197
|
+
export class SettingsFormModel<T> {
|
|
198
|
+
private readonly specs: Map<string, SettingsFieldSpec>;
|
|
199
|
+
private readonly secretSpecs: Map<string, SettingsSecretSpec>;
|
|
200
|
+
private readonly staged = new Map<string, StagedEdit>();
|
|
201
|
+
private readonly listeners = new Set<() => void>();
|
|
202
|
+
private baseline: SettingsFormScopeSnapshot<T> | undefined;
|
|
203
|
+
private readonly unsubscribe: () => void;
|
|
204
|
+
private saving = false;
|
|
205
|
+
private failed = false;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param scope - the shared configuration form for this card's namespace.
|
|
209
|
+
* @param specs - the section fields this card edits.
|
|
210
|
+
* @param secrets - the card's write-only controls, written outside the section.
|
|
211
|
+
*/
|
|
212
|
+
constructor(
|
|
213
|
+
private readonly scope: SettingsFormScope<T>,
|
|
214
|
+
specs: SettingsFieldSpec[],
|
|
215
|
+
secrets: SettingsSecretSpec[] = [],
|
|
216
|
+
) {
|
|
217
|
+
this.specs = new Map(specs.map((spec) => [spec.field, spec]));
|
|
218
|
+
this.secretSpecs = new Map(secrets.map((spec) => [spec.field, spec]));
|
|
219
|
+
this.unsubscribe = scope.subscribe(() => {
|
|
220
|
+
this.publish();
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Publish a projection of this form, rebuilt whenever the scope or a draft changes.
|
|
226
|
+
* @param project - build the card's state from the form's current reads.
|
|
227
|
+
* @returns the store the card's component reads through its bound selector.
|
|
228
|
+
*/
|
|
229
|
+
bind<S>(project: () => S): SnapshotStore<S> {
|
|
230
|
+
const store = createSnapshotStore(project());
|
|
231
|
+
this.listeners.add(() => {
|
|
232
|
+
store.set(project());
|
|
233
|
+
});
|
|
234
|
+
return store;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Read the card-level state: what the Host serves, and what a save would do.
|
|
239
|
+
* @returns the form state every card shares.
|
|
240
|
+
*/
|
|
241
|
+
shell(): SettingsFormShell {
|
|
242
|
+
const snapshot = this.scope.getSnapshot();
|
|
243
|
+
const plan = this.plan();
|
|
244
|
+
return {
|
|
245
|
+
available: snapshot.status === "ready",
|
|
246
|
+
writable: snapshot.writable,
|
|
247
|
+
dirty: plan.length > 0,
|
|
248
|
+
invalid: plan.some((item) => item.run === undefined && item.op === undefined),
|
|
249
|
+
saving: this.saving,
|
|
250
|
+
failed: this.failed,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Read one control's state.
|
|
256
|
+
* @param field - field name of a section field or of a write-only control.
|
|
257
|
+
* @returns the draft text, whether a save would leave an override, and whether it is invalid.
|
|
258
|
+
*/
|
|
259
|
+
field(field: string): SettingsFieldState {
|
|
260
|
+
const staged = this.staged.get(field);
|
|
261
|
+
if (this.secretSpecs.has(field)) {
|
|
262
|
+
return { text: staged?.text ?? "", overridden: false, invalid: false };
|
|
263
|
+
}
|
|
264
|
+
const spec = this.spec(field);
|
|
265
|
+
if (staged === undefined) {
|
|
266
|
+
return {
|
|
267
|
+
text: spec.format(this.sectionValue(field)),
|
|
268
|
+
overridden: this.stored(field),
|
|
269
|
+
invalid: false,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const write = staged.clear ? { kind: "clear" as const } : spec.parse(staged.text);
|
|
273
|
+
return {
|
|
274
|
+
text: staged.text,
|
|
275
|
+
overridden: write?.kind === "set",
|
|
276
|
+
invalid: write === undefined,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Build the edit, reset, save, and discard actions bound to this form.
|
|
282
|
+
* @returns the actions a card's slot entry injects.
|
|
283
|
+
*/
|
|
284
|
+
actions(): SettingsFormActions {
|
|
285
|
+
return {
|
|
286
|
+
edit: (field, text) => {
|
|
287
|
+
this.stage(field, { text, clear: false });
|
|
288
|
+
},
|
|
289
|
+
resetField: (field) => {
|
|
290
|
+
this.stage(field, { text: this.spec(field).format(this.baseValue(field)), clear: true });
|
|
291
|
+
},
|
|
292
|
+
save: () => {
|
|
293
|
+
void this.save();
|
|
294
|
+
},
|
|
295
|
+
discard: () => {
|
|
296
|
+
if (this.staged.size === 0 && !this.failed) return;
|
|
297
|
+
this.staged.clear();
|
|
298
|
+
this.baseline = undefined;
|
|
299
|
+
this.failed = false;
|
|
300
|
+
this.publish();
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Write every staged edit, then re-seed from what the Host accepted.
|
|
307
|
+
*
|
|
308
|
+
* The Host is the only authority on whether a value was accepted — its
|
|
309
|
+
* validators own the constraints no schema can express — so the outcome is
|
|
310
|
+
* read back from the section rather than predicted here. A save that did not
|
|
311
|
+
* land keeps its drafts, so the user can correct them instead of retyping.
|
|
312
|
+
* @returns settlement after every write and the read-back.
|
|
313
|
+
*/
|
|
314
|
+
async save(): Promise<void> {
|
|
315
|
+
const plan = this.plan();
|
|
316
|
+
if (
|
|
317
|
+
!plan.length ||
|
|
318
|
+
this.saving ||
|
|
319
|
+
!this.scope.getSnapshot().writable ||
|
|
320
|
+
plan.some((item) => item.run === undefined && item.op === undefined)
|
|
321
|
+
)
|
|
322
|
+
return;
|
|
323
|
+
this.saving = true;
|
|
324
|
+
this.failed = false;
|
|
325
|
+
this.publish();
|
|
326
|
+
try {
|
|
327
|
+
const ops = plan.flatMap((item) => (item.op === undefined ? [] : [item.op]));
|
|
328
|
+
let landed = !ops.length || (await this.scope.mutate(ops, this.baseline?.revision));
|
|
329
|
+
if (!landed) {
|
|
330
|
+
this.failed = true;
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
for (const item of plan) if (item.run) landed = (await item.run()) && landed;
|
|
334
|
+
if (landed) {
|
|
335
|
+
this.staged.clear();
|
|
336
|
+
this.baseline = undefined;
|
|
337
|
+
}
|
|
338
|
+
this.failed = !landed;
|
|
339
|
+
} catch {
|
|
340
|
+
this.failed = true;
|
|
341
|
+
} finally {
|
|
342
|
+
this.saving = false;
|
|
343
|
+
this.publish();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Release the form's accepted-value subscription. */
|
|
348
|
+
dispose(): void {
|
|
349
|
+
this.unsubscribe();
|
|
350
|
+
this.listeners.clear();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Every staged edit a save would write. An entry whose draft is not a value
|
|
355
|
+
* its field accepts carries no write: the form is still dirty, and the save
|
|
356
|
+
* refuses rather than dropping the edit.
|
|
357
|
+
* @returns the planned writes, in the order the fields were staged.
|
|
358
|
+
*/
|
|
359
|
+
private plan(): PlannedWrite[] {
|
|
360
|
+
const plan: PlannedWrite[] = [];
|
|
361
|
+
for (const [field, staged] of this.staged) {
|
|
362
|
+
const secret = this.secretSpecs.get(field);
|
|
363
|
+
if (secret !== undefined) {
|
|
364
|
+
const value = staged.text.trim();
|
|
365
|
+
if (value !== "") plan.push({ field, run: () => secret.write(value) });
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const spec = this.spec(field);
|
|
369
|
+
if (staged.clear) {
|
|
370
|
+
if (this.stored(field)) plan.push({ field, op: { op: "unset", path: [field] } });
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (staged.text === spec.format(this.sectionValue(field))) continue;
|
|
374
|
+
const write = spec.parse(staged.text);
|
|
375
|
+
if (write === undefined) plan.push({ field });
|
|
376
|
+
else if (write.kind === "clear") plan.push({ field, op: { op: "unset", path: [field] } });
|
|
377
|
+
else plan.push({ field, op: { op: "set", path: [field], value: write.value } });
|
|
378
|
+
}
|
|
379
|
+
return plan;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private stage(field: string, edit: StagedEdit): void {
|
|
383
|
+
this.baseline ??= this.scope.getSnapshot();
|
|
384
|
+
this.staged.set(field, edit);
|
|
385
|
+
this.failed = false;
|
|
386
|
+
this.publish();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private spec(field: string): SettingsFieldSpec {
|
|
390
|
+
const spec = this.specs.get(field);
|
|
391
|
+
// Every call site names a field this card declared; a missing one is a
|
|
392
|
+
// wiring mistake that must not degrade into a silently inert control.
|
|
393
|
+
if (spec === undefined) throw new Error(`plugin card has no field ${field}`);
|
|
394
|
+
return spec;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private snapshotOf(): SettingsFormScopeSnapshot<T> {
|
|
398
|
+
return this.scope.getSnapshot();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private sectionValue(field: string): unknown {
|
|
402
|
+
return (this.snapshotOf().value as Record<string, unknown> | undefined)?.[field];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private baseValue(field: string): unknown {
|
|
406
|
+
return (this.snapshotOf().base as Record<string, unknown> | undefined)?.[field];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private userLayer(): Record<string, unknown> | undefined {
|
|
410
|
+
return this.snapshotOf().user as Record<string, unknown> | undefined;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private stored(field: string): boolean {
|
|
414
|
+
const user = this.userLayer();
|
|
415
|
+
return user !== undefined && Object.hasOwn(user, field);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private publish(): void {
|
|
419
|
+
for (const listener of this.listeners) listener();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// 设置表单原语的 client 出口:模型 + 控件 + 表单框,命名与上游
|
|
2
|
+
// `@deepseek-ai/dsh-client-ui-primitives` 同形(搬迁记录见本包 .agents/adrs/)。
|
|
3
|
+
|
|
4
|
+
export { SettingsFormModel, settingsNumberField, settingsTextField } from "./form-model.ts";
|
|
5
|
+
export type {
|
|
6
|
+
SettingsFieldSpec,
|
|
7
|
+
SettingsFieldState,
|
|
8
|
+
SettingsFieldWrite,
|
|
9
|
+
SettingsFormActions,
|
|
10
|
+
SettingsFormPathOp,
|
|
11
|
+
SettingsFormScope,
|
|
12
|
+
SettingsFormScopeSnapshot,
|
|
13
|
+
SettingsFormShell,
|
|
14
|
+
SettingsSecretSpec,
|
|
15
|
+
} from "./form-model.ts";
|
|
16
|
+
export { SettingsSecretField, SettingsValueField } from "./fields.tsx";
|
|
17
|
+
export type { SettingsFieldProps } from "./fields.tsx";
|
|
18
|
+
export { SettingsForm } from "./SettingsForm.tsx";
|
|
19
|
+
export type { SettingsFormLabels, SettingsFormProps } from "./SettingsForm.tsx";
|