@klarkxy/dsh-plugin-kit 0.3.4 → 0.5.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.
@@ -0,0 +1,161 @@
1
+ import { MODEL_SETTINGS_CHANGED_EVENT } from "./model-settings.js";
2
+ import { parseModelSettingsChangeFrame } from "./model-settings-client.js";
3
+ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
4
+ //#region src/model-settings-changes.ts
5
+ var __runInitializers = function(thisArg, initializers, value) {
6
+ var useValue = arguments.length > 2;
7
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
8
+ return useValue ? value : void 0;
9
+ };
10
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
11
+ function accept(f) {
12
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
13
+ return f;
14
+ }
15
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
16
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
17
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
18
+ var _, done = false;
19
+ for (var i = decorators.length - 1; i >= 0; i--) {
20
+ var context = {};
21
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
22
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
23
+ context.addInitializer = function(f) {
24
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
25
+ extraInitializers.push(accept(f || null));
26
+ };
27
+ var result = (0, decorators[i])(kind === "accessor" ? {
28
+ get: descriptor.get,
29
+ set: descriptor.set
30
+ } : descriptor[key], context);
31
+ if (kind === "accessor") {
32
+ if (result === void 0) continue;
33
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
34
+ if (_ = accept(result.get)) descriptor.get = _;
35
+ if (_ = accept(result.set)) descriptor.set = _;
36
+ if (_ = accept(result.init)) initializers.unshift(_);
37
+ } else if (_ = accept(result)) {
38
+ if (kind === "field") initializers.unshift(_);
39
+ else descriptor[key] = _;
40
+ }
41
+ }
42
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
43
+ done = true;
44
+ };
45
+ /** Attach first, then yield ready: commits during a consumer's baseline read
46
+ * remain queued. Coalescing revisions never invents a settings snapshot.
47
+ * `ownerNs` keeps one plugin's stream to its own settings; omit it to forward every namespace. */
48
+ async function* watchModelSettingsChanges(events, signal, ownerNs) {
49
+ signal.throwIfAborted();
50
+ const pending = /* @__PURE__ */ new Map();
51
+ let full = false;
52
+ let wake;
53
+ const changed = (ns, revision) => {
54
+ if (ownerNs !== void 0 && ns !== ownerNs) return;
55
+ let frame;
56
+ try {
57
+ frame = parseModelSettingsChangeFrame({
58
+ kind: "changed",
59
+ ns,
60
+ revision
61
+ });
62
+ } catch {
63
+ return;
64
+ }
65
+ if (!full) {
66
+ if (!pending.has(ns) && pending.size >= 256) {
67
+ pending.clear();
68
+ full = true;
69
+ } else pending.set(ns, frame);
70
+ }
71
+ wake?.();
72
+ };
73
+ const disposers = [];
74
+ const cleanup = () => {
75
+ for (const off of disposers.splice(0).reverse()) off();
76
+ pending.clear();
77
+ };
78
+ const aborted = () => {
79
+ cleanup();
80
+ wake?.();
81
+ };
82
+ signal.addEventListener("abort", aborted, { once: true });
83
+ try {
84
+ disposers.push(events.on(MODEL_SETTINGS_CHANGED_EVENT, ({ ns, revision }) => changed(ns, revision)));
85
+ disposers.push(events.on("settings/document-updated", changed));
86
+ signal.throwIfAborted();
87
+ yield { kind: "ready" };
88
+ while (true) {
89
+ signal.throwIfAborted();
90
+ if (full) {
91
+ full = false;
92
+ yield { kind: "ready" };
93
+ continue;
94
+ }
95
+ if (pending.size) {
96
+ const [ns, frame] = pending.entries().next().value;
97
+ pending.delete(ns);
98
+ yield frame;
99
+ continue;
100
+ }
101
+ await new Promise((resolve) => {
102
+ wake = resolve;
103
+ if (signal.aborted || full || pending.size) resolve();
104
+ });
105
+ wake = void 0;
106
+ }
107
+ } finally {
108
+ signal.removeEventListener("abort", aborted);
109
+ cleanup();
110
+ }
111
+ }
112
+ /** One owner's invalidation stream, registered on the calling plugin fiber.
113
+ * Nothing here is stored in a module-level singleton. */
114
+ let ModelSettingsChangesController = (() => {
115
+ let _classSuper = TypertRemoteService;
116
+ let _instanceExtraInitializers = [];
117
+ let _watch_decorators;
118
+ return class ModelSettingsChangesController extends _classSuper {
119
+ static {
120
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
121
+ _watch_decorators = [Remote({ mode: "stream" })];
122
+ __esDecorate(this, null, _watch_decorators, {
123
+ kind: "method",
124
+ name: "watch",
125
+ static: false,
126
+ private: false,
127
+ access: {
128
+ has: (obj) => "watch" in obj,
129
+ get: (obj) => obj.watch
130
+ },
131
+ metadata: _metadata
132
+ }, null, _instanceExtraInitializers);
133
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
134
+ enumerable: true,
135
+ configurable: true,
136
+ writable: true,
137
+ value: _metadata
138
+ });
139
+ }
140
+ lifetime = (__runInitializers(this, _instanceExtraInitializers), new AbortController());
141
+ ownerNs;
142
+ constructor(ctx, identity, ownerNs) {
143
+ super(ctx, identity.service, { namespace: identity.namespace });
144
+ this.ownerNs = ownerNs;
145
+ ctx.effect(() => () => {
146
+ this.lifetime.abort(/* @__PURE__ */ new Error("Model settings sync owner unloaded"));
147
+ });
148
+ }
149
+ watch(signal) {
150
+ return watchModelSettingsChanges(this.ctx, AbortSignal.any([this.lifetime.signal, signal]), this.ownerNs);
151
+ }
152
+ };
153
+ })();
154
+ /** Register this owner's controller. `ownerNs` limits the stream to that settings namespace. */
155
+ function createModelSettingsChanges(ctx, identity, ownerNs) {
156
+ return new ModelSettingsChangesController(ctx, identity, ownerNs);
157
+ }
158
+ //#endregion
159
+ export { ModelSettingsChangesController, createModelSettingsChanges, watchModelSettingsChanges };
160
+
161
+ //# sourceMappingURL=model-settings-changes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-settings-changes.js","names":[],"sources":["../src/model-settings-changes.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis'\nimport { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'\nimport type { ModelSettingsChangesIdentity } from './contracts.ts'\nimport { MODEL_SETTINGS_CHANGED_EVENT, type ModelSettingsChange } from './model-settings.ts'\nimport { parseModelSettingsChangeFrame, type ModelSettingsChangeFrame } from './model-settings-client.ts'\n\ninterface ChangeEvents {\n on(name: typeof MODEL_SETTINGS_CHANGED_EVENT, callback: (change: ModelSettingsChange) => void): () => void\n on(name: 'settings/document-updated', callback: (ns: string, revision: number) => void): () => void\n}\n\n/** Attach first, then yield ready: commits during a consumer's baseline read\n * remain queued. Coalescing revisions never invents a settings snapshot.\n * `ownerNs` keeps one plugin's stream to its own settings; omit it to forward every namespace. */\nexport async function* watchModelSettingsChanges(events: ChangeEvents, signal: AbortSignal, ownerNs?: string): AsyncGenerator<ModelSettingsChangeFrame> {\n signal.throwIfAborted()\n const pending = new Map<string, ModelSettingsChangeFrame>()\n let full = false\n let wake: (() => void) | undefined\n const changed = (ns: string, revision: number) => {\n if (ownerNs !== undefined && ns !== ownerNs) return\n let frame: ModelSettingsChangeFrame\n try { frame = parseModelSettingsChangeFrame({ kind: 'changed', ns, revision }) } catch { return }\n if (!full) {\n if (!pending.has(ns) && pending.size >= 256) { pending.clear(); full = true }\n else pending.set(ns, frame)\n }\n wake?.()\n }\n const disposers: (() => void)[] = []\n const cleanup = () => {\n for (const off of disposers.splice(0).reverse()) off()\n pending.clear()\n }\n // Release listeners even if the consumer is paused at a yielded frame.\n const aborted = () => { cleanup(); wake?.() }\n signal.addEventListener('abort', aborted, { once: true })\n try {\n disposers.push(events.on(MODEL_SETTINGS_CHANGED_EVENT, ({ ns, revision }) => changed(ns, revision)))\n disposers.push(events.on('settings/document-updated', changed))\n signal.throwIfAborted()\n yield { kind: 'ready' }\n while (true) {\n signal.throwIfAborted()\n if (full) { full = false; yield { kind: 'ready' }; continue }\n if (pending.size) {\n const [ns, frame] = pending.entries().next().value!\n pending.delete(ns)\n yield frame\n continue\n }\n await new Promise<void>(resolve => {\n wake = resolve\n if (signal.aborted || full || pending.size) resolve()\n })\n wake = undefined\n }\n } finally {\n signal.removeEventListener('abort', aborted)\n cleanup()\n }\n}\n\n/** One owner's invalidation stream, registered on the calling plugin fiber.\n * Nothing here is stored in a module-level singleton. */\nexport class ModelSettingsChangesController extends TypertRemoteService {\n private readonly lifetime = new AbortController()\n private readonly ownerNs: string | undefined\n constructor(ctx: Context, identity: ModelSettingsChangesIdentity, ownerNs?: string) {\n super(ctx, identity.service, { namespace: identity.namespace })\n this.ownerNs = ownerNs\n ctx.effect(() => () => { this.lifetime.abort(new Error('Model settings sync owner unloaded')) })\n }\n\n @Remote({ mode: 'stream' })\n watch(signal: AbortSignal): AsyncIterable<ModelSettingsChangeFrame> {\n return watchModelSettingsChanges(this.ctx as unknown as ChangeEvents, AbortSignal.any([this.lifetime.signal, signal]), this.ownerNs)\n }\n}\n\n/** Register this owner's controller. `ownerNs` limits the stream to that settings namespace. */\nexport function createModelSettingsChanges(ctx: Context, identity: ModelSettingsChangesIdentity, ownerNs?: string): ModelSettingsChangesController {\n return new ModelSettingsChangesController(ctx, identity, ownerNs)\n}\n"],"mappings":";;;;AAAA,IAAI,oBAAwD,SAAU,SAAS,cAAc,OAAO;CAChG,IAAI,WAAW,UAAU,SAAS;CAClC,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,QAAQ,KACrC,QAAQ,WAAW,aAAa,EAAE,CAAC,KAAK,SAAS,KAAK,IAAI,aAAa,EAAE,CAAC,KAAK,OAAO;CAE1F,OAAO,WAAW,QAAQ,KAAK;AACnC;AACA,IAAI,eAA8C,SAAU,MAAM,cAAc,YAAY,WAAW,cAAc,mBAAmB;CACpI,SAAS,OAAO,GAAG;EAAE,IAAI,MAAM,KAAK,KAAK,OAAO,MAAM,YAAY,MAAM,IAAI,UAAU,mBAAmB;EAAG,OAAO;CAAG;CACtH,IAAI,OAAO,UAAU,MAAM,MAAM,SAAS,WAAW,QAAQ,SAAS,WAAW,QAAQ;CACzF,IAAI,SAAS,CAAC,gBAAgB,OAAO,UAAU,YAAY,OAAO,KAAK,YAAY;CACnF,IAAI,aAAa,iBAAiB,SAAS,OAAO,yBAAyB,QAAQ,UAAU,IAAI,IAAI,CAAC;CACtG,IAAI,GAAG,OAAO;CACd,KAAK,IAAI,IAAI,WAAW,SAAS,GAAG,KAAK,GAAG,KAAK;EAC7C,IAAI,UAAU,CAAC;EACf,KAAK,IAAI,KAAK,WAAW,QAAQ,KAAK,MAAM,WAAW,CAAC,IAAI,UAAU;EACtE,KAAK,IAAI,KAAK,UAAU,QAAQ,QAAQ,OAAO,KAAK,UAAU,OAAO;EACrE,QAAQ,iBAAiB,SAAU,GAAG;GAAE,IAAI,MAAM,MAAM,IAAI,UAAU,wDAAwD;GAAG,kBAAkB,KAAK,OAAO,KAAK,IAAI,CAAC;EAAG;EAC5K,IAAI,UAAU,GAAG,WAAW,GAAA,CAAI,SAAS,aAAa;GAAE,KAAK,WAAW;GAAK,KAAK,WAAW;EAAI,IAAI,WAAW,MAAM,OAAO;EAC7H,IAAI,SAAS,YAAY;GACrB,IAAI,WAAW,KAAK,GAAG;GACvB,IAAI,WAAW,QAAQ,OAAO,WAAW,UAAU,MAAM,IAAI,UAAU,iBAAiB;GACxF,IAAI,IAAI,OAAO,OAAO,GAAG,GAAG,WAAW,MAAM;GAC7C,IAAI,IAAI,OAAO,OAAO,GAAG,GAAG,WAAW,MAAM;GAC7C,IAAI,IAAI,OAAO,OAAO,IAAI,GAAG,aAAa,QAAQ,CAAC;EACvD,OACK,IAAI,IAAI,OAAO,MAAM,GAAG;GACzB,IAAI,SAAS,SAAS,aAAa,QAAQ,CAAC;QACvC,WAAW,OAAO;EAC3B;CACJ;CACA,IAAI,QAAQ,OAAO,eAAe,QAAQ,UAAU,MAAM,UAAU;CACpE,OAAO;AACX;;;;AAOA,gBAAuB,0BAA0B,QAAQ,QAAQ,SAAS;CACtE,OAAO,eAAe;CACtB,MAAM,0BAAU,IAAI,IAAI;CACxB,IAAI,OAAO;CACX,IAAI;CACJ,MAAM,WAAW,IAAI,aAAa;EAC9B,IAAI,YAAY,KAAA,KAAa,OAAO,SAChC;EACJ,IAAI;EACJ,IAAI;GACA,QAAQ,8BAA8B;IAAE,MAAM;IAAW;IAAI;GAAS,CAAC;EAC3E,QACM;GACF;EACJ;EACA,IAAI,CAAC,MAAM;GACP,IAAI,CAAC,QAAQ,IAAI,EAAE,KAAK,QAAQ,QAAQ,KAAK;IACzC,QAAQ,MAAM;IACd,OAAO;GACX,OAEI,QAAQ,IAAI,IAAI,KAAK;EAC7B;EACA,OAAO;CACX;CACA,MAAM,YAAY,CAAC;CACnB,MAAM,gBAAgB;EAClB,KAAK,MAAM,OAAO,UAAU,OAAO,CAAC,CAAC,CAAC,QAAQ,GAC1C,IAAI;EACR,QAAQ,MAAM;CAClB;CAEA,MAAM,gBAAgB;EAAE,QAAQ;EAAG,OAAO;CAAG;CAC7C,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CACxD,IAAI;EACA,UAAU,KAAK,OAAO,GAAG,+BAA+B,EAAE,IAAI,eAAe,QAAQ,IAAI,QAAQ,CAAC,CAAC;EACnG,UAAU,KAAK,OAAO,GAAG,6BAA6B,OAAO,CAAC;EAC9D,OAAO,eAAe;EACtB,MAAM,EAAE,MAAM,QAAQ;EACtB,OAAO,MAAM;GACT,OAAO,eAAe;GACtB,IAAI,MAAM;IACN,OAAO;IACP,MAAM,EAAE,MAAM,QAAQ;IACtB;GACJ;GACA,IAAI,QAAQ,MAAM;IACd,MAAM,CAAC,IAAI,SAAS,QAAQ,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC;IAC7C,QAAQ,OAAO,EAAE;IACjB,MAAM;IACN;GACJ;GACA,MAAM,IAAI,SAAQ,YAAW;IACzB,OAAO;IACP,IAAI,OAAO,WAAW,QAAQ,QAAQ,MAClC,QAAQ;GAChB,CAAC;GACD,OAAO,KAAA;EACX;CACJ,UACQ;EACJ,OAAO,oBAAoB,SAAS,OAAO;EAC3C,QAAQ;CACZ;AACJ;;;AAGA,IAAI,wCAAwC;CACxC,IAAI,cAAc;CAClB,IAAI,6BAA6B,CAAC;CAClC,IAAI;CACJ,OAAO,MAAM,uCAAuC,YAAY;EAC5D;GACI,MAAM,YAAY,OAAO,WAAW,cAAc,OAAO,WAAW,OAAO,OAAO,YAAY,OAAO,aAAa,IAAI,IAAI,KAAK;GAC/H,oBAAoB,CAAC,OAAO,EAAE,MAAM,SAAS,CAAC,CAAC;GAC/C,aAAa,MAAM,MAAM,mBAAmB;IAAE,MAAM;IAAU,MAAM;IAAS,QAAQ;IAAO,SAAS;IAAO,QAAQ;KAAE,MAAK,QAAO,WAAW;KAAK,MAAK,QAAO,IAAI;IAAM;IAAG,UAAU;GAAU,GAAG,MAAM,0BAA0B;GAClO,IAAI,WAAW,OAAO,eAAe,MAAM,OAAO,UAAU;IAAE,YAAY;IAAM,cAAc;IAAM,UAAU;IAAM,OAAO;GAAU,CAAC;EAC1I;EACA,YAAY,kBAAkB,MAAM,0BAA0B,GAAG,IAAI,gBAAgB;EACrF;EACA,YAAY,KAAK,UAAU,SAAS;GAChC,MAAM,KAAK,SAAS,SAAS,EAAE,WAAW,SAAS,UAAU,CAAC;GAC9D,KAAK,UAAU;GACf,IAAI,mBAAmB;IAAE,KAAK,SAAS,sBAAM,IAAI,MAAM,oCAAoC,CAAC;GAAG,CAAC;EACpG;EACA,MAAM,QAAQ;GACV,OAAO,0BAA0B,KAAK,KAAK,YAAY,IAAI,CAAC,KAAK,SAAS,QAAQ,MAAM,CAAC,GAAG,KAAK,OAAO;EAC5G;CACJ;AACJ,EAAA,CAAG;;AAGH,SAAgB,2BAA2B,KAAK,UAAU,SAAS;CAC/D,OAAO,IAAI,+BAA+B,KAAK,UAAU,OAAO;AACpE"}
@@ -1,3 +1,4 @@
1
+ import { ModelSettingsChangesIdentity } from "./contracts.js";
1
2
  import { TypertRemoteContribution } from "@deepseek-ai/dsh-typert-protocol";
2
3
  //#region src/model-settings-client.d.ts
3
4
  type ModelSettingsChangeFrame = {
@@ -9,8 +10,14 @@ type ModelSettingsChangeFrame = {
9
10
  };
10
11
  /** This stream carries invalidations only; ready is never a settings snapshot. */
11
12
  declare function parseModelSettingsChangeFrame(value: unknown): ModelSettingsChangeFrame;
12
- /** One contribution mounted by Model Hub; consumers only inject its service. */
13
- declare const modelSettingsChangesRemote: TypertRemoteContribution;
13
+ /** One owner's contribution. Each package mounts only its own endpoint. */
14
+ declare function modelSettingsChangesRemote(packageName: string, identity?: ModelSettingsChangesIdentity): TypertRemoteContribution;
15
+ interface RemoteMountContext {
16
+ inject(dependencies: string[], callback: (scope: unknown) => void): unknown;
17
+ }
18
+ /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
19
+ * then withdraws it, so the contribution cannot outlive the plugin. */
20
+ declare function mountModelSettingsChanges(ctx: RemoteMountContext, packageName: string): void;
14
21
  interface StreamItem {
15
22
  generation: number;
16
23
  value: unknown;
@@ -20,17 +27,17 @@ interface StreamItem {
20
27
  interface ChangeStream extends AsyncIterable<StreamItem> {
21
28
  dispose(): Promise<void>;
22
29
  }
30
+ type OpenChangeStream = (options: {
31
+ name: string;
32
+ open(signal: AbortSignal): AsyncIterable<unknown>;
33
+ ended(accepted: boolean): Error;
34
+ }) => ChangeStream;
23
35
  interface ModelSettingsSubscriberScope {
24
36
  remote: {
25
- modelSettingsChanges: {
26
- watch(signal: AbortSignal): AsyncIterable<unknown>;
27
- };
28
- $stream(options: {
29
- name: string;
30
- open(signal: AbortSignal): AsyncIterable<unknown>;
31
- ended(accepted: boolean): Error;
32
- }): ChangeStream;
33
- };
37
+ $stream: OpenChangeStream;
38
+ } & Record<string, {
39
+ watch?(signal: AbortSignal): AsyncIterable<unknown>;
40
+ } | OpenChangeStream | undefined>;
34
41
  effect(callback: () => () => void | Promise<void>): unknown;
35
42
  }
36
43
  interface ModelSettingsSubscriberContext {
@@ -41,7 +48,7 @@ interface ModelSettingsSubscriber {
41
48
  }
42
49
  /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
43
50
  * A factory shares one logical stream while at least one subscriber remains. */
44
- declare function createModelSettingsSubscriber(ctx: ModelSettingsSubscriberContext, namespace: string): ModelSettingsSubscriber;
51
+ declare function createModelSettingsSubscriber(ctx: ModelSettingsSubscriberContext, packageName: string, identity?: ModelSettingsChangesIdentity, accept?: (ns: string) => boolean): ModelSettingsSubscriber;
45
52
  //#endregion
46
- export { ModelSettingsChangeFrame, ModelSettingsSubscriber, ModelSettingsSubscriberContext, ModelSettingsSubscriberScope, createModelSettingsSubscriber, modelSettingsChangesRemote, parseModelSettingsChangeFrame };
53
+ export { ModelSettingsChangeFrame, ModelSettingsSubscriber, ModelSettingsSubscriberContext, ModelSettingsSubscriberScope, createModelSettingsSubscriber, modelSettingsChangesRemote, mountModelSettingsChanges, parseModelSettingsChangeFrame };
47
54
  //# sourceMappingURL=model-settings-client.d.ts.map
@@ -1,4 +1,4 @@
1
- import { MODEL_SETTINGS_CHANGES_REMOTE } from "./contracts.js";
1
+ import { modelSettingsChangesIdentity } from "./contracts.js";
2
2
  //#region src/model-settings-client.ts
3
3
  /** This stream carries invalidations only; ready is never a settings snapshot. */
4
4
  function parseModelSettingsChangeFrame(value) {
@@ -12,21 +12,44 @@ function parseModelSettingsChangeFrame(value) {
12
12
  };
13
13
  throw new Error("Invalid model settings change frame");
14
14
  }
15
- /** One contribution mounted by Model Hub; consumers only inject its service. */
16
- const modelSettingsChangesRemote = {
17
- package: MODEL_SETTINGS_CHANGES_REMOTE.package,
18
- descriptors: MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
19
- ...descriptor,
20
- result: {
21
- mode: "strict",
22
- typeSymbol: "ModelSettings.ChangeFrame",
23
- create: () => ({ parse: parseModelSettingsChangeFrame })
24
- }
25
- }))
26
- };
15
+ /** One owner's contribution. Each package mounts only its own endpoint. */
16
+ function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
17
+ return {
18
+ package: packageName,
19
+ descriptors: [{
20
+ id: `${identity.namespace}/watch`,
21
+ service: identity.service,
22
+ namespace: identity.namespace,
23
+ method: "watch",
24
+ mode: "stream",
25
+ invocation: { kind: "direct" },
26
+ parameters: [],
27
+ cancellation: { parameter: "signal" },
28
+ result: {
29
+ mode: "strict",
30
+ typeSymbol: "ModelSettings.ChangeFrame",
31
+ create: () => ({ parse: parseModelSettingsChangeFrame })
32
+ }
33
+ }]
34
+ };
35
+ }
36
+ /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
37
+ * then withdraws it, so the contribution cannot outlive the plugin. */
38
+ function mountModelSettingsChanges(ctx, packageName) {
39
+ const contribution = modelSettingsChangesRemote(packageName);
40
+ ctx.inject(["remote"], (injected) => {
41
+ const scope = injected;
42
+ const mounting = scope.remote.$mount(contribution);
43
+ mounting.catch(() => {});
44
+ scope.effect(() => async () => {
45
+ await (await mounting.catch(() => void 0))?.();
46
+ });
47
+ });
48
+ }
27
49
  /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
28
50
  * A factory shares one logical stream while at least one subscriber remains. */
29
- function createModelSettingsSubscriber(ctx, namespace) {
51
+ function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
52
+ const endpoint = identity;
30
53
  const listeners = /* @__PURE__ */ new Set();
31
54
  let injection;
32
55
  let current;
@@ -52,12 +75,18 @@ function createModelSettingsSubscriber(ctx, namespace) {
52
75
  };
53
76
  const start = () => {
54
77
  const life = ++lifecycle;
55
- injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
78
+ injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
56
79
  if (life !== lifecycle || !listeners.size) return;
57
80
  const scope = injected;
81
+ const service = scope.remote[endpoint.namespace];
82
+ if (!service || !("watch" in service) || !service.watch) {
83
+ report("Model settings sync service unavailable");
84
+ return;
85
+ }
86
+ const watch = service.watch;
58
87
  const stream = scope.remote.$stream({
59
88
  name: "Model settings sync",
60
- open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
89
+ open: (signal) => watch(signal),
61
90
  ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
62
91
  });
63
92
  const session = {
@@ -92,7 +121,7 @@ function createModelSettingsSubscriber(ctx, namespace) {
92
121
  invalidate();
93
122
  } else {
94
123
  if (!ready) throw new Error("Model settings sync change arrived before ready");
95
- if (namespace === "*" || frame.ns === namespace) invalidate();
124
+ if (accept(frame.ns)) invalidate();
96
125
  }
97
126
  }
98
127
  } catch (error) {
@@ -138,6 +167,6 @@ function createModelSettingsSubscriber(ctx, namespace) {
138
167
  } });
139
168
  }
140
169
  //#endregion
141
- export { createModelSettingsSubscriber, modelSettingsChangesRemote, parseModelSettingsChangeFrame };
170
+ export { createModelSettingsSubscriber, modelSettingsChangesRemote, mountModelSettingsChanges, parseModelSettingsChangeFrame };
142
171
 
143
172
  //# sourceMappingURL=model-settings-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-settings-client.js","names":[],"sources":["../src/model-settings-client.ts"],"sourcesContent":["import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol'\nimport { MODEL_SETTINGS_CHANGES_REMOTE } from './contracts.ts'\n\nexport type ModelSettingsChangeFrame = { kind: 'ready' } | { kind: 'changed'; ns: string; revision: number }\n\n/** This stream carries invalidations only; ready is never a settings snapshot. */\nexport function parseModelSettingsChangeFrame(value: unknown): ModelSettingsChangeFrame {\n if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid model settings change frame')\n const frame = value as Record<string, unknown>\n if (frame.kind === 'ready' && Object.keys(frame).length === 1) return { kind: 'ready' }\n if (frame.kind === 'changed' && Object.keys(frame).length === 3 && typeof frame.ns === 'string' && frame.ns.length > 0\n && typeof frame.revision === 'number' && Number.isSafeInteger(frame.revision) && frame.revision >= 0) {\n return { kind: 'changed', ns: frame.ns, revision: frame.revision }\n }\n throw new Error('Invalid model settings change frame')\n}\n\n/** One contribution mounted by Model Hub; consumers only inject its service. */\nexport const modelSettingsChangesRemote: TypertRemoteContribution = {\n package: MODEL_SETTINGS_CHANGES_REMOTE.package,\n descriptors: MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map(descriptor => ({\n ...descriptor,\n result: { mode: 'strict' as const, typeSymbol: 'ModelSettings.ChangeFrame', create: () => ({ parse: parseModelSettingsChangeFrame }) },\n })),\n}\n\ninterface StreamItem {\n generation: number\n value: unknown\n signal: AbortSignal\n accept(): void\n}\ninterface ChangeStream extends AsyncIterable<StreamItem> { dispose(): Promise<void> }\nexport interface ModelSettingsSubscriberScope {\n remote: {\n modelSettingsChanges: { watch(signal: AbortSignal): AsyncIterable<unknown> }\n $stream(options: {\n name: string\n open(signal: AbortSignal): AsyncIterable<unknown>\n ended(accepted: boolean): Error\n }): ChangeStream\n }\n effect(callback: () => () => void | Promise<void>): unknown\n}\nexport interface ModelSettingsSubscriberContext {\n inject(dependencies: string[], callback: (scope: unknown) => void): unknown\n}\nexport interface ModelSettingsSubscriber {\n subscribe(onChange: () => void, onError?: (message: string) => void): () => void\n}\n\n/** Native RemoteStream owns carrier reconnects; no timers or browser transport.\n * A factory shares one logical stream while at least one subscriber remains. */\nexport function createModelSettingsSubscriber(ctx: ModelSettingsSubscriberContext, namespace: string): ModelSettingsSubscriber {\n const listeners = new Set<{ change: () => void; error?: (message: string) => void }>()\n let injection: { dispose(): void | Promise<void> } | undefined\n let current: { stream: ChangeStream; active: boolean } | undefined\n let lifecycle = 0\n let failureMessage: string | undefined\n const report = (message: string) => {\n failureMessage = message\n for (const listener of [...listeners]) {\n try { listener.error?.(message) } catch { /* Isolate page callbacks. */ }\n }\n }\n const invalidate = () => {\n for (const listener of [...listeners]) {\n if (!listeners.has(listener)) continue\n try { listener.change() } catch (error) {\n try { listener.error?.(error instanceof Error ? error.message : String(error)) } catch { /* Isolate callbacks. */ }\n }\n }\n }\n const start = () => {\n const life = ++lifecycle\n injection = ctx.inject(['remote', 'remote.modelSettingsChanges'], injected => {\n if (life !== lifecycle || !listeners.size) return\n const scope = injected as ModelSettingsSubscriberScope\n const stream = scope.remote.$stream({\n name: 'Model settings sync',\n open: signal => scope.remote.modelSettingsChanges.watch(signal),\n ended: accepted => new Error(`Model settings sync stream ended ${accepted ? 'after' : 'before'} ready; refresh or reopen the page`),\n })\n const session = { stream, active: true }\n current = session\n scope.effect(() => () => {\n session.active = false\n if (current === session) {\n current = undefined\n if (life === lifecycle && listeners.size) report('Model settings sync service unavailable')\n }\n return stream.dispose()\n })\n void (async () => {\n let generation: number | undefined\n let ready = false\n try {\n for await (const item of stream) {\n if (!session.active || life !== lifecycle || !listeners.size) break\n if (item.signal.aborted) continue\n if (generation !== item.generation) { generation = item.generation; ready = false }\n const frame = parseModelSettingsChangeFrame(item.value)\n if (frame.kind === 'ready') {\n ready = true\n failureMessage = undefined\n // Accept the validated invalidation baseline, not fetched data.\n // Every current listener queues its own latest settings read.\n item.accept()\n invalidate()\n } else {\n if (!ready) throw new Error('Model settings sync change arrived before ready')\n if (namespace === '*' || frame.ns === namespace) invalidate()\n }\n }\n } catch (error) {\n if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error))\n } finally {\n if (current === session) current = undefined\n session.active = false\n await stream.dispose().catch(() => {})\n }\n })()\n }) as { dispose(): void | Promise<void> }\n queueMicrotask(() => {\n if (life === lifecycle && listeners.size && !current) report('Model settings sync service unavailable')\n })\n }\n return Object.freeze({ subscribe(onChange: () => void, onError?: (message: string) => void) {\n const listener = { change: onChange, error: onError }\n listeners.add(listener)\n if (!injection) start()\n else if (failureMessage) queueMicrotask(() => {\n if (listeners.has(listener) && failureMessage) {\n try { listener.error?.(failureMessage) } catch { /* Isolate callbacks. */ }\n }\n })\n return () => {\n if (!listeners.delete(listener) || listeners.size) return\n lifecycle++\n if (current) { current.active = false; void current.stream.dispose().catch(() => {}) }\n current = undefined\n failureMessage = undefined\n const previous = injection\n injection = undefined\n try { void Promise.resolve(previous?.dispose()).catch(() => {}) } catch { /* Already unloaded. */ }\n }\n } })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"model-settings-client.js","names":[],"sources":["../src/model-settings-client.ts"],"sourcesContent":["import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol'\nimport { modelSettingsChangesIdentity, type ModelSettingsChangesIdentity } from './contracts.ts'\n\nexport type ModelSettingsChangeFrame = { kind: 'ready' } | { kind: 'changed'; ns: string; revision: number }\n\n/** This stream carries invalidations only; ready is never a settings snapshot. */\nexport function parseModelSettingsChangeFrame(value: unknown): ModelSettingsChangeFrame {\n if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid model settings change frame')\n const frame = value as Record<string, unknown>\n if (frame.kind === 'ready' && Object.keys(frame).length === 1) return { kind: 'ready' }\n if (frame.kind === 'changed' && Object.keys(frame).length === 3 && typeof frame.ns === 'string' && frame.ns.length > 0\n && typeof frame.revision === 'number' && Number.isSafeInteger(frame.revision) && frame.revision >= 0) {\n return { kind: 'changed', ns: frame.ns, revision: frame.revision }\n }\n throw new Error('Invalid model settings change frame')\n}\n\n/** One owner's contribution. Each package mounts only its own endpoint. */\nexport function modelSettingsChangesRemote(packageName: string, identity: ModelSettingsChangesIdentity = modelSettingsChangesIdentity(packageName)): TypertRemoteContribution {\n return {\n package: packageName,\n descriptors: [{\n id: `${identity.namespace}/watch`,\n service: identity.service,\n namespace: identity.namespace,\n method: 'watch',\n mode: 'stream' as const,\n invocation: { kind: 'direct' as const },\n parameters: [],\n cancellation: { parameter: 'signal' as const },\n result: { mode: 'strict' as const, typeSymbol: 'ModelSettings.ChangeFrame', create: () => ({ parse: parseModelSettingsChangeFrame }) },\n }],\n }\n}\n\ninterface RemoteMountScope {\n remote: { $mount(contribution: TypertRemoteContribution): Promise<() => Promise<void>> }\n effect(callback: () => () => void | Promise<void>): unknown\n}\ninterface RemoteMountContext {\n inject(dependencies: string[], callback: (scope: unknown) => void): unknown\n}\n\n/** Mount from a synchronous apply. Disposal waits for an in-flight mount and\n * then withdraws it, so the contribution cannot outlive the plugin. */\nexport function mountModelSettingsChanges(ctx: RemoteMountContext, packageName: string): void {\n const contribution = modelSettingsChangesRemote(packageName)\n ctx.inject(['remote'], injected => {\n const scope = injected as RemoteMountScope\n const mounting = scope.remote.$mount(contribution)\n void mounting.catch(() => {})\n scope.effect(() => async () => {\n const unmount = await mounting.catch(() => undefined)\n await unmount?.()\n })\n })\n}\n\ninterface StreamItem {\n generation: number\n value: unknown\n signal: AbortSignal\n accept(): void\n}\ninterface ChangeStream extends AsyncIterable<StreamItem> { dispose(): Promise<void> }\ntype OpenChangeStream = (options: {\n name: string\n open(signal: AbortSignal): AsyncIterable<unknown>\n ended(accepted: boolean): Error\n}) => ChangeStream\nexport interface ModelSettingsSubscriberScope {\n remote: { $stream: OpenChangeStream } & Record<string, { watch?(signal: AbortSignal): AsyncIterable<unknown> } | OpenChangeStream | undefined>\n effect(callback: () => () => void | Promise<void>): unknown\n}\nexport interface ModelSettingsSubscriberContext {\n inject(dependencies: string[], callback: (scope: unknown) => void): unknown\n}\nexport interface ModelSettingsSubscriber {\n subscribe(onChange: () => void, onError?: (message: string) => void): () => void\n}\n\n/** Native RemoteStream owns carrier reconnects; no timers or browser transport.\n * A factory shares one logical stream while at least one subscriber remains. */\nexport function createModelSettingsSubscriber(\n ctx: ModelSettingsSubscriberContext,\n packageName: string,\n identity: ModelSettingsChangesIdentity = modelSettingsChangesIdentity(packageName),\n accept: (ns: string) => boolean = ns => ns === packageName,\n): ModelSettingsSubscriber {\n const endpoint = identity\n const listeners = new Set<{ change: () => void; error?: (message: string) => void }>()\n let injection: { dispose(): void | Promise<void> } | undefined\n let current: { stream: ChangeStream; active: boolean } | undefined\n let lifecycle = 0\n let failureMessage: string | undefined\n const report = (message: string) => {\n failureMessage = message\n for (const listener of [...listeners]) {\n try { listener.error?.(message) } catch { /* Isolate page callbacks. */ }\n }\n }\n const invalidate = () => {\n for (const listener of [...listeners]) {\n if (!listeners.has(listener)) continue\n try { listener.change() } catch (error) {\n try { listener.error?.(error instanceof Error ? error.message : String(error)) } catch { /* Isolate callbacks. */ }\n }\n }\n }\n const start = () => {\n const life = ++lifecycle\n injection = ctx.inject(['remote', `remote.${endpoint.namespace}`], injected => {\n if (life !== lifecycle || !listeners.size) return\n const scope = injected as ModelSettingsSubscriberScope\n const service = scope.remote[endpoint.namespace]\n if (!service || !('watch' in service) || !service.watch) { report('Model settings sync service unavailable'); return }\n const watch = service.watch\n const stream = scope.remote.$stream({\n name: 'Model settings sync',\n open: signal => watch(signal),\n ended: accepted => new Error(`Model settings sync stream ended ${accepted ? 'after' : 'before'} ready; refresh or reopen the page`),\n })\n const session = { stream, active: true }\n current = session\n scope.effect(() => () => {\n session.active = false\n if (current === session) {\n current = undefined\n if (life === lifecycle && listeners.size) report('Model settings sync service unavailable')\n }\n return stream.dispose()\n })\n void (async () => {\n let generation: number | undefined\n let ready = false\n try {\n for await (const item of stream) {\n if (!session.active || life !== lifecycle || !listeners.size) break\n if (item.signal.aborted) continue\n if (generation !== item.generation) { generation = item.generation; ready = false }\n const frame = parseModelSettingsChangeFrame(item.value)\n if (frame.kind === 'ready') {\n ready = true\n failureMessage = undefined\n // Accept the validated invalidation baseline, not fetched data.\n // Every current listener queues its own latest settings read.\n item.accept()\n invalidate()\n } else {\n if (!ready) throw new Error('Model settings sync change arrived before ready')\n if (accept(frame.ns)) invalidate()\n }\n }\n } catch (error) {\n if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error))\n } finally {\n if (current === session) current = undefined\n session.active = false\n await stream.dispose().catch(() => {})\n }\n })()\n }) as { dispose(): void | Promise<void> }\n queueMicrotask(() => {\n if (life === lifecycle && listeners.size && !current) report('Model settings sync service unavailable')\n })\n }\n return Object.freeze({ subscribe(onChange: () => void, onError?: (message: string) => void) {\n const listener = { change: onChange, error: onError }\n listeners.add(listener)\n if (!injection) start()\n else if (failureMessage) queueMicrotask(() => {\n if (listeners.has(listener) && failureMessage) {\n try { listener.error?.(failureMessage) } catch { /* Isolate callbacks. */ }\n }\n })\n return () => {\n if (!listeners.delete(listener) || listeners.size) return\n lifecycle++\n if (current) { current.active = false; void current.stream.dispose().catch(() => {}) }\n current = undefined\n failureMessage = undefined\n const previous = injection\n injection = undefined\n try { void Promise.resolve(previous?.dispose()).catch(() => {}) } catch { /* Already unloaded. */ }\n }\n } })\n}\n"],"mappings":"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"}
@@ -219,6 +219,17 @@ ${scope} {
219
219
  /* --- Layout scaffolding. --- */
220
220
  & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
221
221
  & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
222
+ /* Plugin detail pages share one reading column. These are layout recipes;
223
+ * controls keep their own primitive styles. Form spacing follows the host's
224
+ * settings-form/fields.module.css and SettingsForm.module.css. */
225
+ & .dsh-ui-page { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 760px; min-width: 0; }
226
+ & .dsh-ui-form { display: flex; flex-direction: column; gap: 0; min-width: 0; }
227
+ &.dsh-ui-form > .dsh-ui-field, & .dsh-ui-form > .dsh-ui-field { padding: 12px 0; }
228
+ & .dsh-ui-form-footer { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-top: 16px; min-width: 0; }
229
+ & .dsh-ui-fields-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; min-width: 0; margin: 0; padding: 0; border: 0; }
230
+ @media (max-width: 560px) {
231
+ & .dsh-ui-fields-grid { grid-template-columns: minmax(0, 1fr); }
232
+ }
222
233
  & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
223
234
  & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
224
235
  & .dsh-ui-spacer { flex: 1; min-width: 0; }
@@ -246,6 +257,9 @@ ${scope} {
246
257
  * official primitive, so this only owns the vertical rhythm. --- */
247
258
  & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
248
259
  & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
260
+ /* Grid fields share a row, so the stacked-field divider must not win later in
261
+ * the cascade. Reset it here, after the field recipe. */
262
+ & .dsh-ui-fields-grid > .dsh-ui-field { padding: 0; border: 0; }
249
263
  & .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }
250
264
  & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
251
265
  & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
@@ -436,7 +450,7 @@ ${scope} {
436
450
  & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
437
451
  }
438
452
  }
439
- `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
453
+ `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?=\s*\{)/gm, (_match, className) => `&${className}, & ${className}`);
440
454
  }
441
455
  //#endregion
442
456
  export { OFFICIAL_FOCUS_RING, OFFICIAL_THEME_TOKENS, OFFICIAL_THEME_TOKEN_NAMES, officialElevation, officialFocus, officialStrokedElevation, officialUiCss };
@@ -1 +1 @@
1
- {"version":3,"file":"official-ui.js","names":[],"sources":["../src/official-ui.ts"],"sourcesContent":["/**\n * Shared contract that keeps every plugin's browser-side UI on the official\n * DeepSeek Harness Web design language.\n *\n * Two rules produce that language, and both are load-bearing here:\n *\n * 1. Colour, radius, elevation and focus come from the host theme's real\n * `--dsw-*` tokens, written under their own names. Plugins never invent a\n * token, never alias the official ones behind a private palette (a private\n * alias is just a second, diverging theme), and never carries a literal\n * fallback — the host publishes light and dark, and a literal would pin one\n * of them. The only non-colour custom properties used are the host's own\n * content-font axis and frame clearance.\n *\n * 2. Anything the host already ships is used, not reimplemented. Controls come\n * from `@deepseek-ai/dsh-client-ui-primitives` (`Button`, `Input`,\n * `Checkbox`, `Switch`, `Tag`, `Pill`, `SegmentedControl`, `Menu`, `Modal`,\n * `Tooltip`, `Toast`, `DisclosureRow`, `StateDot`, `PathLabel`, the settings\n * form suite). This module only styles what primitives do not cover: page\n * scaffolding, field layout, banners, empty states and scroll regions.\n *\n * The metrics below are transcribed from the primitives' own CSS modules so a\n * feature panel lines up pixel-for-pixel with a native one: the 14/22 body and\n * 13/20 compact type tiers, the 0.5px hairline scale, and the elevation family\n * that draws a surface's edge in its shadow (`border: 0`) instead of a border.\n */\n\n/**\n * Every `--dsw-*` token this repo is allowed to reference, grouped by role.\n *\n * Plugins may only colour through this list. It is exported so the guard test\n * can fail a build when a stylesheet reaches for anything else — the failure\n * mode this prevents is silent: an unknown token resolves to nothing, so a\n * border disappears or a label inherits the wrong colour with no error anywhere.\n */\nexport const OFFICIAL_THEME_TOKENS = {\n /** Surfaces, from the page up to the topmost nested card. */\n surface: [\n '--dsw-alias-bg-base',\n '--dsw-alias-bg-layer-1',\n '--dsw-alias-bg-layer-2',\n '--dsw-alias-bg-layer-3',\n '--dsw-alias-bg-layer-4',\n '--dsw-alias-bg-module-platform',\n '--dsw-alias-bg-overlay',\n ],\n /** Hairlines. l1 is near-invisible on a menu surface, so menus use l2+. */\n border: ['--dsw-alias-border-l1', '--dsw-alias-border-l2', '--dsw-alias-border-l3', '--dsw-alias-border-l4'],\n /** Text. `caption` is menu shortcut text, `dimmed` is placeholder text. */\n label: [\n '--dsw-alias-label-primary',\n '--dsw-alias-label-secondary',\n '--dsw-alias-label-tertiary',\n '--dsw-alias-label-caption',\n '--dsw-alias-label-dimmed',\n '--dsw-alias-label-error',\n '--dsw-alias-label-primary-foreground',\n ],\n /** Interactive fills. The `-danger` hover belongs to destructive rows. */\n interactive: [\n '--dsw-alias-interactive-bg-hover',\n '--dsw-alias-interactive-bg-active',\n '--dsw-alias-interactive-bg-hover-danger',\n ],\n /** Accent and status. `business` is the focus ring's default colour. */\n accent: [\n '--dsw-alias-brand-primary',\n '--dsw-alias-button-primary-fill',\n '--dsw-alias-button-primary-hover',\n '--dsw-alias-button-tool-bar-fill',\n '--dsw-alias-button-tool-bar-hover',\n '--dsw-alias-button-ghost-active-fill',\n '--dsw-alias-button-ghost-active-border',\n ],\n state: [\n '--dsw-alias-state-business-primary',\n '--dsw-alias-state-success-primary',\n '--dsw-alias-state-warn-primary',\n '--dsw-alias-state-error-primary',\n '--dsw-alias-state-idle-primary',\n ],\n /** Corner scale: sm 8, md 12, lg the large card, panel the modal. */\n radius: ['--dsw-radius-sm', '--dsw-radius-md', '--dsw-radius-lg', '--dsw-radius-panel'],\n /** An elevated surface draws its edge in the shadow, so it keeps border: 0. */\n elevation: [\n '--dsw-elevation-soft',\n '--dsw-elevation-stroke',\n '--dsw-elevation-stroke-color',\n '--dsw-elevation-panel',\n '--dsw-elevation-prominent',\n ],\n /** Floats, masks and scrollbars. */\n overlay: [\n '--dsw-menu-surface-fill',\n '--dsw-menu-backdrop-filter',\n '--dsw-mask-blur',\n '--dsw-alias-bg-mask-1',\n '--dsw-alias-menu-icon',\n '--dsw-alias-scrollbar-bg-l2',\n '--dsw-alias-scrollbar-hover-l2',\n '--dsw-shadow-lv3',\n ],\n /** Motion, focus and type axis, published by the host rather than by a component. */\n system: [\n '--dsw-focus-ring-width',\n '--dsw-focus-ring-color',\n '--dsw-font-markdown-code-font-family',\n '--ds-transition-duration',\n '--ds-ease-in-out',\n '--ds-font-family-code',\n '--dsh-scrollbar-thumb',\n '--dsh-scrollbar-thumb-hover',\n '--dsh-content-font-size-secondary',\n '--dsh-content-font-delta',\n '--dsh-frame-top-clearance',\n ],\n} as const\n\n/** Every official token name, flat. Used by the token guard test. */\nexport const OFFICIAL_THEME_TOKEN_NAMES: readonly string[] = Object.values(OFFICIAL_THEME_TOKENS).flat()\n\n/** The elevation steps a surface can be raised by, softest first. */\nexport type OfficialElevation = 'soft' | 'stroke' | 'panel' | 'prominent'\n\n/**\n * The host's focus ring shorthand, verbatim. Writing this per control is how\n * rings drift apart; a stylesheet that draws its own focus style should use\n * {@link officialFocus} instead. The colour fallback is the host's own default.\n */\nexport const OFFICIAL_FOCUS_RING =\n 'var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))'\n\n/**\n * A complete `outline` declaration at the official geometry. `offset` defaults\n * to the 2px the native controls use; the host's inline field rules sit at 1px\n * and a segmented tab pulls the ring inside itself at -2px.\n */\nexport function officialFocus(offset = '2px'): string {\n return `outline: ${OFFICIAL_FOCUS_RING}; outline-offset: ${offset}`\n}\n\n/**\n * An elevated surface. The host draws a raised surface's edge inside its\n * shadow rather than as a border, so a card sets `border: 0` and takes the edge\n * from the elevation — which is also why its border stays off the alias-border\n * set that a flat, non-elevated rule would use.\n */\nexport function officialElevation(level: OfficialElevation): string {\n return `border: 0; box-shadow: var(--dsw-elevation-${level})`\n}\n\n/**\n * An elevated surface whose edge should read as a specific hairline. Rebinding\n * the stroke colour is how a menu card picks l1 while a panel picks l3, without\n * the two growing apart by hand.\n */\nexport function officialStrokedElevation(level: OfficialElevation, color: string): string {\n return `--dsw-elevation-stroke-color: ${color}; ${officialElevation(level)}`\n}\n\n/**\n * The shared stylesheet for a plugin UI, scoped under that plugin's root class.\n *\n * Scoping is what keeps this safe to always-bundle into every client: the rules\n * can only match inside the plugin's own subtree, so two plugins can both mount\n * `dsh-ui-card` without either styling the other's markup.\n *\n * Each recipe matches the root element *and* its descendants, because the\n * commonest arrangement is one element that is both — a surface that carries\n * the root class and `dsh-ui-card`, or a portaled dialog that is the root and\n * the `dsh-ui-surface`. A descendant-only scope would silently leave those\n * unstyled, with no error anywhere.\n *\n * A caveat worth knowing: `Modal` and `Menu` portal to `document.body`, so their\n * content leaves the plugin's subtree entirely. A portaled surface therefore\n * needs its own root class on an element inside the portal.\n *\n * Only patterns that primitives do not provide live here. If a control appears\n * below that `Button`/`Input`/`Switch`/`Tag`/`Menu` already covers, that is a\n * bug in this file: use the primitive and delete the recipe.\n */\nexport function officialUiCss(roots: string | readonly string[]): string {\n // One plugin often mounts several unrelated surfaces (a settings seat, a chat\n // card, a header menu), so the caller names every root. The rules below are\n // nested under a single selector list and use `&`, which the browser expands\n // per root — so one stylesheet covers all of them without duplicating itself.\n const scope = (typeof roots === 'string' ? [roots] : roots).map(root => `.${root}`).join(', ')\n const sheet = `\n/* Official DSH Web UI contract. Generated by @klarkxy/dsh-plugin-kit/official-ui.\n * Do not hand-edit the copy inside a plugin: change this module so every plugin\n * moves together. Scope: ${scope} */\n/* The scope rule below stays open for the whole sheet: every rule after it\n * nests with the parent selector, so one stylesheet can cover several unrelated\n * roots without leaking into a neighbouring plugin. Its closing brace is last. */\n${scope} {\n color: var(--dsw-alias-label-primary);\n font-family: inherit;\n font-size: 14px;\n line-height: 22px;\n min-width: 0;\n\n& *, & *::before, & *::after { box-sizing: border-box; }\n& h1, & h2, & h3, & h4, & p, & figure { margin: 0; }\n/* Keyboard focus only: pointer input keeps the native resting appearance. The\n * ring needs no radius of its own — the outline already follows the control's\n * own border-radius, and overriding it here would flatten every control. */\n& :focus-visible { ${officialFocus()}; }\n& [hidden] { display: none !important; }\n\n/* --- Type tiers. The same 14/22 and 13/20 steps the primitives use. --- */\n& .dsh-ui-title { font-size: 16px; line-height: 24px; font-weight: 500; color: var(--dsw-alias-label-primary); }\n& .dsh-ui-heading { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }\n& .dsh-ui-compact { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary); }\n& .dsh-ui-meta { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }\n& .dsh-ui-hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }\n& .dsh-ui-error { font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); }\n& .dsh-ui-warn { color: var(--dsw-alias-state-warn-primary); }\n& .dsh-ui-success { color: var(--dsw-alias-state-success-primary); }\n& .dsh-ui-muted { color: var(--dsw-alias-label-tertiary); }\n& .dsh-ui-truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n& .dsh-ui-wrap { min-width: 0; overflow-wrap: anywhere; }\n\n/* --- Layout scaffolding. --- */\n& .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }\n& .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }\n& .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }\n& .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }\n& .dsh-ui-spacer { flex: 1; min-width: 0; }\n/* A 0.5px l2 rule is the host's section divider; reserve the same rhythm. */\n& .dsh-ui-divider { height: 0.5px; background: var(--dsw-alias-border-l2); }\n& .dsh-ui-section { display: flex; flex-direction: column; gap: 12px; padding-top: 16px; border-top: 0.5px solid var(--dsw-alias-border-l2); }\n\n/* --- Card: a raised surface whose edge lives in the shadow. --- */\n& .dsh-ui-card {\n ${officialElevation('stroke')}\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 16px;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n min-width: 0;\n}\n& .dsh-ui-card--flat { border: 0.5px solid var(--dsw-alias-border-l3); box-shadow: none; }\n& .dsh-ui-card--nested { background: var(--dsw-alias-bg-layer-2); }\n& .dsh-ui-card--plain { background: none; }\n\n/* --- Field: label, control, help, error. The control itself is an\n * official primitive, so this only owns the vertical rhythm. --- */\n& .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }\n& .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }\n& .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }\n& .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }\n& .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }\n& .dsh-ui-help p { margin: 0; }\n& .dsh-ui-help p + p { margin-top: 8px; }\n& .dsh-ui-required { color: var(--dsw-alias-state-error-primary); }\n& .dsh-ui-control { width: 100%; min-width: 0; }\n/* An invalid control is marked on the control, not only in the message. */\n& .dsh-ui-control[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }\n& .dsh-ui-readonly { display: flex; flex-direction: column; gap: 4px; min-width: 0; }\n& .dsh-ui-readonly > dt { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }\n& .dsh-ui-readonly > dd { margin: 0; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }\n\n/* --- Buttons row. A no-op container: it exists so actions line up on the\n * trailing edge the way every native panel does. --- */\n& .dsh-ui-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 4px; }\n& .dsh-ui-actions-end { justify-content: flex-end; }\n\n/* --- Banners. A banner is a left-rail notice, the shape used for warnings,\n * deprecations and blocking instructions. --- */\n& .dsh-ui-banner {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px 14px;\n border: 0.5px solid var(--dsw-alias-border-l3);\n border-left: 3px solid var(--dsw-alias-state-warn-primary);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-1);\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n& .dsh-ui-banner-title { font-weight: 500; color: var(--dsw-alias-state-warn-primary); }\n& .dsh-ui-banner--info { border-left-color: var(--dsw-alias-state-business-primary); }\n& .dsh-ui-banner--danger { border-left-color: var(--dsw-alias-state-error-primary); }\n& .dsh-ui-banner--success { border-left-color: var(--dsw-alias-state-success-primary); }\n& .dsh-ui-banner p { margin: 0; }\n\n/* --- Inline notice: a transient confirmation with no chrome of its own. --- */\n& .dsh-ui-notice { display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-success-primary); }\n& .dsh-ui-notice--error { color: var(--dsw-alias-state-error-primary); }\n\n/* --- Empty and loading states. Both sit in a dashed well so an empty panel\n * reads as \"nothing here yet\" rather than as a broken layout. --- */\n& .dsh-ui-empty {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 6px;\n padding: 20px 16px;\n border: 0.5px dashed var(--dsw-alias-border-l3);\n border-radius: var(--dsw-radius-md);\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n& .dsh-ui-loading { display: flex; align-items: center; gap: 8px; padding: 20px 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }\n\n/* --- Selectable list. A row is a button-sized target with a hover fill and\n * a selected fill; the name carries the weight, the description does not. --- */\n& .dsh-ui-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; min-width: 0; }\n& .dsh-ui-list-scroll { max-height: min(66vh, 760px); overflow-y: auto; overflow-x: hidden; }\n& .dsh-ui-list-row { border-radius: var(--dsw-radius-md); }\n& .dsh-ui-list-row:hover { background: var(--dsw-alias-interactive-bg-hover); }\n& .dsh-ui-list-row[aria-selected='true'], & .dsh-ui-list-row[data-selected] { background: var(--dsw-alias-interactive-bg-active); }\n& .dsh-ui-list-item {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 4px;\n width: 100%;\n min-width: 0;\n padding: 10px 12px;\n border: 0;\n border-radius: var(--dsw-radius-md);\n background: none;\n font: inherit;\n color: inherit;\n text-align: start;\n cursor: pointer;\n}\n& .dsh-ui-list-item:disabled { cursor: default; }\n& .dsh-ui-list-item:disabled:hover { background: none; }\n& .dsh-ui-list-name { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }\n& .dsh-ui-list-desc { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }\n\n/* --- Code and structured output. Wraps rather than scrolls, because a\n * feature panel is read rather than scanned. --- */\n& .dsh-ui-code {\n padding: 10px 12px;\n border: 0.5px solid var(--dsw-alias-border-l3);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-1);\n font-family: var(--ds-font-family-code);\n font-size: 12px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n overflow-wrap: anywhere;\n white-space: pre-wrap;\n}\n& .dsh-ui-scroll { max-height: 60vh; overflow: auto; overscroll-behavior: contain; }\n\n/* --- A toggle row: label on the left, switch on the trailing edge. This is\n * layout only; the switch itself is the official primitive. --- */\n& .dsh-ui-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-width: 0; padding: 12px 0; }\n& .dsh-ui-toggle-row + .dsh-ui-toggle-row { border-top: 0.5px solid var(--dsw-alias-border-l2); }\n& .dsh-ui-toggle-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }\n& .dsh-ui-toggle-label { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }\n\n/* --- Floating surface: a panel anchored to a trigger. This is the material\n * a menu card is made of — the translucent fill plus its backdrop blur, with\n * the edge riding inside the prominent elevation. A plugin that hand-rolls this\n * is how tokens like --dsw-specific-menu get invented; use this instead. --- */\n& .dsh-ui-surface {\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n position: relative;\n display: flex;\n flex-direction: column;\n border: 0;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-menu-surface-fill);\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n box-shadow: var(--dsw-elevation-prominent);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n min-width: 0;\n}\n& .dsh-ui-surface-body { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px 14px; overflow-y: auto; }\n& .dsh-ui-surface-head { display: flex; align-items: center; gap: 8px; min-width: 0; }\n& .dsh-ui-surface-title { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }\n\n/* --- Native select. The primitives ship no select, so a plugin that offers a\n * closed list of choices keeps the platform control and matches the official\n * settings field geometry exactly (34px, 0.5px l4, radius-md, layer-3). A\n * SegmentedControl is the primitive to reach for when the choices are few\n * enough to show at once. --- */\n& .dsh-ui-select {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n height: 34px;\n padding: 0 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n}\n& .dsh-ui-select:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }\n& .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }\n& .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }\n\n/* --- Native textarea. The primitives ship no textarea either, so a settings\n * prompt keeps the platform element and the same field shell as the select:\n * 0.5px l4, radius-md, layer-3, 13/1.5 type. It grows vertically instead of\n * locking to the select's 34px row. --- */\n& .dsh-ui-textarea {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n resize: vertical;\n padding: 8px 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n& .dsh-ui-textarea:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }\n& .dsh-ui-textarea:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }\n& .dsh-ui-textarea::placeholder { color: var(--dsw-alias-label-dimmed); }\n& .dsh-ui-textarea[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }\n\n/* Long panels scroll inside themselves instead of growing the host page. */\n& .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }\n& .dsh-ui-panel-wide { max-width: 100%; }\n\n@media (prefers-reduced-motion: reduce) {\n & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }\n}\n}\n`\n // Widen every recipe from \"a descendant of the root\" to \"the root, or one of\n // its descendants\". Written as a transform rather than by hand so no recipe\n // can be added later with only the descendant form. The pattern is narrow on\n // purpose: a leading `&` followed by one recipe class and no combinator, so\n // sibling and child rules such as `.dsh-ui-field + .dsh-ui-field` keep their\n // own meaning and are left alone.\n return sheet.replace(/^& (\\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className: string) =>\n `&${className}, & ${className}`)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAa,wBAAwB;;CAEnC,SAAS;EACP;EACA;EACA;EACA;EACA;EACA;EACA;CACF;;CAEA,QAAQ;EAAC;EAAyB;EAAyB;EAAyB;CAAuB;;CAE3G,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;CACF;;CAEA,aAAa;EACX;EACA;EACA;CACF;;CAEA,QAAQ;EACN;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;;CAEA,QAAQ;EAAC;EAAmB;EAAmB;EAAmB;CAAoB;;CAEtF,WAAW;EACT;EACA;EACA;EACA;EACA;CACF;;CAEA,SAAS;EACP;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;;CAEA,QAAQ;EACN;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;AAGA,MAAa,6BAAgD,OAAO,OAAO,qBAAqB,CAAC,CAAC,KAAK;;;;;;AAUvG,MAAa,sBACX;;;;;;AAOF,SAAgB,cAAc,SAAS,OAAe;CACpD,OAAO,YAAY,oBAAoB,oBAAoB;AAC7D;;;;;;;AAQA,SAAgB,kBAAkB,OAAkC;CAClE,OAAO,8CAA8C,MAAM;AAC7D;;;;;;AAOA,SAAgB,yBAAyB,OAA0B,OAAuB;CACxF,OAAO,iCAAiC,MAAM,IAAI,kBAAkB,KAAK;AAC3E;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,cAAc,OAA2C;CAKvE,MAAM,SAAS,OAAO,UAAU,WAAW,CAAC,KAAK,IAAI,MAAA,CAAO,KAAI,SAAQ,IAAI,MAAM,CAAC,CAAC,KAAK,IAAI;CAuQ7F,OAAO;;;4BAnQmB,MAAM;;;;EAIhC,MAAM;;;;;;;;;;;;qBAYa,cAAc,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA4BjC,kBAAkB,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuNjB,QAAQ,wCAAwC,QAAQ,cACnE,IAAI,UAAU,MAAM,WAAW;AACnC"}
1
+ {"version":3,"file":"official-ui.js","names":[],"sources":["../src/official-ui.ts"],"sourcesContent":["/**\n * Shared contract that keeps every plugin's browser-side UI on the official\n * DeepSeek Harness Web design language.\n *\n * Two rules produce that language, and both are load-bearing here:\n *\n * 1. Colour, radius, elevation and focus come from the host theme's real\n * `--dsw-*` tokens, written under their own names. Plugins never invent a\n * token, never alias the official ones behind a private palette (a private\n * alias is just a second, diverging theme), and never carries a literal\n * fallback — the host publishes light and dark, and a literal would pin one\n * of them. The only non-colour custom properties used are the host's own\n * content-font axis and frame clearance.\n *\n * 2. Anything the host already ships is used, not reimplemented. Controls come\n * from `@deepseek-ai/dsh-client-ui-primitives` (`Button`, `Input`,\n * `Checkbox`, `Switch`, `Tag`, `Pill`, `SegmentedControl`, `Menu`, `Modal`,\n * `Tooltip`, `Toast`, `DisclosureRow`, `StateDot`, `PathLabel`, the settings\n * form suite). This module only styles what primitives do not cover: page\n * scaffolding, field layout, banners, empty states and scroll regions.\n *\n * The metrics below are transcribed from the primitives' own CSS modules so a\n * feature panel lines up pixel-for-pixel with a native one: the 14/22 body and\n * 13/20 compact type tiers, the 0.5px hairline scale, and the elevation family\n * that draws a surface's edge in its shadow (`border: 0`) instead of a border.\n */\n\n/**\n * Every `--dsw-*` token this repo is allowed to reference, grouped by role.\n *\n * Plugins may only colour through this list. It is exported so the guard test\n * can fail a build when a stylesheet reaches for anything else — the failure\n * mode this prevents is silent: an unknown token resolves to nothing, so a\n * border disappears or a label inherits the wrong colour with no error anywhere.\n */\nexport const OFFICIAL_THEME_TOKENS = {\n /** Surfaces, from the page up to the topmost nested card. */\n surface: [\n '--dsw-alias-bg-base',\n '--dsw-alias-bg-layer-1',\n '--dsw-alias-bg-layer-2',\n '--dsw-alias-bg-layer-3',\n '--dsw-alias-bg-layer-4',\n '--dsw-alias-bg-module-platform',\n '--dsw-alias-bg-overlay',\n ],\n /** Hairlines. l1 is near-invisible on a menu surface, so menus use l2+. */\n border: ['--dsw-alias-border-l1', '--dsw-alias-border-l2', '--dsw-alias-border-l3', '--dsw-alias-border-l4'],\n /** Text. `caption` is menu shortcut text, `dimmed` is placeholder text. */\n label: [\n '--dsw-alias-label-primary',\n '--dsw-alias-label-secondary',\n '--dsw-alias-label-tertiary',\n '--dsw-alias-label-caption',\n '--dsw-alias-label-dimmed',\n '--dsw-alias-label-error',\n '--dsw-alias-label-primary-foreground',\n ],\n /** Interactive fills. The `-danger` hover belongs to destructive rows. */\n interactive: [\n '--dsw-alias-interactive-bg-hover',\n '--dsw-alias-interactive-bg-active',\n '--dsw-alias-interactive-bg-hover-danger',\n ],\n /** Accent and status. `business` is the focus ring's default colour. */\n accent: [\n '--dsw-alias-brand-primary',\n '--dsw-alias-button-primary-fill',\n '--dsw-alias-button-primary-hover',\n '--dsw-alias-button-tool-bar-fill',\n '--dsw-alias-button-tool-bar-hover',\n '--dsw-alias-button-ghost-active-fill',\n '--dsw-alias-button-ghost-active-border',\n ],\n state: [\n '--dsw-alias-state-business-primary',\n '--dsw-alias-state-success-primary',\n '--dsw-alias-state-warn-primary',\n '--dsw-alias-state-error-primary',\n '--dsw-alias-state-idle-primary',\n ],\n /** Corner scale: sm 8, md 12, lg the large card, panel the modal. */\n radius: ['--dsw-radius-sm', '--dsw-radius-md', '--dsw-radius-lg', '--dsw-radius-panel'],\n /** An elevated surface draws its edge in the shadow, so it keeps border: 0. */\n elevation: [\n '--dsw-elevation-soft',\n '--dsw-elevation-stroke',\n '--dsw-elevation-stroke-color',\n '--dsw-elevation-panel',\n '--dsw-elevation-prominent',\n ],\n /** Floats, masks and scrollbars. */\n overlay: [\n '--dsw-menu-surface-fill',\n '--dsw-menu-backdrop-filter',\n '--dsw-mask-blur',\n '--dsw-alias-bg-mask-1',\n '--dsw-alias-menu-icon',\n '--dsw-alias-scrollbar-bg-l2',\n '--dsw-alias-scrollbar-hover-l2',\n '--dsw-shadow-lv3',\n ],\n /** Motion, focus and type axis, published by the host rather than by a component. */\n system: [\n '--dsw-focus-ring-width',\n '--dsw-focus-ring-color',\n '--dsw-font-markdown-code-font-family',\n '--ds-transition-duration',\n '--ds-ease-in-out',\n '--ds-font-family-code',\n '--dsh-scrollbar-thumb',\n '--dsh-scrollbar-thumb-hover',\n '--dsh-content-font-size-secondary',\n '--dsh-content-font-delta',\n '--dsh-frame-top-clearance',\n ],\n} as const\n\n/** Every official token name, flat. Used by the token guard test. */\nexport const OFFICIAL_THEME_TOKEN_NAMES: readonly string[] = Object.values(OFFICIAL_THEME_TOKENS).flat()\n\n/** The elevation steps a surface can be raised by, softest first. */\nexport type OfficialElevation = 'soft' | 'stroke' | 'panel' | 'prominent'\n\n/**\n * The host's focus ring shorthand, verbatim. Writing this per control is how\n * rings drift apart; a stylesheet that draws its own focus style should use\n * {@link officialFocus} instead. The colour fallback is the host's own default.\n */\nexport const OFFICIAL_FOCUS_RING =\n 'var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))'\n\n/**\n * A complete `outline` declaration at the official geometry. `offset` defaults\n * to the 2px the native controls use; the host's inline field rules sit at 1px\n * and a segmented tab pulls the ring inside itself at -2px.\n */\nexport function officialFocus(offset = '2px'): string {\n return `outline: ${OFFICIAL_FOCUS_RING}; outline-offset: ${offset}`\n}\n\n/**\n * An elevated surface. The host draws a raised surface's edge inside its\n * shadow rather than as a border, so a card sets `border: 0` and takes the edge\n * from the elevation — which is also why its border stays off the alias-border\n * set that a flat, non-elevated rule would use.\n */\nexport function officialElevation(level: OfficialElevation): string {\n return `border: 0; box-shadow: var(--dsw-elevation-${level})`\n}\n\n/**\n * An elevated surface whose edge should read as a specific hairline. Rebinding\n * the stroke colour is how a menu card picks l1 while a panel picks l3, without\n * the two growing apart by hand.\n */\nexport function officialStrokedElevation(level: OfficialElevation, color: string): string {\n return `--dsw-elevation-stroke-color: ${color}; ${officialElevation(level)}`\n}\n\n/**\n * The shared stylesheet for a plugin UI, scoped under that plugin's root class.\n *\n * Scoping is what keeps this safe to always-bundle into every client: the rules\n * can only match inside the plugin's own subtree, so two plugins can both mount\n * `dsh-ui-card` without either styling the other's markup.\n *\n * Each recipe matches the root element *and* its descendants, because the\n * commonest arrangement is one element that is both — a surface that carries\n * the root class and `dsh-ui-card`, or a portaled dialog that is the root and\n * the `dsh-ui-surface`. A descendant-only scope would silently leave those\n * unstyled, with no error anywhere.\n *\n * A caveat worth knowing: `Modal` and `Menu` portal to `document.body`, so their\n * content leaves the plugin's subtree entirely. A portaled surface therefore\n * needs its own root class on an element inside the portal.\n *\n * Only patterns that primitives do not provide live here. If a control appears\n * below that `Button`/`Input`/`Switch`/`Tag`/`Menu` already covers, that is a\n * bug in this file: use the primitive and delete the recipe.\n */\nexport function officialUiCss(roots: string | readonly string[]): string {\n // One plugin often mounts several unrelated surfaces (a settings seat, a chat\n // card, a header menu), so the caller names every root. The rules below are\n // nested under a single selector list and use `&`, which the browser expands\n // per root — so one stylesheet covers all of them without duplicating itself.\n const scope = (typeof roots === 'string' ? [roots] : roots).map(root => `.${root}`).join(', ')\n const sheet = `\n/* Official DSH Web UI contract. Generated by @klarkxy/dsh-plugin-kit/official-ui.\n * Do not hand-edit the copy inside a plugin: change this module so every plugin\n * moves together. Scope: ${scope} */\n/* The scope rule below stays open for the whole sheet: every rule after it\n * nests with the parent selector, so one stylesheet can cover several unrelated\n * roots without leaking into a neighbouring plugin. Its closing brace is last. */\n${scope} {\n color: var(--dsw-alias-label-primary);\n font-family: inherit;\n font-size: 14px;\n line-height: 22px;\n min-width: 0;\n\n& *, & *::before, & *::after { box-sizing: border-box; }\n& h1, & h2, & h3, & h4, & p, & figure { margin: 0; }\n/* Keyboard focus only: pointer input keeps the native resting appearance. The\n * ring needs no radius of its own — the outline already follows the control's\n * own border-radius, and overriding it here would flatten every control. */\n& :focus-visible { ${officialFocus()}; }\n& [hidden] { display: none !important; }\n\n/* --- Type tiers. The same 14/22 and 13/20 steps the primitives use. --- */\n& .dsh-ui-title { font-size: 16px; line-height: 24px; font-weight: 500; color: var(--dsw-alias-label-primary); }\n& .dsh-ui-heading { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }\n& .dsh-ui-compact { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary); }\n& .dsh-ui-meta { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }\n& .dsh-ui-hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }\n& .dsh-ui-error { font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); }\n& .dsh-ui-warn { color: var(--dsw-alias-state-warn-primary); }\n& .dsh-ui-success { color: var(--dsw-alias-state-success-primary); }\n& .dsh-ui-muted { color: var(--dsw-alias-label-tertiary); }\n& .dsh-ui-truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n& .dsh-ui-wrap { min-width: 0; overflow-wrap: anywhere; }\n\n/* --- Layout scaffolding. --- */\n& .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }\n& .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }\n/* Plugin detail pages share one reading column. These are layout recipes;\n * controls keep their own primitive styles. Form spacing follows the host's\n * settings-form/fields.module.css and SettingsForm.module.css. */\n& .dsh-ui-page { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 760px; min-width: 0; }\n& .dsh-ui-form { display: flex; flex-direction: column; gap: 0; min-width: 0; }\n&.dsh-ui-form > .dsh-ui-field, & .dsh-ui-form > .dsh-ui-field { padding: 12px 0; }\n& .dsh-ui-form-footer { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-top: 16px; min-width: 0; }\n& .dsh-ui-fields-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; min-width: 0; margin: 0; padding: 0; border: 0; }\n@media (max-width: 560px) {\n & .dsh-ui-fields-grid { grid-template-columns: minmax(0, 1fr); }\n}\n& .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }\n& .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }\n& .dsh-ui-spacer { flex: 1; min-width: 0; }\n/* A 0.5px l2 rule is the host's section divider; reserve the same rhythm. */\n& .dsh-ui-divider { height: 0.5px; background: var(--dsw-alias-border-l2); }\n& .dsh-ui-section { display: flex; flex-direction: column; gap: 12px; padding-top: 16px; border-top: 0.5px solid var(--dsw-alias-border-l2); }\n\n/* --- Card: a raised surface whose edge lives in the shadow. --- */\n& .dsh-ui-card {\n ${officialElevation('stroke')}\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 16px;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n min-width: 0;\n}\n& .dsh-ui-card--flat { border: 0.5px solid var(--dsw-alias-border-l3); box-shadow: none; }\n& .dsh-ui-card--nested { background: var(--dsw-alias-bg-layer-2); }\n& .dsh-ui-card--plain { background: none; }\n\n/* --- Field: label, control, help, error. The control itself is an\n * official primitive, so this only owns the vertical rhythm. --- */\n& .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }\n& .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }\n/* Grid fields share a row, so the stacked-field divider must not win later in\n * the cascade. Reset it here, after the field recipe. */\n& .dsh-ui-fields-grid > .dsh-ui-field { padding: 0; border: 0; }\n& .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }\n& .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }\n& .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }\n& .dsh-ui-help p { margin: 0; }\n& .dsh-ui-help p + p { margin-top: 8px; }\n& .dsh-ui-required { color: var(--dsw-alias-state-error-primary); }\n& .dsh-ui-control { width: 100%; min-width: 0; }\n/* An invalid control is marked on the control, not only in the message. */\n& .dsh-ui-control[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }\n& .dsh-ui-readonly { display: flex; flex-direction: column; gap: 4px; min-width: 0; }\n& .dsh-ui-readonly > dt { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }\n& .dsh-ui-readonly > dd { margin: 0; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }\n\n/* --- Buttons row. A no-op container: it exists so actions line up on the\n * trailing edge the way every native panel does. --- */\n& .dsh-ui-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 4px; }\n& .dsh-ui-actions-end { justify-content: flex-end; }\n\n/* --- Banners. A banner is a left-rail notice, the shape used for warnings,\n * deprecations and blocking instructions. --- */\n& .dsh-ui-banner {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px 14px;\n border: 0.5px solid var(--dsw-alias-border-l3);\n border-left: 3px solid var(--dsw-alias-state-warn-primary);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-1);\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n& .dsh-ui-banner-title { font-weight: 500; color: var(--dsw-alias-state-warn-primary); }\n& .dsh-ui-banner--info { border-left-color: var(--dsw-alias-state-business-primary); }\n& .dsh-ui-banner--danger { border-left-color: var(--dsw-alias-state-error-primary); }\n& .dsh-ui-banner--success { border-left-color: var(--dsw-alias-state-success-primary); }\n& .dsh-ui-banner p { margin: 0; }\n\n/* --- Inline notice: a transient confirmation with no chrome of its own. --- */\n& .dsh-ui-notice { display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-success-primary); }\n& .dsh-ui-notice--error { color: var(--dsw-alias-state-error-primary); }\n\n/* --- Empty and loading states. Both sit in a dashed well so an empty panel\n * reads as \"nothing here yet\" rather than as a broken layout. --- */\n& .dsh-ui-empty {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 6px;\n padding: 20px 16px;\n border: 0.5px dashed var(--dsw-alias-border-l3);\n border-radius: var(--dsw-radius-md);\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n& .dsh-ui-loading { display: flex; align-items: center; gap: 8px; padding: 20px 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }\n\n/* --- Selectable list. A row is a button-sized target with a hover fill and\n * a selected fill; the name carries the weight, the description does not. --- */\n& .dsh-ui-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; min-width: 0; }\n& .dsh-ui-list-scroll { max-height: min(66vh, 760px); overflow-y: auto; overflow-x: hidden; }\n& .dsh-ui-list-row { border-radius: var(--dsw-radius-md); }\n& .dsh-ui-list-row:hover { background: var(--dsw-alias-interactive-bg-hover); }\n& .dsh-ui-list-row[aria-selected='true'], & .dsh-ui-list-row[data-selected] { background: var(--dsw-alias-interactive-bg-active); }\n& .dsh-ui-list-item {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 4px;\n width: 100%;\n min-width: 0;\n padding: 10px 12px;\n border: 0;\n border-radius: var(--dsw-radius-md);\n background: none;\n font: inherit;\n color: inherit;\n text-align: start;\n cursor: pointer;\n}\n& .dsh-ui-list-item:disabled { cursor: default; }\n& .dsh-ui-list-item:disabled:hover { background: none; }\n& .dsh-ui-list-name { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }\n& .dsh-ui-list-desc { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }\n\n/* --- Code and structured output. Wraps rather than scrolls, because a\n * feature panel is read rather than scanned. --- */\n& .dsh-ui-code {\n padding: 10px 12px;\n border: 0.5px solid var(--dsw-alias-border-l3);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-1);\n font-family: var(--ds-font-family-code);\n font-size: 12px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n overflow-wrap: anywhere;\n white-space: pre-wrap;\n}\n& .dsh-ui-scroll { max-height: 60vh; overflow: auto; overscroll-behavior: contain; }\n\n/* --- A toggle row: label on the left, switch on the trailing edge. This is\n * layout only; the switch itself is the official primitive. --- */\n& .dsh-ui-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-width: 0; padding: 12px 0; }\n& .dsh-ui-toggle-row + .dsh-ui-toggle-row { border-top: 0.5px solid var(--dsw-alias-border-l2); }\n& .dsh-ui-toggle-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }\n& .dsh-ui-toggle-label { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }\n\n/* --- Floating surface: a panel anchored to a trigger. This is the material\n * a menu card is made of — the translucent fill plus its backdrop blur, with\n * the edge riding inside the prominent elevation. A plugin that hand-rolls this\n * is how tokens like --dsw-specific-menu get invented; use this instead. --- */\n& .dsh-ui-surface {\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n position: relative;\n display: flex;\n flex-direction: column;\n border: 0;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-menu-surface-fill);\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n box-shadow: var(--dsw-elevation-prominent);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n min-width: 0;\n}\n& .dsh-ui-surface-body { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px 14px; overflow-y: auto; }\n& .dsh-ui-surface-head { display: flex; align-items: center; gap: 8px; min-width: 0; }\n& .dsh-ui-surface-title { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }\n\n/* --- Native select. The primitives ship no select, so a plugin that offers a\n * closed list of choices keeps the platform control and matches the official\n * settings field geometry exactly (34px, 0.5px l4, radius-md, layer-3). A\n * SegmentedControl is the primitive to reach for when the choices are few\n * enough to show at once. --- */\n& .dsh-ui-select {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n height: 34px;\n padding: 0 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n}\n& .dsh-ui-select:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }\n& .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }\n& .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }\n\n/* --- Native textarea. The primitives ship no textarea either, so a settings\n * prompt keeps the platform element and the same field shell as the select:\n * 0.5px l4, radius-md, layer-3, 13/1.5 type. It grows vertically instead of\n * locking to the select's 34px row. --- */\n& .dsh-ui-textarea {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n resize: vertical;\n padding: 8px 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n& .dsh-ui-textarea:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }\n& .dsh-ui-textarea:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }\n& .dsh-ui-textarea::placeholder { color: var(--dsw-alias-label-dimmed); }\n& .dsh-ui-textarea[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }\n\n/* Long panels scroll inside themselves instead of growing the host page. */\n& .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }\n& .dsh-ui-panel-wide { max-width: 100%; }\n\n@media (prefers-reduced-motion: reduce) {\n & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }\n}\n}\n`\n // Widen every recipe from \"a descendant of the root\" to \"the root, or one of\n // its descendants\". Written as a transform rather than by hand so no recipe\n // can be added later with only the descendant form. The pattern is narrow on\n // purpose: a leading `&` followed by one recipe class and no combinator, so\n // sibling and child rules such as `.dsh-ui-field + .dsh-ui-field` keep their\n // own meaning and are left alone.\n return sheet.replace(/^& (\\.dsh-ui-[a-z0-9-]+)(?=\\s*\\{)/gm, (_match, className: string) =>\n `&${className}, & ${className}`)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAa,wBAAwB;;CAEnC,SAAS;EACP;EACA;EACA;EACA;EACA;EACA;EACA;CACF;;CAEA,QAAQ;EAAC;EAAyB;EAAyB;EAAyB;CAAuB;;CAE3G,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;CACF;;CAEA,aAAa;EACX;EACA;EACA;CACF;;CAEA,QAAQ;EACN;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CACA,OAAO;EACL;EACA;EACA;EACA;EACA;CACF;;CAEA,QAAQ;EAAC;EAAmB;EAAmB;EAAmB;CAAoB;;CAEtF,WAAW;EACT;EACA;EACA;EACA;EACA;CACF;;CAEA,SAAS;EACP;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;;CAEA,QAAQ;EACN;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF;;AAGA,MAAa,6BAAgD,OAAO,OAAO,qBAAqB,CAAC,CAAC,KAAK;;;;;;AAUvG,MAAa,sBACX;;;;;;AAOF,SAAgB,cAAc,SAAS,OAAe;CACpD,OAAO,YAAY,oBAAoB,oBAAoB;AAC7D;;;;;;;AAQA,SAAgB,kBAAkB,OAAkC;CAClE,OAAO,8CAA8C,MAAM;AAC7D;;;;;;AAOA,SAAgB,yBAAyB,OAA0B,OAAuB;CACxF,OAAO,iCAAiC,MAAM,IAAI,kBAAkB,KAAK;AAC3E;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,cAAc,OAA2C;CAKvE,MAAM,SAAS,OAAO,UAAU,WAAW,CAAC,KAAK,IAAI,MAAA,CAAO,KAAI,SAAQ,IAAI,MAAM,CAAC,CAAC,KAAK,IAAI;CAqR7F,OAAO;;;4BAjRmB,MAAM;;;;EAIhC,MAAM;;;;;;;;;;;;qBAYa,cAAc,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAuCjC,kBAAkB,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0NjB,QAAQ,wCAAwC,QAAQ,cACnE,IAAI,UAAU,MAAM,WAAW;AACnC"}