@vrixton/pixel-pet 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Victor Villavicencio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,166 @@
1
- # Temporary Holding Version
1
+ # PixelPet
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A pixel art pet for React: a cat, a dog, an axolotl, a parakeet, or a hamster. It walks along the bottom of its container, falls asleep when nobody's around, reacts to clicks, changes its mood over time, and can speak on demand.
4
+
5
+ It was built for the handheld-console interface of [V-DEX](https://v-dex.vercel.app), where it lives inside the device's screen.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pnpm add @vrixton/pixel-pet
11
+ ```
12
+
13
+ It needs React 19.
14
+
15
+ ## Usage
16
+
17
+ Import the stylesheet once, for example in your root layout, and place the pet inside a positioned container:
18
+
19
+ ```tsx
20
+ import '@vrixton/pixel-pet/styles.css';
21
+ import { PixelPet } from '@vrixton/pixel-pet';
22
+
23
+ export function Screen() {
24
+ return (
25
+ <div style={{ position: 'relative', height: 160 }}>
26
+ <PixelPet petName="Danko" variant="dog" />
27
+ </div>
28
+ );
29
+ }
30
+ ```
31
+
32
+ The pet fills its nearest positioned ancestor and walks along its bottom edge. Clicks pass through the empty area, so the pet never blocks the content underneath it. Its speech bubble rises about 120px above the bottom edge for a one-line message and grows upward when the text wraps, so give the container enough height or leave its overflow visible.
33
+
34
+ The component is already marked as a client component, so in Next.js you can render it straight from a server component.
35
+
36
+ ## Making it speak
37
+
38
+ Pass a ref to get the `say()` method. The component that holds the ref uses hooks, so in Next.js it needs `'use client'`:
39
+
40
+ ```tsx
41
+ 'use client';
42
+
43
+ import { useRef } from 'react';
44
+ import { PixelPet, type PixelPetHandle } from '@vrixton/pixel-pet';
45
+
46
+ export function Screen() {
47
+ const pet = useRef<PixelPetHandle>(null);
48
+
49
+ return (
50
+ <div style={{ position: 'relative', height: 160 }}>
51
+ <PixelPet ref={pet} petName="Danko" variant="dog" />
52
+ <button onClick={() => pet.current?.say('Press the center button.', { emotion: 'curious' })}>
53
+ Ask Danko
54
+ </button>
55
+ </div>
56
+ );
57
+ }
58
+ ```
59
+
60
+ The message shows right away, without hovering, and screen readers announce it. The pet wakes up if it was asleep and stands still while it talks. A new message replaces the current one, a click on the pet interrupts it, and hovering over the pet pauses the timer so the message can be read.
61
+
62
+ | Option | Type | Default | Description |
63
+ | --- | --- | --- | --- |
64
+ | `emotion` | `'happy' \| 'angry' \| 'curious'` | none | Plays that emotion's animation and colors the bubble to match. Without it, the bubble is neutral. |
65
+ | `duration` | `number` | `5000` | How long the message stays, in milliseconds. Pass `Infinity` to keep it until a new message replaces it, the user taps the pet, or the pet falls asleep. |
66
+
67
+ Call `pet.current?.hush()` to close the current message right away, for example when the user dismisses it.
68
+
69
+ ## Props
70
+
71
+ | Prop | Type | Default | Description |
72
+ | --- | --- | --- | --- |
73
+ | `petName` | `string` | `'Static'` | The name shown in the mood messages. |
74
+ | `variant` | `'cat' \| 'dog' \| 'axolotl' \| 'parakeet' \| 'hamster'` | `'cat'` | Which of the bundled sprites to use. |
75
+ | `spriteSrc` | `string` | none | A URL for your own sprite sheet. It takes priority over `variant`. |
76
+ | `color` | `string` | `'#FDDBA0'` | The pet's main color, used for its name and for the border of neutral speech bubbles. |
77
+ | `label` | `string` | `petName` | The accessible name of the pet's button. |
78
+ | `walk` | `boolean` | `true` | Set it to `false` to keep the pet in place. |
79
+ | `messages` | `PetMessages` | English messages | Replaces the text shown for each emotion. |
80
+ | `dialogStyle` | `DialogStyleProps` | none | Styles for every bubble. |
81
+ | `emotionDialogStyles` | `Partial<Record<PetEmotion, DialogStyleProps>>` | none | Styles for the bubble of a single emotion. |
82
+ | `onClick` | `(event) => void` | none | Called when the user clicks or taps the pet. Call `event.preventDefault()` inside it to skip the pet's own reaction. |
83
+ | `ref` | `Ref<PixelPetHandle>` | none | Gives you the `say()` and `hush()` methods. |
84
+
85
+ ## Reusing a configuration
86
+
87
+ `PixelPetProps` is exported, so you can keep a pet's settings in one typed object and spread it:
88
+
89
+ ```tsx
90
+ import { PixelPet, type PixelPetProps } from '@vrixton/pixel-pet';
91
+
92
+ const danko: PixelPetProps = {
93
+ petName: 'Danko',
94
+ variant: 'dog',
95
+ color: '#8BE383',
96
+ };
97
+
98
+ <PixelPet {...danko} />
99
+ <PixelPet {...danko} walk={false} />
100
+ ```
101
+
102
+ ## Translating the messages
103
+
104
+ Each message accepts any React node, so you can translate it and keep the colored words:
105
+
106
+ ```tsx
107
+ <PixelPet
108
+ petName="Danko"
109
+ messages={{
110
+ happy: (
111
+ <>
112
+ <span className="pixel-pet-highlight-pet">Danko</span> está{' '}
113
+ <span className="pixel-pet-highlight-happy">feliz</span>.
114
+ </>
115
+ ),
116
+ angry: 'Danko está abrumado.',
117
+ curious: 'Danko tiene mucha curiosidad.',
118
+ }}
119
+ />
120
+ ```
121
+
122
+ The highlight classes are `pixel-pet-highlight-pet`, `pixel-pet-highlight-happy`, `pixel-pet-highlight-angry`, and `pixel-pet-highlight-curious`. Any emotion you leave out keeps its English message.
123
+
124
+ ## Styling the bubble
125
+
126
+ `dialogStyle` and `emotionDialogStyles` accept these fields:
127
+
128
+ | Field | Default |
129
+ | --- | --- |
130
+ | `backgroundColor` | Depends on the emotion |
131
+ | `textColor` | `'#ffffff'` |
132
+ | `borderColor` | Depends on the emotion |
133
+ | `borderWidth` | `'1px'` |
134
+ | `borderRadius` | `'5px'` |
135
+ | `fontFamily` | `'monospace'` |
136
+
137
+ A field set in `emotionDialogStyles` wins over the same field in `dialogStyle`, which wins over the default. Messages from `say()` without an emotion use `dialogStyle`.
138
+
139
+ The `color` prop sets the `--pixel-pet-color` CSS variable on the pet. You can also set that variable from your own stylesheet on any element that contains the pet.
140
+
141
+ ## Custom sprites
142
+
143
+ A sheet passed through `spriteSrc` has to follow the same layout as the bundled ones in [src/sprites](src/sprites): one row per animation, in the order idle, walk, happy, sleep, angry, and curious, with nine frames per row.
144
+
145
+ ## How it behaves
146
+
147
+ The pet wanders on its own, sometimes walking and sometimes standing, and turns around at the edges of the container. Every 30 seconds it may switch to a random mood for 5 seconds; hovering over it or focusing it with the keyboard shows what it's feeling. Clicking it shows its last mood, and clicking it more than five times in ten seconds makes it angry. After two minutes without mouse, touch, keyboard, or scroll wheel activity on the page, it falls asleep, and any activity wakes it up.
148
+
149
+ The pet is a real button, so it can be reached with the keyboard, has a visible focus outline, and is announced with its name. When the user prefers reduced motion, it stays still, its animation pauses, and the bubble appears without its pop-in effect.
150
+
151
+ ## Development
152
+
153
+ You need Node.js 24 or later and pnpm.
154
+
155
+ ```bash
156
+ pnpm install
157
+ pnpm playground
158
+ pnpm test
159
+ pnpm build
160
+ ```
161
+
162
+ `pnpm playground` opens a local demo with Vite, `pnpm test` runs the tests with Vitest, and `pnpm build` builds the package with tsup.
163
+
164
+ ## License
165
+
166
+ [MIT](LICENSE)
Binary file
Binary file
Binary file
Binary file
package/dist/index.css ADDED
@@ -0,0 +1 @@
1
+ .pixel-pet{position:absolute;inset:0;pointer-events:none}.pixel-pet-container{position:absolute;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;user-select:none;will-change:transform;pointer-events:auto}.pixel-pet-status{position:absolute;top:0;left:0;right:0;display:flex;justify-content:center}.pixel-pet-dialog{padding:6px 10px;font-size:12px;min-width:80px;width:max-content;max-width:240px;box-sizing:border-box;text-align:center;animation:pixel-pet-pop-in .2s ease-out;pointer-events:none;position:absolute;bottom:27px;border:1px solid;color:#fff;box-shadow:none}.pixel-pet-highlight-pet{color:var(--pixel-pet-color, #FDDBA0);font-weight:700}.pixel-pet-highlight-happy{color:#ffeb3b;font-weight:700}.pixel-pet-highlight-angry{color:#f04037;font-weight:700}.pixel-pet-highlight-curious{color:#8be383;font-weight:700}.pixel-pet-sprite{display:block;padding:0;border:0;background-color:transparent;color:inherit;font:inherit;appearance:none;cursor:pointer;image-rendering:pixelated;image-rendering:crisp-edges;background-repeat:no-repeat;width:60px;height:60px;filter:drop-shadow(0px 4px 6px rgba(0,0,0,.6)) saturate(140%)}.pixel-pet-cat{background-image:url("./cat-sprite-4GTCYV2F.png")}.pixel-pet-dog{background-image:url("./dog-sprite-VCBZ3EYT.png")}.pixel-pet-axolotl{background-image:url("./axolotl-sprite-HPGKQ5TQ.png")}.pixel-pet-parakeet{background-image:url("./parakeet-sprite-RZ7M7WXL.png")}.pixel-pet-hamster{background-image:url("./hamster-sprite-FECZBAMR.png")}.pixel-pet-sprite:focus-visible{outline:2px solid currentColor;outline-offset:2px}.pixel-pet-sprite.pixel-pet-flip-x{transform:scaleX(-1)}.pixel-pet-state-idle{animation:pixel-pet-idle 1s steps(9) infinite}.pixel-pet-state-walk{animation:pixel-pet-walk 1.3s steps(9) infinite}.pixel-pet-state-happy{animation:pixel-pet-happy .8s steps(9) infinite;height:65px}.pixel-pet-state-sleep{animation:pixel-pet-sleep 2s steps(9) infinite;height:45px;width:65px}.pixel-pet-state-angry{animation:pixel-pet-angry .8s steps(9) infinite;width:65px}.pixel-pet-state-curious{animation:pixel-pet-curious 3s steps(9) infinite}@keyframes pixel-pet-idle{0%{background-position:0 -5px}to{background-position:-605px -5px}}@keyframes pixel-pet-walk{0%{background-position:0 -73px}to{background-position:-600px -73px}}@keyframes pixel-pet-happy{0%{background-position:0 -135px}to{background-position:-610px -135px}}@keyframes pixel-pet-sleep{0%{background-position:0px -205px}to{background-position:-610px -205px}}@keyframes pixel-pet-angry{0%{background-position:0 -250px}to{background-position:-610px -250px}}@keyframes pixel-pet-curious{0%{background-position:0 -315px}to{background-position:-610px -315px}}@keyframes pixel-pet-pop-in{0%{transform:scale(.8);opacity:0}to{transform:scale(1);opacity:1}}@media(prefers-reduced-motion:reduce){.pixel-pet-sprite{animation-play-state:paused}.pixel-pet-dialog{animation:none}}
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+
3
+ interface DialogStyleProps {
4
+ backgroundColor?: string;
5
+ textColor?: string;
6
+ borderColor?: string;
7
+ borderWidth?: string;
8
+ borderRadius?: string;
9
+ fontFamily?: string;
10
+ }
11
+ type PetEmotion = 'happy' | 'angry' | 'curious';
12
+ type PetAction = 'idle' | 'walk' | 'sleep';
13
+ type PetVariant = 'cat' | 'dog' | 'axolotl' | 'parakeet' | 'hamster';
14
+ type PetMessages = Partial<Record<PetEmotion, React.ReactNode>>;
15
+ interface PetSayOptions {
16
+ emotion?: PetEmotion;
17
+ duration?: number;
18
+ }
19
+ interface PixelPetHandle {
20
+ say: (message: React.ReactNode, options?: PetSayOptions) => void;
21
+ hush: () => void;
22
+ }
23
+ interface PixelPetProps {
24
+ petName?: string;
25
+ variant?: PetVariant;
26
+ spriteSrc?: string;
27
+ color?: string;
28
+ label?: string;
29
+ walk?: boolean;
30
+ messages?: PetMessages;
31
+ dialogStyle?: DialogStyleProps;
32
+ emotionDialogStyles?: Partial<Record<PetEmotion, DialogStyleProps>>;
33
+ onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
34
+ ref?: React.Ref<PixelPetHandle>;
35
+ }
36
+ declare const PixelPet: React.FC<PixelPetProps>;
37
+
38
+ export { type DialogStyleProps, type PetAction, type PetEmotion, type PetMessages, type PetSayOptions, type PetVariant, PixelPet, type PixelPetHandle, type PixelPetProps };
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+
3
+ interface DialogStyleProps {
4
+ backgroundColor?: string;
5
+ textColor?: string;
6
+ borderColor?: string;
7
+ borderWidth?: string;
8
+ borderRadius?: string;
9
+ fontFamily?: string;
10
+ }
11
+ type PetEmotion = 'happy' | 'angry' | 'curious';
12
+ type PetAction = 'idle' | 'walk' | 'sleep';
13
+ type PetVariant = 'cat' | 'dog' | 'axolotl' | 'parakeet' | 'hamster';
14
+ type PetMessages = Partial<Record<PetEmotion, React.ReactNode>>;
15
+ interface PetSayOptions {
16
+ emotion?: PetEmotion;
17
+ duration?: number;
18
+ }
19
+ interface PixelPetHandle {
20
+ say: (message: React.ReactNode, options?: PetSayOptions) => void;
21
+ hush: () => void;
22
+ }
23
+ interface PixelPetProps {
24
+ petName?: string;
25
+ variant?: PetVariant;
26
+ spriteSrc?: string;
27
+ color?: string;
28
+ label?: string;
29
+ walk?: boolean;
30
+ messages?: PetMessages;
31
+ dialogStyle?: DialogStyleProps;
32
+ emotionDialogStyles?: Partial<Record<PetEmotion, DialogStyleProps>>;
33
+ onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
34
+ ref?: React.Ref<PixelPetHandle>;
35
+ }
36
+ declare const PixelPet: React.FC<PixelPetProps>;
37
+
38
+ export { type DialogStyleProps, type PetAction, type PetEmotion, type PetMessages, type PetSayOptions, type PetVariant, PixelPet, type PixelPetHandle, type PixelPetProps };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ 'use client';
2
+ "use strict";var G=Object.defineProperty;var ve=Object.getOwnPropertyDescriptor;var we=Object.getOwnPropertyNames;var Me=Object.prototype.hasOwnProperty;var Ce=(s,a)=>{for(var u in a)G(s,u,{get:a[u],enumerable:!0})},Se=(s,a,u,b)=>{if(a&&typeof a=="object"||typeof a=="function")for(let d of we(a))!Me.call(s,d)&&d!==u&&G(s,d,{get:()=>a[d],enumerable:!(b=ve(a,d))||b.enumerable});return s};var De=s=>Se(G({},"__esModule",{value:!0}),s);var $e={};Ce($e,{PixelPet:()=>Oe});module.exports=De($e);var t=require("react");var r=require("react/jsx-runtime"),Z=["pointermove","pointerdown","keydown","wheel"],ne="(prefers-reduced-motion: reduce)",V=4,Ie=240,ee=5e3,te=65,oe=100,Ae=.04,Le=100,Ne=s=>{if(typeof window.matchMedia!="function")return()=>{};let a=window.matchMedia(ne);return a.addEventListener("change",s),()=>a.removeEventListener("change",s)},Fe=()=>typeof window.matchMedia=="function"&&window.matchMedia(ne).matches,We=()=>(0,t.useSyncExternalStore)(Ne,Fe,()=>!1),_e={happy:{backgroundColor:"#1e1c063d",borderColor:"#ffeb3b"},angry:{backgroundColor:"#21090833",borderColor:"#F04037"},curious:{backgroundColor:"#1f341d3a",borderColor:"#8BE383"}},He={backgroundColor:"#05080cd9",borderColor:"var(--pixel-pet-color, #FDDBA0)"},Oe=({petName:s="Static",variant:a="cat",spriteSrc:u,color:b,label:d,walk:re=!0,messages:A,dialogStyle:g={},emotionDialogStyles:ie={},onClick:se,ref:ae})=>{let[l,c]=(0,t.useState)("idle"),le=We(),y=re&&!le,L=(0,t.useRef)(null),M=(0,t.useRef)(null),Y=(0,t.useRef)(null),[N,pe]=(0,t.useState)(null),[E,C]=(0,t.useState)(null),[ce,q]=(0,t.useState)(!1),[ue,Q]=(0,t.useState)(!1),[de,fe]=(0,t.useState)(!1),[x,me]=(0,t.useState)(null),k=(0,t.useRef)(oe),[F,ge]=(0,t.useState)(0),[W,xe]=(0,t.useState)("right"),he={happy:A?.happy??(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("span",{className:"pixel-pet-highlight-pet",children:s})," is feeling"," ",(0,r.jsx)("span",{className:"pixel-pet-highlight-happy",children:"happy"}),"!"]}),angry:A?.angry??(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("span",{className:"pixel-pet-highlight-pet",children:s})," is feeling"," ",(0,r.jsx)("span",{className:"pixel-pet-highlight-angry",children:"overwhelmed"}),"!"]}),curious:A?.curious??(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("span",{className:"pixel-pet-highlight-pet",children:s})," is very"," ",(0,r.jsx)("span",{className:"pixel-pet-highlight-curious",children:"curious"})]})},K=(0,t.useRef)(l),S=(0,t.useRef)(null),P=(0,t.useRef)(!1),_=(0,t.useRef)(!1),p=(0,t.useRef)(null),j=(0,t.useRef)("happy"),H=(0,t.useRef)([]),T=(0,t.useRef)(!1),R=(0,t.useRef)(null),O=(0,t.useRef)(ee),$=(e,n=!1)=>{l==="sleep"&&c("idle"),j.current=e,C(e),fe(n),p.current&&clearTimeout(p.current),p.current=setTimeout(()=>{_.current||C(null)},5e3)},v=e=>{T.current=e!==null,me(e)},D=()=>{R.current&&clearTimeout(R.current)},J=e=>{D(),e!==1/0&&(R.current=setTimeout(()=>v(null),e))};(0,t.useImperativeHandle)(ae,()=>({say:(e,n={})=>{c(o=>o==="sleep"?"idle":o),v({message:e,emotion:n.emotion??null}),O.current=n.duration??ee,J(O.current)},hush:()=>{D(),v(null)}}),[]),(0,t.useEffect)(()=>{let e,n=()=>{c(o=>o==="sleep"?"idle":o),clearTimeout(e),e=setTimeout(()=>{C(null),v(null),c("sleep")},12e4)};return Z.forEach(o=>{window.addEventListener(o,n,{passive:!0})}),n(),()=>{clearTimeout(e),Z.forEach(o=>{window.removeEventListener(o,n)})}},[]),(0,t.useEffect)(()=>{K.current=l},[l]),(0,t.useEffect)(()=>{let e=setInterval(()=>{if(K.current==="sleep"||T.current)return;let n=["happy","angry","curious"],o=n[Math.floor(Math.random()*n.length)];$(o)},3e4);return()=>{clearInterval(e)}},[]),(0,t.useEffect)(()=>()=>{p.current&&clearTimeout(p.current),R.current&&clearTimeout(R.current)},[]),(0,t.useEffect)(()=>{if(!(l==="sleep"||!y)){if(l==="walk"){let e=Math.floor(Math.random()*175001)+5e3;S.current=setTimeout(()=>{c("idle"),P.current=!0,setTimeout(()=>{P.current=!1},3e4)},e)}else{let e=()=>{if(!P.current&&Math.random()>.4){c("walk");return}S.current=setTimeout(e,5e3)};e()}return()=>{S.current&&clearTimeout(S.current)}}},[l,y]);let B=e=>{k.current=e,M.current&&(M.current.style.transform=`translateX(${e}px)`)};(0,t.useEffect)(()=>{if(l!=="walk"||E!==null||x!==null||!y)return;let e=0,n=F-te;if(n<=e)return;let o=0,i=null,m=w=>{let X=i===null?0:Math.min(w-i,Le);i=w;let h=k.current+(W==="right"?1:-1)*Ae*X;if(h<=e||h>=n){if(Math.random()>.5){c("idle"),P.current=!0,setTimeout(()=>{P.current=!1},3e4);return}B(h<=e?e+2:n-2),xe(z=>z==="right"?"left":"right");return}B(h),o=requestAnimationFrame(m)};return o=requestAnimationFrame(m),()=>cancelAnimationFrame(o)},[l,E,x,W,y,F]),(0,t.useEffect)(()=>{let e=L.current;if(!e)return;let n=i=>{ge(i),B(Math.max(Math.min(k.current,i-te),0))};if(n(e.clientWidth),typeof ResizeObserver>"u")return;let o=new ResizeObserver(([i])=>{i&&n(i.contentRect.width)});return o.observe(e),()=>o.disconnect()},[]);let be=e=>{if(se?.(e),e.defaultPrevented)return;let n=Date.now();l==="sleep"&&c("idle"),T.current&&(D(),v(null));let o=[...H.current,n].filter(i=>n-i<=1e4);H.current=o,o.length>5?(H.current=[],$("angry",!0)):$(j.current,!0)},ye=()=>{q(!0),T.current&&D(),E&&(_.current=!0,p.current&&clearTimeout(p.current))},Ee=()=>{q(!1),T.current&&J(O.current),E&&(_.current=!1,p.current&&clearTimeout(p.current),p.current=setTimeout(()=>{C(null)},5e3))},I=x!==null&&l!=="sleep",f=l==="sleep"?null:I?x.emotion:E,U=I||f!==null&&(ce||ue||de);(0,t.useLayoutEffect)(()=>{let e=L.current,n=M.current,o=Y.current;if(!U||!e||!n||!o)return;let i=e.clientWidth,m=Math.max(Math.min(Ie,i-V*2),0),w=Math.min(o.offsetWidth,m),X=k.current+n.offsetWidth/2,h=V,z=i-V-w,Te=X-w/2,Re=Math.max(Math.min(Te,z),h);pe({left:Re-k.current,maxWidth:m})},[U,f,x,F]);let ke=()=>f?`pixel-pet-state-${f}`:(!y||I)&&l==="walk"?"pixel-pet-state-idle":`pixel-pet-state-${l}`,Pe=e=>{let n=e?_e[e]:He,o=e&&ie[e]||{},i=o.backgroundColor||g.backgroundColor||n.backgroundColor,m=o.borderColor||g.borderColor||n.borderColor;return{backgroundColor:i,color:o.textColor||g.textColor||"#ffffff",border:`${o.borderWidth||g.borderWidth||"1px"} solid ${m}`,borderRadius:o.borderRadius||g.borderRadius||"5px",fontFamily:o.fontFamily||g.fontFamily||"monospace"}};return(0,r.jsx)("div",{className:"pixel-pet",ref:L,style:b?{"--pixel-pet-color":b}:void 0,children:(0,r.jsxs)("div",{ref:M,className:"pixel-pet-container",style:{transform:`translateX(${oe}px)`},onMouseEnter:ye,onMouseLeave:Ee,children:[(0,r.jsx)("div",{className:"pixel-pet-status",role:"status",children:U&&(0,r.jsx)("div",{ref:Y,className:"pixel-pet-dialog",style:{...Pe(f),...N&&{left:N.left,maxWidth:N.maxWidth}},children:I?x.message:f&&he[f]})}),(0,r.jsx)("button",{type:"button","aria-label":d??s,className:`pixel-pet-sprite pixel-pet-${a} ${ke()} ${W==="left"?"pixel-pet-flip-x":""}`,style:u?{backgroundImage:`url("${u}")`}:void 0,onClick:be,onFocus:()=>Q(!0),onBlur:()=>Q(!1)})]})})};0&&(module.exports={PixelPet});
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ 'use client';
2
+ import{useState as l,useEffect as f,useImperativeHandle as Ce,useLayoutEffect as Se,useRef as i,useSyncExternalStore as De}from"react";import{Fragment as V,jsx as a,jsxs as I}from"react/jsx-runtime";var te=["pointermove","pointerdown","keydown","wheel"],ie="(prefers-reduced-motion: reduce)",G=4,Ie=240,oe=5e3,ne=65,re=100,Ae=.04,Le=100,Ne=c=>{if(typeof window.matchMedia!="function")return()=>{};let v=window.matchMedia(ie);return v.addEventListener("change",c),()=>v.removeEventListener("change",c)},Fe=()=>typeof window.matchMedia=="function"&&window.matchMedia(ie).matches,We=()=>De(Ne,Fe,()=>!1),_e={happy:{backgroundColor:"#1e1c063d",borderColor:"#ffeb3b"},angry:{backgroundColor:"#21090833",borderColor:"#F04037"},curious:{backgroundColor:"#1f341d3a",borderColor:"#8BE383"}},He={backgroundColor:"#05080cd9",borderColor:"var(--pixel-pet-color, #FDDBA0)"},Ge=({petName:c="Static",variant:v="cat",spriteSrc:Y,color:q,label:se,walk:ae=!0,messages:A,dialogStyle:m={},emotionDialogStyles:le={},onClick:pe,ref:ce})=>{let[r,p]=l("idle"),ue=We(),h=ae&&!ue,L=i(null),w=i(null),Q=i(null),[N,de]=l(null),[b,M]=l(null),[fe,K]=l(!1),[me,j]=l(!1),[ge,xe]=l(!1),[g,he]=l(null),y=i(re),[F,be]=l(0),[W,ye]=l("right"),Ee={happy:A?.happy??I(V,{children:[a("span",{className:"pixel-pet-highlight-pet",children:c})," is feeling"," ",a("span",{className:"pixel-pet-highlight-happy",children:"happy"}),"!"]}),angry:A?.angry??I(V,{children:[a("span",{className:"pixel-pet-highlight-pet",children:c})," is feeling"," ",a("span",{className:"pixel-pet-highlight-angry",children:"overwhelmed"}),"!"]}),curious:A?.curious??I(V,{children:[a("span",{className:"pixel-pet-highlight-pet",children:c})," is very"," ",a("span",{className:"pixel-pet-highlight-curious",children:"curious"})]})},J=i(r),C=i(null),E=i(!1),_=i(!1),s=i(null),Z=i("happy"),H=i([]),k=i(!1),P=i(null),O=i(oe),$=(e,o=!1)=>{r==="sleep"&&p("idle"),Z.current=e,M(e),xe(o),s.current&&clearTimeout(s.current),s.current=setTimeout(()=>{_.current||M(null)},5e3)},T=e=>{k.current=e!==null,he(e)},S=()=>{P.current&&clearTimeout(P.current)},ee=e=>{S(),e!==1/0&&(P.current=setTimeout(()=>T(null),e))};Ce(ce,()=>({say:(e,o={})=>{p(t=>t==="sleep"?"idle":t),T({message:e,emotion:o.emotion??null}),O.current=o.duration??oe,ee(O.current)},hush:()=>{S(),T(null)}}),[]),f(()=>{let e,o=()=>{p(t=>t==="sleep"?"idle":t),clearTimeout(e),e=setTimeout(()=>{M(null),T(null),p("sleep")},12e4)};return te.forEach(t=>{window.addEventListener(t,o,{passive:!0})}),o(),()=>{clearTimeout(e),te.forEach(t=>{window.removeEventListener(t,o)})}},[]),f(()=>{J.current=r},[r]),f(()=>{let e=setInterval(()=>{if(J.current==="sleep"||k.current)return;let o=["happy","angry","curious"],t=o[Math.floor(Math.random()*o.length)];$(t)},3e4);return()=>{clearInterval(e)}},[]),f(()=>()=>{s.current&&clearTimeout(s.current),P.current&&clearTimeout(P.current)},[]),f(()=>{if(!(r==="sleep"||!h)){if(r==="walk"){let e=Math.floor(Math.random()*175001)+5e3;C.current=setTimeout(()=>{p("idle"),E.current=!0,setTimeout(()=>{E.current=!1},3e4)},e)}else{let e=()=>{if(!E.current&&Math.random()>.4){p("walk");return}C.current=setTimeout(e,5e3)};e()}return()=>{C.current&&clearTimeout(C.current)}}},[r,h]);let B=e=>{y.current=e,w.current&&(w.current.style.transform=`translateX(${e}px)`)};f(()=>{if(r!=="walk"||b!==null||g!==null||!h)return;let e=0,o=F-ne;if(o<=e)return;let t=0,n=null,d=R=>{let X=n===null?0:Math.min(R-n,Le);n=R;let x=y.current+(W==="right"?1:-1)*Ae*X;if(x<=e||x>=o){if(Math.random()>.5){p("idle"),E.current=!0,setTimeout(()=>{E.current=!1},3e4);return}B(x<=e?e+2:o-2),ye(z=>z==="right"?"left":"right");return}B(x),t=requestAnimationFrame(d)};return t=requestAnimationFrame(d),()=>cancelAnimationFrame(t)},[r,b,g,W,h,F]),f(()=>{let e=L.current;if(!e)return;let o=n=>{be(n),B(Math.max(Math.min(y.current,n-ne),0))};if(o(e.clientWidth),typeof ResizeObserver>"u")return;let t=new ResizeObserver(([n])=>{n&&o(n.contentRect.width)});return t.observe(e),()=>t.disconnect()},[]);let ke=e=>{if(pe?.(e),e.defaultPrevented)return;let o=Date.now();r==="sleep"&&p("idle"),k.current&&(S(),T(null));let t=[...H.current,o].filter(n=>o-n<=1e4);H.current=t,t.length>5?(H.current=[],$("angry",!0)):$(Z.current,!0)},Pe=()=>{K(!0),k.current&&S(),b&&(_.current=!0,s.current&&clearTimeout(s.current))},Te=()=>{K(!1),k.current&&ee(O.current),b&&(_.current=!1,s.current&&clearTimeout(s.current),s.current=setTimeout(()=>{M(null)},5e3))},D=g!==null&&r!=="sleep",u=r==="sleep"?null:D?g.emotion:b,U=D||u!==null&&(fe||me||ge);Se(()=>{let e=L.current,o=w.current,t=Q.current;if(!U||!e||!o||!t)return;let n=e.clientWidth,d=Math.max(Math.min(Ie,n-G*2),0),R=Math.min(t.offsetWidth,d),X=y.current+o.offsetWidth/2,x=G,z=n-G-R,we=X-R/2,Me=Math.max(Math.min(we,z),x);de({left:Me-y.current,maxWidth:d})},[U,u,g,F]);let Re=()=>u?`pixel-pet-state-${u}`:(!h||D)&&r==="walk"?"pixel-pet-state-idle":`pixel-pet-state-${r}`,ve=e=>{let o=e?_e[e]:He,t=e&&le[e]||{},n=t.backgroundColor||m.backgroundColor||o.backgroundColor,d=t.borderColor||m.borderColor||o.borderColor;return{backgroundColor:n,color:t.textColor||m.textColor||"#ffffff",border:`${t.borderWidth||m.borderWidth||"1px"} solid ${d}`,borderRadius:t.borderRadius||m.borderRadius||"5px",fontFamily:t.fontFamily||m.fontFamily||"monospace"}};return a("div",{className:"pixel-pet",ref:L,style:q?{"--pixel-pet-color":q}:void 0,children:I("div",{ref:w,className:"pixel-pet-container",style:{transform:`translateX(${re}px)`},onMouseEnter:Pe,onMouseLeave:Te,children:[a("div",{className:"pixel-pet-status",role:"status",children:U&&a("div",{ref:Q,className:"pixel-pet-dialog",style:{...ve(u),...N&&{left:N.left,maxWidth:N.maxWidth}},children:D?g.message:u&&Ee[u]})}),a("button",{type:"button","aria-label":se??c,className:`pixel-pet-sprite pixel-pet-${v} ${Re()} ${W==="left"?"pixel-pet-flip-x":""}`,style:Y?{backgroundImage:`url("${Y}")`}:void 0,onClick:ke,onFocus:()=>j(!0),onBlur:()=>j(!1)})]})})};export{Ge as PixelPet};
package/package.json CHANGED
@@ -1,6 +1,76 @@
1
1
  {
2
2
  "name": "@vrixton/pixel-pet",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "A pixel art pet for React that walks, sleeps, reacts to clicks, and speaks on demand.",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.mts",
12
+ "default": "./dist/index.mjs"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "./styles.css": "./dist/index.css"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": [
25
+ "**/*.css"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "dev": "tsup --watch",
30
+ "playground": "vite",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest",
33
+ "prepublishOnly": "pnpm test && pnpm build"
34
+ },
35
+ "peerDependencies": {
36
+ "react": "^19"
37
+ },
38
+ "devDependencies": {
39
+ "@testing-library/jest-dom": "^7.0.1",
40
+ "@testing-library/react": "^16.3.3",
41
+ "@testing-library/user-event": "^14.6.7",
42
+ "@types/react": "^19",
43
+ "@types/react-dom": "^19",
44
+ "@vitejs/plugin-react": "^6.1.2",
45
+ "jsdom": "^30.1.2",
46
+ "react": "19.2.8",
47
+ "react-dom": "19.2.8",
48
+ "tsup": "^8.0.0",
49
+ "typescript": "^5",
50
+ "vite": "^8.3.0",
51
+ "vitest": "^5.0.1"
52
+ },
53
+ "keywords": [
54
+ "react",
55
+ "virtual-pet",
56
+ "pixel-art",
57
+ "widget"
58
+ ],
59
+ "author": "Victor Villavicencio",
60
+ "license": "MIT",
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "git+https://github.com/Vrixton/pixel-pet.git"
64
+ },
65
+ "bugs": {
66
+ "url": "https://github.com/Vrixton/pixel-pet/issues"
67
+ },
68
+ "homepage": "https://github.com/Vrixton/pixel-pet#readme",
69
+ "publishConfig": {
70
+ "access": "public"
71
+ },
72
+ "packageManager": "pnpm@12.5.1",
73
+ "engines": {
74
+ "node": ">=24"
75
+ }
76
+ }