@linxin666/dsh-pet 0.1.1

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 (79) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +106 -0
  3. package/assets/whale/pet.json +7 -0
  4. package/assets/whale/previews/failed.gif +0 -0
  5. package/assets/whale/previews/idle.gif +0 -0
  6. package/assets/whale/previews/jumping.gif +0 -0
  7. package/assets/whale/previews/review.gif +0 -0
  8. package/assets/whale/previews/running-left.gif +0 -0
  9. package/assets/whale/previews/running-right.gif +0 -0
  10. package/assets/whale/previews/running.gif +0 -0
  11. package/assets/whale/previews/waiting.gif +0 -0
  12. package/assets/whale/previews/waving.gif +0 -0
  13. package/assets/whale/spritesheet.webp +0 -0
  14. package/cordis.patch.yml +10 -0
  15. package/lib/client.js +1515 -0
  16. package/lib/index.js +642 -0
  17. package/lib/invariant.js +34 -0
  18. package/lib/types/affinity.d.ts +83 -0
  19. package/lib/types/affinity.d.ts.map +1 -0
  20. package/lib/types/client/PetDockEntry.d.ts +44 -0
  21. package/lib/types/client/PetDockEntry.d.ts.map +1 -0
  22. package/lib/types/client/PetSettingsCard.d.ts +67 -0
  23. package/lib/types/client/PetSettingsCard.d.ts.map +1 -0
  24. package/lib/types/client/PluginSettingsCard.d.ts +78 -0
  25. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
  26. package/lib/types/client/WhalePet.d.ts +49 -0
  27. package/lib/types/client/WhalePet.d.ts.map +1 -0
  28. package/lib/types/client/index.d.ts +46 -0
  29. package/lib/types/client/index.d.ts.map +1 -0
  30. package/lib/types/client/locales.d.ts +101 -0
  31. package/lib/types/client/locales.d.ts.map +1 -0
  32. package/lib/types/client/pet-store.d.ts +49 -0
  33. package/lib/types/client/pet-store.d.ts.map +1 -0
  34. package/lib/types/client/settings-form.d.ts +119 -0
  35. package/lib/types/client/settings-form.d.ts.map +1 -0
  36. package/lib/types/client/slots-augment.d.ts +19 -0
  37. package/lib/types/client/slots-augment.d.ts.map +1 -0
  38. package/lib/types/client/spritesheet.d.ts +69 -0
  39. package/lib/types/client/spritesheet.d.ts.map +1 -0
  40. package/lib/types/index.d.ts +44 -0
  41. package/lib/types/index.d.ts.map +1 -0
  42. package/lib/types/invariant.d.ts +10 -0
  43. package/lib/types/invariant.d.ts.map +1 -0
  44. package/lib/types/persist.d.ts +45 -0
  45. package/lib/types/persist.d.ts.map +1 -0
  46. package/lib/types/routes.d.ts +22 -0
  47. package/lib/types/routes.d.ts.map +1 -0
  48. package/lib/types/service.d.ts +161 -0
  49. package/lib/types/service.d.ts.map +1 -0
  50. package/lib/types/state.d.ts +80 -0
  51. package/lib/types/state.d.ts.map +1 -0
  52. package/lib/types/treats.d.ts +59 -0
  53. package/lib/types/treats.d.ts.map +1 -0
  54. package/package.json +100 -0
  55. package/src/affinity.test.ts +74 -0
  56. package/src/affinity.ts +144 -0
  57. package/src/client/PetDockEntry.tsx +95 -0
  58. package/src/client/PetSettingsCard.tsx +186 -0
  59. package/src/client/PluginSettingsCard.tsx +207 -0
  60. package/src/client/WhalePet.tsx +342 -0
  61. package/src/client/css-modules.d.ts +6 -0
  62. package/src/client/index.ts +261 -0
  63. package/src/client/locales.ts +108 -0
  64. package/src/client/pet-store.ts +80 -0
  65. package/src/client/pet.module.css +185 -0
  66. package/src/client/settings-card.module.css +277 -0
  67. package/src/client/settings-form.ts +294 -0
  68. package/src/client/slots-augment.ts +27 -0
  69. package/src/client/spritesheet.ts +138 -0
  70. package/src/index.ts +148 -0
  71. package/src/invariant.ts +40 -0
  72. package/src/persist.test.ts +96 -0
  73. package/src/persist.ts +128 -0
  74. package/src/routes.ts +171 -0
  75. package/src/service.ts +389 -0
  76. package/src/state.test.ts +65 -0
  77. package/src/state.ts +156 -0
  78. package/src/treats.test.ts +86 -0
  79. package/src/treats.ts +101 -0
package/lib/client.js ADDED
@@ -0,0 +1,1515 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@linxin666/dsh-pet",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
8
+ let react = require("react");
9
+ let react_dom = require("react-dom");
10
+ let react_jsx_runtime = require("react/jsx-runtime");
11
+ //#region src/client/pet-store.ts
12
+ /**
13
+ * Browser-side pet store: the pet state snapshot plus transient UI feedback
14
+ * (reaction bubbles), written only through the store's audit actions. The
15
+ * RPC polling and interactions live in the plugin apply body; components
16
+ * only ever read snapshots.
17
+ * @module @linxin666/dsh-pet/client/pet-store
18
+ */
19
+ /** Create the pet store handle (apply world only; never module-level). */
20
+ function createPetStore() {
21
+ return (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
22
+ init: () => ({
23
+ snapshot: null,
24
+ state: "loading",
25
+ error: null,
26
+ feedback: null
27
+ }),
28
+ actions: {
29
+ setSnapshot: (draft, snapshot) => {
30
+ draft.snapshot = snapshot;
31
+ draft.state = "ready";
32
+ draft.error = null;
33
+ },
34
+ setState: (draft, state, error) => {
35
+ draft.state = state;
36
+ draft.error = error;
37
+ },
38
+ setFeedback: (draft, feedback) => {
39
+ draft.feedback = feedback;
40
+ }
41
+ }
42
+ });
43
+ }
44
+ /**
45
+ * Track definitions for the whale-girl. Durations are tuned for a soft,
46
+ * slow-healing feel (roughly 2.5× the earlier fast draft — the pet should
47
+ * breathe, not race); calibrate frame counts against the hatch-pet run when
48
+ * the asset lands (rows may carry 4–8 frames).
49
+ */
50
+ const TRACKS = {
51
+ idle: {
52
+ frames: [
53
+ 0,
54
+ 1,
55
+ 2,
56
+ 3,
57
+ 4,
58
+ 5
59
+ ],
60
+ durations: [
61
+ 400,
62
+ 400,
63
+ 500,
64
+ 400,
65
+ 400,
66
+ 500
67
+ ],
68
+ loop: true
69
+ },
70
+ "running-right": {
71
+ frames: [
72
+ 0,
73
+ 1,
74
+ 2,
75
+ 3,
76
+ 4,
77
+ 5,
78
+ 6,
79
+ 7
80
+ ],
81
+ durations: [
82
+ 225,
83
+ 225,
84
+ 225,
85
+ 225,
86
+ 225,
87
+ 225,
88
+ 225,
89
+ 225
90
+ ],
91
+ loop: true
92
+ },
93
+ "running-left": {
94
+ frames: [
95
+ 0,
96
+ 1,
97
+ 2,
98
+ 3,
99
+ 4,
100
+ 5,
101
+ 6,
102
+ 7
103
+ ],
104
+ durations: [
105
+ 225,
106
+ 225,
107
+ 225,
108
+ 225,
109
+ 225,
110
+ 225,
111
+ 225,
112
+ 225
113
+ ],
114
+ loop: true
115
+ },
116
+ waving: {
117
+ frames: [
118
+ 0,
119
+ 1,
120
+ 2,
121
+ 3
122
+ ],
123
+ durations: [
124
+ 350,
125
+ 350,
126
+ 350,
127
+ 350
128
+ ],
129
+ loop: true
130
+ },
131
+ jumping: {
132
+ frames: [
133
+ 0,
134
+ 1,
135
+ 2,
136
+ 3,
137
+ 4
138
+ ],
139
+ durations: [
140
+ 300,
141
+ 300,
142
+ 300,
143
+ 350,
144
+ 350
145
+ ],
146
+ loop: false,
147
+ fallback: "idle"
148
+ },
149
+ failed: {
150
+ frames: [
151
+ 0,
152
+ 1,
153
+ 2,
154
+ 3,
155
+ 4,
156
+ 5,
157
+ 6,
158
+ 7
159
+ ],
160
+ durations: [
161
+ 450,
162
+ 450,
163
+ 450,
164
+ 500,
165
+ 550,
166
+ 600,
167
+ 450,
168
+ 450
169
+ ],
170
+ loop: false,
171
+ fallback: "idle"
172
+ },
173
+ waiting: {
174
+ frames: [
175
+ 0,
176
+ 1,
177
+ 2,
178
+ 3,
179
+ 4,
180
+ 5
181
+ ],
182
+ durations: [
183
+ 450,
184
+ 450,
185
+ 500,
186
+ 450,
187
+ 450,
188
+ 500
189
+ ],
190
+ loop: true
191
+ },
192
+ running: {
193
+ frames: [
194
+ 0,
195
+ 1,
196
+ 2,
197
+ 3,
198
+ 4,
199
+ 5
200
+ ],
201
+ durations: [
202
+ 250,
203
+ 250,
204
+ 250,
205
+ 250,
206
+ 250,
207
+ 250
208
+ ],
209
+ loop: true
210
+ },
211
+ review: {
212
+ frames: [
213
+ 0,
214
+ 1,
215
+ 2,
216
+ 3,
217
+ 4,
218
+ 5
219
+ ],
220
+ durations: [
221
+ 550,
222
+ 550,
223
+ 550,
224
+ 550,
225
+ 550,
226
+ 550
227
+ ],
228
+ loop: true
229
+ }
230
+ };
231
+ /** Row index of one animation track (mirrors state.ts rowOf). */
232
+ function rowOfTrack(animation) {
233
+ return {
234
+ idle: 0,
235
+ "running-right": 1,
236
+ "running-left": 2,
237
+ waving: 3,
238
+ jumping: 4,
239
+ failed: 5,
240
+ waiting: 6,
241
+ running: 7,
242
+ review: 8
243
+ }[animation];
244
+ }
245
+ /**
246
+ * Background-position (px) of one frame cell within the scaled atlas.
247
+ * The background image is scaled by `scale` (element size ÷ cell size), and
248
+ * background-position offsets are applied in SCALED coordinates — using raw
249
+ * atlas coordinates here would drift each frame by the scale factor and
250
+ * render torn/overlapping frames.
251
+ */
252
+ function framePosition(row, col, scale = 1) {
253
+ return {
254
+ x: -col * 192 * scale,
255
+ y: -row * 208 * scale
256
+ };
257
+ }
258
+ /**
259
+ * Detect how many frames each row actually carries by scanning the decoded
260
+ * atlas for non-transparent cells (hatch-pet rows may hold 4–8 frames; the
261
+ * unused trailing cells are fully transparent). Rows whose every sample is
262
+ * transparent report 0.
263
+ * @param image - the fully decoded spritesheet (natural size 1536×1872).
264
+ * @returns per-row frame counts, length 9.
265
+ */
266
+ function detectFrameCounts(image) {
267
+ const canvas = document.createElement("canvas");
268
+ canvas.width = image.naturalWidth;
269
+ canvas.height = image.naturalHeight;
270
+ const ctx = canvas.getContext("2d");
271
+ if (ctx === null) return Array.from({ length: 9 }, () => 8);
272
+ ctx.drawImage(image, 0, 0);
273
+ const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
274
+ const counts = [];
275
+ const stride = 1536;
276
+ const probeStep = 8;
277
+ const margin = 12;
278
+ for (let row = 0; row < 9; row++) {
279
+ let count = 0;
280
+ for (let col = 0; col < 8; col++) {
281
+ let hasContent = false;
282
+ const x0 = col * 192;
283
+ const y0 = row * 208;
284
+ for (let y = y0 + margin; y < y0 + 208 - margin && !hasContent; y += probeStep) for (let x = x0 + margin; x < x0 + 192 - margin && !hasContent; x += probeStep) if ((data[(y * stride + x) * 4 + 3] ?? 0) > 8) hasContent = true;
285
+ if (hasContent) count += 1;
286
+ }
287
+ counts.push(count);
288
+ }
289
+ return counts;
290
+ }
291
+ /**
292
+ * Trim a track to the actual frame count of its row. A row with 0 detected
293
+ * frames degrades to the first frame (the atlas is still loading or corrupt)
294
+ * so the pet never renders blank.
295
+ */
296
+ function trimTrack(track, frameCount) {
297
+ const n = Math.max(1, Math.min(frameCount, track.frames.length));
298
+ return {
299
+ frames: track.frames.slice(0, n),
300
+ durations: track.durations.slice(0, n),
301
+ loop: track.loop,
302
+ ...track.fallback === void 0 ? {} : { fallback: track.fallback }
303
+ };
304
+ }
305
+ //#endregion
306
+ //#region \0dsh-css:/Users/zcl/code/dsh-web-ui/packages/dsh-pet/src/client/pet.module.css.mjs
307
+ const css$1 = ".Xhudva_float{pointer-events:auto;-webkit-user-select:none;user-select:none;flex-direction:column;align-items:center;display:flex;position:fixed}.Xhudva_sprite{image-rendering:auto;touch-action:none;position:relative}.Xhudva_bubble{white-space:nowrap;color:#fff;pointer-events:none;border-radius:999px;margin-bottom:6px;padding:4px 10px;font-size:12px;line-height:1.4;animation:2.6s ease-out forwards Xhudva_pet-bubble-pop;position:absolute;bottom:100%;box-shadow:0 2px 8px #00000040}.Xhudva_bubblePet{background:#f472b6eb}.Xhudva_bubbleFeed{background:#38bdf8eb}@keyframes Xhudva_pet-bubble-pop{0%{opacity:0;transform:translateY(6px)scale(.85)}15%{opacity:1;transform:translateY(0)scale(1.05)}25%{transform:translateY(0)scale(1)}75%{opacity:1}to{opacity:0;transform:translateY(-8px)scale(.95)}}.Xhudva_panel{color:#e2e8f0;backdrop-filter:blur(6px);background:#0f172aeb;border:1px solid #94a3b859;border-radius:10px;flex-direction:column;gap:6px;min-width:132px;padding:8px 10px;font-size:12px;display:flex;position:absolute;bottom:calc(100% + 4px);box-shadow:0 4px 16px #00000059}.Xhudva_rankRow{white-space:nowrap;justify-content:space-between;gap:10px;display:flex}.Xhudva_nameCell{font-weight:600}.Xhudva_renameRow{align-items:center;gap:6px;display:flex}.Xhudva_nameInput{color:#e2e8f0;background:#1e293be6;border:1px solid #7dd3fc80;border-radius:6px;outline:none;flex:1;min-width:0;padding:3px 6px;font-size:12px}.Xhudva_nameInput:focus{border-color:#38bdf8;box-shadow:0 0 0 2px #38bdf873}.Xhudva_actions{gap:6px;display:flex}.Xhudva_action{cursor:pointer;color:#0f172a;background:linear-gradient(#7dd3fc,#38bdf8);border:none;border-radius:6px;flex:1;padding:4px 8px;font-size:12px;transition:filter .12s,box-shadow .12s}.Xhudva_action:hover{filter:brightness(1.08)}.Xhudva_action:active{filter:brightness(.94)}.Xhudva_action:focus-visible{outline:none;box-shadow:0 0 0 2px #38bdf8d9}.Xhudva_summon{color:#7dd3fc;cursor:pointer;background:#0f172abf;border:1px dashed #7dd3fc99;border-radius:999px;padding:2px 10px;font-size:11px;transition:border-color .12s,color .12s,background .12s,box-shadow .12s}.Xhudva_summon:hover{color:#bae6fd;background:#0f172ae6;border-color:#7dd3fcf2}.Xhudva_summon:active{color:#7dd3fc;border-color:#7dd3fccc}.Xhudva_summon:focus-visible{outline:none;box-shadow:0 0 0 2px #38bdf8d9}@media (prefers-reduced-motion:reduce){.Xhudva_bubble{opacity:1;animation:none}.Xhudva_action,.Xhudva_summon{transition:none}}";
308
+ const tagId$1 = "@linxin666/dsh-pet/pet.module.css";
309
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
310
+ const tag = document.createElement("style");
311
+ tag.dataset.plugin = "@linxin666/dsh-pet";
312
+ tag.dataset.pluginCss = tagId$1;
313
+ tag.textContent = css$1;
314
+ document.head.appendChild(tag);
315
+ }
316
+ var pet_module_css_default = {
317
+ "action": "Xhudva_action",
318
+ "actions": "Xhudva_actions",
319
+ "bubble": "Xhudva_bubble",
320
+ "bubbleFeed": "Xhudva_bubbleFeed",
321
+ "bubblePet": "Xhudva_bubblePet",
322
+ "float": "Xhudva_float",
323
+ "nameCell": "Xhudva_nameCell",
324
+ "nameInput": "Xhudva_nameInput",
325
+ "panel": "Xhudva_panel",
326
+ "pet-bubble-pop": "Xhudva_pet-bubble-pop",
327
+ "rankRow": "Xhudva_rankRow",
328
+ "renameRow": "Xhudva_renameRow",
329
+ "sprite": "Xhudva_sprite",
330
+ "summon": "Xhudva_summon"
331
+ };
332
+ //#endregion
333
+ //#region src/client/WhalePet.tsx
334
+ /**
335
+ * Whale-girl companion component — the browser half's centerpiece. Renders a
336
+ * fixed-position floating sprite (React portal onto document.body), plays
337
+ * the spritesheet track matching the host animation snapshot, and exposes
338
+ * the interaction surface: click to pet, hover panel with feed/hide, drag to
339
+ * reposition (persisted via setConfig).
340
+ * @module @linxin666/dsh-pet/client/WhalePet
341
+ */
342
+ /** Browser URL of the whale-girl atlas (served by the host half's own route). */
343
+ const PET_SPRITESHEET_URL = "/pet/whale/spritesheet.webp";
344
+ /** Browser URL of the whale-girl manifest (authoritative per-row frame counts). */
345
+ const PET_MANIFEST_URL = "/pet/whale/pet.json";
346
+ /** Clamp a drag offset inside the viewport with a margin. */
347
+ function clampOffset(value, max) {
348
+ return Math.max(0, Math.min(max, value));
349
+ }
350
+ /**
351
+ * The floating pet. The spritesheet frame advances on requestAnimationFrame
352
+ * with per-frame durations from TRACKS; the atlas image is loaded once and
353
+ * the background position is written straight to the sprite element (no
354
+ * per-frame React state).
355
+ */
356
+ function WhalePet(props) {
357
+ const { snapshot, display, feedback } = props;
358
+ const spriteRef = (0, react.useRef)(null);
359
+ const floatRef = (0, react.useRef)(null);
360
+ const [imageReady, setImageReady] = (0, react.useState)(false);
361
+ const [frameCounts, setFrameCounts] = (0, react.useState)(null);
362
+ const [hovered, setHovered] = (0, react.useState)(false);
363
+ const [renaming, setRenaming] = (0, react.useState)(false);
364
+ const [nameDraft, setNameDraft] = (0, react.useState)("");
365
+ const [dragPos, setDragPos] = (0, react.useState)(null);
366
+ const dragRef = (0, react.useRef)(null);
367
+ const frameRef = (0, react.useRef)({
368
+ track: null,
369
+ index: 0,
370
+ elapsed: 0
371
+ });
372
+ (0, react.useEffect)(() => {
373
+ let cancelled = false;
374
+ const img = new Image();
375
+ img.onload = () => {
376
+ if (cancelled) return;
377
+ setImageReady(true);
378
+ fetch(PET_MANIFEST_URL).then((res) => res.ok ? res.json() : Promise.resolve({})).then((manifest) => {
379
+ if (cancelled) return;
380
+ const frames = manifest.frames;
381
+ if (Array.isArray(frames) && frames.length === 9 && frames.every((n) => typeof n === "number")) setFrameCounts(frames);
382
+ else setFrameCounts(detectFrameCounts(img));
383
+ }).catch(() => {
384
+ if (!cancelled) setFrameCounts(detectFrameCounts(img));
385
+ });
386
+ };
387
+ img.src = PET_SPRITESHEET_URL;
388
+ return () => {
389
+ cancelled = true;
390
+ img.onload = null;
391
+ };
392
+ }, []);
393
+ const spriteScale = display.size / 208;
394
+ const animation = snapshot?.animation ?? "idle";
395
+ const scaleRef = (0, react.useRef)(spriteScale);
396
+ scaleRef.current = spriteScale;
397
+ (0, react.useEffect)(() => {
398
+ const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
399
+ const row = rowOfTrack(animation);
400
+ const leadCol = (frameCounts === null ? TRACKS[animation] : trimTrack(TRACKS[animation], frameCounts[row] ?? TRACKS[animation].frames.length)).frames[0];
401
+ const lead = framePosition(row, leadCol, scaleRef.current);
402
+ if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition = `${lead.x}px ${lead.y}px`;
403
+ if (reduceMotion) return;
404
+ let raf = 0;
405
+ let last = performance.now();
406
+ const tick = (ts) => {
407
+ const delta = ts - last;
408
+ last = ts;
409
+ const row = rowOfTrack(animation);
410
+ const track = frameCounts === null ? TRACKS[animation] : trimTrack(TRACKS[animation], frameCounts[row] ?? TRACKS[animation].frames.length);
411
+ const st = frameRef.current;
412
+ if (st.track !== animation) {
413
+ st.track = animation;
414
+ st.index = 0;
415
+ st.elapsed = 0;
416
+ }
417
+ st.elapsed += delta;
418
+ const maxIndex = track.frames.length - 1;
419
+ while (st.elapsed >= (track.durations[st.index] ?? 0) && st.index < maxIndex) {
420
+ st.elapsed -= track.durations[st.index] ?? 0;
421
+ st.index += 1;
422
+ }
423
+ if (st.elapsed >= (track.durations[st.index] ?? 0)) if (track.loop) {
424
+ st.elapsed = 0;
425
+ st.index = 0;
426
+ } else st.index = maxIndex;
427
+ const col = track.frames[st.index];
428
+ const { x, y } = framePosition(row, col, scaleRef.current);
429
+ if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition = `${x}px ${y}px`;
430
+ raf = requestAnimationFrame(tick);
431
+ };
432
+ raf = requestAnimationFrame(tick);
433
+ return () => cancelAnimationFrame(raf);
434
+ }, [animation, frameCounts]);
435
+ const feedbackDoneRef = (0, react.useRef)(props.onFeedbackDone);
436
+ feedbackDoneRef.current = props.onFeedbackDone;
437
+ (0, react.useEffect)(() => {
438
+ if (feedback === null) return;
439
+ const timer = window.setTimeout(() => feedbackDoneRef.current(), 2600);
440
+ return () => window.clearTimeout(timer);
441
+ }, [feedback]);
442
+ const draggedRef = (0, react.useRef)(false);
443
+ const onPointerDown = (e) => {
444
+ e.preventDefault();
445
+ e.target.setPointerCapture?.(e.pointerId);
446
+ const current = dragPos ?? {
447
+ right: display.right,
448
+ bottom: display.bottom
449
+ };
450
+ dragRef.current = {
451
+ startX: e.clientX,
452
+ startY: e.clientY,
453
+ ...current
454
+ };
455
+ draggedRef.current = false;
456
+ setHovered(false);
457
+ };
458
+ const onPointerMove = (e) => {
459
+ const drag = dragRef.current;
460
+ if (drag === null) return;
461
+ const dx = e.clientX - drag.startX;
462
+ const dy = e.clientY - drag.startY;
463
+ if (Math.abs(dx) > 4 || Math.abs(dy) > 4) draggedRef.current = true;
464
+ const right = clampOffset(drag.right - dx, window.innerWidth - 40);
465
+ const bottom = clampOffset(drag.bottom - dy, window.innerHeight - 40);
466
+ setDragPos({
467
+ right,
468
+ bottom
469
+ });
470
+ };
471
+ const onPointerUp = () => {
472
+ if (dragRef.current === null) return;
473
+ dragRef.current = null;
474
+ if (dragPos !== null) props.onDragEnd(dragPos.right, dragPos.bottom);
475
+ };
476
+ const pos = dragPos ?? {
477
+ right: display.right,
478
+ bottom: display.bottom
479
+ };
480
+ const spriteWidth = Math.round(192 * spriteScale);
481
+ const spriteHeight = Math.round(208 * spriteScale);
482
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
483
+ ref: floatRef,
484
+ className: pet_module_css_default.float,
485
+ style: {
486
+ right: pos.right,
487
+ bottom: pos.bottom,
488
+ zIndex: 2147483e3
489
+ },
490
+ onPointerEnter: () => setHovered(true),
491
+ onPointerLeave: (e) => {
492
+ const next = e.relatedTarget;
493
+ if (next instanceof Node && floatRef.current?.contains(next)) return;
494
+ setHovered(false);
495
+ },
496
+ children: [
497
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
498
+ ref: spriteRef,
499
+ className: pet_module_css_default.sprite,
500
+ style: {
501
+ width: spriteWidth,
502
+ height: spriteHeight,
503
+ backgroundImage: imageReady ? `url(${PET_SPRITESHEET_URL})` : void 0,
504
+ backgroundSize: `${1536 * spriteScale}px ${1872 * spriteScale}px`,
505
+ backgroundRepeat: "no-repeat",
506
+ backgroundPosition: "0 0",
507
+ cursor: dragRef.current === null ? "grab" : "grabbing"
508
+ },
509
+ onPointerDown,
510
+ onPointerMove,
511
+ onPointerUp,
512
+ onClick: () => {
513
+ if (draggedRef.current) return;
514
+ props.onPet();
515
+ },
516
+ role: "button",
517
+ "aria-label": "whale girl"
518
+ }),
519
+ feedback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
520
+ className: `${pet_module_css_default.bubble} ${feedback.kind === "feed" ? pet_module_css_default.bubbleFeed : pet_module_css_default.bubblePet}`,
521
+ children: feedback.text
522
+ }, feedback.at),
523
+ hovered && dragRef.current === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
524
+ className: pet_module_css_default.panel,
525
+ children: renaming ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
526
+ className: pet_module_css_default.renameRow,
527
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
528
+ className: pet_module_css_default.nameInput,
529
+ value: nameDraft,
530
+ maxLength: 20,
531
+ placeholder: props.t("pet.namePlaceholder"),
532
+ autoFocus: true,
533
+ onChange: (e) => setNameDraft(e.target.value),
534
+ onKeyDown: (e) => {
535
+ if (e.key === "Enter") {
536
+ const trimmed = nameDraft.trim();
537
+ if (trimmed !== "") {
538
+ props.onRename(trimmed);
539
+ setRenaming(false);
540
+ }
541
+ } else if (e.key === "Escape") setRenaming(false);
542
+ }
543
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
544
+ type: "button",
545
+ className: pet_module_css_default.action,
546
+ onClick: () => {
547
+ const trimmed = nameDraft.trim();
548
+ if (trimmed !== "") {
549
+ props.onRename(trimmed);
550
+ setRenaming(false);
551
+ }
552
+ },
553
+ children: props.t("pet.confirm")
554
+ })]
555
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
556
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
557
+ className: pet_module_css_default.rankRow,
558
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
559
+ className: pet_module_css_default.nameCell,
560
+ children: snapshot?.name ?? "鲸鱼娘"
561
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: props.t("pet.rank", { rank: snapshot?.affinity.rank ?? "?" }) })]
562
+ }),
563
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
564
+ className: pet_module_css_default.rankRow,
565
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: props.t("pet.treats", { n: snapshot?.treats.stocked ?? 0 }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: props.t("pet.points", { points: snapshot?.affinity.points ?? 0 }) })]
566
+ }),
567
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
568
+ className: pet_module_css_default.actions,
569
+ children: [
570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
571
+ type: "button",
572
+ className: pet_module_css_default.action,
573
+ onClick: props.onFeed,
574
+ children: props.t("pet.feed")
575
+ }),
576
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
577
+ type: "button",
578
+ className: pet_module_css_default.action,
579
+ onClick: () => {
580
+ setNameDraft(snapshot?.name ?? "");
581
+ setRenaming(true);
582
+ },
583
+ children: props.t("pet.rename")
584
+ }),
585
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
586
+ type: "button",
587
+ className: pet_module_css_default.action,
588
+ onClick: props.onHide,
589
+ children: props.t("pet.hide")
590
+ })
591
+ ]
592
+ })
593
+ ] })
594
+ })
595
+ ]
596
+ }), document.body);
597
+ }
598
+ //#endregion
599
+ //#region src/client/PetDockEntry.tsx
600
+ /**
601
+ * Dock anchor inside `conversation.input.selector.context`: the input
602
+ * selector row mounts in EVERY conversation phase (no-session cold start,
603
+ * the blank-session hero, and the active seat), so the floating pet stays on
604
+ * screen on the new-conversation screen too. While visible it mounts the
605
+ * floating WhalePet (portal); while hidden it renders the summon button.
606
+ * @module @linxin666/dsh-pet/client/PetDockEntry
607
+ */
608
+ const DEFAULT_DISPLAY = {
609
+ visible: true,
610
+ size: 160,
611
+ right: 24,
612
+ bottom: 20
613
+ };
614
+ /**
615
+ * Dock entry: while the pet is visible, mount the floating WhalePet (it
616
+ * portals itself onto document.body); while hidden, render the summon
617
+ * button so the pet can always come back. The store is the plugin-owned
618
+ * single instance — the slot system provides none because the pet is
619
+ * host-global, not session-scoped.
620
+ */
621
+ function PetDockEntry(props) {
622
+ const { store, ensure } = props;
623
+ const ui = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
624
+ const snapshot = ui.snapshot;
625
+ const feedback = ui.feedback;
626
+ const visible = snapshot?.display.visible ?? true;
627
+ (0, react.useEffect)(() => {
628
+ ensure();
629
+ }, [ensure]);
630
+ if (visible) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
631
+ "data-pet-dock": true,
632
+ "data-testid": "pet-dock",
633
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WhalePet, {
634
+ snapshot,
635
+ display: snapshot?.display ?? DEFAULT_DISPLAY,
636
+ feedback,
637
+ onPet: props.pet,
638
+ onFeed: props.feed,
639
+ onHide: props.hide,
640
+ onDragEnd: props.dragEnd,
641
+ onRename: props.rename,
642
+ onFeedbackDone: props.feedbackDone,
643
+ t: props.t
644
+ })
645
+ });
646
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
647
+ type: "button",
648
+ className: pet_module_css_default.summon,
649
+ onClick: props.summon,
650
+ "data-testid": "pet-summon",
651
+ children: props.t("pet.summon", { name: snapshot?.name ?? "鲸鱼娘" })
652
+ });
653
+ }
654
+ //#endregion
655
+ //#region \0dsh-css:/Users/zcl/code/dsh-web-ui/packages/dsh-pet/src/client/settings-card.module.css.mjs
656
+ const css = "._2dHfjG_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;list-style:none;transition:border-color .16s,background .16s;overflow:hidden}._2dHfjG_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}._2dHfjG_header{cursor:pointer;text-align:left;width:100%;font:inherit;background:0 0;border:0;align-items:center;gap:8px;padding:10px 14px;transition:background .12s;display:flex}._2dHfjG_header:hover{background:var(--dsw-alias-interactive-bg-hover)}._2dHfjG_header:active{background:var(--dsw-alias-interactive-bg-hover-solid)}._2dHfjG_header:focus-visible{box-shadow:inset 0 0 0 2px var(--dsw-alias-button-info-fill);outline:none}._2dHfjG_headText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}._2dHfjG_name{color:var(--dsw-alias-label-primary);font-weight:600}._2dHfjG_description{color:var(--dsw-alias-label-tertiary);font-size:12px}._2dHfjG_pending{color:var(--dsw-alias-state-warn-primary);font-size:12px}._2dHfjG_chevron{color:var(--dsw-alias-label-tertiary);transition:transform .12s}._2dHfjG_chevronOpen{transform:rotate(180deg)}._2dHfjG_body{flex-direction:column;gap:14px;padding:0 14px 14px;display:flex}._2dHfjG_readOnly{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px}._2dHfjG_footer{justify-content:flex-end;align-items:center;gap:8px;display:flex}._2dHfjG_failed{color:var(--dsw-alias-state-error-primary);margin:0 auto 0 0;font-size:12px}._2dHfjG_discard,._2dHfjG_save{font:inherit;cursor:pointer;border-radius:6px;padding:5px 12px;font-size:13px;transition:color .12s,border-color .12s,background .12s,box-shadow .12s}._2dHfjG_discard{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}._2dHfjG_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}._2dHfjG_discard:active:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._2dHfjG_save{border:1px solid var(--dsw-alias-button-info-fill);background:var(--dsw-alias-button-info-fill);color:var(--dsw-alias-label-primary-foreground)}._2dHfjG_save:hover:not(:disabled){border-color:var(--dsw-alias-button-info-hover);background:var(--dsw-alias-button-info-hover)}._2dHfjG_save:active:not(:disabled){filter:brightness(.94)}._2dHfjG_discard:focus-visible:not(:disabled),._2dHfjG_save:focus-visible:not(:disabled){box-shadow:0 0 0 2px var(--dsw-alias-button-info-fill);outline:none}._2dHfjG_discard:disabled,._2dHfjG_save:disabled{opacity:.5;cursor:default}._2dHfjG_field{flex-direction:column;gap:4px;display:flex}._2dHfjG_head{align-items:center;gap:8px;display:flex}._2dHfjG_label{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}._2dHfjG_badges{align-items:center;gap:6px;display:flex}._2dHfjG_badge{background:var(--dsw-alias-interactive-bg-hover-accent);color:var(--dsw-alias-state-business-primary);border-radius:999px;padding:1px 6px;font-size:11px}._2dHfjG_reset{color:var(--dsw-alias-state-business-primary);cursor:pointer;background:0 0;border:0;border-radius:3px;padding:0;font-size:11px;transition:color .12s,box-shadow .12s}._2dHfjG_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary-bluish);text-decoration:underline}._2dHfjG_reset:active:not(:disabled){color:var(--dsw-alias-state-business-primary)}._2dHfjG_reset:focus-visible:not(:disabled){box-shadow:0 0 0 2px var(--dsw-alias-button-info-fill);outline:none}._2dHfjG_reset:disabled{opacity:.5;cursor:default}._2dHfjG_input,._2dHfjG_select{border:1px solid var(--dsw-alias-border-l2);font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border-radius:6px;padding:6px 8px;font-size:13px}._2dHfjG_inputInvalid{border:1px solid var(--dsw-alias-state-error-primary);font:inherit;color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 8px;font-size:13px}._2dHfjG_input:disabled,._2dHfjG_select:disabled{opacity:.6}._2dHfjG_input:focus,._2dHfjG_select:focus{border-color:var(--dsw-alias-button-info-fill);box-shadow:0 0 0 2px var(--dsw-alias-button-info-fill);outline:none}._2dHfjG_inputInvalid:focus{box-shadow:0 0 0 2px var(--dsw-alias-state-error-primary);outline:none}._2dHfjG_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}._2dHfjG_invalid{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}@media (prefers-reduced-motion:reduce){._2dHfjG_card,._2dHfjG_header,._2dHfjG_chevron,._2dHfjG_chevronOpen,._2dHfjG_reset,._2dHfjG_discard,._2dHfjG_save{transition:none}}";
657
+ const tagId = "@linxin666/dsh-pet/settings-card.module.css";
658
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
659
+ const tag = document.createElement("style");
660
+ tag.dataset.plugin = "@linxin666/dsh-pet";
661
+ tag.dataset.pluginCss = tagId;
662
+ tag.textContent = css;
663
+ document.head.appendChild(tag);
664
+ }
665
+ var settings_card_module_css_default = {
666
+ "badge": "_2dHfjG_badge",
667
+ "badges": "_2dHfjG_badges",
668
+ "body": "_2dHfjG_body",
669
+ "card": "_2dHfjG_card",
670
+ "cardOpen": "_2dHfjG_cardOpen",
671
+ "chevron": "_2dHfjG_chevron",
672
+ "chevronOpen": "_2dHfjG_chevronOpen",
673
+ "description": "_2dHfjG_description",
674
+ "discard": "_2dHfjG_discard",
675
+ "failed": "_2dHfjG_failed",
676
+ "field": "_2dHfjG_field",
677
+ "footer": "_2dHfjG_footer",
678
+ "head": "_2dHfjG_head",
679
+ "headText": "_2dHfjG_headText",
680
+ "header": "_2dHfjG_header",
681
+ "hint": "_2dHfjG_hint",
682
+ "input": "_2dHfjG_input",
683
+ "inputInvalid": "_2dHfjG_inputInvalid",
684
+ "invalid": "_2dHfjG_invalid",
685
+ "label": "_2dHfjG_label",
686
+ "name": "_2dHfjG_name",
687
+ "pending": "_2dHfjG_pending",
688
+ "readOnly": "_2dHfjG_readOnly",
689
+ "reset": "_2dHfjG_reset",
690
+ "save": "_2dHfjG_save",
691
+ "select": "_2dHfjG_select"
692
+ };
693
+ //#endregion
694
+ //#region src/client/PluginSettingsCard.tsx
695
+ /**
696
+ * Shared chrome for the plugin settings card: a disclosure header naming the
697
+ * plugin and what its settings govern, the controls inside, and the save that
698
+ * writes them. Renders nothing while the namespace is unavailable — a
699
+ * deployment that does not compose the owning plugin should show no trace of
700
+ * it. Mirrors the official ui-plugin-config PluginCard in a self-contained
701
+ * slice (this package must not depend on a sibling UI package).
702
+ */
703
+ /**
704
+ * Render one plugin settings card.
705
+ * @param props - the plugin's copy keys, its form state, and its controls.
706
+ * @returns the card, or nothing when the namespace is unavailable.
707
+ */
708
+ function PluginSettingsCard(props) {
709
+ const [open, setOpen] = (0, react.useState)(false);
710
+ const { state } = props;
711
+ if (!state.available) return null;
712
+ const title = props.t(props.titleKey);
713
+ const blocked = !state.dirty || state.invalid || state.saving;
714
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
715
+ className: settings_card_module_css_default.card,
716
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
717
+ type: "button",
718
+ className: settings_card_module_css_default.header,
719
+ "aria-expanded": open,
720
+ "aria-label": `${props.t(open ? "settings.collapse" : "settings.expand")}: ${title}`,
721
+ onClick: () => {
722
+ setOpen(!open);
723
+ },
724
+ children: [
725
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
726
+ className: settings_card_module_css_default.headText,
727
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
728
+ className: settings_card_module_css_default.name,
729
+ children: title
730
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
731
+ className: settings_card_module_css_default.description,
732
+ children: props.t(props.descriptionKey)
733
+ })]
734
+ }),
735
+ state.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
736
+ className: settings_card_module_css_default.pending,
737
+ children: props.t("settings.unsaved")
738
+ }) : null,
739
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
740
+ className: open ? settings_card_module_css_default.chevronOpen : settings_card_module_css_default.chevron,
741
+ children: "▾"
742
+ })
743
+ ]
744
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
745
+ className: settings_card_module_css_default.body,
746
+ children: [
747
+ !state.writable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
748
+ className: settings_card_module_css_default.readOnly,
749
+ role: "status",
750
+ children: props.t("settings.readOnly")
751
+ }) : null,
752
+ props.children,
753
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
754
+ className: settings_card_module_css_default.footer,
755
+ children: [
756
+ state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
757
+ className: settings_card_module_css_default.failed,
758
+ role: "status",
759
+ children: props.t("settings.saveFailed")
760
+ }) : null,
761
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
762
+ type: "button",
763
+ className: settings_card_module_css_default.discard,
764
+ disabled: !state.dirty || state.saving,
765
+ onClick: props.onDiscard,
766
+ children: props.t("settings.discard")
767
+ }),
768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
769
+ type: "button",
770
+ className: settings_card_module_css_default.save,
771
+ disabled: blocked,
772
+ onClick: props.onSave,
773
+ children: props.t(!state.saving ? "settings.save" : "settings.saving")
774
+ })
775
+ ]
776
+ })
777
+ ]
778
+ }) : null]
779
+ });
780
+ }
781
+ /** A staged value field. `numeric` only hints the keypad: which drafts a field accepts is decided by its spec. */
782
+ function ValueField(props) {
783
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
784
+ className: settings_card_module_css_default.field,
785
+ children: [
786
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
787
+ className: settings_card_module_css_default.head,
788
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
789
+ className: settings_card_module_css_default.label,
790
+ htmlFor: props.id,
791
+ children: props.label
792
+ }), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
793
+ className: settings_card_module_css_default.badges,
794
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
795
+ className: settings_card_module_css_default.badge,
796
+ children: props.overriddenLabel
797
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
798
+ type: "button",
799
+ className: settings_card_module_css_default.reset,
800
+ disabled: props.disabled,
801
+ onClick: props.onReset,
802
+ children: props.resetLabel
803
+ })]
804
+ }) : null]
805
+ }),
806
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
807
+ id: props.id,
808
+ className: props.invalid ? settings_card_module_css_default.inputInvalid : settings_card_module_css_default.input,
809
+ type: "text",
810
+ ...props.numeric === true ? { inputMode: "numeric" } : {},
811
+ ...props.invalid ? { "aria-invalid": true } : {},
812
+ value: props.text,
813
+ placeholder: props.placeholder ?? "",
814
+ disabled: props.disabled,
815
+ onChange: (event) => {
816
+ props.onEdit(event.target.value);
817
+ }
818
+ }),
819
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
820
+ className: props.invalid ? settings_card_module_css_default.invalid : settings_card_module_css_default.hint,
821
+ children: props.invalid ? props.invalidLabel : props.hint
822
+ })
823
+ ]
824
+ });
825
+ }
826
+ /** A staged boolean field: 继承 / 开 / 关. */
827
+ function BooleanField(props) {
828
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
829
+ className: settings_card_module_css_default.field,
830
+ children: [
831
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
832
+ className: settings_card_module_css_default.head,
833
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
834
+ className: settings_card_module_css_default.label,
835
+ htmlFor: props.id,
836
+ children: props.label
837
+ }), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
838
+ className: settings_card_module_css_default.badges,
839
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
840
+ className: settings_card_module_css_default.badge,
841
+ children: props.overriddenLabel
842
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
843
+ type: "button",
844
+ className: settings_card_module_css_default.reset,
845
+ disabled: props.disabled,
846
+ onClick: props.onReset,
847
+ children: props.resetLabel
848
+ })]
849
+ }) : null]
850
+ }),
851
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
852
+ id: props.id,
853
+ className: settings_card_module_css_default.select,
854
+ value: props.text,
855
+ disabled: props.disabled,
856
+ onChange: (event) => {
857
+ props.onEdit(event.target.value);
858
+ },
859
+ children: [
860
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
861
+ value: "",
862
+ children: props.inheritLabel
863
+ }),
864
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
865
+ value: "true",
866
+ children: props.onLabel
867
+ }),
868
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
869
+ value: "false",
870
+ children: props.offLabel
871
+ })
872
+ ]
873
+ }),
874
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
875
+ className: settings_card_module_css_default.hint,
876
+ children: props.hint
877
+ })
878
+ ]
879
+ });
880
+ }
881
+ //#endregion
882
+ //#region src/client/settings-form.ts
883
+ /** A whole-number field. An empty draft clears the field; any other draft that is not a finite number blocks the save. */
884
+ function numberField(field) {
885
+ return {
886
+ field,
887
+ format: (value) => typeof value === "number" ? String(value) : "",
888
+ parse: (text) => {
889
+ const trimmed = text.trim();
890
+ if (trimmed === "") return { kind: "clear" };
891
+ const parsed = Number(trimmed);
892
+ return Number.isFinite(parsed) ? {
893
+ kind: "set",
894
+ value: parsed
895
+ } : void 0;
896
+ }
897
+ };
898
+ }
899
+ /** A free-text field. An empty draft clears the field. */
900
+ function textField(field) {
901
+ return {
902
+ field,
903
+ format: (value) => typeof value === "string" ? value : "",
904
+ parse: (text) => {
905
+ const trimmed = text.trim();
906
+ return trimmed === "" ? { kind: "clear" } : {
907
+ kind: "set",
908
+ value: trimmed
909
+ };
910
+ }
911
+ };
912
+ }
913
+ /** A boolean field, edited through true/false draft text. */
914
+ function booleanField(field) {
915
+ return {
916
+ field,
917
+ format: (value) => typeof value === "boolean" ? String(value) : "",
918
+ parse: (text) => {
919
+ if (text === "true") return {
920
+ kind: "set",
921
+ value: true
922
+ };
923
+ if (text === "false") return {
924
+ kind: "set",
925
+ value: false
926
+ };
927
+ }
928
+ };
929
+ }
930
+ /**
931
+ * Stages one card's edits over one settings namespace and writes them on save.
932
+ *
933
+ * The Host is the only authority on whether a value was accepted — its
934
+ * validators own the constraints no schema can express — so the outcome is
935
+ * read back from the section rather than predicted here. A save that did not
936
+ * land keeps its drafts, so the user can correct them instead of retyping.
937
+ */
938
+ var CardForm = class {
939
+ scope;
940
+ specs;
941
+ staged = /* @__PURE__ */ new Map();
942
+ listeners = /* @__PURE__ */ new Set();
943
+ saving = false;
944
+ failed = false;
945
+ /** @param scope - the bound settings scope for this card's namespace. */
946
+ constructor(scope, specs) {
947
+ this.scope = scope;
948
+ this.specs = new Map(specs.map((spec) => [spec.field, spec]));
949
+ scope.subscribe(() => {
950
+ this.publish();
951
+ });
952
+ }
953
+ /** Publish a projection of this form, rebuilt whenever the scope or a draft changes. */
954
+ bind(project) {
955
+ const store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(project());
956
+ this.listeners.add(() => {
957
+ store.set(project());
958
+ });
959
+ return store;
960
+ }
961
+ /** Read the card-level state: what the Host serves, and what a save would do. */
962
+ shell() {
963
+ const snapshot = this.scope.getSnapshot();
964
+ const plan = this.plan();
965
+ return {
966
+ available: snapshot.status === "ready",
967
+ writable: snapshot.writable,
968
+ dirty: plan.length > 0,
969
+ invalid: plan.some((item) => item.run === void 0),
970
+ saving: this.saving,
971
+ failed: this.failed
972
+ };
973
+ }
974
+ /** Read one field's state from the effective section and its staged draft. */
975
+ field(field) {
976
+ const spec = this.specOf(field);
977
+ const staged = this.staged.get(field);
978
+ if (staged === void 0) return {
979
+ text: spec.format(this.sectionValue(field)),
980
+ overridden: this.stored(field),
981
+ invalid: false
982
+ };
983
+ const write = staged.clear ? { kind: "clear" } : spec.parse(staged.text);
984
+ return {
985
+ text: staged.text,
986
+ overridden: write?.kind === "set",
987
+ invalid: write === void 0
988
+ };
989
+ }
990
+ /** The actions the card's slot registration injects. */
991
+ actions() {
992
+ return {
993
+ edit: (field, text) => {
994
+ this.stage(field, {
995
+ text,
996
+ clear: false
997
+ });
998
+ },
999
+ resetField: (field) => {
1000
+ this.stage(field, {
1001
+ text: this.specOf(field).format(this.baseValue(field)),
1002
+ clear: true
1003
+ });
1004
+ },
1005
+ save: () => {
1006
+ this.save();
1007
+ },
1008
+ discard: () => {
1009
+ if (this.staged.size === 0 && !this.failed) return;
1010
+ this.staged.clear();
1011
+ this.failed = false;
1012
+ this.publish();
1013
+ }
1014
+ };
1015
+ }
1016
+ /**
1017
+ * Write every staged edit, then re-seed from what the Host accepted.
1018
+ * @returns settlement after every write and the read-back.
1019
+ */
1020
+ async save() {
1021
+ const plan = this.plan();
1022
+ const writes = plan.flatMap((item) => item.run === void 0 ? [] : [item.run]);
1023
+ if (plan.length === 0 || this.saving || writes.length !== plan.length) return;
1024
+ this.saving = true;
1025
+ this.failed = false;
1026
+ this.publish();
1027
+ let landed = true;
1028
+ for (const write of writes) landed = await write() && landed;
1029
+ if (landed) this.staged.clear();
1030
+ this.saving = false;
1031
+ this.failed = !landed;
1032
+ this.publish();
1033
+ }
1034
+ /**
1035
+ * Every staged edit a save would write. An entry whose draft is not a value
1036
+ * its field accepts carries no write: the form is still dirty, and the save
1037
+ * refuses rather than dropping the edit. A staged edit that matches the
1038
+ * effective section is not a write at all.
1039
+ * @returns the planned writes, in the order the fields were staged.
1040
+ */
1041
+ plan() {
1042
+ const plan = [];
1043
+ for (const [field, staged] of this.staged) {
1044
+ const spec = this.specOf(field);
1045
+ if (staged.clear) {
1046
+ if (this.stored(field)) plan.push({
1047
+ field,
1048
+ run: () => this.clear(field)
1049
+ });
1050
+ continue;
1051
+ }
1052
+ if (staged.text === spec.format(this.sectionValue(field))) continue;
1053
+ const write = spec.parse(staged.text);
1054
+ if (write === void 0) plan.push({
1055
+ field,
1056
+ run: void 0
1057
+ });
1058
+ else if (write.kind === "clear") plan.push({
1059
+ field,
1060
+ run: () => this.clear(field)
1061
+ });
1062
+ else plan.push({
1063
+ field,
1064
+ run: () => this.store(field, write.value)
1065
+ });
1066
+ }
1067
+ return plan;
1068
+ }
1069
+ async clear(field) {
1070
+ await this.scope.unset(field);
1071
+ return !this.stored(field);
1072
+ }
1073
+ async store(field, value) {
1074
+ await this.scope.set(field, value);
1075
+ return this.userLayer()?.[field] === value;
1076
+ }
1077
+ stage(field, edit) {
1078
+ this.staged.set(field, edit);
1079
+ this.failed = false;
1080
+ this.publish();
1081
+ }
1082
+ specOf(field) {
1083
+ const spec = this.specs.get(field);
1084
+ if (spec === void 0) throw new Error(`settings card has no field ${field}`);
1085
+ return spec;
1086
+ }
1087
+ snapshotOf() {
1088
+ return this.scope.getSnapshot();
1089
+ }
1090
+ sectionValue(field) {
1091
+ return this.snapshotOf().value?.[field];
1092
+ }
1093
+ baseValue(field) {
1094
+ return this.snapshotOf().base?.[field];
1095
+ }
1096
+ userLayer() {
1097
+ return this.snapshotOf().user;
1098
+ }
1099
+ stored(field) {
1100
+ const user = this.userLayer();
1101
+ return user !== void 0 && Object.hasOwn(user, field);
1102
+ }
1103
+ publish() {
1104
+ for (const listener of this.listeners) listener();
1105
+ }
1106
+ };
1107
+ //#endregion
1108
+ //#region src/client/PetSettingsCard.tsx
1109
+ /** Bridges the `pet` scope onto the card's staged form. */
1110
+ var PetSettingsCardController = class {
1111
+ form;
1112
+ store;
1113
+ /** @param scope - the bound settings scope for the `pet` namespace. */
1114
+ constructor(scope) {
1115
+ this.form = new CardForm(scope, [
1116
+ booleanField("enabled"),
1117
+ booleanField("visible"),
1118
+ numberField("size"),
1119
+ numberField("right"),
1120
+ numberField("bottom"),
1121
+ textField("name")
1122
+ ]);
1123
+ this.store = this.form.bind(() => this.projection());
1124
+ }
1125
+ projection() {
1126
+ return {
1127
+ ...this.form.shell(),
1128
+ enabled: this.form.field("enabled"),
1129
+ visible: this.form.field("visible"),
1130
+ size: this.form.field("size"),
1131
+ right: this.form.field("right"),
1132
+ bottom: this.form.field("bottom"),
1133
+ name: this.form.field("name")
1134
+ };
1135
+ }
1136
+ /**
1137
+ * Build the face the card's slot registration injects.
1138
+ * @returns the card's snapshot and its form actions.
1139
+ */
1140
+ inject() {
1141
+ return {
1142
+ hooks: { petSettingsCard: this.store },
1143
+ ...this.form.actions()
1144
+ };
1145
+ }
1146
+ };
1147
+ /**
1148
+ * Render the pet settings card.
1149
+ * @param props - locale copy, the card snapshot, and its form actions.
1150
+ * @returns the card.
1151
+ */
1152
+ function PetSettingsCard(props) {
1153
+ const { t } = props;
1154
+ const state = props.usePetSettingsCard((snapshot) => snapshot);
1155
+ const disabled = !state.writable;
1156
+ const fieldProps = {
1157
+ overriddenLabel: t("settings.overridden"),
1158
+ resetLabel: t("settings.reset"),
1159
+ invalidLabel: t("settings.invalidNumber"),
1160
+ disabled
1161
+ };
1162
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(PluginSettingsCard, {
1163
+ t,
1164
+ titleKey: "settings.title",
1165
+ descriptionKey: "settings.description",
1166
+ state,
1167
+ onSave: props.save,
1168
+ onDiscard: props.discard,
1169
+ children: [
1170
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
1171
+ id: "settings-pet-enabled",
1172
+ label: t("settings.enabled"),
1173
+ hint: t("settings.enabledHint"),
1174
+ inheritLabel: t("settings.inherit"),
1175
+ onLabel: t("settings.on"),
1176
+ offLabel: t("settings.off"),
1177
+ ...fieldProps,
1178
+ ...state.enabled,
1179
+ onEdit: (text) => {
1180
+ props.edit("enabled", text);
1181
+ },
1182
+ onReset: () => {
1183
+ props.resetField("enabled");
1184
+ }
1185
+ }),
1186
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BooleanField, {
1187
+ id: "settings-pet-visible",
1188
+ label: t("settings.visible"),
1189
+ hint: t("settings.visibleHint"),
1190
+ inheritLabel: t("settings.inherit"),
1191
+ onLabel: t("settings.on"),
1192
+ offLabel: t("settings.off"),
1193
+ ...fieldProps,
1194
+ ...state.visible,
1195
+ onEdit: (text) => {
1196
+ props.edit("visible", text);
1197
+ },
1198
+ onReset: () => {
1199
+ props.resetField("visible");
1200
+ }
1201
+ }),
1202
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
1203
+ id: "settings-pet-size",
1204
+ label: t("settings.size"),
1205
+ hint: t("settings.sizeHint"),
1206
+ numeric: true,
1207
+ ...fieldProps,
1208
+ ...state.size,
1209
+ onEdit: (text) => {
1210
+ props.edit("size", text);
1211
+ },
1212
+ onReset: () => {
1213
+ props.resetField("size");
1214
+ }
1215
+ }),
1216
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
1217
+ id: "settings-pet-right",
1218
+ label: t("settings.right"),
1219
+ hint: t("settings.rightHint"),
1220
+ numeric: true,
1221
+ ...fieldProps,
1222
+ ...state.right,
1223
+ onEdit: (text) => {
1224
+ props.edit("right", text);
1225
+ },
1226
+ onReset: () => {
1227
+ props.resetField("right");
1228
+ }
1229
+ }),
1230
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
1231
+ id: "settings-pet-bottom",
1232
+ label: t("settings.bottom"),
1233
+ hint: t("settings.bottomHint"),
1234
+ numeric: true,
1235
+ ...fieldProps,
1236
+ ...state.bottom,
1237
+ onEdit: (text) => {
1238
+ props.edit("bottom", text);
1239
+ },
1240
+ onReset: () => {
1241
+ props.resetField("bottom");
1242
+ }
1243
+ }),
1244
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ValueField, {
1245
+ id: "settings-pet-name",
1246
+ label: t("settings.name"),
1247
+ hint: t("settings.nameHint"),
1248
+ ...fieldProps,
1249
+ ...state.name,
1250
+ onEdit: (text) => {
1251
+ props.edit("name", text);
1252
+ },
1253
+ onReset: () => {
1254
+ props.resetField("name");
1255
+ }
1256
+ })
1257
+ ]
1258
+ });
1259
+ }
1260
+ //#endregion
1261
+ //#region src/client/locales.ts
1262
+ /** Chinese copy. */
1263
+ const zh = {
1264
+ "pet.feed": "喂食",
1265
+ "pet.hide": "隐藏",
1266
+ "pet.rename": "改名",
1267
+ "pet.confirm": "确定",
1268
+ "pet.namePlaceholder": "输入新名字",
1269
+ "pet.summon": "召唤{name}",
1270
+ "pet.rank": "亲密度 {rank}",
1271
+ "pet.points": "{points} 点",
1272
+ "pet.treats": "小鱼干 ×{n}",
1273
+ "pet.state.loading": "鲸鱼娘正在赶来…",
1274
+ "pet.state.error": "鲸鱼娘迷路了(连接失败)",
1275
+ "settings.title": "宠物",
1276
+ "settings.description": "鲸鱼娘的显示布局与名字。",
1277
+ "settings.enabled": "启用宠物",
1278
+ "settings.enabledHint": "关闭后隐藏宠物并停止轮询,可在设置里重新启用。",
1279
+ "settings.visible": "显示宠物",
1280
+ "settings.visibleHint": "关闭后宠物隐藏,可从聊天输入区重新召唤。",
1281
+ "settings.size": "大小(px)",
1282
+ "settings.sizeHint": "精灵单元高度,范围 32–512。",
1283
+ "settings.right": "距右侧(px)",
1284
+ "settings.rightHint": "距视口右边缘的水平内缩距离。",
1285
+ "settings.bottom": "距底部(px)",
1286
+ "settings.bottomHint": "距视口底边的垂直内缩距离。",
1287
+ "settings.name": "名字",
1288
+ "settings.nameHint": "宠物显示名,1–20 个字符。",
1289
+ "settings.inherit": "继承",
1290
+ "settings.on": "开",
1291
+ "settings.off": "关",
1292
+ "settings.overridden": "已覆盖",
1293
+ "settings.reset": "恢复默认",
1294
+ "settings.readOnly": "当前部署的设置只读。",
1295
+ "settings.expand": "展开设置",
1296
+ "settings.collapse": "收起设置",
1297
+ "settings.save": "保存",
1298
+ "settings.saving": "保存中…",
1299
+ "settings.discard": "放弃",
1300
+ "settings.unsaved": "未保存",
1301
+ "settings.saveFailed": "部署未接受这些值,已保留供你修改。",
1302
+ "settings.invalidNumber": "请输入数字,留空则使用默认值。"
1303
+ };
1304
+ /** English copy. */
1305
+ const en = {
1306
+ "pet.feed": "Feed",
1307
+ "pet.hide": "Hide",
1308
+ "pet.rename": "Rename",
1309
+ "pet.confirm": "OK",
1310
+ "pet.namePlaceholder": "Enter a new name",
1311
+ "pet.summon": "Summon {name}",
1312
+ "pet.rank": "Affinity {rank}",
1313
+ "pet.points": "{points} pts",
1314
+ "pet.treats": "Treats ×{n}",
1315
+ "pet.state.loading": "The whale girl is on her way…",
1316
+ "pet.state.error": "The whale girl is lost (connection failed)",
1317
+ "settings.title": "Pet",
1318
+ "settings.description": "The whale girl’s display layout and name.",
1319
+ "settings.enabled": "Enable the pet",
1320
+ "settings.enabledHint": "When off, the pet hides and polling stops; re-enable it here.",
1321
+ "settings.visible": "Show the pet",
1322
+ "settings.visibleHint": "When off, the pet hides; summon it again from the input row.",
1323
+ "settings.size": "Size (px)",
1324
+ "settings.sizeHint": "Sprite cell height, 32–512.",
1325
+ "settings.right": "Right inset (px)",
1326
+ "settings.rightHint": "Horizontal inset from the viewport right edge.",
1327
+ "settings.bottom": "Bottom inset (px)",
1328
+ "settings.bottomHint": "Vertical inset from the viewport bottom edge.",
1329
+ "settings.name": "Name",
1330
+ "settings.nameHint": "The pet’s display name, 1–20 characters.",
1331
+ "settings.inherit": "Inherit",
1332
+ "settings.on": "On",
1333
+ "settings.off": "Off",
1334
+ "settings.overridden": "Overridden",
1335
+ "settings.reset": "Reset to default",
1336
+ "settings.readOnly": "This deployment stores settings read-only.",
1337
+ "settings.expand": "Show settings",
1338
+ "settings.collapse": "Hide settings",
1339
+ "settings.save": "Save",
1340
+ "settings.saving": "Saving…",
1341
+ "settings.discard": "Discard",
1342
+ "settings.unsaved": "Unsaved",
1343
+ "settings.saveFailed": "The deployment did not accept these values; they were left for you to correct.",
1344
+ "settings.invalidNumber": "Enter a number, or leave blank to use the default."
1345
+ };
1346
+ //#endregion
1347
+ //#region src/client/index.ts
1348
+ /** Same-origin JSON fetch helper (GET without body, POST with JSON body). */
1349
+ async function petFetch(path, body) {
1350
+ const response = await fetch(path, body === void 0 ? {} : {
1351
+ method: "POST",
1352
+ headers: { "content-type": "application/json" },
1353
+ body: JSON.stringify(body)
1354
+ });
1355
+ if (!response.ok) throw new Error(`pet ${path} failed: ${response.status}`);
1356
+ return await response.json();
1357
+ }
1358
+ /** The live host API instance (always defined; failures surface per call). */
1359
+ const petApi = {
1360
+ state: () => petFetch("/api/pet/state"),
1361
+ interact: (kind) => petFetch("/api/pet/interact", { kind }),
1362
+ setVisible: (visible) => petFetch("/api/pet/set-visible", { visible }),
1363
+ setConfig: (patch) => petFetch("/api/pet/set-config", patch),
1364
+ setName: (name) => petFetch("/api/pet/set-name", { name })
1365
+ };
1366
+ /** Poll interval for the host snapshot. */
1367
+ const POLL_MS = 800;
1368
+ /** Settings namespace the pet settings card edits (the Host plugin registers it). */
1369
+ const PET_SETTINGS_NS = "pet";
1370
+ /** Required services. */
1371
+ const inject = [
1372
+ "slots",
1373
+ "locale",
1374
+ "connection",
1375
+ "settingsScope",
1376
+ "remote"
1377
+ ];
1378
+ /**
1379
+ * Client plugin body: register dictionaries, mount the dock entry and poll
1380
+ * loop while the plugin is enabled, and seat the settings card in the Web UI
1381
+ * plugin group.
1382
+ * @param ctx - client root context.
1383
+ */
1384
+ function apply(ctx) {
1385
+ ctx.effect(() => ctx.locale.register("pet", {
1386
+ zh,
1387
+ en
1388
+ }), "pet: dictionaries");
1389
+ const settingsScope = ctx.settingsScope.bind({ namespace: PET_SETTINGS_NS });
1390
+ const enabled = () => {
1391
+ const snapshot = settingsScope.getSnapshot();
1392
+ return snapshot.status === "ready" ? snapshot.value?.enabled ?? true : snapshot.status === "unavailable";
1393
+ };
1394
+ const petSettings = new PetSettingsCardController(settingsScope);
1395
+ ctx.slots.inject("web-ui.plugin.item", () => ctx.slots.register({
1396
+ name: "web-ui.plugin.item",
1397
+ id: "pet-settings",
1398
+ order: 140,
1399
+ locale: "pet",
1400
+ inject: () => petSettings.inject()
1401
+ }, PetSettingsCard));
1402
+ let disposeUi;
1403
+ const syncUi = () => {
1404
+ if (enabled() && disposeUi === void 0) {
1405
+ const petStore = createPetStore().create();
1406
+ const setSnapshot = petStore.actions.setSnapshot;
1407
+ const setState = petStore.actions.setState;
1408
+ const setFeedback = petStore.actions.setFeedback;
1409
+ const pollNow = () => {
1410
+ petApi.state().then((snapshot) => {
1411
+ setSnapshot(snapshot);
1412
+ }, () => {
1413
+ setState("error", "pet.state transport error");
1414
+ });
1415
+ };
1416
+ const disposePoll = ctx.effect(() => {
1417
+ let timer;
1418
+ const stop = () => {
1419
+ if (timer !== void 0) {
1420
+ window.clearInterval(timer);
1421
+ timer = void 0;
1422
+ }
1423
+ };
1424
+ const start = () => {
1425
+ if (timer === void 0 && document.visibilityState === "visible") timer = window.setInterval(pollNow, POLL_MS);
1426
+ };
1427
+ const onVisibility = () => {
1428
+ if (document.visibilityState === "visible") {
1429
+ pollNow();
1430
+ start();
1431
+ } else stop();
1432
+ };
1433
+ start();
1434
+ document.addEventListener("visibilitychange", onVisibility);
1435
+ return () => {
1436
+ stop();
1437
+ document.removeEventListener("visibilitychange", onVisibility);
1438
+ };
1439
+ }, "pet: poll");
1440
+ const injected = () => ({
1441
+ store: petStore,
1442
+ ensure: pollNow,
1443
+ pet: () => {
1444
+ petApi.interact("pet").then((result) => {
1445
+ setFeedback({
1446
+ text: result.reaction,
1447
+ kind: "pet",
1448
+ at: Date.now()
1449
+ });
1450
+ }, () => {});
1451
+ },
1452
+ feed: () => {
1453
+ petApi.interact("feed").then((result) => {
1454
+ setFeedback({
1455
+ text: result.reaction,
1456
+ kind: "feed",
1457
+ at: Date.now()
1458
+ });
1459
+ }, () => {});
1460
+ },
1461
+ hide: () => {
1462
+ petApi.setVisible(false).then(() => {
1463
+ pollNow();
1464
+ }, () => {});
1465
+ },
1466
+ summon: () => {
1467
+ petApi.setVisible(true).then(() => {
1468
+ pollNow();
1469
+ }, () => {});
1470
+ },
1471
+ dragEnd: (right, bottom) => {
1472
+ petApi.setConfig({
1473
+ right,
1474
+ bottom
1475
+ }).then(() => {
1476
+ pollNow();
1477
+ }, () => {});
1478
+ },
1479
+ rename: (name) => {
1480
+ petApi.setName(name).then((result) => {
1481
+ if (result.ok) pollNow();
1482
+ }, () => {});
1483
+ },
1484
+ feedbackDone: () => {
1485
+ setFeedback(null);
1486
+ }
1487
+ });
1488
+ const disposeDock = ctx.slots.inject("conversation.input.selector.context", () => ctx.slots.register({
1489
+ name: "conversation.input.selector.context",
1490
+ id: "pet",
1491
+ order: 110,
1492
+ inject: injected,
1493
+ locale: "pet"
1494
+ }, PetDockEntry));
1495
+ disposeUi = () => {
1496
+ disposeDock();
1497
+ disposePoll();
1498
+ disposeUi = void 0;
1499
+ };
1500
+ } else if (!enabled() && disposeUi !== void 0) {
1501
+ disposeUi();
1502
+ disposeUi = void 0;
1503
+ }
1504
+ };
1505
+ settingsScope.subscribe(syncUi);
1506
+ syncUi();
1507
+ }
1508
+ //#endregion
1509
+ exports.apply = apply;
1510
+ exports.inject = inject;
1511
+ return module.exports;
1512
+ }
1513
+ });
1514
+
1515
+ //# sourceMappingURL=client.js.map