neko-vue 0.1.6 → 0.1.7

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.
package/CHANGELOG.md CHANGED
@@ -2,19 +2,24 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4
4
 
5
- ## [0.1.6](https://github.com/AscaL/neko-vue/compare/v0.1.5...v0.1.6) (2026-04-05)
5
+ ## [0.1.7](https://github.com/AscaL/neko-vue/compare/v0.1.6...v0.1.7) (2026-04-06)
6
+
7
+ ### Features
6
8
 
9
+ - add options for behavior tracking and display in Neko components ([43a72b0](https://github.com/AscaL/neko-vue/commit/43a72b007b6f9da81441cfb1955b4ad9a92a08ef))
10
+ - implement NekoEngineMovement and viewport handling for enhanced pet behavior and movement logic ([2dd8484](https://github.com/AscaL/neko-vue/commit/2dd848413050e2e51bc7f6e215ed6c6f4bb05407))
11
+
12
+ ## [0.1.6](https://github.com/AscaL/neko-vue/compare/v0.1.5...v0.1.6) (2026-04-05)
7
13
 
8
14
  ### Features
9
15
 
10
- * add NekoEngineApi and NekoEngineState types for enhanced engine functionality ([138ca8d](https://github.com/AscaL/neko-vue/commit/138ca8d2486ec6e8ace114803db32bea0a14548b))
16
+ - add NekoEngineApi and NekoEngineState types for enhanced engine functionality ([138ca8d](https://github.com/AscaL/neko-vue/commit/138ca8d2486ec6e8ace114803db32bea0a14548b))
11
17
 
12
18
  ## [0.1.5](https://github.com/AscaL/neko-vue/compare/v0.1.4...v0.1.5) (2026-04-04)
13
19
 
14
-
15
20
  ### Features
16
21
 
17
- * implement viewport clamping for Neko sprite and enhance resize handling with tests ([086d2ca](https://github.com/AscaL/neko-vue/commit/086d2caddc44afd1043c18c633b9ab02cf505598))
22
+ - implement viewport clamping for Neko sprite and enhance resize handling with tests ([086d2ca](https://github.com/AscaL/neko-vue/commit/086d2caddc44afd1043c18c633b9ab02cf505598))
18
23
 
19
24
  ## [0.1.4](https://github.com/AscaL/neko-vue/compare/v0.1.3...v0.1.4) (2026-04-04)
20
25
 
@@ -22,11 +27,10 @@ All notable changes to this project will be documented in this file. See [commit
22
27
 
23
28
  ## [0.1.2](https://github.com/AscaL/neko-vue/compare/v0.1.1...v0.1.2) (2026-04-04)
24
29
 
25
-
26
30
  ### Features
27
31
 
28
- * add new behavior cycle types and utility functions to enhance behavior mode handling ([c5dd242](https://github.com/AscaL/neko-vue/commit/c5dd242d27164085865665521d79acd6b3fd8b89))
29
- * enhance NekoPet component with public props and improve behavior mode handling in runtime ([53ed755](https://github.com/AscaL/neko-vue/commit/53ed7553fea2078c5ff648c5753a87420eb76b32))
32
+ - add new behavior cycle types and utility functions to enhance behavior mode handling ([c5dd242](https://github.com/AscaL/neko-vue/commit/c5dd242d27164085865665521d79acd6b3fd8b89))
33
+ - enhance NekoPet component with public props and improve behavior mode handling in runtime ([53ed755](https://github.com/AscaL/neko-vue/commit/53ed7553fea2078c5ff648c5753a87420eb76b32))
30
34
 
31
35
  ## [0.1.1](https://github.com/AscaL/neko-vue/compare/v0.1.0...v0.1.1) (2026-04-04)
32
36
 
package/README.md CHANGED
@@ -18,24 +18,24 @@ Vue 3 integration for a **viewport-fixed** desktop pet: **`NekoPet`**, **`useNek
18
18
 
19
19
  The **playground** is the canonical integration reference:
20
20
 
21
- | Route | File | Purpose |
22
- | ----- | ---- | ------- |
23
- | `/` | [`playground/src/DemoNekoPet.vue`](./playground/src/DemoNekoPet.vue) | `<NekoPet />`, exposed `instance`, HUD via [`useNekoHud.ts`](./playground/src/useNekoHud.ts) |
24
- | `/composable` | [`playground/src/DemoUseNekoAnchor.vue`](./playground/src/DemoUseNekoAnchor.vue) | `useNeko` + `anchorRef` / `useTemplateRef`, flags panel |
25
- | `/customize` | [`playground/src/DemoCustomize.vue`](./playground/src/DemoCustomize.vue) | Sandbox: placement, `behaviorCycle`, `cursorStandoffPx`, **Apply** |
21
+ | Route | File | Purpose |
22
+ | ------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
23
+ | `/` | [`playground/src/DemoNekoPet.vue`](./playground/src/DemoNekoPet.vue) | `<NekoPet />`, `show-behavior-on-click`, `@behavior-mode-change`, HUD + last emit |
24
+ | `/composable` | [`playground/src/DemoUseNekoAnchor.vue`](./playground/src/DemoUseNekoAnchor.vue) | `useNeko` + anchor; toggles for `showBehaviorOnClick` / `onBehaviorModeChange` logger |
25
+ | `/customize` | [`playground/src/DemoCustomize.vue`](./playground/src/DemoCustomize.vue) | Sandbox: placement, `behaviorCycle`, `cursorStandoffPx`, **Apply** |
26
26
 
27
27
  Routing: [`playground/src/router.ts`](./playground/src/router.ts). Shell UI: [`playground/src/App.vue`](./playground/src/App.vue), [`playground/src/PlaygroundLiveStats.vue`](./playground/src/PlaygroundLiveStats.vue).
28
28
 
29
29
  ### `NekoPet` vs `useNeko`
30
30
 
31
- | | Use when |
32
- | --- | --- |
33
- | **`NekoPet`** | Props only; anchor via **`anchor-selector`** (CSS string). |
34
- | **`useNeko`** | **`anchorRef`**, **`instance` / `isReady` / `error`**, **`setMode`**, **`destroy`**, **`petInteractionAwake`**, or options from **`computed()`**. |
31
+ | | Use when |
32
+ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33
+ | **`NekoPet`** | Props + **`@behavior-mode-change`**; optional **`show-behavior-on-click`** built-in label. Anchor via **`anchor-selector`** only (no template ref). |
34
+ | **`useNeko`** | **`anchorRef`**, **`instance` / `isReady` / `error`**, **`setMode`**, **`destroy`**, **`petInteractionAwake`**, plus all **`NekoOptions`** (e.g. **`onBehaviorModeChange`**, **`showBehaviorOnClick`**) in reactive options. |
35
35
 
36
36
  `NekoPet` forwards props into `useNeko` ([`src/vue/NekoPet.ts`](./src/vue/NekoPet.ts)); core logic: [`src/vue/useNeko.ts`](./src/vue/useNeko.ts).
37
37
 
38
- **Imports:** `import { NekoPet, useNeko, … } from "neko-vue"`. In templates, multi-word props use **kebab-case** (`start-corner`, `behavior-mode`, `behavior-cycle`, `cursor-standoff-px`, `anchor-selector`, `respect-reduced-motion`, `rest-until-first-pet-interaction`, …).
38
+ **Imports:** `import { NekoPet, useNeko, … } from "neko-vue"`. In templates, multi-word props use **kebab-case** (`start-corner`, `behavior-mode`, `behavior-cycle`, `cursor-standoff-px`, `anchor-selector`, `respect-reduced-motion`, `rest-until-first-pet-interaction`, `show-behavior-on-click`, …). Listen for **`@behavior-mode-change`** on **`NekoPet`** for the new **`BehaviorMode`** after each click cycle.
39
39
 
40
40
  ---
41
41
 
@@ -43,10 +43,12 @@ Routing: [`playground/src/router.ts`](./playground/src/router.ts). Shell UI: [`p
43
43
 
44
44
  - **Pet is not in your DOM tree** — it is fixed to the viewport; “placement” is **`startX` / `startY`**, **`startCorner`**, **`anchorRef`** / **`anchorSelector`** (see [`src/placement/nekoPlacement.ts`](./src/placement/nekoPlacement.ts)).
45
45
  - **Per axis:** explicit coord → anchor top-left → corner → `0`. **`0`** is valid. Corner math uses **`NEKOJS_SPRITE_SIZE` (32)**.
46
- - **Anchor gate:** `createNeko` waits until the element exists and has **non-zero** size. **`ResizeObserver`** runs in **`useNeko`** only when **`anchorRef`** is set—not for **`anchorSelector`** alone.
46
+ - **Viewport bounds:** Runtime [`readViewportBounds`](./src/runtime/nekoViewport.ts) uses **`visualViewport`** when it reports **positive** width and height (typical mobile chrome), otherwise **`document.documentElement.clientWidth`** and **`window.innerHeight`**. The pet loop runs on **`requestAnimationFrame`**; **`window`** **resize** and **`visualViewport`** **`resize`/`scroll`** are coalesced with rAF. On those layout updates, **`home`** and the sprite snap to **initial placement** clamped into the new bounds (all **`behaviorMode`** values, **`follow`** or **`rest`**).
47
+ - **Anchor gate (`useNeko` / `NekoPet`):** If **`anchorRef`** or **`anchorSelector`** is set, creation is **deferred** until the resolved element exists and has **positive** layout size (`getBoundingClientRect` / `offsetWidth`×`offsetHeight`). Low-level **`createNeko()`** itself does not perform this wait—it appends the pet to **`document.body`** immediately. **`ResizeObserver`** (for recreate when the anchor box changes) runs in **`useNeko`** only when **`anchorRef`** is set, not for **`anchorSelector`** alone.
47
48
  - **`mode`:** **`follow`** (loop on) vs **`rest`** (stop at resolved home after create). Imperative: **`setMode`**, **`restAtOrigin`**, **`resumeFollow`**, or reactive **`mode`** in options. Changing placement-related options **recreates** the instance (no teleport API).
48
- - **`behaviorMode`:** **Initial** create (+ special cases leaving the first-click gate); **live** mode advances by **clicking the cat** (if **`allowBehaviorChange`**). On recreate, the **previous engine mode** is kept unless leaving that gate (`applyBehaviorModeForRecreate` in [`src/vue/useNeko.ts`](./src/vue/useNeko.ts)). Enum **`BehaviorMode`** (ids **0…6** in the engine), **`BEHAVIOR_MODES_IN_ORDER`**, **`DEFAULT_NEKO_BEHAVIOR_CYCLE`**, **`BehaviorCycle`**, **`isBehaviorMode`** — [`src/types/index.ts`](./src/types/index.ts). **Readable strings (HUD / logs / hovers):** **`formatBehaviorMode`**, **`behaviorModeEnumName`**, **`BEHAVIOR_MODE_LABELS`**. **Stable cycle typing** (avoid inferred `0 \| 2 \| 1` unions): **`behaviorCycleOf(BehaviorMode.ChaseMouse, …)`**. **`NekoPet`** is cast to **`DefineComponent<NekoPetPublicProps>`** so Volar shows **`BehaviorMode`** on props, not plain `number`. Custom click order: **`behaviorCycle`**; invalid ids stripped in [`normalizeBehaviorCycle`](./src/runtime/nekojsRuntime.ts).
49
+ - **`behaviorMode`:** **Initial** create (+ special cases leaving the first-click gate); **live** mode advances by **clicking the cat** (if **`allowBehaviorChange`**). On recreate, the **previous engine mode** is kept unless leaving that gate (`applyBehaviorModeForRecreate` in [`src/vue/useNeko.ts`](./src/vue/useNeko.ts)). Enum **`BehaviorMode`** (ids **0…6**), **`BEHAVIOR_MODES_IN_ORDER`**, **`DEFAULT_NEKO_BEHAVIOR_CYCLE`**, **`BehaviorCycle`**, **`isBehaviorMode`** — [`src/types/index.ts`](./src/types/index.ts); per-tick behaviors and movement state machine — [`src/runtime/nekoEngineMovement.ts`](./src/runtime/nekoEngineMovement.ts). **Readable strings (HUD / logs / hovers):** **`formatBehaviorMode`**, **`behaviorModeEnumName`**, **`BEHAVIOR_MODE_LABELS`**. **Stable cycle typing** (avoid inferred `0 \| 2 \| 1` unions): **`behaviorCycleOf(BehaviorMode.ChaseMouse, …)`**. **`NekoPet`** is cast to **`DefineComponent<NekoPetPublicProps>`** so Volar shows **`BehaviorMode`** on props, not plain `number`. Custom click order: **`behaviorCycle`**; invalid ids stripped in [`normalizeBehaviorCycle`](./src/runtime/nekojsRuntime.ts).
49
50
  - **`allowBehaviorChange`:** On `NekoPet`, default **`undefined`** so the field is omitted and the engine default **`true`** applies (see props table below).
51
+ - **Click → next behavior:** When **`allowBehaviorChange`** is true, each pet **`mousedown`** advances **`behaviorCycle`**. Listen with **`onBehaviorModeChange`** in **`createNeko`** / **`useNeko`** options, or **`@behavior-mode-change`** on **`NekoPet`**. Set **`showBehaviorOnClick`** (or **`NekoOptions.showBehaviorOnClick`**) to show a short built-in **`BEHAVIOR_MODE_LABELS`** hint above the sprite.
50
52
  - **`restUntilFirstPetInteraction`:** First pointer-down wakes **`follow`** without consuming the first cycle step; then normal cycle. **`petInteractionAwake`** tracks this.
51
53
  - **Reduced motion:** Default **skip** load + create; **`skippedForReducedMotion`**. Opt out: **`respectReducedMotion: false`** (use with care). Helper: **`prefersReducedMotion()`** ([`src/utils/prefersReducedMotion.ts`](./src/utils/prefersReducedMotion.ts)).
52
54
  - **SSR:** Never run **`loadNekoRuntime`**, **`useNeko`**, or mount **`NekoPet`** on the server. **Nuxt 4:** [`.client.vue` + `#components` / auto-import](https://nuxt.com/docs/4.x/guide/directory-structure/components#client-components) and/or [`<ClientOnly>`](https://nuxt.com/docs/4.x/api/components/client-only).
@@ -59,37 +61,46 @@ Routing: [`playground/src/router.ts`](./playground/src/router.ts). Shell UI: [`p
59
61
 
60
62
  `defineComponent` in [`src/vue/NekoPet.ts`](./src/vue/NekoPet.ts). Exposes **`instance`** (**`shallowRef`**, unwrapped on parent component ref like **`useNeko`’s `instance`**).
61
63
 
62
- | Script | Template | Default | Notes |
63
- | ------ | -------- | ------- | ----- |
64
- | `speed` | `speed` | omit → **24** | Logic tick step; ~5 ticks/s in engine. |
65
- | `fps` | `fps` | omit → **120** | Animation interval. |
66
- | `behaviorMode` | `behavior-mode` | omit | Initial only; clicks change live mode. |
67
- | `idleThreshold` | `idle-threshold` | omit → **6** | Idle distance (px). |
68
- | `cursorStandoffPx` | `cursor-standoff-px` | omit / 0 | Chase: min distance from pointer. |
69
- | `allowBehaviorChange` | `allow-behavior-change` | **`undefined`** | `undefined` → engine default **true**; `false` disables click cycle. |
70
- | `behaviorCycle` | `behavior-cycle` | omit | → **`DEFAULT_NEKO_BEHAVIOR_CYCLE`**. |
71
- | `startX` / `startY` | `start-x` / `start-y` | omit | Home coords; **0** valid. |
72
- | `autoStart` | `auto-start` | **true** | Loop after create unless **`rest`** or **false**. |
73
- | `respectReducedMotion` | `respect-reduced-motion` | **true** | Skip when user prefers reduced motion. |
74
- | `startCorner` | `start-corner` | omit | `top-left` … `bottom-right` for missing axes. |
75
- | `anchorSelector` | `anchor-selector` | omit | `querySelector`; prefer **`anchorRef`** in **`useNeko`**. |
76
- | `mode` | `mode` | **follow** | `follow` \| `rest`. |
77
- | `restUntilFirstPetInteraction` | `rest-until-first-pet-interaction` | **`undefined`** | First click wakes follow. |
78
- | `debug` | `debug` | **false** | **`[neko-vue]`** placement / recreate logs. |
79
-
80
- **Option groups:** Engine — `speed`, `fps`, `behaviorMode`, `idleThreshold`, `cursorStandoffPx`, `behaviorCycle`, `allowBehaviorChange`, `startX`/`startY`. Placement — `startCorner`, `anchorRef` (composable) / `anchorSelector` (component). Loop — `mode`, `autoStart`, `respectReducedMotion`, `restUntilFirstPetInteraction`. Most engine/placement changes **recreate**; **`behaviorMode`** in options does not drive recreate except first create / gate exit.
64
+ | Script | Template | Default | Notes |
65
+ | ------------------------------ | ---------------------------------- | --------------- | --------------------------------------------------------------------------------------------------------- |
66
+ | `speed` | `speed` | omit → **24** | Logic tick step; ~5 ticks/s in engine. |
67
+ | `fps` | `fps` | omit → **120** | Nominal display rate: the engine steps `update()` from **`requestAnimationFrame`** using `1000 / fps` ms. |
68
+ | `behaviorMode` | `behavior-mode` | omit | Initial only; clicks change live mode. |
69
+ | `idleThreshold` | `idle-threshold` | omit → **6** | Idle distance (px). |
70
+ | `cursorStandoffPx` | `cursor-standoff-px` | omit / 0 | Chase: min distance from pointer. |
71
+ | `allowBehaviorChange` | `allow-behavior-change` | **`undefined`** | `undefined` → engine default **true**; `false` disables click cycle. |
72
+ | `behaviorCycle` | `behavior-cycle` | omit | → **`DEFAULT_NEKO_BEHAVIOR_CYCLE`**. |
73
+ | `startX` / `startY` | `start-x` / `start-y` | omit | Home coords; **0** valid. |
74
+ | `autoStart` | `auto-start` | **true** | Loop after create unless **`rest`** or **false**. |
75
+ | `respectReducedMotion` | `respect-reduced-motion` | **true** | Skip when user prefers reduced motion. |
76
+ | `startCorner` | `start-corner` | omit | `top-left` … `bottom-right` for missing axes. |
77
+ | `anchorSelector` | `anchor-selector` | omit | `querySelector`; prefer **`anchorRef`** in **`useNeko`**. |
78
+ | `mode` | `mode` | **follow** | `follow` \| `rest`. |
79
+ | `restUntilFirstPetInteraction` | `rest-until-first-pet-interaction` | **`undefined`** | First click wakes follow. |
80
+ | `debug` | `debug` | **false** | **`[neko-vue]`** placement / recreate logs. |
81
+ | `showBehaviorOnClick` | `show-behavior-on-click` | **false** | Built-in label above the sprite after each click cycle (needs **`allowBehaviorChange`**). |
82
+
83
+ **Emits (`NekoPet`):** **`behaviorModeChange`** — payload is the new **`BehaviorMode`** after each pet click advances the cycle (same timing as **`onBehaviorModeChange`** on **`createNeko`** / **`useNeko`**).
84
+
85
+ **Option groups:** Engine — `speed`, `fps`, `behaviorMode`, `idleThreshold`, `cursorStandoffPx`, `behaviorCycle`, `allowBehaviorChange`, `startX`/`startY`, **`onBehaviorModeChange`**, **`showBehaviorOnClick`**. Placement — `startCorner`, `anchorRef` (composable) / `anchorSelector` (component). Loop — `mode`, `autoStart`, `respectReducedMotion`, `restUntilFirstPetInteraction`. Most engine/placement changes **recreate**; **`behaviorMode`** in options does not drive recreate except first create / gate exit. **`onBehaviorModeChange`** identity changes also **recreate** (use a stable handler if undesired).
81
86
 
82
87
  ### `useNeko()` return value
83
88
 
84
- | Name | Type / role |
85
- | ---- | ----------- |
86
- | `instance` | `ShallowRef<NekoInstance \| null>` |
87
- | `error` | `Ref<Error \| null>` |
88
- | `isReady` | `Ref<boolean>` |
89
- | `skippedForReducedMotion` | `Ref<boolean>` |
90
- | `mode` | Readonly ref `follow` \| `rest` |
91
- | `petInteractionAwake` | Readonly ref (with **`restUntilFirstPetInteraction`**) |
92
- | `setMode`, `restAtOrigin`, `resumeFollow`, `destroy` | Imperative |
89
+ | Name | Type / role |
90
+ | ---------------------------------------------------- | ------------------------------------------------------ |
91
+ | `instance` | `ShallowRef<NekoInstance \| null>` |
92
+ | `error` | `Ref<Error \| null>` |
93
+ | `isReady` | `Ref<boolean>` |
94
+ | `skippedForReducedMotion` | `Ref<boolean>` |
95
+ | `mode` | Readonly ref `follow` \| `rest` |
96
+ | `petInteractionAwake` | Readonly ref (with **`restUntilFirstPetInteraction`**) |
97
+ | `setMode`, `restAtOrigin`, `resumeFollow`, `destroy` | Imperative |
98
+
99
+ Options are **`UseNekoOptions`** = **`NekoOptions`** + placement / loop flags (`startCorner`, `anchorRef`, `mode`, …). Programmatic **`createNeko(opts)`** uses the same engine fields: **`onBehaviorModeChange`** (new mode after each click cycle), **`showBehaviorOnClick`** (built-in hint). Changing the **function identity** of **`onBehaviorModeChange`** or toggling **`showBehaviorOnClick`** **recreates** the pet; use a **stable** handler (e.g. a `function` declared once in `setup`) if you need to avoid that.
100
+
101
+ ### `NekoOptions` (engine / `createNeko`)
102
+
103
+ Full interface: [`src/types/index.ts`](./src/types/index.ts). Besides motion and placement, notable fields include **`allowBehaviorChange`**, **`behaviorCycle`**, **`onBehaviorModeChange`**, and **`showBehaviorOnClick`**. The bundled runtime implements these in [`nekojsRuntime.ts`](./src/runtime/nekojsRuntime.ts) (`cycleBehavior`, hint element class **`neko-behavior-hint`**).
93
104
 
94
105
  ### `loadNekoRuntime()`
95
106
 
@@ -99,13 +110,13 @@ Routing: [`playground/src/router.ts`](./playground/src/router.ts). Shell UI: [`p
99
110
 
100
111
  [`package.json` `exports`](./package.json) + pack entries [`src/entries/*.ts`](./src/entries/) ([`vite.config.ts`](./vite.config.ts) **`pack.entry`**, **`pack.exports: false`**).
101
112
 
102
- | Import | Exposes |
103
- | ------ | ------- |
104
- | `neko-vue` | Full API ([`src/index.ts`](./src/index.ts)) |
105
- | `neko-vue/types` | Same as root type exports: behavior helpers **`formatBehaviorMode`**, **`behaviorModeEnumName`**, **`BEHAVIOR_MODE_LABELS`**, **`behaviorCycleOf`**, plus `BehaviorMode`, `BehaviorCycle`, etc. — no Vue |
106
- | `neko-vue/placement` | `cornerToStartXY`, `resolveStartPosition`, corners / input types |
107
- | `neko-vue/runtime` | `loadNekoRuntime` only |
108
- | `neko-vue/vue` | `useNeko`, `NekoPet`, **`NekoPetPublicProps`** (typed props for Volar), option types |
113
+ | Import | Exposes |
114
+ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
115
+ | `neko-vue` | Full API ([`src/index.ts`](./src/index.ts)) |
116
+ | `neko-vue/types` | Same as root type exports: behavior helpers **`formatBehaviorMode`**, **`behaviorModeEnumName`**, **`BEHAVIOR_MODE_LABELS`**, **`behaviorCycleOf`**, plus `BehaviorMode`, `BehaviorCycle`, etc. — no Vue |
117
+ | `neko-vue/placement` | `cornerToStartXY`, `resolveStartPosition`, corners / input types |
118
+ | `neko-vue/runtime` | `loadNekoRuntime` only |
119
+ | `neko-vue/vue` | `useNeko`, `NekoPet`, **`NekoPetPublicProps`** (typed props for Volar), option types |
109
120
 
110
121
  ---
111
122
 
@@ -113,8 +124,10 @@ Routing: [`playground/src/router.ts`](./playground/src/router.ts). Shell UI: [`p
113
124
 
114
125
  - **Nothing draws:** Confirm the runtime **chunk** loads (Network). Check console.
115
126
  - **Skipped pet:** **`prefers-reduced-motion: reduce`** — intentional unless you set **`respectReducedMotion: false`** (with consent).
116
- - **Wrong corner on first paint:** Viewport **`clientWidth` / `innerHeight`** can be **0** briefly; **`debug: true`** logs resolved coords.
117
- - **Listeners / timers:** Unmount should call **`stop`** + **`destroy`**; engine uses **`AbortController`** ([`src/runtime/nekojsRuntime.ts`](./src/runtime/nekojsRuntime.ts)).
127
+ - **Wrong corner on first paint:** Viewport **`clientWidth` / `innerHeight`** (or **`visualViewport`**) can be **0** briefly; **`debug: true`** logs resolved coords.
128
+ - **Pet jumps when the window resizes:** Expected. Any layout change handled by the engine moves **`home`** and the sprite to **initial placement** clamped inside the new bounds (every **`behaviorMode`**, **`follow`** or **`rest`**). See [`nekoViewport.ts`](./src/runtime/nekoViewport.ts) + [`nekojsRuntime.ts`](./src/runtime/nekojsRuntime.ts); coverage in [`tests/neko.test.ts`](./tests/neko.test.ts) (`Neko resize`).
129
+ - **Pet recreates on every parent render:** Often an **inline** **`onBehaviorModeChange`** (or other options) getting a **new function identity** each render — use a **stable** reference (see **`useNeko`** note above).
130
+ - **Listeners / timers:** Unmount should call **`stop`** + **`destroy`**; engine uses **`AbortController`**, **`requestAnimationFrame`** for the draw loop, coalesced rAF for layout clamping, and clears behavior-hint timers ([`src/runtime/nekojsRuntime.ts`](./src/runtime/nekojsRuntime.ts)).
118
131
 
119
132
  ---
120
133
 
@@ -122,11 +135,13 @@ Routing: [`playground/src/router.ts`](./playground/src/router.ts). Shell UI: [`p
122
135
 
123
136
  Classic “one-file” neko demos often use a **GIF** or scalable strip. This engine uses **fixed 32×32** cells—use **`NEKOJS_SPRITE_SIZE`** for layout math. **HMR** in dev can briefly duplicate pets; **full refresh** fixes it. The runtime loads via a **single shared** dynamic import (separate bundler chunk); no alternate script URL at runtime.
124
137
 
138
+ Logic ticks and movement live in [`nekoEngineMovement.ts`](./src/runtime/nekoEngineMovement.ts); viewport sizing in [`nekoViewport.ts`](./src/runtime/nekoViewport.ts); **`createNeko`** / DOM / lifecycle / click-cycle UI in [`nekojsRuntime.ts`](./src/runtime/nekojsRuntime.ts). The display loop is **rAF**-driven at the configured **`fps`**; in-engine logic still advances at the original ~**5** “original ticks” per second inside `update()`. Optional **`showBehaviorOnClick`** injects a **`.neko-behavior-hint`** child on the sprite root (overridable with your own CSS if needed).
139
+
125
140
  ---
126
141
 
127
142
  ## Development
128
143
 
129
- - **Layout:** `src/types`, `src/runtime`, `src/vue`, `src/placement`, `src/utils`, `src/entries` (subpath barrels), root [`src/index.ts`](./src/index.ts).
144
+ - **Layout:** `src/types`, `src/runtime`, `src/vue`, `src/placement`, `src/utils`, `src/entries` (subpath barrels), root [`src/index.ts`](./src/index.ts). **Runtime:** [`nekojsRuntime.ts`](./src/runtime/nekojsRuntime.ts), [`nekoEngineMovement.ts`](./src/runtime/nekoEngineMovement.ts), [`nekoViewport.ts`](./src/runtime/nekoViewport.ts), [`loadNekoRuntime.ts`](./src/runtime/loadNekoRuntime.ts), [`nekoSpritesData.ts`](./src/runtime/nekoSpritesData.ts). Maintainer notes: [`plan.md`](./plan.md).
130
145
  - **Playground:** run / HMR / port **5175** — [`playground/README.md`](./playground/README.md).
131
146
  - **Scripts:** [`package.json`](./package.json) — `vp test`; `vp run check` & `vp run check:playground` (or `bun run check:all`); `vp pack`; `bun run playground:dev`. Do **not** run bare **`vp check`** on the whole tree (playground would resolve wrong `node_modules`).
132
147
  - **CI:** [`.github/workflows/ci.yml`](./.github/workflows/ci.yml).
@@ -1,5 +1,5 @@
1
1
  import { n as NekoStartCorner } from "./nekoPlacement-fXmlU6ys.mjs";
2
- import { f as NekoInstance, i as BehaviorMode, p as NekoOptions, r as BehaviorCycle } from "./index-BGxU7veJ.mjs";
2
+ import { f as NekoInstance, i as BehaviorMode, p as NekoOptions, r as BehaviorCycle } from "./index-CJBe9Tds.mjs";
3
3
  import * as _$vue from "vue";
4
4
  import { DefineComponent, MaybeRefOrGetter } from "vue";
5
5
 
@@ -15,6 +15,10 @@ type NekoFollowMode = "follow" | "rest";
15
15
  * If you wrap options in **`computed(() => ({ … }))`** and include `behaviorMode`, changing it still
16
16
  * invalidates that computed and may re-run internal watchers — use **`reactive`** for the options
17
17
  * object if you need to mutate `behaviorMode` without that effect.
18
+ *
19
+ * **`onBehaviorModeChange`** and **`showBehaviorOnClick`** are part of the recreate fingerprint; use a
20
+ * **stable** `onBehaviorModeChange` reference (e.g. a top-level function) if you do not want the pet
21
+ * recreated on every parent render.
18
22
  */
19
23
  type UseNekoOptions = NekoOptions & {
20
24
  /**
@@ -86,7 +90,7 @@ declare function useNeko(options?: MaybeRefOrGetter<UseNekoOptions | undefined>)
86
90
  interface NekoPetPublicProps {
87
91
  /** Pixels per engine logic tick (omit → default **24**). */
88
92
  speed?: number;
89
- /** Sprite animation frame rate (omit → default **120**). */
93
+ /** Nominal display rate — engine steps from `requestAnimationFrame` at `1000 / fps` ms (omit → **120**). */
90
94
  fps?: number;
91
95
  /**
92
96
  * Initial {@link BehaviorMode} at create time. Clicks on the pet change the live mode when
@@ -146,6 +150,11 @@ interface NekoPetPublicProps {
146
150
  restUntilFirstPetInteraction?: boolean;
147
151
  /** Log placement / recreate steps with prefix `[neko-vue]`. */
148
152
  debug?: boolean;
153
+ /**
154
+ * When true, a short built-in label appears above the sprite after each click-to-cycle step
155
+ * (requires {@link allowBehaviorChange}).
156
+ */
157
+ showBehaviorOnClick?: boolean;
149
158
  }
150
159
  /**
151
160
  * Mounts the desktop pet on the client. Renders a minimal hidden root node for Vue; the engine draws
@@ -1,6 +1,6 @@
1
- import { r as BehaviorMode, u as isBehaviorMode } from "./types-De8imAgT.mjs";
2
- import { n as resolveStartPosition, r as nekoVueDebug } from "./nekoPlacement-CW-BnKgW.mjs";
3
- import { t as loadNekoRuntime } from "./loadNekoRuntime-BduhAtZ8.mjs";
1
+ import { r as BehaviorMode, u as isBehaviorMode } from "./types-DGv86pPP.mjs";
2
+ import { n as resolveStartPosition, r as nekoVueDebug } from "./nekoPlacement-DVX15n-h.mjs";
3
+ import { t as loadNekoRuntime } from "./loadNekoRuntime-CarfwqJ5.mjs";
4
4
  import { computed, defineComponent, h, onBeforeUnmount, onMounted, readonly, ref, shallowRef, toValue, watch, watchEffect } from "vue";
5
5
  //#region src/utils/prefersReducedMotion.ts
6
6
  /**
@@ -134,6 +134,7 @@ function useNeko(options = {}) {
134
134
  if ((toValue(options) ?? {}).restUntilFirstPetInteraction !== true || petInteractionAwake.value) return;
135
135
  if (typeof document === "undefined") return;
136
136
  const onPointerDown = (e) => {
137
+ if (petInteractionAwake.value) return;
137
138
  const pet = document.querySelector(".neko");
138
139
  const t = e.target;
139
140
  if (!pet || !(t instanceof Node) || !(pet === t || pet.contains(t))) return;
@@ -188,7 +189,9 @@ function useNeko(options = {}) {
188
189
  autoStart: raw.autoStart,
189
190
  respectReducedMotion: raw.respectReducedMotion,
190
191
  anchorSelector: raw.anchorSelector,
191
- debug: raw.debug
192
+ debug: raw.debug,
193
+ showBehaviorOnClick: raw.showBehaviorOnClick === true,
194
+ onBehaviorModeChange: raw.onBehaviorModeChange
192
195
  };
193
196
  }, async () => {
194
197
  const gen = ++mountGen;
@@ -337,6 +340,7 @@ function useNeko(options = {}) {
337
340
  */
338
341
  var NekoPet_default = defineComponent({
339
342
  name: "NekoPet",
343
+ emits: { behaviorModeChange: (mode) => isBehaviorMode(mode) },
340
344
  props: {
341
345
  speed: Number,
342
346
  fps: Number,
@@ -371,9 +375,16 @@ var NekoPet_default = defineComponent({
371
375
  debug: {
372
376
  type: Boolean,
373
377
  default: false
378
+ },
379
+ showBehaviorOnClick: {
380
+ type: Boolean,
381
+ default: false
374
382
  }
375
383
  },
376
- setup(props, { expose }) {
384
+ setup(props, { expose, emit }) {
385
+ const notifyBehaviorModeChange = (mode) => {
386
+ emit("behaviorModeChange", mode);
387
+ };
377
388
  const { instance } = useNeko(computed(() => ({
378
389
  speed: props.speed,
379
390
  fps: props.fps,
@@ -390,7 +401,9 @@ var NekoPet_default = defineComponent({
390
401
  anchorSelector: props.anchorSelector || void 0,
391
402
  mode: props.mode,
392
403
  restUntilFirstPetInteraction: props.restUntilFirstPetInteraction,
393
- debug: props.debug
404
+ debug: props.debug,
405
+ showBehaviorOnClick: props.showBehaviorOnClick,
406
+ onBehaviorModeChange: notifyBehaviorModeChange
394
407
  })));
395
408
  expose({ instance });
396
409
  return () => h("span", {
@@ -1,8 +1,8 @@
1
1
  //#region src/types/index.d.ts
2
- /** Sprite edge length in CSS pixels for viewport bounds (`clientWidth/innerHeight - this`). */
2
+ /** Sprite edge length in CSS pixels for viewport bounds (see runtime `readViewportBounds`). */
3
3
  declare const NEKOJS_SPRITE_SIZE: 32;
4
4
  /**
5
- * Pet behavior / AI mode. Numeric values **must** match the bundled engine (`src/runtime/nekojsRuntime.ts`).
5
+ * Pet behavior / AI mode. Numeric values **must** match the bundled engine (`src/runtime/nekoEngineMovement.ts`).
6
6
  * Click cycles through {@link NekoOptions.behaviorCycle} when `allowBehaviorChange` is true.
7
7
  */
8
8
  declare enum BehaviorMode {
@@ -70,8 +70,9 @@ declare function behaviorCycleOf(...modes: BehaviorMode[]): BehaviorCycle;
70
70
  *
71
71
  * Numeric defaults use **nullish coalescing** (`??`): omit or pass `undefined` for engine defaults;
72
72
  * **`0` is a real value** for `speed`, `fps`, `idleThreshold`, `startX`, and `startY` on this interface.
73
- * Horizontal bounds use `document.documentElement.clientWidth - {@link NEKOJS_SPRITE_SIZE}`; vertical uses
74
- * `window.innerHeight - {@link NEKOJS_SPRITE_SIZE}`.
73
+ * Bounds: `visualViewport` width/height (when available and positive) or else
74
+ * `document.documentElement.clientWidth` / `window.innerHeight`, each minus {@link NEKOJS_SPRITE_SIZE}
75
+ * (see runtime `readViewportBounds`).
75
76
  */
76
77
  interface NekoOptions {
77
78
  /**
@@ -79,7 +80,8 @@ interface NekoOptions {
79
80
  */
80
81
  speed?: number;
81
82
  /**
82
- * Render frame rate (default **120** when omitted).
83
+ * Nominal display rate (default **120** when omitted). The bundled engine steps `update()` from
84
+ * `requestAnimationFrame` using `1000 / fps` ms between steps.
83
85
  */
84
86
  fps?: number;
85
87
  /**
@@ -115,15 +117,27 @@ interface NekoOptions {
115
117
  * Initial Y position. `0` is valid; omit for default `0`.
116
118
  */
117
119
  startY?: number;
120
+ /**
121
+ * Called after each pet click advances the live {@link behaviorMode} (when
122
+ * {@link allowBehaviorChange} is true). Receives the **new** mode. Captured when the instance is
123
+ * created; changing this reference in `useNeko` options triggers recreate (use a stable function
124
+ * if you want to avoid that).
125
+ */
126
+ onBehaviorModeChange?: (mode: BehaviorMode) => void;
127
+ /**
128
+ * When true, a short built-in label appears above the sprite with the new behavior’s
129
+ * {@link BEHAVIOR_MODE_LABELS} text after each successful click cycle step.
130
+ */
131
+ showBehaviorOnClick?: boolean;
118
132
  }
119
133
  /**
120
134
  * Minimal handle returned by {@link createNeko} / {@link loadNekoRuntime}. The bundled engine may
121
135
  * expose additional helpers (e.g. `isIdle`).
122
136
  */
123
137
  interface NekoInstance {
124
- /** Starts or resumes the animation interval. */
138
+ /** Starts or resumes the `requestAnimationFrame` animation loop. */
125
139
  start(): void;
126
- /** Stops the interval without removing the pet from the DOM. */
140
+ /** Stops the animation loop without removing the pet from the DOM. */
127
141
  stop(): void;
128
142
  /** Stops the loop, removes listeners, and deletes the pet element. */
129
143
  destroy(): void;
@@ -142,6 +156,16 @@ interface NekoInstance {
142
156
  */
143
157
  homeX?: number;
144
158
  homeY?: number;
159
+ /**
160
+ * Bundled engine only — whether the sprite is in a stationary / idle animation state.
161
+ * Test mocks may omit.
162
+ */
163
+ isIdle?(): boolean;
164
+ /**
165
+ * Bundled engine only — assign PNG/Data URL frames for the sprite. Used internally by `createNeko`.
166
+ * Test mocks may omit.
167
+ */
168
+ setSprites?(sprites: readonly string[]): void;
145
169
  }
146
170
  /** All mutable fields for the viewport-fixed pet engine (closure-scoped; used by `nekojsRuntime`). */
147
171
  type NekoEngineState = {
@@ -170,6 +194,7 @@ type NekoEngineState = {
170
194
  mouseY: number | null;
171
195
  hasMouseMoved: boolean;
172
196
  element: HTMLDivElement;
197
+ spriteImg: HTMLImageElement;
173
198
  spriteImages: string[];
174
199
  allowBehaviorChange: boolean;
175
200
  animationTable: [number, number][];
@@ -177,17 +202,24 @@ type NekoEngineState = {
177
202
  ballX: number;
178
203
  ballY: number;
179
204
  ballVX: number;
180
- ballVY: number;
205
+ ballVY: number; /** Set when ball-chase mode has spawned the invisible ball; cleared when leaving that mode. */
206
+ ballActive: boolean;
181
207
  running: boolean;
182
- intervalId: ReturnType<typeof setInterval> | null;
208
+ animationFrameId: number | null;
183
209
  tickAccumulator: number;
184
210
  actionCount: number;
185
211
  lastMoveDX: number;
186
212
  lastMoveDY: number;
187
213
  cursorStandoffPx: number;
188
- behaviorCycle: readonly BehaviorMode[];
214
+ behaviorCycle: readonly BehaviorMode[]; /** Initial spawn/home from options; re-clamped on resize so the viewport can grow again. */
215
+ placementHomeX: number;
216
+ placementHomeY: number;
189
217
  homeX: number;
190
- homeY: number;
218
+ homeY: number; /** From {@link NekoOptions.onBehaviorModeChange}; invoked after each click cycle step. */
219
+ onBehaviorModeChange?: (mode: BehaviorMode) => void; /** From {@link NekoOptions.showBehaviorOnClick}. */
220
+ showBehaviorOnClick: boolean; /** Built-in behavior label element; cleaned up in `destroy`. */
221
+ behaviorHintEl: HTMLDivElement | null; /** Browser timer id from `window.setTimeout` (numeric in DOM typings). */
222
+ behaviorHintTimeoutId: number | null;
191
223
  };
192
224
  /** Full engine handle: {@link NekoInstance} plus internal helpers (`setSprites`, `isIdle`). */
193
225
  type NekoEngineApi = NekoInstance & {
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { i as resolveStartPosition, n as NekoStartCorner, r as cornerToStartXY } from "./nekoPlacement-fXmlU6ys.mjs";
2
- import { _ as isBehaviorMode, a as BehaviorModes, c as LoadNekoRuntimeOptions, d as NekoEngineState, f as NekoInstance, g as formatBehaviorMode, h as behaviorModeEnumName, i as BehaviorMode, l as NEKOJS_SPRITE_SIZE, m as behaviorCycleOf, n as BEHAVIOR_MODE_LABELS, o as CreateNekoFn, p as NekoOptions, r as BehaviorCycle, s as DEFAULT_NEKO_BEHAVIOR_CYCLE, t as BEHAVIOR_MODES_IN_ORDER, u as NekoEngineApi } from "./index-BGxU7veJ.mjs";
3
- import { t as loadNekoRuntime } from "./loadNekoRuntime-BqOla6to.mjs";
4
- import { a as useNeko, i as UseNekoOptions, n as _default, r as NekoFollowMode, t as NekoPetPublicProps } from "./NekoPet-BWrleApv.mjs";
2
+ import { _ as isBehaviorMode, a as BehaviorModes, c as LoadNekoRuntimeOptions, d as NekoEngineState, f as NekoInstance, g as formatBehaviorMode, h as behaviorModeEnumName, i as BehaviorMode, l as NEKOJS_SPRITE_SIZE, m as behaviorCycleOf, n as BEHAVIOR_MODE_LABELS, o as CreateNekoFn, p as NekoOptions, r as BehaviorCycle, s as DEFAULT_NEKO_BEHAVIOR_CYCLE, t as BEHAVIOR_MODES_IN_ORDER, u as NekoEngineApi } from "./index-CJBe9Tds.mjs";
3
+ import { t as loadNekoRuntime } from "./loadNekoRuntime-DdKHhZUx.mjs";
4
+ import { a as useNeko, i as UseNekoOptions, n as _default, r as NekoFollowMode, t as NekoPetPublicProps } from "./NekoPet-C1UcoSGo.mjs";
5
5
 
6
6
  //#region src/utils/debugLog.d.ts
7
7
  /** Opt-in `console` helper; prefix keeps DevTools filter easy. */
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { a as DEFAULT_NEKO_BEHAVIOR_CYCLE, c as behaviorModeEnumName, i as BehaviorModes, l as formatBehaviorMode, n as BEHAVIOR_MODE_LABELS, o as NEKOJS_SPRITE_SIZE, r as BehaviorMode, s as behaviorCycleOf, t as BEHAVIOR_MODES_IN_ORDER, u as isBehaviorMode } from "./types-De8imAgT.mjs";
2
- import { n as resolveStartPosition, r as nekoVueDebug, t as cornerToStartXY } from "./nekoPlacement-CW-BnKgW.mjs";
3
- import { t as loadNekoRuntime } from "./loadNekoRuntime-BduhAtZ8.mjs";
4
- import { n as useNeko, r as prefersReducedMotion, t as NekoPet_default } from "./NekoPet-CsvPuZow.mjs";
1
+ import { a as DEFAULT_NEKO_BEHAVIOR_CYCLE, c as behaviorModeEnumName, i as BehaviorModes, l as formatBehaviorMode, n as BEHAVIOR_MODE_LABELS, o as NEKOJS_SPRITE_SIZE, r as BehaviorMode, s as behaviorCycleOf, t as BEHAVIOR_MODES_IN_ORDER, u as isBehaviorMode } from "./types-DGv86pPP.mjs";
2
+ import { n as resolveStartPosition, r as nekoVueDebug, t as cornerToStartXY } from "./nekoPlacement-DVX15n-h.mjs";
3
+ import { t as loadNekoRuntime } from "./loadNekoRuntime-CarfwqJ5.mjs";
4
+ import { n as useNeko, r as prefersReducedMotion, t as NekoPet_default } from "./NekoPet-CLH3uMg0.mjs";
5
5
  export { BEHAVIOR_MODES_IN_ORDER, BEHAVIOR_MODE_LABELS, BehaviorMode, BehaviorModes, DEFAULT_NEKO_BEHAVIOR_CYCLE, NEKOJS_SPRITE_SIZE, NekoPet_default as NekoPet, behaviorCycleOf, behaviorModeEnumName, cornerToStartXY, formatBehaviorMode, isBehaviorMode, loadNekoRuntime, nekoVueDebug, prefersReducedMotion, resolveStartPosition, useNeko };
@@ -12,7 +12,7 @@ let bundledLoadPromise = null;
12
12
  * Dynamically imports the bundled typed runtime (`./nekojsRuntime.ts`). Defines `window.createNeko`.
13
13
  */
14
14
  async function importBundledNeko() {
15
- await import("./nekojsRuntime-CiPaD_IV.mjs");
15
+ await import("./nekojsRuntime-2Ax6jUB6.mjs");
16
16
  const fn = getCreateNekoFromGlobal();
17
17
  if (!fn) throw new Error("neko-vue: bundled neko.js did not define `createNeko`.");
18
18
  return fn;
@@ -1,4 +1,4 @@
1
- import { c as LoadNekoRuntimeOptions, o as CreateNekoFn } from "./index-BGxU7veJ.mjs";
1
+ import { c as LoadNekoRuntimeOptions, o as CreateNekoFn } from "./index-CJBe9Tds.mjs";
2
2
 
3
3
  //#region src/runtime/loadNekoRuntime.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import "./types-De8imAgT.mjs";
1
+ import "./types-DGv86pPP.mjs";
2
2
  //#region src/utils/debugLog.ts
3
3
  /** Opt-in `console` helper; prefix keeps DevTools filter easy. */
4
4
  function nekoVueDebug(enabled, label, ...args) {