@huanlin/dsh-plugin-preface-context 0.1.0

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 (38) hide show
  1. package/README.md +113 -0
  2. package/cordis.patch.yml +11 -0
  3. package/lib/client.js +794 -0
  4. package/lib/client.js.map +1 -0
  5. package/lib/index.js +127 -0
  6. package/lib/types/client/bind-snapshot-selector.d.ts +7 -0
  7. package/lib/types/client/bind-snapshot-selector.js +19 -0
  8. package/lib/types/client/client/bind-snapshot-selector.d.ts +7 -0
  9. package/lib/types/client/client/bind-snapshot-selector.js +19 -0
  10. package/lib/types/client/client/index.d.ts +39 -0
  11. package/lib/types/client/client/index.js +50 -0
  12. package/lib/types/client/client/locales.d.ts +7 -0
  13. package/lib/types/client/client/locales.js +41 -0
  14. package/lib/types/client/client/preface-card-controller.d.ts +94 -0
  15. package/lib/types/client/client/preface-card-controller.js +181 -0
  16. package/lib/types/client/client/preface-card.css.d.ts +53 -0
  17. package/lib/types/client/client/preface-card.css.js +327 -0
  18. package/lib/types/client/client/preface-card.d.ts +27 -0
  19. package/lib/types/client/client/preface-card.js +41 -0
  20. package/lib/types/client/config.d.ts +41 -0
  21. package/lib/types/client/config.js +42 -0
  22. package/lib/types/client/index.d.ts +39 -0
  23. package/lib/types/client/index.js +50 -0
  24. package/lib/types/client/locales.d.ts +7 -0
  25. package/lib/types/client/locales.js +41 -0
  26. package/lib/types/client/preface-card-controller.d.ts +94 -0
  27. package/lib/types/client/preface-card-controller.js +181 -0
  28. package/lib/types/client/preface-card.css.d.ts +53 -0
  29. package/lib/types/client/preface-card.css.js +327 -0
  30. package/lib/types/client/preface-card.d.ts +27 -0
  31. package/lib/types/client/preface-card.js +41 -0
  32. package/lib/types/config.d.ts +40 -0
  33. package/lib/types/config.js +42 -0
  34. package/lib/types/index.d.ts +46 -0
  35. package/lib/types/index.js +64 -0
  36. package/lib/types/settings.d.ts +50 -0
  37. package/lib/types/settings.js +85 -0
  38. package/package.json +130 -0
@@ -0,0 +1,181 @@
1
+ /**
2
+ * The preface-context card's staged form over the `preface-context` settings
3
+ * namespace.
4
+ *
5
+ * The upstream `ui-settings-plugins` package owns a `CardForm` helper, but it
6
+ * is an internal module (not re-exported from the package's `/client` public
7
+ * face), so an external plugin cannot reuse it. This controller is a slim
8
+ * equivalent: it stages the two fields (`enabled` boolean, `contextText`
9
+ * multiline string) and writes them through the bound {@link SettingsScope}
10
+ * on save, re-deriving its projection whenever the scope or a draft changes.
11
+ *
12
+ * Unlike the upstream `CardForm`, the `contextText` field is a multiline
13
+ * textarea: its draft preserves internal whitespace and newlines (the
14
+ * upstream `textField` helper trims, which would collapse a multi-paragraph
15
+ * preface into one line).
16
+ */
17
+ /** A minimal snapshot store: a value + a listener set. Self-contained so the
18
+ * controller does not pull `createSnapshotStore` (a browser-bundle symbol)
19
+ * into its unit-test graph. */
20
+ class MiniSnapshotStore {
21
+ snapshot;
22
+ listeners = new Set();
23
+ constructor(initial) {
24
+ this.snapshot = initial;
25
+ }
26
+ getSnapshot() {
27
+ return this.snapshot;
28
+ }
29
+ set(value) {
30
+ this.snapshot = value;
31
+ for (const listener of [...this.listeners])
32
+ listener();
33
+ }
34
+ update(mutator) {
35
+ // Shallow-clone then mutate (the projections here are plain objects with
36
+ // no nested mutation, so a deep immer draft is unnecessary).
37
+ const draft = Array.isArray(this.snapshot) ? [...this.snapshot] : { ...this.snapshot };
38
+ mutator(draft);
39
+ this.set(draft);
40
+ }
41
+ subscribe(listener) {
42
+ this.listeners.add(listener);
43
+ return () => { this.listeners.delete(listener); };
44
+ }
45
+ }
46
+ /** The default value for a field when the user-layer carries none. */
47
+ function baseValue(snapshot, field) {
48
+ return snapshot.base?.[field];
49
+ }
50
+ /** The current resolved value of a field. */
51
+ function sectionValue(snapshot, field) {
52
+ return snapshot.value?.[field];
53
+ }
54
+ /** Whether the user layer carries an entry for a field. */
55
+ function stored(snapshot, field) {
56
+ const user = snapshot.user;
57
+ return user !== undefined && Object.hasOwn(user, field);
58
+ }
59
+ /** Format a stored value as draft text for a field. */
60
+ function format(field, value) {
61
+ if (field === 'enabled')
62
+ return value === true ? 'true' : 'false';
63
+ if (field === 'contextText')
64
+ return typeof value === 'string' ? value : '';
65
+ return '';
66
+ }
67
+ /** Parse draft text into a write value, or undefined when invalid. */
68
+ function parse(field, text) {
69
+ if (field === 'enabled') {
70
+ // The toggle is a boolean; the textarea is not used for this field.
71
+ return { value: text === 'true' };
72
+ }
73
+ // contextText: empty draft clears the field; otherwise the raw text is the
74
+ // value. Internal whitespace and newlines are preserved.
75
+ const trimmed = text.trim();
76
+ return trimmed === '' ? { clear: true } : { value: text };
77
+ }
78
+ /**
79
+ * Bridges the `preface-context` scope onto the card's staged form.
80
+ *
81
+ * Publishes through a snapshot store because slot components read through a
82
+ * snapshot selector, while both the scope and the local drafts change
83
+ * underneath; every projection is rebuilt from the two together.
84
+ */
85
+ export class PrefaceCardController {
86
+ scope;
87
+ store;
88
+ staged = new Map();
89
+ saving = false;
90
+ failed = false;
91
+ /** @param scope - the bound settings scope for the `preface-context` namespace. */
92
+ constructor(scope) {
93
+ this.scope = scope;
94
+ this.store = new MiniSnapshotStore(this.project());
95
+ scope.subscribe(() => { this.publish(); });
96
+ }
97
+ /** @returns the store the card's component reads through its bound selector. */
98
+ get snapshot() {
99
+ return this.store;
100
+ }
101
+ project() {
102
+ const snapshot = this.scope.getSnapshot();
103
+ const dirty = this.staged.size > 0;
104
+ return {
105
+ available: snapshot.status === 'ready',
106
+ writable: snapshot.writable,
107
+ dirty,
108
+ saving: this.saving,
109
+ failed: this.failed,
110
+ enabled: this.fieldState(snapshot, 'enabled'),
111
+ contextText: this.fieldState(snapshot, 'contextText'),
112
+ };
113
+ }
114
+ fieldState(snapshot, field) {
115
+ const staged = this.staged.get(field);
116
+ if (staged === undefined) {
117
+ return { text: format(field, sectionValue(snapshot, field)), overridden: stored(snapshot, field) };
118
+ }
119
+ const write = staged.clear ? { clear: true } : parse(field, staged.text);
120
+ return {
121
+ text: staged.text,
122
+ overridden: write !== undefined && 'value' in write,
123
+ };
124
+ }
125
+ /** Build the face the card's slot registration injects. */
126
+ inject() {
127
+ return {
128
+ hooks: { prefaceCard: this.store },
129
+ edit: (field, text) => { this.stage(field, { text, clear: false }); },
130
+ resetField: (field) => {
131
+ this.stage(field, { text: format(field, baseValue(this.scope.getSnapshot(), field)), clear: true });
132
+ },
133
+ save: () => { void this.save(); },
134
+ discard: () => {
135
+ if (this.staged.size === 0 && !this.failed)
136
+ return;
137
+ this.staged.clear();
138
+ this.failed = false;
139
+ this.publish();
140
+ },
141
+ };
142
+ }
143
+ stage(field, edit) {
144
+ this.staged.set(field, edit);
145
+ this.failed = false;
146
+ this.publish();
147
+ }
148
+ publish() {
149
+ this.store.set(this.project());
150
+ }
151
+ /** Write every staged edit, then re-seed from what the Host accepted. */
152
+ async save() {
153
+ if (this.staged.size === 0 || this.saving)
154
+ return;
155
+ this.saving = true;
156
+ this.failed = false;
157
+ this.publish();
158
+ let landed = true;
159
+ for (const [field, staged] of this.staged) {
160
+ const write = staged.clear ? { clear: true } : parse(field, staged.text);
161
+ if (write === undefined)
162
+ continue;
163
+ try {
164
+ if ('clear' in write) {
165
+ await this.scope.unset(field);
166
+ }
167
+ else {
168
+ await this.scope.set(field, write.value);
169
+ }
170
+ }
171
+ catch {
172
+ landed = false;
173
+ }
174
+ }
175
+ if (landed)
176
+ this.staged.clear();
177
+ this.saving = false;
178
+ this.failed = !landed;
179
+ this.publish();
180
+ }
181
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Card styles + runtime style injection for the preface-context settings
3
+ * card.
4
+ *
5
+ * The DSH client loader never serves `.css` artifacts — every style must be
6
+ * injected inside the bundle's factory closure (the same contract the
7
+ * upstream tsdown.client preset implements with lightningcss). A plain
8
+ * template-string stylesheet installed once at plugin activation is the slim
9
+ * equivalent for a hand-rolled bundle: static, prefixed class names
10
+ * (`dsh-pfc-*`, unique in the document) replace the hashed CSS-modules map,
11
+ * and the `<style data-plugin-css>` tag is removed when the plugin unloads.
12
+ *
13
+ * Colors resolve through `--dsw-alias-*` tokens so the card adapts to the
14
+ * light/dark theme.
15
+ *
16
+ * @module @huanlin/dsh-plugin-preface-context/client/preface-card.css
17
+ */
18
+ /** Class-name map consumed by the card component (CSS-modules replacement). */
19
+ export declare const css: {
20
+ readonly card: "dsh-pfc-card";
21
+ readonly cardOpen: "dsh-pfc-card--open";
22
+ readonly header: "dsh-pfc-header";
23
+ readonly headText: "dsh-pfc-head-text";
24
+ readonly name: "dsh-pfc-name";
25
+ readonly description: "dsh-pfc-description";
26
+ readonly pending: "dsh-pfc-pending";
27
+ readonly chevron: "dsh-pfc-chevron";
28
+ readonly chevronOpen: "dsh-pfc-chevron--open";
29
+ readonly body: "dsh-pfc-body";
30
+ readonly readOnly: "dsh-pfc-readonly";
31
+ readonly field: "dsh-pfc-field";
32
+ readonly head: "dsh-pfc-field-head";
33
+ readonly label: "dsh-pfc-label";
34
+ readonly badges: "dsh-pfc-badges";
35
+ readonly badge: "dsh-pfc-badge";
36
+ readonly reset: "dsh-pfc-reset";
37
+ readonly textarea: "dsh-pfc-textarea";
38
+ readonly toggleRow: "dsh-pfc-toggle-row";
39
+ readonly toggle: "dsh-pfc-toggle";
40
+ readonly toggleOn: "dsh-pfc-toggle--on";
41
+ readonly knob: "dsh-pfc-knob";
42
+ readonly knobOn: "dsh-pfc-knob--on";
43
+ readonly hint: "dsh-pfc-hint";
44
+ readonly footer: "dsh-pfc-footer";
45
+ readonly failed: "dsh-pfc-failed";
46
+ readonly discard: "dsh-pfc-discard";
47
+ readonly save: "dsh-pfc-save";
48
+ };
49
+ /**
50
+ * Install the card stylesheet as one tagged `<style>` element.
51
+ * @returns the disposer removing the tag (idempotent outside a document).
52
+ */
53
+ export declare function installPrefaceCardStyles(): () => void;
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Card styles + runtime style injection for the preface-context settings
3
+ * card.
4
+ *
5
+ * The DSH client loader never serves `.css` artifacts — every style must be
6
+ * injected inside the bundle's factory closure (the same contract the
7
+ * upstream tsdown.client preset implements with lightningcss). A plain
8
+ * template-string stylesheet installed once at plugin activation is the slim
9
+ * equivalent for a hand-rolled bundle: static, prefixed class names
10
+ * (`dsh-pfc-*`, unique in the document) replace the hashed CSS-modules map,
11
+ * and the `<style data-plugin-css>` tag is removed when the plugin unloads.
12
+ *
13
+ * Colors resolve through `--dsw-alias-*` tokens so the card adapts to the
14
+ * light/dark theme.
15
+ *
16
+ * @module @huanlin/dsh-plugin-preface-context/client/preface-card.css
17
+ */
18
+ /** Class-name map consumed by the card component (CSS-modules replacement). */
19
+ export const css = {
20
+ card: 'dsh-pfc-card',
21
+ cardOpen: 'dsh-pfc-card--open',
22
+ header: 'dsh-pfc-header',
23
+ headText: 'dsh-pfc-head-text',
24
+ name: 'dsh-pfc-name',
25
+ description: 'dsh-pfc-description',
26
+ pending: 'dsh-pfc-pending',
27
+ chevron: 'dsh-pfc-chevron',
28
+ chevronOpen: 'dsh-pfc-chevron--open',
29
+ body: 'dsh-pfc-body',
30
+ readOnly: 'dsh-pfc-readonly',
31
+ field: 'dsh-pfc-field',
32
+ head: 'dsh-pfc-field-head',
33
+ label: 'dsh-pfc-label',
34
+ badges: 'dsh-pfc-badges',
35
+ badge: 'dsh-pfc-badge',
36
+ reset: 'dsh-pfc-reset',
37
+ textarea: 'dsh-pfc-textarea',
38
+ toggleRow: 'dsh-pfc-toggle-row',
39
+ toggle: 'dsh-pfc-toggle',
40
+ toggleOn: 'dsh-pfc-toggle--on',
41
+ knob: 'dsh-pfc-knob',
42
+ knobOn: 'dsh-pfc-knob--on',
43
+ hint: 'dsh-pfc-hint',
44
+ footer: 'dsh-pfc-footer',
45
+ failed: 'dsh-pfc-failed',
46
+ discard: 'dsh-pfc-discard',
47
+ save: 'dsh-pfc-save',
48
+ };
49
+ /** `data-plugin-css` tag id of the injected stylesheet. */
50
+ const STYLE_TAG_ID = '@huanlin/dsh-plugin-preface-context/preface-card.css';
51
+ /** The card stylesheet (injected as one tagged `<style>` at activation). */
52
+ const CSS = `
53
+ .dsh-pfc-card {
54
+ list-style: none;
55
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.22));
56
+ background: var(--dsw-alias-bg-layer-3, transparent);
57
+ border-radius: 12px;
58
+ transition: border-color 0.16s, background 0.16s;
59
+ overflow: hidden;
60
+ }
61
+
62
+ .dsh-pfc-card--open {
63
+ border-color: var(--dsw-alias-border-l3, rgba(128, 128, 128, 0.35));
64
+ }
65
+
66
+ .dsh-pfc-header {
67
+ display: flex;
68
+ width: 100%;
69
+ align-items: center;
70
+ gap: 12px;
71
+ padding: 14px 16px;
72
+ font: inherit;
73
+ color: inherit;
74
+ text-align: left;
75
+ background: transparent;
76
+ border: 0;
77
+ cursor: pointer;
78
+ }
79
+
80
+ .dsh-pfc-head-text {
81
+ display: flex;
82
+ flex: 1 1 auto;
83
+ flex-direction: column;
84
+ gap: 4px;
85
+ min-width: 0;
86
+ }
87
+
88
+ .dsh-pfc-name {
89
+ font-size: 15px;
90
+ font-weight: 600;
91
+ line-height: 1.4;
92
+ color: var(--dsw-alias-label-primary, inherit);
93
+ }
94
+
95
+ .dsh-pfc-description {
96
+ font-size: 13px;
97
+ line-height: 1.5;
98
+ color: var(--dsw-alias-label-tertiary, rgba(128, 128, 128, 0.7));
99
+ }
100
+
101
+ .dsh-pfc-pending {
102
+ flex: none;
103
+ white-space: nowrap;
104
+ font-size: 11px;
105
+ font-weight: 500;
106
+ line-height: 17px;
107
+ padding: 1px 8px;
108
+ border-radius: 999px;
109
+ color: var(--dsw-alias-label-secondary, inherit);
110
+ background: var(--dsw-alias-bg-module-platform, rgba(128, 128, 128, 0.12));
111
+ }
112
+
113
+ .dsh-pfc-chevron {
114
+ flex: none;
115
+ display: inline-flex;
116
+ align-items: center;
117
+ color: var(--dsw-alias-label-tertiary, inherit);
118
+ transition: transform 0.16s;
119
+ }
120
+
121
+ .dsh-pfc-chevron--open {
122
+ transform: rotate(180deg);
123
+ }
124
+
125
+ .dsh-pfc-body {
126
+ margin: 0 16px;
127
+ padding: 12px 0 4px;
128
+ border-top: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.22));
129
+ }
130
+
131
+ .dsh-pfc-readonly {
132
+ margin: 0 0 8px;
133
+ font-size: 12px;
134
+ line-height: 1.5;
135
+ color: var(--dsw-alias-label-tertiary, rgba(128, 128, 128, 0.7));
136
+ }
137
+
138
+ .dsh-pfc-field {
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: 4px;
142
+ margin: 0 0 12px;
143
+ }
144
+
145
+ .dsh-pfc-field-head {
146
+ display: flex;
147
+ align-items: center;
148
+ gap: 8px;
149
+ }
150
+
151
+ .dsh-pfc-label {
152
+ font-size: 13px;
153
+ font-weight: 500;
154
+ color: var(--dsw-alias-label-primary, inherit);
155
+ }
156
+
157
+ .dsh-pfc-badges {
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 6px;
161
+ margin-left: auto;
162
+ }
163
+
164
+ .dsh-pfc-badge {
165
+ font-size: 11px;
166
+ font-weight: 500;
167
+ line-height: 17px;
168
+ padding: 1px 8px;
169
+ border-radius: 999px;
170
+ color: var(--dsw-alias-label-secondary, inherit);
171
+ background: var(--dsw-alias-bg-module-platform, rgba(128, 128, 128, 0.12));
172
+ }
173
+
174
+ .dsh-pfc-reset {
175
+ appearance: none;
176
+ font: inherit;
177
+ font-size: 12px;
178
+ padding: 0;
179
+ border: 0;
180
+ background: transparent;
181
+ cursor: pointer;
182
+ color: var(--dsw-alias-brand-primary, #0a84ff);
183
+ }
184
+
185
+ .dsh-pfc-reset:disabled {
186
+ cursor: not-allowed;
187
+ opacity: 0.5;
188
+ }
189
+
190
+ .dsh-pfc-textarea {
191
+ width: 100%;
192
+ box-sizing: border-box;
193
+ min-height: 96px;
194
+ resize: vertical;
195
+ padding: 6px 10px;
196
+ font-family: var(--dsw-font-mono, ui-monospace, monospace);
197
+ font-size: 13px;
198
+ line-height: 1.5;
199
+ color: var(--dsw-alias-label-primary, inherit);
200
+ background: var(--dsw-alias-bg-layer-3, transparent);
201
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.3));
202
+ border-radius: 8px;
203
+ }
204
+
205
+ .dsh-pfc-textarea:focus {
206
+ outline: none;
207
+ border-color: var(--dsw-alias-brand-primary, #0a84ff);
208
+ }
209
+
210
+ .dsh-pfc-textarea:disabled {
211
+ opacity: 0.6;
212
+ cursor: not-allowed;
213
+ }
214
+
215
+ .dsh-pfc-toggle-row {
216
+ display: flex;
217
+ align-items: center;
218
+ gap: 10px;
219
+ }
220
+
221
+ .dsh-pfc-toggle {
222
+ position: relative;
223
+ width: 40px;
224
+ height: 22px;
225
+ flex: none;
226
+ padding: 0;
227
+ background: var(--dsw-alias-bg-module-platform, rgba(128, 128, 128, 0.12));
228
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.3));
229
+ border-radius: 999px;
230
+ cursor: pointer;
231
+ transition: background 0.16s, border-color 0.16s;
232
+ }
233
+
234
+ .dsh-pfc-toggle--on {
235
+ background: var(--dsw-alias-brand-primary, #0a84ff);
236
+ border-color: var(--dsw-alias-brand-primary, #0a84ff);
237
+ }
238
+
239
+ .dsh-pfc-knob {
240
+ position: absolute;
241
+ top: 2px;
242
+ left: 2px;
243
+ width: 16px;
244
+ height: 16px;
245
+ background: var(--dsw-alias-bg-layer-1, #fff);
246
+ border-radius: 50%;
247
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
248
+ transition: transform 0.16s;
249
+ }
250
+
251
+ .dsh-pfc-knob--on {
252
+ transform: translateX(18px);
253
+ }
254
+
255
+ .dsh-pfc-toggle:disabled {
256
+ cursor: not-allowed;
257
+ opacity: 0.5;
258
+ }
259
+
260
+ .dsh-pfc-hint {
261
+ margin: 0;
262
+ font-size: 12px;
263
+ line-height: 1.5;
264
+ color: var(--dsw-alias-label-tertiary, rgba(128, 128, 128, 0.6));
265
+ }
266
+
267
+ .dsh-pfc-footer {
268
+ display: flex;
269
+ align-items: center;
270
+ justify-content: flex-end;
271
+ gap: 8px;
272
+ padding: 12px 0 4px;
273
+ border-top: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.22));
274
+ }
275
+
276
+ .dsh-pfc-failed {
277
+ flex: 1 1 auto;
278
+ min-width: 0;
279
+ margin: 0;
280
+ font-size: 12px;
281
+ line-height: 1.5;
282
+ color: var(--dsw-alias-label-error, #ff453a);
283
+ }
284
+
285
+ .dsh-pfc-discard,
286
+ .dsh-pfc-save {
287
+ appearance: none;
288
+ font: inherit;
289
+ font-size: 13px;
290
+ font-weight: 500;
291
+ line-height: 20px;
292
+ padding: 5px 14px;
293
+ border: 1px solid transparent;
294
+ border-radius: 8px;
295
+ cursor: pointer;
296
+ color: var(--dsw-alias-label-primary, inherit);
297
+ background: var(--dsw-alias-bg-module-platform, rgba(128, 128, 128, 0.12));
298
+ transition: background 0.16s, opacity 0.16s;
299
+ }
300
+
301
+ .dsh-pfc-save {
302
+ background: var(--dsw-alias-brand-primary, #0a84ff);
303
+ color: var(--dsw-alias-bg-layer-1, #fff);
304
+ }
305
+
306
+ .dsh-pfc-discard:disabled,
307
+ .dsh-pfc-save:disabled {
308
+ cursor: not-allowed;
309
+ opacity: 0.5;
310
+ }
311
+ `;
312
+ /**
313
+ * Install the card stylesheet as one tagged `<style>` element.
314
+ * @returns the disposer removing the tag (idempotent outside a document).
315
+ */
316
+ export function installPrefaceCardStyles() {
317
+ if (typeof document === 'undefined')
318
+ return () => { };
319
+ if (document.querySelector(`style[data-plugin-css='${STYLE_TAG_ID}']`) !== null)
320
+ return () => { };
321
+ const style = document.createElement('style');
322
+ style.dataset.plugin = '@huanlin/dsh-plugin-preface-context';
323
+ style.dataset.pluginCss = STYLE_TAG_ID;
324
+ style.textContent = CSS;
325
+ document.head.appendChild(style);
326
+ return () => { style.remove(); };
327
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The preface-context settings card: a collapsible card on the "插件配置"
3
+ * settings page. Renders an enable toggle and a multiline textarea for the
4
+ * context text. Stages edits locally and writes them on save through the
5
+ * bound {@link PrefaceCardController}.
6
+ *
7
+ * The chrome mirrors the upstream `PluginCard` shape (a header button
8
+ * stacking name over description, a dirty pill, a rotating chevron; a body
9
+ * with the form controls and a footer with Discard/Save) because the
10
+ * upstream client value face exports no reusable card. Styles come from the
11
+ * statically injected `preface-card.css` stylesheet (the loader never serves
12
+ * `.css` artifacts); every color resolves through a `--dsw-alias-*` token so
13
+ * the card adapts to the light/dark theme.
14
+ */
15
+ import { type ReactNode } from 'react';
16
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
17
+ import type { PrefaceCardFace, PrefaceCardActions } from './preface-card-controller.ts';
18
+ /** Injected dependencies of the card (slot `inject`). */
19
+ export type PrefaceCardInjected = PrefaceCardFace & PrefaceCardActions;
20
+ /** Props the renderer binds for the card. */
21
+ export type PrefaceCardProps = PropsRuntime<'settings.plugin.item'> & PropsLocale<'settings.preface-context'> & InjectFace<PrefaceCardInjected>;
22
+ /**
23
+ * Render the preface-context card.
24
+ * @param props - locale copy, the card snapshot, and its form actions.
25
+ * @returns the card, or nothing when the namespace is unavailable.
26
+ */
27
+ export declare function PrefaceCard(props: PrefaceCardProps): ReactNode;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The preface-context settings card: a collapsible card on the "插件配置"
4
+ * settings page. Renders an enable toggle and a multiline textarea for the
5
+ * context text. Stages edits locally and writes them on save through the
6
+ * bound {@link PrefaceCardController}.
7
+ *
8
+ * The chrome mirrors the upstream `PluginCard` shape (a header button
9
+ * stacking name over description, a dirty pill, a rotating chevron; a body
10
+ * with the form controls and a footer with Discard/Save) because the
11
+ * upstream client value face exports no reusable card. Styles come from the
12
+ * statically injected `preface-card.css` stylesheet (the loader never serves
13
+ * `.css` artifacts); every color resolves through a `--dsw-alias-*` token so
14
+ * the card adapts to the light/dark theme.
15
+ */
16
+ import { useState } from 'react';
17
+ import clsx from 'clsx';
18
+ import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives';
19
+ import { css } from "./preface-card.css.js";
20
+ /**
21
+ * Render the preface-context card.
22
+ * @param props - locale copy, the card snapshot, and its form actions.
23
+ * @returns the card, or nothing when the namespace is unavailable.
24
+ */
25
+ export function PrefaceCard(props) {
26
+ const [open, setOpen] = useState(false);
27
+ const { t } = props;
28
+ const state = props.usePrefaceCard(snapshot => snapshot);
29
+ if (!state.available)
30
+ return null;
31
+ const title = t('title');
32
+ const blocked = !state.dirty || state.saving;
33
+ const enabledOn = state.enabled.text === 'true';
34
+ return (_jsxs("li", { className: clsx(css.card, open && css.cardOpen), children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${t(open ? 'collapse' : 'expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [_jsxs("span", { className: css.headText, children: [_jsx("span", { className: css.name, children: title }), _jsx("span", { className: css.description, children: t('description') })] }), state.dirty ? _jsx("span", { className: css.pending, children: t('unsaved') }) : null, _jsx(IconChevronDownOutline14, { className: clsx(css.chevron, open && css.chevronOpen) })] }), open
35
+ ? (_jsxs("div", { className: css.body, children: [!state.writable ? _jsx("p", { className: css.readOnly, role: "status", children: t('readOnly') }) : null, _jsxs("div", { className: css.field, children: [_jsxs("div", { className: css.head, children: [_jsx("label", { className: css.label, children: t('enabled') }), state.enabled.overridden
36
+ ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: t('overridden') }), _jsx("button", { type: "button", className: css.reset, disabled: !state.writable, onClick: () => { props.resetField('enabled'); }, children: t('reset') })] }))
37
+ : null] }), _jsx("div", { className: css.toggleRow, children: _jsx("button", { type: "button", role: "switch", "aria-checked": enabledOn, className: clsx(css.toggle, enabledOn && css.toggleOn), disabled: !state.writable, onClick: () => { props.edit('enabled', enabledOn ? 'false' : 'true'); }, children: _jsx("span", { className: clsx(css.knob, enabledOn && css.knobOn) }) }) }), _jsx("p", { className: css.hint, children: t('enabledHint') })] }), _jsxs("div", { className: css.field, children: [_jsxs("div", { className: css.head, children: [_jsx("label", { className: css.label, children: t('contextText') }), state.contextText.overridden
38
+ ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: t('overridden') }), _jsx("button", { type: "button", className: css.reset, disabled: !state.writable, onClick: () => { props.resetField('contextText'); }, children: t('reset') })] }))
39
+ : null] }), _jsx("textarea", { id: "plugin-config-preface-context-text", className: css.textarea, value: state.contextText.text, placeholder: t('contextTextPlaceholder'), disabled: !state.writable, onChange: (event) => { props.edit('contextText', event.target.value); } }), _jsx("p", { className: css.hint, children: t('contextTextHint') })] }), _jsxs("div", { className: css.footer, children: [state.failed ? _jsx("p", { className: css.failed, role: "status", children: t('saveFailed') }) : null, _jsx("button", { type: "button", className: css.discard, disabled: !state.dirty || state.saving, onClick: props.discard, children: t('discard') }), _jsx("button", { type: "button", className: css.save, disabled: blocked, onClick: props.save, children: t(state.saving ? 'saving' : 'save') })] })] }))
40
+ : null] }));
41
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Config schema for the preface-context plugin (Schemastery, strict).
3
+ *
4
+ * The plugin-row config (the `entry` passed to `apply`) is the composition
5
+ * BASE of the `preface-context` settings namespace. The settings user layer
6
+ * is layered on top (schema defaults → base → user layer) when a settings
7
+ * service is mounted; without one, the entry is the sole source.
8
+ *
9
+ * @module @huanlin/dsh-plugin-preface-context/config
10
+ */
11
+ import z from 'schemastery';
12
+ /** User-editable configuration for the preface-context plugin. */
13
+ export interface PrefaceConfig {
14
+ /** Master switch: when false, no context is injected at session start. */
15
+ enabled: boolean;
16
+ /** The text block injected as model-visible instructions context. */
17
+ contextText: string;
18
+ }
19
+ /**
20
+ * Schemastery schema for the `preface-context` settings namespace.
21
+ *
22
+ * Strict by construction: unknown keys fail validation here, even though the
23
+ * settings service itself is non-strict and would otherwise accept them.
24
+ */
25
+ export declare const Config: z<Schemastery.ObjectS<{
26
+ enabled: z<boolean, boolean>;
27
+ contextText: z<string, string>;
28
+ }>, Schemastery.ObjectT<{
29
+ enabled: z<boolean, boolean>;
30
+ contextText: z<string, string>;
31
+ }>>;
32
+ /**
33
+ * Resolve a raw config patch through the schema, returning a full
34
+ * {@link PrefaceConfig} with defaults applied. Unknown keys are rejected
35
+ * here (the settings service itself is non-strict and would otherwise accept
36
+ * them), matching the Loader's strict validation.
37
+ * @param input - a partial or complete config object.
38
+ * @returns the schema-resolved config.
39
+ * @throws when the input carries an unknown key or a value the schema rejects.
40
+ */
41
+ export declare function resolvePrefaceConfig(input: Partial<PrefaceConfig>): PrefaceConfig;