@linxin666/dsh-pet 0.1.19 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +19 -12
  3. package/README.zh.md +19 -12
  4. package/assets/whale/pet.json +9 -0
  5. package/lib/client.js +202 -38
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +897 -42
  8. package/lib/invariant.js +1 -1
  9. package/lib/{state-Diiq8vjk.js → state-Ch3f4luZ.js} +51 -23
  10. package/lib/types/affinity.d.ts +22 -18
  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 +666 -0
  16. package/lib/types/client/PetDockEntry.d.ts +2 -0
  17. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  18. package/lib/types/client/PetDockEntry.js +2 -2
  19. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  20. package/lib/types/client/PetSettingsCard.js +2 -2
  21. package/lib/types/client/PetSprite.d.ts +2 -0
  22. package/lib/types/client/PetSprite.d.ts.map +1 -1
  23. package/lib/types/client/PetSprite.js +100 -25
  24. package/lib/types/client/PluginSettingsCard.d.ts +7 -1
  25. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  26. package/lib/types/client/PluginSettingsCard.js +5 -3
  27. package/lib/types/client/index.d.ts +1 -1
  28. package/lib/types/client/index.d.ts.map +1 -1
  29. package/lib/types/client/index.js +31 -6
  30. package/lib/types/client/locales.d.ts +82 -80
  31. package/lib/types/client/locales.d.ts.map +1 -1
  32. package/lib/types/client/locales.js +2 -0
  33. package/lib/types/client/sequences.d.ts +10 -0
  34. package/lib/types/client/sequences.d.ts.map +1 -0
  35. package/lib/types/client/sequences.js +20 -0
  36. package/lib/types/client/settings-form.d.ts.map +1 -1
  37. package/lib/types/client/spritesheet.d.ts +1 -1
  38. package/lib/types/client/spritesheet.d.ts.map +1 -1
  39. package/lib/types/client/spritesheet.js +3 -12
  40. package/lib/types/event-projection.d.ts +14 -1
  41. package/lib/types/event-projection.d.ts.map +1 -1
  42. package/lib/types/event-projection.js +38 -17
  43. package/lib/types/index.d.ts +2 -1
  44. package/lib/types/index.d.ts.map +1 -1
  45. package/lib/types/index.js +15 -13
  46. package/lib/types/invariant.js +2 -2
  47. package/lib/types/ledger.d.ts.map +1 -1
  48. package/lib/types/ledger.js +18 -10
  49. package/lib/types/mount-once.d.ts +25 -0
  50. package/lib/types/mount-once.d.ts.map +1 -0
  51. package/lib/types/mount-once.js +42 -0
  52. package/lib/types/persist.d.ts.map +1 -1
  53. package/lib/types/persist.js +6 -4
  54. package/lib/types/registry.d.ts +7 -1
  55. package/lib/types/registry.d.ts.map +1 -1
  56. package/lib/types/registry.js +37 -1
  57. package/lib/types/remarks.d.ts +2 -0
  58. package/lib/types/remarks.d.ts.map +1 -1
  59. package/lib/types/remarks.js +5 -0
  60. package/lib/types/routes.js +2 -2
  61. package/lib/types/service.d.ts +16 -6
  62. package/lib/types/service.d.ts.map +1 -1
  63. package/lib/types/service.js +26 -8
  64. package/lib/types/state.d.ts +7 -4
  65. package/lib/types/state.d.ts.map +1 -1
  66. package/lib/types/state.js +20 -20
  67. package/package.json +9 -9
  68. package/src/affinity.test.ts +4 -2
  69. package/src/affinity.ts +37 -6
  70. package/src/chatter.test.ts +154 -0
  71. package/src/chatter.ts +697 -0
  72. package/src/client/PetDockEntry.tsx +3 -0
  73. package/src/client/PetSprite.test.tsx +246 -4
  74. package/src/client/PetSprite.tsx +132 -36
  75. package/src/client/PluginSettingsCard.tsx +10 -0
  76. package/src/client/index.ts +26 -3
  77. package/src/client/locales.ts +2 -0
  78. package/src/client/pet-css.test.ts +42 -0
  79. package/src/client/pet.module.css +173 -22
  80. package/src/client/sequences.test.ts +33 -0
  81. package/src/client/sequences.ts +33 -0
  82. package/src/client/spritesheet.ts +3 -13
  83. package/src/event-projection.ts +49 -16
  84. package/src/index.ts +4 -1
  85. package/src/ledger.test.ts +42 -1
  86. package/src/ledger.ts +15 -7
  87. package/src/mount-once.ts +48 -0
  88. package/src/persist.test.ts +18 -1
  89. package/src/persist.ts +3 -1
  90. package/src/registry.test.ts +50 -3
  91. package/src/registry.ts +47 -3
  92. package/src/remarks.ts +6 -0
  93. package/src/service.ts +42 -9
  94. package/src/state.test.ts +13 -0
  95. package/src/state.ts +24 -18
package/lib/client.js CHANGED
@@ -62,21 +62,55 @@ window.__ModuleLoader__.load({
62
62
  return n;
63
63
  }
64
64
  //#endregion
65
- //#region src/client/spritesheet.ts
66
- /** Row index of one animation track (the fixed 9-row contract). */
67
- function rowOfTrack(animation) {
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
+ }
86
+ /** The spritesheet row index for one animation track. */
87
+ function rowOf(animation) {
68
88
  return {
69
- idle: 0,
89
+ "idle": 0,
70
90
  "running-right": 1,
71
91
  "running-left": 2,
72
- waving: 3,
73
- jumping: 4,
74
- failed: 5,
75
- waiting: 6,
76
- running: 7,
77
- review: 8
92
+ "waving": 3,
93
+ "jumping": 4,
94
+ "failed": 5,
95
+ "waiting": 6,
96
+ "running": 7,
97
+ "review": 8
78
98
  }[animation];
79
99
  }
100
+ //#endregion
101
+ //#region src/client/spritesheet.ts
102
+ /**
103
+ * Spritesheet geometry helpers — parameterized by the pet definition the
104
+ * host serves over '/api/pet/pets', so the browser half renders any registry
105
+ * entry without per-pet code. The per-track tables (frames, durations, loop,
106
+ * fallback) also come from the registry; these helpers only place frames,
107
+ * guard track lengths, and map the fixed 9-row animation contract.
108
+ * @module @linxin666/dsh-pet/client/spritesheet
109
+ */
110
+ /** Row index of one animation track (the fixed 9-row contract). */
111
+ function rowOfTrack(animation) {
112
+ return rowOf(animation);
113
+ }
80
114
  /**
81
115
  * Background-position (px) of one frame cell within the scaled atlas.
82
116
  * The background image is scaled by `scale` (element size ÷ cell size), and
@@ -106,8 +140,32 @@ window.__ModuleLoader__.load({
106
140
  };
107
141
  }
108
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
109
167
  //#region \0dsh-css:packages/dsh-pet/src/client/pet.module.css.mjs
110
- 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:none;flex-direction:column;align-items:center;gap:4px;display:flex;position:absolute;bottom:100%}.kz2Bea_bubbleStack .kz2Bea_bubble{margin-bottom:0;position:relative;bottom:auto}@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;padding:8px 10px;font-size:12px;display:flex;position:absolute;bottom:100%;box-shadow:0 4px 16px #00000059}.kz2Bea_panel:after{content:\"\";height:14px;position:absolute;top:100%;left:0;right:0}.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:#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;backdrop-filter:blur(8px);letter-spacing:.02em;background:linear-gradient(160deg,#1e293be0,#0f172aeb);border:1px solid #7dd3fc8c;max-width:min(280px,100vw - 24px);animation:.24s ease-out kz2Bea_pet-bubble-in;overflow:hidden;box-shadow:0 4px 14px #02061766,0 0 0 1px #7dd3fc1f,0 0 18px #38bdf82e}.kz2Bea_bubbleWhisper{letter-spacing:.03em;white-space:nowrap;text-overflow:ellipsis;color:#f5d0fe;backdrop-filter:blur(8px);pointer-events:none;background:linear-gradient(150deg,#581c8785,#9d174d70);border:1px dashed #f0abfc8c;border-radius:999px;max-width:min(260px,100vw - 24px);padding:4px 12px;font-size:11.5px;font-style:italic;line-height:1.45;animation:.32s cubic-bezier(.22,1,.36,1) kz2Bea_pet-whisper-in;position:relative;overflow:hidden;box-shadow:0 3px 12px #2e106566,0 0 14px #d946ef29}.kz2Bea_bubbleWhisper:before{content:\"「\";opacity:.75;margin-right:2px}.kz2Bea_bubbleWhisper:after{content:\"」\";opacity:.75;margin-left:2px}.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-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:#e2e8f0;backdrop-filter:blur(10px);transform-origin:50% 0;background:linear-gradient(165deg,#1e293bf0,#0f172af5);border:1px solid #94a3b866;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 #7dd3fc14,inset 0 1px #e2e8f014}.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,#7dd3fc,#38bdf8 60%,#818cf8);color:#0000;-webkit-background-clip:text;background-clip:text;font-weight:600}.kz2Bea_statRank{color:#c4b5fd;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:#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,transform .12s;box-shadow:0 2px 6px #38bdf840}.kz2Bea_action:hover{filter:brightness(1.08);transform:translateY(-1px);box-shadow:0 4px 10px #38bdf859}.kz2Bea_action:active{filter:brightness(.94);transform:translateY(0);box-shadow:0 1px 4px #38bdf840}.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,.kz2Bea_bubbleStatus,.kz2Bea_bubbleWhisper,.kz2Bea_panel{opacity:1;animation:none}.kz2Bea_action,.kz2Bea_summon{transition:none}}";
111
169
  const tagId$2 = "@linxin666/dsh-pet/pet.module.css";
112
170
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
113
171
  const tag = document.createElement("style");
@@ -120,18 +178,27 @@ window.__ModuleLoader__.load({
120
178
  "action": "kz2Bea_action",
121
179
  "actions": "kz2Bea_actions",
122
180
  "bubble": "kz2Bea_bubble",
181
+ "bubbleClickable": "kz2Bea_bubbleClickable",
123
182
  "bubbleFeed": "kz2Bea_bubbleFeed",
124
183
  "bubblePet": "kz2Bea_bubblePet",
125
184
  "bubbleStack": "kz2Bea_bubbleStack",
126
185
  "bubbleStatus": "kz2Bea_bubbleStatus",
186
+ "bubbleWhisper": "kz2Bea_bubbleWhisper",
127
187
  "float": "kz2Bea_float",
128
188
  "nameCell": "kz2Bea_nameCell",
129
189
  "nameInput": "kz2Bea_nameInput",
130
190
  "panel": "kz2Bea_panel",
191
+ "panelAbove": "kz2Bea_panelAbove",
192
+ "pet-bubble-in": "kz2Bea_pet-bubble-in",
131
193
  "pet-bubble-pop": "kz2Bea_pet-bubble-pop",
194
+ "pet-panel-in": "kz2Bea_pet-panel-in",
195
+ "pet-whisper-in": "kz2Bea_pet-whisper-in",
132
196
  "rankRow": "kz2Bea_rankRow",
133
197
  "renameRow": "kz2Bea_renameRow",
134
198
  "sprite": "kz2Bea_sprite",
199
+ "statPoints": "kz2Bea_statPoints",
200
+ "statRank": "kz2Bea_statRank",
201
+ "statTreats": "kz2Bea_statTreats",
135
202
  "summon": "kz2Bea_summon"
136
203
  };
137
204
  //#endregion
@@ -160,10 +227,15 @@ window.__ModuleLoader__.load({
160
227
  const { snapshot, definition, display, feedback } = props;
161
228
  const spriteRef = (0, react.useRef)(null);
162
229
  const floatRef = (0, react.useRef)(null);
230
+ const panelRef = (0, react.useRef)(null);
231
+ const bubbleRef = (0, react.useRef)(null);
163
232
  const [imageReady, setImageReady] = (0, react.useState)(false);
164
233
  const [hovered, setHovered] = (0, react.useState)(false);
165
234
  const [renaming, setRenaming] = (0, react.useState)(false);
235
+ const [panelAbove, setPanelAbove] = (0, react.useState)(false);
236
+ const [panelLift, setPanelLift] = (0, react.useState)(0);
166
237
  const [nameDraft, setNameDraft] = (0, react.useState)("");
238
+ const composingRef = (0, react.useRef)(false);
167
239
  const [dragPos, setDragPos] = (0, react.useState)(null);
168
240
  const dragRef = (0, react.useRef)(null);
169
241
  const hideTimerRef = (0, react.useRef)(null);
@@ -176,6 +248,7 @@ window.__ModuleLoader__.load({
176
248
  const columns = definition.columns;
177
249
  const rows = definition.rows;
178
250
  const tracks = definition.tracks;
251
+ const sequences = definition.sequences;
179
252
  (0, react.useEffect)(() => {
180
253
  let cancelled = false;
181
254
  const img = new Image();
@@ -189,23 +262,36 @@ window.__ModuleLoader__.load({
189
262
  };
190
263
  }, [definition.atlasUrl]);
191
264
  const spriteScale = display.size / cell.height;
265
+ const phase = snapshot?.phase ?? "idle";
192
266
  const animation = snapshot?.animation ?? "idle";
193
267
  const scaleRef = (0, react.useRef)(spriteScale);
194
268
  scaleRef.current = spriteScale;
195
269
  (0, react.useEffect)(() => {
196
270
  const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
197
- const row = rowOfTrack(animation);
198
- const leadCol = trimTrack(tracks[animation], rows[row] ?? tracks[animation].frames.length).frames[0];
271
+ const sequence = animation === animationForPhase(phase) ? sequences?.[phase] : void 0;
272
+ const leadAnimation = sequence?.[0] ?? animation;
273
+ const row = rowOfTrack(leadAnimation);
274
+ const track = trimTrack(tracks[leadAnimation], rows[row] ?? tracks[leadAnimation].frames.length);
275
+ const leadCol = track.frames[0];
199
276
  const lead = framePosition(cell, columns, row, leadCol, scaleRef.current);
200
277
  if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition = lead.x + "px " + lead.y + "px";
201
278
  if (reduceMotion) return;
202
279
  let raf = 0;
203
280
  let last = performance.now();
281
+ let sequenceElapsed = 0;
204
282
  const tick = (ts) => {
205
283
  const delta = ts - last;
206
284
  last = ts;
207
- const row = rowOfTrack(animation);
208
- const track = trimTrack(tracks[animation], rows[row] ?? tracks[animation].frames.length);
285
+ if (sequence !== void 0) {
286
+ sequenceElapsed += delta;
287
+ const current = sequenceFrameAt(sequence, tracks, sequenceElapsed);
288
+ const currentRow = rowOfTrack(current.animation);
289
+ const col = trimTrack(tracks[current.animation], rows[currentRow] ?? tracks[current.animation].frames.length).frames[current.frameIndex];
290
+ const pos = framePosition(cell, columns, currentRow, col, scaleRef.current);
291
+ if (spriteRef.current !== null) spriteRef.current.style.backgroundPosition = pos.x + "px " + pos.y + "px";
292
+ raf = requestAnimationFrame(tick);
293
+ return;
294
+ }
209
295
  const st = frameRef.current;
210
296
  if (st.track !== animation) {
211
297
  st.track = animation;
@@ -231,10 +317,12 @@ window.__ModuleLoader__.load({
231
317
  return () => cancelAnimationFrame(raf);
232
318
  }, [
233
319
  animation,
320
+ phase,
234
321
  cell,
235
322
  columns,
236
323
  rows,
237
- tracks
324
+ tracks,
325
+ sequences
238
326
  ]);
239
327
  const feedbackDoneRef = (0, react.useRef)(props.onFeedbackDone);
240
328
  feedbackDoneRef.current = props.onFeedbackDone;
@@ -290,8 +378,38 @@ window.__ModuleLoader__.load({
290
378
  const spriteWidth = Math.round(cell.width * spriteScale);
291
379
  const spriteHeight = Math.round(cell.height * spriteScale);
292
380
  const sessionBubbles = snapshot?.sessions ?? [];
293
- const statusBubble = feedback === null && !hovered && sessionBubbles.length === 0 ? snapshot?.bubble : void 0;
381
+ const statusBubble = feedback === null && sessionBubbles.length === 0 ? snapshot?.bubble : void 0;
382
+ const whisper = feedback === null ? snapshot?.whisper : void 0;
383
+ const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== void 0 || whisper !== void 0;
294
384
  const displayName = snapshot?.name ?? definition.displayName;
385
+ (0, react.useLayoutEffect)(() => {
386
+ if (!hovered) {
387
+ setPanelAbove(false);
388
+ setPanelLift(0);
389
+ return;
390
+ }
391
+ const updatePanelPlacement = () => {
392
+ const sprite = spriteRef.current;
393
+ const panel = panelRef.current;
394
+ if (sprite === null || panel === null) return;
395
+ const above = window.innerHeight - sprite.getBoundingClientRect().bottom < panel.getBoundingClientRect().height + 8;
396
+ setPanelAbove(above);
397
+ const bubbleHeight = above ? bubbleRef.current?.getBoundingClientRect().height ?? 0 : 0;
398
+ setPanelLift(bubbleHeight > 0 ? Math.ceil(bubbleHeight) + 14 : 0);
399
+ };
400
+ updatePanelPlacement();
401
+ window.addEventListener("resize", updatePanelPlacement);
402
+ return () => window.removeEventListener("resize", updatePanelPlacement);
403
+ }, [
404
+ hovered,
405
+ renaming,
406
+ pos.right,
407
+ pos.bottom,
408
+ display.size,
409
+ bubblePresent,
410
+ sessionBubbles.length,
411
+ feedback
412
+ ]);
295
413
  return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
296
414
  ref: floatRef,
297
415
  className: pet_module_css_default.float,
@@ -307,6 +425,7 @@ window.__ModuleLoader__.load({
307
425
  onPointerLeave: (e) => {
308
426
  const next = e.relatedTarget;
309
427
  if (next instanceof Node && floatRef.current?.contains(next)) return;
428
+ if (renaming) return;
310
429
  clearHideTimer();
311
430
  hideTimerRef.current = window.setTimeout(() => setHovered(false), 300);
312
431
  },
@@ -318,7 +437,7 @@ window.__ModuleLoader__.load({
318
437
  width: spriteWidth,
319
438
  height: spriteHeight,
320
439
  backgroundImage: imageReady ? "url(" + definition.atlasUrl + ")" : void 0,
321
- backgroundSize: cell.width * columns * spriteScale + "px " + cell.height * rows.length * spriteScale + "px",
440
+ backgroundSize: cell.width * columns * spriteScale + "px " + cell.height * (definition.atlasRows ?? rows.length) * spriteScale + "px",
322
441
  backgroundRepeat: "no-repeat",
323
442
  backgroundPosition: "0 0",
324
443
  cursor: dragRef.current === null ? "grab" : "grabbing"
@@ -334,26 +453,40 @@ window.__ModuleLoader__.load({
334
453
  "aria-label": definition.displayName
335
454
  }),
336
455
  feedback !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
456
+ ref: bubbleRef,
337
457
  className: clsx(pet_module_css_default.bubble, feedback.kind === "feed" ? pet_module_css_default.bubbleFeed : pet_module_css_default.bubblePet),
338
458
  children: feedback.text
339
459
  }, feedback.at),
340
- feedback === null && !hovered && sessionBubbles.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
460
+ feedback === null && (sessionBubbles.length > 0 || statusBubble !== void 0 || whisper !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
461
+ ref: bubbleRef,
341
462
  className: pet_module_css_default.bubbleStack,
342
- role: "status",
343
- "aria-live": "polite",
344
- children: sessionBubbles.map((session) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
345
- className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus),
346
- children: session.bubble
347
- }, session.sessionId))
348
- }),
349
- statusBubble !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
350
- className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus),
351
- role: "status",
352
- "aria-live": "polite",
353
- children: statusBubble
463
+ children: [
464
+ sessionBubbles.map((session) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
465
+ type: "button",
466
+ className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus, pet_module_css_default.bubbleClickable),
467
+ title: props.t("pet.openSessionHint"),
468
+ onClick: () => {
469
+ props.onOpenSession(session.sessionId);
470
+ },
471
+ children: session.bubble
472
+ }, session.sessionId)),
473
+ sessionBubbles.length === 0 && statusBubble !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
474
+ className: clsx(pet_module_css_default.bubble, pet_module_css_default.bubbleStatus),
475
+ role: "status",
476
+ "aria-live": "polite",
477
+ children: statusBubble
478
+ }),
479
+ whisper !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
480
+ className: pet_module_css_default.bubbleWhisper,
481
+ children: whisper
482
+ }, whisper)
483
+ ]
354
484
  }),
355
485
  hovered && dragRef.current === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
356
- className: pet_module_css_default.panel,
486
+ ref: panelRef,
487
+ className: clsx(pet_module_css_default.panel, panelAbove && pet_module_css_default.panelAbove),
488
+ "data-placement": panelAbove ? "above" : "below",
489
+ style: panelAbove && panelLift > 0 ? { marginBottom: panelLift } : void 0,
357
490
  onPointerEnter: () => {
358
491
  clearHideTimer();
359
492
  },
@@ -366,8 +499,14 @@ window.__ModuleLoader__.load({
366
499
  placeholder: props.t("pet.namePlaceholder"),
367
500
  autoFocus: true,
368
501
  onChange: (e) => setNameDraft(e.target.value),
502
+ onCompositionStart: () => {
503
+ composingRef.current = true;
504
+ },
505
+ onCompositionEnd: () => {
506
+ composingRef.current = false;
507
+ },
369
508
  onKeyDown: (e) => {
370
- if (e.nativeEvent.isComposing) return;
509
+ if (composingRef.current || e.nativeEvent.isComposing || e.key === "Process") return;
371
510
  if (e.key === "Enter") {
372
511
  const trimmed = nameDraft.trim();
373
512
  if (trimmed !== "") {
@@ -394,11 +533,20 @@ window.__ModuleLoader__.load({
394
533
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
395
534
  className: pet_module_css_default.nameCell,
396
535
  children: displayName
397
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: props.t("pet.rank", { rank: snapshot?.affinity.rank ?? "?" }) })]
536
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
537
+ className: pet_module_css_default.statRank,
538
+ children: props.t("pet.rank", { rank: snapshot?.affinity.rank ?? "?" })
539
+ })]
398
540
  }),
399
541
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
400
542
  className: pet_module_css_default.rankRow,
401
- 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 }) })]
543
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
544
+ className: pet_module_css_default.statTreats,
545
+ children: props.t("pet.treats", { n: snapshot?.treats.stocked ?? 0 })
546
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
547
+ className: pet_module_css_default.statPoints,
548
+ children: props.t("pet.points", { points: snapshot?.affinity.points ?? 0 })
549
+ })]
402
550
  }),
403
551
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
404
552
  className: pet_module_css_default.actions,
@@ -413,6 +561,7 @@ window.__ModuleLoader__.load({
413
561
  type: "button",
414
562
  className: pet_module_css_default.action,
415
563
  onClick: () => {
564
+ clearHideTimer();
416
565
  setNameDraft(displayName);
417
566
  setRenaming(true);
418
567
  },
@@ -481,6 +630,7 @@ window.__ModuleLoader__.load({
481
630
  onHide: props.hide,
482
631
  onDragEnd: props.dragEnd,
483
632
  onRename: props.rename,
633
+ onOpenSession: props.openSession,
484
634
  onFeedbackDone: props.feedbackDone,
485
635
  t: props.t
486
636
  })
@@ -645,7 +795,7 @@ window.__ModuleLoader__.load({
645
795
  children: props.t("settings.readOnly")
646
796
  }) : null,
647
797
  props.children,
648
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
798
+ props.hideFooter === true ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
649
799
  className: settings_card_module_css_default.footer,
650
800
  children: [
651
801
  state.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
@@ -1346,6 +1496,7 @@ window.__ModuleLoader__.load({
1346
1496
  "pet.treats": "小鱼干 ×{n}",
1347
1497
  "pet.state.loading": "宠物正在赶来…",
1348
1498
  "pet.state.error": "宠物迷路了(连接失败)",
1499
+ "pet.openSessionHint": "点击跳转到对应会话",
1349
1500
  "settings.title": "宠物",
1350
1501
  "settings.description": "选择宠物并调整它的显示布局。",
1351
1502
  "settings.pet": "宠物",
@@ -1389,6 +1540,7 @@ window.__ModuleLoader__.load({
1389
1540
  "pet.treats": "Treats ×{n}",
1390
1541
  "pet.state.loading": "The pet is on its way…",
1391
1542
  "pet.state.error": "The pet is lost (connection failed)",
1543
+ "pet.openSessionHint": "Click to jump to this session",
1392
1544
  "settings.title": "Pet",
1393
1545
  "settings.description": "Pick a pet and tune its display layout.",
1394
1546
  "settings.pet": "Pet",
@@ -1467,13 +1619,14 @@ window.__ModuleLoader__.load({
1467
1619
  const POLL_MS = 2e3;
1468
1620
  /** Settings namespace the pet settings card edits (the Host plugin registers it). */
1469
1621
  const PET_SETTINGS_NS = "pet";
1470
- /** Required services. */
1622
+ /** Required services (sessions powers bubble-to-session navigation). */
1471
1623
  const inject = [
1472
1624
  "slots",
1473
1625
  "locale",
1474
1626
  "connection",
1475
1627
  "settingsScope",
1476
- "remote"
1628
+ "remote",
1629
+ "sessions"
1477
1630
  ];
1478
1631
  /**
1479
1632
  * Client plugin body: register dictionaries, mount the global pet entry and
@@ -1515,14 +1668,19 @@ window.__ModuleLoader__.load({
1515
1668
  const setState = petStore.actions.setState;
1516
1669
  const setFeedback = petStore.actions.setFeedback;
1517
1670
  let petsLoaded = false;
1671
+ let stateSeq = 0;
1518
1672
  const pollNow = () => {
1519
1673
  if (!petsLoaded) petApi.pets().then((list) => {
1520
1674
  petsLoaded = true;
1521
1675
  setPets(list);
1522
1676
  }, () => {});
1677
+ const seq = stateSeq + 1;
1678
+ stateSeq = seq;
1523
1679
  petApi.state().then((snapshot) => {
1680
+ if (seq !== stateSeq) return;
1524
1681
  setSnapshot(snapshot);
1525
1682
  }, () => {
1683
+ if (seq !== stateSeq) return;
1526
1684
  setState("error", "pet.state transport error");
1527
1685
  });
1528
1686
  };
@@ -1550,9 +1708,15 @@ window.__ModuleLoader__.load({
1550
1708
  document.removeEventListener("visibilitychange", onVisibility);
1551
1709
  };
1552
1710
  }, "pet: poll");
1711
+ const sessions = ctx.sessions;
1712
+ const openSession = (sessionId) => {
1713
+ if (sessions.list.getSnapshot().byId[sessionId] === void 0) return;
1714
+ sessions.open(sessionId);
1715
+ };
1553
1716
  const injected = () => ({
1554
1717
  store: petStore,
1555
1718
  ensure: pollNow,
1719
+ openSession,
1556
1720
  pet: () => {
1557
1721
  petApi.interact("pet").then((result) => {
1558
1722
  setFeedback({