@linxin666/dsh-pet 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +106 -0
  3. package/assets/whale/pet.json +7 -0
  4. package/assets/whale/previews/failed.gif +0 -0
  5. package/assets/whale/previews/idle.gif +0 -0
  6. package/assets/whale/previews/jumping.gif +0 -0
  7. package/assets/whale/previews/review.gif +0 -0
  8. package/assets/whale/previews/running-left.gif +0 -0
  9. package/assets/whale/previews/running-right.gif +0 -0
  10. package/assets/whale/previews/running.gif +0 -0
  11. package/assets/whale/previews/waiting.gif +0 -0
  12. package/assets/whale/previews/waving.gif +0 -0
  13. package/assets/whale/spritesheet.webp +0 -0
  14. package/cordis.patch.yml +10 -0
  15. package/lib/client.js +1515 -0
  16. package/lib/index.js +642 -0
  17. package/lib/invariant.js +34 -0
  18. package/lib/types/affinity.d.ts +83 -0
  19. package/lib/types/affinity.d.ts.map +1 -0
  20. package/lib/types/client/PetDockEntry.d.ts +44 -0
  21. package/lib/types/client/PetDockEntry.d.ts.map +1 -0
  22. package/lib/types/client/PetSettingsCard.d.ts +67 -0
  23. package/lib/types/client/PetSettingsCard.d.ts.map +1 -0
  24. package/lib/types/client/PluginSettingsCard.d.ts +78 -0
  25. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
  26. package/lib/types/client/WhalePet.d.ts +49 -0
  27. package/lib/types/client/WhalePet.d.ts.map +1 -0
  28. package/lib/types/client/index.d.ts +46 -0
  29. package/lib/types/client/index.d.ts.map +1 -0
  30. package/lib/types/client/locales.d.ts +101 -0
  31. package/lib/types/client/locales.d.ts.map +1 -0
  32. package/lib/types/client/pet-store.d.ts +49 -0
  33. package/lib/types/client/pet-store.d.ts.map +1 -0
  34. package/lib/types/client/settings-form.d.ts +119 -0
  35. package/lib/types/client/settings-form.d.ts.map +1 -0
  36. package/lib/types/client/slots-augment.d.ts +19 -0
  37. package/lib/types/client/slots-augment.d.ts.map +1 -0
  38. package/lib/types/client/spritesheet.d.ts +69 -0
  39. package/lib/types/client/spritesheet.d.ts.map +1 -0
  40. package/lib/types/index.d.ts +44 -0
  41. package/lib/types/index.d.ts.map +1 -0
  42. package/lib/types/invariant.d.ts +10 -0
  43. package/lib/types/invariant.d.ts.map +1 -0
  44. package/lib/types/persist.d.ts +45 -0
  45. package/lib/types/persist.d.ts.map +1 -0
  46. package/lib/types/routes.d.ts +22 -0
  47. package/lib/types/routes.d.ts.map +1 -0
  48. package/lib/types/service.d.ts +161 -0
  49. package/lib/types/service.d.ts.map +1 -0
  50. package/lib/types/state.d.ts +80 -0
  51. package/lib/types/state.d.ts.map +1 -0
  52. package/lib/types/treats.d.ts +59 -0
  53. package/lib/types/treats.d.ts.map +1 -0
  54. package/package.json +100 -0
  55. package/src/affinity.test.ts +74 -0
  56. package/src/affinity.ts +144 -0
  57. package/src/client/PetDockEntry.tsx +95 -0
  58. package/src/client/PetSettingsCard.tsx +186 -0
  59. package/src/client/PluginSettingsCard.tsx +207 -0
  60. package/src/client/WhalePet.tsx +342 -0
  61. package/src/client/css-modules.d.ts +6 -0
  62. package/src/client/index.ts +261 -0
  63. package/src/client/locales.ts +108 -0
  64. package/src/client/pet-store.ts +80 -0
  65. package/src/client/pet.module.css +185 -0
  66. package/src/client/settings-card.module.css +277 -0
  67. package/src/client/settings-form.ts +294 -0
  68. package/src/client/slots-augment.ts +27 -0
  69. package/src/client/spritesheet.ts +138 -0
  70. package/src/index.ts +148 -0
  71. package/src/invariant.ts +40 -0
  72. package/src/persist.test.ts +96 -0
  73. package/src/persist.ts +128 -0
  74. package/src/routes.ts +171 -0
  75. package/src/service.ts +389 -0
  76. package/src/state.test.ts +65 -0
  77. package/src/state.ts +156 -0
  78. package/src/treats.test.ts +86 -0
  79. package/src/treats.ts +101 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * dsh-pet browser half — registers the whale-girl into the conversation
3
+ * input selector row (the same every-phase row the git branch chip uses) and
4
+ * drives it from the host's same-origin `/api/pet/*` JSON endpoints: poll the
5
+ * host snapshot (~800 ms), forward interactions, persist drag positions. The
6
+ * row anchor mounts the floating pet via portal; when the pet is hidden the
7
+ * anchor becomes the summon button. Anchoring in the selector row (rather
8
+ * than the session-only composer dock band) keeps the pet floating on the
9
+ * new-conversation screen too, where no session exists to scope a slot by.
10
+ * @module @linxin666/dsh-pet/client
11
+ */
12
+
13
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
14
+ // Type-only: pulls the locale plugin's Context merge (ctx.locale).
15
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
16
+ // Type-only: pulls the settings-surface Context merge (ctx.settingsScope).
17
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
18
+ import type {} from '@deepseek-ai/dsh-client-ui-slots'
19
+ import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
20
+ import type { PetDisplayConfig } from '../persist.ts'
21
+ import type { PetInteractResult, PetStateView } from '../service.ts'
22
+ import type { PetInteraction } from '../affinity.ts'
23
+ import { createPetStore, type PetStoreInstance } from './pet-store.ts'
24
+ import { PetDockEntry, type PetInjected } from './PetDockEntry.tsx'
25
+ import { PetSettingsCard, PetSettingsCardController, type PetSettings } from './PetSettingsCard.tsx'
26
+ import { NS, en, zh } from './locales.ts'
27
+
28
+ /** The host pet API as the browser sees it (same-origin JSON endpoints). */
29
+ interface PetHttpApi {
30
+ state(): Promise<PetStateView>
31
+ interact(kind: PetInteraction): Promise<PetInteractResult>
32
+ setVisible(visible: boolean): Promise<{ ok: true; display: PetDisplayConfig }>
33
+ setConfig(patch: Partial<PetDisplayConfig>): Promise<{ ok: true; display: PetDisplayConfig }>
34
+ setName(name: string): Promise<{ ok: true; name: string } | { ok: false; error: string }>
35
+ }
36
+
37
+ /** Same-origin JSON fetch helper (GET without body, POST with JSON body). */
38
+ async function petFetch<T>(path: string, body?: unknown): Promise<T> {
39
+ const response = await fetch(path, body === undefined
40
+ ? {}
41
+ : {
42
+ method: 'POST',
43
+ headers: { 'content-type': 'application/json' },
44
+ body: JSON.stringify(body),
45
+ })
46
+ if (!response.ok) {
47
+ throw new Error(`pet ${path} failed: ${response.status}`)
48
+ }
49
+ return (await response.json()) as T
50
+ }
51
+
52
+ /** The live host API instance (always defined; failures surface per call). */
53
+ const petApi: PetHttpApi = {
54
+ state: () => petFetch('/api/pet/state'),
55
+ interact: (kind) => petFetch('/api/pet/interact', { kind }),
56
+ setVisible: (visible) => petFetch('/api/pet/set-visible', { visible }),
57
+ setConfig: (patch) => petFetch('/api/pet/set-config', patch),
58
+ setName: (name) => petFetch('/api/pet/set-name', { name }),
59
+ }
60
+
61
+ /** Poll interval for the host snapshot. */
62
+ const POLL_MS = 800
63
+
64
+ /** Settings namespace the pet settings card edits (the Host plugin registers it). */
65
+ const PET_SETTINGS_NS = 'pet'
66
+
67
+ /** Required services. */
68
+ export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote']
69
+
70
+ /** Re-exported for consumers that type against the injected face. */
71
+ export type { PetInjected, PetDockEntryProps } from './PetDockEntry.tsx'
72
+ export type { PetUiState, PetFeedback } from './pet-store.ts'
73
+ export type { PetSettingsCardFace, PetSettingsCardState } from './PetSettingsCard.tsx'
74
+
75
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
76
+ interface SlotMap {
77
+ /**
78
+ * The child slot the Web UI plugin group declares; this card registers
79
+ * into the group instead of the top-level `settings.plugin.item` list.
80
+ * Spelled here with the same shape so this package can register without
81
+ * depending on the sibling UI package.
82
+ */
83
+ 'web-ui.plugin.item': { kind: 'list'; scope: 'root'; owner: SettingsPluginItemOwnerProps }
84
+ }
85
+ }
86
+
87
+ /** Owner share of a plugin card (the group card supplies nothing). */
88
+ export interface SettingsPluginItemOwnerProps {
89
+ /** Marker field: card owner props are intentionally empty. */
90
+ children?: never
91
+ }
92
+
93
+ /**
94
+ * Client plugin body: register dictionaries, mount the dock entry and poll
95
+ * loop while the plugin is enabled, and seat the settings card in the Web UI
96
+ * plugin group.
97
+ * @param ctx - client root context.
98
+ */
99
+ export function apply(ctx: ClientContext): void {
100
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'pet: dictionaries')
101
+
102
+ const settingsScope = ctx.settingsScope.bind<PetSettings>({ namespace: PET_SETTINGS_NS })
103
+ const enabled = (): boolean => {
104
+ const snapshot = settingsScope.getSnapshot()
105
+ return snapshot.status === 'ready'
106
+ ? snapshot.value?.enabled ?? true
107
+ : snapshot.status === 'unavailable'
108
+ }
109
+
110
+ // Plugin configuration card: one staged form over the `pet` settings
111
+ // namespace, contributed to the Web UI plugin group.
112
+ const petSettings = new PetSettingsCardController(settingsScope)
113
+ ctx.slots.inject('web-ui.plugin.item', () => ctx.slots.register({
114
+ name: 'web-ui.plugin.item',
115
+ id: 'pet-settings',
116
+ order: 140,
117
+ locale: NS,
118
+ inject: () => petSettings.inject(),
119
+ }, PetSettingsCard))
120
+
121
+ // The dock entry, its store, and the poll loop live while the plugin is
122
+ // enabled; toggling the setting off hides the pet and stops polling.
123
+ let disposeUi: (() => void) | undefined
124
+ const syncUi = (): void => {
125
+ if (enabled() && disposeUi === undefined) {
126
+ // ONE store instance for the whole app, owned by this apply body. The
127
+ // pet is host-global (state/display/interactions are /api/pet/*
128
+ // endpoints with no session dimension), so the slot system's per-session
129
+ // store scoping would only reset the pet on session switches and leave
130
+ // it stateless on the new-conversation screen (no session to scope by).
131
+ const petStore: PetStoreInstance = createPetStore().create()
132
+ const setSnapshot = petStore.actions.setSnapshot
133
+ const setState = petStore.actions.setState
134
+ const setFeedback = petStore.actions.setFeedback
135
+
136
+ const pollNow = (): void => {
137
+ petApi.state().then((snapshot) => {
138
+ setSnapshot(snapshot)
139
+ }, () => {
140
+ setState('error', 'pet.state transport error')
141
+ })
142
+ }
143
+
144
+ const disposePoll = ctx.effect(() => {
145
+ // Poll only while the tab is visible: the host snapshot does not
146
+ // change while the page is hidden, so a background interval would
147
+ // only burn RPCs (browser throttling is an unreliable backstop).
148
+ // Coming back to the tab refreshes the pet immediately instead of
149
+ // waiting out the next 800 ms cycle.
150
+ let timer: number | undefined
151
+ const stop = (): void => {
152
+ if (timer !== undefined) {
153
+ window.clearInterval(timer)
154
+ timer = undefined
155
+ }
156
+ }
157
+ const start = (): void => {
158
+ if (timer === undefined && document.visibilityState === 'visible') {
159
+ timer = window.setInterval(pollNow, POLL_MS)
160
+ }
161
+ }
162
+ const onVisibility = (): void => {
163
+ if (document.visibilityState === 'visible') {
164
+ pollNow()
165
+ start()
166
+ } else {
167
+ stop()
168
+ }
169
+ }
170
+ start()
171
+ document.addEventListener('visibilitychange', onVisibility)
172
+ return () => {
173
+ stop()
174
+ document.removeEventListener('visibilitychange', onVisibility)
175
+ }
176
+ }, 'pet: poll')
177
+
178
+ const injected = (): PetInjected => ({
179
+ store: petStore,
180
+ ensure: pollNow,
181
+ pet: () => {
182
+ petApi.interact('pet').then((result) => {
183
+ setFeedback({
184
+ text: result.reaction,
185
+ kind: 'pet',
186
+ at: Date.now(),
187
+ })
188
+ }, () => {
189
+ // Ignore transport errors on interactions; the next poll resyncs.
190
+ })
191
+ },
192
+ feed: () => {
193
+ petApi.interact('feed').then((result) => {
194
+ setFeedback({
195
+ text: result.reaction,
196
+ kind: 'feed',
197
+ at: Date.now(),
198
+ })
199
+ }, () => {
200
+ // Ignore transport errors on interactions; the next poll resyncs.
201
+ })
202
+ },
203
+ hide: () => {
204
+ petApi.setVisible(false).then(() => {
205
+ pollNow()
206
+ }, () => {
207
+ // Ignore; next poll resyncs.
208
+ })
209
+ },
210
+ summon: () => {
211
+ petApi.setVisible(true).then(() => {
212
+ pollNow()
213
+ }, () => {
214
+ // Ignore; next poll resyncs.
215
+ })
216
+ },
217
+ dragEnd: (right, bottom) => {
218
+ petApi.setConfig({ right, bottom }).then(() => {
219
+ pollNow()
220
+ }, () => {
221
+ // Ignore; next poll resyncs.
222
+ })
223
+ },
224
+ rename: (name) => {
225
+ petApi.setName(name).then((result) => {
226
+ if (result.ok) pollNow()
227
+ }, () => {
228
+ // Ignore; next poll resyncs.
229
+ })
230
+ },
231
+ feedbackDone: () => {
232
+ setFeedback(null)
233
+ },
234
+ })
235
+
236
+ // The input selector row mounts in EVERY conversation phase (cold
237
+ // start, blank-session hero, active seat) — the composer dock band
238
+ // only renders for an active session, which is why the pet used to
239
+ // vanish on the new-conversation screen.
240
+ const disposeDock = ctx.slots.inject('conversation.input.selector.context', () =>
241
+ ctx.slots.register({
242
+ name: 'conversation.input.selector.context',
243
+ id: 'pet',
244
+ order: 110,
245
+ inject: injected,
246
+ locale: NS,
247
+ }, PetDockEntry))
248
+
249
+ disposeUi = () => {
250
+ disposeDock()
251
+ disposePoll()
252
+ disposeUi = undefined
253
+ }
254
+ } else if (!enabled() && disposeUi !== undefined) {
255
+ disposeUi()
256
+ disposeUi = undefined
257
+ }
258
+ }
259
+ settingsScope.subscribe(syncUi)
260
+ syncUi()
261
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * dsh-pet locale dictionaries (zh/en).
3
+ * @module @linxin666/dsh-pet/client/locales
4
+ */
5
+
6
+ /** Dictionary namespace this package registers. */
7
+ export const NS = 'pet'
8
+
9
+ /** Chinese copy. */
10
+ export const zh = {
11
+ 'pet.feed': '喂食',
12
+ 'pet.hide': '隐藏',
13
+ 'pet.rename': '改名',
14
+ 'pet.confirm': '确定',
15
+ 'pet.namePlaceholder': '输入新名字',
16
+ 'pet.summon': '召唤{name}',
17
+ 'pet.rank': '亲密度 {rank}',
18
+ 'pet.points': '{points} 点',
19
+ 'pet.treats': '小鱼干 ×{n}',
20
+ 'pet.state.loading': '鲸鱼娘正在赶来…',
21
+ 'pet.state.error': '鲸鱼娘迷路了(连接失败)',
22
+ // 插件设置卡片(settings.plugin.item 席位)。
23
+ 'settings.title': '宠物',
24
+ 'settings.description': '鲸鱼娘的显示布局与名字。',
25
+ 'settings.enabled': '启用宠物',
26
+ 'settings.enabledHint': '关闭后隐藏宠物并停止轮询,可在设置里重新启用。',
27
+ 'settings.visible': '显示宠物',
28
+ 'settings.visibleHint': '关闭后宠物隐藏,可从聊天输入区重新召唤。',
29
+ 'settings.size': '大小(px)',
30
+ 'settings.sizeHint': '精灵单元高度,范围 32–512。',
31
+ 'settings.right': '距右侧(px)',
32
+ 'settings.rightHint': '距视口右边缘的水平内缩距离。',
33
+ 'settings.bottom': '距底部(px)',
34
+ 'settings.bottomHint': '距视口底边的垂直内缩距离。',
35
+ 'settings.name': '名字',
36
+ 'settings.nameHint': '宠物显示名,1–20 个字符。',
37
+ 'settings.inherit': '继承',
38
+ 'settings.on': '开',
39
+ 'settings.off': '关',
40
+ 'settings.overridden': '已覆盖',
41
+ 'settings.reset': '恢复默认',
42
+ 'settings.readOnly': '当前部署的设置只读。',
43
+ 'settings.expand': '展开设置',
44
+ 'settings.collapse': '收起设置',
45
+ 'settings.save': '保存',
46
+ 'settings.saving': '保存中…',
47
+ 'settings.discard': '放弃',
48
+ 'settings.unsaved': '未保存',
49
+ 'settings.saveFailed': '部署未接受这些值,已保留供你修改。',
50
+ 'settings.invalidNumber': '请输入数字,留空则使用默认值。',
51
+ } as const
52
+
53
+ /** English copy. */
54
+ export const en = {
55
+ 'pet.feed': 'Feed',
56
+ 'pet.hide': 'Hide',
57
+ 'pet.rename': 'Rename',
58
+ 'pet.confirm': 'OK',
59
+ 'pet.namePlaceholder': 'Enter a new name',
60
+ 'pet.summon': 'Summon {name}',
61
+ 'pet.rank': 'Affinity {rank}',
62
+ 'pet.points': '{points} pts',
63
+ 'pet.treats': 'Treats ×{n}',
64
+ 'pet.state.loading': 'The whale girl is on her way…',
65
+ 'pet.state.error': 'The whale girl is lost (connection failed)',
66
+ // Plugin settings card (the `settings.plugin.item` seat).
67
+ 'settings.title': 'Pet',
68
+ 'settings.description': 'The whale girl\u2019s display layout and name.',
69
+ 'settings.enabled': 'Enable the pet',
70
+ 'settings.enabledHint': 'When off, the pet hides and polling stops; re-enable it here.',
71
+ 'settings.visible': 'Show the pet',
72
+ 'settings.visibleHint': 'When off, the pet hides; summon it again from the input row.',
73
+ 'settings.size': 'Size (px)',
74
+ 'settings.sizeHint': 'Sprite cell height, 32\u2013512.',
75
+ 'settings.right': 'Right inset (px)',
76
+ 'settings.rightHint': 'Horizontal inset from the viewport right edge.',
77
+ 'settings.bottom': 'Bottom inset (px)',
78
+ 'settings.bottomHint': 'Vertical inset from the viewport bottom edge.',
79
+ 'settings.name': 'Name',
80
+ 'settings.nameHint': 'The pet\u2019s display name, 1\u201320 characters.',
81
+ 'settings.inherit': 'Inherit',
82
+ 'settings.on': 'On',
83
+ 'settings.off': 'Off',
84
+ 'settings.overridden': 'Overridden',
85
+ 'settings.reset': 'Reset to default',
86
+ 'settings.readOnly': 'This deployment stores settings read-only.',
87
+ 'settings.expand': 'Show settings',
88
+ 'settings.collapse': 'Hide settings',
89
+ 'settings.save': 'Save',
90
+ 'settings.saving': 'Saving\u2026',
91
+ 'settings.discard': 'Discard',
92
+ 'settings.unsaved': 'Unsaved',
93
+ 'settings.saveFailed': 'The deployment did not accept these values; they were left for you to correct.',
94
+ 'settings.invalidNumber': 'Enter a number, or leave blank to use the default.',
95
+ } as const
96
+
97
+ /** Key union for this namespace. */
98
+ export type PetKey = keyof typeof zh
99
+
100
+ /** The settings-card slice of the pet dictionary. */
101
+ export type SettingsCardKey = PetKey
102
+
103
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
104
+ interface LocaleNamespaceMap {
105
+ /** dsh-pet UI copy. */
106
+ pet: PetKey
107
+ }
108
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Browser-side pet store: the pet state snapshot plus transient UI feedback
3
+ * (reaction bubbles), written only through the store's audit actions. The
4
+ * RPC polling and interactions live in the plugin apply body; components
5
+ * only ever read snapshots.
6
+ * @module @linxin666/dsh-pet/client/pet-store
7
+ */
8
+
9
+ import { defineStore } from '@deepseek-ai/dsh-client-runtime/client'
10
+ import type { EngineStoreHandle, EngineStoreInstance } from '@deepseek-ai/dsh-client-runtime/client'
11
+ import type { PetStateView } from '../service.ts'
12
+ import type { PetInteraction } from '../affinity.ts'
13
+
14
+ /** One transient reaction bubble on the pet. */
15
+ export interface PetFeedback {
16
+ /** Bubble copy. */
17
+ text: string
18
+ /** Interaction kind driving the reaction animation. */
19
+ kind: PetInteraction | 'none'
20
+ /** Epoch ms when the bubble appeared (for expiry). */
21
+ at: number
22
+ }
23
+
24
+ /** Pet UI state as consumers see it. */
25
+ export interface PetUiState {
26
+ /** Latest host snapshot; null before the first successful fetch. */
27
+ snapshot: PetStateView | null
28
+ /** Fetch lifecycle. */
29
+ state: 'loading' | 'ready' | 'error'
30
+ /** Transport error message (for the debug surface), when any. */
31
+ error: string | null
32
+ /** Active reaction bubble, if any. */
33
+ feedback: PetFeedback | null
34
+ }
35
+
36
+ /** Store write set. */
37
+ export type PetUiActions = {
38
+ /** Replace the host snapshot (poll result). */
39
+ setSnapshot: (draft: PetUiState, snapshot: PetStateView) => void
40
+ /** Mark the fetch lifecycle. */
41
+ setState: (draft: PetUiState, state: PetUiState['state'], error: string | null) => void
42
+ /** Show a reaction bubble. */
43
+ setFeedback: (draft: PetUiState, feedback: PetFeedback | null) => void
44
+ }
45
+
46
+ /** Create the pet store handle (apply world only; never module-level). */
47
+ export function createPetStore(): EngineStoreHandle<PetUiState, PetUiActions> {
48
+ return defineStore({
49
+ init: (): PetUiState => ({
50
+ snapshot: null,
51
+ state: 'loading',
52
+ error: null,
53
+ feedback: null,
54
+ }),
55
+ actions: {
56
+ setSnapshot: (draft, snapshot) => {
57
+ draft.snapshot = snapshot
58
+ draft.state = 'ready'
59
+ draft.error = null
60
+ },
61
+ setState: (draft, state, error) => {
62
+ draft.state = state
63
+ draft.error = error
64
+ },
65
+ setFeedback: (draft, feedback) => {
66
+ draft.feedback = feedback
67
+ },
68
+ },
69
+ })
70
+ }
71
+
72
+ export type { PetInteraction }
73
+
74
+ /**
75
+ * A live pet store instance (one per host, owned by the plugin apply body —
76
+ * the pet itself is host-global, so its UI state must not ride the slot
77
+ * system's per-session store scoping).
78
+ */
79
+ export type PetStoreInstance = EngineStoreInstance<PetUiState, PetUiActions>
80
+
@@ -0,0 +1,185 @@
1
+ .float {
2
+ position: fixed;
3
+ pointer-events: auto;
4
+ user-select: none;
5
+ -webkit-user-select: none;
6
+ display: flex;
7
+ flex-direction: column;
8
+ align-items: center;
9
+ }
10
+
11
+ .sprite {
12
+ position: relative;
13
+ image-rendering: auto;
14
+ touch-action: none;
15
+ }
16
+
17
+ .bubble {
18
+ position: absolute;
19
+ bottom: 100%;
20
+ margin-bottom: 6px;
21
+ white-space: nowrap;
22
+ padding: 4px 10px;
23
+ border-radius: 999px;
24
+ font-size: 12px;
25
+ line-height: 1.4;
26
+ color: #fff;
27
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
28
+ animation: pet-bubble-pop 2.6s ease-out forwards;
29
+ pointer-events: none;
30
+ }
31
+
32
+ .bubblePet {
33
+ background: rgba(244, 114, 182, 0.92);
34
+ }
35
+
36
+ .bubbleFeed {
37
+ background: rgba(56, 189, 248, 0.92);
38
+ }
39
+
40
+ @keyframes pet-bubble-pop {
41
+ 0% {
42
+ opacity: 0;
43
+ transform: translateY(6px) scale(0.85);
44
+ }
45
+ 15% {
46
+ opacity: 1;
47
+ transform: translateY(0) scale(1.05);
48
+ }
49
+ 25% {
50
+ transform: translateY(0) scale(1);
51
+ }
52
+ 75% {
53
+ opacity: 1;
54
+ }
55
+ 100% {
56
+ opacity: 0;
57
+ transform: translateY(-8px) scale(0.95);
58
+ }
59
+ }
60
+
61
+ .panel {
62
+ position: absolute;
63
+ bottom: calc(100% + 4px);
64
+ background: rgba(15, 23, 42, 0.92);
65
+ border: 1px solid rgba(148, 163, 184, 0.35);
66
+ border-radius: 10px;
67
+ padding: 8px 10px;
68
+ color: #e2e8f0;
69
+ font-size: 12px;
70
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
71
+ backdrop-filter: blur(6px);
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: 6px;
75
+ min-width: 132px;
76
+ }
77
+
78
+ .rankRow {
79
+ display: flex;
80
+ justify-content: space-between;
81
+ gap: 10px;
82
+ white-space: nowrap;
83
+ }
84
+
85
+ .nameCell {
86
+ font-weight: 600;
87
+ }
88
+
89
+ .renameRow {
90
+ display: flex;
91
+ gap: 6px;
92
+ align-items: center;
93
+ }
94
+
95
+ .nameInput {
96
+ flex: 1;
97
+ min-width: 0;
98
+ border: 1px solid rgba(125, 211, 252, 0.5);
99
+ border-radius: 6px;
100
+ background: rgba(30, 41, 59, 0.9);
101
+ color: #e2e8f0;
102
+ font-size: 12px;
103
+ padding: 3px 6px;
104
+ outline: none;
105
+ }
106
+
107
+ .nameInput:focus {
108
+ border-color: #38bdf8;
109
+ /* 2px accent ring (theme-agnostic) replacing the removed default outline. */
110
+ box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.45);
111
+ }
112
+
113
+ .actions {
114
+ display: flex;
115
+ gap: 6px;
116
+ }
117
+
118
+ .action {
119
+ flex: 1;
120
+ border: none;
121
+ border-radius: 6px;
122
+ padding: 4px 8px;
123
+ font-size: 12px;
124
+ cursor: pointer;
125
+ color: #0f172a;
126
+ background: linear-gradient(180deg, #7dd3fc, #38bdf8);
127
+ transition: filter 120ms ease, box-shadow 120ms ease;
128
+ }
129
+
130
+ .action:hover {
131
+ filter: brightness(1.08);
132
+ }
133
+
134
+ .action:active {
135
+ filter: brightness(0.94);
136
+ }
137
+
138
+ .action:focus-visible {
139
+ outline: none;
140
+ /* 2px accent ring on the pet's own palette; stays visible on the dark
141
+ glass panel regardless of the GUI theme (theme-agnostic). */
142
+ box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.85);
143
+ }
144
+
145
+ .summon {
146
+ border: 1px dashed rgba(125, 211, 252, 0.6);
147
+ background: rgba(15, 23, 42, 0.75);
148
+ color: #7dd3fc;
149
+ border-radius: 999px;
150
+ padding: 2px 10px;
151
+ font-size: 11px;
152
+ cursor: pointer;
153
+ transition: border-color 120ms ease, color 120ms ease,
154
+ background 120ms ease, box-shadow 120ms ease;
155
+ }
156
+
157
+ .summon:hover {
158
+ border-color: rgba(125, 211, 252, 0.95);
159
+ color: #bae6fd;
160
+ background: rgba(15, 23, 42, 0.9);
161
+ }
162
+
163
+ .summon:active {
164
+ color: #7dd3fc;
165
+ border-color: rgba(125, 211, 252, 0.8);
166
+ }
167
+
168
+ .summon:focus-visible {
169
+ outline: none;
170
+ box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.85);
171
+ }
172
+
173
+ /* Reduced motion: freeze the reaction bubble and kill the overlay transitions
174
+ so the pet's motion is static or minimal without hiding interaction. */
175
+ @media (prefers-reduced-motion: reduce) {
176
+ .bubble {
177
+ animation: none;
178
+ opacity: 1;
179
+ }
180
+
181
+ .action,
182
+ .summon {
183
+ transition: none;
184
+ }
185
+ }