diff --git a/extensions/baby-buddy/.eslintrc.json b/extensions/baby-buddy/.eslintrc.json new file mode 100644 index 00000000000..e6258e11d91 --- /dev/null +++ b/extensions/baby-buddy/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "root": true, + "extends": ["@raycast"] +} diff --git a/extensions/baby-buddy/.gitignore b/extensions/baby-buddy/.gitignore new file mode 100644 index 00000000000..9ff34e3e9f3 --- /dev/null +++ b/extensions/baby-buddy/.gitignore @@ -0,0 +1,13 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# Raycast specific files +raycast-env.d.ts +.raycast-swift-build +.swiftpm +compiled_raycast_swift + +# misc +.DS_Store diff --git a/extensions/baby-buddy/.prettierrc b/extensions/baby-buddy/.prettierrc new file mode 100644 index 00000000000..fc0f5030683 --- /dev/null +++ b/extensions/baby-buddy/.prettierrc @@ -0,0 +1,4 @@ +{ + "printWidth": 120, + "singleQuote": false +} diff --git a/extensions/baby-buddy/CHANGELOG.md b/extensions/baby-buddy/CHANGELOG.md new file mode 100644 index 00000000000..17d778103bc --- /dev/null +++ b/extensions/baby-buddy/CHANGELOG.md @@ -0,0 +1,11 @@ +# Baby Buddy Changelog + +## [Initial Version] - {PR_MERGE_DATE} +- Initial version code +- Added "Show Baby Info" command to display children and their recent activities +- Added ability to view detailed information about each child +- Added support for viewing last feeding, sleep, and diaper change information +- Added "Active Timers" command to monitor and manage running timers +- Added ability to create new timers with custom start times +- Added ability to convert timers into Baby Buddy events (feeding, pumping, sleep, tummy time) +- Added AI tooling diff --git a/extensions/baby-buddy/README.md b/extensions/baby-buddy/README.md new file mode 100644 index 00000000000..90ad3326fa5 --- /dev/null +++ b/extensions/baby-buddy/README.md @@ -0,0 +1,66 @@ +# Baby Buddy Raycast Extension + +A Raycast extension for interacting with your [Baby Buddy](https://github.com/babybuddy/babybuddy) instance. + +## Features + +- View information about your children +- See last feeding, sleep, and diaper change times +- View detailed information about each child + - See daily totals for feedings, sleep, diapers, and tummy time + - Browse detailed activity history for each child + - View comprehensive statistics for each activity type +- Monitor and manage active timers +- Create new timers with custom start times +- Convert timers into Baby Buddy events (feeding, pumping, sleep, tummy time) +- Edit timer details including name and start time + +## Setup + +1. Install the extension +2. Configure your Baby Buddy URL and API key in the extension preferences + - The URL should be the base URL of your Baby Buddy instance (e.g., `https://baby.example.com`) + - You can generate an API key in Baby Buddy under Settings > API + +## Commands + +- **Show Baby Info**: Displays a list of your children with their recent activity information + - View detailed child information including age and daily activity totals + - Browse activity history for feedings, sleep, diaper changes, and tummy time +- **Active Timers**: Shows all currently running timers with the ability to: + - Edit timer details (name and start time) + - End timers and convert them to Baby Buddy events (feeding, pumping, sleep, tummy time) + - Delete timers that were started by mistake + - Reset timers to start them over + - Create new timers for various activities + +## Timer Workflow + +1. **Create a timer** for an activity (feeding, pumping, sleep, tummy time) +2. **End the timer** when the activity is complete +3. **Choose the activity type** that the timer was for +4. **Fill in details** specific to that activity (amount, notes, etc.) +5. The timer is automatically deleted and a proper Baby Buddy event is created + +## AI Commands + +Baby Buddy Raycast Extension includes natural language AI capabilities to make tracking and managing baby activities easier: + +- **Log Activities with Natural Language**: Create records quickly using commands like: + - "Log a wet diaper for Emma at 2:30pm" + - "Record a 15-minute tummy time for Jacob" + - "Track 4oz bottle feeding for Sophia from 3:15pm to 3:30pm" + - "Log Emma slept from 7pm to 6am" + +- **Quick Status Queries**: Get information instantly: + - "When was Emma's last feeding?" + - "How long did Jacob sleep today?" + - "Show me Sophia's diaper changes for today" + - "What was Emma's total sleep time yesterday?" + +- **Weekly and Monthly Reports**: Generate summaries using natural language: + - "Summarize Jacob's feeding patterns this week" + - "Show me Emma's sleep trends for the month" + - "Generate a report of Sophia's diaper changes for the past 7 days" + +The AI interprets your requests and interacts directly with your Baby Buddy instance, saving you time and making baby tracking more intuitive. If you only have one child, it shouldn't need their name. If you have multiple children but typically only use one, you can edit the extension in the Raycast settings and add a prompt to default to a child by their name (e.g. "If I need to provide a child name but did not, assume I mean Emma") \ No newline at end of file diff --git a/extensions/baby-buddy/assets/icon.png b/extensions/baby-buddy/assets/icon.png new file mode 100644 index 00000000000..28beb5570fc Binary files /dev/null and b/extensions/baby-buddy/assets/icon.png differ diff --git a/extensions/baby-buddy/metadata/baby-buddy-1.png b/extensions/baby-buddy/metadata/baby-buddy-1.png new file mode 100644 index 00000000000..d1e3d7d7e33 Binary files /dev/null and b/extensions/baby-buddy/metadata/baby-buddy-1.png differ diff --git a/extensions/baby-buddy/metadata/baby-buddy-2.png b/extensions/baby-buddy/metadata/baby-buddy-2.png new file mode 100644 index 00000000000..2dabb286a77 Binary files /dev/null and b/extensions/baby-buddy/metadata/baby-buddy-2.png differ diff --git a/extensions/baby-buddy/metadata/baby-buddy-3.png b/extensions/baby-buddy/metadata/baby-buddy-3.png new file mode 100644 index 00000000000..51e71777658 Binary files /dev/null and b/extensions/baby-buddy/metadata/baby-buddy-3.png differ diff --git a/extensions/baby-buddy/package-lock.json b/extensions/baby-buddy/package-lock.json new file mode 100644 index 00000000000..72a915db8c0 --- /dev/null +++ b/extensions/baby-buddy/package-lock.json @@ -0,0 +1,3771 @@ +{ + "name": "baby-buddy", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "baby-buddy", + "license": "MIT", + "dependencies": { + "@raycast/api": "^1.60.0", + "@raycast/utils": "^1.17.0", + "axios": "^1.6.7", + "date-fns": "^2.30.0" + }, + "devDependencies": { + "@raycast/eslint-config": "^1.0.11", + "@types/node": "20.8.10", + "@types/react": "18.3.3", + "eslint": "^8.57.0", + "prettier": "^3.3.3", + "typescript": "^5.4.5" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@inquirer/checkbox": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.4.tgz", + "integrity": "sha512-d30576EZdApjAMceijXA5jDzRQHT/MygbC+J8I7EqA6f/FRpYxlRtRJbHF8gHeWYeSdOuTEJqonn7QLB1ELezA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.8.tgz", + "integrity": "sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.9.tgz", + "integrity": "sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==", + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.9.tgz", + "integrity": "sha512-8HjOppAxO7O4wV1ETUlJFg6NDjp/W2NP5FB9ZPAcinAlNT4ZIWOLe2pUVwmmPRSV0NMdI5r/+lflN55AwZOKSw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.11.tgz", + "integrity": "sha512-OZSUW4hFMW2TYvX/Sv+NnOZgO8CHT2TU1roUCUIF2T+wfw60XFRRp9MRUPCT06cRnKL+aemt2YmTWwt7rOrNEA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", + "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.8.tgz", + "integrity": "sha512-WXJI16oOZ3/LiENCAxe8joniNp8MQxF6Wi5V+EBbVA0ZIOpFcL4I9e7f7cXse0HJeIPCWO8Lcgnk98juItCi7Q==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.11.tgz", + "integrity": "sha512-pQK68CsKOgwvU2eA53AG/4npRTH2pvs/pZ2bFvzpBhrznh8Mcwt19c+nMO7LHRr3Vreu1KPhNBF3vQAKrjIulw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.11.tgz", + "integrity": "sha512-dH6zLdv+HEv1nBs96Case6eppkRggMe8LoOTl30+Gq5Wf27AO/vHFgStTVz4aoevLdNXqwE23++IXGw4eiOXTg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.4.0.tgz", + "integrity": "sha512-EZiJidQOT4O5PYtqnu1JbF0clv36oW2CviR66c7ma4LsupmmQlUwmdReGKRp456OWPWMz3PdrPiYg3aCk3op2w==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.4", + "@inquirer/confirm": "^5.1.8", + "@inquirer/editor": "^4.2.9", + "@inquirer/expand": "^4.0.11", + "@inquirer/input": "^4.1.8", + "@inquirer/number": "^3.0.11", + "@inquirer/password": "^4.0.11", + "@inquirer/rawlist": "^4.0.11", + "@inquirer/search": "^3.0.11", + "@inquirer/select": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.11.tgz", + "integrity": "sha512-uAYtTx0IF/PqUAvsRrF3xvnxJV516wmR6YVONOmCWJbbt87HcDHLfL9wmBQFbNJRv5kCjdYKrZcavDkH3sVJPg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.11.tgz", + "integrity": "sha512-9CWQT0ikYcg6Ls3TOa7jljsD7PgjcsYEM0bYE+Gkz+uoW9u8eaJCRHJKkucpRE5+xKtaaDbrND+nPDoxzjYyew==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.1.0.tgz", + "integrity": "sha512-z0a2fmgTSRN+YBuiK1ROfJ2Nvrpij5lVN3gPDkQGhavdvIVGHGW29LwYZfM/j42Ai2hUghTI/uoBuTbrJk42bA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz", + "integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oclif/core": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.2.10.tgz", + "integrity": "sha512-fAqcXgqkUm4v5FYy7qWP4w1HaOlVSVJveah+yVTo5Nm5kTiXhmD5mQQ7+knGeBaStyrtQy6WardoC2xSic9rlQ==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.2", + "ansis": "^3.17.0", + "clean-stack": "^3.0.1", + "cli-spinners": "^2.9.2", + "debug": "^4.4.0", + "ejs": "^3.1.10", + "get-package-type": "^0.1.0", + "globby": "^11.1.0", + "indent-string": "^4.0.0", + "is-wsl": "^2.2.0", + "lilconfig": "^3.1.3", + "minimatch": "^9.0.5", + "semver": "^7.6.3", + "string-width": "^4.2.3", + "supports-color": "^8", + "widest-line": "^3.1.0", + "wordwrap": "^1.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-autocomplete": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-3.2.25.tgz", + "integrity": "sha512-xb0fiyGi78OGr7l4xbRe/rlIaFA8WgntIrn7FC/fnDpgq4wqmWLgYQ7LImE+eAIMCL9ozw6Cy915AaVAw6bxiA==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4", + "ansis": "^3.16.0", + "debug": "^4.4.0", + "ejs": "^3.1.10" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-help": { + "version": "6.2.27", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.27.tgz", + "integrity": "sha512-RWSWtCFVObRmCwgxVOye3lsYbPHTnB7G4He5LEAg2tf600Sil5yXEOL/ULx1TqL/XOQxKqRvmLn/rLQOMT85YA==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/plugin-not-found": { + "version": "3.2.46", + "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-3.2.46.tgz", + "integrity": "sha512-SN6zCU4AVcsQW8H881CfHpAuK12YspbnLppMlwvHooDCByN962ow702QVImoQ5HJ5MFkLUA8k7iByshsFFfHzg==", + "license": "MIT", + "dependencies": { + "@inquirer/prompts": "^7.3.2", + "@oclif/core": "^4", + "ansis": "^3.17.0", + "fast-levenshtein": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@raycast/api": { + "version": "1.93.2", + "resolved": "https://registry.npmjs.org/@raycast/api/-/api-1.93.2.tgz", + "integrity": "sha512-7f8aPqD83Tqu2cZ5fgUYaB4cita/lNQYrmueheBswPbv6ZQ/FBwhzxQJN/2j6dS7lglo8IxCjnm8Cxxl8qWruA==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^4.0.33", + "@oclif/plugin-autocomplete": "^3.2.10", + "@oclif/plugin-help": "^6.2.18", + "@oclif/plugin-not-found": "^3.2.28", + "@types/node": "20.8.10", + "@types/react": "18.3.3", + "esbuild": "^0.25.0", + "react": "18.3.1" + }, + "bin": { + "ray": "bin/run.js" + }, + "engines": { + "node": ">=20.5.0" + }, + "peerDependencies": { + "@types/node": "20.8.10", + "@types/react": "18.3.3", + "react-devtools": "5.2.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "react-devtools": { + "optional": true + } + } + }, + "node_modules/@raycast/eslint-config": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@raycast/eslint-config/-/eslint-config-1.0.11.tgz", + "integrity": "sha512-I0Lt8bwahVGkANUBxripIxKptMBz1Ou+UXGwfqgFvKwo1gVLrnlEngxaspQJA8L5pvzQkQMwizVCSgNC3bddWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@raycast/eslint-plugin": "^1.0.11", + "@rushstack/eslint-patch": "^1.10.4", + "@typescript-eslint/eslint-plugin": "^6.8.0", + "@typescript-eslint/parser": "^6.8.0", + "eslint-config-prettier": "^9.1.0" + }, + "peerDependencies": { + "eslint": ">=7", + "prettier": ">=2", + "typescript": ">=4" + } + }, + "node_modules/@raycast/eslint-plugin": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@raycast/eslint-plugin/-/eslint-plugin-1.0.16.tgz", + "integrity": "sha512-OyFL/W75/4hlgdUUI80Eoes0HjpVrJ8I1kB/PBH2RLjbcK22TC6IwZPXvhBZ5jF962O1TqtOuHrTjySwDaa/cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^5.62.0" + }, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/@raycast/utils": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@raycast/utils/-/utils-1.19.1.tgz", + "integrity": "sha512-/udUGcTZCgZZwzesmjBkqG5naQZTD/ZLHbqRwkWcF+W97vf9tr9raxKyQjKsdZ17OVllw2T3sHBQsVUdEmCm2g==", + "license": "MIT", + "dependencies": { + "cross-fetch": "^3.1.6", + "dequal": "^2.0.3", + "object-hash": "^3.0.0", + "signal-exit": "^4.0.2", + "stream-chain": "^2.2.5", + "stream-json": "^1.8.0" + }, + "peerDependencies": { + "@raycast/api": ">=1.69.0" + } + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz", + "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.8.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", + "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz", + "integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "license": "MIT", + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/optionator/node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/extensions/baby-buddy/package.json b/extensions/baby-buddy/package.json new file mode 100644 index 00000000000..e7c1cb73ddb --- /dev/null +++ b/extensions/baby-buddy/package.json @@ -0,0 +1,233 @@ +{ + "$schema": "https://www.raycast.com/schemas/extension.json", + "name": "baby-buddy", + "title": "Baby Buddy", + "description": "Interact with baby buddy", + "icon": "icon.png", + "author": "ShaneTheKing", + "categories": [ + "Applications", "Web" + ], + "license": "MIT", + "commands": [ + { + "name": "index", + "title": "Show Baby Info", + "description": "View information about your children", + "mode": "view" + }, + { + "name": "timers", + "title": "Active Timers", + "description": "View and manage active timers", + "mode": "view" + } + ], + "preferences": [ + { + "name": "baseUrl", + "title": "Baby Buddy URL", + "description": "The URL of your Baby Buddy instance (e.g., https://baby.example.com)", + "type": "textfield", + "required": true + }, + { + "name": "apiKey", + "title": "API Key", + "description": "Your Baby Buddy API key", + "type": "password", + "required": true + } + ], + "dependencies": { + "@raycast/api": "^1.60.0", + "@raycast/utils": "^1.17.0", + "axios": "^1.6.7", + "date-fns": "^2.30.0" + }, + "devDependencies": { + "@raycast/eslint-config": "^1.0.11", + "@types/node": "20.8.10", + "@types/react": "18.3.3", + "eslint": "^8.57.0", + "prettier": "^3.3.3", + "typescript": "^5.4.5" + }, + "scripts": { + "build": "ray build", + "dev": "ray develop", + "fix-lint": "ray lint --fix", + "lint": "ray lint", + "prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1", + "publish": "npx @raycast/api@latest publish" + }, + "tools": [ + { + "title": "Get Children", + "name": "getChildren", + "description": "Get a list of all children registered in Baby Buddy" + }, + { + "title": "Get Active Timers", + "name": "getActiveTimers", + "description": "Get a list of all active timers" + }, + { + "title": "Get Feedings", + "name": "getFeedings", + "description": "Get feeding information for a specific child" + }, + { + "title": "Get Sleep", + "name": "getSleep", + "description": "Get sleep information for a specific child" + }, + { + "title": "Get Diapers", + "name": "getDiapers", + "description": "Get diaper change information for a specific child" + }, + { + "title": "Get Tummy Time", + "name": "getTummyTime", + "description": "Get tummy time information for a specific child" + }, + { + "title": "Create Diaper", + "name": "createDiaper", + "description": "Create a new diaper change entry for a child" + }, + { + "title": "Create Feeding", + "name": "createFeeding", + "description": "Create a new feeding entry for a child" + }, + { + "title": "Edit Feeding", + "name": "editFeeding", + "description": "Update an existing feeding entry for a child" + }, + { + "title": "Delete Feeding", + "name": "deleteFeeding", + "description": "Delete a feeding entry" + }, + { + "title": "Edit Sleep", + "name": "editSleep", + "description": "Update an existing sleep entry for a child" + }, + { + "title": "Delete Sleep", + "name": "deleteSleep", + "description": "Delete a sleep entry" + }, + { + "title": "Edit Tummy Time", + "name": "editTummyTime", + "description": "Update an existing tummy time entry for a child" + }, + { + "title": "Delete Tummy Time", + "name": "deleteTummyTime", + "description": "Delete a tummy time entry" + }, + { + "title": "Edit Diaper", + "name": "editDiaper", + "description": "Update an existing diaper change entry for a child" + }, + { + "title": "Delete Diaper", + "name": "deleteDiaper", + "description": "Delete a diaper change entry" + }, + { + "title": "Edit Timer", + "name": "editTimer", + "description": "Update an existing timer" + }, + { + "title": "Delete Timer", + "name": "deleteTimer", + "description": "Delete a timer" + }, + { + "title": "Create Sleep", + "name": "createSleep", + "description": "Create a new sleep entry for a child" + }, + { + "title": "Create Tummy Time", + "name": "createTummyTime", + "description": "Create a new tummy time entry for a child" + }, + { + "title": "Create Timer", + "name": "createTimer", + "description": "Create a new timer for a child" + }, + { + "title": "Get Sleeping Children", + "name": "getSleepingChildren", + "description": "Get a list of children who are currently sleeping" + }, + { + "title": "Get Date Time", + "name": "getDateTime", + "description": "Get the current date and time, useful for if the AI needs a relative time or date. It can't seem to get the correct time on its own, so this tool provides the current time and date." + } + ], + "ai": { + "instructions": "If the user does not specify a child name and they have more than one child, ask them to specify a child name. If they only have one child, use that child's name without asking. All time-based inputs should be DateTimes unless otherwise specified, if the user only provides a time assume it's today unless otherwise specified. For time based outputs, format the output to the user's regional date time format. When the user refers to a feeding time, they usually want to know when it began. Other times with time ranges they want to know when they ended. If you need to know the user's current time or date for any reason, use the getDateTime tool. When it makes sense, use both the full time of an event and how duration or relative time, properly formatted. If the user asks you to convert a timer, that means they want you to use that timer for the basis of an activity - the start time should be the activity's start time and, unless otherwise specified, the end time should be now - and then delete the timer after the activity is created. If the user asks you to stop a timer, that means they want you to delete it.", + "evals": [ + { + "input": "When is the last time my children ate?", + "usedAsExample": true + }, + { + "input": "When is the last time my children had a diaper change?", + "usedAsExample": true + }, + { + "input": "Have my children eaten since their last nap?", + "usedAsExample": true + }, + { + "input": "Convert the active timer to a feeding entry for formula", + "usedAsExample": true + }, + { + "input": "Start a sleep timer starting 20 minutes ago", + "usedAsExample": true + }, + { + "input" : "@baby-buddy list active timers", + "usedAsExample": false, + "mocks" : { + "getActiveTimers" : [ + { + "child" : 3, + "childName" : "Child 2", + "duration" : "00:23:39.151727", + "id" : 5658, + "name" : "Sleep please", + "start" : "2025-03-28T17:10:00-07:00", + "user" : 1 + } + ] + }, + "expected" : [ + { + "callsTool" : { + "arguments" : { + + }, + "name" : "getActiveTimers" + } + } + ] + } + ] + } +} \ No newline at end of file diff --git a/extensions/baby-buddy/src/api.ts b/extensions/baby-buddy/src/api.ts new file mode 100644 index 00000000000..1811811146f --- /dev/null +++ b/extensions/baby-buddy/src/api.ts @@ -0,0 +1,559 @@ +import { getPreferenceValues } from "@raycast/api"; +import axios from "axios"; + +interface Preferences { + baseUrl: string; + apiKey: string; +} + +export interface Child { + id: number; + first_name: string; + last_name: string; + birth_date: string; + slug: string; + picture: string | null; +} + +export interface FeedingEntry { + id: number; + child: number; + start: string; + end: string; + duration: string; + type: string; + method: string; + amount: number | null; + notes: string; +} + +export interface SleepEntry { + id: number; + child: number; + start: string; + end: string; + duration: string; + nap: boolean; + notes: string; +} + +export interface DiaperEntry { + id: number; + child: number; + time: string; + wet: boolean; + solid: boolean; + color: string; + amount: number | null; + notes: string; +} + +export interface Timer { + id: number; + name: string; + start: string; + end: string | null; + duration: string | null; + active: boolean; + user: number; + child: number; +} + +export interface PumpingEntry { + id: number; + child: number; + start: string; + end: string; + duration: string; + amount: number | null; + notes: string; +} + +export interface TummyTimeEntry { + id: number; + child: number; + start: string; + end: string; + duration: string; + milestone: string; + notes: string; +} + +export class BabyBuddyAPI { + private baseUrl: string; + private apiKey: string; + + constructor() { + const preferences = getPreferenceValues(); + this.baseUrl = preferences.baseUrl.endsWith("/") ? preferences.baseUrl.slice(0, -1) : preferences.baseUrl; + this.apiKey = preferences.apiKey; + } + + private async request(endpoint: string): Promise { + const response = await axios.get(`${this.baseUrl}/api/${endpoint}`, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + return response.data; + } + + async getChildren(): Promise { + const response = await this.request<{ results: Child[] }>("children/"); + return response.results; + } + + async getLastFeeding(childId: number): Promise { + const response = await this.request<{ results: FeedingEntry[] }>( + `feedings/?child=${childId}&limit=1&ordering=-start`, + ); + return response.results.length > 0 ? response.results[0] : null; + } + + async getLastSleep(childId: number): Promise { + const response = await this.request<{ results: SleepEntry[] }>(`sleep/?child=${childId}&limit=1&ordering=-end`); + return response.results.length > 0 ? response.results[0] : null; + } + + async getLastDiaper(childId: number): Promise { + const response = await this.request<{ results: DiaperEntry[] }>(`changes/?child=${childId}&limit=1&ordering=-time`); + return response.results.length > 0 ? response.results[0] : null; + } + + async getActiveTimers(): Promise { + const response = await this.request<{ results: Timer[] }>("timers/?active=true"); + return response.results; + } + + async getTimerById(timerId: number): Promise { + try { + return await this.request(`timers/${timerId}/`); + } catch (error) { + console.error(`Failed to get timer with ID ${timerId}:`, error); + return null; + } + } + + async getChildName(childId: number): Promise { + try { + const response = await this.request<{ results: Child[] }>(`children/?id=${childId}`); + if (response.results && response.results.length > 0) { + const child = response.results[0]; + return `${child.first_name} ${child.last_name}`; + } + return "Unknown Child"; + } catch (error) { + console.error("Failed to get child name:", error); + return "Unknown Child"; + } + } + + async createTimer(childId: number, name: string, start: string): Promise { + try { + const response = await axios.post( + `${this.baseUrl}/api/timers/`, + { + child: childId, + name: name, + start: start, + active: true, + }, + { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }, + ); + return response.data; + } catch (error) { + console.error("Failed to create timer:", error); + throw error; + } + } + + async updateTimerName(timerId: number, name: string): Promise { + try { + const response = await axios.patch( + `${this.baseUrl}/api/timers/${timerId}/`, + { name: name }, + { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }, + ); + return response.data; + } catch (error) { + console.error("Failed to update timer name:", error); + throw error; + } + } + + async deleteTimer(timerId: number): Promise { + try { + await axios.delete(`${this.baseUrl}/api/timers/${timerId}/`, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + } catch (error) { + console.error("Failed to delete timer:", error); + throw error; + } + } + + async resetTimer(timerId: number): Promise { + try { + // Reset the timer by updating its start time to now + const response = await axios.patch( + `${this.baseUrl}/api/timers/${timerId}/`, + { + start: new Date().toISOString(), + end: null, // Clear any end time if it exists + active: true, // Ensure the timer is active + }, + { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }, + ); + return response.data; + } catch (error) { + console.error("Failed to reset timer:", error); + throw error; + } + } + + // Feeding methods + async createFeeding(data: Partial): Promise { + try { + const response = await axios.post(`${this.baseUrl}/api/feedings/`, data, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + return response.data; + } catch (error) { + console.error("Failed to create feeding:", error); + throw error; + } + } + + // Pumping methods + async createPumping(data: { + child: number; + start: string; + end: string; + amount: number | null; + notes: string; + }): Promise { + try { + const response = await axios.post(`${this.baseUrl}/api/pumping/`, data, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + return response.data; + } catch (error) { + console.error("Failed to create pumping:", error); + throw error; + } + } + + // Sleep methods + async createSleep(data: Partial): Promise { + try { + const response = await axios.post(`${this.baseUrl}/api/sleep/`, data, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + return response.data; + } catch (error) { + console.error("Failed to create sleep:", error); + throw error; + } + } + + async updateSleep(sleepId: number, data: Partial): Promise { + try { + const response = await axios.patch(`${this.baseUrl}/api/sleep/${sleepId}/`, data, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + return response.data; + } catch (error) { + console.error("Failed to update sleep entry:", error); + throw error; + } + } + + async deleteSleep(sleepId: number): Promise { + try { + await axios.delete(`${this.baseUrl}/api/sleep/${sleepId}/`, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + } catch (error) { + console.error("Failed to delete sleep entry:", error); + throw error; + } + } + + // Tummy time methods + async createTummyTime(data: Partial): Promise { + try { + const response = await axios.post(`${this.baseUrl}/api/tummy-times/`, data, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + return response.data; + } catch (error) { + console.error("Failed to create tummy time:", error); + throw error; + } + } + + async updateTummyTime(tummyTimeId: number, data: Partial): Promise { + try { + const response = await axios.patch(`${this.baseUrl}/api/tummy-times/${tummyTimeId}/`, data, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + return response.data; + } catch (error) { + console.error("Failed to update tummy time entry:", error); + throw error; + } + } + + async deleteTummyTime(tummyTimeId: number): Promise { + try { + await axios.delete(`${this.baseUrl}/api/tummy-times/${tummyTimeId}/`, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + } catch (error) { + console.error("Failed to delete tummy time entry:", error); + throw error; + } + } + + async updateTimer(timerId: number, updates: { name?: string; start?: string; end?: string }): Promise { + try { + const response = await axios.patch(`${this.baseUrl}/api/timers/${timerId}/`, updates, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + return response.data; + } catch (error) { + console.error("Failed to update timer:", error); + throw error; + } + } + + // Methods for fetching recent activity data + async getRecentFeedings(childId: number, limit: number = 20): Promise { + const response = await this.request<{ results: FeedingEntry[] }>(`feedings/?child=${childId}&limit=${limit}`); + return response.results; + } + + async getRecentSleep(childId: number, limit: number = 20): Promise { + const response = await this.request<{ results: SleepEntry[] }>(`sleep/?child=${childId}&limit=${limit}`); + return response.results; + } + + async getRecentDiapers(childId: number, limit: number = 20): Promise { + const response = await this.request<{ results: DiaperEntry[] }>(`changes/?child=${childId}&limit=${limit}`); + return response.results; + } + + async getRecentTummyTime(childId: number, limit: number = 20): Promise { + const response = await this.request<{ results: TummyTimeEntry[] }>(`tummy-times/?child=${childId}&limit=${limit}`); + return response.results; + } + + async getLastTummyTime(childId: number): Promise { + const response = await this.request<{ results: TummyTimeEntry[] }>( + `tummy-times/?child=${childId}&limit=1&ordering=-end`, + ); + return response.results.length > 0 ? response.results[0] : null; + } + + // Methods for fetching activity data since a specific date + async getFeedings(childId: number, startDate: string, limit: number = 100): Promise { + const response = await this.request<{ results: FeedingEntry[] }>( + `feedings/?child=${childId}&start__gte=${startDate}&limit=${limit}&ordering=-start`, + ); + return response.results; + } + + async getSleep(childId: number, startDate: string, limit: number = 100): Promise { + const response = await this.request<{ results: SleepEntry[] }>( + `sleep/?child=${childId}&start__gte=${startDate}&limit=${limit}`, + ); + return response.results; + } + + async getDiapers(childId: number, startDate: string, limit: number = 100): Promise { + const response = await this.request<{ results: DiaperEntry[] }>( + `changes/?child=${childId}&time__gte=${startDate}&limit=${limit}`, + ); + return response.results; + } + + async getTummyTime(childId: number, startDate: string, limit: number = 100): Promise { + const response = await this.request<{ results: TummyTimeEntry[] }>( + `tummy-times/?child=${childId}&start__gte=${startDate}&limit=${limit}`, + ); + return response.results; + } + + // Methods for fetching today's data + async getTodayFeedings(childId: number): Promise { + const todayStart = new Date(); + todayStart.setHours(0, 0, 0, 0); + + // Format date as YYYY-MM-DD for the API + const startDate = todayStart.toISOString().split("T")[0]; + + const url = `feedings/?child=${childId}&start__date=${startDate}&limit=100&ordering=-start`; + const response = await this.request<{ results: FeedingEntry[] }>(url); + return response.results; + } + + async getTodaySleep(childId: number): Promise { + const todayStart = new Date(); + todayStart.setHours(0, 0, 0, 0); + + // Format date as YYYY-MM-DD for the API + const startDate = todayStart.toISOString().split("T")[0]; + + const response = await this.request<{ results: SleepEntry[] }>( + `sleep/?child=${childId}&end__date=${startDate}&limit=100&ordering=-end`, + ); + return response.results; + } + + async getTodayDiapers(childId: number): Promise { + const todayStart = new Date(); + todayStart.setHours(0, 0, 0, 0); + + // Format date as YYYY-MM-DD for the API + const startDate = todayStart.toISOString().split("T")[0]; + + const response = await this.request<{ results: DiaperEntry[] }>( + `changes/?child=${childId}&time__date=${startDate}&limit=100&ordering=-time`, + ); + return response.results; + } + + async getTodayTummyTime(childId: number): Promise { + const todayStart = new Date(); + todayStart.setHours(0, 0, 0, 0); + + // Format date as YYYY-MM-DD for the API + const startDate = todayStart.toISOString().split("T")[0]; + + const response = await this.request<{ results: TummyTimeEntry[] }>( + `tummy-times/?child=${childId}&end__date=${startDate}&limit=100&ordering=-end`, + ); + return response.results; + } + + // Methods for managing feedings + async updateFeeding(feedingId: number, data: Partial): Promise { + try { + const response = await axios.patch(`${this.baseUrl}/api/feedings/${feedingId}/`, data, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + return response.data; + } catch (error) { + console.error("Failed to update feeding:", error); + throw error; + } + } + + async deleteFeeding(feedingId: number): Promise { + try { + await axios.delete(`${this.baseUrl}/api/feedings/${feedingId}/`, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + } catch (error) { + console.error("Failed to delete feeding:", error); + throw error; + } + } + + // Diaper methods + async createDiaper(data: Partial): Promise { + try { + const response = await axios.post(`${this.baseUrl}/api/changes/`, data, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + return response.data; + } catch (error) { + console.error("Failed to create diaper change:", error); + throw error; + } + } + + async updateDiaper(diaperId: number, data: Partial): Promise { + try { + const response = await axios.patch(`${this.baseUrl}/api/changes/${diaperId}/`, data, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + return response.data; + } catch (error) { + console.error("Failed to update diaper change:", error); + throw error; + } + } + + async deleteDiaper(diaperId: number): Promise { + try { + await axios.delete(`${this.baseUrl}/api/changes/${diaperId}/`, { + headers: { + Authorization: `Token ${this.apiKey}`, + "Content-Type": "application/json", + }, + }); + } catch (error) { + console.error("Failed to delete diaper change:", error); + throw error; + } + } +} diff --git a/extensions/baby-buddy/src/components/ChildDetailView.tsx b/extensions/baby-buddy/src/components/ChildDetailView.tsx new file mode 100644 index 00000000000..9d8b8427831 --- /dev/null +++ b/extensions/baby-buddy/src/components/ChildDetailView.tsx @@ -0,0 +1,237 @@ +import { ActionPanel, Action, Detail, Icon, useNavigation } from "@raycast/api"; +import { useState, useEffect } from "react"; +import { BabyBuddyAPI } from "../api"; +import { Child, FeedingEntry, SleepEntry, DiaperEntry, TummyTimeEntry } from "../api"; +import { + formatPreciseTimeAgo, // Import the new function + formatMinutesToFullDuration, + formatDiaperDescription, + formatErrorMessage, +} from "../utils"; +import { + calculateTotalFeedingAmount, + calculateTotalSleepMinutes, + calculateTotalTummyTimeMinutes, + countWetDiapers, + countSolidDiapers, + calculateTotalDiaperAmount, + calculateAge, +} from "../utils/statistics"; +import { getTodayDateRange } from "../utils/date-helpers"; +import FeedingList from "./FeedingList"; +import SleepList from "./SleepList"; +import DiaperList from "./DiaperList"; +import TummyTimeList from "./TummyTimeList"; +import { showFailureToast } from "@raycast/utils"; + +interface ChildDetailViewProps { + child: Child; +} + +interface ChildStats { + lastFeeding: FeedingEntry | null; + lastSleep: SleepEntry | null; + lastDiaper: DiaperEntry | null; + lastTummyTime: TummyTimeEntry | null; + todayFeedings: FeedingEntry[]; + todaySleep: SleepEntry[]; + todayDiapers: DiaperEntry[]; + todayTummyTime: TummyTimeEntry[]; +} + +export default function ChildDetailView({ child }: ChildDetailViewProps) { + const [isLoading, setIsLoading] = useState(true); + const [stats, setStats] = useState({ + lastFeeding: null, + lastSleep: null, + lastDiaper: null, + lastTummyTime: null, + todayFeedings: [], + todaySleep: [], + todayDiapers: [], + todayTummyTime: [], + }); + + const navigation = useNavigation(); + + useEffect(() => { + async function fetchChildStats() { + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + + // Fetch the latest entries for each activity + const lastFeeding = await api.getLastFeeding(child.id); + const lastSleep = await api.getLastSleep(child.id); + const lastDiaper = await api.getLastDiaper(child.id); + const lastTummyTime = await api.getLastTummyTime(child.id); + + // Fetch today's entries for each activity + const allFeedings = await api.getTodayFeedings(child.id); + const allSleep = await api.getTodaySleep(child.id); + const allDiapers = await api.getTodayDiapers(child.id); + const allTummyTime = await api.getTodayTummyTime(child.id); + + // Filter activities to only include today's data + const { start: todayStart, end: todayEnd } = getTodayDateRange(); + + const todayFeedings = allFeedings.filter((feeding) => new Date(feeding.start) >= todayStart); + const todaySleep = allSleep.filter((sleep) => new Date(sleep.end) >= todayStart); + const todayDiapers = allDiapers.filter((diaper) => new Date(diaper.time) >= todayStart); + const todayTummyTime = allTummyTime.filter((tummyTime) => { + const tummyTimeEndDate = new Date(tummyTime.end); + return tummyTimeEndDate >= todayStart && tummyTimeEndDate < todayEnd; + }); + + setStats({ + lastFeeding, + lastSleep, + lastDiaper, + lastTummyTime, + todayFeedings, + todaySleep, + todayDiapers, + todayTummyTime, + }); + + setIsLoading(false); + } catch (error) { + showFailureToast("Failed to fetch child stats", { message: formatErrorMessage(error) }); + setIsLoading(false); + } + } + + fetchChildStats(); + }, [child.id]); + + // Calculate total feeding amount for today + const totalFeedingAmount = calculateTotalFeedingAmount(stats.todayFeedings); + + // Calculate total sleep duration for today (in minutes) + const totalSleepMinutes = calculateTotalSleepMinutes(stats.todaySleep); + + // Calculate total tummy time duration for today (in minutes) + const totalTummyTimeMinutes = calculateTotalTummyTimeMinutes(stats.todayTummyTime); + + // Count wet and solid diapers + const wetDiapers = countWetDiapers(stats.todayDiapers); + const solidDiapers = countSolidDiapers(stats.todayDiapers); + + // Calculate total diaper amount + const totalDiaperAmount = calculateTotalDiaperAmount(stats.todayDiapers); + + const markdown = ` +# ${child.first_name} ${child.last_name} + +## Feeding +${ + stats.lastFeeding + ? `Last feeding: **${formatPreciseTimeAgo(stats.lastFeeding.start)}** (${stats.lastFeeding.type}, ${stats.lastFeeding.method}${stats.lastFeeding.amount ? `, ${stats.lastFeeding.amount}` : ""})` + : "No recent feedings recorded" +} + +Total today: **${totalFeedingAmount.toFixed(1)}** + +## Sleep +${ + stats.lastSleep + ? `Last sleep: **${formatPreciseTimeAgo(stats.lastSleep.end)}** (${stats.lastSleep.duration})` + : "No recent sleep recorded" +} + +Total today: **${formatMinutesToFullDuration(totalSleepMinutes)}** + +## Diaper Changes +${ + stats.lastDiaper + ? `Last change: **${formatPreciseTimeAgo(stats.lastDiaper.time)}** (${formatDiaperDescription(stats.lastDiaper)})` + : "No recent diaper changes recorded" +} + +Total today: **${stats.todayDiapers.length}** changes (${wetDiapers} wet, ${solidDiapers} solid${totalDiaperAmount > 0 ? `, ${totalDiaperAmount.toFixed(1)} amount` : ""}) + +## Tummy Time +${ + stats.lastTummyTime + ? `Last tummy time: **${formatPreciseTimeAgo(stats.lastTummyTime.end)}** (${stats.lastTummyTime.duration})` + : "No recent tummy time recorded" +} + +Total today: **${formatMinutesToFullDuration(totalTummyTimeMinutes)}** +`; + + return ( + + } + shortcut={{ modifiers: ["cmd"], key: "1" }} + /> + } + shortcut={{ modifiers: ["cmd"], key: "2" }} + /> + } + shortcut={{ modifiers: ["cmd"], key: "3" }} + /> + } + shortcut={{ modifiers: ["cmd"], key: "4" }} + /> + + } + metadata={ + + + + + + 0 ? `, ${totalDiaperAmount.toFixed(1)} amount` : ""})`} + /> + + + + navigation.push()} + /> + navigation.push()} + /> + navigation.push()} + /> + navigation.push()} + /> + + + } + navigationTitle={`${child.first_name} Details`} + /> + ); +} diff --git a/extensions/baby-buddy/src/components/CreateDiaperForm.tsx b/extensions/baby-buddy/src/components/CreateDiaperForm.tsx new file mode 100644 index 00000000000..479defce0ce --- /dev/null +++ b/extensions/baby-buddy/src/components/CreateDiaperForm.tsx @@ -0,0 +1,123 @@ +import { Form, ActionPanel, Action, showToast, Toast, useNavigation } from "@raycast/api"; +import { useState } from "react"; +import { BabyBuddyAPI, Timer } from "../api"; +import { DIAPER_COLORS } from "../utils/constants"; +import { formatDiaperData } from "../utils/form-helpers"; +import { formatErrorMessage } from "../utils/formatters"; +import { validateDiaperForm } from "../utils/validators"; + +interface CreateDiaperFormProps { + timer: Timer; + childName: string; + onEventCreated: () => void; +} + +export default function CreateDiaperForm({ timer, childName, onEventCreated }: CreateDiaperFormProps) { + const [isLoading, setIsLoading] = useState(false); + const [time, setTime] = useState(timer.end ? new Date(timer.end) : new Date()); + const [isWet, setIsWet] = useState(false); + const [isSolid, setIsSolid] = useState(false); + const [color, setColor] = useState(""); + const [amount, setAmount] = useState(""); + const [notes, setNotes] = useState(""); + const navigation = useNavigation(); + + async function handleSubmit() { + // Validate the form + if (!validateDiaperForm(isWet, isSolid)) { + return; + } + + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + + // Format and prepare the data + const diaperData = formatDiaperData({ + childId: timer.child, + time, + isWet, + isSolid, + color, + amount, + notes, + }); + + // Create the diaper entry + await api.createDiaper(diaperData); + + // Only delete the timer if it's a real timer (id > 0) + if (timer.id > 0) { + await api.deleteTimer(timer.id); + } + + await showToast({ + style: Toast.Style.Success, + title: "Diaper Change Created", + message: `Diaper change created for ${childName}`, + }); + + // Call the callback to refresh and navigate + onEventCreated(); + } catch (error: unknown) { + console.error("Failed to create diaper change:", error); + + await showToast({ + style: Toast.Style.Failure, + title: "Failed to Create Diaper Change", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + + return ( +
+ + navigation.pop()} /> + + } + > + + + + + newValue && setTime(newValue)} /> + + + + + + + + {isSolid && ( + + + {DIAPER_COLORS.map((colorOption) => ( + + ))} + + )} + + + + + + ); +} diff --git a/extensions/baby-buddy/src/components/CreateFeedingForm.tsx b/extensions/baby-buddy/src/components/CreateFeedingForm.tsx new file mode 100644 index 00000000000..0e230055485 --- /dev/null +++ b/extensions/baby-buddy/src/components/CreateFeedingForm.tsx @@ -0,0 +1,138 @@ +import { Form, ActionPanel, Action, showToast, Toast, useNavigation } from "@raycast/api"; +import { useState } from "react"; +import { BabyBuddyAPI, Timer } from "../api"; +import { FEEDING_TYPES, FEEDING_METHODS } from "../utils/constants"; +import { createFeedingData } from "../utils/form-helpers"; +import { formatErrorMessage } from "../utils/formatters"; +import { showInvalidTimeRangeError } from "../utils/validators"; +import { validateTimeRange } from "../utils/date-helpers"; +import { showFailureToast } from "@raycast/utils"; + +interface CreateFeedingFormProps { + timer: Timer; + childName: string; + onEventCreated: () => void; +} + +export default function CreateFeedingForm({ timer, childName, onEventCreated }: CreateFeedingFormProps) { + const [isLoading, setIsLoading] = useState(false); + const [startTime, setStartTime] = useState(() => { + // Ensure start time is before end time + const start = new Date(timer.start); + const end = timer.end ? new Date(timer.end) : new Date(); + + // If start time is not before end time, set it 1 second before + if (start >= end) { + const newStart = new Date(end); + newStart.setSeconds(newStart.getSeconds() - 1); + return newStart; + } + + return start; + }); + const [endTime, setEndTime] = useState(timer.end ? new Date(timer.end) : new Date()); + const navigation = useNavigation(); + + async function handleSubmit(values: { type: string; method: string; amount?: string; notes?: string }) { + // Validate that end time is after start time + if (!validateTimeRange(startTime, endTime)) { + showInvalidTimeRangeError(); + return; + } + + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + + // Format and prepare the data using utility function + const feedingData = createFeedingData({ + childId: timer.child, + startTime: startTime.toISOString(), + endTime: endTime.toISOString(), + type: values.type, + method: values.method, + amount: values.amount, + notes: values.notes || "", + }); + + // Create the feeding entry + await api.createFeeding(feedingData); + + // Only delete the timer if it's a real timer (id > 0) + if (timer.id > 0) { + await api.deleteTimer(timer.id); + } + + await showToast({ + style: Toast.Style.Success, + title: "Feeding Created", + message: `Feeding created for ${childName}`, + }); + + // Call the callback to refresh and navigate + onEventCreated(); + } catch (error: unknown) { + console.error("Failed to create feeding:", error); + + await showFailureToast({ + title: "Failed to Create Feeding", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + + // Validate that end time is after start time + const isTimeRangeValid = validateTimeRange(startTime, endTime); + + return ( +
+ + navigation.pop()} /> + + } + > + + + + + newValue && setStartTime(newValue)} + /> + + newValue && setEndTime(newValue)} + /> + + {!isTimeRangeValid && } + + + + + {FEEDING_TYPES.map((type) => ( + + ))} + + + + {FEEDING_METHODS.map((method) => ( + + ))} + + + + + + + ); +} diff --git a/extensions/baby-buddy/src/components/CreatePumpingForm.tsx b/extensions/baby-buddy/src/components/CreatePumpingForm.tsx new file mode 100644 index 00000000000..f25c51a885d --- /dev/null +++ b/extensions/baby-buddy/src/components/CreatePumpingForm.tsx @@ -0,0 +1,129 @@ +import { Form, ActionPanel, Action, showToast, Toast, useNavigation } from "@raycast/api"; +import { useState } from "react"; +import { BabyBuddyAPI, Timer } from "../api"; +import axios from "axios"; +import { showFailureToast } from "@raycast/utils"; + +interface CreatePumpingFormProps { + timer: Timer; + childName: string; + onEventCreated: () => void; +} + +export default function CreatePumpingForm({ timer, childName, onEventCreated }: CreatePumpingFormProps) { + const [isLoading, setIsLoading] = useState(false); + const [startTime, setStartTime] = useState(new Date(timer.start)); + const [endTime, setEndTime] = useState(timer.end ? new Date(timer.end) : new Date()); + const navigation = useNavigation(); + + async function handleSubmit(values: { amount?: string; notes?: string }) { + if (!isTimeRangeValid) { + await showFailureToast({ + title: "Invalid Time Range", + message: "End time must be after start time", + }); + return; + } + + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + + // Format dates properly + const startISOString = startTime.toISOString(); + const endISOString = endTime.toISOString(); + + // Create the pumping entry + await api.createPumping({ + child: timer.child, + start: startISOString, + end: endISOString, + amount: values.amount ? parseFloat(values.amount) : null, + notes: values.notes || "", + }); + + // Delete the timer + await api.deleteTimer(timer.id); + + await showToast({ + style: Toast.Style.Success, + title: "Pumping Created", + message: `Pumping entry created for ${childName}`, + }); + + // Call the callback to refresh the timer list + onEventCreated(); + + // Navigate back to the main list (pop all the way back) + navigation.pop(); + navigation.pop(); // Pop twice to get back to the main timer list + } catch (error: unknown) { + console.error("Failed to create pumping:", error); + setIsLoading(false); + + let errorMessage = "Please try again"; + + // Check if it's an Axios error with response data + if (axios.isAxiosError(error) && error.response?.data) { + const errorData = error.response.data; + if (typeof errorData === "object") { + // Join all error messages + const messages = Object.entries(errorData) + .map(([key, value]) => `${key}: ${value}`) + .join(", "); + if (messages) { + errorMessage = messages; + } + } else if (typeof errorData === "string") { + errorMessage = errorData; + } + } + + await showFailureToast({ + title: "Failed to Create Pumping", + message: errorMessage, + }); + } + } + + // Validate that end time is after start time + const isTimeRangeValid = endTime > startTime; + + return ( +
+ + navigation.pop()} /> + + } + > + + + + + newValue && setStartTime(newValue)} + /> + + newValue && setEndTime(newValue)} + /> + + {!isTimeRangeValid && } + + + + + + + + ); +} diff --git a/extensions/baby-buddy/src/components/CreateSleepForm.tsx b/extensions/baby-buddy/src/components/CreateSleepForm.tsx new file mode 100644 index 00000000000..c842c846b25 --- /dev/null +++ b/extensions/baby-buddy/src/components/CreateSleepForm.tsx @@ -0,0 +1,128 @@ +import { Form, ActionPanel, Action, showToast, Toast, useNavigation } from "@raycast/api"; +import { useState } from "react"; +import { BabyBuddyAPI, Timer } from "../api"; +import { createSleepData } from "../utils/form-helpers"; +import { formatErrorMessage } from "../utils/formatters"; +import { showInvalidTimeRangeError } from "../utils/validators"; +import { validateTimeRange } from "../utils/date-helpers"; +import { showFailureToast } from "@raycast/utils"; + +interface CreateSleepFormProps { + timer: Timer; + childName: string; + onEventCreated: () => void; +} + +export default function CreateSleepForm({ timer, childName, onEventCreated }: CreateSleepFormProps) { + const [isLoading, setIsLoading] = useState(false); + const [startTime, setStartTime] = useState(() => { + // Ensure start time is before end time + const start = new Date(timer.start); + const end = timer.end ? new Date(timer.end) : new Date(); + + // If start time is not before end time, set it 1 second before + if (start >= end) { + const newStart = new Date(end); + newStart.setSeconds(newStart.getSeconds() - 1); + return newStart; + } + + return start; + }); + const [endTime, setEndTime] = useState(timer.end ? new Date(timer.end) : new Date()); + const [isNap, setIsNap] = useState(false); + const [notes, setNotes] = useState(""); + const navigation = useNavigation(); + + // Validate that end time is after start time + const isTimeRangeValid = validateTimeRange(startTime, endTime); + + async function handleSubmit() { + if (!isTimeRangeValid) { + showInvalidTimeRangeError(); + return; + } + + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + + // Format and prepare the data using utility function + const sleepData = createSleepData({ + childId: timer.child, + startTime: startTime.toISOString(), + endTime: endTime.toISOString(), + isNap, + notes, + }); + + // Create the sleep entry + await api.createSleep(sleepData); + + // Only delete the timer if it's a real timer (id > 0) + if (timer.id > 0) { + await api.deleteTimer(timer.id); + } + + await showToast({ + style: Toast.Style.Success, + title: "Sleep Entry Created", + message: `Sleep entry created for ${childName}`, + }); + + // Call the callback to refresh and navigate + onEventCreated(); + } catch (error: unknown) { + await showFailureToast({ + title: "Failed to Create Sleep Entry", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + + return ( +
+ + navigation.pop()} /> + + } + > + + + + + newValue && setStartTime(newValue)} + /> + + newValue && setEndTime(newValue)} + /> + + {!isTimeRangeValid && } + + + + + + + + ); +} diff --git a/extensions/baby-buddy/src/components/CreateTimerForm.tsx b/extensions/baby-buddy/src/components/CreateTimerForm.tsx new file mode 100644 index 00000000000..8ca7ad24649 --- /dev/null +++ b/extensions/baby-buddy/src/components/CreateTimerForm.tsx @@ -0,0 +1,119 @@ +import { Form, ActionPanel, Action, showToast, Toast, useNavigation } from "@raycast/api"; +import { useState, useEffect } from "react"; +import { BabyBuddyAPI, Child } from "../api"; +import { TIMER_TYPES } from "../utils/constants"; +import { formatErrorMessage, getTimerName } from "../utils/form-helpers"; + +interface CreateTimerFormProps { + onTimerCreated: () => void; +} + +export default function CreateTimerForm({ onTimerCreated }: CreateTimerFormProps) { + const [isLoading, setIsLoading] = useState(true); + const [children, setChildren] = useState([]); + const [customName, setCustomName] = useState(""); + const [selectedTimerType, setSelectedTimerType] = useState("feeding"); + const [startDateTime, setStartDateTime] = useState(new Date()); + const { pop } = useNavigation(); + + useEffect(() => { + async function fetchChildren() { + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + const childrenData = await api.getChildren(); + setChildren(childrenData); + } catch (error) { + showToast({ + style: Toast.Style.Failure, + title: "Failed to fetch children", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + + fetchChildren(); + }, []); + + async function handleSubmit(values: { childId: string; timerType: string; customName: string; startDateTime: Date }) { + try { + const api = new BabyBuddyAPI(); + const childId = parseInt(values.childId); + + // Determine the timer name using the utility function + const timerName = getTimerName(values.timerType, values.customName); + + // Use the datetime directly + const startISOString = values.startDateTime.toISOString(); + + await api.createTimer(childId, timerName, startISOString); + + await showToast({ + style: Toast.Style.Success, + title: "Timer Created", + message: `${timerName} timer created`, + }); + + onTimerCreated(); + pop(); + } catch (error) { + console.error("Failed to create timer:", error); + await showToast({ + style: Toast.Style.Failure, + title: "Failed to Create Timer", + message: formatErrorMessage(error), + }); + } + } + + return ( +
+ + + } + > + + {children.map((child) => ( + + ))} + + + setSelectedTimerType(newValue)} + > + {TIMER_TYPES.map((type) => ( + + ))} + + + {selectedTimerType === "other" && ( + + )} + + newValue && setStartDateTime(newValue)} + /> + + ); +} diff --git a/extensions/baby-buddy/src/components/CreateTummyTimeForm.tsx b/extensions/baby-buddy/src/components/CreateTummyTimeForm.tsx new file mode 100644 index 00000000000..c0e7ecf495d --- /dev/null +++ b/extensions/baby-buddy/src/components/CreateTummyTimeForm.tsx @@ -0,0 +1,147 @@ +import { Form, ActionPanel, Action, showToast, Toast, useNavigation } from "@raycast/api"; +import { useState } from "react"; +import { BabyBuddyAPI, Timer } from "../api"; +import axios from "axios"; +import { showFailureToast } from "@raycast/utils"; + +interface CreateTummyTimeFormProps { + timer: Timer; + childName: string; + onEventCreated: () => void; +} + +export default function CreateTummyTimeForm({ timer, childName, onEventCreated }: CreateTummyTimeFormProps) { + const [isLoading, setIsLoading] = useState(false); + const [startTime, setStartTime] = useState(() => { + // Ensure start time is before end time + const start = new Date(timer.start); + const end = timer.end ? new Date(timer.end) : new Date(); + + // If start time is not before end time, set it 1 second before + if (start >= end) { + const newStart = new Date(end); + newStart.setSeconds(newStart.getSeconds() - 1); + return newStart; + } + + return start; + }); + const [endTime, setEndTime] = useState(timer.end ? new Date(timer.end) : new Date()); + const [milestone, setMilestone] = useState(""); + const navigation = useNavigation(); + + // Validate that end time is after start time + const isTimeRangeValid = endTime > startTime; + + async function handleSubmit() { + if (!isTimeRangeValid) { + showFailureToast({ + title: "Invalid time range", + message: "End time must be after start time", + }); + return; + } + + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + + // Format dates properly + const startISOString = startTime.toISOString(); + const endISOString = endTime.toISOString(); + + // Prepare the data + const tummyTimeData = { + child: timer.child, + start: startISOString, + end: endISOString, + milestone: milestone || "", + }; + + // Create the tummy time entry + await api.createTummyTime(tummyTimeData); + + // Only delete the timer if it's a real timer (id > 0) + if (timer.id > 0) { + await api.deleteTimer(timer.id); + } + + await showToast({ + style: Toast.Style.Success, + title: "Tummy Time Created", + message: `Tummy time created for ${childName}`, + }); + + // Call the callback to refresh and navigate + onEventCreated(); + } catch (error: unknown) { + let errorMessage = "Please try again"; + + // Check if it's an Axios error with response data + if (axios.isAxiosError(error) && error.response?.data) { + const errorData = error.response.data; + if (typeof errorData === "object") { + // Join all error messages + const messages = Object.entries(errorData) + .map(([key, value]) => `${key}: ${value}`) + .join(", "); + if (messages) { + errorMessage = messages; + } + } else if (typeof errorData === "string") { + errorMessage = errorData; + } + } + + await showToast({ + style: Toast.Style.Failure, + title: "Failed to Create Tummy Time", + message: errorMessage, + }); + } finally { + setIsLoading(false); + } + } + + return ( +
+ + navigation.pop()} /> + + } + > + + + + + newValue && setStartTime(newValue)} + /> + + newValue && setEndTime(newValue)} + /> + + {!isTimeRangeValid && } + + + + + + ); +} diff --git a/extensions/baby-buddy/src/components/DiaperList.tsx b/extensions/baby-buddy/src/components/DiaperList.tsx new file mode 100644 index 00000000000..bf54475e4a4 --- /dev/null +++ b/extensions/baby-buddy/src/components/DiaperList.tsx @@ -0,0 +1,354 @@ +import { + List, + ActionPanel, + Action, + Icon, + useNavigation, + showToast, + Toast, + confirmAlert, + Form, + Alert, +} from "@raycast/api"; +import { useState, useEffect } from "react"; +import { BabyBuddyAPI, Child, DiaperEntry } from "../api"; +import { formatTimeAgo, formatTimeWithTooltip } from "../utils"; +import { formatErrorMessage } from "../utils/form-helpers"; +import CreateDiaperForm from "./CreateDiaperForm"; + +// Define the available diaper colors +const DIAPER_COLORS = [ + { id: "black", name: "Black" }, + { id: "brown", name: "Brown" }, + { id: "green", name: "Green" }, + { id: "yellow", name: "Yellow" }, + { id: "white", name: "White" }, +]; + +interface DiaperListProps { + child: Child; +} + +export default function DiaperList({ child }: DiaperListProps) { + const [isLoading, setIsLoading] = useState(true); + const [diaperEntries, setDiaperEntries] = useState([]); + const navigation = useNavigation(); + + async function fetchDiaperEntries() { + setIsLoading(true); + try { + const api = new BabyBuddyAPI(); + const entries = await api.getRecentDiapers(child.id); + // Sort by time, newest first + entries.sort((a: DiaperEntry, b: DiaperEntry) => new Date(b.time).getTime() - new Date(a.time).getTime()); + setDiaperEntries(entries); + } catch (error) { + showToast({ + style: Toast.Style.Failure, + title: "Failed to Load Diaper Changes", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + + async function createDiaperAndNavigateBack() { + await fetchDiaperEntries(); + navigation.pop(); + } + + useEffect(() => { + fetchDiaperEntries(); + }, [child.id]); + + async function handleDeleteDiaper(diaper: DiaperEntry) { + if ( + await confirmAlert({ + title: "Delete Diaper Change", + message: `Are you sure you want to delete this diaper change from ${formatTimeAgo(diaper.time)}?`, + primaryAction: { + title: "Delete", + style: Alert.ActionStyle.Destructive, + }, + }) + ) { + try { + const api = new BabyBuddyAPI(); + await api.deleteDiaper(diaper.id); + showToast({ + style: Toast.Style.Success, + title: "Diaper Change Deleted", + message: "The diaper change has been deleted", + }); + fetchDiaperEntries(); + } catch (error) { + showToast({ + style: Toast.Style.Failure, + title: "Failed to Delete Diaper Change", + message: formatErrorMessage(error), + }); + } + } + } + + function getIconForDiaperType(diaper: DiaperEntry) { + if (diaper.wet && diaper.solid) { + return Icon.Stars; + } else if (diaper.wet) { + return Icon.Droplets; + } else if (diaper.solid) { + return Icon.Circle; + } + return Icon.Circle; + } + + function getDiaperTypeText(diaper: DiaperEntry) { + const types = []; + if (diaper.wet) types.push("Wet"); + if (diaper.solid) types.push("Solid"); + return types.join(" & "); + } + + function getColorText(diaper: DiaperEntry) { + if (diaper.solid && diaper.color) { + return diaper.color.charAt(0).toUpperCase() + diaper.color.slice(1); + } + return ""; + } + + function getAmountText(diaper: DiaperEntry) { + if (diaper.amount !== null) { + return `${diaper.amount}`; + } + return ""; + } + + function handleCreateDiaper() { + // Create a dummy timer with the child's ID + const dummyTimer = { + id: 0, + name: "New Diaper Change", + start: new Date().toISOString(), + end: new Date().toISOString(), + active: false, + user: 0, + child: child.id, + duration: "0:00:00", + }; + + navigation.push( + , + ); + } + + return ( + + + + + } + > + + {diaperEntries.map((diaper) => ( + handleDeleteDiaper(diaper)} + onCreateDiaper={handleCreateDiaper} + onRefresh={fetchDiaperEntries} + getDiaperTypeText={getDiaperTypeText} + getColorText={getColorText} + getAmountText={getAmountText} + getIconForDiaperType={getIconForDiaperType} + /> + ))} + + + ); +} + +interface DiaperListItemProps { + diaper: DiaperEntry; + onDelete: () => void; + onCreateDiaper: () => void; + onRefresh: () => void; + getDiaperTypeText: (diaper: DiaperEntry) => string; + getColorText: (diaper: DiaperEntry) => string; + getAmountText: (diaper: DiaperEntry) => string; + getIconForDiaperType: (diaper: DiaperEntry) => Icon; +} + +function DiaperListItem({ + diaper, + onDelete, + onCreateDiaper, + onRefresh, + getDiaperTypeText, + getColorText, + getAmountText, + getIconForDiaperType, +}: DiaperListItemProps) { + const navigation = useNavigation(); + const timeInfo = formatTimeWithTooltip(diaper.time); + + return ( + + { + navigation.push( + { + showToast({ + style: Toast.Style.Success, + title: "Diaper Change Updated", + message: "The diaper change has been updated", + }); + navigation.pop(); + // Refresh the diaper list after editing + onRefresh(); + }} + />, + ); + }} + /> + + + + } + /> + ); +} + +interface EditDiaperFormProps { + diaper: DiaperEntry; + onDiaperUpdated: () => void; +} + +function EditDiaperForm({ diaper, onDiaperUpdated }: EditDiaperFormProps) { + const [isLoading, setIsLoading] = useState(false); + const [time, setTime] = useState(new Date(diaper.time)); + const [isWet, setIsWet] = useState(diaper.wet); + const [isSolid, setIsSolid] = useState(diaper.solid); + const [color, setColor] = useState(diaper.color || ""); + const [amount, setAmount] = useState(diaper.amount !== null ? diaper.amount.toString() : ""); + const [notes, setNotes] = useState(diaper.notes || ""); + const navigation = useNavigation(); + + async function handleSubmit() { + // Validate that at least one of wet or solid is selected + if (!isWet && !isSolid) { + showToast({ + style: Toast.Style.Failure, + title: "Invalid diaper change", + message: "At least one of Wet or Solid must be selected", + }); + return; + } + + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + + // Format date properly + const timeISOString = time.toISOString(); + + // Parse amount to number or null + const amountValue = amount ? parseFloat(amount) : null; + + // Prepare the data + const diaperData = { + time: timeISOString, + wet: isWet, + solid: isSolid, + color: isSolid ? color : "", + amount: amountValue, + notes: notes || "", + }; + + // Update the diaper entry + await api.updateDiaper(diaper.id, diaperData); + + onDiaperUpdated(); + } catch (error) { + showToast({ + style: Toast.Style.Failure, + title: "Failed to Update Diaper Change", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + + return ( +
+ + navigation.pop()} /> + + } + > + newValue && setTime(newValue)} + /> + + + + + + + + {isSolid && ( + + + {DIAPER_COLORS.map((colorOption) => ( + + ))} + + )} + + + + + + ); +} diff --git a/extensions/baby-buddy/src/components/EditTimerForm.tsx b/extensions/baby-buddy/src/components/EditTimerForm.tsx new file mode 100644 index 00000000000..db576d4c035 --- /dev/null +++ b/extensions/baby-buddy/src/components/EditTimerForm.tsx @@ -0,0 +1,78 @@ +import { Form, ActionPanel, Action, useNavigation } from "@raycast/api"; +import { useState } from "react"; +import { BabyBuddyAPI, Timer } from "../api"; +import { formatErrorMessage, prepareTimerUpdateData } from "../utils/form-helpers"; +import { showFailureToast } from "@raycast/utils"; + +interface EditTimerFormProps { + timer: Timer; + childName: string; + onTimerUpdated: () => void; +} + +export default function EditTimerForm({ timer, childName, onTimerUpdated }: EditTimerFormProps) { + const [isLoading, setIsLoading] = useState(false); + const [timerName, setTimerName] = useState(timer.name); + const [startDateTime, setStartDateTime] = useState(new Date(timer.start)); + const { pop } = useNavigation(); + + async function handleSubmit() { + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + + // Format data using the utility function + const updateData = prepareTimerUpdateData({ + timerName, + startTime: startDateTime.toISOString(), + }); + + // Update the timer + await api.updateTimer(timer.id, updateData); + + await showFailureToast({ + title: "Timer Updated", + message: `${timerName} timer updated`, + }); + + onTimerUpdated(); + pop(); + } catch (error) { + await showFailureToast({ + title: "Failed to Update Timer", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + + return ( +
+ + pop()} /> + + } + > + + + + + newValue && setStartDateTime(newValue)} + /> + + ); +} diff --git a/extensions/baby-buddy/src/components/FeedingList.tsx b/extensions/baby-buddy/src/components/FeedingList.tsx new file mode 100644 index 00000000000..82aa2c1ef8c --- /dev/null +++ b/extensions/baby-buddy/src/components/FeedingList.tsx @@ -0,0 +1,317 @@ +import { + List, + ActionPanel, + Action, + Icon, + showToast, + Toast, + confirmAlert, + Form, + Alert, + useNavigation, +} from "@raycast/api"; +import { useState, useEffect } from "react"; +import { BabyBuddyAPI, Child, FeedingEntry } from "../api"; +import { formatDuration, formatTimeAgo, formatTimeWithTooltip } from "../utils"; +import { formatErrorMessage } from "../utils/form-helpers"; +import CreateFeedingForm from "./CreateFeedingForm"; +import { showFailureToast } from "@raycast/utils"; + +interface FeedingListProps { + child: Child; +} + +export default function FeedingList({ child }: FeedingListProps) { + const [isLoading, setIsLoading] = useState(true); + const [feedings, setFeedings] = useState([]); + const navigation = useNavigation(); + + async function fetchFeedings() { + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + // Get all feedings for this child, sorted by newest first + const feedingsData = await api.getFeedings(child.id, "", 100); + setFeedings(feedingsData); + } catch (error) { + showFailureToast({ + title: "Failed to fetch feedings", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + + async function updateFeedingsAndNavigateBack() { + await fetchFeedings(); + navigation.pop(); + } + + useEffect(() => { + fetchFeedings(); + }, [child.id]); + + async function handleDeleteFeeding(feeding: FeedingEntry) { + const shouldDelete = await confirmAlert({ + title: "Delete Feeding", + message: `Are you sure you want to delete this feeding from ${formatTimeAgo(feeding.start)}?`, + primaryAction: { + title: "Delete", + style: Alert.ActionStyle.Destructive, + }, + }); + if (shouldDelete) { + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + await api.deleteFeeding(feeding.id); + showToast({ + style: Toast.Style.Success, + title: "Feeding deleted", + }); + fetchFeedings(); + } catch (error) { + showToast({ + style: Toast.Style.Failure, + title: "Failed to delete feeding", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + } + + return ( + + + } + /> + + + } + > + {feedings.map((feeding) => ( + handleDeleteFeeding(feeding)} + onFeedingUpdated={updateFeedingsAndNavigateBack} + /> + ))} + + ); +} + +interface FeedingListItemProps { + feeding: FeedingEntry; + childName: string; + onFeedingDeleted: () => void; + onFeedingUpdated: () => void; +} + +function FeedingListItem({ feeding, childName, onFeedingDeleted, onFeedingUpdated }: FeedingListItemProps) { + const timeInfo = formatTimeWithTooltip(feeding.start); + + // Format duration for display + // Get icon based on feeding type + const getIconForFeedingType = (type: string) => { + switch (type.toLowerCase()) { + case "breast milk": + return Icon.Droplets; + case "fortified breast milk": + return Icon.Stars; + case "formula": + return Icon.Mug; + case "solid food": + return Icon.Circle; + default: + return Icon.Mug; + } + }; + + // Get the appropriate icon for the feeding type + const icon = getIconForFeedingType(feeding.type); + + return ( + + + + {feeding.amount && } + + + + {feeding.notes && } + + } + /> + } + actions={ + + } + /> + + + } + shortcut={{ modifiers: ["cmd"], key: "n" }} + /> + + + } + /> + ); +} + +interface EditFeedingFormProps { + feeding: FeedingEntry; + childName: string; + onFeedingUpdated: () => void; +} + +function EditFeedingForm({ feeding, childName, onFeedingUpdated }: EditFeedingFormProps) { + const [isLoading, setIsLoading] = useState(false); + const [type, setType] = useState(feeding.type); + const [method, setMethod] = useState(feeding.method); + const [amount, setAmount] = useState(feeding.amount?.toString() || ""); + const [notes, setNotes] = useState(feeding.notes || ""); + + async function handleSubmit() { + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + let parsedAmount: number | undefined = undefined; + if (amount) { + parsedAmount = parseFloat(amount); + if (isNaN(parsedAmount)) { + showFailureToast({ + title: "Invalid Amount", + message: "Please enter a valid number for the amount.", + }); + setIsLoading(false); + return; + } + } + + await api.updateFeeding(feeding.id, { + type, + method, + amount: parsedAmount, + notes, + }); + + showToast({ + style: Toast.Style.Success, + title: "Feeding updated", + }); + + onFeedingUpdated(); + } catch (error) { + showFailureToast({ + title: "Failed to update feeding", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + + return ( +
+ + + } + > + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/extensions/baby-buddy/src/components/SleepList.tsx b/extensions/baby-buddy/src/components/SleepList.tsx new file mode 100644 index 00000000000..f0135d26a54 --- /dev/null +++ b/extensions/baby-buddy/src/components/SleepList.tsx @@ -0,0 +1,307 @@ +import { + List, + ActionPanel, + Action, + Icon, + useNavigation, + showToast, + Toast, + confirmAlert, + Form, + Alert, +} from "@raycast/api"; +import { useState, useEffect } from "react"; +import { BabyBuddyAPI, Child, SleepEntry } from "../api"; +import { formatDuration, formatTimeAgo, formatTimeWithTooltip } from "../utils"; +import { formatErrorMessage } from "../utils/form-helpers"; +import CreateSleepForm from "./CreateSleepForm"; +import { showFailureToast } from "@raycast/utils"; + +interface SleepListProps { + child: Child; +} + +export default function SleepList({ child }: SleepListProps) { + const [isLoading, setIsLoading] = useState(true); + const [sleepEntries, setSleepEntries] = useState([]); + const navigation = useNavigation(); + + async function fetchSleepEntries() { + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + // Get all sleep entries for this child, sorted by newest first + const sleepData = await api.getSleep(child.id, "", 100); + setSleepEntries(sleepData); + setIsLoading(false); + } catch (error) { + showToast({ + style: Toast.Style.Failure, + title: "Failed to fetch sleep entries", + message: formatErrorMessage(error), + }); + setIsLoading(false); + } + } + + async function createSleepAndNavigateBack() { + await fetchSleepEntries(); + navigation.pop(); + console.log("Sleep entry created and navigation popped"); + } + + useEffect(() => { + fetchSleepEntries(); + }, [child.id]); + + async function handleDeleteSleep(sleep: SleepEntry) { + if ( + await confirmAlert({ + title: "Delete Sleep Entry", + message: `Are you sure you want to delete this sleep entry from ${formatTimeAgo(sleep.end)}?`, + primaryAction: { + title: "Delete", + style: Alert.ActionStyle.Destructive, + }, + }) + ) { + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + await api.deleteSleep(sleep.id); + showToast({ + style: Toast.Style.Success, + title: "Sleep entry deleted", + }); + await fetchSleepEntries(); + } catch (error) { + showFailureToast({ + title: "Failed to delete sleep entry", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + } + + return ( + + + } + /> + + + } + > + {sleepEntries.map((sleep) => ( + handleDeleteSleep(sleep)} + onSleepCreated={createSleepAndNavigateBack} + onSleepUpdated={fetchSleepEntries} + /> + ))} + + ); +} + +interface SleepListItemProps { + sleep: SleepEntry; + childName: string; + onSleepDeleted: () => void; + onSleepCreated: () => void; + onSleepUpdated: () => void; +} + +function SleepListItem({ sleep, childName, onSleepDeleted, onSleepCreated, onSleepUpdated }: SleepListItemProps) { + const timeInfo = formatTimeWithTooltip(sleep.end); + + // Get icon based on nap status + const getSleepIcon = (isNap: boolean) => { + return isNap ? Icon.Sun : Icon.Moon; + }; + + return ( + + + + + + + {sleep.notes && } + + } + /> + } + actions={ + + } + /> + + + } + shortcut={{ modifiers: ["cmd"], key: "n" }} + /> + + + } + /> + ); +} + +interface EditSleepFormProps { + sleep: SleepEntry; + childName: string; + onSleepUpdated: () => void; +} + +function EditSleepForm({ sleep, childName, onSleepUpdated }: EditSleepFormProps) { + const [isLoading, setIsLoading] = useState(false); + const [startTime, setStartTime] = useState(new Date(sleep.start)); + const [endTime, setEndTime] = useState(new Date(sleep.end)); + const [isNap, setIsNap] = useState(sleep.nap); + const [notes, setNotes] = useState(sleep.notes || ""); + const navigation = useNavigation(); + + // Validate that end time is after start time + const isTimeRangeValid = endTime > startTime; + + async function handleSubmit() { + if (!isTimeRangeValid) { + showToast({ + style: Toast.Style.Failure, + title: "Invalid time range", + message: "End time must be after start time", + }); + return; + } + + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + + await api.updateSleep(sleep.id, { + start: startTime.toISOString(), + end: endTime.toISOString(), + nap: isNap, + notes, + }); + + showToast({ + style: Toast.Style.Success, + title: "Sleep entry updated", + }); + + onSleepUpdated(); + navigation.pop(); + } catch (error) { + showFailureToast({ + title: "Failed to update sleep entry", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + + return ( +
+ + + } + > + newValue && setStartTime(newValue)} + /> + + newValue && setEndTime(newValue)} + /> + + {!isTimeRangeValid && } + + + + + + ); +} diff --git a/extensions/baby-buddy/src/components/StopTimerForm.tsx b/extensions/baby-buddy/src/components/StopTimerForm.tsx new file mode 100644 index 00000000000..ab8179ff818 --- /dev/null +++ b/extensions/baby-buddy/src/components/StopTimerForm.tsx @@ -0,0 +1,278 @@ +import { Form, ActionPanel, Action, showToast, Toast, useNavigation, Icon, confirmAlert } from "@raycast/api"; +import { useState, useEffect } from "react"; +import { BabyBuddyAPI, Timer } from "../api"; +import { formatErrorMessage } from "../utils/formatters"; + +interface StopTimerFormProps { + timer: Timer; + childName: string; + onTimerStopped: () => void; + onTimerReset: () => void; + onTimerDeleted: () => void; +} + +// Timer types for stopping a timer +const TIMER_TYPES = [ + { id: "feeding", name: "Feeding", keywords: ["feed", "nursing", "bottle", "breast", "formula", "milk", "eat"] }, + { id: "pumping", name: "Pumping", keywords: ["pump", "express"] }, + { id: "sleep", name: "Sleep", keywords: ["sleep", "nap", "bed", "rest"] }, + { id: "tummy-time", name: "Tummy Time", keywords: ["tummy", "belly", "stomach"] }, +]; + +export default function StopTimerForm({ + timer, + childName, + onTimerStopped, + onTimerReset, + onTimerDeleted, +}: StopTimerFormProps) { + const [isLoading, setIsLoading] = useState(false); + const [selectedTimerType, setSelectedTimerType] = useState(""); + const [stopDateTime, setStopDateTime] = useState(new Date()); + const { push, pop } = useNavigation(); + + // Determine the most likely timer type based on the timer name + useEffect(() => { + const timerNameLower = timer.name.toLowerCase(); + + // Check each timer type for keyword matches + let bestMatch = ""; + let bestMatchScore = 0; + + TIMER_TYPES.forEach((type) => { + const matchScore = type.keywords.reduce((score, keyword) => { + return timerNameLower.includes(keyword) ? score + 1 : score; + }, 0); + + if (matchScore > bestMatchScore) { + bestMatchScore = matchScore; + bestMatch = type.id; + } + }); + + // If we found a match, set it as the selected type + if (bestMatch) { + setSelectedTimerType(bestMatch); + } else { + // Default to the first option if no match + setSelectedTimerType(TIMER_TYPES[0].id); + } + }, [timer.name]); + + async function handleSubmit(values: { timerType: string }) { + try { + setIsLoading(true); + + // Import the list components for navigation after creation + const { default: FeedingList } = await import("./FeedingList"); + const { default: SleepList } = await import("./SleepList"); + const { default: TummyTimeList } = await import("./TummyTimeList"); + + // Get the child object from the API + const api = new BabyBuddyAPI(); + const children = await api.getChildren(); + const child = children.find((c) => c.id === timer.child); + + if (!child) { + showToast({ + style: Toast.Style.Failure, + title: "Child Not Found", + message: "Could not find the child associated with this timer", + }); + setIsLoading(false); + return; + } + + // Create a callback that will navigate to the appropriate list after creation + const onEventCreated = () => { + // First call the original callback to refresh the timer list + onTimerStopped(); + + // Then navigate to the appropriate list + switch (values.timerType) { + case "feeding": + push(); + break; + case "sleep": + push(); + break; + case "tummy-time": + push(); + break; + default: + // No specific navigation for pumping or other types + pop(); + } + }; + + // Navigate to the appropriate form based on the timer type + switch (values.timerType) { + case "feeding": { + // We'll import the component dynamically to avoid circular dependencies + const { default: CreateFeedingForm } = await import("./CreateFeedingForm"); + push( + , + ); + break; + } + case "pumping": { + const { default: CreatePumpingForm } = await import("./CreatePumpingForm"); + push( + , + ); + break; + } + case "sleep": { + const { default: CreateSleepForm } = await import("./CreateSleepForm"); + push( + , + ); + break; + } + case "tummy-time": { + const { default: CreateTummyTimeForm } = await import("./CreateTummyTimeForm"); + push( + , + ); + break; + } + default: { + // If no specific event type is selected, just delete the timer + const api = new BabyBuddyAPI(); + await api.deleteTimer(timer.id); + + await showToast({ + style: Toast.Style.Success, + title: "Timer Deleted", + message: `${timer.name} timer deleted`, + }); + + onTimerDeleted(); + pop(); + } + } + } catch (error) { + setIsLoading(false); + await showToast({ + style: Toast.Style.Failure, + title: "Failed to Process Timer", + message: formatErrorMessage(error), + }); + } + } + + async function handleDelete() { + // Show confirmation dialog before deleting + const options = { + title: "Delete Timer", + message: `Are you sure you want to delete the "${timer.name}" timer for ${childName}?`, + icon: Icon.Trash, + }; + + if (await confirmAlert(options)) { + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + await api.deleteTimer(timer.id); + + await showToast({ + style: Toast.Style.Success, + title: "Timer Deleted", + message: `${timer.name} timer deleted`, + }); + + onTimerDeleted(); + pop(); + } catch (error) { + setIsLoading(false); + await showToast({ + style: Toast.Style.Failure, + title: "Failed to Delete Timer", + message: formatErrorMessage(error), + }); + } + } + } + + async function handleReset() { + // Show confirmation dialog before resetting + const options = { + title: "Reset Timer", + message: `Are you sure you want to reset the "${timer.name}" timer for ${childName}?`, + icon: Icon.ArrowClockwise, + }; + + if (await confirmAlert(options)) { + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + await api.resetTimer(timer.id); + + await showToast({ + style: Toast.Style.Success, + title: "Timer Reset", + message: `${timer.name} timer reset`, + }); + + onTimerReset(); + pop(); + } catch (error) { + setIsLoading(false); + await showToast({ + style: Toast.Style.Failure, + title: "Failed to Reset Timer", + message: formatErrorMessage(error), + }); + } + } + } + + // Validate that the stop time is after the start time + const isTimeRangeValid = stopDateTime > new Date(timer.start); + + return ( +
+ + + + + } + > + + + + {TIMER_TYPES.map((type) => ( + + ))} + + + newValue && setStopDateTime(newValue)} + /> + + {!isTimeRangeValid && } + + ); +} diff --git a/extensions/baby-buddy/src/components/TummyTimeList.tsx b/extensions/baby-buddy/src/components/TummyTimeList.tsx new file mode 100644 index 00000000000..0ce7c5edea9 --- /dev/null +++ b/extensions/baby-buddy/src/components/TummyTimeList.tsx @@ -0,0 +1,280 @@ +import { + List, + ActionPanel, + Action, + Icon, + useNavigation, + showToast, + Toast, + confirmAlert, + Form, + Alert, +} from "@raycast/api"; +import { useState, useEffect } from "react"; +import { BabyBuddyAPI, Child, TummyTimeEntry } from "../api"; +import { formatDuration, formatTimeAgo, formatTimeWithTooltip } from "../utils"; +import { formatErrorMessage } from "../utils/form-helpers"; +import CreateTummyTimeForm from "./CreateTummyTimeForm"; + +interface TummyTimeListProps { + child: Child; +} + +export default function TummyTimeList({ child }: TummyTimeListProps) { + const [isLoading, setIsLoading] = useState(true); + const [tummyTimeEntries, setTummyTimeEntries] = useState([]); + const navigation = useNavigation(); + + async function fetchTummyTimeEntries() { + setIsLoading(true); + try { + const api = new BabyBuddyAPI(); + const entries = await api.getRecentTummyTime(child.id); + // Sort by end time, newest first + entries.sort((a: TummyTimeEntry, b: TummyTimeEntry) => new Date(b.end).getTime() - new Date(a.end).getTime()); + setTummyTimeEntries(entries); + } catch (error) { + showToast({ + style: Toast.Style.Failure, + title: "Failed to Load Tummy Time", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + + async function createTummyTimeAndNavigateBack() { + await fetchTummyTimeEntries(); + navigation.pop(); + } + + useEffect(() => { + fetchTummyTimeEntries(); + }, [child.id]); + + async function handleDeleteTummyTime(tummyTime: TummyTimeEntry) { + if ( + await confirmAlert({ + title: "Delete Tummy Time", + message: `Are you sure you want to delete this tummy time from ${formatTimeAgo(tummyTime.end)}?`, + primaryAction: { + title: "Delete", + style: Alert.ActionStyle.Destructive, + }, + }) + ) { + try { + const api = new BabyBuddyAPI(); + await api.deleteTummyTime(tummyTime.id); + showToast({ + style: Toast.Style.Success, + title: "Tummy Time Deleted", + message: "The tummy time has been deleted", + }); + fetchTummyTimeEntries(); + } catch (error) { + showToast({ + style: Toast.Style.Failure, + title: "Failed to Delete Tummy Time", + message: formatErrorMessage(error), + }); + } + } + } + + function handleCreateTummyTime() { + // Create a dummy timer with the child's ID + const dummyTimer = { + id: 0, + name: "New Tummy Time", + start: new Date().toISOString(), + end: new Date(new Date().getTime() + 1000).toISOString(), + active: false, + user: 0, + child: child.id, + duration: "0:00:00", + }; + + navigation.push( + , + ); + } + + return ( + + + + + } + > + + {tummyTimeEntries.map((tummyTime) => ( + handleDeleteTummyTime(tummyTime)} + onCreateTummyTime={handleCreateTummyTime} + onRefresh={fetchTummyTimeEntries} + formatDuration={formatDuration} + /> + ))} + + + ); +} + +interface TummyTimeListItemProps { + tummyTime: TummyTimeEntry; + onDelete: () => void; + onCreateTummyTime: () => void; + onRefresh: () => void; + formatDuration: (duration: string) => string; +} + +function TummyTimeListItem({ + tummyTime, + onDelete, + onCreateTummyTime, + onRefresh, + formatDuration, +}: TummyTimeListItemProps) { + const navigation = useNavigation(); + const timeInfo = formatTimeWithTooltip(tummyTime.end); + + function handleEdit() { + navigation.push( + { + showToast({ + style: Toast.Style.Success, + title: "Tummy Time Updated", + message: "The tummy time has been updated", + }); + navigation.pop(); + // Refresh the tummy time list after editing + onRefresh(); + }} + />, + ); + } + + return ( + + + + + + } + /> + ); +} + +interface EditTummyTimeFormProps { + tummyTime: TummyTimeEntry; + onTummyTimeUpdated: () => void; +} + +function EditTummyTimeForm({ tummyTime, onTummyTimeUpdated }: EditTummyTimeFormProps) { + const [isLoading, setIsLoading] = useState(false); + const [startTime, setStartTime] = useState(new Date(tummyTime.start)); + const [endTime, setEndTime] = useState(new Date(tummyTime.end)); + const [milestone, setMilestone] = useState(tummyTime.milestone || ""); + const navigation = useNavigation(); + + // Validate that end time is after start time + const isTimeRangeValid = endTime > startTime; + + async function handleSubmit() { + if (!isTimeRangeValid) { + showToast({ + style: Toast.Style.Failure, + title: "Invalid time range", + message: "End time must be after start time", + }); + return; + } + + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + + // Format dates properly + const startISOString = startTime.toISOString(); + const endISOString = endTime.toISOString(); + + // Prepare the data + const tummyTimeData = { + start: startISOString, + end: endISOString, + milestone: milestone || "", + }; + + // Update the tummy time entry + await api.updateTummyTime(tummyTime.id, tummyTimeData); + + onTummyTimeUpdated(); + } catch (error) { + showToast({ + style: Toast.Style.Failure, + title: "Failed to Update Tummy Time", + message: formatErrorMessage(error), + }); + } finally { + setIsLoading(false); + } + } + + return ( +
+ + navigation.pop()} /> + + } + > + newValue && setStartTime(newValue)} + /> + + newValue && setEndTime(newValue)} + /> + + {!isTimeRangeValid && } + + + + + + ); +} diff --git a/extensions/baby-buddy/src/index.tsx b/extensions/baby-buddy/src/index.tsx new file mode 100644 index 00000000000..59a2e231932 --- /dev/null +++ b/extensions/baby-buddy/src/index.tsx @@ -0,0 +1,205 @@ +import { ActionPanel, Action, List, Icon, Color, Detail, useNavigation } from "@raycast/api"; +import { useState, useEffect } from "react"; +import { BabyBuddyAPI, Child, FeedingEntry, SleepEntry, DiaperEntry } from "./api"; +import { formatTimeWithTooltip, getDiaperDescription } from "./utils"; +import ChildDetailView from "./components/ChildDetailView"; +import CreateFeedingForm from "./components/CreateFeedingForm"; +import CreateSleepForm from "./components/CreateSleepForm"; +import CreateDiaperForm from "./components/CreateDiaperForm"; +import CreateTummyTimeForm from "./components/CreateTummyTimeForm"; +import FeedingList from "./components/FeedingList"; +import SleepList from "./components/SleepList"; +import DiaperList from "./components/DiaperList"; +import TummyTimeList from "./components/TummyTimeList"; + +export default function Command() { + const [isLoading, setIsLoading] = useState(true); + const [children, setChildren] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchChildren() { + try { + const api = new BabyBuddyAPI(); + const childrenData = await api.getChildren(); + setChildren(childrenData); + setIsLoading(false); + } catch (e) { + setError("Failed to fetch children. Please check your Baby Buddy URL and API key."); + setIsLoading(false); + } + } + + fetchChildren(); + }, []); + + if (error) { + return ; + } + + return ( + + {children.map((child) => ( + + ))} + + ); +} + +function ChildListItem({ child }: { child: Child }) { + const [lastFeeding, setLastFeeding] = useState(null); + const [lastSleep, setLastSleep] = useState(null); + const [lastDiaper, setLastDiaper] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const navigation = useNavigation(); + + useEffect(() => { + async function fetchChildData() { + try { + const api = new BabyBuddyAPI(); + const [feedingData, sleepData, diaperData] = await Promise.all([ + api.getLastFeeding(child.id), + api.getLastSleep(child.id), + api.getLastDiaper(child.id), + ]); + + setLastFeeding(feedingData); + setLastSleep(sleepData); + setLastDiaper(diaperData); + setIsLoading(false); + } catch (e) { + console.error("Failed to fetch child data:", e); + setIsLoading(false); + } + } + + fetchChildData(); + }, [child.id]); + + const accessories = []; + + if (!isLoading) { + if (lastFeeding) { + accessories.push({ + icon: { source: Icon.Mug, tintColor: Color.Blue }, + tooltip: formatTimeWithTooltip(lastFeeding.start).tooltip, + text: formatTimeWithTooltip(lastFeeding.start).text, + }); + } + + if (lastSleep) { + accessories.push({ + icon: { source: Icon.Moon, tintColor: Color.Purple }, + tooltip: formatTimeWithTooltip(lastSleep.end).tooltip, + text: formatTimeWithTooltip(lastSleep.end).text, + }); + } + + if (lastDiaper) { + const diaperType = getDiaperDescription(lastDiaper.wet, lastDiaper.solid); + accessories.push({ + icon: { source: Icon.Trash, tintColor: Color.Green }, + tooltip: `Last diaper (${diaperType}): ${formatTimeWithTooltip(lastDiaper.time).tooltip}`, + text: formatTimeWithTooltip(lastDiaper.time).text, + }); + } + } + + function handleCreateActivity(activityType: string) { + // Create a dummy timer with the child's ID + const endTime = new Date(); + const startTime = new Date(endTime); + startTime.setSeconds(startTime.getSeconds() - 1); // Set start time 1 second before end time + + const dummyTimer = { + id: 0, + name: `New ${activityType}`, + start: startTime.toISOString(), + end: endTime.toISOString(), + active: false, + user: 0, + child: child.id, + duration: "0:00:00", + }; + + const childName = `${child.first_name} ${child.last_name}`; + + // After creating an activity, navigate to the specific activity list + const onEventCreated = () => { + // First pop the create form from the navigation stack + navigation.pop(); + + // Then push the appropriate list view + console.log("Activity type:", activityType); + switch (activityType) { + case "Feeding": + navigation.push(); + break; + case "Sleep": + navigation.push(); + break; + case "Diaper Change": + navigation.push(); + break; + case "Tummy Time": + navigation.push(); + break; + default: + navigation.push(); + } + }; + + switch (activityType) { + case "Feeding": + return ; + case "Sleep": + return ; + case "Diaper Change": + return ; + case "Tummy Time": + return ; + default: + return null; + } + } + + return ( + + } /> + + + + + + + + + } + /> + ); +} diff --git a/extensions/baby-buddy/src/timers.tsx b/extensions/baby-buddy/src/timers.tsx new file mode 100644 index 00000000000..922307d284e --- /dev/null +++ b/extensions/baby-buddy/src/timers.tsx @@ -0,0 +1,234 @@ +import { ActionPanel, Action, List, Icon, Color, Detail, showToast, Toast, confirmAlert } from "@raycast/api"; +import { useState, useEffect, useCallback } from "react"; +import { BabyBuddyAPI, Timer } from "./api"; +import { calculateElapsedTime, formatErrorMessage, formatTimeWithTooltip } from "./utils"; +import CreateTimerForm from "./components/CreateTimerForm"; +import StopTimerForm from "./components/StopTimerForm"; +import EditTimerForm from "./components/EditTimerForm"; +import { showFailureToast } from "@raycast/utils"; + +interface TimerWithDetails extends Timer { + childName: string; + elapsedTime: string; +} + +export default function Command() { + const [isLoading, setIsLoading] = useState(true); + const [timers, setTimers] = useState([]); + const [error, setError] = useState(null); + const [refreshTrigger, setRefreshTrigger] = useState(0); + + const refreshTimers = useCallback(() => { + setRefreshTrigger((prev) => prev + 1); + }, []); + + useEffect(() => { + async function fetchTimers() { + try { + setIsLoading(true); + const api = new BabyBuddyAPI(); + const activeTimers = await api.getActiveTimers(); + + const timersWithDetails = await Promise.all( + activeTimers.map(async (timer) => { + const childName = await api.getChildName(timer.child); + const elapsedTime = calculateElapsedTime(timer.start); + return { ...timer, childName, elapsedTime }; + }), + ); + + setTimers(timersWithDetails); + } catch (e) { + setError("Failed to fetch timers. Please check your Baby Buddy URL and API key."); + } finally { + setIsLoading(false); + } + } + + fetchTimers(); + + // Update elapsed time every second + const intervalId = setInterval(() => { + setTimers((prevTimers) => + prevTimers.map((timer) => ({ + ...timer, + elapsedTime: calculateElapsedTime(timer.start), + })), + ); + }, 1000); + + return () => clearInterval(intervalId); + }, [refreshTrigger]); + + if (error) { + return ; + } + + return ( + + + } + /> + + } + > + + {timers.map((timer) => ( + + ))} + + {timers.length === 0 && !isLoading && ( + + } + /> + + } + /> + )} + + ); +} + +function TimerListItem({ timer, onTimerStopped }: { timer: TimerWithDetails; onTimerStopped: () => void }) { + const getTimerIcon = (timerName: string) => { + if (!timerName) { + return { source: Icon.Clock, tintColor: Color.Green }; + } + + const name = timerName.toLowerCase(); + if (name.includes("feed") || name.includes("nursing") || name.includes("bottle")) { + return { source: Icon.Mug, tintColor: Color.Blue }; + } else if (name.includes("sleep") || name.includes("nap")) { + return { source: Icon.Moon, tintColor: Color.Purple }; + } else if (name.includes("tummy") || name.includes("play")) { + return { source: Icon.Star, tintColor: Color.Orange }; + } else if (name.includes("bath")) { + return { source: Icon.Droplets, tintColor: Color.Blue }; + } else if (name.includes("pump")) { + return { source: Icon.Droplets, tintColor: Color.PrimaryText }; + } else { + return { source: Icon.Clock, tintColor: Color.Green }; + } + }; + + const handleDeleteTimer = async () => { + // Show confirmation dialog before deleting + const options = { + title: "Delete Timer", + message: `Are you sure you want to delete the "${timer.name || "Unnamed Timer"}" timer for ${timer.childName}?`, + icon: Icon.Trash, + }; + + if (await confirmAlert(options)) { + try { + const api = new BabyBuddyAPI(); + await api.deleteTimer(timer.id); + + await showToast({ + style: Toast.Style.Success, + title: "Timer Deleted", + message: `${timer.name || "Unnamed Timer"} timer deleted`, + }); + + onTimerStopped(); // Refresh the timer list + } catch (error) { + await showFailureToast({ + title: "Failed to Delete Timer", + message: "Failed to delete the timer: " + formatErrorMessage(error), + }); + } + } + }; + + const handleResetTimer = async () => { + // Show confirmation dialog before resetting + const options = { + title: "Reset Timer", + message: `Are you sure you want to reset the "${timer.name || "Unnamed Timer"}" timer for ${timer.childName}?`, + icon: Icon.ArrowClockwise, + }; + + if (await confirmAlert(options)) { + try { + const api = new BabyBuddyAPI(); + await api.resetTimer(timer.id); + + await showToast({ + style: Toast.Style.Success, + title: "Timer Reset", + message: `${timer.name || "Unnamed Timer"} timer reset to current time`, + }); + + onTimerStopped(); // Refresh the timer list + } catch (error) { + await showToast({ + style: Toast.Style.Failure, + title: "Failed to Reset Timer", + message: "Please try again", + }); + } + } + }; + + return ( + + } + /> + + } + /> + } + /> + + + + } + /> + ); +} diff --git a/extensions/baby-buddy/src/tools/createDiaper.ts b/extensions/baby-buddy/src/tools/createDiaper.ts new file mode 100644 index 00000000000..0b3040e8a8d --- /dev/null +++ b/extensions/baby-buddy/src/tools/createDiaper.ts @@ -0,0 +1,83 @@ +import { showToast, Toast } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import { BabyBuddyAPI } from "../api"; +import { findChildByName } from "../utils/api-helpers"; +import { formatDiaperDataFromContents } from "../utils/form-helpers"; +import { formatErrorMessage } from "../utils/formatters"; +import { formatTimeToISO, getContentsDescription, normalizeContents } from "../utils/normalizers"; + +/** + * Create a new diaper change entry for a child + * + * If asked to create a diaper change with 2 different amount values, + * create multiple diapers. E.g. "log a wet 1 and solid 2" should create + * 2 diaper changes. If there's two different types selected (wet and solid) but + * they have the same value, they can both be selected in the same diaper entry. + * + * @param childName - The name of the child + * @param contents - Contents of the diaper (wet, solid, both) + * @param color - Color of the diaper contents + * @param amount - Amount description + * @param notes - Any notes about the diaper change + * @param time - The time of the diaper change (ISO string). If not provided, current time will be used. + */ +export default async function ({ + childName, + contents, + color = "", + amount = "", + notes = "", + time, +}: { + childName: string; + contents: string; + color?: string; + amount?: string; + notes?: string; + time?: string; +}) { + const api = new BabyBuddyAPI(); + const children = await api.getChildren(); + + // Find child using the utility function + const child = findChildByName(children, childName); + + if (!child) { + throw new Error(`Child with name ${childName} not found`); + } + + // Format time to ISO using utility function + const formattedTime = formatTimeToISO(time) || new Date().toISOString(); + + // Normalize contents using utility function + const normalizedContents = normalizeContents(contents); + + // Format and prepare the data using utility function + const diaperData = { + ...formatDiaperDataFromContents({ + childId: child.id, + time: formattedTime, + contents, + color, + amount, + notes, + }), + }; + + try { + const newDiaper = await api.createDiaper(diaperData); + + await showToast({ + style: Toast.Style.Success, + title: "Diaper Change Created", + message: `Recorded ${getContentsDescription(normalizedContents)} diaper for ${child.first_name}`, + }); + + return newDiaper; + } catch (error) { + await showFailureToast({ + title: "Error Creating Diaper Change", + message: formatErrorMessage(error), + }); + } +} diff --git a/extensions/baby-buddy/src/tools/createFeeding.ts b/extensions/baby-buddy/src/tools/createFeeding.ts new file mode 100644 index 00000000000..3b430048822 --- /dev/null +++ b/extensions/baby-buddy/src/tools/createFeeding.ts @@ -0,0 +1,72 @@ +import { showToast, Toast } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import { BabyBuddyAPI } from "../api"; +import { findChildByName } from "../utils/api-helpers"; +import { createFeedingData } from "../utils/form-helpers"; +import { formatErrorMessage } from "../utils/formatters"; + +/** + * Create a new feeding entry for a child + * @param childName - The name of the child + * @param type - The type of feeding (breast milk, formula, solid food, fortified breast milk) + * @param method - The feeding method (bottle, left breast, right breast, both breasts) + * @param amount - The amount fed, if applicable + * @param notes - Any notes about the feeding + * @param startTime - The start time of the feeding (ISO string). If not provided, 5 minutes ago will be used. + * @param endTime - The end time of the feeding (ISO string). If not provided, current time will be used. + */ +export default async function ({ + childName, + type, + method = "bottle", + amount = "", + notes = "", + startTime, + endTime = new Date().toISOString(), +}: { + childName: string; + type: string; + method?: string; + amount?: string; + notes?: string; + startTime?: string; + endTime?: string; +}) { + const api = new BabyBuddyAPI(); + const children = await api.getChildren(); + + // Find child using the utility function + const child = findChildByName(children, childName); + + if (!child) { + throw new Error(`Child with name ${childName} not found`); + } + + // Create complete feeding data using utility function + const feedingData = createFeedingData({ + childId: child.id, + startTime, + endTime, + type, + method, + amount, + notes, + }); + + try { + const newFeeding = await api.createFeeding(feedingData); + + await showToast({ + style: Toast.Style.Success, + title: "Feeding Created", + message: `Recorded ${feedingData.type} feeding for ${child.first_name}`, + }); + + return newFeeding; + } catch (error) { + await showFailureToast({ + title: "Error", + message: formatErrorMessage(error), + }); + } +} diff --git a/extensions/baby-buddy/src/tools/createSleep.ts b/extensions/baby-buddy/src/tools/createSleep.ts new file mode 100644 index 00000000000..b8c255b6904 --- /dev/null +++ b/extensions/baby-buddy/src/tools/createSleep.ts @@ -0,0 +1,63 @@ +import { showToast, Toast } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import { BabyBuddyAPI } from "../api"; +import { createSleepData, formatErrorMessage } from "../utils/form-helpers"; +import { findChildByName } from "../utils/normalizers"; + +/** + * Create a new sleep entry for a child + * @param childName - The name of the child + * @param notes - Any notes about the sleep + * @param startTime - The start time of the sleep (ISO string). If not provided, 5 minutes ago will be used. + * @param endTime - The end time of the sleep (ISO string). If not provided, current time will be used. + * @param isNap - Whether this sleep is a nap (true) or night sleep (false) + */ +export default async function ({ + childName, + notes = "", + startTime, + endTime, + isNap = false, +}: { + childName: string; + notes?: string; + startTime?: string; + endTime?: string; + isNap?: boolean; +}) { + const api = new BabyBuddyAPI(); + const children = await api.getChildren(); + + // Find child using the utility function + const child = findChildByName(children, childName); + + if (!child) { + throw new Error(`Child with name ${childName} not found`); + } + + // Create complete sleep data using utility function + const sleepData = createSleepData({ + childId: child.id, + startTime, + endTime, + isNap, + notes, + }); + + try { + const newSleep = await api.createSleep(sleepData); + + await showToast({ + style: Toast.Style.Success, + title: "Sleep Created", + message: `Recorded ${isNap ? "nap" : "sleep"} session for ${child.first_name}`, + }); + + return newSleep; + } catch (error) { + await showFailureToast({ + title: "Error", + message: formatErrorMessage(error), + }); + } +} diff --git a/extensions/baby-buddy/src/tools/createTimer.ts b/extensions/baby-buddy/src/tools/createTimer.ts new file mode 100644 index 00000000000..8c0008035ca --- /dev/null +++ b/extensions/baby-buddy/src/tools/createTimer.ts @@ -0,0 +1,49 @@ +import { showToast, Toast } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import { BabyBuddyAPI } from "../api"; +import { findChildByName } from "../utils/api-helpers"; +import { createTimerData } from "../utils/form-helpers"; +import { formatErrorMessage } from "../utils/formatters"; + +/** + * Create a new timer for a child + * @param childName - The name of the child + * @param name - Name for the timer (e.g., "Feeding", "Sleep", "Tummy Time") + * @param time - The start time for the timer. If not provided, current time will be used. + */ +export default async function ({ childName, name, time }: { childName: string; name: string; time?: string }) { + const api = new BabyBuddyAPI(); + const children = await api.getChildren(); + + // Find child using the utility function + const child = findChildByName(children, childName); + + if (!child) { + throw new Error(`Child with name ${childName} not found`); + } + + // Create timer data using utility function + const timerData = createTimerData({ + childId: child.id, + name, + startTime: time ? new Date(time) : undefined, + }); + + try { + // Call API with the correct parameters + const newTimer = await api.createTimer(child.id, timerData.name, timerData.start); + + await showToast({ + style: Toast.Style.Success, + title: "Timer Created", + message: `Started ${name} timer for ${child.first_name}`, + }); + + return newTimer; + } catch (error) { + await showFailureToast({ + title: "Error", + message: formatErrorMessage(error), + }); + } +} diff --git a/extensions/baby-buddy/src/tools/createTummyTime.ts b/extensions/baby-buddy/src/tools/createTummyTime.ts new file mode 100644 index 00000000000..6635d1e5f9e --- /dev/null +++ b/extensions/baby-buddy/src/tools/createTummyTime.ts @@ -0,0 +1,80 @@ +import { showToast, Toast } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import { BabyBuddyAPI } from "../api"; +import { formatErrorMessage } from "../utils/form-helpers"; +import { calculateDuration, findChildByName, formatTimeToISO } from "../utils/normalizers"; + +/** + * Create a new tummy time entry for a child + * @param childName - The name of the child + * @param milestone - Any milestone achieved during tummy time + * @param notes - Any notes about the tummy time + * @param startTime - The start time of the tummy time (ISO string). If not provided, 5 minutes ago will be used. + * @param endTime - The end time of the tummy time (ISO string). If not provided, current time will be used. + */ +export default async function ({ + childName, + milestone = "", + notes = "", + startTime, + endTime, +}: { + childName: string; + milestone?: string; + notes?: string; + startTime?: string; + endTime?: string; +}) { + try { + const api = new BabyBuddyAPI(); + const children = await api.getChildren(); + + // Find child using the utility function + const child = findChildByName(children, childName); + + if (!child) { + throw new Error(`Child with name ${childName} not found`); + } + + // Set default times if not provided + const now = new Date(); + const defaultStartTime = new Date(now.getTime() - 5 * 60 * 1000); // 5 minutes ago + + // Format times to ISO using utility function + const formattedStartTime = formatTimeToISO(startTime) || defaultStartTime.toISOString(); + const formattedEndTime = formatTimeToISO(endTime) || now.toISOString(); + + // Validate that end time is after start time + if (new Date(formattedEndTime) <= new Date(formattedStartTime)) { + throw new Error("End time must be after start time"); + } + + // Calculate duration using utility function + const duration = calculateDuration(formattedStartTime, formattedEndTime); + + // Create the tummy time entry + const tummyTimeData = { + child: child.id, + start: formattedStartTime, + end: formattedEndTime, + duration, + milestone, + notes, + }; + + const newTummyTime = await api.createTummyTime(tummyTimeData); + + await showToast({ + style: Toast.Style.Success, + title: "Tummy Time Created", + message: `Recorded tummy time for ${child.first_name}`, + }); + + return newTummyTime; + } catch (error) { + await showFailureToast({ + title: "Error", + message: formatErrorMessage(error), + }); + } +} diff --git a/extensions/baby-buddy/src/tools/deleteDiaper.ts b/extensions/baby-buddy/src/tools/deleteDiaper.ts new file mode 100644 index 00000000000..eec89fa6fa3 --- /dev/null +++ b/extensions/baby-buddy/src/tools/deleteDiaper.ts @@ -0,0 +1,50 @@ +import { Action, showToast, Toast, Tool } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import axios from "axios"; +import { BabyBuddyAPI } from "../api"; +import { formatErrorMessage } from "../utils"; + +type DeleteDiaperInput = { + /** + * The ID of the diaper change entry to delete + */ + diaperId: number; +}; + +/** + * Confirmation function that will be called before the action is executed + */ +export const confirmation: Tool.Confirmation = async () => { + return { + style: Action.Style.Destructive, + message: "Are you sure you want to delete this diaper change?", + }; +}; + +export default async function deleteDiaper({ diaperId }: DeleteDiaperInput) { + const api = new BabyBuddyAPI(); + + try { + await api.deleteDiaper(diaperId); + + await showToast({ + style: Toast.Style.Success, + title: "Diaper Change Deleted", + message: `Deleted diaper change #${diaperId}`, + }); + + return { success: true, diaperId }; + } catch (error) { + let errorMessage = "Failed to delete diaper change"; + if (axios.isAxiosError(error) && error.response) { + errorMessage += `: ${formatErrorMessage(error)}`; + } + + await showFailureToast({ + title: "Error", + message: errorMessage, + }); + + throw error; + } +} diff --git a/extensions/baby-buddy/src/tools/deleteFeeding.ts b/extensions/baby-buddy/src/tools/deleteFeeding.ts new file mode 100644 index 00000000000..b499e272197 --- /dev/null +++ b/extensions/baby-buddy/src/tools/deleteFeeding.ts @@ -0,0 +1,56 @@ +import { Action, showToast, Toast, Tool } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import axios from "axios"; +import { BabyBuddyAPI } from "../api"; +import { formatErrorMessage } from "../utils"; + +type DeleteFeedingInput = { + /** + * The ID of the feeding entry to delete + */ + feedingId: number; +}; + +/** + * Confirmation function that will be called before the action is executed + */ +export const confirmation: Tool.Confirmation = async (input) => { + return { + style: Action.Style.Destructive, + message: "Are you sure you want to delete this feeding entry?", + info: [ + { + name: "Feeding ID", + value: `#${input.feedingId}`, + }, + ], + }; +}; + +export default async function deleteFeeding({ feedingId }: DeleteFeedingInput) { + const api = new BabyBuddyAPI(); + + try { + await api.deleteFeeding(feedingId); + + await showToast({ + style: Toast.Style.Success, + title: "Feeding Deleted", + message: `Deleted feeding #${feedingId}`, + }); + + return { success: true, feedingId }; + } catch (error) { + let errorMessage = "Failed to delete feeding"; + if (axios.isAxiosError(error) && error.response) { + errorMessage += `: ${formatErrorMessage(error)}`; + } + + await showFailureToast({ + title: "Error", + message: errorMessage, + }); + + throw error; + } +} diff --git a/extensions/baby-buddy/src/tools/deleteSleep.ts b/extensions/baby-buddy/src/tools/deleteSleep.ts new file mode 100644 index 00000000000..b32edc76ab6 --- /dev/null +++ b/extensions/baby-buddy/src/tools/deleteSleep.ts @@ -0,0 +1,50 @@ +import { Action, showToast, Toast, Tool } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import axios from "axios"; +import { BabyBuddyAPI } from "../api"; +import { formatErrorMessage } from "../utils"; + +type DeleteSleepInput = { + /** + * The ID of the sleep entry to delete + */ + sleepId: number; +}; + +/** + * Confirmation function that will be called before the action is executed + */ +export const confirmation: Tool.Confirmation = async () => { + return { + style: Action.Style.Destructive, + message: "Are you sure you want to delete this sleep entry?", + }; +}; + +export default async function deleteSleep({ sleepId }: DeleteSleepInput) { + const api = new BabyBuddyAPI(); + + try { + await api.deleteSleep(sleepId); + + await showToast({ + style: Toast.Style.Success, + title: "Sleep Deleted", + message: `Deleted sleep #${sleepId}`, + }); + + return { success: true, sleepId }; + } catch (error) { + let errorMessage = "Failed to delete sleep"; + if (axios.isAxiosError(error) && error.response) { + errorMessage += `: ${formatErrorMessage(error.response.data)}`; + } + + await showFailureToast({ + title: "Error", + message: errorMessage, + }); + + throw error; + } +} diff --git a/extensions/baby-buddy/src/tools/deleteTimer.ts b/extensions/baby-buddy/src/tools/deleteTimer.ts new file mode 100644 index 00000000000..8dbe4eb65e8 --- /dev/null +++ b/extensions/baby-buddy/src/tools/deleteTimer.ts @@ -0,0 +1,110 @@ +import { Action, showToast, Toast, Tool } from "@raycast/api"; +import axios from "axios"; +import { BabyBuddyAPI } from "../api"; +import { formatErrorMessage } from "../utils"; + +type DeleteTimerInput = { + /** + * The ID of the timer to delete + */ + timerId: number; +}; + +/** + * Confirmation function that will be called before the action is executed + */ +export const confirmation: Tool.Confirmation = async (input) => { + // Fetch timer details to get the name + try { + const api = new BabyBuddyAPI(); + const timer = await api.getTimerById(input.timerId); + + if (timer) { + // Try to get child name for additional context + let childName = ""; + try { + childName = await api.getChildName(timer.child); + } catch { + // Ignore errors with getting child name + } + + return { + style: Action.Style.Destructive, + message: "Are you sure you want to delete this timer?", + info: [ + { + name: "Timer Name", + value: timer.name, + }, + { + name: "Child", + value: childName || "Unknown", + }, + { + name: "Start Time", + value: new Date(timer.start).toLocaleString(), + }, + { + name: "Timer ID", + value: `#${input.timerId}`, + }, + ], + }; + } else { + return { + style: Action.Style.Destructive, + message: "Are you sure you want to delete this timer?", + info: [ + { + name: "Timer ID", + value: `#${input.timerId}`, + }, + ], + }; + } + } catch (error) { + // Fallback in case we can't get the timer name + return { + style: Action.Style.Destructive, + message: "Are you sure you want to delete this timer?", + info: [ + { + name: "Timer ID", + value: `#${input.timerId}`, + }, + ], + }; + } +}; + +/** + * Stop or delete a timer + */ +export default async function deleteTimer({ timerId }: DeleteTimerInput) { + const api = new BabyBuddyAPI(); + + try { + await api.deleteTimer(timerId); + + await showToast({ + style: Toast.Style.Success, + title: "Timer Deleted", + message: `Deleted timer #${timerId}`, + }); + + return { success: true, timerId }; + } catch (error) { + let errorMessage = "Failed to delete timer"; + if (axios.isAxiosError(error) && error.response) { + errorMessage += `: ${formatErrorMessage(error)}`; + } + + await showToast({ + style: Toast.Style.Failure, + title: "Error Deleting Timer", + message: errorMessage, + }); + + throw error; + } +} diff --git a/extensions/baby-buddy/src/tools/deleteTummyTime.ts b/extensions/baby-buddy/src/tools/deleteTummyTime.ts new file mode 100644 index 00000000000..3102a16d342 --- /dev/null +++ b/extensions/baby-buddy/src/tools/deleteTummyTime.ts @@ -0,0 +1,47 @@ +import { Action, showToast, Toast, Tool } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import axios from "axios"; +import { BabyBuddyAPI } from "../api"; + +type DeleteTummyTimeInput = { + /** + * The ID of the tummy time entry to delete + */ + tummyTimeId: number; +}; + +/** + * Confirmation function that will be called before the action is executed + */ +export const confirmation: Tool.Confirmation = async () => { + return { + style: Action.Style.Destructive, + message: "Are you sure you want to delete this tummy time entry?", + }; +}; + +export default async function deleteTummyTime({ tummyTimeId }: DeleteTummyTimeInput) { + const api = new BabyBuddyAPI(); + + try { + await api.deleteTummyTime(tummyTimeId); + + await showToast({ + style: Toast.Style.Success, + title: "Tummy Time Deleted", + message: `Deleted tummy time #${tummyTimeId}`, + }); + + return { success: true, tummyTimeId }; + } catch (error) { + let errorMessage = "Failed to delete tummy time"; + if (axios.isAxiosError(error) && error.response) { + errorMessage += `: ${JSON.stringify(error.response.data)}`; + } + + await showFailureToast({ + title: "Error", + message: errorMessage, + }); + } +} diff --git a/extensions/baby-buddy/src/tools/editDiaper.ts b/extensions/baby-buddy/src/tools/editDiaper.ts new file mode 100644 index 00000000000..e6190036e1e --- /dev/null +++ b/extensions/baby-buddy/src/tools/editDiaper.ts @@ -0,0 +1,95 @@ +import { showToast, Toast } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import { BabyBuddyAPI } from "../api"; +import { formatErrorMessage, prepareDiaperUpdateData } from "../utils/form-helpers"; +import { findChildByName } from "../utils/normalizers"; + +type EditDiaperInput = { + /** + * The ID of the diaper change entry to edit + */ + diaperId: number; + /** + * The name of the child this diaper change is for + */ + childName: string; + /** + * Whether the diaper was wet + */ + wet?: boolean; + /** + * Whether the diaper contained solid waste + */ + solid?: boolean; + /** + * The color of solid waste, if applicable + */ + color?: string; + /** + * The amount, if applicable + */ + amount?: string; + /** + * Notes about the diaper change + */ + notes?: string; + /** + * Time of the diaper change (ISO string or HH:MM:SS format) + */ + time?: string; +}; + +export default async function editDiaper({ + diaperId, + childName, + wet, + solid, + color, + amount, + notes, + time, +}: EditDiaperInput) { + const api = new BabyBuddyAPI(); + + const children = await api.getChildren(); + const child = children.length === 1 ? children[0] : findChildByName(children, childName || ""); + + if (!child) { + throw new Error(`Child with name ${childName} not found`); + } + + const childId = child.id; + + // Prepare update data using utility function + const updateData = prepareDiaperUpdateData({ + childId, + time, + wet, + solid, + color, + amount, + notes, + }); + + // Only proceed if there's something to update + if (Object.keys(updateData).length === 0) { + return { message: "No updates provided" }; + } + + try { + const updatedDiaper = await api.updateDiaper(diaperId, updateData); + + await showToast({ + style: Toast.Style.Success, + title: "Diaper Change Updated", + message: `Updated diaper change #${diaperId}`, + }); + + return updatedDiaper; + } catch (error) { + await showFailureToast({ + title: "Error", + message: formatErrorMessage(error), + }); + } +} diff --git a/extensions/baby-buddy/src/tools/editFeeding.ts b/extensions/baby-buddy/src/tools/editFeeding.ts new file mode 100644 index 00000000000..c1a05684081 --- /dev/null +++ b/extensions/baby-buddy/src/tools/editFeeding.ts @@ -0,0 +1,100 @@ +import { showToast, Toast } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import { BabyBuddyAPI } from "../api"; +import { formatErrorMessage, prepareFeedingUpdateData } from "../utils/form-helpers"; +import { findChildByName } from "../utils/normalizers"; + +type EditFeedingInput = { + /** + * The ID of the feeding entry to edit + */ + feedingId: number; + /** + * The name of the child this feeding is for + */ + childName?: string; + /** + * Valid options are Breast Milk, Formula, Fortified Breast Milk, Solid Food + */ + type?: string; + /** + * Valid options are Bottle, left breast, right breast, both breasts + */ + method?: string; + /** + * The amount of food or milk + */ + amount?: string; + /** + * Notes about the feeding + */ + notes?: string; + /** + * Start time for the feeding (ISO string or HH:MM:SS format) + */ + startTime?: string; + /** + * End time for the feeding (ISO string or HH:MM:SS format) + */ + endTime?: string; +}; + +export default async function editFeeding({ + feedingId, + childName, + type, + method, + amount, + notes, + startTime, + endTime, +}: EditFeedingInput) { + const api = new BabyBuddyAPI(); + + let childId: number | undefined; + + // If childName is provided, look up the child ID + if (childName) { + const children = await api.getChildren(); + const child = findChildByName(children, childName); + + if (!child) { + throw new Error(`Child with name ${childName} not found`); + } + + childId = child.id; + } + + // Prepare update data using utility function + const updateData = prepareFeedingUpdateData({ + childId, + startTime, + endTime, + type, + method, + amount, + notes, + }); + + // Only proceed if there's something to update + if (Object.keys(updateData).length === 0) { + return { message: "No updates provided" }; + } + + try { + const updatedFeeding = await api.updateFeeding(feedingId, updateData); + + await showToast({ + style: Toast.Style.Success, + title: "Feeding Updated", + message: `Updated feeding #${feedingId}`, + }); + + return updatedFeeding; + } catch (error) { + await showFailureToast({ + title: "Error", + message: formatErrorMessage(error), + }); + } +} diff --git a/extensions/baby-buddy/src/tools/editSleep.ts b/extensions/baby-buddy/src/tools/editSleep.ts new file mode 100644 index 00000000000..98a1013ce36 --- /dev/null +++ b/extensions/baby-buddy/src/tools/editSleep.ts @@ -0,0 +1,81 @@ +import { showToast, Toast } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import { BabyBuddyAPI } from "../api"; +import { formatErrorMessage, prepareSleepUpdateData } from "../utils/form-helpers"; +import { findChildByName } from "../utils/normalizers"; + +type EditSleepInput = { + /** + * The ID of the sleep entry to edit + */ + sleepId: number; + /** + * The name of the child this sleep is for + */ + childName?: string; + /** + * Whether this is a nap (true) or night sleep (false) + */ + isNap?: boolean; + /** + * Notes about the sleep + */ + notes?: string; + /** + * Start time for the sleep (ISO string or HH:MM:SS format) + */ + startTime?: string; + /** + * End time for the sleep (ISO string or HH:MM:SS format) + */ + endTime?: string; +}; + +export default async function editSleep({ sleepId, childName, isNap, notes, startTime, endTime }: EditSleepInput) { + const api = new BabyBuddyAPI(); + + let childId: number | undefined; + + // If childName is provided, look up the child ID + if (childName) { + const children = await api.getChildren(); + const child = findChildByName(children, childName); + + if (!child) { + throw new Error(`Child with name ${childName} not found`); + } + + childId = child.id; + } + + // Prepare update data using utility function + const updateData = prepareSleepUpdateData({ + childId, + startTime, + endTime, + isNap, + notes, + }); + + // Only proceed if there's something to update + if (Object.keys(updateData).length === 0) { + throw new Error("No updates provided"); + } + + try { + const updatedSleep = await api.updateSleep(sleepId, updateData); + + await showToast({ + style: Toast.Style.Success, + title: "Sleep Updated", + message: `Updated sleep #${sleepId}`, + }); + + return updatedSleep; + } catch (error) { + await showFailureToast({ + title: "Error Updating Sleep", + message: formatErrorMessage(error), + }); + } +} diff --git a/extensions/baby-buddy/src/tools/editTimer.ts b/extensions/baby-buddy/src/tools/editTimer.ts new file mode 100644 index 00000000000..82750a0f6cd --- /dev/null +++ b/extensions/baby-buddy/src/tools/editTimer.ts @@ -0,0 +1,60 @@ +import { showToast, Toast } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import { BabyBuddyAPI } from "../api"; +import { formatErrorMessage, prepareTimerUpdateData } from "../utils/form-helpers"; + +type EditTimerInput = { + /** + * The ID of the timer to edit + */ + timerId: number; + /** + * The name of the timer + */ + timerName?: string; + /** + * Start time for the timer (ISO string or HH:MM:SS format) + */ + startTime?: string; + /** + * End time for the timer (ISO string or HH:MM:SS format) + */ + endTime?: string; +}; + +/** + * Update a timer + */ +export default async function editTimer({ timerId, timerName, startTime, endTime }: EditTimerInput) { + const api = new BabyBuddyAPI(); + + // Prepare update data using utility function + const updateData = prepareTimerUpdateData({ + timerName, + startTime, + endTime, + }); + + // Only proceed if there's something to update + if (Object.keys(updateData).length === 0) { + return { message: "No updates provided" }; + } + + try { + // Update timer with all data at once + const updatedTimer = await api.updateTimer(timerId, updateData); + + await showToast({ + style: Toast.Style.Success, + title: "Timer Updated", + message: `Updated timer #${timerId}`, + }); + + return updatedTimer; + } catch (error) { + await showFailureToast({ + title: "Error", + message: formatErrorMessage(error), + }); + } +} diff --git a/extensions/baby-buddy/src/tools/editTummyTime.ts b/extensions/baby-buddy/src/tools/editTummyTime.ts new file mode 100644 index 00000000000..1e77410aef3 --- /dev/null +++ b/extensions/baby-buddy/src/tools/editTummyTime.ts @@ -0,0 +1,99 @@ +import { showToast, Toast } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; +import { BabyBuddyAPI, TummyTimeEntry } from "../api"; +import { formatErrorMessage } from "../utils/form-helpers"; +import { calculateDuration, findChildByName, formatTimeToISO } from "../utils/normalizers"; + +type EditTummyTimeInput = { + /** + * The ID of the tummy time entry to edit + */ + tummyTimeId: number; + /** + * The name of the child this tummy time is for + */ + childName?: string; + /** + * Milestone achieved during tummy time + */ + milestone?: string; + /** + * Notes about the tummy time + */ + notes?: string; + /** + * Start time for the tummy time (ISO string or HH:MM:SS format) + */ + startTime?: string; + /** + * End time for the tummy time (ISO string or HH:MM:SS format) + */ + endTime?: string; +}; + +export default async function editTummyTime({ + tummyTimeId, + childName, + milestone, + notes, + startTime, + endTime, +}: EditTummyTimeInput) { + const api = new BabyBuddyAPI(); + + let childId: number | undefined; + + // If childName is provided, look up the child ID + if (childName) { + const children = await api.getChildren(); + const child = findChildByName(children, childName); + + if (!child) { + throw new Error(`Child with name ${childName} not found`); + } + + childId = child.id; + } + + // Format times to ISO using utility function + const formattedStartTime = formatTimeToISO(startTime); + const formattedEndTime = formatTimeToISO(endTime); + + // Calculate duration if both start and end times are provided + let duration: string | undefined; + if (formattedStartTime && formattedEndTime) { + duration = calculateDuration(formattedStartTime, formattedEndTime); + } + + // Build the update data + const updateData: Partial = {}; + + if (childId !== undefined) updateData.child = childId; + if (formattedStartTime !== undefined) updateData.start = formattedStartTime; + if (formattedEndTime !== undefined) updateData.end = formattedEndTime; + if (duration !== undefined) updateData.duration = duration; + if (milestone !== undefined) updateData.milestone = milestone; + if (notes !== undefined) updateData.notes = notes; + + // Only proceed if there's something to update + if (Object.keys(updateData).length === 0) { + return { message: "No updates provided" }; + } + + try { + const updatedTummyTime = await api.updateTummyTime(tummyTimeId, updateData); + + await showToast({ + style: Toast.Style.Success, + title: "Tummy Time Updated", + message: `Updated tummy time #${tummyTimeId}`, + }); + + return updatedTummyTime; + } catch (error) { + await showFailureToast({ + title: "Error", + message: formatErrorMessage(error), + }); + } +} diff --git a/extensions/baby-buddy/src/tools/getActiveTimers.ts b/extensions/baby-buddy/src/tools/getActiveTimers.ts new file mode 100644 index 00000000000..6507d1f81c7 --- /dev/null +++ b/extensions/baby-buddy/src/tools/getActiveTimers.ts @@ -0,0 +1,30 @@ +import { BabyBuddyAPI, Timer } from "../api"; + +interface ActiveTimer extends Timer { + childName: string; +} + +/** + * Get all active timers + * + * For start time, show the time formatter hh:mm:ss with no padding zeros + * For duration, show the time in normal language like "1 hour, 30 minutes, 10 seconds" + */ +export default async function (): Promise { + try { + const api = new BabyBuddyAPI(); + const timers = await api.getActiveTimers(); + + // Enhance timers with child names + const enhancedTimers = await Promise.all( + timers.map(async (timer) => { + const childName = await api.getChildName(timer.child); + return { ...timer, childName }; + }), + ); + + return enhancedTimers; + } catch (error) { + throw new Error("Failed to fetch active timers"); + } +} diff --git a/extensions/baby-buddy/src/tools/getChildren.ts b/extensions/baby-buddy/src/tools/getChildren.ts new file mode 100644 index 00000000000..472fb89ba64 --- /dev/null +++ b/extensions/baby-buddy/src/tools/getChildren.ts @@ -0,0 +1,11 @@ +import { BabyBuddyAPI } from "../api"; + +export default async function () { + try { + const api = new BabyBuddyAPI(); + const children = await api.getChildren(); + return children; + } catch (error) { + throw new Error("Failed to fetch children"); + } +} diff --git a/extensions/baby-buddy/src/tools/getDateTime.ts b/extensions/baby-buddy/src/tools/getDateTime.ts new file mode 100644 index 00000000000..e906b3abe7e --- /dev/null +++ b/extensions/baby-buddy/src/tools/getDateTime.ts @@ -0,0 +1,8 @@ +/** + * Get the current date and time, useful if the user needs a relative time + * for example, if they ask "create a timer starting 10 minutes ago", use this + * tool to get the current time and subtract 10 minutes. + */ +export default function () { + return new Date().toLocaleString(); +} diff --git a/extensions/baby-buddy/src/tools/getDiapers.ts b/extensions/baby-buddy/src/tools/getDiapers.ts new file mode 100644 index 00000000000..1efe1360d0f --- /dev/null +++ b/extensions/baby-buddy/src/tools/getDiapers.ts @@ -0,0 +1,55 @@ +import { BabyBuddyAPI, DiaperEntry } from "../api"; +import { formatErrorMessage } from "../utils"; +import { findChildByName } from "../utils/normalizers"; + +type GetDiapersInput = { + /** + * The name of the child to get diaper change entries for + */ + childName: string; + /** + * Number of diaper change entries to retrieve (default: 10) + */ + limit?: number; + /** + * Whether to return only today's diaper change entries (default: false) + */ + todayOnly?: boolean; +}; + +export default async function getDiapers({ + childName, + limit = 10, + todayOnly = false, +}: GetDiapersInput): Promise<(DiaperEntry & { childName: string })[]> { + try { + const api = new BabyBuddyAPI(); + const children = await api.getChildren(); + + // Find child using the utility function + const child = findChildByName(children, childName); + + if (!child) { + throw new Error(`Child with name ${childName} not found`); + } + + let diapers: DiaperEntry[]; + + if (todayOnly) { + diapers = await api.getTodayDiapers(child.id); + } else { + // Get recent diaper change entries + diapers = await api.getRecentDiapers(child.id, limit); + } + + // Add child name to each diaper change entry + const enhancedDiapers = diapers.map((entry) => ({ + ...entry, + childName: `${child?.first_name || ""} ${child?.last_name || ""}`, + })); + + return enhancedDiapers; + } catch (error) { + throw new Error(formatErrorMessage(error)); + } +} diff --git a/extensions/baby-buddy/src/tools/getFeedings.ts b/extensions/baby-buddy/src/tools/getFeedings.ts new file mode 100644 index 00000000000..48e99cd64e6 --- /dev/null +++ b/extensions/baby-buddy/src/tools/getFeedings.ts @@ -0,0 +1,57 @@ +import { BabyBuddyAPI, FeedingEntry } from "../api"; +import { formatErrorMessage } from "../utils"; +import { findChildByName } from "../utils/normalizers"; + +type GetFeedingsInput = { + /** + * The name of the child to get feedings for + */ + childName: string; + /** + * Number of feedings to retrieve (default: 10) + */ + limit?: number; + /** + * Whether to return only today's feedings (default: false) + */ + todayOnly?: boolean; +}; + +export default async function getFeedings({ + childName, + limit = 10, + todayOnly = false, +}: GetFeedingsInput): Promise<(FeedingEntry & { childName: string })[]> { + try { + const api = new BabyBuddyAPI(); + const children = await api.getChildren(); + + // Find child using the utility function + const child = findChildByName(children, childName); + + if (!child) { + throw new Error(`Child with name ${childName} not found`); + } + + let feedings: FeedingEntry[]; + + if (todayOnly) { + feedings = await api.getTodayFeedings(child.id); + } else { + // Get recent feedings + limit = Math.max(limit, 1); + limit = Math.min(limit, 500); + feedings = await api.getRecentFeedings(child.id, limit); + } + + // Add child name to each feeding + const enhancedFeedings = feedings.map((feeding) => ({ + ...feeding, + childName: `${child.first_name} ${child.last_name}`, + })); + + return enhancedFeedings; + } catch (error) { + throw new Error(`Error fetching feedings: ${formatErrorMessage(error)}`); + } +} diff --git a/extensions/baby-buddy/src/tools/getSleep.ts b/extensions/baby-buddy/src/tools/getSleep.ts new file mode 100644 index 00000000000..e1dec7c2ae6 --- /dev/null +++ b/extensions/baby-buddy/src/tools/getSleep.ts @@ -0,0 +1,54 @@ +import { BabyBuddyAPI, SleepEntry } from "../api"; +import { findChildByName } from "../utils/normalizers"; + +type GetSleepInput = { + /** + * The name of the child to get sleep entries for + */ + childName: string; + /** + * Number of sleep entries to retrieve (default: 10) + */ + limit?: number; + /** + * Whether to return only today's sleep entries (default: false) + */ + todayOnly?: boolean; +}; + +export default async function getSleep({ + childName, + limit = 10, + todayOnly = false, +}: GetSleepInput): Promise<(SleepEntry & { childName: string })[]> { + try { + const api = new BabyBuddyAPI(); + const children = await api.getChildren(); + + // Find child using the utility function + const child = findChildByName(children, childName); + + if (!child) { + throw new Error(`Child with name ${childName} not found`); + } + + let sleep: SleepEntry[]; + + if (todayOnly) { + sleep = await api.getTodaySleep(child.id); + } else { + // Get recent sleep entries + sleep = await api.getRecentSleep(child.id, limit); + } + + // Add child name to each sleep entry + const enhancedSleep = sleep.map((entry) => ({ + ...entry, + childName: `${child.first_name} ${child.last_name}`, + })); + + return enhancedSleep; + } catch (error) { + throw new Error(`Error fetching sleep entries: ${error}`); + } +} diff --git a/extensions/baby-buddy/src/tools/getSleepingChildren.ts b/extensions/baby-buddy/src/tools/getSleepingChildren.ts new file mode 100644 index 00000000000..aa45f8c4dda --- /dev/null +++ b/extensions/baby-buddy/src/tools/getSleepingChildren.ts @@ -0,0 +1,31 @@ +import { BabyBuddyAPI } from "../api"; +import { formatErrorMessage } from "../utils"; + +export default async function () { + try { + const api = new BabyBuddyAPI(); + const activeTimers = await api.getActiveTimers(); + + // Filter for sleep timers + const sleepTimers = activeTimers.filter((timer) => timer.name.toLowerCase().includes("sleep")); + + // Get children details for each sleep timer + const children = await api.getChildren(); + + const sleepingChildren = sleepTimers + .map((timer) => { + const child = children.find((c) => c.id === timer.child); + return { + id: child?.id, + name: child?.first_name, + sleepingSince: timer.start, + timerId: timer.id, + }; + }) + .filter((child) => child.id !== undefined); + + return sleepingChildren; + } catch (error) { + throw new Error("Failed to fetch sleeping children: " + formatErrorMessage(error)); + } +} diff --git a/extensions/baby-buddy/src/tools/getTummyTime.ts b/extensions/baby-buddy/src/tools/getTummyTime.ts new file mode 100644 index 00000000000..e6efb15dc2f --- /dev/null +++ b/extensions/baby-buddy/src/tools/getTummyTime.ts @@ -0,0 +1,54 @@ +import { BabyBuddyAPI, TummyTimeEntry } from "../api"; +import { findChildByName } from "../utils/normalizers"; + +type GetTummyTimeInput = { + /** + * The name of the child to get tummy time entries for + */ + childName: string; + /** + * Number of tummy time entries to retrieve (default: 10) + */ + limit?: number; + /** + * Whether to return only today's tummy time entries (default: false) + */ + todayOnly?: boolean; +}; + +export default async function getTummyTime({ + childName, + limit = 10, + todayOnly = false, +}: GetTummyTimeInput): Promise<(TummyTimeEntry & { childName: string })[]> { + try { + const api = new BabyBuddyAPI(); + const children = await api.getChildren(); + + // Find child using the utility function + const child = findChildByName(children, childName); + + if (!child) { + throw new Error(`Child with name ${childName} not found`); + } + + let tummyTime: TummyTimeEntry[]; + + if (todayOnly) { + tummyTime = await api.getTodayTummyTime(child.id); + } else { + // Get recent tummy time entries + tummyTime = await api.getRecentTummyTime(child.id, limit); + } + + // Add child name to each tummy time entry + const enhancedTummyTime = tummyTime.map((entry) => ({ + ...entry, + childName: `${child.first_name} ${child.last_name}`, + })); + + return enhancedTummyTime; + } catch (error) { + throw new Error(`Error fetching tummy time entries: ${error}`); + } +} diff --git a/extensions/baby-buddy/src/tools/index.ts b/extensions/baby-buddy/src/tools/index.ts new file mode 100644 index 00000000000..3c458d3c2a8 --- /dev/null +++ b/extensions/baby-buddy/src/tools/index.ts @@ -0,0 +1,8 @@ +export { default as createDiaper } from "./createDiaper"; +export { default as createFeeding } from "./createFeeding"; +export { default as createSleep } from "./createSleep"; +export { default as createTimer } from "./createTimer"; +export { default as createTummyTime } from "./createTummyTime"; +export { default as getActiveTimers } from "./getActiveTimers"; +export { default as getChildren } from "./getChildren"; +export { default as getSleepingChildren } from "./getSleepingChildren"; diff --git a/extensions/baby-buddy/src/utils/api-helpers.ts b/extensions/baby-buddy/src/utils/api-helpers.ts new file mode 100644 index 00000000000..af8c6fa5aa3 --- /dev/null +++ b/extensions/baby-buddy/src/utils/api-helpers.ts @@ -0,0 +1,242 @@ +/** + * Utility functions for preparing and transforming API data + */ + +import { Child, DiaperEntry, FeedingEntry, SleepEntry } from "../api"; +import { TIMER_TYPES } from "./constants"; +import { calculateDuration, formatTimeToISO } from "./date-helpers"; + +/** + * Find a child by name in an array of children + */ +export function findChildByName(children: Child[], childName: string): Child | undefined { + // First try exact match + let child = children.find((c) => `${c.first_name} ${c.last_name}`.toLowerCase() === childName.toLowerCase()); + + // If no exact match, try by first name only + if (!child) { + child = children.find((c) => c.first_name.toLowerCase() === childName.toLowerCase()); + } + + // If still no match, try contains + if (!child) { + child = children.find((c) => `${c.first_name} ${c.last_name}`.toLowerCase().includes(childName.toLowerCase())); + } + + return child; +} + +/** + * Normalize feeding type values to match Baby Buddy API expectations + * Valid types: breast milk, formula, fortified breast milk, solid food + */ +export function normalizeType(type: string): string { + type = type.toLowerCase(); + + // Valid types in Baby Buddy: breast milk, formula, fortified breast milk, solid food + if (type.includes("breast") && type.includes("milk")) { + return "breast milk"; + } else if (type.includes("formula")) { + return "formula"; + } else if (type.includes("fortified")) { + return "fortified breast milk"; + } else if (type.includes("solid")) { + return "solid food"; + } + + // Default to breast milk if no match + return "breast milk"; +} + +/** + * Normalizes feeding method values to match Baby Buddy API expectations + * Valid methods: bottle, left breast, right breast, both breasts + */ +export function normalizeMethod(method: string): string { + method = method.toLowerCase(); + + // Valid methods in Baby Buddy: bottle, left breast, right breast, both breasts + if (method.includes("bottle")) { + return "bottle"; + } else if (method.includes("self")) { + return "self fed"; + } else if (method.includes("left")) { + return "left breast"; + } else if (method.includes("right")) { + return "right breast"; + } else if (method.includes("both")) { + return "both breasts"; + } else if (method.includes("breast")) { + // If just "breast" is specified, default to "both breasts" + return "both breasts"; + } + + // Default to bottle if no match + return "bottle"; +} + +/** + * Normalize diaper contents description to wet/solid values + */ +export function normalizeContents(contents: string): { wet: boolean; solid: boolean } { + contents = contents.toLowerCase(); + const wet = contents.includes("wet") || contents.includes("urine") || contents.includes("pee"); + const solid = contents.includes("solid") || contents.includes("poop") || contents.includes("bm"); + + return { wet, solid }; +} + +/** + * Get a description of diaper contents + */ +export function getContentsDescription(contents: { wet: boolean; solid: boolean }): string { + if (contents.wet && contents.solid) { + return "wet and solid"; + } else if (contents.wet) { + return "wet"; + } else if (contents.solid) { + return "solid"; + } else { + return "unknown"; + } +} + +/** + * Prepare diaper update data for API request + */ +export function prepareDiaperUpdateData(params: { + childId: number; + time?: string; + wet?: boolean; + solid?: boolean; + color?: string; + amount?: string | number; + notes?: string; +}): Partial & { child: number } { + const { childId, time, wet, solid, color, amount, notes } = params; + + const updateData: Partial & { child: number } = { child: childId }; + + if (time !== undefined) updateData.time = formatTimeToISO(time) || time; + if (wet !== undefined) updateData.wet = wet; + if (solid !== undefined) updateData.solid = solid; + + // Only include color if solid is true + if (solid && color !== undefined) updateData.color = color; + + // Convert amount to number if it's a string + if (amount !== undefined) { + updateData.amount = typeof amount === "string" ? parseFloat(amount) || null : amount; + } + + if (notes !== undefined) updateData.notes = notes; + + return updateData; +} + +/** + * Prepare feeding update data for API request + */ +export function prepareFeedingUpdateData(params: { + childId?: number; + startTime?: string; + endTime?: string; + type?: string; + method?: string; + amount?: string | number; + notes?: string; +}): Partial { + const { childId, startTime, endTime, type, method, amount, notes } = params; + + const updateData: Partial = {}; + + if (childId !== undefined) updateData.child = childId; + if (startTime !== undefined) updateData.start = formatTimeToISO(startTime) || startTime; + if (endTime !== undefined) updateData.end = formatTimeToISO(endTime) || endTime; + + // Calculate duration if both start and end times are provided + if (startTime && endTime) { + updateData.duration = calculateDuration(updateData.start as string, updateData.end as string); + } + + if (type !== undefined) updateData.type = normalizeType(type); + if (method !== undefined) updateData.method = normalizeMethod(method); + + // Convert amount to number if it's a string + if (amount !== undefined) { + updateData.amount = typeof amount === "string" ? parseFloat(amount) || null : amount; + } + + if (notes !== undefined) updateData.notes = notes; + + return updateData; +} + +/** + * Prepare sleep update data for API request + */ +export function prepareSleepUpdateData(params: { + childId?: number; + startTime?: string; + endTime?: string; + isNap?: boolean; + notes?: string; +}): Partial { + const { childId, startTime, endTime, isNap, notes } = params; + + const updateData: Partial = {}; + + if (childId !== undefined) updateData.child = childId; + if (startTime !== undefined) updateData.start = formatTimeToISO(startTime) || startTime; + if (endTime !== undefined) updateData.end = formatTimeToISO(endTime) || endTime; + + // Calculate duration if both start and end times are provided + if (startTime && endTime) { + updateData.duration = calculateDuration( + formatTimeToISO(startTime) || startTime, + formatTimeToISO(endTime) || endTime, + ); + } + + if (isNap !== undefined) updateData.nap = isNap; + if (notes !== undefined) updateData.notes = notes; + + return updateData; +} + +/** + * Prepare timer update data for API request + */ +export function prepareTimerUpdateData(params: { timerName?: string; startTime?: string; endTime?: string }): { + name?: string; + start?: string; + end?: string; +} { + const { timerName, startTime, endTime } = params; + + const updateData: { name?: string; start?: string; end?: string } = {}; + + if (timerName !== undefined) updateData.name = timerName; + if (startTime !== undefined) updateData.start = formatTimeToISO(startTime) || startTime; + if (endTime !== undefined) updateData.end = formatTimeToISO(endTime) || endTime; + + return updateData; +} + +/** + * Get a timer name based on type and optional custom name + */ +export function getTimerName(timerType: string, customName: string): string { + if (customName && customName.trim()) { + return customName.trim(); + } + + // Find the timer type in the constants + const timerTypeObj = TIMER_TYPES.find((t) => t.id === timerType); + + if (timerTypeObj) { + return timerTypeObj.name; + } + + return "Timer"; +} diff --git a/extensions/baby-buddy/src/utils/constants.ts b/extensions/baby-buddy/src/utils/constants.ts new file mode 100644 index 00000000000..e14297d4741 --- /dev/null +++ b/extensions/baby-buddy/src/utils/constants.ts @@ -0,0 +1,51 @@ +/** + * Diaper colors available in the Baby Buddy system + */ +export const DIAPER_COLORS = [ + { id: "black", name: "Black" }, + { id: "brown", name: "Brown" }, + { id: "green", name: "Green" }, + { id: "yellow", name: "Yellow" }, + { id: "white", name: "White" }, +]; + +/** + * Feeding types available in the Baby Buddy system + */ +export const FEEDING_TYPES = [ + { id: "breast milk", name: "Breast Milk" }, + { id: "formula", name: "Formula" }, + { id: "solid food", name: "Solid Food" }, + { id: "fortified breast milk", name: "Fortified Breast Milk" }, +]; + +/** + * Feeding methods available in the Baby Buddy system + */ +export const FEEDING_METHODS = [ + { id: "bottle", name: "Bottle" }, + { id: "left breast", name: "Left Breast" }, + { id: "right breast", name: "Right Breast" }, + { id: "both breasts", name: "Both Breasts" }, + { id: "parent fed", name: "Parent Fed" }, + { id: "self fed", name: "Self Fed" }, +]; + +/** + * Sleep types available in the Baby Buddy system + */ +export const SLEEP_TYPES = [ + { id: true, name: "Nap" }, + { id: false, name: "Night Sleep" }, +]; + +/** + * Common timer types in Baby Buddy + */ +export const TIMER_TYPES = [ + { id: "feeding", name: "Feeding" }, + { id: "pumping", name: "Pumping" }, + { id: "sleep", name: "Sleep" }, + { id: "tummy-time", name: "Tummy Time" }, + { id: "other", name: "Other" }, +]; diff --git a/extensions/baby-buddy/src/utils/date-helpers.ts b/extensions/baby-buddy/src/utils/date-helpers.ts new file mode 100644 index 00000000000..06fde7b96d8 --- /dev/null +++ b/extensions/baby-buddy/src/utils/date-helpers.ts @@ -0,0 +1,235 @@ +/** + * Utility functions for handling dates and times + */ + +import { differenceInSeconds, formatDistanceToNow, parseISO } from "date-fns"; + +/** + * Formats a time string to ISO format if provided + * Handles both ISO strings and HH:MM:SS format + * @param timeString - The time string to format + * @returns The formatted ISO string or undefined if no time string provided + */ +export function formatTimeToISO(timeString?: string): string | undefined { + if (!timeString) return undefined; + + // If it already looks like an ISO string, return it + if (timeString.includes("T") && timeString.includes("-") && timeString.includes(":")) { + return timeString; + } + + // If it's an HH:MM:SS format, convert to today's date with that time + if (timeString.includes(":") && timeString.split(":").length >= 2) { + const now = new Date(); + const parts = timeString.split(":"); + const hours = parseInt(parts[0], 10); + const minutes = parseInt(parts[1], 10); + const seconds = parts.length > 2 ? parseInt(parts[2], 10) : 0; + + now.setHours(hours); + now.setMinutes(minutes); + now.setSeconds(seconds); + + return now.toISOString(); + } + + // If it's a Date object that was stringified, parse it + try { + return new Date(timeString).toISOString(); + } catch (e) { + return undefined; + } +} + +/** + * Calculates duration between two times in HH:MM:SS format + * @param startTime - ISO string for the start time + * @param endTime - ISO string for the end time + * @returns Duration string in HH:MM:SS format or undefined if times are invalid + */ +export function calculateDuration(startTime?: string, endTime?: string): string | undefined { + if (!startTime || !endTime) return undefined; + + try { + const start = new Date(startTime); + const end = new Date(endTime); + + // Calculate the difference in milliseconds + const diff = end.getTime() - start.getTime(); + + // Convert to hours, minutes, seconds + const hours = Math.floor(diff / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((diff % (1000 * 60)) / 1000); + + // Format as HH:MM:SS + return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; + } catch (e) { + return undefined; + } +} + +/** + * Validates that end time is after start time + * @param startTime - The start time + * @param endTime - The end time + * @returns boolean indicating if the time range is valid + */ +export function validateTimeRange(startTime: Date, endTime: Date): boolean { + return endTime > startTime; +} + +/** + * Formats a time relative to now (e.g., "2 hours ago") + * @param dateString - ISO date string to format + * @returns Formatted string like "2 hours ago" + */ +export function formatTimeAgo(dateString: string | undefined): string { + if (!dateString) return "Unknown"; + try { + const date = parseISO(dateString); + return formatDistanceToNow(date, { addSuffix: true }); + } catch (error) { + console.error("Error parsing date:", error); + return "Invalid date"; + } +} + +/** + * Formats a date as a full date and time string + * @param dateString - ISO date string to format + * @returns Formatted date and time string + */ +export function formatFullTime(dateString: string | undefined): string { + if (!dateString) return "Unknown"; + try { + const date = parseISO(dateString); + // Format without leading zeros and with seconds + const hours = date.getHours() % 12 || 12; // Convert 0 to 12 for 12-hour format + const minutes = date.getMinutes(); + const seconds = date.getSeconds(); + const ampm = date.getHours() >= 12 ? "PM" : "AM"; + const month = date.getMonth() + 1; // getMonth() is zero-based + const day = date.getDate(); + const year = date.getFullYear(); + + return `${month}/${day}/${year} ${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")} ${ampm}`; + } catch (error) { + console.error("Error formatting full time:", error); + return "Invalid date"; + } +} + +/** + * Formats a time for display with a tooltip containing additional details + * @param dateString - ISO date string to format + * @returns Object with text and tooltip properties + */ +export function formatTimeWithTooltip(dateString: string | undefined): { text: string; tooltip: string } { + return { + text: formatTimeAgo(dateString), + tooltip: formatFullTime(dateString), + }; +} + +/** + * Format a duration string into a more readable format + * @param durationString Baby Buddy duration string in format "HH:MM:SS" + * @returns Formatted duration string like "2h 30m" + */ +export function formatDuration(durationString: string | undefined): string { + if (!durationString) return "Unknown"; + + // Baby Buddy durations are in format "HH:MM:SS" + const parts = durationString.split(":"); + if (parts.length !== 3) return durationString; + + const hours = parseInt(parts[0], 10); + const minutes = parseInt(parts[1], 10); + + if (hours > 0) { + return `${hours}h ${minutes}m`; + } else if (minutes > 0) { + return `${minutes}m`; + } else { + return parts[2] + "s"; + } +} + +/** + * Calculates and formats the elapsed time since a start time + * @param startTimeString - ISO string of the start time + * @returns Formatted elapsed time (e.g., "2h 30m 15s") + */ +export function calculateElapsedTime(startTimeString: string): string { + try { + const startTime = parseISO(startTimeString); + const now = new Date(); + const seconds = differenceInSeconds(now, startTime); + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + if (hours > 0) { + return `${hours}h ${minutes}m ${remainingSeconds}s`; + } else if (minutes > 0) { + return `${minutes}m ${remainingSeconds}s`; + } else { + return `${remainingSeconds}s`; + } + } catch (error) { + console.error("Error calculating elapsed time:", error); + return "Unknown"; + } +} + +/** + * Calculates and formats the precise elapsed time since a given date string. + * @param dateString - ISO date string + * @returns Formatted string like "1h 23m 45s ago" or "Invalid date" + */ +export function formatPreciseTimeAgo(dateString: string | undefined): string { + if (!dateString) return "Unknown"; + try { + const date = parseISO(dateString); + const now = new Date(); + const seconds = differenceInSeconds(now, date); + + if (seconds < 0) return "in the future"; // Handle cases where the date is in the future + if (seconds < 60) return `${seconds}s ago`; + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + let result = ""; + if (hours > 0) { + result += `${hours}h `; + } + if (minutes > 0) { + result += `${minutes}m `; + } + if (remainingSeconds > 0 || (hours === 0 && minutes === 0)) { + // Show seconds if less than a minute or if needed for precision when minutes/hours are present + result += `${remainingSeconds}s`; + } + + return result.trim() + " ago"; + } catch (error) { + return "Invalid date"; + } +} + +/** + * Gets the start of today and end of today for filtering activities + */ +export function getTodayDateRange(): { start: Date; end: Date } { + const todayStart = new Date(); + todayStart.setHours(0, 0, 0, 0); + + const todayEnd = new Date(todayStart); + todayEnd.setDate(todayEnd.getDate() + 1); + + return { start: todayStart, end: todayEnd }; +} diff --git a/extensions/baby-buddy/src/utils/diaper-helpers.ts b/extensions/baby-buddy/src/utils/diaper-helpers.ts new file mode 100644 index 00000000000..bfc2f23319f --- /dev/null +++ b/extensions/baby-buddy/src/utils/diaper-helpers.ts @@ -0,0 +1,12 @@ +/** + * Get a description of diaper contents (wet, solid or both) + * @param wet Whether the diaper is wet + * @param solid Whether the diaper is solid + * @returns A description of the diaper contents + */ +export function getDiaperDescription(wet: boolean, solid: boolean): string { + if (wet && solid) return "wet and solid"; + if (wet) return "wet"; + if (solid) return "solid"; + return ""; +} diff --git a/extensions/baby-buddy/src/utils/form-helpers.ts b/extensions/baby-buddy/src/utils/form-helpers.ts new file mode 100644 index 00000000000..abd696657ca --- /dev/null +++ b/extensions/baby-buddy/src/utils/form-helpers.ts @@ -0,0 +1,135 @@ +/** + * DEPRECATED: This file exists for backwards compatibility + * These functions have been moved to specialized utilities + * Import directly from utils/index.ts (or specific utility files) for new code + */ + +// Import the needed functions from their new homes +import { + findChildByName, + getContentsDescription, + getTimerName, + normalizeContents, + normalizeMethod, + normalizeType, + prepareDiaperUpdateData, + prepareFeedingUpdateData, + prepareSleepUpdateData, + prepareTimerUpdateData, +} from "./api-helpers"; +import { calculateDuration, formatTimeToISO, validateTimeRange } from "./date-helpers"; +import { formatErrorMessage } from "./formatters"; +import { isValidDiaperType, showInvalidTimeRangeError, validateDiaperForm } from "./validators"; + +// Re-export them all +export { + calculateDuration, + findChildByName, + formatErrorMessage, + formatTimeToISO, + getContentsDescription, + getTimerName, + isValidDiaperType, + normalizeContents, + normalizeMethod, + normalizeType, + prepareDiaperUpdateData, + prepareFeedingUpdateData, + prepareSleepUpdateData, + prepareTimerUpdateData, + showInvalidTimeRangeError, + validateDiaperForm, + validateTimeRange, +}; + +// Handle the remaining functions that haven't been moved yet +// These will need to be refactored or moved in a future update + +/** + * Create feeding data for API submission + */ +export function createFeedingData(params: { + childId: number; + startTime?: string; + endTime?: string; + type: string; + method: string; + amount?: string; + notes?: string; +}) { + // Just pass through to the new utility function + return prepareFeedingUpdateData(params); +} + +/** + * Create sleep data for API submission + */ +export function createSleepData(params: { + childId: number; + startTime?: string; + endTime?: string; + isNap?: boolean; + notes?: string; +}) { + // Just pass through to the new utility function + return prepareSleepUpdateData(params); +} + +/** + * Create diaper data for API submission + */ +export function formatDiaperData(params: { + childId: number; + time: Date; + isWet: boolean; + isSolid: boolean; + color: string; + amount: string; + notes: string; +}) { + return prepareDiaperUpdateData({ + childId: params.childId, + time: params.time.toISOString(), + wet: params.isWet, + solid: params.isSolid, + color: params.color, + amount: params.amount, + notes: params.notes, + }); +} + +/** + * Format diaper data from contents string + */ +export function formatDiaperDataFromContents(params: { + childId: number; + time: string; + contents: string; + color: string; + amount: string; + notes: string; +}) { + const { wet, solid } = normalizeContents(params.contents); + + return formatDiaperData({ + childId: params.childId, + time: new Date(params.time), + isWet: wet, + isSolid: solid, + color: params.color, + amount: params.amount, + notes: params.notes, + }); +} + +/** + * Create timer data for API submission + */ +export function createTimerData(params: { childId: number; name: string; startTime?: Date }) { + return { + child: params.childId, + name: params.name, + start: params.startTime ? params.startTime.toISOString() : new Date().toISOString(), + active: true, + }; +} diff --git a/extensions/baby-buddy/src/utils/formatters.ts b/extensions/baby-buddy/src/utils/formatters.ts new file mode 100644 index 00000000000..fd5589ac219 --- /dev/null +++ b/extensions/baby-buddy/src/utils/formatters.ts @@ -0,0 +1,123 @@ +/** + * Utility functions for formatting data for display or API submission + */ + +import axios from "axios"; +import { DiaperEntry } from "../api"; + +/** + * Formats an error message for display in toast notifications + */ +export function formatErrorMessage(error: unknown): string { + if (typeof error === "string") { + return error; + } + + // Handle axios errors + if (axios.isAxiosError(error) && error.response?.data) { + const errorData = error.response.data; + + if (typeof errorData === "string") { + return errorData; + } + + if (typeof errorData === "object" && errorData !== null) { + // Extract error messages from object + return Object.entries(errorData) + .map(([key, value]) => `${key}: ${value}`) + .join(", "); + } + + return error.message; + } + + // Handle generic errors with message property + if (error instanceof Error) { + return error.message; + } + + return "An unknown error occurred"; +} + +/** + * Format a diaper entry for display + */ +export function formatDiaperDescription(diaper: DiaperEntry): string { + const parts = []; + + // Build type description + if (diaper.wet && diaper.solid) { + parts.push("wet and solid"); + } else if (diaper.wet) { + parts.push("wet"); + } else if (diaper.solid) { + parts.push("solid"); + } + + // Add color if solid + if (diaper.solid && diaper.color) { + parts.push(`${diaper.color}`); + } + + // Add amount if present + if (diaper.amount) { + parts.push(`amount: ${diaper.amount}`); + } + + return parts.join(", "); +} + +/** + * Format a duration in minutes to a human-readable string + */ +export function formatMinutesToFullDuration(minutes: number): string { + if (minutes < 60) { + return `${minutes}m`; + } + + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + + if (remainingMinutes === 0) { + return `${hours}h`; + } + + return `${hours}h ${remainingMinutes}m`; +} + +/** + * Format a child's age based on birth date + */ +export function formatAge(birthDate: string): string { + const birth = new Date(birthDate); + if (isNaN(birth.getTime())) { + return "Invalid birth date"; + } + const now = new Date(); + // Calculate difference in milliseconds + const diffMs = now.getTime() - birth.getTime(); + + // Convert to days + const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (days < 30) { + return `${days} days`; + } + + // Convert to months + const months = Math.floor(days / 30.44); + + if (months < 24) { + return `${months} months`; + } + + // Convert to years + const years = Math.floor(months / 12); + const remainingMonths = months % 12; + + if (remainingMonths === 0) { + return `${years} years`; + } + + return `${years} years, ${remainingMonths} months`; +} diff --git a/extensions/baby-buddy/src/utils/index.ts b/extensions/baby-buddy/src/utils/index.ts new file mode 100644 index 00000000000..afdec40310a --- /dev/null +++ b/extensions/baby-buddy/src/utils/index.ts @@ -0,0 +1,16 @@ +/** + * Main exports for utilities + * + * This file re-exports all utility functions from their specialized modules, + * making them available in one place for backward compatibility. + */ + +// Re-export everything from all utility files +export * from "./api-helpers"; +export * from "./constants"; +export * from "./date-helpers"; +export * from "./diaper-helpers"; +export * from "./form-helpers"; // For backwards compatibility +export * from "./formatters"; +export * from "./statistics"; +export * from "./validators"; diff --git a/extensions/baby-buddy/src/utils/normalizers.ts b/extensions/baby-buddy/src/utils/normalizers.ts new file mode 100644 index 00000000000..988d92a4d9d --- /dev/null +++ b/extensions/baby-buddy/src/utils/normalizers.ts @@ -0,0 +1,152 @@ +/** + * Utility functions for normalizing values for Baby Buddy API + */ + +import { Child } from "../api"; + +/** + * Normalizes feeding type values to match Baby Buddy API expectations + * Valid types: breast milk, formula, fortified breast milk, solid food + */ +export function normalizeType(type: string): string { + type = type.toLowerCase(); + + // Valid types in Baby Buddy: breast milk, formula, fortified breast milk, solid food + if (type.includes("breast") && type.includes("milk")) { + return "breast milk"; + } else if (type.includes("formula")) { + return "formula"; + } else if (type.includes("fortified")) { + return "fortified breast milk"; + } else if (type.includes("solid")) { + return "solid food"; + } + + // Default to breast milk if no match + return "breast milk"; +} + +/** + * Normalizes feeding method values to match Baby Buddy API expectations + * Valid methods: bottle, left breast, right breast, both breasts + */ +export function normalizeMethod(method: string): string { + method = method.toLowerCase(); + + // Valid methods in Baby Buddy: bottle, left breast, right breast, both breasts + if (method.includes("bottle")) { + return "bottle"; + } else if (method.includes("self")) { + return "self fed"; + } else if (method.includes("left")) { + return "left breast"; + } else if (method.includes("right")) { + return "right breast"; + } else if (method.includes("both")) { + return "both breasts"; + } else if (method.includes("breast")) { + // If just "breast" is specified, default to "both breasts" + return "both breasts"; + } + + // Default to bottle if no match + return "bottle"; +} + +/** + * Formats a time string to ISO format + * Handles HH:MM:SS format and ISO format + */ +export function formatTimeToISO(timeString?: string): string | undefined { + if (!timeString) return undefined; + + if (timeString.includes("T") && timeString.includes("-")) { + // Already in ISO format + return timeString; + } else if (timeString.includes(":")) { + // HH:MM:SS or HH:MM format + const today = new Date(); + const [hours, minutes, seconds = "00"] = timeString.split(":").map((part) => part.trim()); + + today.setHours(parseInt(hours, 10)); + today.setMinutes(parseInt(minutes, 10)); + today.setSeconds(parseInt(seconds, 10)); + today.setMilliseconds(0); + + return today.toISOString(); + } + + return undefined; +} + +/** + * Calculates duration between two dates in HH:MM:SS format + */ +export function calculateDuration(startTime?: string, endTime?: string): string | undefined { + if (!startTime || !endTime) return undefined; + + try { + const startDate = new Date(startTime); + const endDate = new Date(endTime); + const durationMs = endDate.getTime() - startDate.getTime(); + + const durationHours = Math.floor(durationMs / (1000 * 60 * 60)); + const durationMinutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)); + const durationSeconds = Math.floor((durationMs % (1000 * 60)) / 1000); + + return `${durationHours.toString().padStart(2, "0")}:${durationMinutes.toString().padStart(2, "0")}:${durationSeconds.toString().padStart(2, "0")}`; + } catch { + return undefined; + } +} + +/** + * Finds a child by name with flexible matching + * @param children - List of children to search + * @param childName - Name to match (can be first name, last name, or full name) + */ +export function findChildByName(children: Child[], childName: string): Child | undefined { + return children.find( + (c) => + c.first_name.toLowerCase() === childName.toLowerCase() || + c.first_name.toLowerCase().includes(childName.toLowerCase()) || + `${c.first_name} ${c.last_name}`.toLowerCase() === childName.toLowerCase() || + `${c.first_name} ${c.last_name}`.toLowerCase().includes(childName.toLowerCase()), + ); +} + +/** + * Normalizes diaper contents values + * @param contents - Description of the diaper contents (wet, solid, both) + * @returns Object with wet and solid boolean properties + */ +export function normalizeContents(contents: string): { wet: boolean; solid: boolean } { + contents = contents.toLowerCase(); + + if (contents.includes("both")) { + return { wet: true, solid: true }; + } else if (contents.includes("wet")) { + return { wet: true, solid: false }; + } else if (contents.includes("solid") || contents.includes("poop") || contents.includes("bm")) { + return { wet: false, solid: true }; + } + + // Default to wet if no match + return { wet: true, solid: false }; +} + +/** + * Gets a human-readable description of diaper contents + * @param contents - Object with wet and solid boolean properties + * @returns String description of the contents + */ +export function getContentsDescription(contents: { wet: boolean; solid: boolean }): string { + if (contents.wet && contents.solid) { + return "wet and solid"; + } else if (contents.wet) { + return "wet"; + } else if (contents.solid) { + return "solid"; + } + return "unknown"; +} diff --git a/extensions/baby-buddy/src/utils/statistics.ts b/extensions/baby-buddy/src/utils/statistics.ts new file mode 100644 index 00000000000..668b226c341 --- /dev/null +++ b/extensions/baby-buddy/src/utils/statistics.ts @@ -0,0 +1,94 @@ +/** + * Utility functions for calculating statistics and metrics + */ + +import { DiaperEntry, FeedingEntry, SleepEntry, TummyTimeEntry } from "../api"; + +/** + * Calculate total feeding amount + */ +export function calculateTotalFeedingAmount(feedings: FeedingEntry[]): number { + return feedings.reduce((total, feeding) => { + return total + (feeding.amount || 0); + }, 0); +} + +/** + * Calculate total sleep duration in minutes + */ +export function calculateTotalSleepMinutes(sleep: SleepEntry[]): number { + return sleep.reduce((total, entry) => { + // Parse the duration string (format: "HH:MM:SS") + const [hours, minutes, seconds] = entry.duration.split(":").map(Number); + return Math.round(total + (hours * 60 + minutes + seconds / 60)); + }, 0); +} + +/** + * Calculate total tummy time duration in minutes + */ +export function calculateTotalTummyTimeMinutes(tummyTime: TummyTimeEntry[]): number { + return tummyTime.reduce((total, entry) => { + // Parse the duration string (format: "HH:MM:SS") + const [hours, minutes] = entry.duration.split(":").map(Number); + return total + (hours * 60 + minutes); + }, 0); +} + +/** + * Count wet diapers + */ +export function countWetDiapers(diapers: DiaperEntry[]): number { + return diapers.filter((diaper) => diaper.wet).length; +} + +/** + * Count solid diapers + */ +export function countSolidDiapers(diapers: DiaperEntry[]): number { + return diapers.filter((diaper) => diaper.solid).length; +} + +/** + * Calculate total diaper amount + */ +export function calculateTotalDiaperAmount(diapers: DiaperEntry[]): number { + return diapers.reduce((total, diaper) => { + return total + (diaper.amount || 0); + }, 0); +} + +/** + * Calculate age in years and months + */ +export function calculateAge(birthDate: string): string { + const birth = new Date(birthDate); + const now = new Date(); + + // Calculate difference in milliseconds + const diffMs = now.getTime() - birth.getTime(); + + // Convert to days + const days = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (days < 30) { + return `${days} days`; + } + + // Convert to months + const months = Math.floor(days / 30.44); + + if (months < 24) { + return `${months} months`; + } + + // Convert to years + const years = Math.floor(months / 12); + const remainingMonths = months % 12; + + if (remainingMonths === 0) { + return `${years} years`; + } + + return `${years} years, ${remainingMonths} months`; +} diff --git a/extensions/baby-buddy/src/utils/validators.ts b/extensions/baby-buddy/src/utils/validators.ts new file mode 100644 index 00000000000..ae3c7015596 --- /dev/null +++ b/extensions/baby-buddy/src/utils/validators.ts @@ -0,0 +1,40 @@ +/** + * Utility functions for validating user input + */ + +import { showToast, Toast } from "@raycast/api"; +import { showFailureToast } from "@raycast/utils"; + +/** + * Validates that a diaper entry has at least wet or solid + */ +export function isValidDiaperType(wet: boolean, solid: boolean): boolean { + return wet || solid; +} + +/** + * Validates diaper form data and shows a toast if invalid + * @returns boolean indicating if the data is valid + */ +export function validateDiaperForm(isWet: boolean, isSolid: boolean): boolean { + // Validate that at least one of wet or solid is selected + if (!isValidDiaperType(isWet, isSolid)) { + showFailureToast({ + title: "Invalid diaper change", + message: "At least one of Wet or Solid must be selected", + }); + return false; + } + return true; +} + +/** + * Shows a toast for invalid time range error + */ +export function showInvalidTimeRangeError(): void { + showToast({ + style: Toast.Style.Failure, + title: "Invalid time range", + message: "End time must be after start time", + }); +} diff --git a/extensions/baby-buddy/tsconfig.json b/extensions/baby-buddy/tsconfig.json new file mode 100644 index 00000000000..abb15806bec --- /dev/null +++ b/extensions/baby-buddy/tsconfig.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "include": ["src/**/*", "raycast-env.d.ts"], + "compilerOptions": { + "lib": ["ES2023"], + "module": "commonjs", + "target": "ES2022", + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "resolveJsonModule": true + } +}