@vrixton/pixel-pet 0.0.0-stage → 0.1.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,163 @@
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. |
66
+
67
+ ## Props
68
+
69
+ | Prop | Type | Default | Description |
70
+ | --- | --- | --- | --- |
71
+ | `petName` | `string` | `'Static'` | The name shown in the mood messages. |
72
+ | `variant` | `'cat' \| 'dog' \| 'axolotl' \| 'parakeet' \| 'hamster'` | `'cat'` | Which of the bundled sprites to use. |
73
+ | `spriteSrc` | `string` | none | A URL for your own sprite sheet. It takes priority over `variant`. |
74
+ | `color` | `string` | `'#FDDBA0'` | The pet's main color, used for its name and for the border of neutral speech bubbles. |
75
+ | `label` | `string` | `petName` | The accessible name of the pet's button. |
76
+ | `walk` | `boolean` | `true` | Set it to `false` to keep the pet in place. |
77
+ | `messages` | `PetMessages` | English messages | Replaces the text shown for each emotion. |
78
+ | `dialogStyle` | `DialogStyleProps` | none | Styles for every bubble. |
79
+ | `emotionDialogStyles` | `Partial<Record<PetEmotion, DialogStyleProps>>` | none | Styles for the bubble of a single emotion. |
80
+ | `ref` | `Ref<PixelPetHandle>` | none | Gives you the `say()` method. |
81
+
82
+ ## Reusing a configuration
83
+
84
+ `PixelPetProps` is exported, so you can keep a pet's settings in one typed object and spread it:
85
+
86
+ ```tsx
87
+ import { PixelPet, type PixelPetProps } from '@vrixton/pixel-pet';
88
+
89
+ const danko: PixelPetProps = {
90
+ petName: 'Danko',
91
+ variant: 'dog',
92
+ color: '#8BE383',
93
+ };
94
+
95
+ <PixelPet {...danko} />
96
+ <PixelPet {...danko} walk={false} />
97
+ ```
98
+
99
+ ## Translating the messages
100
+
101
+ Each message accepts any React node, so you can translate it and keep the colored words:
102
+
103
+ ```tsx
104
+ <PixelPet
105
+ petName="Danko"
106
+ messages={{
107
+ happy: (
108
+ <>
109
+ <span className="pixel-pet-highlight-pet">Danko</span> está{' '}
110
+ <span className="pixel-pet-highlight-happy">feliz</span>.
111
+ </>
112
+ ),
113
+ angry: 'Danko está abrumado.',
114
+ curious: 'Danko tiene mucha curiosidad.',
115
+ }}
116
+ />
117
+ ```
118
+
119
+ 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.
120
+
121
+ ## Styling the bubble
122
+
123
+ `dialogStyle` and `emotionDialogStyles` accept these fields:
124
+
125
+ | Field | Default |
126
+ | --- | --- |
127
+ | `backgroundColor` | Depends on the emotion |
128
+ | `textColor` | `'#ffffff'` |
129
+ | `borderColor` | Depends on the emotion |
130
+ | `borderWidth` | `'1px'` |
131
+ | `borderRadius` | `'5px'` |
132
+ | `fontFamily` | `'monospace'` |
133
+
134
+ 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`.
135
+
136
+ 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.
137
+
138
+ ## Custom sprites
139
+
140
+ 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.
141
+
142
+ ## How it behaves
143
+
144
+ 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.
145
+
146
+ 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.
147
+
148
+ ## Development
149
+
150
+ You need Node.js 24 or later and pnpm.
151
+
152
+ ```bash
153
+ pnpm install
154
+ pnpm playground
155
+ pnpm test
156
+ pnpm build
157
+ ```
158
+
159
+ `pnpm playground` opens a local demo with Vite, `pnpm test` runs the tests with Vitest, and `pnpm build` builds the package with tsup.
160
+
161
+ ## License
162
+
163
+ [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,36 @@
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
+ }
22
+ interface PixelPetProps {
23
+ petName?: string;
24
+ variant?: PetVariant;
25
+ spriteSrc?: string;
26
+ color?: string;
27
+ label?: string;
28
+ walk?: boolean;
29
+ messages?: PetMessages;
30
+ dialogStyle?: DialogStyleProps;
31
+ emotionDialogStyles?: Partial<Record<PetEmotion, DialogStyleProps>>;
32
+ ref?: React.Ref<PixelPetHandle>;
33
+ }
34
+ declare const PixelPet: React.FC<PixelPetProps>;
35
+
36
+ export { type DialogStyleProps, type PetAction, type PetEmotion, type PetMessages, type PetSayOptions, type PetVariant, PixelPet, type PixelPetHandle, type PixelPetProps };
@@ -0,0 +1,36 @@
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
+ }
22
+ interface PixelPetProps {
23
+ petName?: string;
24
+ variant?: PetVariant;
25
+ spriteSrc?: string;
26
+ color?: string;
27
+ label?: string;
28
+ walk?: boolean;
29
+ messages?: PetMessages;
30
+ dialogStyle?: DialogStyleProps;
31
+ emotionDialogStyles?: Partial<Record<PetEmotion, DialogStyleProps>>;
32
+ ref?: React.Ref<PixelPetHandle>;
33
+ }
34
+ declare const PixelPet: React.FC<PixelPetProps>;
35
+
36
+ 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 Re=Object.getOwnPropertyDescriptor;var we=Object.getOwnPropertyNames;var ve=Object.prototype.hasOwnProperty;var Ce=(i,s)=>{for(var u in s)G(i,u,{get:s[u],enumerable:!0})},Me=(i,s,u,b)=>{if(s&&typeof s=="object"||typeof s=="function")for(let d of we(s))!ve.call(i,d)&&d!==u&&G(i,d,{get:()=>s[d],enumerable:!(b=Re(s,d))||b.enumerable});return i};var Se=i=>Me(G({},"__esModule",{value:!0}),i);var He={};Ce(He,{PixelPet:()=>Oe});module.exports=Se(He);var t=require("react");var r=require("react/jsx-runtime"),Z=["pointermove","pointerdown","keydown","wheel"],ne="(prefers-reduced-motion: reduce)",V=4,De=240,ee=5e3,te=65,oe=100,Ie=.04,Ae=100,Le=i=>{if(typeof window.matchMedia!="function")return()=>{};let s=window.matchMedia(ne);return s.addEventListener("change",i),()=>s.removeEventListener("change",i)},Ne=()=>typeof window.matchMedia=="function"&&window.matchMedia(ne).matches,Fe=()=>(0,t.useSyncExternalStore)(Le,Ne,()=>!1),We={happy:{backgroundColor:"#1e1c063d",borderColor:"#ffeb3b"},angry:{backgroundColor:"#21090833",borderColor:"#F04037"},curious:{backgroundColor:"#1f341d3a",borderColor:"#8BE383"}},_e={backgroundColor:"#05080cd9",borderColor:"var(--pixel-pet-color, #FDDBA0)"},Oe=({petName:i="Static",variant:s="cat",spriteSrc:u,color:b,label:d,walk:re=!0,messages:I,dialogStyle:g={},emotionDialogStyles:ie={},ref:se})=>{let[a,c]=(0,t.useState)("idle"),ae=Fe(),y=re&&!ae,A=(0,t.useRef)(null),v=(0,t.useRef)(null),Y=(0,t.useRef)(null),[L,le]=(0,t.useState)(null),[k,C]=(0,t.useState)(null),[pe,q]=(0,t.useState)(!1),[ce,Q]=(0,t.useState)(!1),[ue,de]=(0,t.useState)(!1),[x,fe]=(0,t.useState)(null),E=(0,t.useRef)(oe),[N,me]=(0,t.useState)(0),[F,ge]=(0,t.useState)("right"),xe={happy:I?.happy??(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("span",{className:"pixel-pet-highlight-pet",children:i})," is feeling"," ",(0,r.jsx)("span",{className:"pixel-pet-highlight-happy",children:"happy"}),"!"]}),angry:I?.angry??(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("span",{className:"pixel-pet-highlight-pet",children:i})," is feeling"," ",(0,r.jsx)("span",{className:"pixel-pet-highlight-angry",children:"overwhelmed"}),"!"]}),curious:I?.curious??(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)("span",{className:"pixel-pet-highlight-pet",children:i})," is very"," ",(0,r.jsx)("span",{className:"pixel-pet-highlight-curious",children:"curious"})]})},K=(0,t.useRef)(a),M=(0,t.useRef)(null),P=(0,t.useRef)(!1),W=(0,t.useRef)(!1),p=(0,t.useRef)(null),j=(0,t.useRef)("happy"),_=(0,t.useRef)([]),T=(0,t.useRef)(!1),R=(0,t.useRef)(null),O=(0,t.useRef)(ee),H=(e,n=!1)=>{a==="sleep"&&c("idle"),j.current=e,C(e),de(n),p.current&&clearTimeout(p.current),p.current=setTimeout(()=>{W.current||C(null)},5e3)},S=e=>{T.current=e!==null,fe(e)},$=()=>{R.current&&clearTimeout(R.current)},J=e=>{$(),R.current=setTimeout(()=>S(null),e)};(0,t.useImperativeHandle)(se,()=>({say:(e,n={})=>{c(o=>o==="sleep"?"idle":o),S({message:e,emotion:n.emotion??null}),O.current=n.duration??ee,J(O.current)}}),[]),(0,t.useEffect)(()=>{let e,n=()=>{c(o=>o==="sleep"?"idle":o),clearTimeout(e),e=setTimeout(()=>{C(null),S(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=a},[a]),(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)];H(o)},3e4);return()=>{clearInterval(e)}},[]),(0,t.useEffect)(()=>()=>{p.current&&clearTimeout(p.current),R.current&&clearTimeout(R.current)},[]),(0,t.useEffect)(()=>{if(!(a==="sleep"||!y)){if(a==="walk"){let e=Math.floor(Math.random()*175001)+5e3;M.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}M.current=setTimeout(e,5e3)};e()}return()=>{M.current&&clearTimeout(M.current)}}},[a,y]);let U=e=>{E.current=e,v.current&&(v.current.style.transform=`translateX(${e}px)`)};(0,t.useEffect)(()=>{if(a!=="walk"||k!==null||x!==null||!y)return;let e=0,n=N-te;if(n<=e)return;let o=0,l=null,m=w=>{let B=l===null?0:Math.min(w-l,Ae);l=w;let h=E.current+(F==="right"?1:-1)*Ie*B;if(h<=e||h>=n){if(Math.random()>.5){c("idle"),P.current=!0,setTimeout(()=>{P.current=!1},3e4);return}U(h<=e?e+2:n-2),ge(z=>z==="right"?"left":"right");return}U(h),o=requestAnimationFrame(m)};return o=requestAnimationFrame(m),()=>cancelAnimationFrame(o)},[a,k,x,F,y,N]),(0,t.useEffect)(()=>{let e=A.current;if(!e)return;let n=l=>{me(l),U(Math.max(Math.min(E.current,l-te),0))};if(n(e.clientWidth),typeof ResizeObserver>"u")return;let o=new ResizeObserver(([l])=>{l&&n(l.contentRect.width)});return o.observe(e),()=>o.disconnect()},[]);let he=()=>{let e=Date.now();a==="sleep"&&c("idle"),T.current&&($(),S(null));let n=[..._.current,e].filter(o=>e-o<=1e4);_.current=n,n.length>5?(_.current=[],H("angry",!0)):H(j.current,!0)},be=()=>{q(!0),T.current&&$(),k&&(W.current=!0,p.current&&clearTimeout(p.current))},ye=()=>{q(!1),T.current&&J(O.current),k&&(W.current=!1,p.current&&clearTimeout(p.current),p.current=setTimeout(()=>{C(null)},5e3))},D=x!==null&&a!=="sleep",f=a==="sleep"?null:D?x.emotion:k,X=D||f!==null&&(pe||ce||ue);(0,t.useLayoutEffect)(()=>{let e=A.current,n=v.current,o=Y.current;if(!X||!e||!n||!o)return;let l=e.clientWidth,m=Math.max(Math.min(De,l-V*2),0),w=Math.min(o.offsetWidth,m),B=E.current+n.offsetWidth/2,h=V,z=l-V-w,Pe=B-w/2,Te=Math.max(Math.min(Pe,z),h);le({left:Te-E.current,maxWidth:m})},[X,f,x,N]);let ke=()=>f?`pixel-pet-state-${f}`:(!y||D)&&a==="walk"?"pixel-pet-state-idle":`pixel-pet-state-${a}`,Ee=e=>{let n=e?We[e]:_e,o=e&&ie[e]||{},l=o.backgroundColor||g.backgroundColor||n.backgroundColor,m=o.borderColor||g.borderColor||n.borderColor;return{backgroundColor:l,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:A,style:b?{"--pixel-pet-color":b}:void 0,children:(0,r.jsxs)("div",{ref:v,className:"pixel-pet-container",style:{transform:`translateX(${oe}px)`},onMouseEnter:be,onMouseLeave:ye,children:[(0,r.jsx)("div",{className:"pixel-pet-status",role:"status",children:X&&(0,r.jsx)("div",{ref:Y,className:"pixel-pet-dialog",style:{...Ee(f),...L&&{left:L.left,maxWidth:L.maxWidth}},children:D?x.message:f&&xe[f]})}),(0,r.jsx)("button",{type:"button","aria-label":d??i,className:`pixel-pet-sprite pixel-pet-${s} ${ke()} ${F==="left"?"pixel-pet-flip-x":""}`,style:u?{backgroundImage:`url("${u}")`}:void 0,onClick:he,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 Me,useRef as i,useSyncExternalStore as Se}from"react";import{Fragment as V,jsx as a,jsxs as D}from"react/jsx-runtime";var te=["pointermove","pointerdown","keydown","wheel"],ie="(prefers-reduced-motion: reduce)",G=4,De=240,oe=5e3,ne=65,re=100,Ie=.04,Ae=100,Le=c=>{if(typeof window.matchMedia!="function")return()=>{};let R=window.matchMedia(ie);return R.addEventListener("change",c),()=>R.removeEventListener("change",c)},Ne=()=>typeof window.matchMedia=="function"&&window.matchMedia(ie).matches,Fe=()=>Se(Le,Ne,()=>!1),We={happy:{backgroundColor:"#1e1c063d",borderColor:"#ffeb3b"},angry:{backgroundColor:"#21090833",borderColor:"#F04037"},curious:{backgroundColor:"#1f341d3a",borderColor:"#8BE383"}},_e={backgroundColor:"#05080cd9",borderColor:"var(--pixel-pet-color, #FDDBA0)"},ze=({petName:c="Static",variant:R="cat",spriteSrc:Y,color:q,label:se,walk:ae=!0,messages:I,dialogStyle:m={},emotionDialogStyles:le={},ref:pe})=>{let[n,p]=l("idle"),ce=Fe(),h=ae&&!ce,A=i(null),w=i(null),Q=i(null),[L,ue]=l(null),[b,v]=l(null),[de,K]=l(!1),[fe,j]=l(!1),[me,ge]=l(!1),[g,xe]=l(null),y=i(re),[N,he]=l(0),[F,be]=l("right"),ye={happy:I?.happy??D(V,{children:[a("span",{className:"pixel-pet-highlight-pet",children:c})," is feeling"," ",a("span",{className:"pixel-pet-highlight-happy",children:"happy"}),"!"]}),angry:I?.angry??D(V,{children:[a("span",{className:"pixel-pet-highlight-pet",children:c})," is feeling"," ",a("span",{className:"pixel-pet-highlight-angry",children:"overwhelmed"}),"!"]}),curious:I?.curious??D(V,{children:[a("span",{className:"pixel-pet-highlight-pet",children:c})," is very"," ",a("span",{className:"pixel-pet-highlight-curious",children:"curious"})]})},J=i(n),C=i(null),k=i(!1),W=i(!1),s=i(null),Z=i("happy"),_=i([]),E=i(!1),P=i(null),O=i(oe),H=(e,o=!1)=>{n==="sleep"&&p("idle"),Z.current=e,v(e),ge(o),s.current&&clearTimeout(s.current),s.current=setTimeout(()=>{W.current||v(null)},5e3)},M=e=>{E.current=e!==null,xe(e)},$=()=>{P.current&&clearTimeout(P.current)},ee=e=>{$(),P.current=setTimeout(()=>M(null),e)};Ce(pe,()=>({say:(e,o={})=>{p(t=>t==="sleep"?"idle":t),M({message:e,emotion:o.emotion??null}),O.current=o.duration??oe,ee(O.current)}}),[]),f(()=>{let e,o=()=>{p(t=>t==="sleep"?"idle":t),clearTimeout(e),e=setTimeout(()=>{v(null),M(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=n},[n]),f(()=>{let e=setInterval(()=>{if(J.current==="sleep"||E.current)return;let o=["happy","angry","curious"],t=o[Math.floor(Math.random()*o.length)];H(t)},3e4);return()=>{clearInterval(e)}},[]),f(()=>()=>{s.current&&clearTimeout(s.current),P.current&&clearTimeout(P.current)},[]),f(()=>{if(!(n==="sleep"||!h)){if(n==="walk"){let e=Math.floor(Math.random()*175001)+5e3;C.current=setTimeout(()=>{p("idle"),k.current=!0,setTimeout(()=>{k.current=!1},3e4)},e)}else{let e=()=>{if(!k.current&&Math.random()>.4){p("walk");return}C.current=setTimeout(e,5e3)};e()}return()=>{C.current&&clearTimeout(C.current)}}},[n,h]);let U=e=>{y.current=e,w.current&&(w.current.style.transform=`translateX(${e}px)`)};f(()=>{if(n!=="walk"||b!==null||g!==null||!h)return;let e=0,o=N-ne;if(o<=e)return;let t=0,r=null,d=T=>{let B=r===null?0:Math.min(T-r,Ae);r=T;let x=y.current+(F==="right"?1:-1)*Ie*B;if(x<=e||x>=o){if(Math.random()>.5){p("idle"),k.current=!0,setTimeout(()=>{k.current=!1},3e4);return}U(x<=e?e+2:o-2),be(z=>z==="right"?"left":"right");return}U(x),t=requestAnimationFrame(d)};return t=requestAnimationFrame(d),()=>cancelAnimationFrame(t)},[n,b,g,F,h,N]),f(()=>{let e=A.current;if(!e)return;let o=r=>{he(r),U(Math.max(Math.min(y.current,r-ne),0))};if(o(e.clientWidth),typeof ResizeObserver>"u")return;let t=new ResizeObserver(([r])=>{r&&o(r.contentRect.width)});return t.observe(e),()=>t.disconnect()},[]);let ke=()=>{let e=Date.now();n==="sleep"&&p("idle"),E.current&&($(),M(null));let o=[..._.current,e].filter(t=>e-t<=1e4);_.current=o,o.length>5?(_.current=[],H("angry",!0)):H(Z.current,!0)},Ee=()=>{K(!0),E.current&&$(),b&&(W.current=!0,s.current&&clearTimeout(s.current))},Pe=()=>{K(!1),E.current&&ee(O.current),b&&(W.current=!1,s.current&&clearTimeout(s.current),s.current=setTimeout(()=>{v(null)},5e3))},S=g!==null&&n!=="sleep",u=n==="sleep"?null:S?g.emotion:b,X=S||u!==null&&(de||fe||me);Me(()=>{let e=A.current,o=w.current,t=Q.current;if(!X||!e||!o||!t)return;let r=e.clientWidth,d=Math.max(Math.min(De,r-G*2),0),T=Math.min(t.offsetWidth,d),B=y.current+o.offsetWidth/2,x=G,z=r-G-T,we=B-T/2,ve=Math.max(Math.min(we,z),x);ue({left:ve-y.current,maxWidth:d})},[X,u,g,N]);let Te=()=>u?`pixel-pet-state-${u}`:(!h||S)&&n==="walk"?"pixel-pet-state-idle":`pixel-pet-state-${n}`,Re=e=>{let o=e?We[e]:_e,t=e&&le[e]||{},r=t.backgroundColor||m.backgroundColor||o.backgroundColor,d=t.borderColor||m.borderColor||o.borderColor;return{backgroundColor:r,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:A,style:q?{"--pixel-pet-color":q}:void 0,children:D("div",{ref:w,className:"pixel-pet-container",style:{transform:`translateX(${re}px)`},onMouseEnter:Ee,onMouseLeave:Pe,children:[a("div",{className:"pixel-pet-status",role:"status",children:X&&a("div",{ref:Q,className:"pixel-pet-dialog",style:{...Re(u),...L&&{left:L.left,maxWidth:L.maxWidth}},children:S?g.message:u&&ye[u]})}),a("button",{type:"button","aria-label":se??c,className:`pixel-pet-sprite pixel-pet-${R} ${Te()} ${F==="left"?"pixel-pet-flip-x":""}`,style:Y?{backgroundImage:`url("${Y}")`}:void 0,onClick:ke,onFocus:()=>j(!0),onBlur:()=>j(!1)})]})})};export{ze 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.1.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
+ }