@qilin-agent/client-ui-primitives 3.1.3 → 3.1.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/README.i18n.yaml +36 -36
- package/README.md +7 -1
- package/README.zh.md +7 -1
- package/lib/CommandText.module.css +12 -0
- package/lib/MenuSurface.module.css +1 -1
- package/lib/index.js +913 -362
- package/lib/settings-form/SettingsForm.module.css +53 -0
- package/lib/settings-form/fields.module.css +145 -0
- package/lib/types/CommandText.d.ts +16 -0
- package/lib/types/TerminalBlock.d.ts +2 -0
- package/lib/types/darwin-desktop.d.ts +9 -0
- package/lib/types/index.d.ts +9 -1
- package/lib/types/settings-form/SettingsForm.d.ts +46 -0
- package/lib/types/settings-form/fields.d.ts +70 -0
- package/lib/types/settings-form/form-model.d.ts +214 -0
- package/package.json +4 -3
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* Settings form: the controls a page shows under a plugin's title, and the save that writes them. */
|
|
2
|
+
|
|
3
|
+
.form {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.readOnly,
|
|
9
|
+
.unavailable {
|
|
10
|
+
margin: 0 0 12px;
|
|
11
|
+
font-size: 12px;
|
|
12
|
+
line-height: 1.5;
|
|
13
|
+
color: var(--qilin-alias-label-tertiary);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.footer {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
padding-top: 16px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.failed {
|
|
24
|
+
flex: 1;
|
|
25
|
+
min-width: 0;
|
|
26
|
+
margin: 0;
|
|
27
|
+
font-size: 12px;
|
|
28
|
+
line-height: 1.5;
|
|
29
|
+
color: var(--qilin-alias-label-error);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.save {
|
|
33
|
+
appearance: none;
|
|
34
|
+
border: 1px solid transparent;
|
|
35
|
+
border-radius: var(--qilin-radius-md);
|
|
36
|
+
padding: 5px 14px;
|
|
37
|
+
font: inherit;
|
|
38
|
+
font-size: 13px;
|
|
39
|
+
line-height: 1.5;
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
background: var(--qilin-alias-label-primary);
|
|
42
|
+
color: var(--qilin-alias-bg-layer-3);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.save:disabled {
|
|
46
|
+
opacity: 0.4;
|
|
47
|
+
cursor: default;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.save:focus-visible {
|
|
51
|
+
outline: var(--qilin-focus-ring-width) solid var(--qilin-focus-ring-color, var(--qilin-alias-state-business-primary));
|
|
52
|
+
outline-offset: 1px;
|
|
53
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/* Settings form fields: label, control, override badge, and hint. */
|
|
2
|
+
|
|
3
|
+
.field {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: 6px;
|
|
7
|
+
padding: 12px 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.field + .field {
|
|
11
|
+
border-top: 0.5px solid var(--qilin-alias-border-l2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.head {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: 8px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.label {
|
|
21
|
+
flex: 1;
|
|
22
|
+
min-width: 0;
|
|
23
|
+
font-size: 13px;
|
|
24
|
+
font-weight: 500;
|
|
25
|
+
line-height: 1.5;
|
|
26
|
+
color: var(--qilin-alias-label-primary);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.labelGroup {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 4px;
|
|
33
|
+
flex: 1;
|
|
34
|
+
min-width: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.labelGroup > .label {
|
|
38
|
+
flex: 0 1 auto;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.helpButton {
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
flex: none;
|
|
46
|
+
width: 24px;
|
|
47
|
+
height: 24px;
|
|
48
|
+
padding: 0;
|
|
49
|
+
border: 0;
|
|
50
|
+
border-radius: var(--qilin-radius-sm);
|
|
51
|
+
background: none;
|
|
52
|
+
color: var(--qilin-alias-label-tertiary);
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.helpButton:hover,
|
|
57
|
+
.helpButton[aria-expanded='true'] {
|
|
58
|
+
background: var(--qilin-alias-bg-layer-4);
|
|
59
|
+
color: var(--qilin-alias-label-secondary);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.helpButton:focus-visible {
|
|
63
|
+
outline: var(--qilin-focus-ring-width) solid var(--qilin-focus-ring-color, var(--qilin-alias-state-business-primary));
|
|
64
|
+
outline-offset: 1px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.help {
|
|
68
|
+
padding: 10px 0 0;
|
|
69
|
+
font-size: 12px;
|
|
70
|
+
line-height: 1.6;
|
|
71
|
+
color: var(--qilin-alias-label-secondary);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.help > p {
|
|
75
|
+
margin: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.help > p + p {
|
|
79
|
+
margin-top: 8px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.badges {
|
|
83
|
+
display: inline-flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
gap: 8px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.reset {
|
|
89
|
+
border: none;
|
|
90
|
+
background: none;
|
|
91
|
+
padding: 0;
|
|
92
|
+
font: inherit;
|
|
93
|
+
font-size: 12px;
|
|
94
|
+
line-height: 1.5;
|
|
95
|
+
color: var(--qilin-alias-label-secondary);
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.reset:hover:not(:disabled) {
|
|
100
|
+
color: var(--qilin-alias-label-primary);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.reset:disabled {
|
|
104
|
+
cursor: default;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.input {
|
|
108
|
+
height: 34px;
|
|
109
|
+
padding: 0 12px;
|
|
110
|
+
border: 0.5px solid var(--qilin-alias-border-l4);
|
|
111
|
+
border-radius: var(--qilin-radius-md);
|
|
112
|
+
background: var(--qilin-alias-bg-layer-3);
|
|
113
|
+
font: inherit;
|
|
114
|
+
font-size: 13px;
|
|
115
|
+
line-height: 1.5;
|
|
116
|
+
color: var(--qilin-alias-label-primary);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.input:focus-visible {
|
|
120
|
+
outline: none;
|
|
121
|
+
border-color: var(--qilin-alias-state-business-primary);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.input:disabled {
|
|
125
|
+
color: var(--qilin-alias-label-tertiary);
|
|
126
|
+
cursor: default;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.input[aria-invalid='true'] {
|
|
130
|
+
border-color: var(--qilin-alias-state-error-primary);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.invalid {
|
|
134
|
+
margin: 0;
|
|
135
|
+
font-size: 12px;
|
|
136
|
+
line-height: 1.5;
|
|
137
|
+
color: var(--qilin-alias-state-error-primary);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.hint {
|
|
141
|
+
margin: 0;
|
|
142
|
+
font-size: 12px;
|
|
143
|
+
line-height: 1.5;
|
|
144
|
+
color: var(--qilin-alias-label-tertiary);
|
|
145
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Display text and owner-localized identification for a command scrollport. */
|
|
2
|
+
export interface CommandTextProps {
|
|
3
|
+
/** Original command or formatted command arguments, without truncation. */
|
|
4
|
+
text: string;
|
|
5
|
+
/** Accessible name while the text can scroll horizontally. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Owner typography and positioning. */
|
|
8
|
+
className?: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Render command text without adding Tab stops for content that fits.
|
|
12
|
+
* @param props - Text, localized scrollport name, and owner styling.
|
|
13
|
+
* @returns A horizontal text scrollport, named and keyboard-focusable only when overflowing.
|
|
14
|
+
*/
|
|
15
|
+
export declare function CommandText({ text, label, className }: CommandTextProps): import("react").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=CommandText.d.ts.map
|
|
@@ -17,6 +17,8 @@ export interface TerminalBlockLabels {
|
|
|
17
17
|
failed: string;
|
|
18
18
|
/** Run-state text for a clean settle. */
|
|
19
19
|
done: string;
|
|
20
|
+
/** Accessible name of one command line's scrollport, numbered from 1. */
|
|
21
|
+
commandLine: (index: number) => string;
|
|
20
22
|
/** Copy-button idle label. */
|
|
21
23
|
copy: string;
|
|
22
24
|
/** Copy-button label during the post-copy confirmation window. */
|
|
@@ -6,4 +6,13 @@
|
|
|
6
6
|
* @returns true only inside the macOS Electron shell.
|
|
7
7
|
*/
|
|
8
8
|
export declare function isDarwinDesktop(): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Subscribe to `data-platform` flips on `<html>` for decisions made outside
|
|
11
|
+
* render (apply-world projections that cannot re-read the DOM per frame).
|
|
12
|
+
* The listener fires on attribute changes after this call, not for the
|
|
13
|
+
* current value — pair it with a fresh `isDarwinDesktop()` read.
|
|
14
|
+
* @param listener - Called on every `data-platform` attribute change.
|
|
15
|
+
* @returns Disposer that stops watching.
|
|
16
|
+
*/
|
|
17
|
+
export declare function watchDarwinDesktop(listener: () => void): () => void;
|
|
9
18
|
//# sourceMappingURL=darwin-desktop.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cordis-free React primitives styled only through `--qilin-*` tokens.
|
|
3
3
|
*/
|
|
4
|
+
export { SettingsForm } from './settings-form/SettingsForm.tsx';
|
|
5
|
+
export type { SettingsFormLabels, SettingsFormProps } from './settings-form/SettingsForm.tsx';
|
|
6
|
+
export { SettingsSecretField, SettingsValueField } from './settings-form/fields.tsx';
|
|
7
|
+
export type { SettingsFieldProps } from './settings-form/fields.tsx';
|
|
8
|
+
export { SettingsFormModel, settingsNumberField, settingsTextField } from './settings-form/form-model.ts';
|
|
9
|
+
export type { SettingsFieldSpec, SettingsFieldState, SettingsFieldWrite, SettingsFormActions, SettingsFormPathOp, SettingsFormScope, SettingsFormScopeSnapshot, SettingsFormShell, SettingsSecretSpec, } from './settings-form/form-model.ts';
|
|
10
|
+
export { CommandText } from './CommandText.tsx';
|
|
11
|
+
export type { CommandTextProps } from './CommandText.tsx';
|
|
4
12
|
export { StateDot } from './StateDot.tsx';
|
|
5
13
|
export type { StateDotState } from './StateDot.tsx';
|
|
6
14
|
export { DisclosureRow } from './DisclosureRow.tsx';
|
|
@@ -53,7 +61,7 @@ export { useCopyFeedback } from './use-copy-feedback.ts';
|
|
|
53
61
|
export type { CopyFeedback } from './use-copy-feedback.ts';
|
|
54
62
|
export { relativeTime } from './relative-time.ts';
|
|
55
63
|
export { rankByName } from './rank-by-name.ts';
|
|
56
|
-
export { isDarwinDesktop } from './darwin-desktop.ts';
|
|
64
|
+
export { isDarwinDesktop, watchDarwinDesktop } from './darwin-desktop.ts';
|
|
57
65
|
export type { RelativeTime, RelativeTimeUnit } from './relative-time.ts';
|
|
58
66
|
export { JsonTree } from './JsonTree.tsx';
|
|
59
67
|
export type { JsonTreeProps, JsonTreeLabels } from './JsonTree.tsx';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One plugin's settings form as its page on the Plugins page shows it: the
|
|
3
|
+
* read-only notice when the deployment stores settings read-only, the
|
|
4
|
+
* plugin's controls, and the save that writes every staged edit. The page
|
|
5
|
+
* draws the plugin's title and one-liner itself.
|
|
6
|
+
*
|
|
7
|
+
* Only a save writes. Leaving the page drops every staged edit, so the form
|
|
8
|
+
* discards on unmount and offers no discard control. A form whose namespace
|
|
9
|
+
* the Host stopped serving says so in place of its controls rather than
|
|
10
|
+
* showing fields nothing would accept.
|
|
11
|
+
*/
|
|
12
|
+
import { type ReactNode } from 'react';
|
|
13
|
+
import type { SettingsFormShell } from './form-model.ts';
|
|
14
|
+
/** The copy the form frame renders, from the owning plugin's dictionary. */
|
|
15
|
+
export interface SettingsFormLabels {
|
|
16
|
+
/** Shown in place of the controls while the namespace is not served. */
|
|
17
|
+
unavailable: string;
|
|
18
|
+
/** Shown over the controls while the document is read-only. */
|
|
19
|
+
readOnly: string;
|
|
20
|
+
/** Shown beside the save after a save the Host did not accept. */
|
|
21
|
+
saveFailed: string;
|
|
22
|
+
/** The save control. */
|
|
23
|
+
save: string;
|
|
24
|
+
/** The save control while a save is crossing the wire. */
|
|
25
|
+
saving: string;
|
|
26
|
+
}
|
|
27
|
+
/** Form chrome shared by every settings page. */
|
|
28
|
+
export interface SettingsFormProps {
|
|
29
|
+
/** The frame's copy. */
|
|
30
|
+
labels: SettingsFormLabels;
|
|
31
|
+
/** The form state: availability, writability, and what a save would do. */
|
|
32
|
+
state: SettingsFormShell;
|
|
33
|
+
/** Write every staged edit. */
|
|
34
|
+
onSave: () => void;
|
|
35
|
+
/** Drop every staged edit; the form calls it when it leaves the page. */
|
|
36
|
+
onDiscard: () => void;
|
|
37
|
+
/** The plugin's controls. */
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Render one plugin's settings form.
|
|
42
|
+
* @param props - the form's copy and state, its controls, and the save and discard actions.
|
|
43
|
+
* @returns the form, or the unavailable line while the namespace is not served.
|
|
44
|
+
*/
|
|
45
|
+
export declare function SettingsForm(props: SettingsFormProps): import("react").JSX.Element;
|
|
46
|
+
//# sourceMappingURL=SettingsForm.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The controls of a settings form. Each renders one field's label, its staged
|
|
3
|
+
* text, whether saving would leave an override, and — when one stands — the
|
|
4
|
+
* reset that stages a clear back to the composition layer. Nothing here
|
|
5
|
+
* writes: a control reports what the user typed, and the form's save is the
|
|
6
|
+
* single point where a draft becomes a document mutation.
|
|
7
|
+
*/
|
|
8
|
+
import { type ReactNode } from 'react';
|
|
9
|
+
/** What every settings field control needs regardless of its value type. */
|
|
10
|
+
export interface SettingsFieldProps {
|
|
11
|
+
/** Stable id associating the label with its control. */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Visible label. */
|
|
14
|
+
label: string;
|
|
15
|
+
/** One-line explanation rendered under the control. */
|
|
16
|
+
hint: string;
|
|
17
|
+
/** Draft text this control renders. */
|
|
18
|
+
text: string;
|
|
19
|
+
/** True when saving would leave a user-layer entry for this field. */
|
|
20
|
+
overridden: boolean;
|
|
21
|
+
/** True when the draft is not a value this field accepts. */
|
|
22
|
+
invalid: boolean;
|
|
23
|
+
/** Copy for the overridden badge. */
|
|
24
|
+
overriddenLabel: string;
|
|
25
|
+
/** Copy for the reset control. */
|
|
26
|
+
resetLabel: string;
|
|
27
|
+
/** Copy shown in place of the hint while the draft is invalid. */
|
|
28
|
+
invalidLabel: string;
|
|
29
|
+
/** Disables every control (read-only document, or an unavailable namespace). */
|
|
30
|
+
disabled: boolean;
|
|
31
|
+
/** Stage draft text. */
|
|
32
|
+
onEdit: (text: string) => void;
|
|
33
|
+
/** Stage a clear so the field re-inherits the composition layer. */
|
|
34
|
+
onReset: () => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A staged value field. `numeric` only hints the keypad: which drafts a field
|
|
38
|
+
* accepts is decided by its spec, so the control never silently rewrites what
|
|
39
|
+
* the user typed.
|
|
40
|
+
* @param props - the field's copy, its staged text, and the edit actions.
|
|
41
|
+
* @returns the labelled control.
|
|
42
|
+
*/
|
|
43
|
+
export declare function SettingsValueField(props: Omit<SettingsFieldProps, 'hint'> & {
|
|
44
|
+
/** Optional explanation shown below the input. */
|
|
45
|
+
hint?: string;
|
|
46
|
+
/** Rules disclosed by the information button beside the label. */
|
|
47
|
+
help?: {
|
|
48
|
+
label: string;
|
|
49
|
+
content: ReactNode;
|
|
50
|
+
};
|
|
51
|
+
/** Hints a numeric keypad without narrowing what the control accepts. */
|
|
52
|
+
numeric?: boolean;
|
|
53
|
+
/** Placeholder shown while the draft is empty. */
|
|
54
|
+
placeholder?: string;
|
|
55
|
+
}): import("react").JSX.Element;
|
|
56
|
+
/**
|
|
57
|
+
* A write-only credential control. The value never rides a response, so the
|
|
58
|
+
* control reports only whether one is configured and starts blank; a blank
|
|
59
|
+
* draft writes nothing, which keeps the stored key rather than clearing it.
|
|
60
|
+
* The control asks browsers not to autofill saved login passwords.
|
|
61
|
+
* @param props - the field's copy, its staged text, and the configured state.
|
|
62
|
+
* @returns the labelled control.
|
|
63
|
+
*/
|
|
64
|
+
export declare function SettingsSecretField(props: Pick<SettingsFieldProps, 'id' | 'label' | 'hint' | 'text' | 'disabled' | 'onEdit'> & {
|
|
65
|
+
/** Whether the Host reports a configured credential for this reference. */
|
|
66
|
+
configured: boolean;
|
|
67
|
+
/** Copy describing the configured state. */
|
|
68
|
+
stateLabel: string;
|
|
69
|
+
}): import("react").JSX.Element;
|
|
70
|
+
//# sourceMappingURL=fields.d.ts.map
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The staged form model behind a plugin's settings page.
|
|
3
|
+
*
|
|
4
|
+
* A card stages what the user types and writes it only when they save. Each
|
|
5
|
+
* settings write is a durable, revision-fenced document mutation, so a control
|
|
6
|
+
* that committed as it settled turned one edit into a write the user never
|
|
7
|
+
* asked for and could not preview; staged text makes what is on screen exactly
|
|
8
|
+
* what a save would store.
|
|
9
|
+
*
|
|
10
|
+
* A field shows its effective value — the user layer over the composition
|
|
11
|
+
* layer over the schema default — and whether the user layer carries it. That
|
|
12
|
+
* presence, not a value comparison, is what marks a field overridden: an
|
|
13
|
+
* override equal to the composition default is still an override.
|
|
14
|
+
*/
|
|
15
|
+
import { type SnapshotStore } from '@qilin-agent/client-store';
|
|
16
|
+
/** What the model reads of one Host entry's form. */
|
|
17
|
+
export interface SettingsFormScopeSnapshot<T> {
|
|
18
|
+
/** `ready` while the Host serves the entry to this client; the form renders nothing otherwise. */
|
|
19
|
+
status: 'loading' | 'ready' | 'unavailable';
|
|
20
|
+
/** Last accepted schema-resolved section; undefined before the first acceptance. */
|
|
21
|
+
value: T | undefined;
|
|
22
|
+
/** Composition layer the value resolves over: what a field reverts to once cleared. */
|
|
23
|
+
base: unknown;
|
|
24
|
+
/** Raw user layer as stored; a field's PRESENCE here is what marks it overridden. */
|
|
25
|
+
user: unknown;
|
|
26
|
+
/** Whether the Host document accepts writes. */
|
|
27
|
+
writable: boolean;
|
|
28
|
+
/** Revision the snapshot was read at; a save fences its mutation with the revision its drafts started from. */
|
|
29
|
+
revision: number | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** One path edit a save sends, as the shared configuration form's `mutate` accepts it. */
|
|
32
|
+
export type SettingsFormPathOp = {
|
|
33
|
+
op: 'set';
|
|
34
|
+
path: readonly string[];
|
|
35
|
+
value: unknown;
|
|
36
|
+
} | {
|
|
37
|
+
op: 'unset';
|
|
38
|
+
path: readonly string[];
|
|
39
|
+
};
|
|
40
|
+
/** The entry form the model stages over: the reads and the atomic write of the form `ui-settings` shares per Host entry. */
|
|
41
|
+
export interface SettingsFormScope<T> {
|
|
42
|
+
/** @returns the current sync snapshot. */
|
|
43
|
+
getSnapshot(): SettingsFormScopeSnapshot<T>;
|
|
44
|
+
/**
|
|
45
|
+
* Observe snapshot replacements.
|
|
46
|
+
* @param listener - invoked after each snapshot change.
|
|
47
|
+
* @returns the disposer removing this listener.
|
|
48
|
+
*/
|
|
49
|
+
subscribe(listener: () => void): () => void;
|
|
50
|
+
/**
|
|
51
|
+
* Apply ordered field edits in one revision-fenced write.
|
|
52
|
+
* @param ops - the edits, in staging order.
|
|
53
|
+
* @param expectedRevision - the revision the drafts were staged against, when known.
|
|
54
|
+
* @returns true for Host acceptance, false for refusal, after any recovery read.
|
|
55
|
+
*/
|
|
56
|
+
mutate(ops: readonly SettingsFormPathOp[], expectedRevision?: number): Promise<boolean>;
|
|
57
|
+
}
|
|
58
|
+
/** The write one field's staged text performs when the card is saved. */
|
|
59
|
+
export type SettingsFieldWrite = {
|
|
60
|
+
kind: 'set';
|
|
61
|
+
value: unknown;
|
|
62
|
+
} | {
|
|
63
|
+
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
|
+
* A control whose value is written outside the settings section. A credential
|
|
79
|
+
* literal never rides a response, so its draft has nothing to seed from: it is
|
|
80
|
+
* blank until typed, and a blank draft writes nothing.
|
|
81
|
+
*/
|
|
82
|
+
export interface SettingsSecretSpec {
|
|
83
|
+
/** Field name addressing this control inside the card's form. */
|
|
84
|
+
field: string;
|
|
85
|
+
/** Write the staged text; resolves to whether the Host accepted it. */
|
|
86
|
+
write: (text: string) => Promise<boolean>;
|
|
87
|
+
}
|
|
88
|
+
/** One field as a card's control renders it. */
|
|
89
|
+
export interface SettingsFieldState {
|
|
90
|
+
/** Draft text the control renders. */
|
|
91
|
+
text: string;
|
|
92
|
+
/**
|
|
93
|
+
* Whether saving would leave a user-layer entry for this field. A staged
|
|
94
|
+
* edit answers for itself, so the badge previews the save rather than
|
|
95
|
+
* reporting a state the pending edit already contradicts.
|
|
96
|
+
*/
|
|
97
|
+
overridden: boolean;
|
|
98
|
+
/** Whether the draft is not a value this field accepts, which blocks saving. */
|
|
99
|
+
invalid: boolean;
|
|
100
|
+
}
|
|
101
|
+
/** Form state every plugin card shares. */
|
|
102
|
+
export interface SettingsFormShell {
|
|
103
|
+
/** False while the namespace is not served to this client; the card renders nothing. */
|
|
104
|
+
available: boolean;
|
|
105
|
+
/** Whether the Host document accepts writes. */
|
|
106
|
+
writable: boolean;
|
|
107
|
+
/** Whether the form holds edits that a save would write. */
|
|
108
|
+
dirty: boolean;
|
|
109
|
+
/** Whether any staged draft is invalid, which blocks the save. */
|
|
110
|
+
invalid: boolean;
|
|
111
|
+
/** Whether a save is crossing the wire. */
|
|
112
|
+
saving: boolean;
|
|
113
|
+
/** Whether the last save did not land as staged; cleared by the next edit or save. */
|
|
114
|
+
failed: boolean;
|
|
115
|
+
}
|
|
116
|
+
/** The write actions every plugin card's slot entry injects. */
|
|
117
|
+
export interface SettingsFormActions {
|
|
118
|
+
/** Stage draft text for one field. */
|
|
119
|
+
edit: (field: string, text: string) => void;
|
|
120
|
+
/** Stage a clear, so saving lets the field re-inherit the composition layer. */
|
|
121
|
+
resetField: (field: string) => void;
|
|
122
|
+
/** Write every staged edit, then re-seed from what the Host accepted. */
|
|
123
|
+
save: () => void;
|
|
124
|
+
/** Drop every staged edit. */
|
|
125
|
+
discard: () => void;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A whole-number field. An empty draft clears the field; any other draft that
|
|
129
|
+
* is not a finite number blocks the save.
|
|
130
|
+
* @param field - field name inside the namespace section.
|
|
131
|
+
* @returns the field's conversion spec.
|
|
132
|
+
*/
|
|
133
|
+
export declare function settingsNumberField(field: string): SettingsFieldSpec;
|
|
134
|
+
/**
|
|
135
|
+
* A free-text field. An empty draft clears the field, so emptying the control
|
|
136
|
+
* and saving is the same gesture as resetting it.
|
|
137
|
+
* @param field - field name inside the namespace section.
|
|
138
|
+
* @returns the field's conversion spec.
|
|
139
|
+
*/
|
|
140
|
+
export declare function settingsTextField(field: string): SettingsFieldSpec;
|
|
141
|
+
/**
|
|
142
|
+
* Stages one card's edits over one settings namespace and writes them on save.
|
|
143
|
+
*
|
|
144
|
+
* The form publishes through a snapshot store because slot components read
|
|
145
|
+
* through a snapshot selector, while both the scope and the local drafts
|
|
146
|
+
* change underneath; every projection is rebuilt from the two together.
|
|
147
|
+
*/
|
|
148
|
+
export declare class SettingsFormModel<T> {
|
|
149
|
+
private readonly scope;
|
|
150
|
+
private readonly specs;
|
|
151
|
+
private readonly secretSpecs;
|
|
152
|
+
private readonly staged;
|
|
153
|
+
private readonly listeners;
|
|
154
|
+
private baseline;
|
|
155
|
+
private readonly unsubscribe;
|
|
156
|
+
private saving;
|
|
157
|
+
private failed;
|
|
158
|
+
/**
|
|
159
|
+
* @param scope - the shared configuration form for this card's namespace.
|
|
160
|
+
* @param specs - the section fields this card edits.
|
|
161
|
+
* @param secrets - the card's write-only controls, written outside the section.
|
|
162
|
+
*/
|
|
163
|
+
constructor(scope: SettingsFormScope<T>, specs: SettingsFieldSpec[], secrets?: SettingsSecretSpec[]);
|
|
164
|
+
/**
|
|
165
|
+
* Publish a projection of this form, rebuilt whenever the scope or a draft changes.
|
|
166
|
+
* @param project - build the card's state from the form's current reads.
|
|
167
|
+
* @returns the store the card's component reads through its bound selector.
|
|
168
|
+
*/
|
|
169
|
+
bind<S>(project: () => S): SnapshotStore<S>;
|
|
170
|
+
/**
|
|
171
|
+
* Read the card-level state: what the Host serves, and what a save would do.
|
|
172
|
+
* @returns the form state every card shares.
|
|
173
|
+
*/
|
|
174
|
+
shell(): SettingsFormShell;
|
|
175
|
+
/**
|
|
176
|
+
* Read one control's state.
|
|
177
|
+
* @param field - field name of a section field or of a write-only control.
|
|
178
|
+
* @returns the draft text, whether a save would leave an override, and whether it is invalid.
|
|
179
|
+
*/
|
|
180
|
+
field(field: string): SettingsFieldState;
|
|
181
|
+
/**
|
|
182
|
+
* Build the edit, reset, save, and discard actions bound to this form.
|
|
183
|
+
* @returns the actions a card's slot entry injects.
|
|
184
|
+
*/
|
|
185
|
+
actions(): SettingsFormActions;
|
|
186
|
+
/**
|
|
187
|
+
* Write every staged edit, then re-seed from what the Host accepted.
|
|
188
|
+
*
|
|
189
|
+
* The Host is the only authority on whether a value was accepted — its
|
|
190
|
+
* validators own the constraints no schema can express — so the outcome is
|
|
191
|
+
* read back from the section rather than predicted here. A save that did not
|
|
192
|
+
* land keeps its drafts, so the user can correct them instead of retyping.
|
|
193
|
+
* @returns settlement after every write and the read-back.
|
|
194
|
+
*/
|
|
195
|
+
save(): Promise<void>;
|
|
196
|
+
/** Release the form's accepted-value subscription. */
|
|
197
|
+
dispose(): void;
|
|
198
|
+
/**
|
|
199
|
+
* Every staged edit a save would write. An entry whose draft is not a value
|
|
200
|
+
* its field accepts carries no write: the form is still dirty, and the save
|
|
201
|
+
* refuses rather than dropping the edit.
|
|
202
|
+
* @returns the planned writes, in the order the fields were staged.
|
|
203
|
+
*/
|
|
204
|
+
private plan;
|
|
205
|
+
private stage;
|
|
206
|
+
private spec;
|
|
207
|
+
private snapshotOf;
|
|
208
|
+
private sectionValue;
|
|
209
|
+
private baseValue;
|
|
210
|
+
private userLayer;
|
|
211
|
+
private stored;
|
|
212
|
+
private publish;
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=form-model.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qilin-agent/client-ui-primitives",
|
|
3
3
|
"description": "Pure React atoms for the qilin web UI: controls, icons, markdown, and JSON inspectors (zero cordis)",
|
|
4
|
-
"version": "3.1.
|
|
4
|
+
"version": "3.1.4",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -51,8 +51,9 @@
|
|
|
51
51
|
"react-dom": "^18.2.0",
|
|
52
52
|
"shiki": "^4.3.1",
|
|
53
53
|
"simple-icons": "16.31.0",
|
|
54
|
-
"@qilin-agent/
|
|
55
|
-
"@qilin-agent/
|
|
54
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
55
|
+
"@qilin-agent/client-store": "3.1.4",
|
|
56
|
+
"@qilin-agent/util-workspace-path": "3.1.4"
|
|
56
57
|
},
|
|
57
58
|
"files": [
|
|
58
59
|
"lib/index.js",
|