@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
@@ -1 +1 @@
1
- {"version":3,"file":"PetDockEntry.d.ts","sourceRoot":"","sources":["../../../src/client/PetDockEntry.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAmC,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAEtD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,2DAA2D;AAC3D,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,KAAK,EAAE,gBAAgB,CAAA;IACvB,kFAAkF;IAClF,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,8BAA8B;IAC9B,GAAG,EAAE,MAAM,IAAI,CAAA;IACf,uBAAuB;IACvB,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,uBAAuB;IACvB,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,qCAAqC;IACrC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAChD,uDAAuD;IACvD,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,2DAA2D;IAC3D,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,iCAAiC;IACjC,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB;AAED,yFAAyF;AACzF,MAAM,MAAM,iBAAiB,GAC3B,WAAW,GACT,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;AAI1B;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,CAqDnE"}
1
+ {"version":3,"file":"PetDockEntry.d.ts","sourceRoot":"","sources":["../../../src/client/PetDockEntry.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAmC,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAEtD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,2DAA2D;AAC3D,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,KAAK,EAAE,gBAAgB,CAAA;IACvB,kFAAkF;IAClF,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,8BAA8B;IAC9B,GAAG,EAAE,MAAM,IAAI,CAAA;IACf,uBAAuB;IACvB,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,uBAAuB;IACvB,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,qCAAqC;IACrC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAChD,uDAAuD;IACvD,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,2DAA2D;IAC3D,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,iCAAiC;IACjC,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB;AAED,yFAAyF;AACzF,MAAM,MAAM,iBAAiB,GAC3B,WAAW,GACT,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;AAI1B;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,CAsDnE"}
@@ -43,5 +43,5 @@ export function PetDockEntry(props) {
43
43
  right: display.right,
44
44
  bottom: display.bottom,
45
45
  zIndex: 2147483000,
46
- }, onClick: props.summon, "data-testid": "pet-summon", children: props.t('pet.summon', { name: snapshot?.name ?? '' }) }));
46
+ }, onClick: props.summon, "data-testid": "pet-summon", "data-dsh-part": "summon-button", children: props.t('pet.summon', { name: snapshot?.name ?? '' }) }));
47
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"PetSprite.d.ts","sourceRoot":"","sources":["../../../src/client/PetSprite.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAqC,WAAW,EAAE,MAAM,OAAO,CAAA;AAG3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAGjD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,8EAA8E;IAC9E,UAAU,EAAE,aAAa,CAAA;IACzB,qDAAqD;IACrD,OAAO,EAAE,gBAAgB,CAAA;IACzB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;IAC5B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,uDAAuD;IACvD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,4DAA4D;IAC5D,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,2DAA2D;IAC3D,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,+CAA+C;IAC/C,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;CAC1B;AAOD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,WAAW,CA6T5D"}
1
+ {"version":3,"file":"PetSprite.d.ts","sourceRoot":"","sources":["../../../src/client/PetSprite.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAoD,WAAW,EAAE,MAAM,OAAO,CAAA;AAG1F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAIjD,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAA;AAGjC,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC7B,gDAAgD;IAChD,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAA;IAC7B,8EAA8E;IAC9E,UAAU,EAAE,aAAa,CAAA;IACzB,qDAAqD;IACrD,OAAO,EAAE,gBAAgB,CAAA;IACzB,sCAAsC;IACtC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAA;IAC5B,8BAA8B;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,sCAAsC;IACtC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,uDAAuD;IACvD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,4DAA4D;IAC5D,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,2DAA2D;IAC3D,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,+CAA+C;IAC/C,CAAC,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAA;CAC1B;AAOD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,WAAW,CAud5D"}
@@ -9,10 +9,12 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
9
9
  * pet id), so one component renders every registry entry.
10
10
  * @module @linxin666/dsh-pet/client/PetSprite
11
11
  */
12
- import { useEffect, useRef, useState } from 'react';
12
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
13
13
  import { createPortal } from 'react-dom';
14
14
  import clsx from 'clsx';
15
15
  import { framePosition, rowOfTrack, trimTrack } from './spritesheet.js';
16
+ import { sequenceFrameAt } from './sequences.js';
17
+ import { animationForPhase } from '../state.js';
16
18
  import styles from './pet.module.css';
17
19
  /** Clamp a drag offset inside the viewport with a margin. */
18
20
  function clampOffset(value, max) {
@@ -28,10 +30,29 @@ export function PetSprite(props) {
28
30
  const { snapshot, definition, display, feedback } = props;
29
31
  const spriteRef = useRef(null);
30
32
  const floatRef = useRef(null);
33
+ const panelRef = useRef(null);
34
+ // Whichever bubble surface is currently rendered (feedback, the session
35
+ // stack, or the legacy status bubble) — only one exists at a time.
36
+ const bubbleRef = useRef(null);
31
37
  const [imageReady, setImageReady] = useState(false);
32
38
  const [hovered, setHovered] = useState(false);
39
+ // Multi-session bubble stack: collapsed by default (only the display
40
+ // session's bubble + a '+N' badge), expanded on stack hover (peek) or by
41
+ // tapping the badge (pinned, for touch). The display session's bubble
42
+ // anchors the bottom of the stack and never moves when extras open above
43
+ // it, so the pointer target cannot flicker.
44
+ const [stackPeek, setStackPeek] = useState(false);
45
+ const [stackPinned, setStackPinned] = useState(false);
33
46
  const [renaming, setRenaming] = useState(false);
47
+ const [panelAbove, setPanelAbove] = useState(false);
48
+ // Extra margin-bottom for the above-panel so it stacks clear of the
49
+ // bubbles instead of overlapping them (both anchor at the sprite's top).
50
+ const [panelLift, setPanelLift] = useState(0);
34
51
  const [nameDraft, setNameDraft] = useState('');
52
+ // Explicit IME composition tracking: some input methods (WeChat IME on
53
+ // Windows) report keydowns with isComposing === false mid-composition, so
54
+ // the native flag alone is not a safe submit/cancel guard (#303).
55
+ const composingRef = useRef(false);
35
56
  const [dragPos, setDragPos] = useState(null);
36
57
  const dragRef = useRef(null);
37
58
  const hideTimerRef = useRef(null);
@@ -44,6 +65,7 @@ export function PetSprite(props) {
44
65
  const columns = definition.columns;
45
66
  const rows = definition.rows;
46
67
  const tracks = definition.tracks;
68
+ const sequences = definition.sequences;
47
69
  // Load the atlas once; the definition carries the authoritative per-row
48
70
  // frame counts and per-track durations, so nothing else is fetched.
49
71
  useEffect(() => {
@@ -66,14 +88,17 @@ export function PetSprite(props) {
66
88
  // its track's first frame instead of animating (presentation-only; the
67
89
  // animation state machine is untouched).
68
90
  const spriteScale = display.size / cell.height;
91
+ const phase = snapshot?.phase ?? 'idle';
69
92
  const animation = snapshot?.animation ?? 'idle';
70
93
  const scaleRef = useRef(spriteScale);
71
94
  scaleRef.current = spriteScale;
72
95
  useEffect(() => {
73
96
  const reduceMotion = typeof window !== 'undefined'
74
97
  && window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;
75
- const row = rowOfTrack(animation);
76
- const track = trimTrack(tracks[animation], rows[row] ?? tracks[animation].frames.length);
98
+ const sequence = animation === animationForPhase(phase) ? sequences?.[phase] : undefined;
99
+ const leadAnimation = sequence?.[0] ?? animation;
100
+ const row = rowOfTrack(leadAnimation);
101
+ const track = trimTrack(tracks[leadAnimation], rows[row] ?? tracks[leadAnimation].frames.length);
77
102
  // Paint one static sprite frame up front either way, so the pet is never
78
103
  // blank while the loop heat-up runs.
79
104
  const leadCol = track.frames[0];
@@ -85,9 +110,23 @@ export function PetSprite(props) {
85
110
  return;
86
111
  let raf = 0;
87
112
  let last = performance.now();
113
+ let sequenceElapsed = 0;
88
114
  const tick = (ts) => {
89
115
  const delta = ts - last;
90
116
  last = ts;
117
+ if (sequence !== undefined) {
118
+ sequenceElapsed += delta;
119
+ const current = sequenceFrameAt(sequence, tracks, sequenceElapsed);
120
+ const currentRow = rowOfTrack(current.animation);
121
+ const currentTrack = trimTrack(tracks[current.animation], rows[currentRow] ?? tracks[current.animation].frames.length);
122
+ const col = currentTrack.frames[current.frameIndex];
123
+ const pos = framePosition(cell, columns, currentRow, col, scaleRef.current);
124
+ if (spriteRef.current !== null) {
125
+ spriteRef.current.style.backgroundPosition = pos.x + 'px ' + pos.y + 'px';
126
+ }
127
+ raf = requestAnimationFrame(tick);
128
+ return;
129
+ }
91
130
  // row/track come from the effect scope: they were computed once above
92
131
  // and this effect re-runs when animation/tracks/rows change, so the
93
132
  // per-frame recompute (trimTrack slices fresh arrays) is pure waste.
@@ -121,7 +160,7 @@ export function PetSprite(props) {
121
160
  };
122
161
  raf = requestAnimationFrame(tick);
123
162
  return () => cancelAnimationFrame(raf);
124
- }, [animation, cell, columns, rows, tracks]);
163
+ }, [animation, phase, cell, columns, rows, tracks, sequences]);
125
164
  // Auto-clear the feedback bubble after its CSS animation. The callback
126
165
  // rides a ref so re-renders never reset the timer: the 2s poll rebuilds
127
166
  // `props` every tick, and depending on it would starve the timeout.
@@ -143,6 +182,10 @@ export function PetSprite(props) {
143
182
  hideTimerRef.current = null;
144
183
  }
145
184
  };
185
+ // Clear any pending auto-hide timer on unmount: a stray callback after
186
+ // teardown reads window through react-dom and failed CI runs with
187
+ // "window is not defined" (slow-runner timing, PetSprite.test.tsx).
188
+ useEffect(() => () => clearHideTimer(), []);
146
189
  const onPointerDown = (e) => {
147
190
  e.preventDefault();
148
191
  e.target.setPointerCapture?.(e.pointerId);
@@ -173,37 +216,84 @@ export function PetSprite(props) {
173
216
  const pos = dragPos ?? { right: display.right, bottom: display.bottom };
174
217
  const spriteWidth = Math.round(cell.width * spriteScale);
175
218
  const spriteHeight = Math.round(cell.height * spriteScale);
176
- // Concurrent sessions each render their own bubble (stacked above the
177
- // sprite); the legacy single 'bubble' is the fallback when the host serves
178
- // no per-session list. The hover panel now sits beside the sprite, so the
179
- // bubbles stay visible and clickable while hovering no region swap.
219
+ // Concurrent sessions share one bubble slot: only the display session
220
+ // speaks by default, and the rest hide behind a '+N' badge until the stack
221
+ // is hovered/pinned open. The legacy single 'bubble' is the fallback when
222
+ // the host serves no per-session list. The hover panel normally sits below
223
+ // the sprite, so the bubbles stay visible and clickable — no region swap.
180
224
  const sessionBubbles = snapshot?.sessions ?? [];
225
+ const stackOpen = stackPeek || stackPinned;
226
+ const collapsed = !stackOpen && sessionBubbles.length > 1;
227
+ const visibleSessions = collapsed ? sessionBubbles.slice(0, 1) : sessionBubbles;
181
228
  const statusBubble = feedback === null && sessionBubbles.length === 0
182
229
  ? snapshot?.bubble
183
230
  : undefined;
231
+ // The display session's inner whisper (碎碎念) — short inner-voice copy
232
+ // woken by the model's output. Instead of a second bubble of its own, a
233
+ // fresh whisper takes over the display session's bubble (the stack top, or
234
+ // the single status bubble) and re-tints it, so the pet never wears two
235
+ // voices at once. Interaction feedback takes over the whole bubble area
236
+ // while it plays, so whispers yield to it like status copy.
237
+ const whisper = feedback === null ? snapshot?.whisper : undefined;
238
+ const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== undefined || whisper !== undefined;
184
239
  const displayName = snapshot?.name ?? definition.displayName;
240
+ // A settled session list can no longer stay pinned open.
241
+ useEffect(() => {
242
+ if (sessionBubbles.length <= 1)
243
+ setStackPinned(false);
244
+ }, [sessionBubbles.length]);
245
+ useLayoutEffect(() => {
246
+ if (!hovered) {
247
+ setPanelAbove(false);
248
+ setPanelLift(0);
249
+ return;
250
+ }
251
+ const updatePanelPlacement = () => {
252
+ const sprite = spriteRef.current;
253
+ const panel = panelRef.current;
254
+ if (sprite === null || panel === null)
255
+ return;
256
+ const availableBelow = window.innerHeight - sprite.getBoundingClientRect().bottom;
257
+ const above = availableBelow < panel.getBoundingClientRect().height + 8;
258
+ setPanelAbove(above);
259
+ // The fallback above-placement shares the sprite's top edge with the
260
+ // bubble(s); lift the panel by the bubble area's height so the two
261
+ // never overlap (8px base gap + 6px clearance above the top bubble).
262
+ const bubbleHeight = above ? bubbleRef.current?.getBoundingClientRect().height ?? 0 : 0;
263
+ setPanelLift(bubbleHeight > 0 ? Math.ceil(bubbleHeight) + 14 : 0);
264
+ };
265
+ updatePanelPlacement();
266
+ window.addEventListener('resize', updatePanelPlacement);
267
+ return () => window.removeEventListener('resize', updatePanelPlacement);
268
+ }, [hovered, renaming, pos.right, pos.bottom, display.size, bubblePresent, sessionBubbles.length, stackOpen, feedback]);
185
269
  const float = (_jsxs("div", { ref: floatRef, className: styles.float, style: { right: pos.right, bottom: pos.bottom, zIndex: 2147483000 }, onPointerEnter: () => {
186
270
  clearHideTimer();
187
271
  setHovered(true);
188
272
  }, onPointerLeave: (e) => {
189
- // The panel renders OUTSIDE the container's box (absolute, beside
273
+ // The panel renders OUTSIDE the container's box (absolute, below
190
274
  // the sprite), so moving onto it fires pointerleave on the container.
191
275
  // Treat a target still inside the container's DOM (the overflowed
192
276
  // panel) as "still hovering"; otherwise give the pointer a short
193
- // grace period to reach the panel across the gap beside the sprite.
277
+ // grace period to reach the panel across the gap below the sprite.
194
278
  // The bridge ('.panel::after') keeps the pointer inside the hit
195
279
  // area, and the grace period covers a slow mouse crossing the
196
280
  // remaining sliver.
197
281
  const next = e.relatedTarget;
198
282
  if (next instanceof Node && floatRef.current?.contains(next))
199
283
  return;
284
+ // Never auto-hide while the rename box is open: moving the pointer
285
+ // onto an IME candidate window (an OS-level window outside the
286
+ // webview) fires pointerleave, and unmounting the input mid-IME-
287
+ // composition crashes some input methods / the renderer (#303).
288
+ if (renaming)
289
+ return;
200
290
  clearHideTimer();
201
291
  hideTimerRef.current = window.setTimeout(() => setHovered(false), 300);
202
292
  }, children: [_jsx("div", { ref: spriteRef, className: styles.sprite, style: {
203
293
  width: spriteWidth,
204
294
  height: spriteHeight,
205
295
  backgroundImage: imageReady ? 'url(' + definition.atlasUrl + ')' : undefined,
206
- backgroundSize: (cell.width * columns * spriteScale) + 'px ' + (cell.height * rows.length * spriteScale) + 'px',
296
+ backgroundSize: (cell.width * columns * spriteScale) + 'px ' + (cell.height * (definition.atlasRows ?? rows.length) * spriteScale) + 'px',
207
297
  backgroundRepeat: 'no-repeat',
208
298
  backgroundPosition: '0 0',
209
299
  cursor: dragRef.current === null ? 'grab' : 'grabbing',
@@ -213,17 +303,41 @@ export function PetSprite(props) {
213
303
  if (draggedRef.current)
214
304
  return;
215
305
  props.onPet();
216
- }, role: "button", "aria-label": definition.displayName }), feedback !== null && (_jsx("div", { className: clsx(styles.bubble, feedback.kind === 'feed' ? styles.bubbleFeed : styles.bubblePet), children: feedback.text }, feedback.at)), feedback === null && sessionBubbles.length > 0 && (_jsx("div", { className: styles.bubbleStack, children: sessionBubbles.map(session => (_jsx("button", { type: "button", className: clsx(styles.bubble, styles.bubbleStatus, styles.bubbleClickable), title: props.t('pet.openSessionHint'), onClick: () => { props.onOpenSession(session.sessionId); }, children: session.bubble }, session.sessionId))) })), statusBubble !== undefined && (_jsx("div", { className: clsx(styles.bubble, styles.bubbleStatus), role: "status", "aria-live": "polite", children: statusBubble })), hovered && dragRef.current === null && (_jsx("div", { className: styles.panel, onPointerEnter: () => {
306
+ }, role: "button", "aria-label": definition.displayName }), feedback !== null && (_jsx("div", { ref: bubbleRef, className: clsx(styles.bubble, feedback.kind === 'feed' ? styles.bubbleFeed : styles.bubblePet), children: feedback.text }, feedback.at)), feedback === null && (sessionBubbles.length > 0 || statusBubble !== undefined || whisper !== undefined) && (_jsxs("div", { ref: bubbleRef, className: styles.bubbleStack, onPointerEnter: () => setStackPeek(true), onPointerLeave: () => setStackPeek(false), children: [visibleSessions.map((session, index) => {
307
+ // The whisper rides the display session's bubble — the stack's
308
+ // primary entry (DOM-first, rendered bottom-most by the reversed
309
+ // column so it stays glued to the sprite when extras open above).
310
+ // The key swap restarts the entrance animation so the mood change
311
+ // reads as the bubble re-speaking.
312
+ const speaksWhisper = index === 0 && whisper !== undefined;
313
+ const bubble = (_jsx("button", { type: "button", className: clsx(styles.bubble, styles.bubbleStatus, styles.bubbleClickable, speaksWhisper && styles.bubbleWhisper), title: props.t('pet.openSessionHint'), onClick: () => { props.onOpenSession(session.sessionId); }, children: speaksWhisper ? whisper : session.bubble }, speaksWhisper ? 'whisper:' + whisper : session.sessionId));
314
+ // The primary bubble carries the '+N' badge while other sessions
315
+ // hide behind it; the badge toggles the pinned (touch) expansion.
316
+ if (index !== 0 || sessionBubbles.length <= 1)
317
+ return bubble;
318
+ return (_jsxs("span", { className: styles.bubbleAnchor, children: [bubble, _jsx("button", { type: "button", className: styles.bubbleMore, title: stackOpen
319
+ ? props.t('pet.collapseSessions')
320
+ : props.t('pet.moreSessions', { n: sessionBubbles.length - 1 }), "aria-label": stackOpen
321
+ ? props.t('pet.collapseSessions')
322
+ : props.t('pet.moreSessions', { n: sessionBubbles.length - 1 }), "aria-expanded": stackOpen, onClick: (e) => {
323
+ e.stopPropagation();
324
+ setStackPinned(open => !open);
325
+ }, children: stackOpen ? '×' : '+' + String(sessionBubbles.length - 1) })] }, "primary"));
326
+ }), sessionBubbles.length === 0 && (statusBubble !== undefined || whisper !== undefined) && (_jsx("div", { className: clsx(styles.bubble, styles.bubbleStatus, whisper !== undefined && styles.bubbleWhisper), role: "status", "aria-live": "polite", children: whisper ?? statusBubble }, whisper === undefined ? 'status' : 'whisper:' + whisper))] })), hovered && dragRef.current === null && (_jsx("div", { ref: panelRef, className: clsx(styles.panel, panelAbove && styles.panelAbove), "data-placement": panelAbove ? 'above' : 'below', style: panelAbove && panelLift > 0
327
+ ? { marginBottom: panelLift }
328
+ : undefined, onPointerEnter: () => {
217
329
  // Reaching the panel (or its bridge) must cancel any hide timer
218
330
  // the container's pointerleave may have armed while the pointer
219
331
  // crossed the sliver between the sprite and the panel.
220
332
  clearHideTimer();
221
- }, children: renaming ? (_jsxs("div", { className: styles.renameRow, children: [_jsx("input", { className: styles.nameInput, value: nameDraft, maxLength: 20, placeholder: props.t('pet.namePlaceholder'), autoFocus: true, onChange: (e) => setNameDraft(e.target.value), onKeyDown: (e) => {
333
+ }, children: renaming ? (_jsxs("div", { className: styles.renameRow, children: [_jsx("input", { className: styles.nameInput, value: nameDraft, maxLength: 20, placeholder: props.t('pet.namePlaceholder'), autoFocus: true, onChange: (e) => setNameDraft(e.target.value), onCompositionStart: () => { composingRef.current = true; }, onCompositionEnd: () => { composingRef.current = false; }, onKeyDown: (e) => {
222
334
  // While an IME composition is active (e.g. selecting a
223
335
  // Chinese candidate), Enter/Escape keydowns belong to the
224
336
  // input method: ignore them so candidate selection can
225
- // neither submit the draft nor close the rename box.
226
- if (e.nativeEvent.isComposing)
337
+ // neither submit the draft nor close the rename box. The
338
+ // explicit ref and the 'Process' key cover IMEs that mark
339
+ // composition keydowns with isComposing === false (#303).
340
+ if (composingRef.current || e.nativeEvent.isComposing || e.key === 'Process')
227
341
  return;
228
342
  if (e.key === 'Enter') {
229
343
  const trimmed = nameDraft.trim();
@@ -241,7 +355,10 @@ export function PetSprite(props) {
241
355
  props.onRename(trimmed);
242
356
  setRenaming(false);
243
357
  }
244
- }, children: props.t('pet.confirm') })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.nameCell, children: displayName }), _jsx("span", { children: props.t('pet.rank', { rank: snapshot?.affinity.rank ?? '?' }) })] }), _jsxs("div", { className: styles.rankRow, children: [_jsx("span", { children: props.t('pet.treats', { n: snapshot?.treats.stocked ?? 0 }) }), _jsx("span", { children: props.t('pet.points', { points: snapshot?.affinity.points ?? 0 }) })] }), _jsxs("div", { className: styles.actions, children: [_jsx("button", { type: "button", className: styles.action, onClick: props.onFeed, children: props.t('pet.feed') }), _jsx("button", { type: "button", className: styles.action, onClick: () => {
358
+ }, children: props.t('pet.confirm') })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.nameCell, children: displayName }), _jsx("span", { className: styles.statRank, children: props.t('pet.rank', { rank: snapshot?.affinity.rank ?? '?' }) })] }), _jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.statTreats, children: props.t('pet.treats', { n: snapshot?.treats.stocked ?? 0 }) }), _jsx("span", { className: styles.statPoints, children: props.t('pet.points', { points: snapshot?.affinity.points ?? 0 }) })] }), _jsxs("div", { className: styles.actions, children: [_jsx("button", { type: "button", className: styles.action, onClick: props.onFeed, children: props.t('pet.feed') }), _jsx("button", { type: "button", className: styles.action, onClick: () => {
359
+ // Cancel any pending hide so the rename box cannot
360
+ // unmount right as the user starts typing (#303).
361
+ clearHideTimer();
245
362
  setNameDraft(displayName);
246
363
  setRenaming(true);
247
364
  }, children: props.t('pet.rename') }), _jsx("button", { type: "button", className: styles.action, onClick: props.onHide, children: props.t('pet.hide') })] })] })) }))] }));
@@ -39,6 +39,12 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
39
39
  * already provides the selection.
40
40
  */
41
41
  alwaysOpen?: boolean;
42
+ /**
43
+ * Hide the save/discard footer: cards whose body applies its own changes
44
+ * immediately (an embedded external settings section) have no staged
45
+ * edits, so the footer would sit there permanently disabled.
46
+ */
47
+ hideFooter?: boolean;
42
48
  /** The plugin's controls. */
43
49
  children: ReactNode;
44
50
  }
@@ -82,6 +88,27 @@ export declare function ValueField(props: FieldProps & {
82
88
  /** Placeholder shown while the draft is empty. */
83
89
  placeholder?: string;
84
90
  }): import("react").JSX.Element;
91
+ interface SelectOption {
92
+ value: string;
93
+ label: string;
94
+ }
95
+ /**
96
+ * The shared dual-mode select control. While an appearance skin is active it
97
+ * renders the legacy native `<select>` untouched, so element-level skin
98
+ * selectors keep working; under the default appearance it renders a
99
+ * self-drawn `role="listbox"` popup whose open/close is transition-animated.
100
+ * Staged cards reach it through BooleanField/ChoiceField; immediate-apply
101
+ * editors (the side-card prefs) bind it directly through onEdit.
102
+ * 双模式下拉框:皮肤激活时用原生 select,默认外观用自绘动画弹层。
103
+ */
104
+ export declare function SelectField(props: {
105
+ id: string;
106
+ options: ReadonlyArray<SelectOption>;
107
+ value: string;
108
+ disabled: boolean;
109
+ invalid: boolean;
110
+ onEdit: (text: string) => void;
111
+ }): import("react").JSX.Element;
85
112
  /** A staged boolean field: 继承 / 开 / 关. */
86
113
  export declare function BooleanField(props: FieldProps & {
87
114
  /** Copy for the inherit option. */
@@ -101,4 +128,5 @@ export declare function ChoiceField(props: FieldProps & {
101
128
  label: string;
102
129
  }>;
103
130
  }): import("react").JSX.Element;
131
+ export {};
104
132
  //# sourceMappingURL=PluginSettingsCard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"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"}
@@ -8,7 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  * it. Inlined into each consumer's client bundle; mirrors the official
9
9
  * ui-plugin-config PluginCard in a self-contained slice.
10
10
  */
11
- import { useState } from 'react';
11
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
12
12
  import css from './settings-card.module.css';
13
13
  /** Copy keys the card chrome itself reads; every consumer locale carries this shared vocabulary. */
14
14
  export const CARD_COPY_KEYS = [
@@ -52,9 +52,11 @@ export function PluginSettingsCard(props) {
52
52
  : null] }));
53
53
  }
54
54
  return (_jsxs("li", { className: cardClass, children: [header, expanded
55
- ? (_jsxs("div", { className: css.body, children: [!state.writable ? _jsx("p", { className: css.readOnly, role: "status", children: props.t('settings.readOnly') }) : null, props.children, _jsxs("div", { className: css.footer, children: [state.failed
56
- ? (_jsxs("p", { className: css.failed, role: "status", children: [props.t('settings.saveFailed'), state.failedReason ? ' - ' + state.failedReason : ''] }))
57
- : null, _jsx("button", { type: "button", className: css.discard, disabled: !state.dirty || state.saving, onClick: props.onDiscard, children: props.t('settings.discard') }), _jsx("button", { type: "button", className: css.save, disabled: blocked, onClick: props.onSave, children: props.t(!state.saving ? 'settings.save' : 'settings.saving') })] })] }))
55
+ ? (_jsxs("div", { className: css.body, children: [!state.writable ? _jsx("p", { className: css.readOnly, role: "status", children: props.t('settings.readOnly') }) : null, props.children, props.hideFooter === true
56
+ ? null
57
+ : (_jsxs("div", { className: css.footer, children: [state.failed
58
+ ? (_jsxs("p", { className: css.failed, role: "status", children: [props.t('settings.saveFailed'), state.failedReason ? ' - ' + state.failedReason : ''] }))
59
+ : null, _jsx("button", { type: "button", className: css.discard, disabled: !state.dirty || state.saving, onClick: props.onDiscard, children: props.t('settings.discard') }), _jsx("button", { type: "button", className: css.save, disabled: blocked, onClick: props.onSave, children: props.t(!state.saving ? 'settings.save' : 'settings.saving') })] }))] }))
58
60
  : null] }));
59
61
  }
60
62
  /** A staged value field. `numeric` only hints the keypad: which drafts a field accepts is decided by its spec. */
@@ -63,15 +65,154 @@ export function ValueField(props) {
63
65
  ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: props.overriddenLabel }), _jsx("button", { type: "button", className: css.reset, disabled: props.disabled, onClick: props.onReset, children: props.resetLabel })] }))
64
66
  : null] }), _jsx("input", { id: props.id, className: props.invalid ? css.inputInvalid : css.input, type: "text", ...props.numeric === true ? { inputMode: 'numeric' } : {}, ...props.invalid ? { 'aria-invalid': true } : {}, value: props.text, placeholder: props.placeholder ?? '', disabled: props.disabled, onChange: (event) => { props.onEdit(event.target.value); } }), _jsx("p", { className: props.invalid ? css.invalid : css.hint, children: props.invalid ? props.invalidLabel : props.hint })] }));
65
67
  }
68
+ const NON_SKIN_BODY_MARKERS = new Set(['dshSkinCenter', 'dshSidebarCollapsed']);
69
+ // 检测使用使用皮肤,用了皮肤退回原生select样式,防止样式冲突。默认外观下使用优化后的select样式。
70
+ function isSkinActive() {
71
+ const datasetList = Object.keys(document.body.dataset);
72
+ const isActive = datasetList.some(key => key.startsWith('dsh') && !NON_SKIN_BODY_MARKERS.has(key));
73
+ return isActive;
74
+ }
75
+ const SELECT_CLOSE_MS = 100;
76
+ /**
77
+ * The shared dual-mode select control. While an appearance skin is active it
78
+ * renders the legacy native `<select>` untouched, so element-level skin
79
+ * selectors keep working; under the default appearance it renders a
80
+ * self-drawn `role="listbox"` popup whose open/close is transition-animated.
81
+ * Staged cards reach it through BooleanField/ChoiceField; immediate-apply
82
+ * editors (the side-card prefs) bind it directly through onEdit.
83
+ * 双模式下拉框:皮肤激活时用原生 select,默认外观用自绘动画弹层。
84
+ */
85
+ export function SelectField(props) {
86
+ const { id, options, value } = props;
87
+ const [open, setOpen] = useState(false);
88
+ const [closing, setClosing] = useState(false);
89
+ const [phase, setPhase] = useState('initial');
90
+ const [activeIndex, setActiveIndex] = useState(0);
91
+ const closeTimer = useRef(undefined);
92
+ const wrapRef = useRef(null);
93
+ const popupRef = useRef(null);
94
+ const currentIndex = () => {
95
+ const index = options.findIndex(option => option.value === value);
96
+ return index >= 0 ? index : 0;
97
+ };
98
+ const close = useCallback(() => {
99
+ if (closeTimer.current !== undefined)
100
+ clearTimeout(closeTimer.current);
101
+ setClosing(true);
102
+ closeTimer.current = setTimeout(() => {
103
+ setClosing(false);
104
+ setOpen(false);
105
+ }, SELECT_CLOSE_MS);
106
+ }, []);
107
+ const openPopup = () => {
108
+ if (closeTimer.current !== undefined)
109
+ clearTimeout(closeTimer.current);
110
+ setActiveIndex(currentIndex());
111
+ setPhase('initial');
112
+ setClosing(false);
113
+ setOpen(true);
114
+ };
115
+ const commit = (index) => {
116
+ const option = options[index];
117
+ if (option)
118
+ props.onEdit(option.value);
119
+ close();
120
+ };
121
+ const onTriggerClick = () => {
122
+ if (props.disabled)
123
+ return;
124
+ if (open && !closing)
125
+ close();
126
+ else
127
+ openPopup();
128
+ };
129
+ const onKeyDown = (event) => {
130
+ if (props.disabled)
131
+ return;
132
+ const count = options.length;
133
+ switch (event.key) {
134
+ case 'ArrowDown':
135
+ case 'ArrowUp':
136
+ case 'Enter':
137
+ case ' ':
138
+ event.preventDefault();
139
+ if (!open) {
140
+ openPopup();
141
+ }
142
+ else if (!closing) {
143
+ if (event.key === 'ArrowDown')
144
+ setActiveIndex(index => (index + 1) % count);
145
+ else if (event.key === 'ArrowUp')
146
+ setActiveIndex(index => (index - 1 + count) % count);
147
+ else
148
+ commit(activeIndex);
149
+ }
150
+ break;
151
+ case 'Escape':
152
+ if (open) {
153
+ event.preventDefault();
154
+ event.stopPropagation();
155
+ close();
156
+ }
157
+ break;
158
+ case 'Tab':
159
+ if (open)
160
+ close();
161
+ break;
162
+ }
163
+ };
164
+ useEffect(() => () => {
165
+ if (closeTimer.current !== undefined)
166
+ clearTimeout(closeTimer.current);
167
+ }, []);
168
+ useLayoutEffect(() => {
169
+ if (open && !closing && phase === 'initial') {
170
+ void popupRef.current?.offsetHeight;
171
+ setPhase('open');
172
+ }
173
+ }, [open, closing, phase]);
174
+ useEffect(() => {
175
+ if (!open)
176
+ return;
177
+ const onPointerDown = (event) => {
178
+ const target = event.target;
179
+ if (target instanceof Node && !wrapRef.current?.contains(target))
180
+ close();
181
+ };
182
+ document.addEventListener('pointerdown', onPointerDown);
183
+ return () => document.removeEventListener('pointerdown', onPointerDown);
184
+ }, [open, close]);
185
+ useEffect(() => {
186
+ if (props.disabled && open)
187
+ close();
188
+ }, [props.disabled, open, close]);
189
+ // 使用了皮肤则退回原生select样式防止样式冲突。
190
+ if (isSkinActive()) {
191
+ return (_jsx("select", { id: id, className: css.select, value: value, disabled: props.disabled, onChange: (event) => { props.onEdit(event.target.value); }, children: options.map(option => (_jsx("option", { value: option.value, children: option.label }, option.value))) }));
192
+ }
193
+ const label = options.find(option => option.value === value)?.label ?? '';
194
+ const popupClass = closing
195
+ ? `${css.selectPopup} ${css.selectPopupClose}`
196
+ : phase === 'open'
197
+ ? `${css.selectPopup} ${css.selectPopupOpen}`
198
+ : css.selectPopup;
199
+ return (_jsxs("div", { className: css.selectWrap, ref: wrapRef, children: [_jsxs("button", { type: "button", id: id, className: `${css.select} ${css.selectButton}`, disabled: props.disabled, "aria-haspopup": "listbox", "aria-expanded": open, "aria-activedescendant": open ? `${id}-o${activeIndex}` : undefined, "aria-invalid": props.invalid || undefined, onClick: onTriggerClick, onKeyDown: onKeyDown, children: [_jsx("span", { className: css.selectLabel, children: label }), _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: open ? `${css.selectChevron} ${css.selectChevronOpen}` : css.selectChevron, "aria-hidden": "true", children: _jsx("path", { 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", fill: "currentColor" }) })] }), open
200
+ ? (_jsx("div", { className: popupClass, role: "listbox", ref: popupRef, children: options.map((option, index) => (_jsx("div", { id: `${id}-o${index}`, role: "option", "aria-selected": option.value === value, className: `${css.selectOption}${option.value === value ? ` ${css.selectOptionSelected}` : ''}${index === activeIndex && !closing ? ` ${css.selectOptionActive}` : ''}`, onClick: () => { commit(index); }, children: option.label }, option.value))) }))
201
+ : null] }));
202
+ }
66
203
  /** A staged boolean field: 继承 / 开 / 关. */
67
204
  export function BooleanField(props) {
68
205
  return (_jsxs("div", { className: css.field, children: [_jsxs("div", { className: css.head, children: [_jsx("label", { className: css.label, htmlFor: props.id, children: props.label }), props.overridden
69
206
  ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: props.overriddenLabel }), _jsx("button", { type: "button", className: css.reset, disabled: props.disabled, onClick: props.onReset, children: props.resetLabel })] }))
70
- : null] }), _jsxs("select", { id: props.id, className: css.select, value: props.text, disabled: props.disabled, onChange: (event) => { props.onEdit(event.target.value); }, children: [_jsx("option", { value: "", children: props.inheritLabel }), _jsx("option", { value: "true", children: props.onLabel }), _jsx("option", { value: "false", children: props.offLabel })] }), _jsx("p", { className: css.hint, children: props.hint })] }));
207
+ : null] }), _jsx(SelectField, { id: props.id, options: [
208
+ { value: '', label: props.inheritLabel },
209
+ { value: 'true', label: props.onLabel },
210
+ { value: 'false', label: props.offLabel },
211
+ ], value: props.text, disabled: props.disabled, invalid: props.invalid, onEdit: props.onEdit }), _jsx("p", { className: css.hint, children: props.hint })] }));
71
212
  }
72
213
  /** A staged enumerated field rendered as a select. */
73
214
  export function ChoiceField(props) {
74
215
  return (_jsxs("div", { className: css.field, children: [_jsxs("div", { className: css.head, children: [_jsx("label", { className: css.label, htmlFor: props.id, children: props.label }), props.overridden
75
216
  ? (_jsxs("span", { className: css.badges, children: [_jsx("span", { className: css.badge, children: props.overriddenLabel }), _jsx("button", { type: "button", className: css.reset, disabled: props.disabled, onClick: props.onReset, children: props.resetLabel })] }))
76
- : null] }), _jsxs("select", { id: props.id, className: css.select, value: props.text, disabled: props.disabled, onChange: (event) => { props.onEdit(event.target.value); }, children: [_jsx("option", { value: "", children: props.inheritLabel }), props.choices.map(choice => (_jsx("option", { value: choice.value, children: choice.label }, choice.value)))] }), _jsx("p", { className: props.invalid ? css.invalid : css.hint, children: props.invalid ? props.invalidLabel : props.hint })] }));
217
+ : null] }), _jsx(SelectField, { id: props.id, options: [{ value: '', label: props.inheritLabel }, ...props.choices], value: props.text, disabled: props.disabled, invalid: props.invalid, onEdit: props.onEdit }), _jsx("p", { className: props.invalid ? css.invalid : css.hint, children: props.invalid ? props.invalidLabel : props.hint })] }));
77
218
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAwB,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AA6DnI,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAiN9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAwB,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AA6DnI,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAkN9C"}
@@ -241,6 +241,7 @@ export function apply(ctx) {
241
241
  // when visible, and the hidden-state summon button is fixed-positioned.
242
242
  const container = document.createElement('div');
243
243
  container.dataset.dshPetRoot = '';
244
+ container.dataset.dshPlugin = 'pet';
244
245
  document.body.appendChild(container);
245
246
  const petRoot = createRoot(container);
246
247
  petRoot.render(createElement(PetDockEntry, { ...injected(), t }));
@@ -18,6 +18,8 @@ export declare const zh: {
18
18
  readonly 'pet.state.loading': '宠物正在赶来…';
19
19
  readonly 'pet.state.error': '宠物迷路了(连接失败)';
20
20
  readonly 'pet.openSessionHint': '点击跳转到对应会话';
21
+ readonly 'pet.moreSessions': '展开其余 {n} 个会话的气泡';
22
+ readonly 'pet.collapseSessions': '收起会话气泡';
21
23
  readonly 'settings.title': '宠物';
22
24
  readonly 'settings.description': '选择宠物并调整它的显示布局。';
23
25
  readonly 'settings.pet': '宠物';
@@ -62,6 +64,8 @@ export declare const en: {
62
64
  readonly 'pet.state.loading': 'The pet is on its way…';
63
65
  readonly 'pet.state.error': 'The pet is lost (connection failed)';
64
66
  readonly 'pet.openSessionHint': 'Click to jump to this session';
67
+ readonly 'pet.moreSessions': 'Expand {n} more session bubbles';
68
+ readonly 'pet.collapseSessions': 'Collapse session bubbles';
65
69
  readonly 'settings.title': 'Pet';
66
70
  readonly 'settings.description': 'Pick a pet and tune its display layout.';
67
71
  readonly 'settings.pet': 'Pet';
@@ -1 +1 @@
1
- {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE;aACb,UAAU,EAAE,IAAI;aAChB,UAAU,EAAE,IAAI;aAChB,YAAY,EAAE,IAAI;aAClB,aAAa,EAAE,IAAI;aACnB,qBAAqB,EAAE,OAAO;aAC9B,YAAY,EAAE,UAAU;aACxB,UAAU,EAAE,YAAY;aACxB,YAAY,EAAE,YAAY;aAC1B,YAAY,EAAE,UAAU;aACxB,mBAAmB,EAAE,SAAS;aAC9B,iBAAiB,EAAE,aAAa;aAChC,qBAAqB,EAAE,WAAW;aAClC,kBAAkB,EAAE,iBAAiB;aACrC,sBAAsB,EAAE,QAAQ;aAEhC,gBAAgB,EAAE,IAAI;aACtB,sBAAsB,EAAE,gBAAgB;aACxC,cAAc,EAAE,IAAI;aACpB,kBAAkB,EAAE,+BAA+B;aACnD,kBAAkB,EAAE,MAAM;aAC1B,sBAAsB,EAAE,yBAAyB;aACjD,kBAAkB,EAAE,MAAM;aAC1B,sBAAsB,EAAE,sBAAsB;aAC9C,eAAe,EAAE,QAAQ;aACzB,mBAAmB,EAAE,mBAAmB;aACxC,gBAAgB,EAAE,SAAS;aAC3B,oBAAoB,EAAE,gBAAgB;aACtC,iBAAiB,EAAE,SAAS;aAC5B,qBAAqB,EAAE,eAAe;aACtC,kBAAkB,EAAE,IAAI;aACxB,aAAa,EAAE,GAAG;aAClB,cAAc,EAAE,GAAG;aACnB,qBAAqB,EAAE,KAAK;aAC5B,gBAAgB,EAAE,MAAM;aACxB,qBAAqB,EAAE,8HAA8H;aACrJ,mBAAmB,EAAE,YAAY;aACjC,iBAAiB,EAAE,MAAM;aACzB,mBAAmB,EAAE,MAAM;aAC3B,eAAe,EAAE,IAAI;aACrB,iBAAiB,EAAE,MAAM;aACzB,kBAAkB,EAAE,IAAI;aACxB,kBAAkB,EAAE,KAAK;aACzB,qBAAqB,EAAE,mBAAmB;aAC1C,wBAAwB,EAAE,iBAAiB;CACnC,CAAA;AAEV,oBAAoB;AACpB,eAAO,MAAM,EAAE;aACb,UAAU,EAAE,MAAM;aAClB,UAAU,EAAE,MAAM;aAClB,YAAY,EAAE,QAAQ;aACtB,aAAa,EAAE,IAAI;aACnB,qBAAqB,EAAE,kBAAkB;aACzC,YAAY,EAAE,eAAe;aAC7B,UAAU,EAAE,iBAAiB;aAC7B,YAAY,EAAE,cAAc;aAC5B,YAAY,EAAE,aAAa;aAC3B,mBAAmB,EAAE,wBAAwB;aAC7C,iBAAiB,EAAE,qCAAqC;aACxD,qBAAqB,EAAE,+BAA+B;aACtD,kBAAkB,EAAE,iCAAiC;aACrD,sBAAsB,EAAE,0BAA0B;aAElD,gBAAgB,EAAE,KAAK;aACvB,sBAAsB,EAAE,yCAAyC;aACjE,cAAc,EAAE,KAAK;aACrB,kBAAkB,EAAE,oFAAoF;aACxG,kBAAkB,EAAE,gBAAgB;aACpC,sBAAsB,EAAE,+DAA+D;aACvF,kBAAkB,EAAE,cAAc;aAClC,sBAAsB,EAAE,8DAA8D;aACtF,eAAe,EAAE,WAAW;aAC5B,mBAAmB,EAAE,6BAAkC;aACvD,gBAAgB,EAAE,kBAAkB;aACpC,oBAAoB,EAAE,gDAAgD;aACtE,iBAAiB,EAAE,mBAAmB;aACtC,qBAAqB,EAAE,+CAA+C;aACtE,kBAAkB,EAAE,SAAS;aAC7B,aAAa,EAAE,IAAI;aACnB,cAAc,EAAE,KAAK;aACrB,qBAAqB,EAAE,YAAY;aACnC,gBAAgB,EAAE,kBAAkB;aACpC,qBAAqB,EAAE,2PAA2P;aAClR,mBAAmB,EAAE,4CAA4C;aACjE,iBAAiB,EAAE,eAAe;aAClC,mBAAmB,EAAE,eAAe;aACpC,eAAe,EAAE,MAAM;aACvB,iBAAiB,EAAE,SAAc;aACjC,kBAAkB,EAAE,SAAS;aAC7B,kBAAkB,EAAE,SAAS;aAC7B,qBAAqB,EAAE,gFAAgF;aACvG,wBAAwB,EAAE,oDAAoD;CACtE,CAAA;AAEV,oCAAoC;AACpC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;AAEpC,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGnD;AAED;;;;;;;GAOG;AACH,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQvE;AAED,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,uBAAuB;QACvB,GAAG,EAAE,MAAM,CAAA;KACZ;CACF"}
@@ -18,6 +18,8 @@ export const zh = {
18
18
  'pet.state.loading': '宠物正在赶来…',
19
19
  'pet.state.error': '宠物迷路了(连接失败)',
20
20
  'pet.openSessionHint': '点击跳转到对应会话',
21
+ 'pet.moreSessions': '展开其余 {n} 个会话的气泡',
22
+ 'pet.collapseSessions': '收起会话气泡',
21
23
  // 一级设置页(settings.section 席位)。
22
24
  'settings.title': '宠物',
23
25
  'settings.description': '选择宠物并调整它的显示布局。',
@@ -63,6 +65,8 @@ export const en = {
63
65
  'pet.state.loading': 'The pet is on its way…',
64
66
  'pet.state.error': 'The pet is lost (connection failed)',
65
67
  'pet.openSessionHint': 'Click to jump to this session',
68
+ 'pet.moreSessions': 'Expand {n} more session bubbles',
69
+ 'pet.collapseSessions': 'Collapse session bubbles',
66
70
  // First-level settings section (the `settings.section` seat).
67
71
  'settings.title': 'Pet',
68
72
  'settings.description': 'Pick a pet and tune its display layout.',