@morlay/dsh-client-ui-primitives 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +32 -9
  2. package/dist/client-DYp99CgT.mjs +3730 -0
  3. package/dist/client-uyVkC7mu.cjs +17197 -0
  4. package/dist/client.cjs +54 -0
  5. package/dist/client.d.cts +4 -0
  6. package/dist/client.d.mts +3 -11680
  7. package/dist/client.mjs +3 -4575
  8. package/dist/index-AMHtcyMt.d.mts +647 -0
  9. package/dist/index.cjs +54 -0
  10. package/dist/index.d.mts +4 -5
  11. package/dist/index.mjs +4 -5
  12. package/dist/reference-hchCWm3z.cjs +482 -0
  13. package/dist/reference.cjs +4 -0
  14. package/dist/reference.d.mts +3 -0
  15. package/dist/reference.mjs +1 -0
  16. package/package.json +31 -35
  17. package/src/client/controls/Bubble.module.css +17 -0
  18. package/src/client/controls/Bubble.tsx +17 -0
  19. package/src/client/controls/Button.module.css +10 -0
  20. package/src/client/controls/Button.tsx +29 -0
  21. package/src/client/controls/DisclosureRow.module.css +22 -0
  22. package/src/client/controls/DisclosureRow.tsx +20 -0
  23. package/src/client/controls/IconButton.module.css +50 -0
  24. package/src/client/controls/IconButton.tsx +47 -0
  25. package/src/client/controls/ModelRouteList.module.css +100 -0
  26. package/src/client/controls/ModelRouteList.tsx +118 -0
  27. package/src/client/controls/MultilineField.module.css +26 -0
  28. package/src/client/controls/MultilineField.tsx +19 -0
  29. package/src/client/controls/SearchSelect.module.css +34 -0
  30. package/src/client/controls/SearchSelect.tsx +138 -0
  31. package/src/client/controls/SeatButton.module.css +53 -0
  32. package/src/client/controls/SeatButton.tsx +39 -0
  33. package/src/client/controls/Spinner.module.css +18 -0
  34. package/src/client/controls/Spinner.tsx +14 -0
  35. package/src/client/controls/TagInput.module.css +105 -0
  36. package/src/client/controls/TagInput.tsx +152 -0
  37. package/src/client/controls/TimeLabel.module.css +16 -0
  38. package/src/client/controls/TimeLabel.tsx +15 -0
  39. package/src/client/controls/VisuallyHidden.module.css +9 -0
  40. package/src/client/controls/VisuallyHidden.tsx +16 -0
  41. package/src/client/controls/classes.ts +5 -0
  42. package/src/client/controls/index.ts +24 -0
  43. package/src/client/controls/tags.ts +24 -0
  44. package/src/client/index.ts +16 -2
  45. package/src/client/layout/Row.tsx +89 -0
  46. package/src/client/layout/Stack.tsx +82 -0
  47. package/src/client/layout/Text.tsx +61 -0
  48. package/src/client/layout/index.ts +16 -0
  49. package/src/client/layout/layout.module.css +217 -0
  50. package/src/client/layout/scales.ts +91 -0
  51. package/src/client/schema-form/Editor.tsx +65 -65
  52. package/src/client/schema-form/SchemaForm.tsx +9 -4
  53. package/src/client/schema-form/controller.ts +5 -2
  54. package/src/client/schema-form/geometry.ts +3 -0
  55. package/src/client/schema-form/index.ts +5 -0
  56. package/src/client/schema-form/schema-form.module.css +335 -0
  57. package/src/client/schema-form/testing/fake-scope.ts +0 -1
  58. package/src/client/schema-form/value.tsx +15 -9
  59. package/src/client/settings-form/SettingsFieldRow.module.css +93 -0
  60. package/src/client/settings-form/SettingsFieldRow.tsx +105 -0
  61. package/src/client/settings-form/index.ts +5 -18
  62. package/src/css-modules.d.ts +6 -0
  63. package/src/index.ts +6 -2
  64. package/src/reference-markdown.tsx +2 -6
  65. package/dist/rolldown-runtime-EAsCxrDo.mjs +0 -14
  66. package/locale/en.json +0 -6
  67. package/locale/zh.json +0 -6
  68. package/src/client/schema-form/styles.ts +0 -321
  69. package/src/client/settings-form/SettingsForm.styles.ts +0 -58
  70. package/src/client/settings-form/SettingsForm.tsx +0 -68
  71. package/src/client/settings-form/fields.styles.ts +0 -158
  72. package/src/client/settings-form/fields.tsx +0 -184
  73. package/src/client/settings-form/form-model.ts +0 -339
  74. package/src/client/styling/css.ts +0 -27
  75. package/src/client/styling/focus.ts +0 -9
  76. package/src/client/styling/index.ts +0 -6
  77. package/src/client/styling/styled.tsx +0 -54
  78. package/src/client/styling/styling.ts +0 -142
  79. package/src/client/styling/token.ts +0 -275
  80. package/src/client/styling/toolkit.ts +0 -37
  81. package/src/client/theme.generated.ts +0 -846
  82. package/src/client/theme.ts +0 -5
  83. /package/src/{client/markdown-text.ts → markdown-text.ts} +0 -0
@@ -1,184 +0,0 @@
1
- // 搬自 vendor/deepseek-harness/packages/client/ui-primitives/src/settings-form/fields.tsx
2
- // (上游 `@deepseek-ai/dsh-client-ui-primitives`):结构与行为同形,样式从 CSS Modules 换成包内 css-in-js。
3
- // 上游用的 Tag 与 IconInfoOutlineRegular 没有一起搬——本包不依赖上游 primitives,用等价的本地徽标与图标。
4
-
5
- import { useState, type ReactNode } from "react";
6
- import { styled } from "../styling/styled.tsx";
7
- import { styles } from "./fields.styles.ts";
8
-
9
- const Field = styled("div")(styles.field);
10
- const Head = styled("div")(styles.head);
11
- const LabelGroup = styled("div")(styles.labelGroup);
12
- const GroupLabel = styled("label")(styles.label, styles.groupLabel);
13
- const HeadLabel = styled("label")(styles.label);
14
- const HelpButton = styled("button")(styles.helpButton);
15
- const Help = styled("div")(styles.help);
16
- const Badges = styled("span")(styles.badges);
17
- const NeutralBadge = styled("span")(styles.badge, styles.badgeNeutral);
18
- const QuietBadge = styled("span")(styles.badge, styles.badgeQuiet);
19
- const Reset = styled("button")(styles.reset);
20
- const Input = styled("input")(styles.input);
21
- const Invalid = styled("p")(styles.invalid);
22
- const Hint = styled("p")(styles.hint);
23
-
24
- // 上游 icons 里的 IconInfoOutlineRegular(fill-only 几何,整份图标表不搬)。
25
- function InfoGlyph({ size }: { size: number }) {
26
- return (
27
- <svg
28
- width={size}
29
- height={size}
30
- viewBox="0 0 14 14"
31
- fill="none"
32
- xmlns="http://www.w3.org/2000/svg"
33
- aria-hidden="true"
34
- >
35
- <path
36
- d="M12.5757 7.00012C12.5757 3.92085 10.0794 1.42463 7.00012 1.42456C3.9208 1.42456 1.42456 3.9208 1.42456 7.00012C1.42463 10.0794 3.92085 12.5757 7.00012 12.5757C10.0793 12.5756 12.5756 10.0793 12.5757 7.00012ZM13.8002 7.00012C13.8001 10.7559 10.7559 13.8001 7.00012 13.8002C3.2443 13.8002 0.199291 10.7559 0.199219 7.00012C0.199219 3.24426 3.24426 0.199219 7.00012 0.199219C10.7559 0.199291 13.8002 3.2443 13.8002 7.00012Z"
37
- fill="currentColor"
38
- />
39
- <path d="M7.6127 3.18921V4.55986H6.38735V3.18921H7.6127Z" fill="currentColor" />
40
- <path d="M7.6127 5.68921V10.8109H6.38735V5.68921H7.6127Z" fill="currentColor" />
41
- </svg>
42
- );
43
- }
44
-
45
- // What every settings field control needs regardless of its value type.
46
- export interface SettingsFieldProps {
47
- // Stable id associating the label with its control.
48
- id: string;
49
- // Visible label.
50
- label: string;
51
- // One-line explanation rendered under the control.
52
- hint: string;
53
- // Draft text this control renders.
54
- text: string;
55
- // True when saving would leave a user-layer entry for this field.
56
- overridden: boolean;
57
- // True when the draft is not a value this field accepts.
58
- invalid: boolean;
59
- // Copy for the overridden badge.
60
- overriddenLabel: string;
61
- // Copy for the reset control.
62
- resetLabel: string;
63
- // Copy shown in place of the hint while the draft is invalid.
64
- invalidLabel: string;
65
- // Disables every control (read-only document, or an unavailable namespace).
66
- disabled: boolean;
67
- // Stage draft text.
68
- onEdit: (text: string) => void;
69
- // Stage a clear so the field re-inherits the composition layer.
70
- onReset: () => void;
71
- }
72
-
73
- // A staged value field. `numeric` only hints the keypad: which drafts a field accepts is decided by its spec, so the
74
- // control never silently rewrites what the user typed.
75
- export function SettingsValueField(
76
- props: Omit<SettingsFieldProps, "hint"> & {
77
- // Optional explanation shown below the input.
78
- hint?: string;
79
- // Rules disclosed by the information button beside the label.
80
- help?: { label: string; content: ReactNode };
81
- // Hints a numeric keypad without narrowing what the control accepts.
82
- numeric?: boolean;
83
- // Placeholder shown while the draft is empty.
84
- placeholder?: string;
85
- },
86
- ) {
87
- const [helpOpen, setHelpOpen] = useState(false);
88
- const helpId = `${props.id}-help`;
89
- const messageId = `${props.id}-message`;
90
- const hasMessage = props.invalid || Boolean(props.hint);
91
- const description = [hasMessage ? messageId : "", helpOpen ? helpId : ""]
92
- .filter(Boolean)
93
- .join(" ");
94
- return (
95
- <Field>
96
- <Head>
97
- <LabelGroup>
98
- <GroupLabel htmlFor={props.id}>{props.label}</GroupLabel>
99
- {props.help !== undefined ? (
100
- <HelpButton
101
- type="button"
102
- aria-label={props.help.label}
103
- aria-expanded={helpOpen}
104
- aria-controls={helpId}
105
- onClick={() => {
106
- setHelpOpen(!helpOpen);
107
- }}
108
- >
109
- <InfoGlyph size={12} />
110
- </HelpButton>
111
- ) : null}
112
- </LabelGroup>
113
- {props.overridden ? (
114
- <Badges>
115
- <NeutralBadge>{props.overriddenLabel}</NeutralBadge>
116
- <Reset type="button" disabled={props.disabled} onClick={props.onReset}>
117
- {props.resetLabel}
118
- </Reset>
119
- </Badges>
120
- ) : null}
121
- </Head>
122
- <Input
123
- id={props.id}
124
- type="text"
125
- {...(props.numeric === true ? { inputMode: "numeric" as const } : {})}
126
- {...(props.invalid ? { "aria-invalid": true } : {})}
127
- aria-describedby={description || undefined}
128
- value={props.text}
129
- placeholder={props.placeholder ?? ""}
130
- disabled={props.disabled}
131
- onChange={(event) => {
132
- props.onEdit(event.target.value);
133
- }}
134
- />
135
- {hasMessage ? (
136
- props.invalid ? (
137
- <Invalid id={messageId}>{props.invalidLabel}</Invalid>
138
- ) : (
139
- <Hint id={messageId}>{props.hint}</Hint>
140
- )
141
- ) : null}
142
- {props.help !== undefined && helpOpen ? (
143
- <Help id={helpId} role="region" aria-label={props.help.label}>
144
- {props.help.content}
145
- </Help>
146
- ) : null}
147
- </Field>
148
- );
149
- }
150
-
151
- // A write-only credential control. The value never rides a response, so the control reports only whether one is
152
- // configured and starts blank; a blank draft writes nothing, which keeps the stored key rather than clearing it.
153
- // 控件要求浏览器不要把已存的登录密码自动填进来(上游 11c0511271)。
154
- export function SettingsSecretField(
155
- props: Pick<SettingsFieldProps, "id" | "label" | "hint" | "text" | "disabled" | "onEdit"> & {
156
- // Whether the Host reports a configured credential for this reference.
157
- configured: boolean;
158
- // Copy describing the configured state.
159
- stateLabel: string;
160
- },
161
- ) {
162
- const Badge = props.configured ? NeutralBadge : QuietBadge;
163
- return (
164
- <Field>
165
- <Head>
166
- <HeadLabel htmlFor={props.id}>{props.label}</HeadLabel>
167
- <Badges>
168
- <Badge>{props.stateLabel}</Badge>
169
- </Badges>
170
- </Head>
171
- <Input
172
- id={props.id}
173
- type="password"
174
- autoComplete="new-password"
175
- value={props.text}
176
- disabled={props.disabled}
177
- onChange={(event) => {
178
- props.onEdit(event.target.value);
179
- }}
180
- />
181
- <Hint>{props.hint}</Hint>
182
- </Field>
183
- );
184
- }
@@ -1,339 +0,0 @@
1
- // 与 vendor/deepseek-harness/packages/client/ui-primitives/src/settings-form/form-model.ts 逐行对应;
2
- // 改这里之前先看上游是否也改了同一处(上游只读)。
3
-
4
- // The staged form model behind a plugin's settings page: a card stages what the user types and writes it only on
5
- // save, and a field is marked overridden by its user-layer presence rather than by a value comparison.
6
-
7
- import { createSnapshotStore, type SnapshotStore } from "@deepseek-ai/dsh-client-store";
8
-
9
- // What the model reads of one Host entry's form.
10
- export interface SettingsFormScopeSnapshot<T> {
11
- // `ready` while the Host serves the entry to this client; the form renders nothing otherwise.
12
- status: "loading" | "ready" | "unavailable";
13
- // Last accepted schema-resolved section; undefined before the first acceptance.
14
- value: T | undefined;
15
- // Composition layer the value resolves over: what a field reverts to once cleared.
16
- base: unknown;
17
- // Raw user layer as stored; a field's PRESENCE here is what marks it overridden.
18
- user: unknown;
19
- // Whether the Host document accepts writes.
20
- writable: boolean;
21
- // Revision the snapshot was read at; a save fences its mutation with the revision its drafts started from.
22
- revision: number | undefined;
23
- }
24
-
25
- // One path edit a save sends, as the shared configuration form's `mutate` accepts it.
26
- export type SettingsFormPathOp =
27
- | { op: "set"; path: readonly string[]; value: unknown }
28
- | { op: "unset"; path: readonly string[] };
29
-
30
- // The entry form the model stages over: the reads and the atomic write of the form `ui-settings` shares per Host entry.
31
- export interface SettingsFormScope<T> {
32
- getSnapshot(): SettingsFormScopeSnapshot<T>;
33
- // Observe snapshot replacements.
34
- subscribe(listener: () => void): () => void;
35
- // Apply ordered field edits in one revision-fenced write; resolves after any recovery read.
36
- mutate(ops: readonly SettingsFormPathOp[], expectedRevision?: number): Promise<boolean>;
37
- }
38
-
39
- // The write one field's staged text performs when the card is saved.
40
- export type SettingsFieldWrite = { kind: "set"; value: unknown } | { kind: "clear" };
41
-
42
- // How one section field converts between its stored value and its draft text.
43
- export interface SettingsFieldSpec {
44
- // Field name inside the namespace section.
45
- field: string;
46
- // Render a stored value as draft text; the empty string when the section carries none.
47
- format: (value: unknown) => string;
48
- // The write this draft text stages, or undefined when the text is not a value this field accepts (blocks the save).
49
- parse: (text: string) => SettingsFieldWrite | undefined;
50
- }
51
-
52
- // A control whose value is written outside the settings section: a credential literal never rides a response, so its
53
- // draft is blank until typed and a blank draft writes nothing.
54
- export interface SettingsSecretSpec {
55
- // Field name addressing this control inside the card's form.
56
- field: string;
57
- // Write the staged text; resolves to whether the Host accepted it.
58
- write: (text: string) => Promise<boolean>;
59
- }
60
-
61
- // One field as a card's control renders it.
62
- export interface SettingsFieldState {
63
- // Draft text the control renders.
64
- text: string;
65
- // Whether saving would leave a user-layer entry for this field; a staged edit answers for itself.
66
- overridden: boolean;
67
- // Whether the draft is not a value this field accepts, which blocks saving.
68
- invalid: boolean;
69
- }
70
-
71
- // Form state every plugin card shares.
72
- export interface SettingsFormShell {
73
- // False while the namespace is not served to this client; the card renders nothing.
74
- available: boolean;
75
- // Whether the Host document accepts writes.
76
- writable: boolean;
77
- // Whether the form holds edits that a save would write.
78
- dirty: boolean;
79
- // Whether any staged draft is invalid, which blocks the save.
80
- invalid: boolean;
81
- // Whether a save is crossing the wire.
82
- saving: boolean;
83
- // Whether the last save did not land as staged; cleared by the next edit or save.
84
- failed: boolean;
85
- }
86
-
87
- // The write actions every plugin card's slot entry injects.
88
- export interface SettingsFormActions {
89
- // Stage draft text for one field.
90
- edit: (field: string, text: string) => void;
91
- // Stage a clear, so saving lets the field re-inherit the composition layer.
92
- resetField: (field: string) => void;
93
- // Write every staged edit, then re-seed from what the Host accepted.
94
- save: () => void;
95
- // Drop every staged edit.
96
- discard: () => void;
97
- }
98
-
99
- // One field's staged edit.
100
- interface StagedEdit {
101
- // Draft text the control renders.
102
- text: string;
103
- // True when this edit clears the field whatever text it shows.
104
- clear: boolean;
105
- }
106
-
107
- // One staged edit resolved into the write a save performs.
108
- interface PlannedWrite {
109
- // Field this entry writes.
110
- field: string;
111
- // Perform the write and report whether the Host holds the staged value afterwards.
112
- run?: () => Promise<boolean>;
113
- op?: SettingsFormPathOp;
114
- }
115
-
116
- // A whole-number field: an empty draft clears the field, any other draft that is not a finite number blocks the save.
117
- export function settingsNumberField(field: string): SettingsFieldSpec {
118
- return {
119
- field,
120
- // A section that carries no number for this field renders empty rather than as a value nobody chose.
121
- format: (value) => (typeof value === "number" ? String(value) : ""),
122
- parse: (text) => {
123
- const trimmed = text.trim();
124
- if (trimmed === "") return { kind: "clear" };
125
- const parsed = Number(trimmed);
126
- return Number.isFinite(parsed) ? { kind: "set", value: parsed } : undefined;
127
- },
128
- };
129
- }
130
-
131
- // A free-text field: an empty draft clears the field, so emptying the control and saving is the same as resetting it.
132
- export function settingsTextField(field: string): SettingsFieldSpec {
133
- return {
134
- field,
135
- format: (value) => (typeof value === "string" ? value : ""),
136
- parse: (text) => {
137
- const trimmed = text.trim();
138
- return trimmed === "" ? { kind: "clear" } : { kind: "set", value: trimmed };
139
- },
140
- };
141
- }
142
-
143
- // Stages one card's edits over one settings namespace and writes them on save. The form publishes through a snapshot
144
- // store because slot components read through a snapshot selector while both the scope and the drafts change underneath.
145
- export class SettingsFormModel<T> {
146
- private readonly specs: Map<string, SettingsFieldSpec>;
147
- private readonly secretSpecs: Map<string, SettingsSecretSpec>;
148
- private readonly staged = new Map<string, StagedEdit>();
149
- private readonly listeners = new Set<() => void>();
150
- private baseline: SettingsFormScopeSnapshot<T> | undefined;
151
- private readonly unsubscribe: () => void;
152
- private saving = false;
153
- private failed = false;
154
-
155
- constructor(
156
- private readonly scope: SettingsFormScope<T>,
157
- specs: SettingsFieldSpec[],
158
- secrets: SettingsSecretSpec[] = [],
159
- ) {
160
- this.specs = new Map(specs.map((spec) => [spec.field, spec]));
161
- this.secretSpecs = new Map(secrets.map((spec) => [spec.field, spec]));
162
- this.unsubscribe = scope.subscribe(() => {
163
- this.publish();
164
- });
165
- }
166
-
167
- // Publish a projection of this form, rebuilt whenever the scope or a draft changes.
168
- bind<S>(project: () => S): SnapshotStore<S> {
169
- const store = createSnapshotStore(project());
170
- this.listeners.add(() => {
171
- store.set(project());
172
- });
173
- return store;
174
- }
175
-
176
- // Read the card-level state: what the Host serves, and what a save would do.
177
- shell(): SettingsFormShell {
178
- const snapshot = this.scope.getSnapshot();
179
- const plan = this.plan();
180
- return {
181
- available: snapshot.status === "ready",
182
- writable: snapshot.writable,
183
- dirty: plan.length > 0,
184
- invalid: plan.some((item) => item.run === undefined && item.op === undefined),
185
- saving: this.saving,
186
- failed: this.failed,
187
- };
188
- }
189
-
190
- // Read one control's state: draft text, whether a save would leave an override, and whether it is invalid.
191
- field(field: string): SettingsFieldState {
192
- const staged = this.staged.get(field);
193
- if (this.secretSpecs.has(field)) {
194
- return { text: staged?.text ?? "", overridden: false, invalid: false };
195
- }
196
- const spec = this.spec(field);
197
- if (staged === undefined) {
198
- return {
199
- text: spec.format(this.sectionValue(field)),
200
- overridden: this.stored(field),
201
- invalid: false,
202
- };
203
- }
204
- const write = staged.clear ? { kind: "clear" as const } : spec.parse(staged.text);
205
- return {
206
- text: staged.text,
207
- overridden: write?.kind === "set",
208
- invalid: write === undefined,
209
- };
210
- }
211
-
212
- // Build the edit, reset, save, and discard actions bound to this form.
213
- actions(): SettingsFormActions {
214
- return {
215
- edit: (field, text) => {
216
- this.stage(field, { text, clear: false });
217
- },
218
- resetField: (field) => {
219
- this.stage(field, { text: this.spec(field).format(this.baseValue(field)), clear: true });
220
- },
221
- save: () => {
222
- void this.save();
223
- },
224
- discard: () => {
225
- if (this.staged.size === 0 && !this.failed) return;
226
- this.staged.clear();
227
- this.baseline = undefined;
228
- this.failed = false;
229
- this.publish();
230
- },
231
- };
232
- }
233
-
234
- // Write every staged edit, then re-seed from what the Host accepted: the outcome is read back rather than predicted
235
- // (the Host is the only authority), and a save that did not land keeps its drafts so the user can correct them.
236
- async save(): Promise<void> {
237
- const plan = this.plan();
238
- if (
239
- !plan.length ||
240
- this.saving ||
241
- !this.scope.getSnapshot().writable ||
242
- plan.some((item) => item.run === undefined && item.op === undefined)
243
- )
244
- return;
245
- this.saving = true;
246
- this.failed = false;
247
- this.publish();
248
- try {
249
- const ops = plan.flatMap((item) => (item.op === undefined ? [] : [item.op]));
250
- let landed = !ops.length || (await this.scope.mutate(ops, this.baseline?.revision));
251
- if (!landed) {
252
- this.failed = true;
253
- return;
254
- }
255
- for (const item of plan) if (item.run) landed = (await item.run()) && landed;
256
- if (landed) {
257
- this.staged.clear();
258
- this.baseline = undefined;
259
- }
260
- this.failed = !landed;
261
- } catch {
262
- this.failed = true;
263
- } finally {
264
- this.saving = false;
265
- this.publish();
266
- }
267
- }
268
-
269
- // Release the form's accepted-value subscription.
270
- dispose(): void {
271
- this.unsubscribe();
272
- this.listeners.clear();
273
- }
274
-
275
- // Every staged edit a save would write; an entry whose draft its field rejects carries no write (the form stays
276
- // dirty, and the save refuses rather than dropping the edit).
277
- private plan(): PlannedWrite[] {
278
- const plan: PlannedWrite[] = [];
279
- for (const [field, staged] of this.staged) {
280
- const secret = this.secretSpecs.get(field);
281
- if (secret !== undefined) {
282
- const value = staged.text.trim();
283
- if (value !== "") plan.push({ field, run: () => secret.write(value) });
284
- continue;
285
- }
286
- const spec = this.spec(field);
287
- if (staged.clear) {
288
- if (this.stored(field)) plan.push({ field, op: { op: "unset", path: [field] } });
289
- continue;
290
- }
291
- if (staged.text === spec.format(this.sectionValue(field))) continue;
292
- const write = spec.parse(staged.text);
293
- if (write === undefined) plan.push({ field });
294
- else if (write.kind === "clear") plan.push({ field, op: { op: "unset", path: [field] } });
295
- else plan.push({ field, op: { op: "set", path: [field], value: write.value } });
296
- }
297
- return plan;
298
- }
299
-
300
- private stage(field: string, edit: StagedEdit): void {
301
- this.baseline ??= this.scope.getSnapshot();
302
- this.staged.set(field, edit);
303
- this.failed = false;
304
- this.publish();
305
- }
306
-
307
- private spec(field: string): SettingsFieldSpec {
308
- const spec = this.specs.get(field);
309
- // Every call site names a field this card declared; a missing one is a wiring mistake that must not become a
310
- // silently inert control.
311
- if (spec === undefined) throw new Error(`plugin card has no field ${field}`);
312
- return spec;
313
- }
314
-
315
- private snapshotOf(): SettingsFormScopeSnapshot<T> {
316
- return this.scope.getSnapshot();
317
- }
318
-
319
- private sectionValue(field: string): unknown {
320
- return (this.snapshotOf().value as Record<string, unknown> | undefined)?.[field];
321
- }
322
-
323
- private baseValue(field: string): unknown {
324
- return (this.snapshotOf().base as Record<string, unknown> | undefined)?.[field];
325
- }
326
-
327
- private userLayer(): Record<string, unknown> | undefined {
328
- return this.snapshotOf().user as Record<string, unknown> | undefined;
329
- }
330
-
331
- private stored(field: string): boolean {
332
- const user = this.userLayer();
333
- return user !== undefined && Object.hasOwn(user, field);
334
- }
335
-
336
- private publish(): void {
337
- for (const listener of this.listeners) listener();
338
- }
339
- }
@@ -1,27 +0,0 @@
1
- import type { StandardProperties, VendorProperties } from "csstype";
2
-
3
- type NonStandardProperties = {
4
- cornerShape?: CSSValue<string>;
5
- clip?: CSSValue<string>;
6
- WebkitBoxOrient?: CSSValue<string>;
7
- };
8
-
9
- type Properties = Omit<StandardProperties & VendorProperties, keyof NonStandardProperties> &
10
- NonStandardProperties;
11
-
12
- export type CSSVar = `--${string}`;
13
- export type CSSSelector = `&${string}` | `${string}&` | `@${string}`;
14
-
15
- export type CSSValue<Fallback = string | number> = Fallback | (() => Iterable<Fallback>);
16
-
17
- export type CSSObject<Props extends object, Fallback = string | number> = {
18
- [K in keyof Props]?: Props[K] | CSSValue<Fallback>;
19
- } & {
20
- [K in CSSVar]?: CSSValue<Fallback>;
21
- };
22
-
23
- type Nested<Props extends object> = CSSObject<Props> & {
24
- [K in CSSSelector]?: Nested<Props>;
25
- };
26
-
27
- export type CSSProps = Nested<Properties>;
@@ -1,9 +0,0 @@
1
- // 焦点环口径搬上游 ui-theme 的 `focus.css`:颜色读官方 `--dsw-focus-ring-color`
2
- // (主题把它按输入模态解析成蓝,指针模态下置 transparent 抑制环),变量缺失时回退业务蓝;
3
- // 宽度读官方的 `--dsw-focus-ring-width`。偏移与线宽的特例仍由各控件自己声明。
4
- import { dsw } from "../theme.ts";
5
- import { Token } from "./token.ts";
6
-
7
- export const focusRing = `${String(dsw.focus.ring.width)} solid ${Token.collect(
8
- Token.fallbackVar(dsw.focus.ring.color, String(dsw.alias.state.business.primary)),
9
- )}`;
@@ -1,6 +0,0 @@
1
- export type * from "./css.ts";
2
- export { Styling, styling } from "./styling.ts";
3
- export { Token } from "./token.ts";
4
- export type { CSSVarRef, TokenVars, Tokens } from "./token.ts";
5
- export { styled, styleOf } from "./styled.tsx";
6
- export type { StyledComponent } from "./styled.tsx";
@@ -1,54 +0,0 @@
1
- // 轻量 styled:intrinsic / 组件 + ref,样式走 data-css 属性(不做 polymorphic,
2
- // 参考实现里的 asChild 由 ark-ui 提供,我们不需要)。
3
-
4
- import { createElement, forwardRef, useMemo } from "react";
5
- import type {
6
- ComponentProps,
7
- ComponentPropsWithRef,
8
- ElementType,
9
- ForwardRefExoticComponent,
10
- Ref,
11
- RefAttributes,
12
- } from "react";
13
- import type { CSSProps } from "./css.ts";
14
- import { styling } from "./styling.ts";
15
- import { toMerged } from "./toolkit.ts";
16
-
17
- type StyledProps<T extends ElementType> = Omit<ComponentProps<T>, "ref">;
18
- type StyledRef<T extends ElementType> =
19
- ComponentPropsWithRef<T>["ref"] extends Ref<infer R> ? R : never;
20
-
21
- const sxSymbol: unique symbol = Symbol("styled.sx");
22
-
23
- export type StyledComponent<T extends ElementType> = ForwardRefExoticComponent<
24
- StyledProps<T> & RefAttributes<StyledRef<T>>
25
- > & { readonly [sxSymbol]: CSSProps };
26
-
27
- // `styled('div')({ padding: '4px' }, { '&:hover': { … } })` → 带 `data-css-*` 的组件。
28
- // 多个样式对象按顺序深合并(后者覆盖前者),便于把变体叠在基样式上。
29
- export function styled<T extends ElementType>(
30
- Component: T,
31
- defaultProps: Partial<ComponentProps<T>> = {},
32
- ) {
33
- return (...styles: CSSProps[]): StyledComponent<T> => {
34
- const sx = styles.reduce<CSSProps>((merged, style) => toMerged(merged, style), {});
35
-
36
- const Styled = forwardRef<StyledRef<T>, StyledProps<T>>((props, ref) => {
37
- const styleProps = useMemo(() => styling.props(sx), []);
38
- return createElement(Component as ElementType, {
39
- ...defaultProps,
40
- ...props,
41
- ...styleProps,
42
- ref,
43
- });
44
- });
45
- Styled.displayName = `styled(${typeof Component === "string" ? Component : (Component.displayName ?? Component.name ?? "Component")})`;
46
-
47
- return Object.assign(Styled, { [sxSymbol]: sx }) as StyledComponent<T>;
48
- };
49
- }
50
-
51
- // 取回 styled 组件携带的样式对象(需要在其之上继续组合时用)。
52
- export function styleOf(component: StyledComponent<ElementType>): CSSProps {
53
- return component[sxSymbol];
54
- }