@linxin666/dsh-pet 0.1.15 → 0.1.16

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 (74) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +73 -38
  3. package/README.zh.md +91 -55
  4. package/assets/whale/pet.json +12 -1
  5. package/cordis.patch.yml +4 -4
  6. package/lib/client.js +270 -334
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +641 -87
  9. package/lib/types/client/PetDockEntry.d.ts +14 -12
  10. package/lib/types/client/PetDockEntry.d.ts.map +1 -1
  11. package/lib/types/client/PetDockEntry.js +13 -8
  12. package/lib/types/client/PetSettingsCard.d.ts +22 -9
  13. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  14. package/lib/types/client/PetSettingsCard.js +42 -7
  15. package/lib/types/client/{WhalePet.d.ts → PetSprite.d.ts} +21 -20
  16. package/lib/types/client/PetSprite.d.ts.map +1 -0
  17. package/lib/types/client/{WhalePet.js → PetSprite.js} +44 -67
  18. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  19. package/lib/types/client/PluginSettingsCard.js +3 -1
  20. package/lib/types/client/index.d.ts +11 -8
  21. package/lib/types/client/index.d.ts.map +1 -1
  22. package/lib/types/client/index.js +30 -12
  23. package/lib/types/client/locales.d.ts +10 -10
  24. package/lib/types/client/locales.js +10 -10
  25. package/lib/types/client/pet-store.d.ts +5 -0
  26. package/lib/types/client/pet-store.d.ts.map +1 -1
  27. package/lib/types/client/pet-store.js +4 -0
  28. package/lib/types/client/settings-form.d.ts +62 -0
  29. package/lib/types/client/settings-form.d.ts.map +1 -1
  30. package/lib/types/client/settings-form.js +61 -12
  31. package/lib/types/client/spritesheet.d.ts +14 -48
  32. package/lib/types/client/spritesheet.d.ts.map +1 -1
  33. package/lib/types/client/spritesheet.js +13 -81
  34. package/lib/types/index.d.ts +24 -11
  35. package/lib/types/index.d.ts.map +1 -1
  36. package/lib/types/index.js +53 -33
  37. package/lib/types/ledger.d.ts +4 -2
  38. package/lib/types/ledger.d.ts.map +1 -1
  39. package/lib/types/ledger.js +11 -4
  40. package/lib/types/persist.d.ts +10 -3
  41. package/lib/types/persist.d.ts.map +1 -1
  42. package/lib/types/persist.js +32 -5
  43. package/lib/types/registry.d.ts +158 -0
  44. package/lib/types/registry.d.ts.map +1 -0
  45. package/lib/types/registry.js +276 -0
  46. package/lib/types/routes.d.ts +10 -10
  47. package/lib/types/routes.d.ts.map +1 -1
  48. package/lib/types/routes.js +169 -51
  49. package/lib/types/service.d.ts +46 -13
  50. package/lib/types/service.d.ts.map +1 -1
  51. package/lib/types/service.js +73 -16
  52. package/package.json +2 -2
  53. package/src/client/PetDockEntry.tsx +34 -26
  54. package/src/client/PetSettingsCard.tsx +67 -22
  55. package/src/client/{WhalePet.test.tsx → PetSprite.test.tsx} +56 -8
  56. package/src/client/{WhalePet.tsx → PetSprite.tsx} +55 -73
  57. package/src/client/PluginSettingsCard.tsx +7 -1
  58. package/src/client/index.ts +36 -14
  59. package/src/client/locales.ts +10 -10
  60. package/src/client/pet-store.ts +9 -0
  61. package/src/client/settings-card.module.css +3 -0
  62. package/src/client/settings-form.ts +114 -12
  63. package/src/client/spritesheet.ts +16 -92
  64. package/src/index.test.ts +23 -0
  65. package/src/index.ts +89 -45
  66. package/src/ledger.test.ts +5 -3
  67. package/src/ledger.ts +11 -4
  68. package/src/persist.test.ts +52 -3
  69. package/src/persist.ts +42 -8
  70. package/src/registry.test.ts +132 -0
  71. package/src/registry.ts +400 -0
  72. package/src/routes.ts +166 -53
  73. package/src/service.ts +102 -20
  74. package/lib/types/client/WhalePet.d.ts.map +0 -1
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Global floating pet entry. The pet is host-global (its state, display and
3
- * interactions live on `/api/pet/*` endpoints with no session dimension), so
3
+ * interactions live on '/api/pet/*' endpoints with no session dimension), so
4
4
  * it must not ride a session-scoped slot — on the new-conversation screen no
5
5
  * session exists to scope a slot by, and the pet would vanish (issue #48).
6
- * The client half therefore mounts this entry straight onto `document.body`
7
- * (see index.ts): while visible it renders the floating WhalePet (a portal),
8
- * while hidden it renders a fixed-position summon button.
6
+ * The client half therefore mounts this entry straight onto 'document.body'
7
+ * (see index.ts): while visible it renders the floating PetSprite (a
8
+ * portal), while hidden it renders a fixed-position summon button. Which
9
+ * sprite renders is decided by the host snapshot's pet id resolved against
10
+ * the registry list — no per-pet component exists.
9
11
  * @module @linxin666/dsh-pet/client/PetDockEntry
10
12
  */
11
13
  import { type ReactElement } from 'react';
@@ -14,21 +16,21 @@ import type { PetStoreInstance } from './pet-store.ts';
14
16
  import { NS } from './locales.ts';
15
17
  /** Injected actions handed to the dock entry component. */
16
18
  export interface PetInjected {
17
- /** The app-wide pet store instance (snapshot + feedback). */
19
+ /** The app-wide pet store instance (snapshot + registry list + feedback). */
18
20
  store: PetStoreInstance;
19
- /** Ensure the first snapshot is fetched (called on mount). */
21
+ /** Ensure the first snapshot (and registry list) is fetched (called on mount). */
20
22
  ensure: () => void;
21
- /** Pet the whale girl (click). */
23
+ /** Pet the sprite (click). */
22
24
  pet: () => void;
23
- /** Feed the whale girl. */
25
+ /** Feed the sprite. */
24
26
  feed: () => void;
25
- /** Hide the whale girl. */
27
+ /** Hide the sprite. */
26
28
  hide: () => void;
27
- /** Summon the hidden whale girl back. */
29
+ /** Summon the hidden sprite back. */
28
30
  summon: () => void;
29
31
  /** Persist a drag position. */
30
32
  dragEnd: (right: number, bottom: number) => void;
31
- /** Rename the pet (persisted by the host). */
33
+ /** Rename the selected pet (persisted by the host). */
32
34
  rename: (name: string) => void;
33
35
  /** Clear the reaction bubble. */
34
36
  feedbackDone: () => void;
@@ -36,7 +38,7 @@ export interface PetInjected {
36
38
  /** Composed props of the global pet entry (locale + injected; no slot runtime share). */
37
39
  export type PetDockEntryProps = PetInjected & PropsLocale<typeof NS>;
38
40
  /**
39
- * Dock entry: while the pet is visible, mount the floating WhalePet (it
41
+ * Dock entry: while the pet is visible, mount the floating PetSprite (it
40
42
  * portals itself onto document.body); while hidden, render the summon
41
43
  * button so the pet can always come back. The store is the plugin-owned
42
44
  * single instance — the slot system provides none because the pet is
@@ -1 +1 @@
1
- {"version":3,"file":"PetDockEntry.d.ts","sourceRoot":"","sources":["../../../src/client/PetDockEntry.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;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,6DAA6D;IAC7D,KAAK,EAAE,gBAAgB,CAAA;IACvB,8DAA8D;IAC9D,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,kCAAkC;IAClC,GAAG,EAAE,MAAM,IAAI,CAAA;IACf,2BAA2B;IAC3B,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,2BAA2B;IAC3B,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,yCAAyC;IACzC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,+BAA+B;IAC/B,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAChD,8CAA8C;IAC9C,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,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,CA8CnE"}
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,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,CAoDnE"}
@@ -1,20 +1,22 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
3
  * Global floating pet entry. The pet is host-global (its state, display and
4
- * interactions live on `/api/pet/*` endpoints with no session dimension), so
4
+ * interactions live on '/api/pet/*' endpoints with no session dimension), so
5
5
  * it must not ride a session-scoped slot — on the new-conversation screen no
6
6
  * session exists to scope a slot by, and the pet would vanish (issue #48).
7
- * The client half therefore mounts this entry straight onto `document.body`
8
- * (see index.ts): while visible it renders the floating WhalePet (a portal),
9
- * while hidden it renders a fixed-position summon button.
7
+ * The client half therefore mounts this entry straight onto 'document.body'
8
+ * (see index.ts): while visible it renders the floating PetSprite (a
9
+ * portal), while hidden it renders a fixed-position summon button. Which
10
+ * sprite renders is decided by the host snapshot's pet id resolved against
11
+ * the registry list — no per-pet component exists.
10
12
  * @module @linxin666/dsh-pet/client/PetDockEntry
11
13
  */
12
14
  import { useEffect, useSyncExternalStore } from 'react';
13
- import { WhalePet } from "./WhalePet.js";
15
+ import { PetSprite } from "./PetSprite.js";
14
16
  import styles from './pet.module.css';
15
17
  const DEFAULT_DISPLAY = { visible: true, size: 160, right: 24, bottom: 20 };
16
18
  /**
17
- * Dock entry: while the pet is visible, mount the floating WhalePet (it
19
+ * Dock entry: while the pet is visible, mount the floating PetSprite (it
18
20
  * portals itself onto document.body); while hidden, render the summon
19
21
  * button so the pet can always come back. The store is the plugin-owned
20
22
  * single instance — the slot system provides none because the pet is
@@ -25,12 +27,15 @@ export function PetDockEntry(props) {
25
27
  const ui = useSyncExternalStore(store.subscribe, store.getSnapshot);
26
28
  const snapshot = ui.snapshot;
27
29
  const feedback = ui.feedback;
30
+ const definition = ui.pets.find(entry => entry.id === snapshot?.pet.id) ?? null;
28
31
  const visible = snapshot?.display.visible ?? true;
29
32
  useEffect(() => {
30
33
  ensure();
31
34
  }, [ensure]);
32
35
  if (visible) {
33
- return (_jsx("span", { "data-pet-dock": true, "data-testid": "pet-dock", children: _jsx(WhalePet, { snapshot: snapshot, display: snapshot?.display ?? DEFAULT_DISPLAY, feedback: feedback, onPet: props.pet, onFeed: props.feed, onHide: props.hide, onDragEnd: props.dragEnd, onRename: props.rename, onFeedbackDone: props.feedbackDone, t: props.t }) }));
36
+ return (_jsx("span", { "data-pet-dock": true, "data-testid": "pet-dock", children: snapshot === null || definition === null
37
+ ? null
38
+ : (_jsx(PetSprite, { snapshot: snapshot, definition: definition, display: snapshot.display, feedback: feedback, onPet: props.pet, onFeed: props.feed, onHide: props.hide, onDragEnd: props.dragEnd, onRename: props.rename, onFeedbackDone: props.feedbackDone, t: props.t })) }));
34
39
  }
35
40
  const display = snapshot?.display ?? DEFAULT_DISPLAY;
36
41
  return (_jsx("button", { type: "button", className: styles.summon, style: {
@@ -38,5 +43,5 @@ export function PetDockEntry(props) {
38
43
  right: display.right,
39
44
  bottom: display.bottom,
40
45
  zIndex: 2147483000,
41
- }, onClick: props.summon, "data-testid": "pet-summon", children: props.t('pet.summon', { name: snapshot?.name ?? '鲸鱼娘' }) }));
46
+ }, onClick: props.summon, "data-testid": "pet-summon", children: props.t('pet.summon', { name: snapshot?.name ?? '' }) }));
42
47
  }
@@ -1,7 +1,9 @@
1
1
  /**
2
- * The pet settings card: display layout and name, bound to the `pet` settings
3
- * namespace the host plugin registers. Registered into the
4
- * `settings.plugin.item` slot the plugin-configuration section renders.
2
+ * The pet settings card: pet selection plus display layout, bound to the
3
+ * 'pet' settings namespace the host plugin registers. Registered into the
4
+ * 'web-ui.plugin.item' slot the plugin-configuration section renders. The
5
+ * petId choices come from the registry endpoint ('/api/pet/pets') — the same
6
+ * list the sprite renders from — so the card carries no per-pet knowledge.
5
7
  */
6
8
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
7
9
  import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
@@ -18,8 +20,8 @@ export interface PetSettings {
18
20
  right?: number;
19
21
  /** Vertical inset from the viewport bottom edge, px. */
20
22
  bottom?: number;
21
- /** User-customizable pet display name. */
22
- name?: string;
23
+ /** Selected pet id (a registry entry). */
24
+ petId?: string;
23
25
  }
24
26
  /** What the pet settings card renders. */
25
27
  export interface PetSettingsCardState extends CardShell {
@@ -33,8 +35,13 @@ export interface PetSettingsCardState extends CardShell {
33
35
  right: CardFieldState;
34
36
  /** Bottom inset. */
35
37
  bottom: CardFieldState;
36
- /** Pet name. */
37
- name: CardFieldState;
38
+ /** Selected pet. */
39
+ petId: CardFieldState;
40
+ /** Pet choices (registry ids + display names), loaded from the host. */
41
+ petChoices: readonly {
42
+ value: string;
43
+ label: string;
44
+ }[];
38
45
  }
39
46
  /** The registration-side face the card's slot entry injects. */
40
47
  export interface PetSettingsCardFace extends CardActions {
@@ -43,12 +50,18 @@ export interface PetSettingsCardFace extends CardActions {
43
50
  petSettingsCard: SnapshotStore<PetSettingsCardState>;
44
51
  };
45
52
  }
46
- /** Bridges the `pet` scope onto the card's staged form. */
53
+ /** Bridges the 'pet' scope onto the card's staged form. */
47
54
  export declare class PetSettingsCardController {
48
55
  private readonly form;
49
56
  private readonly store;
50
- /** @param scope - the bound settings scope for the `pet` namespace. */
57
+ private readonly petChoices;
58
+ private readonly petLabels;
59
+ private loaded;
60
+ private attempts;
61
+ /** @param scope - the bound settings scope for the 'pet' namespace. */
51
62
  constructor(scope: SettingsScope<PetSettings>);
63
+ /** Resolve the registry choices once (retried a few times on failure). */
64
+ private loadPets;
52
65
  private projection;
53
66
  /**
54
67
  * Build the face the card's slot registration injects.
@@ -1 +1 @@
1
- {"version":3,"file":"PetSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PetSettingsCard.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAE1F,OAAO,EAAkD,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,IAAI,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExJ,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qBAAqB;IACrB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,0CAA0C;AAC1C,MAAM,WAAW,oBAAqB,SAAQ,SAAS;IACrD,4BAA4B;IAC5B,OAAO,EAAE,cAAc,CAAA;IACvB,qBAAqB;IACrB,OAAO,EAAE,cAAc,CAAA;IACvB,iBAAiB;IACjB,IAAI,EAAE,cAAc,CAAA;IACpB,mBAAmB;IACnB,KAAK,EAAE,cAAc,CAAA;IACrB,oBAAoB;IACpB,MAAM,EAAE,cAAc,CAAA;IACtB,gBAAgB;IAChB,IAAI,EAAE,cAAc,CAAA;CACrB;AAED,gEAAgE;AAChE,MAAM,WAAW,mBAAoB,SAAQ,WAAW;IACtD,KAAK,EAAE;QACL,iEAAiE;QACjE,eAAe,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAA;KACrD,CAAA;CACF;AAED,2DAA2D;AAC3D,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAE3D,uEAAuE;gBAC3D,KAAK,EAAE,aAAa,CAAC,WAAW,CAAC;IAY7C,OAAO,CAAC,UAAU;IAYlB;;;OAGG;IACH,MAAM,IAAI,mBAAmB;CAG9B;AAED,0DAA0D;AAC1D,MAAM,MAAM,oBAAoB,GAC9B,YAAY,CAAC,oBAAoB,CAAC,GAChC,WAAW,CAAC,KAAK,CAAC,GAClB,UAAU,CAAC,mBAAmB,CAAC,CAAA;AAEnC;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,+BAoF1D"}
1
+ {"version":3,"file":"PetSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PetSettingsCard.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC7F,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,wCAAwC,CAAA;AAE1F,OAAO,EAAoD,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,IAAI,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAE1J,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qBAAqB;IACrB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,0CAA0C;AAC1C,MAAM,WAAW,oBAAqB,SAAQ,SAAS;IACrD,4BAA4B;IAC5B,OAAO,EAAE,cAAc,CAAA;IACvB,qBAAqB;IACrB,OAAO,EAAE,cAAc,CAAA;IACvB,iBAAiB;IACjB,IAAI,EAAE,cAAc,CAAA;IACpB,mBAAmB;IACnB,KAAK,EAAE,cAAc,CAAA;IACrB,oBAAoB;IACpB,MAAM,EAAE,cAAc,CAAA;IACtB,oBAAoB;IACpB,KAAK,EAAE,cAAc,CAAA;IACrB,wEAAwE;IACxE,UAAU,EAAE,SAAS;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CACxD;AAED,gEAAgE;AAChE,MAAM,WAAW,mBAAoB,SAAQ,WAAW;IACtD,KAAK,EAAE;QACL,iEAAiE;QACjE,eAAe,EAAE,aAAa,CAAC,oBAAoB,CAAC,CAAA;KACrD,CAAA;CACF;AAeD,2DAA2D;AAC3D,qBAAa,yBAAyB;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAI3D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAe;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4B;IACtD,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,QAAQ,CAAI;IAEpB,uEAAuE;gBAC3D,KAAK,EAAE,aAAa,CAAC,WAAW,CAAC;IAa7C,0EAA0E;YAC5D,QAAQ;IAgBtB,OAAO,CAAC,UAAU;IAalB;;;OAGG;IACH,MAAM,IAAI,mBAAmB;CAG9B;AAED,0DAA0D;AAC1D,MAAM,MAAM,oBAAoB,GAC9B,YAAY,CAAC,oBAAoB,CAAC,GAChC,WAAW,CAAC,KAAK,CAAC,GAClB,UAAU,CAAC,mBAAmB,CAAC,CAAA;AAEnC;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,+BAsF1D"}
@@ -1,11 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { PluginSettingsCard, ValueField, BooleanField } from "./PluginSettingsCard.js";
3
- import { CardForm, booleanField, numberField, textField } from "./settings-form.js";
4
- /** Bridges the `pet` scope onto the card's staged form. */
2
+ import { PluginSettingsCard, ValueField, BooleanField, ChoiceField } from "./PluginSettingsCard.js";
3
+ import { CardForm, booleanField, choiceField, numberField } from "./settings-form.js";
4
+ /** Fetch the registry list (the same data the sprite renders from). */
5
+ async function fetchPetChoices() {
6
+ const response = await fetch('/api/pet/pets');
7
+ if (!response.ok)
8
+ throw new Error('pet pets failed: ' + response.status);
9
+ return (await response.json());
10
+ }
11
+ /** Bridges the 'pet' scope onto the card's staged form. */
5
12
  export class PetSettingsCardController {
6
13
  form;
7
14
  store;
8
- /** @param scope - the bound settings scope for the `pet` namespace. */
15
+ // The choice list rides a mutable array shared with the choiceField spec,
16
+ // so loading the registry re-validates and re-formats the petId field
17
+ // without rebuilding the form.
18
+ petChoices = [];
19
+ petLabels = new Map();
20
+ loaded = false;
21
+ attempts = 0;
22
+ /** @param scope - the bound settings scope for the 'pet' namespace. */
9
23
  constructor(scope) {
10
24
  this.form = new CardForm(scope, [
11
25
  booleanField('enabled'),
@@ -13,9 +27,29 @@ export class PetSettingsCardController {
13
27
  numberField('size'),
14
28
  numberField('right'),
15
29
  numberField('bottom'),
16
- textField('name'),
30
+ choiceField('petId', this.petChoices),
17
31
  ]);
18
32
  this.store = this.form.bind(() => this.projection());
33
+ void this.loadPets();
34
+ }
35
+ /** Resolve the registry choices once (retried a few times on failure). */
36
+ async loadPets() {
37
+ if (this.loaded)
38
+ return;
39
+ try {
40
+ const list = await fetchPetChoices();
41
+ this.petChoices.splice(0, this.petChoices.length, ...list.map(choice => choice.id));
42
+ for (const choice of list)
43
+ this.petLabels.set(choice.id, choice.displayName);
44
+ this.loaded = true;
45
+ this.store.set(this.projection());
46
+ }
47
+ catch {
48
+ this.attempts += 1;
49
+ if (this.attempts < 3) {
50
+ window.setTimeout(() => { void this.loadPets(); }, 3000);
51
+ }
52
+ }
19
53
  }
20
54
  projection() {
21
55
  return {
@@ -25,7 +59,8 @@ export class PetSettingsCardController {
25
59
  size: this.form.field('size'),
26
60
  right: this.form.field('right'),
27
61
  bottom: this.form.field('bottom'),
28
- name: this.form.field('name'),
62
+ petId: this.form.field('petId'),
63
+ petChoices: this.petChoices.map(id => ({ value: id, label: this.petLabels.get(id) ?? id })),
29
64
  };
30
65
  }
31
66
  /**
@@ -51,5 +86,5 @@ export function PetSettingsCard(props) {
51
86
  invalidLabel: t('settings.invalidNumber'),
52
87
  disabled,
53
88
  };
54
- return (_jsxs(PluginSettingsCard, { t: t, titleKey: "settings.title", descriptionKey: "settings.description", state: state, onSave: props.save, onDiscard: props.discard, children: [_jsx(BooleanField, { id: "settings-pet-enabled", label: t('settings.enabled'), hint: t('settings.enabledHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.enabled, onEdit: (text) => { props.edit('enabled', text); }, onReset: () => { props.resetField('enabled'); } }), _jsx(BooleanField, { id: "settings-pet-visible", label: t('settings.visible'), hint: t('settings.visibleHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.visible, onEdit: (text) => { props.edit('visible', text); }, onReset: () => { props.resetField('visible'); } }), _jsx(ValueField, { id: "settings-pet-size", label: t('settings.size'), hint: t('settings.sizeHint'), numeric: true, ...fieldProps, ...state.size, onEdit: (text) => { props.edit('size', text); }, onReset: () => { props.resetField('size'); } }), _jsx(ValueField, { id: "settings-pet-right", label: t('settings.right'), hint: t('settings.rightHint'), numeric: true, ...fieldProps, ...state.right, onEdit: (text) => { props.edit('right', text); }, onReset: () => { props.resetField('right'); } }), _jsx(ValueField, { id: "settings-pet-bottom", label: t('settings.bottom'), hint: t('settings.bottomHint'), numeric: true, ...fieldProps, ...state.bottom, onEdit: (text) => { props.edit('bottom', text); }, onReset: () => { props.resetField('bottom'); } }), _jsx(ValueField, { id: "settings-pet-name", label: t('settings.name'), hint: t('settings.nameHint'), ...fieldProps, ...state.name, onEdit: (text) => { props.edit('name', text); }, onReset: () => { props.resetField('name'); } })] }));
89
+ return (_jsxs(PluginSettingsCard, { t: t, titleKey: "settings.title", descriptionKey: "settings.description", state: state, onSave: props.save, onDiscard: props.discard, children: [_jsx(BooleanField, { id: "settings-pet-enabled", label: t('settings.enabled'), hint: t('settings.enabledHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.enabled, onEdit: (text) => { props.edit('enabled', text); }, onReset: () => { props.resetField('enabled'); } }), _jsx(ChoiceField, { id: "settings-pet-pet", label: t('settings.pet'), hint: t('settings.petHint'), inheritLabel: t('settings.inherit'), ...fieldProps, ...state.petId, choices: state.petChoices, onEdit: (text) => { props.edit('petId', text); }, onReset: () => { props.resetField('petId'); } }), _jsx(BooleanField, { id: "settings-pet-visible", label: t('settings.visible'), hint: t('settings.visibleHint'), inheritLabel: t('settings.inherit'), onLabel: t('settings.on'), offLabel: t('settings.off'), ...fieldProps, ...state.visible, onEdit: (text) => { props.edit('visible', text); }, onReset: () => { props.resetField('visible'); } }), _jsx(ValueField, { id: "settings-pet-size", label: t('settings.size'), hint: t('settings.sizeHint'), numeric: true, ...fieldProps, ...state.size, onEdit: (text) => { props.edit('size', text); }, onReset: () => { props.resetField('size'); } }), _jsx(ValueField, { id: "settings-pet-right", label: t('settings.right'), hint: t('settings.rightHint'), numeric: true, ...fieldProps, ...state.right, onEdit: (text) => { props.edit('right', text); }, onReset: () => { props.resetField('right'); } }), _jsx(ValueField, { id: "settings-pet-bottom", label: t('settings.bottom'), hint: t('settings.bottomHint'), numeric: true, ...fieldProps, ...state.bottom, onEdit: (text) => { props.edit('bottom', text); }, onReset: () => { props.resetField('bottom'); } })] }));
55
90
  }
@@ -1,38 +1,39 @@
1
1
  /**
2
- * Whale-girl companion component — the browser half's centerpiece. Renders a
2
+ * Pet sprite companion component — the browser half's centerpiece. Renders a
3
3
  * fixed-position floating sprite (React portal onto document.body), plays
4
- * the spritesheet track matching the host animation snapshot, and exposes
5
- * the interaction surface: click to pet, hover panel with feed/hide, drag to
6
- * reposition (persisted via setConfig).
7
- * @module @linxin666/dsh-pet/client/WhalePet
4
+ * the track matching the host animation snapshot, and exposes the
5
+ * interaction surface: click to pet, hover panel with feed/rename/hide, drag
6
+ * to reposition (persisted via setConfig). Everything visual comes from the
7
+ * pet definition the host serves ('/api/pet/pets' + the state snapshot's
8
+ * pet id), so one component renders every registry entry.
9
+ * @module @linxin666/dsh-pet/client/PetSprite
8
10
  */
9
11
  import type { ReactPortal } from 'react';
10
12
  import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
11
13
  import type { PetDisplayConfig } from '../persist.ts';
12
14
  import type { PetStateView } from '../service.ts';
15
+ import type { PetDefinition } from '../registry.ts';
13
16
  import type { PetFeedback } from './pet-store.ts';
14
17
  import { NS } from './locales.ts';
15
- /** Browser URL of the whale-girl atlas (served by the host half's own route). */
16
- export declare const PET_SPRITESHEET_URL = "/pet/whale/spritesheet.webp";
17
- /** Browser URL of the whale-girl manifest (authoritative per-row frame counts). */
18
- export declare const PET_MANIFEST_URL = "/pet/whale/pet.json";
19
- /** Props injected by the slot registration (store actions + locale). */
20
- export interface WhalePetProps {
18
+ /** Props injected by the plugin apply body (store actions + locale). */
19
+ export interface PetSpriteProps {
21
20
  /** Latest host snapshot; null while loading. */
22
21
  snapshot: PetStateView | null;
22
+ /** The selected pet's registry definition (atlas URL + geometry + tracks). */
23
+ definition: PetDefinition;
23
24
  /** Display configuration (persisted by the host). */
24
25
  display: PetDisplayConfig;
25
26
  /** Active reaction bubble, if any. */
26
27
  feedback: PetFeedback | null;
27
- /** Pet the whale girl (click). */
28
+ /** Pet the sprite (click). */
28
29
  onPet: () => void;
29
- /** Feed the whale girl (panel button). */
30
+ /** Feed the sprite (panel button). */
30
31
  onFeed: () => void;
31
- /** Hide the whale girl (panel button). */
32
+ /** Hide the sprite (panel button). */
32
33
  onHide: () => void;
33
34
  /** Persist a drag position. */
34
35
  onDragEnd: (right: number, bottom: number) => void;
35
- /** Rename the pet (persisted by the host). */
36
+ /** Rename the selected pet (persisted by the host). */
36
37
  onRename: (name: string) => void;
37
38
  /** Clear the reaction bubble (after its CSS animation). */
38
39
  onFeedbackDone: () => void;
@@ -41,9 +42,9 @@ export interface WhalePetProps {
41
42
  }
42
43
  /**
43
44
  * The floating pet. The spritesheet frame advances on requestAnimationFrame
44
- * with per-frame durations from TRACKS; the atlas image is loaded once and
45
- * the background position is written straight to the sprite element (no
46
- * per-frame React state).
45
+ * with per-frame durations from the definition's tracks; the atlas image is
46
+ * loaded once and the background position is written straight to the sprite
47
+ * element (no per-frame React state).
47
48
  */
48
- export declare function WhalePet(props: WhalePetProps): ReactPortal;
49
- //# sourceMappingURL=WhalePet.d.ts.map
49
+ export declare function PetSprite(props: PetSpriteProps): ReactPortal;
50
+ //# sourceMappingURL=PetSprite.d.ts.map
@@ -0,0 +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,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,CAuS5D"}
@@ -1,36 +1,34 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
- * Whale-girl companion component — the browser half's centerpiece. Renders a
3
+ * Pet sprite companion component — the browser half's centerpiece. Renders a
4
4
  * fixed-position floating sprite (React portal onto document.body), plays
5
- * the spritesheet track matching the host animation snapshot, and exposes
6
- * the interaction surface: click to pet, hover panel with feed/hide, drag to
7
- * reposition (persisted via setConfig).
8
- * @module @linxin666/dsh-pet/client/WhalePet
5
+ * the track matching the host animation snapshot, and exposes the
6
+ * interaction surface: click to pet, hover panel with feed/rename/hide, drag
7
+ * to reposition (persisted via setConfig). Everything visual comes from the
8
+ * pet definition the host serves ('/api/pet/pets' + the state snapshot's
9
+ * pet id), so one component renders every registry entry.
10
+ * @module @linxin666/dsh-pet/client/PetSprite
9
11
  */
10
12
  import { useEffect, useRef, useState } from 'react';
11
13
  import { createPortal } from 'react-dom';
12
- import { framePosition, FRAME_WIDTH, FRAME_HEIGHT, FRAME_COLUMNS, TRACKS, rowOfTrack, trimTrack, detectFrameCounts } from "./spritesheet.js";
14
+ import clsx from 'clsx';
15
+ import { framePosition, rowOfTrack, trimTrack } from "./spritesheet.js";
13
16
  import styles from './pet.module.css';
14
- /** Browser URL of the whale-girl atlas (served by the host half's own route). */
15
- export const PET_SPRITESHEET_URL = '/pet/whale/spritesheet.webp';
16
- /** Browser URL of the whale-girl manifest (authoritative per-row frame counts). */
17
- export const PET_MANIFEST_URL = '/pet/whale/pet.json';
18
17
  /** Clamp a drag offset inside the viewport with a margin. */
19
18
  function clampOffset(value, max) {
20
19
  return Math.max(0, Math.min(max, value));
21
20
  }
22
21
  /**
23
22
  * The floating pet. The spritesheet frame advances on requestAnimationFrame
24
- * with per-frame durations from TRACKS; the atlas image is loaded once and
25
- * the background position is written straight to the sprite element (no
26
- * per-frame React state).
23
+ * with per-frame durations from the definition's tracks; the atlas image is
24
+ * loaded once and the background position is written straight to the sprite
25
+ * element (no per-frame React state).
27
26
  */
28
- export function WhalePet(props) {
29
- const { snapshot, display, feedback } = props;
27
+ export function PetSprite(props) {
28
+ const { snapshot, definition, display, feedback } = props;
30
29
  const spriteRef = useRef(null);
31
30
  const floatRef = useRef(null);
32
31
  const [imageReady, setImageReady] = useState(false);
33
- const [frameCounts, setFrameCounts] = useState(null);
34
32
  const [hovered, setHovered] = useState(false);
35
33
  const [renaming, setRenaming] = useState(false);
36
34
  const [nameDraft, setNameDraft] = useState('');
@@ -42,66 +40,46 @@ export function WhalePet(props) {
42
40
  index: 0,
43
41
  elapsed: 0,
44
42
  });
45
- // Load the atlas once; then resolve per-row frame counts so tracks never
46
- // play the transparent trailing cells of a short row. One decoded Image
47
- // feeds both the sprite render and the frame-count detection. The counts
48
- // prefer the authoritatively recorded `frames` field on the pet.json
49
- // manifest route and only fall back to the getImageData atlas scan when
50
- // that field is absent (older manifests).
43
+ const cell = definition.cell;
44
+ const columns = definition.columns;
45
+ const rows = definition.rows;
46
+ const tracks = definition.tracks;
47
+ // Load the atlas once; the definition carries the authoritative per-row
48
+ // frame counts and per-track durations, so nothing else is fetched.
51
49
  useEffect(() => {
52
50
  let cancelled = false;
53
51
  const img = new Image();
54
52
  img.onload = () => {
55
- if (cancelled)
56
- return;
57
- setImageReady(true);
58
- fetch(PET_MANIFEST_URL)
59
- .then((res) => (res.ok ? res.json() : Promise.resolve({})))
60
- .then((manifest) => {
61
- if (cancelled)
62
- return;
63
- const frames = manifest.frames;
64
- if (Array.isArray(frames) && frames.length === 9 && frames.every((n) => typeof n === 'number')) {
65
- setFrameCounts(frames);
66
- }
67
- else {
68
- setFrameCounts(detectFrameCounts(img));
69
- }
70
- })
71
- .catch(() => {
72
- if (!cancelled)
73
- setFrameCounts(detectFrameCounts(img));
74
- });
53
+ if (!cancelled)
54
+ setImageReady(true);
75
55
  };
76
- img.src = PET_SPRITESHEET_URL;
56
+ img.src = definition.atlasUrl;
77
57
  return () => {
78
58
  cancelled = true;
79
59
  img.onload = null;
80
60
  };
81
- }, []);
61
+ }, [definition.atlasUrl]);
82
62
  // Frame loop: advance the current track and write background-position.
83
63
  // Offsets must be in SCALED coordinates (background-position applies to the
84
64
  // scaled background image), so the current sprite scale rides a ref that
85
65
  // the loop reads every tick. Under prefers-reduced-motion the sprite holds
86
66
  // its track's first frame instead of animating (presentation-only; the
87
67
  // animation state machine is untouched).
88
- const spriteScale = display.size / FRAME_HEIGHT;
68
+ const spriteScale = display.size / cell.height;
89
69
  const animation = snapshot?.animation ?? 'idle';
90
70
  const scaleRef = useRef(spriteScale);
91
71
  scaleRef.current = spriteScale;
92
72
  useEffect(() => {
93
73
  const reduceMotion = typeof window !== 'undefined'
94
74
  && 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);
95
77
  // Paint one static sprite frame up front either way, so the pet is never
96
78
  // blank while the loop heat-up runs.
97
- const row = rowOfTrack(animation);
98
- const track = frameCounts === null
99
- ? TRACKS[animation]
100
- : trimTrack(TRACKS[animation], frameCounts[row] ?? TRACKS[animation].frames.length);
101
79
  const leadCol = track.frames[0];
102
- const lead = framePosition(row, leadCol, scaleRef.current);
80
+ const lead = framePosition(cell, columns, row, leadCol, scaleRef.current);
103
81
  if (spriteRef.current !== null) {
104
- spriteRef.current.style.backgroundPosition = `${lead.x}px ${lead.y}px`;
82
+ spriteRef.current.style.backgroundPosition = lead.x + 'px ' + lead.y + 'px';
105
83
  }
106
84
  if (reduceMotion)
107
85
  return;
@@ -113,9 +91,7 @@ export function WhalePet(props) {
113
91
  // Trim the track to the row's real frame count (transparent cells
114
92
  // would render as a vanishing pet).
115
93
  const row = rowOfTrack(animation);
116
- const track = frameCounts === null
117
- ? TRACKS[animation]
118
- : trimTrack(TRACKS[animation], frameCounts[row] ?? TRACKS[animation].frames.length);
94
+ const track = trimTrack(tracks[animation], rows[row] ?? tracks[animation].frames.length);
119
95
  const st = frameRef.current;
120
96
  if (st.track !== animation) {
121
97
  st.track = animation;
@@ -138,17 +114,17 @@ export function WhalePet(props) {
138
114
  }
139
115
  }
140
116
  const col = track.frames[st.index];
141
- const { x, y } = framePosition(row, col, scaleRef.current);
117
+ const pos = framePosition(cell, columns, row, col, scaleRef.current);
142
118
  if (spriteRef.current !== null) {
143
- spriteRef.current.style.backgroundPosition = `${x}px ${y}px`;
119
+ spriteRef.current.style.backgroundPosition = pos.x + 'px ' + pos.y + 'px';
144
120
  }
145
121
  raf = requestAnimationFrame(tick);
146
122
  };
147
123
  raf = requestAnimationFrame(tick);
148
124
  return () => cancelAnimationFrame(raf);
149
- }, [animation, frameCounts]);
125
+ }, [animation, cell, columns, rows, tracks]);
150
126
  // Auto-clear the feedback bubble after its CSS animation. The callback
151
- // rides a ref so re-renders never reset the timer: the 800ms poll rebuilds
127
+ // rides a ref so re-renders never reset the timer: the 2s poll rebuilds
152
128
  // `props` every tick, and depending on it would starve the timeout.
153
129
  const feedbackDoneRef = useRef(props.onFeedbackDone);
154
130
  feedbackDoneRef.current = props.onFeedbackDone;
@@ -160,7 +136,7 @@ export function WhalePet(props) {
160
136
  }, [feedback]);
161
137
  // Dragging: pointer events on the sprite; position is right/bottom based.
162
138
  // `draggedRef` records whether the pointer actually moved, so the browser's
163
- // trailing click (fired after pointerup) does not pet the whale.
139
+ // trailing click (fired after pointerup) does not pet the sprite.
164
140
  const draggedRef = useRef(false);
165
141
  const clearHideTimer = () => {
166
142
  if (hideTimerRef.current !== null) {
@@ -196,9 +172,10 @@ export function WhalePet(props) {
196
172
  props.onDragEnd(dragPos.right, dragPos.bottom);
197
173
  };
198
174
  const pos = dragPos ?? { right: display.right, bottom: display.bottom };
199
- const spriteWidth = Math.round(FRAME_WIDTH * spriteScale);
200
- const spriteHeight = Math.round(FRAME_HEIGHT * spriteScale);
175
+ const spriteWidth = Math.round(cell.width * spriteScale);
176
+ const spriteHeight = Math.round(cell.height * spriteScale);
201
177
  const statusBubble = feedback === null && !hovered ? snapshot?.bubble : undefined;
178
+ const displayName = snapshot?.name ?? definition.displayName;
202
179
  const float = (_jsxs("div", { ref: floatRef, className: styles.float, style: { right: pos.right, bottom: pos.bottom, zIndex: 2147483000 }, onPointerEnter: () => {
203
180
  clearHideTimer();
204
181
  setHovered(true);
@@ -208,7 +185,7 @@ export function WhalePet(props) {
208
185
  // container. Treat a target still inside the container's DOM (the
209
186
  // overflowed panel) as "still hovering"; otherwise give the pointer a
210
187
  // short grace period to reach the panel across the gap above it. The
211
- // bridge (`.panel::after`) keeps the pointer inside the hit area, and
188
+ // bridge ('.panel::after') keeps the pointer inside the hit area, and
212
189
  // the grace period covers a slow mouse crossing the remaining sliver.
213
190
  const next = e.relatedTarget;
214
191
  if (next instanceof Node && floatRef.current?.contains(next))
@@ -218,8 +195,8 @@ export function WhalePet(props) {
218
195
  }, children: [_jsx("div", { ref: spriteRef, className: styles.sprite, style: {
219
196
  width: spriteWidth,
220
197
  height: spriteHeight,
221
- backgroundImage: imageReady ? `url(${PET_SPRITESHEET_URL})` : undefined,
222
- backgroundSize: `${FRAME_WIDTH * FRAME_COLUMNS * spriteScale}px ${FRAME_HEIGHT * 9 * spriteScale}px`,
198
+ backgroundImage: imageReady ? 'url(' + definition.atlasUrl + ')' : undefined,
199
+ backgroundSize: (cell.width * columns * spriteScale) + 'px ' + (cell.height * rows.length * spriteScale) + 'px',
223
200
  backgroundRepeat: 'no-repeat',
224
201
  backgroundPosition: '0 0',
225
202
  cursor: dragRef.current === null ? 'grab' : 'grabbing',
@@ -229,7 +206,7 @@ export function WhalePet(props) {
229
206
  if (draggedRef.current)
230
207
  return;
231
208
  props.onPet();
232
- }, role: "button", "aria-label": "whale girl" }), feedback !== null && (_jsx("div", { className: `${styles.bubble} ${feedback.kind === 'feed' ? styles.bubbleFeed : styles.bubblePet}`, children: feedback.text }, feedback.at)), statusBubble !== undefined && (_jsx("div", { className: `${styles.bubble} ${styles.bubbleStatus}`, role: "status", "aria-live": "polite", children: statusBubble })), hovered && dragRef.current === null && (_jsx("div", { className: styles.panel, onPointerEnter: () => {
209
+ }, 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)), 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: () => {
233
210
  // Reaching the panel (or its bridge) must cancel any hide timer
234
211
  // the container's pointerleave may have armed while the pointer
235
212
  // crossed the sliver between the sprite and the panel.
@@ -257,8 +234,8 @@ export function WhalePet(props) {
257
234
  props.onRename(trimmed);
258
235
  setRenaming(false);
259
236
  }
260
- }, children: props.t('pet.confirm') })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.nameCell, children: snapshot?.name ?? '鲸鱼娘' }), _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: () => {
261
- setNameDraft(snapshot?.name ?? '');
237
+ }, 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: () => {
238
+ setNameDraft(displayName);
262
239
  setRenaming(true);
263
240
  }, children: props.t('pet.rename') }), _jsx("button", { type: "button", className: styles.action, onClick: props.onHide, children: props.t('pet.hide') })] })] })) }))] }));
264
241
  return createPortal(float, document.body);
@@ -1 +1 @@
1
- {"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAGnD,oGAAoG;AACpG,eAAO,MAAM,cAAc,kMAUjB,CAAA;AAEV,6CAA6C;AAC7C,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzD,oEAAoE;AACpE,MAAM,MAAM,eAAe,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,IAAI,IAAI,GAAG,WAAW,CAAA;AAE9E,wDAAwD;AACxD,MAAM,WAAW,uBAAuB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IACnE,0CAA0C;IAC1C,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAA;IACnF,uCAAuC;IACvC,QAAQ,EAAE,IAAI,CAAA;IACd,4EAA4E;IAC5E,cAAc,EAAE,IAAI,CAAA;IACpB,kFAAkF;IAClF,KAAK,EAAE,SAAS,CAAA;IAChB,+BAA+B;IAC/B,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,8BAA8B;IAC9B,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,6BAA6B;IAC7B,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,uBAAuB,CAAC,IAAI,CAAC,sCA2GpG;AAED,oEAAoE;AACpE,MAAM,WAAW,UAAU;IACzB,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAA;IACV,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAA;IACZ,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,UAAU,EAAE,OAAO,CAAA;IACnB,6DAA6D;IAC7D,OAAO,EAAE,OAAO,CAAA;IAChB,qCAAqC;IACrC,eAAe,EAAE,MAAM,CAAA;IACvB,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,QAAQ,EAAE,OAAO,CAAA;IACjB,wBAAwB;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,oEAAoE;IACpE,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,kHAAkH;AAClH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG;IAC7C,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,+BAqCA;AAED,0CAA0C;AAC1C,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG;IAC/C,mCAAmC;IACnC,YAAY,EAAE,MAAM,CAAA;IACpB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;CACjB,+BAmCA;AAED,sDAAsD;AACtD,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG;IAC9C,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAA;IACpB,mEAAmE;IACnE,OAAO,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACzD,+BAsCA"}
1
+ {"version":3,"file":"PluginSettingsCard.d.ts","sourceRoot":"","sources":["../../../src/client/PluginSettingsCard.tsx"],"names":[],"mappings":"AACA;;;;;;;GAOG;AAEH,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAGnD,oGAAoG;AACpG,eAAO,MAAM,cAAc,kMAUjB,CAAA;AAEV,6CAA6C;AAC7C,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAEzD,oEAAoE;AACpE,MAAM,MAAM,eAAe,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,IAAI,IAAI,GAAG,WAAW,CAAA;AAE9E,wDAAwD;AACxD,MAAM,WAAW,uBAAuB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IACnE,0CAA0C;IAC1C,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,CAAA;IACnF,uCAAuC;IACvC,QAAQ,EAAE,IAAI,CAAA;IACd,4EAA4E;IAC5E,cAAc,EAAE,IAAI,CAAA;IACpB,kFAAkF;IAClF,KAAK,EAAE,SAAS,CAAA;IAChB,+BAA+B;IAC/B,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,8BAA8B;IAC9B,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,6BAA6B;IAC7B,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,uBAAuB,CAAC,IAAI,CAAC,sCAiHpG;AAED,oEAAoE;AACpE,MAAM,WAAW,UAAU;IACzB,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAA;IACV,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAA;IACZ,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,UAAU,EAAE,OAAO,CAAA;IACnB,6DAA6D;IAC7D,OAAO,EAAE,OAAO,CAAA;IAChB,qCAAqC;IACrC,eAAe,EAAE,MAAM,CAAA;IACvB,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAA;IACpB,gFAAgF;IAChF,QAAQ,EAAE,OAAO,CAAA;IACjB,wBAAwB;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,oEAAoE;IACpE,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,kHAAkH;AAClH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG;IAC7C,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,+BAqCA;AAED,0CAA0C;AAC1C,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG;IAC/C,mCAAmC;IACnC,YAAY,EAAE,MAAM,CAAA;IACpB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;CACjB,+BAmCA;AAED,sDAAsD;AACtD,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG;IAC9C,oEAAoE;IACpE,YAAY,EAAE,MAAM,CAAA;IACpB,mEAAmE;IACnE,OAAO,EAAE,aAAa,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACzD,+BAsCA"}
@@ -46,7 +46,9 @@ export function PluginSettingsCard(props) {
46
46
  : null] }));
47
47
  }
48
48
  return (_jsxs("li", { className: cardClass, children: [_jsxs("button", { type: "button", className: css.header, "aria-expanded": open, "aria-label": `${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`, onClick: () => { setOpen(!open); }, children: [_jsxs("span", { className: css.headText, children: [_jsx("span", { className: css.name, title: title, children: title }), _jsx("span", { className: css.description, title: description, children: description })] }), state.dirty ? _jsx("span", { className: css.pending, title: props.t('settings.unsaved'), children: props.t('settings.unsaved') }) : null, _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: open ? `${css.chevron} ${css.chevronOpen}` : css.chevron, 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
49
- ? (_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 ? _jsx("p", { className: css.failed, role: "status", children: props.t('settings.saveFailed') }) : 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') })] })] }))
49
+ ? (_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
50
+ ? (_jsxs("p", { className: css.failed, role: "status", children: [props.t('settings.saveFailed'), state.failedReason ? ' - ' + state.failedReason : ''] }))
51
+ : 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') })] })] }))
50
52
  : null] }));
51
53
  }
52
54
  /** A staged value field. `numeric` only hints the keypad: which drafts a field accepts is decided by its spec. */