@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/dist/client.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { a as formatReferenceMention, c as parseReferenceToken, i as formatReference, n as ReferenceSpan, o as isLocalReference, r as findReferences, s as parseReference, t as Reference } from "./reference-BX54bj6_.mjs";
2
- import { ComponentProps, ComponentPropsWithRef, ElementType, ForwardRefExoticComponent, Ref, RefAttributes } from "react";
2
+ import { ComponentProps, ComponentPropsWithRef, ElementType, ForwardRefExoticComponent, ReactNode, Ref, RefAttributes } from "react";
3
+ import { SnapshotStore } from "@deepseek-ai/dsh-client-store";
3
4
  import { MarkdownFileMentions, MarkdownLabels, MarkdownLabels as MarkdownLabels$1, MarkdownPathImages } from "@deepseek-ai/dsh-client-ui-primitives";
4
5
  import { Context } from "@deepseek-ai/cordis";
5
6
  //#region ../../../node_modules/.pnpm/csstype@3.2.3/node_modules/csstype/index.d.ts
@@ -9414,7 +9415,7 @@ declare function styleOf(component: StyledComponent<ElementType>): CSSProps;
9414
9415
  //#endregion
9415
9416
  //#region src/client/theme.generated.d.ts
9416
9417
  /**
9417
- * 官方主题的 `--dsw-*` token 全集(367 个),由 packages/client/ui-primitives/scripts/gen-design-tokens.mts 生成,勿手改。
9418
+ * 官方主题的 `--dsw-*` token 全集(373 个),由 packages/client/ui-primitives/scripts/gen-design-tokens.mts 生成,勿手改。
9418
9419
  *
9419
9420
  * 叶子是该变量的**默认值**(官方 light 主题里首次出现的定义):既是类型推导的来源,也可作 fallback。
9420
9421
  * `"$"` 键出现在「自身也是 token 的分支」上(命名体系里有 40 个短名同时是长名前缀的 token)。
@@ -9518,6 +9519,20 @@ declare const designTokens: {
9518
9519
  readonly deleted: "var(--dsw-static-red-600-a08)";
9519
9520
  };
9520
9521
  };
9522
+ readonly file: {
9523
+ readonly diff: {
9524
+ readonly added: {
9525
+ readonly bg: "rgb(230, 244, 231)";
9526
+ readonly gutter: "rgb(237, 247, 237)";
9527
+ readonly marker: "rgb(1, 162, 65)";
9528
+ };
9529
+ readonly deleted: {
9530
+ readonly bg: "rgb(252, 230, 226)";
9531
+ readonly gutter: "rgb(253, 236, 233)";
9532
+ readonly marker: "rgb(186, 39, 35)";
9533
+ };
9534
+ };
9535
+ };
9521
9536
  readonly interactive: {
9522
9537
  readonly bg: {
9523
9538
  readonly active: "rgba(38, 49, 72, 0.1)";
@@ -10261,6 +10276,20 @@ declare const dsw: TokenVars<{
10261
10276
  readonly deleted: "var(--dsw-static-red-600-a08)";
10262
10277
  };
10263
10278
  };
10279
+ readonly file: {
10280
+ readonly diff: {
10281
+ readonly added: {
10282
+ readonly bg: "rgb(230, 244, 231)";
10283
+ readonly gutter: "rgb(237, 247, 237)";
10284
+ readonly marker: "rgb(1, 162, 65)";
10285
+ };
10286
+ readonly deleted: {
10287
+ readonly bg: "rgb(252, 230, 226)";
10288
+ readonly gutter: "rgb(253, 236, 233)";
10289
+ readonly marker: "rgb(186, 39, 35)";
10290
+ };
10291
+ };
10292
+ };
10264
10293
  readonly interactive: {
10265
10294
  readonly bg: {
10266
10295
  readonly active: "rgba(38, 49, 72, 0.1)";
@@ -10909,6 +10938,302 @@ declare const dsw: TokenVars<{
10909
10938
  type MarkdownCopySeat = (key: "copy" | "copied" | "markdown.footnotes") => string;
10910
10939
  declare function markdownLabels(t: MarkdownCopySeat): MarkdownLabels;
10911
10940
  //#endregion
10941
+ //#region src/client/settings-form/form-model.d.ts
10942
+ /** What the model reads of one Host entry's form. */
10943
+ interface SettingsFormScopeSnapshot<T> {
10944
+ /** `ready` while the Host serves the entry to this client; the form renders nothing otherwise. */
10945
+ status: "loading" | "ready" | "unavailable";
10946
+ /** Last accepted schema-resolved section; undefined before the first acceptance. */
10947
+ value: T | undefined;
10948
+ /** Composition layer the value resolves over: what a field reverts to once cleared. */
10949
+ base: unknown;
10950
+ /** Raw user layer as stored; a field's PRESENCE here is what marks it overridden. */
10951
+ user: unknown;
10952
+ /** Whether the Host document accepts writes. */
10953
+ writable: boolean;
10954
+ /** Revision the snapshot was read at; a save fences its mutation with the revision its drafts started from. */
10955
+ revision: number | undefined;
10956
+ }
10957
+ /** One path edit a save sends, as the shared configuration form's `mutate` accepts it. */
10958
+ type SettingsFormPathOp = {
10959
+ op: "set";
10960
+ path: readonly string[];
10961
+ value: unknown;
10962
+ } | {
10963
+ op: "unset";
10964
+ path: readonly string[];
10965
+ };
10966
+ /** The entry form the model stages over: the reads and the atomic write of the form `ui-settings` shares per Host entry. */
10967
+ interface SettingsFormScope<T> {
10968
+ /** @returns the current sync snapshot. */
10969
+ getSnapshot(): SettingsFormScopeSnapshot<T>;
10970
+ /**
10971
+ * Observe snapshot replacements.
10972
+ * @param listener - invoked after each snapshot change.
10973
+ * @returns the disposer removing this listener.
10974
+ */
10975
+ subscribe(listener: () => void): () => void;
10976
+ /**
10977
+ * Apply ordered field edits in one revision-fenced write.
10978
+ * @param ops - the edits, in staging order.
10979
+ * @param expectedRevision - the revision the drafts were staged against, when known.
10980
+ * @returns true for Host acceptance, false for refusal, after any recovery read.
10981
+ */
10982
+ mutate(ops: readonly SettingsFormPathOp[], expectedRevision?: number): Promise<boolean>;
10983
+ }
10984
+ /** The write one field's staged text performs when the card is saved. */
10985
+ type SettingsFieldWrite = {
10986
+ kind: "set";
10987
+ value: unknown;
10988
+ } | {
10989
+ kind: "clear";
10990
+ };
10991
+ /** How one section field converts between its stored value and its draft text. */
10992
+ interface SettingsFieldSpec {
10993
+ /** Field name inside the namespace section. */
10994
+ field: string;
10995
+ /** Render a stored value as draft text; the empty string when the section carries none. */
10996
+ format: (value: unknown) => string;
10997
+ /**
10998
+ * The write this draft text stages, or undefined when the text is not a
10999
+ * value this field accepts — which blocks the save rather than discarding it.
11000
+ */
11001
+ parse: (text: string) => SettingsFieldWrite | undefined;
11002
+ }
11003
+ /**
11004
+ * A control whose value is written outside the settings section. A credential
11005
+ * literal never rides a response, so its draft has nothing to seed from: it is
11006
+ * blank until typed, and a blank draft writes nothing.
11007
+ */
11008
+ interface SettingsSecretSpec {
11009
+ /** Field name addressing this control inside the card's form. */
11010
+ field: string;
11011
+ /** Write the staged text; resolves to whether the Host accepted it. */
11012
+ write: (text: string) => Promise<boolean>;
11013
+ }
11014
+ /** One field as a card's control renders it. */
11015
+ interface SettingsFieldState {
11016
+ /** Draft text the control renders. */
11017
+ text: string;
11018
+ /**
11019
+ * Whether saving would leave a user-layer entry for this field. A staged
11020
+ * edit answers for itself, so the badge previews the save rather than
11021
+ * reporting a state the pending edit already contradicts.
11022
+ */
11023
+ overridden: boolean;
11024
+ /** Whether the draft is not a value this field accepts, which blocks saving. */
11025
+ invalid: boolean;
11026
+ }
11027
+ /** Form state every plugin card shares. */
11028
+ interface SettingsFormShell {
11029
+ /** False while the namespace is not served to this client; the card renders nothing. */
11030
+ available: boolean;
11031
+ /** Whether the Host document accepts writes. */
11032
+ writable: boolean;
11033
+ /** Whether the form holds edits that a save would write. */
11034
+ dirty: boolean;
11035
+ /** Whether any staged draft is invalid, which blocks the save. */
11036
+ invalid: boolean;
11037
+ /** Whether a save is crossing the wire. */
11038
+ saving: boolean;
11039
+ /** Whether the last save did not land as staged; cleared by the next edit or save. */
11040
+ failed: boolean;
11041
+ }
11042
+ /** The write actions every plugin card's slot entry injects. */
11043
+ interface SettingsFormActions {
11044
+ /** Stage draft text for one field. */
11045
+ edit: (field: string, text: string) => void;
11046
+ /** Stage a clear, so saving lets the field re-inherit the composition layer. */
11047
+ resetField: (field: string) => void;
11048
+ /** Write every staged edit, then re-seed from what the Host accepted. */
11049
+ save: () => void;
11050
+ /** Drop every staged edit. */
11051
+ discard: () => void;
11052
+ }
11053
+ /**
11054
+ * A whole-number field. An empty draft clears the field; any other draft that
11055
+ * is not a finite number blocks the save.
11056
+ * @param field - field name inside the namespace section.
11057
+ * @returns the field's conversion spec.
11058
+ */
11059
+ declare function settingsNumberField(field: string): SettingsFieldSpec;
11060
+ /**
11061
+ * A free-text field. An empty draft clears the field, so emptying the control
11062
+ * and saving is the same gesture as resetting it.
11063
+ * @param field - field name inside the namespace section.
11064
+ * @returns the field's conversion spec.
11065
+ */
11066
+ declare function settingsTextField(field: string): SettingsFieldSpec;
11067
+ /**
11068
+ * Stages one card's edits over one settings namespace and writes them on save.
11069
+ *
11070
+ * The form publishes through a snapshot store because slot components read
11071
+ * through a snapshot selector, while both the scope and the local drafts
11072
+ * change underneath; every projection is rebuilt from the two together.
11073
+ */
11074
+ declare class SettingsFormModel<T> {
11075
+ private readonly scope;
11076
+ private readonly specs;
11077
+ private readonly secretSpecs;
11078
+ private readonly staged;
11079
+ private readonly listeners;
11080
+ private baseline;
11081
+ private readonly unsubscribe;
11082
+ private saving;
11083
+ private failed;
11084
+ /**
11085
+ * @param scope - the shared configuration form for this card's namespace.
11086
+ * @param specs - the section fields this card edits.
11087
+ * @param secrets - the card's write-only controls, written outside the section.
11088
+ */
11089
+ constructor(scope: SettingsFormScope<T>, specs: SettingsFieldSpec[], secrets?: SettingsSecretSpec[]);
11090
+ /**
11091
+ * Publish a projection of this form, rebuilt whenever the scope or a draft changes.
11092
+ * @param project - build the card's state from the form's current reads.
11093
+ * @returns the store the card's component reads through its bound selector.
11094
+ */
11095
+ bind<S>(project: () => S): SnapshotStore<S>;
11096
+ /**
11097
+ * Read the card-level state: what the Host serves, and what a save would do.
11098
+ * @returns the form state every card shares.
11099
+ */
11100
+ shell(): SettingsFormShell;
11101
+ /**
11102
+ * Read one control's state.
11103
+ * @param field - field name of a section field or of a write-only control.
11104
+ * @returns the draft text, whether a save would leave an override, and whether it is invalid.
11105
+ */
11106
+ field(field: string): SettingsFieldState;
11107
+ /**
11108
+ * Build the edit, reset, save, and discard actions bound to this form.
11109
+ * @returns the actions a card's slot entry injects.
11110
+ */
11111
+ actions(): SettingsFormActions;
11112
+ /**
11113
+ * Write every staged edit, then re-seed from what the Host accepted.
11114
+ *
11115
+ * The Host is the only authority on whether a value was accepted — its
11116
+ * validators own the constraints no schema can express — so the outcome is
11117
+ * read back from the section rather than predicted here. A save that did not
11118
+ * land keeps its drafts, so the user can correct them instead of retyping.
11119
+ * @returns settlement after every write and the read-back.
11120
+ */
11121
+ save(): Promise<void>;
11122
+ /** Release the form's accepted-value subscription. */
11123
+ dispose(): void;
11124
+ /**
11125
+ * Every staged edit a save would write. An entry whose draft is not a value
11126
+ * its field accepts carries no write: the form is still dirty, and the save
11127
+ * refuses rather than dropping the edit.
11128
+ * @returns the planned writes, in the order the fields were staged.
11129
+ */
11130
+ private plan;
11131
+ private stage;
11132
+ private spec;
11133
+ private snapshotOf;
11134
+ private sectionValue;
11135
+ private baseValue;
11136
+ private userLayer;
11137
+ private stored;
11138
+ private publish;
11139
+ }
11140
+ //#endregion
11141
+ //#region src/client/settings-form/fields.d.ts
11142
+ /** What every settings field control needs regardless of its value type. */
11143
+ interface SettingsFieldProps {
11144
+ /** Stable id associating the label with its control. */
11145
+ id: string;
11146
+ /** Visible label. */
11147
+ label: string;
11148
+ /** One-line explanation rendered under the control. */
11149
+ hint: string;
11150
+ /** Draft text this control renders. */
11151
+ text: string;
11152
+ /** True when saving would leave a user-layer entry for this field. */
11153
+ overridden: boolean;
11154
+ /** True when the draft is not a value this field accepts. */
11155
+ invalid: boolean;
11156
+ /** Copy for the overridden badge. */
11157
+ overriddenLabel: string;
11158
+ /** Copy for the reset control. */
11159
+ resetLabel: string;
11160
+ /** Copy shown in place of the hint while the draft is invalid. */
11161
+ invalidLabel: string;
11162
+ /** Disables every control (read-only document, or an unavailable namespace). */
11163
+ disabled: boolean;
11164
+ /** Stage draft text. */
11165
+ onEdit: (text: string) => void;
11166
+ /** Stage a clear so the field re-inherits the composition layer. */
11167
+ onReset: () => void;
11168
+ }
11169
+ /**
11170
+ * A staged value field. `numeric` only hints the keypad: which drafts a field
11171
+ * accepts is decided by its spec, so the control never silently rewrites what
11172
+ * the user typed.
11173
+ * @param props - the field's copy, its staged text, and the edit actions.
11174
+ * @returns the labelled control.
11175
+ */
11176
+ declare function SettingsValueField(props: Omit<SettingsFieldProps, "hint"> & {
11177
+ /** Optional explanation shown below the input. */
11178
+ hint?: string;
11179
+ /** Rules disclosed by the information button beside the label. */
11180
+ help?: {
11181
+ label: string;
11182
+ content: ReactNode;
11183
+ };
11184
+ /** Hints a numeric keypad without narrowing what the control accepts. */
11185
+ numeric?: boolean;
11186
+ /** Placeholder shown while the draft is empty. */
11187
+ placeholder?: string;
11188
+ }): import("react/jsx-runtime").JSX.Element;
11189
+ /**
11190
+ * A write-only credential control. The value never rides a response, so the
11191
+ * control reports only whether one is configured and starts blank; a blank
11192
+ * draft writes nothing, which keeps the stored key rather than clearing it.
11193
+ * @param props - the field's copy, its staged text, and the configured state.
11194
+ * @returns the labelled control.
11195
+ */
11196
+ declare function SettingsSecretField(props: Pick<SettingsFieldProps, "id" | "label" | "hint" | "text" | "disabled" | "onEdit"> & {
11197
+ /** Whether the Host reports a configured credential for this reference. */
11198
+ configured: boolean;
11199
+ /** Copy describing the configured state. */
11200
+ stateLabel: string;
11201
+ }): import("react/jsx-runtime").JSX.Element;
11202
+ //#endregion
11203
+ //#region src/client/settings-form/SettingsForm.d.ts
11204
+ /** The copy the form frame renders, from the owning plugin's dictionary. */
11205
+ interface SettingsFormLabels {
11206
+ /** Shown in place of the controls while the namespace is not served. */
11207
+ unavailable: string;
11208
+ /** Shown over the controls while the document is read-only. */
11209
+ readOnly: string;
11210
+ /** Shown beside the save after a save the Host did not accept. */
11211
+ saveFailed: string;
11212
+ /** The save control. */
11213
+ save: string;
11214
+ /** The save control while a save is crossing the wire. */
11215
+ saving: string;
11216
+ }
11217
+ /** Form chrome shared by every settings page. */
11218
+ interface SettingsFormProps {
11219
+ /** The frame's copy. */
11220
+ labels: SettingsFormLabels;
11221
+ /** The form state: availability, writability, and what a save would do. */
11222
+ state: SettingsFormShell;
11223
+ /** Write every staged edit. */
11224
+ onSave: () => void;
11225
+ /** Drop every staged edit; the form calls it when it leaves the page. */
11226
+ onDiscard: () => void;
11227
+ /** The plugin's controls. */
11228
+ children: ReactNode;
11229
+ }
11230
+ /**
11231
+ * Render one plugin's settings form.
11232
+ * @param props - the form's copy and state, its controls, and the save and discard actions.
11233
+ * @returns the form, or the unavailable line while the namespace is not served.
11234
+ */
11235
+ declare function SettingsForm(props: SettingsFormProps): import("react/jsx-runtime").JSX.Element;
11236
+ //#endregion
10912
11237
  //#region src/reference-markdown.d.ts
10913
11238
  interface ReferenceActions {
10914
11239
  readonly openFile: (path: string) => void;
@@ -10928,4 +11253,4 @@ declare const ReferenceMarkdown: import("react").MemoExoticComponent<({ text, la
10928
11253
  declare const inject: readonly string[];
10929
11254
  declare function apply(_ctx: Context): void;
10930
11255
  //#endregion
10931
- export { type CSSObject, type CSSProps, type CSSSelector, type CSSValue, type CSSVar, type CSSVarRef, MarkdownCopySeat, type Reference, type ReferenceActions, ReferenceMarkdown, type ReferenceMarkdownProps, type ReferenceSpan, type StyledComponent, Styling, Token, type TokenVars, type Tokens, apply, designTokens, dsw, findReferences, formatReference, formatReferenceMention, inject, isLocalReference, markdownLabels, parseReference, parseReferenceToken, referenceMentions, styleOf, styled, styling };
11256
+ export { type CSSObject, type CSSProps, type CSSSelector, type CSSValue, type CSSVar, type CSSVarRef, MarkdownCopySeat, type Reference, type ReferenceActions, ReferenceMarkdown, type ReferenceMarkdownProps, type ReferenceSpan, type SettingsFieldProps, type SettingsFieldSpec, type SettingsFieldState, type SettingsFieldWrite, SettingsForm, type SettingsFormActions, type SettingsFormLabels, SettingsFormModel, type SettingsFormPathOp, type SettingsFormProps, type SettingsFormScope, type SettingsFormScopeSnapshot, type SettingsFormShell, SettingsSecretField, type SettingsSecretSpec, SettingsValueField, type StyledComponent, Styling, Token, type TokenVars, type Tokens, apply, designTokens, dsw, findReferences, formatReference, formatReferenceMention, inject, isLocalReference, markdownLabels, parseReference, parseReferenceToken, referenceMentions, settingsNumberField, settingsTextField, styleOf, styled, styling };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@morlay/dsh-client-ui-primitives",
3
- "version": "0.0.2-alpha.7",
4
- "description": "css-in-js styling kit (styled / Token / Styling) for the forked conversation UI: consumes the official --dsw-* design tokens, so no CSS Modules precompile step is needed.",
3
+ "version": "0.0.2-alpha.8",
4
+ "description": "css-in-js styling kit (styled / Token / Styling) plus the shared reference parser and renderer (findReferences / parseReferenceToken) for the forked conversation UI: consumes the official --dsw-* design tokens, so no CSS Modules precompile step is needed.",
5
5
  "keywords": [
6
6
  "client",
7
7
  "css-in-js",
@@ -42,7 +42,8 @@
42
42
  "micromark-util-types": "^2.0.2"
43
43
  },
44
44
  "devDependencies": {
45
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-alpha.2",
45
+ "@deepseek-ai/dsh-client-store": "0.1.7-rc.1",
46
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-rc.1",
46
47
  "@types/mdast": "^4.0.4",
47
48
  "@types/react": "~18.3.1",
48
49
  "csstype": "^3.1.3",
@@ -63,6 +64,6 @@
63
64
  },
64
65
  "scripts": {
65
66
  "build": "pnpm exec tsdown",
66
- "gen:tokens": "pnpm exec tsx scripts/gen-design-tokens.mts"
67
+ "gen:tokens": "pnpm exec tsx scripts/gen-design-tokens.mts && pnpm exec oxfmt src/client/theme.generated.ts"
67
68
  }
68
69
  }
@@ -3,6 +3,7 @@ import type { Context } from "@deepseek-ai/cordis";
3
3
  export * from "./styling/index.ts";
4
4
  export * from "./theme.ts";
5
5
  export * from "./markdown-labels.ts";
6
+ export * from "./settings-form/index.ts";
6
7
 
7
8
  export {
8
9
  findReferences,
@@ -0,0 +1,57 @@
1
+ // 样式搬自 vendor/deepseek-harness/packages/client/ui-primitives/src/settings-form/SettingsForm.module.css,
2
+ // 换成包内 css-in-js(CSSProps + dsw);上游那份是 CSS Modules,本包不做预编译。
3
+
4
+ import type { CSSProps } from "../styling/css.ts";
5
+ import { dsw } from "../theme.ts";
6
+
7
+ // 上游引用的 --dsw-alias-label-error 不在官方主题定义集内(token 树里没有这个变量),
8
+ // 照抄引用而不是换一个近义 token——官方主题哪天定义它,这里自动跟着生效。
9
+ const failedColor = "var(--dsw-alias-label-error)";
10
+
11
+ export const styles = {
12
+ form: {
13
+ display: "flex",
14
+ flexDirection: "column",
15
+ },
16
+ // 上游 readOnly 与 unavailable 同一条规则:两处用同一个样式对象。
17
+ notice: {
18
+ margin: "0 0 12px",
19
+ fontSize: "12px",
20
+ lineHeight: 1.5,
21
+ color: dsw.alias.label.tertiary,
22
+ },
23
+ footer: {
24
+ display: "flex",
25
+ alignItems: "center",
26
+ gap: "8px",
27
+ paddingTop: "16px",
28
+ },
29
+ failed: {
30
+ flex: 1,
31
+ minWidth: 0,
32
+ margin: 0,
33
+ fontSize: "12px",
34
+ lineHeight: 1.5,
35
+ color: failedColor,
36
+ },
37
+ save: {
38
+ appearance: "none",
39
+ border: "1px solid transparent",
40
+ borderRadius: "8px",
41
+ padding: "5px 14px",
42
+ font: "inherit",
43
+ fontSize: "13px",
44
+ lineHeight: 1.5,
45
+ cursor: "pointer",
46
+ background: dsw.alias.label.primary,
47
+ color: dsw.alias.bg.layer["3"],
48
+ "&:disabled": {
49
+ opacity: 0.4,
50
+ cursor: "default",
51
+ },
52
+ "&:focus-visible": {
53
+ outline: `2px solid ${String(dsw.alias.brand.primary)}`,
54
+ outlineOffset: "1px",
55
+ },
56
+ },
57
+ } satisfies Record<string, CSSProps>;
@@ -0,0 +1,72 @@
1
+ // 搬自 vendor/deepseek-harness/packages/client/ui-primitives/src/settings-form/SettingsForm.tsx
2
+ // (上游 `@deepseek-ai/dsh-client-ui-primitives`):行为同形,样式从 CSS Modules 换成包内 css-in-js。
3
+
4
+ import { useEffect, useRef, type ReactNode } from "react";
5
+ import { styled } from "../styling/styled.tsx";
6
+ import type { SettingsFormShell } from "./form-model.ts";
7
+ import { styles } from "./SettingsForm.styles.ts";
8
+
9
+ const Form = styled("div")(styles.form);
10
+ const Notice = styled("p")(styles.notice);
11
+ const Footer = styled("div")(styles.footer);
12
+ const Failed = styled("p")(styles.failed);
13
+ const Save = styled("button")(styles.save);
14
+
15
+ /** The copy the form frame renders, from the owning plugin's dictionary. */
16
+ export interface SettingsFormLabels {
17
+ /** Shown in place of the controls while the namespace is not served. */
18
+ unavailable: string;
19
+ /** Shown over the controls while the document is read-only. */
20
+ readOnly: string;
21
+ /** Shown beside the save after a save the Host did not accept. */
22
+ saveFailed: string;
23
+ /** The save control. */
24
+ save: string;
25
+ /** The save control while a save is crossing the wire. */
26
+ saving: string;
27
+ }
28
+
29
+ /** Form chrome shared by every settings page. */
30
+ export interface SettingsFormProps {
31
+ /** The frame's copy. */
32
+ labels: SettingsFormLabels;
33
+ /** The form state: availability, writability, and what a save would do. */
34
+ state: SettingsFormShell;
35
+ /** Write every staged edit. */
36
+ onSave: () => void;
37
+ /** Drop every staged edit; the form calls it when it leaves the page. */
38
+ onDiscard: () => void;
39
+ /** The plugin's controls. */
40
+ children: ReactNode;
41
+ }
42
+
43
+ /**
44
+ * Render one plugin's settings form.
45
+ * @param props - the form's copy and state, its controls, and the save and discard actions.
46
+ * @returns the form, or the unavailable line while the namespace is not served.
47
+ */
48
+ export function SettingsForm(props: SettingsFormProps) {
49
+ const { state, labels } = props;
50
+ const discard = useRef(props.onDiscard);
51
+ discard.current = props.onDiscard;
52
+ useEffect(
53
+ () => () => {
54
+ discard.current();
55
+ },
56
+ [],
57
+ );
58
+ if (!state.available) return <Notice role="status">{labels.unavailable}</Notice>;
59
+ const blocked = !state.dirty || state.invalid || state.saving;
60
+ return (
61
+ <Form>
62
+ {!state.writable ? <Notice role="status">{labels.readOnly}</Notice> : null}
63
+ {props.children}
64
+ <Footer>
65
+ {state.failed ? <Failed role="status">{labels.saveFailed}</Failed> : null}
66
+ <Save type="button" disabled={blocked} onClick={props.onSave}>
67
+ {state.saving ? labels.saving : labels.save}
68
+ </Save>
69
+ </Footer>
70
+ </Form>
71
+ );
72
+ }