@linxin666/dsh-pet 0.1.20 → 0.2.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 (85) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +18 -11
  3. package/README.zh.md +18 -11
  4. package/assets/whale/pet.json +9 -0
  5. package/lib/client.js +394 -58
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +1007 -43
  8. package/lib/invariant.js +1 -1
  9. package/lib/{state-Diiq8vjk.js → state-DrMX22GL.js} +86 -28
  10. package/lib/types/affinity.d.ts +4 -0
  11. package/lib/types/affinity.d.ts.map +1 -1
  12. package/lib/types/affinity.js +32 -6
  13. package/lib/types/chatter.d.ts +111 -0
  14. package/lib/types/chatter.d.ts.map +1 -0
  15. package/lib/types/chatter.js +756 -0
  16. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  17. package/lib/types/client/PetDockEntry.js +1 -1
  18. package/lib/types/client/PetSprite.d.ts.map +1 -1
  19. package/lib/types/client/PetSprite.js +133 -16
  20. package/lib/types/client/PluginSettingsCard.d.ts +28 -0
  21. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  22. package/lib/types/client/PluginSettingsCard.js +147 -6
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/client/index.js +1 -0
  25. package/lib/types/client/locales.d.ts +4 -0
  26. package/lib/types/client/locales.d.ts.map +1 -1
  27. package/lib/types/client/locales.js +4 -0
  28. package/lib/types/client/sequences.d.ts +10 -0
  29. package/lib/types/client/sequences.d.ts.map +1 -0
  30. package/lib/types/client/sequences.js +20 -0
  31. package/lib/types/event-projection.d.ts +14 -1
  32. package/lib/types/event-projection.d.ts.map +1 -1
  33. package/lib/types/event-projection.js +38 -17
  34. package/lib/types/ledger.d.ts.map +1 -1
  35. package/lib/types/ledger.js +15 -7
  36. package/lib/types/loopback.d.ts +25 -0
  37. package/lib/types/loopback.d.ts.map +1 -0
  38. package/lib/types/loopback.js +68 -0
  39. package/lib/types/persist.d.ts.map +1 -1
  40. package/lib/types/persist.js +3 -1
  41. package/lib/types/registry.d.ts +7 -1
  42. package/lib/types/registry.d.ts.map +1 -1
  43. package/lib/types/registry.js +47 -3
  44. package/lib/types/remarks.d.ts +2 -0
  45. package/lib/types/remarks.d.ts.map +1 -1
  46. package/lib/types/remarks.js +35 -0
  47. package/lib/types/routes.d.ts.map +1 -1
  48. package/lib/types/routes.js +14 -0
  49. package/lib/types/service.d.ts +6 -0
  50. package/lib/types/service.d.ts.map +1 -1
  51. package/lib/types/service.js +15 -2
  52. package/lib/types/state.d.ts +7 -4
  53. package/lib/types/state.d.ts.map +1 -1
  54. package/lib/types/state.js +20 -20
  55. package/package.json +9 -9
  56. package/src/affinity.test.ts +4 -2
  57. package/src/affinity.ts +37 -6
  58. package/src/chatter.test.ts +154 -0
  59. package/src/chatter.ts +787 -0
  60. package/src/client/PetDockEntry.test.tsx +93 -0
  61. package/src/client/PetDockEntry.tsx +1 -0
  62. package/src/client/PetSprite.test.tsx +253 -3
  63. package/src/client/PetSprite.tsx +192 -37
  64. package/src/client/PluginSettingsCard.tsx +229 -18
  65. package/src/client/index.test.tsx +95 -0
  66. package/src/client/index.ts +1 -0
  67. package/src/client/locales.ts +4 -0
  68. package/src/client/pet-css.test.ts +54 -0
  69. package/src/client/pet.module.css +215 -57
  70. package/src/client/sequences.test.ts +33 -0
  71. package/src/client/sequences.ts +33 -0
  72. package/src/client/settings-card.module.css +90 -1
  73. package/src/event-projection.ts +49 -16
  74. package/src/ledger.test.ts +42 -1
  75. package/src/ledger.ts +15 -7
  76. package/src/loopback.ts +63 -0
  77. package/src/persist.test.ts +18 -1
  78. package/src/persist.ts +3 -1
  79. package/src/registry.test.ts +79 -4
  80. package/src/registry.ts +58 -6
  81. package/src/remarks.ts +36 -0
  82. package/src/routes.ts +11 -0
  83. package/src/service.ts +27 -2
  84. package/src/state.test.ts +13 -0
  85. package/src/state.ts +24 -18
package/lib/client.js CHANGED
@@ -63,6 +63,26 @@ window.__ModuleLoader__.load({
63
63
  }
64
64
  //#endregion
65
65
  //#region src/state.ts
66
+ /**
67
+ * Map one activity phase onto the animation contract.
68
+ * - thinking → `running` and tool → `running-right` (focused work).
69
+ * - review → `review` while answer text is streaming.
70
+ * - waiting → `waiting` (expectant pose, needs user input).
71
+ * - done → `jumping` (celebration), then back to `idle` after the window.
72
+ * - failed → `failed` briefly, then back to `idle`.
73
+ * - idle → `idle` (calm breathing loop).
74
+ */
75
+ function animationForPhase(phase) {
76
+ switch (phase) {
77
+ case "thinking": return "running";
78
+ case "tool": return "running-right";
79
+ case "review": return "review";
80
+ case "waiting": return "waiting";
81
+ case "done": return "jumping";
82
+ case "failed": return "failed";
83
+ case "idle": return "idle";
84
+ }
85
+ }
66
86
  /** The spritesheet row index for one animation track. */
67
87
  function rowOf(animation) {
68
88
  return {
@@ -120,8 +140,32 @@ window.__ModuleLoader__.load({
120
140
  };
121
141
  }
122
142
  //#endregion
143
+ //#region src/client/sequences.ts
144
+ /** Resolve the active track and frame after elapsed milliseconds of a looping sequence. */
145
+ function sequenceFrameAt(sequence, tracks, elapsedMs) {
146
+ const itemDurations = sequence.map((animation) => tracks[animation].durations.reduce((sum, value) => sum + value, 0));
147
+ const sequenceDuration = itemDurations.reduce((sum, value) => sum + value, 0);
148
+ let offset = Math.max(0, elapsedMs) % sequenceDuration;
149
+ let itemIndex = 0;
150
+ while (itemIndex < sequence.length - 1 && offset >= itemDurations[itemIndex]) {
151
+ offset -= itemDurations[itemIndex];
152
+ itemIndex += 1;
153
+ }
154
+ const animation = sequence[itemIndex];
155
+ const track = tracks[animation];
156
+ let frameIndex = 0;
157
+ while (frameIndex < track.frames.length - 1 && offset >= track.durations[frameIndex]) {
158
+ offset -= track.durations[frameIndex];
159
+ frameIndex += 1;
160
+ }
161
+ return {
162
+ animation,
163
+ frameIndex
164
+ };
165
+ }
166
+ //#endregion
123
167
  //#region \0dsh-css:packages/dsh-pet/src/client/pet.module.css.mjs
124
- const css$2 = ".kz2Bea_float{pointer-events:auto;-webkit-user-select:none;user-select:none;flex-direction:column;align-items:center;display:flex;position:fixed}.kz2Bea_sprite{image-rendering:auto;touch-action:none;position:relative}.kz2Bea_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 kz2Bea_pet-bubble-pop;position:absolute;bottom:100%;box-shadow:0 2px 8px #00000040}.kz2Bea_bubblePet{background:#f472b6eb}.kz2Bea_bubbleFeed{background:#38bdf8eb}.kz2Bea_bubbleStatus{text-overflow:ellipsis;background:#0f172ae6;border:1px solid #7dd3fc80;max-width:min(280px,100vw - 24px);animation:none;overflow:hidden}.kz2Bea_bubbleStack{pointer-events:auto;flex-direction:column;align-items:center;gap:4px;display:flex;position:absolute;bottom:100%}.kz2Bea_bubbleStack .kz2Bea_bubble{pointer-events:auto;margin-bottom:0;position:relative;bottom:auto}.kz2Bea_bubbleClickable{cursor:pointer;font:inherit;text-align:center;appearance:none;transition:filter .12s,box-shadow .12s}.kz2Bea_bubbleClickable:hover{filter:brightness(1.12)}.kz2Bea_bubbleClickable:focus-visible{outline:none;box-shadow:0 0 0 2px #38bdf8d9}@keyframes kz2Bea_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)}}.kz2Bea_panel{color:#e2e8f0;backdrop-filter:blur(6px);background:#0f172aeb;border:1px solid #94a3b859;border-radius:10px;flex-direction:column;gap:6px;min-width:132px;margin-right:8px;padding:8px 10px;font-size:12px;display:flex;position:absolute;bottom:0;right:100%;box-shadow:0 4px 16px #00000059}.kz2Bea_panel:after{content:\"\";width:14px;position:absolute;top:0;bottom:0;left:100%}.kz2Bea_rankRow{white-space:nowrap;justify-content:space-between;gap:10px;display:flex}.kz2Bea_nameCell{font-weight:600}.kz2Bea_renameRow{align-items:center;gap:6px;display:flex}.kz2Bea_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}.kz2Bea_nameInput:focus{border-color:#38bdf8;box-shadow:0 0 0 2px #38bdf873}.kz2Bea_actions{gap:6px;display:flex}.kz2Bea_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}.kz2Bea_action:hover{filter:brightness(1.08)}.kz2Bea_action:active{filter:brightness(.94)}.kz2Bea_action:focus-visible{outline:none;box-shadow:0 0 0 2px #38bdf8d9}.kz2Bea_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}.kz2Bea_summon:hover{color:#bae6fd;background:#0f172ae6;border-color:#7dd3fcf2}.kz2Bea_summon:active{color:#7dd3fc;border-color:#7dd3fccc}.kz2Bea_summon:focus-visible{outline:none;box-shadow:0 0 0 2px #38bdf8d9}@media (prefers-reduced-motion:reduce){.kz2Bea_bubble{opacity:1;animation:none}.kz2Bea_action,.kz2Bea_summon{transition:none}}";
168
+ const css$2 = ".kz2Bea_float{pointer-events:auto;-webkit-user-select:none;user-select:none;flex-direction:column;align-items:center;display:flex;position:fixed}.kz2Bea_sprite{image-rendering:auto;touch-action:none;position:relative}.kz2Bea_bubble{white-space:nowrap;color:#f4f7ff;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 kz2Bea_pet-bubble-pop;position:absolute;bottom:100%;box-shadow:0 2px 8px #00000040}.kz2Bea_bubblePet{background:#4d6bfef2}.kz2Bea_bubbleFeed{background:#0369a1f2}.kz2Bea_bubbleStatus{text-overflow:ellipsis;backdrop-filter:blur(8px);letter-spacing:.02em;background:linear-gradient(160deg,#131c36e6,#070b1af0);border:1px solid #7e98ff73;max-width:min(280px,100vw - 24px);padding:5px 12px;animation:.24s ease-out kz2Bea_pet-bubble-in;overflow:hidden;box-shadow:0 4px 12px #02061759,inset 0 1px #e2e8ff1a,0 0 12px #4d6bfe2e}.kz2Bea_bubbleWhisper{letter-spacing:.03em;animation:.32s cubic-bezier(.22,1,.36,1) kz2Bea_pet-whisper-in}.kz2Bea_bubbleWhisper:before{content:\"「\";opacity:.7;margin-right:2px}.kz2Bea_bubbleWhisper:after{content:\"」\";opacity:.7;margin-left:2px}.kz2Bea_bubbleStack{pointer-events:auto;flex-direction:column-reverse;align-items:center;gap:4px;display:flex;position:absolute;bottom:100%}.kz2Bea_bubbleAnchor{display:inline-flex;position:relative}.kz2Bea_bubbleMore{z-index:1;color:#eef2ff;cursor:pointer;background:linear-gradient(160deg,#4a68f5f2,#2f44b8f2);border:1px solid #7e98ff8c;border-radius:999px;justify-content:center;align-items:center;min-width:18px;height:18px;padding:0 5px;font-size:10px;font-weight:600;line-height:1;transition:filter .12s;display:inline-flex;position:absolute;top:-9px;right:-10px;box-shadow:0 2px 6px #02061766}.kz2Bea_bubbleMore:hover{filter:brightness(1.15)}.kz2Bea_bubbleMore:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}.kz2Bea_bubbleStack .kz2Bea_bubble{pointer-events:auto;margin-bottom:0;position:relative;bottom:auto}.kz2Bea_bubbleClickable{cursor:pointer;font:inherit;text-align:center;appearance:none;transition:filter .12s,box-shadow .12s}.kz2Bea_bubbleClickable:hover{filter:brightness(1.12)}.kz2Bea_bubbleClickable:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}@keyframes kz2Bea_pet-bubble-in{0%{opacity:0;transform:translateY(6px)scale(.96)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes kz2Bea_pet-whisper-in{0%{opacity:0;transform:translateY(8px)scale(.9)}60%{transform:translateY(-1px)scale(1.02)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes kz2Bea_pet-panel-in{0%{opacity:0;transform:translateY(4px)scale(.97)}to{opacity:1;transform:translateY(0)scale(1)}}@keyframes kz2Bea_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)}}.kz2Bea_panel{color:#e6ebf8;backdrop-filter:blur(10px);transform-origin:50% 0;background:linear-gradient(165deg,#131c36f2,#070b1af7);border:1px solid #7e98ff4d;border-radius:12px;flex-direction:column;gap:7px;min-width:148px;margin-top:8px;padding:10px 12px;font-size:12px;animation:.2s cubic-bezier(.22,1,.36,1) kz2Bea_pet-panel-in;display:flex;position:absolute;top:100%;box-shadow:0 8px 24px #02061773,0 0 0 1px #4d6bfe1a,inset 0 1px #e2e8ff14}.kz2Bea_panel:after{content:\"\";height:14px;position:absolute;bottom:100%;left:0;right:0}.kz2Bea_panelAbove{transform-origin:50% 100%;margin-top:0;margin-bottom:8px;top:auto;bottom:100%}.kz2Bea_panelAbove:after{top:100%;bottom:auto}.kz2Bea_rankRow{white-space:nowrap;justify-content:space-between;gap:10px;display:flex}.kz2Bea_nameCell{letter-spacing:.02em;background:linear-gradient(90deg,#a9c1ff,#6c8bff 60%,#4d6bfe);color:#0000;-webkit-background-clip:text;background-clip:text;font-weight:600}.kz2Bea_statRank{color:#9db4ff;font-weight:600}.kz2Bea_statTreats{color:#7dd3fc;font-weight:600}.kz2Bea_statPoints{color:#fcd34d;font-weight:600}.kz2Bea_renameRow{align-items:center;gap:6px;display:flex}.kz2Bea_nameInput{color:#e6ebf8;background:#131c36e6;border:1px solid #7e98ff80;border-radius:6px;outline:none;flex:1;min-width:0;padding:3px 6px;font-size:12px}.kz2Bea_nameInput:focus{border-color:#4d6bfe;box-shadow:0 0 0 2px #4d6bfe73}.kz2Bea_actions{gap:6px;display:flex}.kz2Bea_action{cursor:pointer;color:#fff;background:linear-gradient(#4a68f5,#3a55e0);border:none;border-radius:6px;flex:1;padding:4px 8px;font-size:12px;font-weight:600;transition:filter .12s,box-shadow .12s,transform .12s;box-shadow:0 2px 6px #4d6bfe4d}.kz2Bea_action:hover{filter:brightness(1.08);transform:translateY(-1px);box-shadow:0 4px 10px #4d6bfe66}.kz2Bea_action:active{filter:brightness(.94);transform:translateY(0);box-shadow:0 1px 4px #4d6bfe4d}.kz2Bea_action:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}.kz2Bea_summon{color:#8ea6ff;cursor:pointer;background:#070b1abf;border:1px dashed #7e98ff99;border-radius:999px;padding:2px 10px;font-size:11px;transition:border-color .12s,color .12s,background .12s,box-shadow .12s}.kz2Bea_summon:hover{color:#c3d3ff;background:#070b1ae6;border-color:#7e98fff2}.kz2Bea_summon:active{color:#8ea6ff;border-color:#7e98ffcc}.kz2Bea_summon:focus-visible{outline:none;box-shadow:0 0 0 2px #7e98ffe6}@media (prefers-reduced-motion:reduce){.kz2Bea_bubble,.kz2Bea_bubbleStatus,.kz2Bea_bubbleWhisper,.kz2Bea_panel{opacity:1;animation:none}.kz2Bea_action,.kz2Bea_summon,.kz2Bea_bubbleMore{transition:none}}";
125
169
  const tagId$2 = "@linxin666/dsh-pet/pet.module.css";
126
170
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
127
171
  const tag = document.createElement("style");
@@ -134,19 +178,29 @@ window.__ModuleLoader__.load({
134
178
  "action": "kz2Bea_action",
135
179
  "actions": "kz2Bea_actions",
136
180
  "bubble": "kz2Bea_bubble",
181
+ "bubbleAnchor": "kz2Bea_bubbleAnchor",
137
182
  "bubbleClickable": "kz2Bea_bubbleClickable",
138
183
  "bubbleFeed": "kz2Bea_bubbleFeed",
184
+ "bubbleMore": "kz2Bea_bubbleMore",
139
185
  "bubblePet": "kz2Bea_bubblePet",
140
186
  "bubbleStack": "kz2Bea_bubbleStack",
141
187
  "bubbleStatus": "kz2Bea_bubbleStatus",
188
+ "bubbleWhisper": "kz2Bea_bubbleWhisper",
142
189
  "float": "kz2Bea_float",
143
190
  "nameCell": "kz2Bea_nameCell",
144
191
  "nameInput": "kz2Bea_nameInput",
145
192
  "panel": "kz2Bea_panel",
193
+ "panelAbove": "kz2Bea_panelAbove",
194
+ "pet-bubble-in": "kz2Bea_pet-bubble-in",
146
195
  "pet-bubble-pop": "kz2Bea_pet-bubble-pop",
196
+ "pet-panel-in": "kz2Bea_pet-panel-in",
197
+ "pet-whisper-in": "kz2Bea_pet-whisper-in",
147
198
  "rankRow": "kz2Bea_rankRow",
148
199
  "renameRow": "kz2Bea_renameRow",
149
200
  "sprite": "kz2Bea_sprite",
201
+ "statPoints": "kz2Bea_statPoints",
202
+ "statRank": "kz2Bea_statRank",
203
+ "statTreats": "kz2Bea_statTreats",
150
204
  "summon": "kz2Bea_summon"
151
205
  };
152
206
  //#endregion
@@ -175,10 +229,17 @@ window.__ModuleLoader__.load({
175
229
  const { snapshot, definition, display, feedback } = props;
176
230
  const spriteRef = (0, react.useRef)(null);
177
231
  const floatRef = (0, react.useRef)(null);
232
+ const panelRef = (0, react.useRef)(null);
233
+ const bubbleRef = (0, react.useRef)(null);
178
234
  const [imageReady, setImageReady] = (0, react.useState)(false);
179
235
  const [hovered, setHovered] = (0, react.useState)(false);
236
+ const [stackPeek, setStackPeek] = (0, react.useState)(false);
237
+ const [stackPinned, setStackPinned] = (0, react.useState)(false);
180
238
  const [renaming, setRenaming] = (0, react.useState)(false);
239
+ const [panelAbove, setPanelAbove] = (0, react.useState)(false);
240
+ const [panelLift, setPanelLift] = (0, react.useState)(0);
181
241
  const [nameDraft, setNameDraft] = (0, react.useState)("");
242
+ const composingRef = (0, react.useRef)(false);
182
243
  const [dragPos, setDragPos] = (0, react.useState)(null);
183
244
  const dragRef = (0, react.useRef)(null);
184
245
  const hideTimerRef = (0, react.useRef)(null);
@@ -191,6 +252,7 @@ window.__ModuleLoader__.load({
191
252
  const columns = definition.columns;
192
253
  const rows = definition.rows;
193
254
  const tracks = definition.tracks;
255
+ const sequences = definition.sequences;
194
256
  (0, react.useEffect)(() => {
195
257
  let cancelled = false;
196
258
  const img = new Image();
@@ -204,22 +266,36 @@ window.__ModuleLoader__.load({
204
266
  };
205
267
  }, [definition.atlasUrl]);
206
268
  const spriteScale = display.size / cell.height;
269
+ const phase = snapshot?.phase ?? "idle";
207
270
  const animation = snapshot?.animation ?? "idle";
208
271
  const scaleRef = (0, react.useRef)(spriteScale);
209
272
  scaleRef.current = spriteScale;
210
273
  (0, react.useEffect)(() => {
211
274
  const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
212
- const row = rowOfTrack(animation);
213
- const track = trimTrack(tracks[animation], rows[row] ?? tracks[animation].frames.length);
275
+ const sequence = animation === animationForPhase(phase) ? sequences?.[phase] : void 0;
276
+ const leadAnimation = sequence?.[0] ?? animation;
277
+ const row = rowOfTrack(leadAnimation);
278
+ const track = trimTrack(tracks[leadAnimation], rows[row] ?? tracks[leadAnimation].frames.length);
214
279
  const leadCol = track.frames[0];
215
280
  const lead = framePosition(cell, columns, row, leadCol, scaleRef.current);
216
281
  if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition = lead.x + "px " + lead.y + "px";
217
282
  if (reduceMotion) return;
218
283
  let raf = 0;
219
284
  let last = performance.now();
285
+ let sequenceElapsed = 0;
220
286
  const tick = (ts) => {
221
287
  const delta = ts - last;
222
288
  last = ts;
289
+ if (sequence !== void 0) {
290
+ sequenceElapsed += delta;
291
+ const current = sequenceFrameAt(sequence, tracks, sequenceElapsed);
292
+ const currentRow = rowOfTrack(current.animation);
293
+ const col = trimTrack(tracks[current.animation], rows[currentRow] ?? tracks[current.animation].frames.length).frames[current.frameIndex];
294
+ const pos = framePosition(cell, columns, currentRow, col, scaleRef.current);
295
+ if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition = pos.x + "px " + pos.y + "px";
296
+ raf = requestAnimationFrame(tick);
297
+ return;
298
+ }
223
299
  const st = frameRef.current;
224
300
  if (st.track !== animation) {
225
301
  st.track = animation;
@@ -245,10 +321,12 @@ window.__ModuleLoader__.load({
245
321
  return () => cancelAnimationFrame(raf);
246
322
  }, [
247
323
  animation,
324
+ phase,
248
325
  cell,
249
326
  columns,
250
327
  rows,
251
- tracks
328
+ tracks,
329
+ sequences
252
330
  ]);
253
331
  const feedbackDoneRef = (0, react.useRef)(props.onFeedbackDone);
254
332
  feedbackDoneRef.current = props.onFeedbackDone;
@@ -264,6 +342,7 @@ window.__ModuleLoader__.load({
264
342
  hideTimerRef.current = null;
265
343
  }
266
344
  };
345
+ (0, react.useEffect)(() => () => clearHideTimer(), []);
267
346
  const onPointerDown = (e) => {
268
347
  e.preventDefault();
269
348
  e.target.setPointerCapture?.(e.pointerId);
@@ -304,8 +383,44 @@ window.__ModuleLoader__.load({
304
383
  const spriteWidth = Math.round(cell.width * spriteScale);
305
384
  const spriteHeight = Math.round(cell.height * spriteScale);
306
385
  const sessionBubbles = snapshot?.sessions ?? [];
386
+ const stackOpen = stackPeek || stackPinned;
387
+ const visibleSessions = !stackOpen && sessionBubbles.length > 1 ? sessionBubbles.slice(0, 1) : sessionBubbles;
307
388
  const statusBubble = feedback === null && sessionBubbles.length === 0 ? snapshot?.bubble : void 0;
389
+ const whisper = feedback === null ? snapshot?.whisper : void 0;
390
+ const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== void 0 || whisper !== void 0;
308
391
  const displayName = snapshot?.name ?? definition.displayName;
392
+ (0, react.useEffect)(() => {
393
+ if (sessionBubbles.length <= 1) setStackPinned(false);
394
+ }, [sessionBubbles.length]);
395
+ (0, react.useLayoutEffect)(() => {
396
+ if (!hovered) {
397
+ setPanelAbove(false);
398
+ setPanelLift(0);
399
+ return;
400
+ }
401
+ const updatePanelPlacement = () => {
402
+ const sprite = spriteRef.current;
403
+ const panel = panelRef.current;
404
+ if (sprite === null || panel === null) return;
405
+ const above = window.innerHeight - sprite.getBoundingClientRect().bottom < panel.getBoundingClientRect().height + 8;
406
+ setPanelAbove(above);
407
+ const bubbleHeight = above ? bubbleRef.current?.getBoundingClientRect().height ?? 0 : 0;
408
+ setPanelLift(bubbleHeight > 0 ? Math.ceil(bubbleHeight) + 14 : 0);
409
+ };
410
+ updatePanelPlacement();
411
+ window.addEventListener("resize", updatePanelPlacement);
412
+ return () => window.removeEventListener("resize", updatePanelPlacement);
413
+ }, [
414
+ hovered,
415
+ renaming,
416
+ pos.right,
417
+ pos.bottom,
418
+ display.size,
419
+ bubblePresent,
420
+ sessionBubbles.length,
421
+ stackOpen,
422
+ feedback
423
+ ]);
309
424
  return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
310
425
  ref: floatRef,
311
426
  className: pet_module_css_default.float,
@@ -321,6 +436,7 @@ window.__ModuleLoader__.load({
321
436
  onPointerLeave: (e) => {
322
437
  const next = e.relatedTarget;
323
438
  if (next instanceof Node && floatRef.current?.contains(next)) return;
439
+ if (renaming) return;
324
440
  clearHideTimer();
325
441
  hideTimerRef.current = window.setTimeout(() => setHovered(false), 300);
326
442
  },
@@ -332,7 +448,7 @@ window.__ModuleLoader__.load({
332
448
  width: spriteWidth,
333
449
  height: spriteHeight,
334
450
  backgroundImage: imageReady ? "url(" + definition.atlasUrl + ")" : void 0,
335
- backgroundSize: cell.width * columns * spriteScale + "px " + cell.height * rows.length * spriteScale + "px",
451
+ backgroundSize: cell.width * columns * spriteScale + "px " + cell.height * (definition.atlasRows ?? rows.length) * spriteScale + "px",
336
452
  backgroundRepeat: "no-repeat",
337
453
  backgroundPosition: "0 0",
338
454
  cursor: dragRef.current === null ? "grab" : "grabbing"
@@ -348,29 +464,54 @@ window.__ModuleLoader__.load({
348
464
  "aria-label": definition.displayName
349
465
  }),
350
466
  feedback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
467
+ ref: bubbleRef,
351
468
  className: clsx(pet_module_css_default.bubble, feedback.kind === "feed" ? pet_module_css_default.bubbleFeed : pet_module_css_default.bubblePet),
352
469
  children: feedback.text
353
470
  }, feedback.at),
354
- feedback === null && sessionBubbles.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
471
+ feedback === null && (sessionBubbles.length > 0 || statusBubble !== void 0 || whisper !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
472
+ ref: bubbleRef,
355
473
  className: pet_module_css_default.bubbleStack,
356
- children: sessionBubbles.map((session) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
357
- type: "button",
358
- className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, pet_module_css_default.bubbleClickable),
359
- title: props.t("pet.openSessionHint"),
360
- onClick: () => {
361
- props.onOpenSession(session.sessionId);
362
- },
363
- children: session.bubble
364
- }, session.sessionId))
365
- }),
366
- statusBubble !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
367
- className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus),
368
- role: "status",
369
- "aria-live": "polite",
370
- children: statusBubble
474
+ onPointerEnter: () => setStackPeek(true),
475
+ onPointerLeave: () => setStackPeek(false),
476
+ children: [visibleSessions.map((session, index) => {
477
+ const speaksWhisper = index === 0 && whisper !== void 0;
478
+ const bubble = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
479
+ type: "button",
480
+ className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, pet_module_css_default.bubbleClickable, speaksWhisper && pet_module_css_default.bubbleWhisper),
481
+ title: props.t("pet.openSessionHint"),
482
+ onClick: () => {
483
+ props.onOpenSession(session.sessionId);
484
+ },
485
+ children: speaksWhisper ? whisper : session.bubble
486
+ }, speaksWhisper ? "whisper:" + whisper : session.sessionId);
487
+ if (index !== 0 || sessionBubbles.length <= 1) return bubble;
488
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
489
+ className: pet_module_css_default.bubbleAnchor,
490
+ children: [bubble, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
491
+ type: "button",
492
+ className: pet_module_css_default.bubbleMore,
493
+ title: stackOpen ? props.t("pet.collapseSessions") : props.t("pet.moreSessions", { n: sessionBubbles.length - 1 }),
494
+ "aria-label": stackOpen ? props.t("pet.collapseSessions") : props.t("pet.moreSessions", { n: sessionBubbles.length - 1 }),
495
+ "aria-expanded": stackOpen,
496
+ onClick: (e) => {
497
+ e.stopPropagation();
498
+ setStackPinned((open) => !open);
499
+ },
500
+ children: stackOpen ? "×" : "+" + String(sessionBubbles.length - 1)
501
+ })]
502
+ }, "primary");
503
+ }), sessionBubbles.length === 0 && (statusBubble !== void 0 || whisper !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
504
+ className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, whisper !== void 0 && pet_module_css_default.bubbleWhisper),
505
+ role: "status",
506
+ "aria-live": "polite",
507
+ children: whisper ?? statusBubble
508
+ }, whisper === void 0 ? "status" : "whisper:" + whisper)]
371
509
  }),
372
510
  hovered && dragRef.current === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
373
- className: pet_module_css_default.panel,
511
+ ref: panelRef,
512
+ className: clsx(pet_module_css_default.panel, panelAbove && pet_module_css_default.panelAbove),
513
+ "data-placement": panelAbove ? "above" : "below",
514
+ style: panelAbove && panelLift > 0 ? { marginBottom: panelLift } : void 0,
374
515
  onPointerEnter: () => {
375
516
  clearHideTimer();
376
517
  },
@@ -383,8 +524,14 @@ window.__ModuleLoader__.load({
383
524
  placeholder: props.t("pet.namePlaceholder"),
384
525
  autoFocus: true,
385
526
  onChange: (e) => setNameDraft(e.target.value),
527
+ onCompositionStart: () => {
528
+ composingRef.current = true;
529
+ },
530
+ onCompositionEnd: () => {
531
+ composingRef.current = false;
532
+ },
386
533
  onKeyDown: (e) => {
387
- if (e.nativeEvent.isComposing) return;
534
+ if (composingRef.current || e.nativeEvent.isComposing || e.key === "Process") return;
388
535
  if (e.key === "Enter") {
389
536
  const trimmed = nameDraft.trim();
390
537
  if (trimmed !== "") {
@@ -411,11 +558,20 @@ window.__ModuleLoader__.load({
411
558
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
412
559
  className: pet_module_css_default.nameCell,
413
560
  children: displayName
414
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: props.t("pet.rank", { rank: snapshot?.affinity.rank ?? "?" }) })]
561
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
562
+ className: pet_module_css_default.statRank,
563
+ children: props.t("pet.rank", { rank: snapshot?.affinity.rank ?? "?" })
564
+ })]
415
565
  }),
416
566
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
417
567
  className: pet_module_css_default.rankRow,
418
- 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 }) })]
568
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
569
+ className: pet_module_css_default.statTreats,
570
+ children: props.t("pet.treats", { n: snapshot?.treats.stocked ?? 0 })
571
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
572
+ className: pet_module_css_default.statPoints,
573
+ children: props.t("pet.points", { points: snapshot?.affinity.points ?? 0 })
574
+ })]
419
575
  }),
420
576
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
421
577
  className: pet_module_css_default.actions,
@@ -430,6 +586,7 @@ window.__ModuleLoader__.load({
430
586
  type: "button",
431
587
  className: pet_module_css_default.action,
432
588
  onClick: () => {
589
+ clearHideTimer();
433
590
  setNameDraft(displayName);
434
591
  setRenaming(true);
435
592
  },
@@ -515,12 +672,13 @@ window.__ModuleLoader__.load({
515
672
  },
516
673
  onClick: props.summon,
517
674
  "data-testid": "pet-summon",
675
+ "data-dsh-part": "summon-button",
518
676
  children: props.t("pet.summon", { name: snapshot?.name ?? "" })
519
677
  });
520
678
  }
521
679
  //#endregion
522
680
  //#region \0dsh-css:packages/dsh-pet/src/client/settings-card.module.css.mjs
523
- const css$1 = ".kKk9aW_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.kKk9aW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.kKk9aW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.kKk9aW_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.kKk9aW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.kKk9aW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.kKk9aW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.kKk9aW_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_chevronOpen{transform:rotate(180deg)}.kKk9aW_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.kKk9aW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.kKk9aW_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.kKk9aW_discard,.kKk9aW_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.kKk9aW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.kKk9aW_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.kKk9aW_discard:disabled,.kKk9aW_save:disabled{opacity:.4;cursor:default}.kKk9aW_discard:focus-visible,.kKk9aW_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.kKk9aW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.kKk9aW_field+.kKk9aW_field{border-top:1px solid var(--dsw-alias-border-l2)}.kKk9aW_head{align-items:center;gap:8px;display:flex}.kKk9aW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.kKk9aW_badges{align-items:center;gap:8px;display:inline-flex}.kKk9aW_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.kKk9aW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.kKk9aW_reset:disabled{cursor:default}.kKk9aW_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.kKk9aW_input,.kKk9aW_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_input:focus-visible,.kKk9aW_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.kKk9aW_input:disabled,.kKk9aW_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.kKk9aW_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.kKk9aW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.kKk9aW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.kKk9aW_card,.kKk9aW_header,.kKk9aW_chevron,.kKk9aW_chevronOpen,.kKk9aW_discard,.kKk9aW_save{transition:none}}";
681
+ const css$1 = ".kKk9aW_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}.kKk9aW_card:hover{border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}.kKk9aW_header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.kKk9aW_headerStatic{border-radius:12px;align-items:center;gap:12px;width:100%;padding:14px 16px;display:flex}.kKk9aW_headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.kKk9aW_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}.kKk9aW_description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.kKk9aW_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_chevronOpen{transform:rotate(180deg)}.kKk9aW_body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}.kKk9aW_readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_notExposed{color:var(--dsw-alias-state-warn-primary);margin:12px 0 0;font-size:12px;line-height:1.5}.kKk9aW_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}.kKk9aW_failed{min-width:0;color:var(--dsw-alias-label-error);text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-size:12px;line-height:1.5;overflow:hidden}.kKk9aW_discard,.kKk9aW_save{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}.kKk9aW_discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}.kKk9aW_discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}.kKk9aW_save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.kKk9aW_discard:disabled,.kKk9aW_save:disabled{opacity:.4;cursor:default}.kKk9aW_discard:focus-visible,.kKk9aW_save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.kKk9aW_field{flex-direction:column;gap:6px;padding:12px 0;display:flex}.kKk9aW_field+.kKk9aW_field{border-top:1px solid var(--dsw-alias-border-l2)}.kKk9aW_head{align-items:center;gap:8px;display:flex}.kKk9aW_label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}.kKk9aW_badges{align-items:center;gap:8px;display:inline-flex}.kKk9aW_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}.kKk9aW_reset{font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px;line-height:1.5}.kKk9aW_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.kKk9aW_reset:disabled{cursor:default}.kKk9aW_reset:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.kKk9aW_input,.kKk9aW_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_input:focus-visible,.kKk9aW_select:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.kKk9aW_input:disabled,.kKk9aW_select:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}.kKk9aW_inputInvalid{border:1px solid var(--dsw-alias-label-error);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}.kKk9aW_inputInvalid:focus-visible{outline:2px solid var(--dsw-alias-label-error);outline-offset:1px;border-color:var(--dsw-alias-label-error)}.kKk9aW_selectWrap{position:relative}.kKk9aW_selectButton{appearance:none;text-align:left;cursor:pointer;justify-content:space-between;align-items:center;gap:8px;width:100%;display:flex}.kKk9aW_selectLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.kKk9aW_selectChevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}.kKk9aW_selectChevronOpen{transform:rotate(180deg)}.kKk9aW_selectPopup{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);max-height:240px;box-shadow:0 8px 24px var(--dsw-alias-bg-mask-2);opacity:0;border-radius:8px;flex-direction:column;padding:4px;transition:opacity .1s,transform .1s;display:flex;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;transform:translateY(-4px)}.kKk9aW_selectPopupOpen{opacity:1;transform:none}.kKk9aW_selectPopupClose{opacity:0;pointer-events:none;transform:translateY(-4px)}.kKk9aW_selectOption{color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-overflow:ellipsis;border-radius:6px;padding:6px 10px;font-size:13px;line-height:1.5;overflow:hidden}.kKk9aW_selectOption:hover,.kKk9aW_selectOptionActive{background:var(--dsw-alias-interactive-bg-hover)}.kKk9aW_selectOptionSelected{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);font-weight:500}.kKk9aW_invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}.kKk9aW_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}@media (prefers-reduced-motion:reduce){.kKk9aW_card,.kKk9aW_header,.kKk9aW_chevron,.kKk9aW_chevronOpen,.kKk9aW_discard,.kKk9aW_save,.kKk9aW_selectChevron,.kKk9aW_selectChevronOpen,.kKk9aW_selectPopup{transition:none}}";
524
682
  const tagId$1 = "@linxin666/dsh-pet/settings-card.module.css";
525
683
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
526
684
  const tag = document.createElement("style");
@@ -557,7 +715,18 @@ window.__ModuleLoader__.load({
557
715
  "readOnly": "kKk9aW_readOnly",
558
716
  "reset": "kKk9aW_reset",
559
717
  "save": "kKk9aW_save",
560
- "select": "kKk9aW_select"
718
+ "select": "kKk9aW_select",
719
+ "selectButton": "kKk9aW_selectButton",
720
+ "selectChevron": "kKk9aW_selectChevron",
721
+ "selectChevronOpen": "kKk9aW_selectChevronOpen",
722
+ "selectLabel": "kKk9aW_selectLabel",
723
+ "selectOption": "kKk9aW_selectOption",
724
+ "selectOptionActive": "kKk9aW_selectOptionActive",
725
+ "selectOptionSelected": "kKk9aW_selectOptionSelected",
726
+ "selectPopup": "kKk9aW_selectPopup",
727
+ "selectPopupClose": "kKk9aW_selectPopupClose",
728
+ "selectPopupOpen": "kKk9aW_selectPopupOpen",
729
+ "selectWrap": "kKk9aW_selectWrap"
561
730
  };
562
731
  //#endregion
563
732
  //#region src/client/PluginSettingsCard.tsx
@@ -663,7 +832,7 @@ window.__ModuleLoader__.load({
663
832
  children: props.t("settings.readOnly")
664
833
  }) : null,
665
834
  props.children,
666
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
835
+ props.hideFooter === true ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
667
836
  className: settings_card_module_css_default.footer,
668
837
  children: [
669
838
  state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
@@ -736,6 +905,175 @@ window.__ModuleLoader__.load({
736
905
  ]
737
906
  });
738
907
  }
908
+ const NON_SKIN_BODY_MARKERS = /* @__PURE__ */ new Set(["dshSkinCenter", "dshSidebarCollapsed"]);
909
+ function isSkinActive() {
910
+ return Object.keys(document.body.dataset).some((key) => key.startsWith("dsh") && !NON_SKIN_BODY_MARKERS.has(key));
911
+ }
912
+ const SELECT_CLOSE_MS = 100;
913
+ /**
914
+ * The shared dual-mode select control. While an appearance skin is active it
915
+ * renders the legacy native `<select>` untouched, so element-level skin
916
+ * selectors keep working; under the default appearance it renders a
917
+ * self-drawn `role="listbox"` popup whose open/close is transition-animated.
918
+ * Staged cards reach it through BooleanField/ChoiceField; immediate-apply
919
+ * editors (the side-card prefs) bind it directly through onEdit.
920
+ * 双模式下拉框:皮肤激活时用原生 select,默认外观用自绘动画弹层。
921
+ */
922
+ function SelectField(props) {
923
+ const { id, options, value } = props;
924
+ const [open, setOpen] = (0, react.useState)(false);
925
+ const [closing, setClosing] = (0, react.useState)(false);
926
+ const [phase, setPhase] = (0, react.useState)("initial");
927
+ const [activeIndex, setActiveIndex] = (0, react.useState)(0);
928
+ const closeTimer = (0, react.useRef)(void 0);
929
+ const wrapRef = (0, react.useRef)(null);
930
+ const popupRef = (0, react.useRef)(null);
931
+ const currentIndex = () => {
932
+ const index = options.findIndex((option) => option.value === value);
933
+ return index >= 0 ? index : 0;
934
+ };
935
+ const close = (0, react.useCallback)(() => {
936
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
937
+ setClosing(true);
938
+ closeTimer.current = setTimeout(() => {
939
+ setClosing(false);
940
+ setOpen(false);
941
+ }, SELECT_CLOSE_MS);
942
+ }, []);
943
+ const openPopup = () => {
944
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
945
+ setActiveIndex(currentIndex());
946
+ setPhase("initial");
947
+ setClosing(false);
948
+ setOpen(true);
949
+ };
950
+ const commit = (index) => {
951
+ const option = options[index];
952
+ if (option) props.onEdit(option.value);
953
+ close();
954
+ };
955
+ const onTriggerClick = () => {
956
+ if (props.disabled) return;
957
+ if (open && !closing) close();
958
+ else openPopup();
959
+ };
960
+ const onKeyDown = (event) => {
961
+ if (props.disabled) return;
962
+ const count = options.length;
963
+ switch (event.key) {
964
+ case "ArrowDown":
965
+ case "ArrowUp":
966
+ case "Enter":
967
+ case " ":
968
+ event.preventDefault();
969
+ if (!open) openPopup();
970
+ else if (!closing) if (event.key === "ArrowDown") setActiveIndex((index) => (index + 1) % count);
971
+ else if (event.key === "ArrowUp") setActiveIndex((index) => (index - 1 + count) % count);
972
+ else commit(activeIndex);
973
+ break;
974
+ case "Escape":
975
+ if (open) {
976
+ event.preventDefault();
977
+ event.stopPropagation();
978
+ close();
979
+ }
980
+ break;
981
+ case "Tab":
982
+ if (open) close();
983
+ break;
984
+ }
985
+ };
986
+ (0, react.useEffect)(() => () => {
987
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
988
+ }, []);
989
+ (0, react.useLayoutEffect)(() => {
990
+ if (open && !closing && phase === "initial") {
991
+ popupRef.current?.offsetHeight;
992
+ setPhase("open");
993
+ }
994
+ }, [
995
+ open,
996
+ closing,
997
+ phase
998
+ ]);
999
+ (0, react.useEffect)(() => {
1000
+ if (!open) return;
1001
+ const onPointerDown = (event) => {
1002
+ const target = event.target;
1003
+ if (target instanceof Node && !wrapRef.current?.contains(target)) close();
1004
+ };
1005
+ document.addEventListener("pointerdown", onPointerDown);
1006
+ return () => document.removeEventListener("pointerdown", onPointerDown);
1007
+ }, [open, close]);
1008
+ (0, react.useEffect)(() => {
1009
+ if (props.disabled && open) close();
1010
+ }, [
1011
+ props.disabled,
1012
+ open,
1013
+ close
1014
+ ]);
1015
+ if (isSkinActive()) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1016
+ id,
1017
+ className: settings_card_module_css_default.select,
1018
+ value,
1019
+ disabled: props.disabled,
1020
+ onChange: (event) => {
1021
+ props.onEdit(event.target.value);
1022
+ },
1023
+ children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1024
+ value: option.value,
1025
+ children: option.label
1026
+ }, option.value))
1027
+ });
1028
+ const label = options.find((option) => option.value === value)?.label ?? "";
1029
+ const popupClass = closing ? `${settings_card_module_css_default.selectPopup} ${settings_card_module_css_default.selectPopupClose}` : phase === "open" ? `${settings_card_module_css_default.selectPopup} ${settings_card_module_css_default.selectPopupOpen}` : settings_card_module_css_default.selectPopup;
1030
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1031
+ className: settings_card_module_css_default.selectWrap,
1032
+ ref: wrapRef,
1033
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1034
+ type: "button",
1035
+ id,
1036
+ className: `${settings_card_module_css_default.select} ${settings_card_module_css_default.selectButton}`,
1037
+ disabled: props.disabled,
1038
+ "aria-haspopup": "listbox",
1039
+ "aria-expanded": open,
1040
+ "aria-activedescendant": open ? `${id}-o${activeIndex}` : void 0,
1041
+ "aria-invalid": props.invalid || void 0,
1042
+ onClick: onTriggerClick,
1043
+ onKeyDown,
1044
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1045
+ className: settings_card_module_css_default.selectLabel,
1046
+ children: label
1047
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1048
+ width: "14",
1049
+ height: "14",
1050
+ viewBox: "0 0 14 14",
1051
+ fill: "none",
1052
+ xmlns: "http://www.w3.org/2000/svg",
1053
+ className: open ? `${settings_card_module_css_default.selectChevron} ${settings_card_module_css_default.selectChevronOpen}` : settings_card_module_css_default.selectChevron,
1054
+ "aria-hidden": "true",
1055
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1056
+ d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
1057
+ fill: "currentColor"
1058
+ })
1059
+ })]
1060
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1061
+ className: popupClass,
1062
+ role: "listbox",
1063
+ ref: popupRef,
1064
+ children: options.map((option, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1065
+ id: `${id}-o${index}`,
1066
+ role: "option",
1067
+ "aria-selected": option.value === value,
1068
+ className: `${settings_card_module_css_default.selectOption}${option.value === value ? ` ${settings_card_module_css_default.selectOptionSelected}` : ""}${index === activeIndex && !closing ? ` ${settings_card_module_css_default.selectOptionActive}` : ""}`,
1069
+ onClick: () => {
1070
+ commit(index);
1071
+ },
1072
+ children: option.label
1073
+ }, option.value))
1074
+ }) : null]
1075
+ });
1076
+ }
739
1077
  /** A staged boolean field: 继承 / 开 / 关. */
740
1078
  function BooleanField(props) {
741
1079
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -761,28 +1099,26 @@ window.__ModuleLoader__.load({
761
1099
  })]
762
1100
  }) : null]
763
1101
  }),
764
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1102
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
765
1103
  id: props.id,
766
- className: settings_card_module_css_default.select,
767
- value: props.text,
768
- disabled: props.disabled,
769
- onChange: (event) => {
770
- props.onEdit(event.target.value);
771
- },
772
- children: [
773
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1104
+ options: [
1105
+ {
774
1106
  value: "",
775
- children: props.inheritLabel
776
- }),
777
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1107
+ label: props.inheritLabel
1108
+ },
1109
+ {
778
1110
  value: "true",
779
- children: props.onLabel
780
- }),
781
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1111
+ label: props.onLabel
1112
+ },
1113
+ {
782
1114
  value: "false",
783
- children: props.offLabel
784
- })
785
- ]
1115
+ label: props.offLabel
1116
+ }
1117
+ ],
1118
+ value: props.text,
1119
+ disabled: props.disabled,
1120
+ invalid: props.invalid,
1121
+ onEdit: props.onEdit
786
1122
  }),
787
1123
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
788
1124
  className: settings_card_module_css_default.hint,
@@ -816,21 +1152,16 @@ window.__ModuleLoader__.load({
816
1152
  })]
817
1153
  }) : null]
818
1154
  }),
819
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1155
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
820
1156
  id: props.id,
821
- className: settings_card_module_css_default.select,
1157
+ options: [{
1158
+ value: "",
1159
+ label: props.inheritLabel
1160
+ }, ...props.choices],
822
1161
  value: props.text,
823
1162
  disabled: props.disabled,
824
- onChange: (event) => {
825
- props.onEdit(event.target.value);
826
- },
827
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
828
- value: "",
829
- children: props.inheritLabel
830
- }), props.choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
831
- value: choice.value,
832
- children: choice.label
833
- }, choice.value))]
1163
+ invalid: props.invalid,
1164
+ onEdit: props.onEdit
834
1165
  }),
835
1166
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
836
1167
  className: props.invalid ? settings_card_module_css_default.invalid : settings_card_module_css_default.hint,
@@ -1365,6 +1696,8 @@ window.__ModuleLoader__.load({
1365
1696
  "pet.state.loading": "宠物正在赶来…",
1366
1697
  "pet.state.error": "宠物迷路了(连接失败)",
1367
1698
  "pet.openSessionHint": "点击跳转到对应会话",
1699
+ "pet.moreSessions": "展开其余 {n} 个会话的气泡",
1700
+ "pet.collapseSessions": "收起会话气泡",
1368
1701
  "settings.title": "宠物",
1369
1702
  "settings.description": "选择宠物并调整它的显示布局。",
1370
1703
  "settings.pet": "宠物",
@@ -1409,6 +1742,8 @@ window.__ModuleLoader__.load({
1409
1742
  "pet.state.loading": "The pet is on its way…",
1410
1743
  "pet.state.error": "The pet is lost (connection failed)",
1411
1744
  "pet.openSessionHint": "Click to jump to this session",
1745
+ "pet.moreSessions": "Expand {n} more session bubbles",
1746
+ "pet.collapseSessions": "Collapse session bubbles",
1412
1747
  "settings.title": "Pet",
1413
1748
  "settings.description": "Pick a pet and tune its display layout.",
1414
1749
  "settings.pet": "Pet",
@@ -1632,6 +1967,7 @@ window.__ModuleLoader__.load({
1632
1967
  });
1633
1968
  const container = document.createElement("div");
1634
1969
  container.dataset.dshPetRoot = "";
1970
+ container.dataset.dshPlugin = "pet";
1635
1971
  document.body.appendChild(container);
1636
1972
  const petRoot = (0, react_dom_client.createRoot)(container);
1637
1973
  petRoot.render((0, react.createElement)(PetDockEntry, {