From 9693f5b4d9629f7083f1cb8d46004a2b5298a4e8 Mon Sep 17 00:00:00 2001 From: Raj Gara Date: Fri, 17 Oct 2025 00:16:32 -0400 Subject: [PATCH] Splitly - expense splitting app with OCR receipt scanning --- .gitignore | 6 + .meteor/.finished-upgraders | 19 + .meteor/.gitignore | 1 + .meteor/.id | 7 + .meteor/packages | 18 + .meteor/platforms | 2 + .meteor/release | 1 + .meteor/versions | 76 + .meteorignore | 3 + README.md | 128 +- client/main.html | 9 + client/main.js | 17 + client/styles/splitPage.css | 94 + config/app.config.json | 9 + eslint.config.mjs | 53 + imports/api/bills.ts | 559 +++ imports/api/models.ts | 68 + imports/api/publications.ts | 11 + imports/api/users.ts | 52 + imports/api/utils.js | 33 + imports/infra/indexedDb.ts | 28 + imports/startup/client/routes.js | 34 + imports/ui/blaze/components/userModal.html | 53 + imports/ui/blaze/components/userModal.js | 171 + imports/ui/blaze/layout.html | 59 + imports/ui/blaze/layout.js | 139 + imports/ui/blaze/pages/analysis.html | 82 + imports/ui/blaze/pages/analysis.js | 122 + imports/ui/blaze/pages/billDetail.html | 116 + imports/ui/blaze/pages/billDetail.js | 78 + imports/ui/blaze/pages/dashboard.html | 105 + imports/ui/blaze/pages/dashboard.js | 496 +++ imports/ui/blaze/pages/history.html | 86 + imports/ui/blaze/pages/history.js | 124 + imports/ui/blaze/pages/settings.html | 61 + imports/ui/blaze/pages/settings.js | 82 + imports/ui/blaze/pages/splitPage.html | 202 + imports/ui/blaze/pages/splitPage.js | 252 ++ package-lock.json | 4465 ++++++++++++++++++++ package.json | 31 + public/favicon.svg | 7 + server/main.ts | 8 + types/meteor.d.ts | 52 + 43 files changed, 8017 insertions(+), 2 deletions(-) create mode 100644 .meteor/.finished-upgraders create mode 100644 .meteor/.gitignore create mode 100644 .meteor/.id create mode 100644 .meteor/packages create mode 100644 .meteor/platforms create mode 100644 .meteor/release create mode 100644 .meteor/versions create mode 100644 .meteorignore create mode 100644 client/main.html create mode 100644 client/main.js create mode 100644 client/styles/splitPage.css create mode 100644 config/app.config.json create mode 100644 eslint.config.mjs create mode 100644 imports/api/bills.ts create mode 100644 imports/api/models.ts create mode 100644 imports/api/publications.ts create mode 100644 imports/api/users.ts create mode 100644 imports/api/utils.js create mode 100644 imports/infra/indexedDb.ts create mode 100644 imports/startup/client/routes.js create mode 100644 imports/ui/blaze/components/userModal.html create mode 100644 imports/ui/blaze/components/userModal.js create mode 100644 imports/ui/blaze/layout.html create mode 100644 imports/ui/blaze/layout.js create mode 100644 imports/ui/blaze/pages/analysis.html create mode 100644 imports/ui/blaze/pages/analysis.js create mode 100644 imports/ui/blaze/pages/billDetail.html create mode 100644 imports/ui/blaze/pages/billDetail.js create mode 100644 imports/ui/blaze/pages/dashboard.html create mode 100644 imports/ui/blaze/pages/dashboard.js create mode 100644 imports/ui/blaze/pages/history.html create mode 100644 imports/ui/blaze/pages/history.js create mode 100644 imports/ui/blaze/pages/settings.html create mode 100644 imports/ui/blaze/pages/settings.js create mode 100644 imports/ui/blaze/pages/splitPage.html create mode 100644 imports/ui/blaze/pages/splitPage.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/favicon.svg create mode 100644 server/main.ts create mode 100644 types/meteor.d.ts diff --git a/.gitignore b/.gitignore index 9a5aced..0230d63 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,12 @@ build/Release node_modules/ jspm_packages/ +# Meteor local build artifacts +.meteor/local + +# macOS Finder metadata +.DS_Store + # Snowpack dependency directory (https://snowpack.dev/) web_modules/ diff --git a/.meteor/.finished-upgraders b/.meteor/.finished-upgraders new file mode 100644 index 0000000..c07b6ff --- /dev/null +++ b/.meteor/.finished-upgraders @@ -0,0 +1,19 @@ +# This file contains information which helps Meteor properly upgrade your +# app when you run 'meteor update'. You should check it into version control +# with your project. + +notices-for-0.9.0 +notices-for-0.9.1 +0.9.4-platform-file +notices-for-facebook-graph-api-2 +1.2.0-standard-minifiers-package +1.2.0-meteor-platform-split +1.2.0-cordova-changes +1.2.0-breaking-changes +1.3.0-split-minifiers-package +1.4.0-remove-old-dev-bundle-link +1.4.1-add-shell-server-package +1.4.3-split-account-service-packages +1.5-add-dynamic-import-package +1.7-split-underscore-from-meteor-base +1.8.3-split-jquery-from-blaze diff --git a/.meteor/.gitignore b/.meteor/.gitignore new file mode 100644 index 0000000..4083037 --- /dev/null +++ b/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/.meteor/.id b/.meteor/.id new file mode 100644 index 0000000..108a07e --- /dev/null +++ b/.meteor/.id @@ -0,0 +1,7 @@ +# This file contains a token that is unique to your project. +# Check it into your repository along with the rest of this directory. +# It can be used for purposes such as: +# - ensuring you don't accidentally deploy one app on top of another +# - providing package authors with aggregated statistics + +f13qjxma99ghm.gasvfpq72l4a diff --git a/.meteor/packages b/.meteor/packages new file mode 100644 index 0000000..a0ea823 --- /dev/null +++ b/.meteor/packages @@ -0,0 +1,18 @@ +# Meteor packages used by this project +# Each line is a package name followed by an optional version constraint. +# See https://docs.meteor.com/ for more info. + +meteor-base@1.5.2 +mongo@2.1.4 +blaze-html-templates +reactive-var@1.0.13 +ostrio:flow-router-extra +tracker@1.3.4 +ecmascript@0.16.13 +typescript@5.6.6 +jquery +standard-minifier-css +standard-minifier-js +shell-server +dynamic-import +underscore diff --git a/.meteor/platforms b/.meteor/platforms new file mode 100644 index 0000000..8a3a35f --- /dev/null +++ b/.meteor/platforms @@ -0,0 +1,2 @@ +browser +server diff --git a/.meteor/release b/.meteor/release new file mode 100644 index 0000000..4876d6f --- /dev/null +++ b/.meteor/release @@ -0,0 +1 @@ +METEOR@3.3.2 diff --git a/.meteor/versions b/.meteor/versions new file mode 100644 index 0000000..9bcd858 --- /dev/null +++ b/.meteor/versions @@ -0,0 +1,76 @@ +allow-deny@2.1.0 +autoupdate@2.0.1 +babel-compiler@7.12.2 +babel-runtime@1.5.2 +base64@1.0.13 +binary-heap@1.0.12 +blaze@3.0.2 +blaze-html-templates@3.0.0 +blaze-tools@2.0.0 +boilerplate-generator@2.0.2 +caching-compiler@2.0.1 +caching-html-compiler@2.0.0 +callback-hook@1.6.1 +check@1.4.4 +core-runtime@1.0.0 +ddp@1.4.2 +ddp-client@3.1.1 +ddp-common@1.4.4 +ddp-server@3.1.2 +diff-sequence@1.1.3 +dynamic-import@0.7.4 +ecmascript@0.16.13 +ecmascript-runtime@0.8.3 +ecmascript-runtime-client@0.12.3 +ecmascript-runtime-server@0.11.1 +ejson@1.1.5 +es5-shim@4.8.1 +facts-base@1.0.2 +fetch@0.1.6 +geojson-utils@1.0.12 +hot-code-push@1.0.5 +html-tools@2.0.0 +htmljs@2.0.1 +id-map@1.2.0 +inter-process-messaging@0.1.2 +jquery@3.0.2 +logging@1.3.6 +meteor@2.1.1 +meteor-base@1.5.2 +minifier-css@2.0.1 +minifier-js@3.0.4 +minimongo@2.0.4 +modern-browsers@0.2.3 +modules@0.20.3 +modules-runtime@0.13.2 +mongo@2.1.4 +mongo-decimal@0.2.0 +mongo-dev-server@1.1.1 +mongo-id@1.0.9 +npm-mongo@6.16.1 +observe-sequence@2.0.0 +ordered-dict@1.2.0 +ostrio:flow-router-extra@3.12.1 +promise@1.0.0 +random@1.2.2 +react-fast-refresh@0.2.9 +reactive-dict@1.3.2 +reactive-var@1.0.13 +reload@1.3.2 +retry@1.1.1 +routepolicy@1.1.2 +shell-server@0.6.2 +socket-stream-client@0.6.1 +spacebars@2.0.0 +spacebars-compiler@2.0.0 +standard-minifier-css@1.9.3 +standard-minifier-js@3.1.1 +templating@1.4.4 +templating-compiler@2.0.0 +templating-runtime@2.0.1 +templating-tools@2.0.0 +tracker@1.3.4 +typescript@5.6.6 +underscore@1.6.4 +webapp@2.0.7 +webapp-hashing@1.1.2 diff --git a/.meteorignore b/.meteorignore new file mode 100644 index 0000000..e00924d --- /dev/null +++ b/.meteorignore @@ -0,0 +1,3 @@ +# Exclude linting and dev tooling configs from Meteor build +eslint.config.mjs +/.vscode/ \ No newline at end of file diff --git a/README.md b/README.md index 330feac..9ffce78 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,126 @@ -# Splitly -Scan, Split & Simplify Shared Expenses +# Splitly (Meteor Expense Splitting App) + +Offline-friendly expense splitting app built with Meteor + Blaze + Bootstrap. + +## Features + +- ✅ Create and manage expense bills with multiple users +- ✅ Split expenses equally among selected users per item +- ✅ Add/remove users dynamically in edit mode +- ✅ OCR receipt scanning with Tesseract.js +- ✅ Offline support with IndexedDB caching +- ✅ Responsive mobile-first design +- ✅ Real-time updates with Meteor reactivity +- ✅ Bill history and analysis views +- ✅ Settings management + +## Structure (flattened root) +``` +.meteor/ # Meteor internal config +package.json # App dependencies +config/app.config.json # Feature flags & provider stubs +client/main.js # Client entrypoint (imports routes) +server/main.ts # Server startup & publications import +imports/ + api/ # Collections, models, publications, methods + models.ts # Data interfaces + computeExpenseSummary() + bills.ts # Bills collection + Meteor methods + publications.ts # Meteor.publish definitions + infra/ + indexedDb.ts # Offline cache (IndexedDB via idb) + startup/client/routes.js # FlowRouter + BlazeLayout route definitions + ui/blaze/ # Blaze UI layer + layout.{html,js} # Main layout (navbar, alerts, spinner) + pages/ # Dashboard, NewBill, History, Analysis, BillDetail, Settings +types/ (legacy shims) # Ambient TS shims (React removed) +``` + +## Core Features +| Feature | Status | +|---------|--------| +| Routing & Navigation (FlowRouter + Blaze) | ✅ | +| MongoDB persistence | ✅ | +| IndexedDB offline cache (flagged) | ✅ (basic sync) | +| Feature flags (config + localStorage overrides) | ✅ | +| Equal / Percent / Fixed splits (UI + logic) | ✅ (percent validates ~100%) | +| Share editor per item | ✅ | +| OCR stub ingestion (textarea parse) | ✅ (stub; real OCR pending) | +| Multi-bill history + detail view | ✅ | +| Analysis page (filter + inline bar) | ✅ (charts enhancement pending) | +| Settings page | ✅ | +| Alerts (success/error toasts, Blaze) | ✅ | +| Loading indicators (initial sync / OCR) | ✅ | + +## Data Model Summary +- UserProfile: `{ id, name, contact?, preferences? }` +- Item: `{ id, name, price, userIds[], splitType?, shares? }` where `splitType` in `equal|percent|fixed` and `shares` entries each carry `type` + `value`. +- BillDoc: `{ _id?, createdAt, updatedAt?, users[], items[], currency? }` +- ExpenseSummary: derived totals per user; percent shares auto-scaled if not exactly 100%; fixed remainder distributed evenly. + +## Offline Flow +1. On initial load (subscription not ready & empty Minimongo): load cached bills from IndexedDB. +2. When subscription becomes ready: cache latest bills back to IndexedDB. +3. Future: diff/merge strategy & stale detection. + +## Running + +```bash +# Install dependencies +meteor npm install + +# Start development server +npm start +# or +meteor run + +# Lint code +npm run lint + +# Fix lint issues automatically +npm run lint:fix + +# Build for production +npm run build + +# Deploy to Meteor servers (optional) +npm run deploy +``` + +Open: http://localhost:3000 + +## Configuration Flags +`config/app.config.json` +```json +{ + "features": { "indexedDbSync": true, "analysisPage": true }, + "api": { "ocrProvider": "stub" } +} +``` +Disable Analysis Page by setting `analysisPage: false`. + +Feature flags are read in Blaze templates (`layout.js`, `settings.js`) via localStorage overrides (`flag_`). A refresh applies route gating changes. + +## Next Enhancements +1. Real OCR (image/PDF upload + provider integration). +2. User rename + reuse across bills; bill title field. +3. More robust percent/fixed validation UI (visual totals meter). +4. Advanced Analysis (pie chart, time series, multi-bill aggregates). +5. Offline merge conflict & stale detection strategy. +6. Tests: unit (computeExpenseSummary), methods validation, offline loader. +7. Decide on TypeScript retention vs full JS (Blaze code uses plain JS modules; models/methods remain TS for now). +8. Import/export (CSV), multi-currency support. +9. Authentication & per-user ownership. +10. Performance tuning & Lighthouse pass. + +## Contributing Guidelines +- Keep features incremental; one page / one method at a time. +- Maintain data model single source (`models.ts`). +- Add tests alongside new logic. +- Avoid silent failures; surface errors in UI. +- Remove temporary type shims once real types integrated. + +## License +TBD (add license file if distributing publicly). + +--- +Legacy React layer removed; repository now focuses on a lean Meteor + Blaze implementation. diff --git a/client/main.html b/client/main.html new file mode 100644 index 0000000..62cc83c --- /dev/null +++ b/client/main.html @@ -0,0 +1,9 @@ + + Splitly - Split Bills & Expenses + + + + + +
+ diff --git a/client/main.js b/client/main.js new file mode 100644 index 0000000..43ba306 --- /dev/null +++ b/client/main.js @@ -0,0 +1,17 @@ +import 'bootstrap/dist/css/bootstrap.min.css'; +// Import Bootstrap components individually to ensure they're available +import { Modal, Collapse, Dropdown } from 'bootstrap'; +import { Meteor } from 'meteor/meteor'; + +// Make Bootstrap components available globally +Meteor.startup(() => { + if (typeof window !== 'undefined') { + window.bootstrap = window.bootstrap || {}; + window.bootstrap.Modal = Modal; + window.bootstrap.Collapse = Collapse; + window.bootstrap.Dropdown = Dropdown; + } +}); + +// Blaze layout & routes are defined under imports/startup/client. +import '/imports/startup/client/routes'; diff --git a/client/styles/splitPage.css b/client/styles/splitPage.css new file mode 100644 index 0000000..c01614a --- /dev/null +++ b/client/styles/splitPage.css @@ -0,0 +1,94 @@ +/* Mobile-first Split Page Styles */ + +/* Item Cards */ +.item-card { + transition: background-color 0.2s; +} + +.item-card:hover { + background-color: #f8f9fa; +} + +.item-number { + font-size: 0.875rem; + font-weight: 600; + color: #6c757d; + min-width: 32px; + flex-shrink: 0; +} + +.item-name { + flex-grow: 1; +} + +.item-price { + font-size: 0.95rem; + font-weight: 600; + color: #198754; + min-width: 60px; + text-align: right; + flex-shrink: 0; +} + +/* Delete button wrapper - fixed width to prevent layout shift */ +.delete-button-wrapper { + min-width: 45px; + display: flex; + justify-content: flex-end; + align-items: center; +} + +/* User chips */ +.user-chip { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + border: 1px solid #dee2e6; + background-color: #f8f9fa; + color: #495057; + border-radius: 1rem; + transition: all 0.2s; + white-space: nowrap; +} + +.user-chip:hover { + background-color: #e9ecef; + border-color: #adb5bd; +} + +.user-chip.active { + background-color: #0d6efd; + color: white; + border-color: #0d6efd; +} + +/* Summary Section */ +.card-header.bg-success { + background-color: #198754 !important; +} + +/* Items List */ +.items-list { + max-height: none; +} + +.item-card:last-child { + border-bottom: none !important; +} + +/* Responsive Design */ +@media (max-width: 576px) { + .item-number { + font-size: 0.75rem; + min-width: 28px; + } +} + +/* Summary Cards Per Person */ +.card.border { + border-color: #dee2e6 !important; +} + +.card.border:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + transition: box-shadow 0.2s; +} diff --git a/config/app.config.json b/config/app.config.json new file mode 100644 index 0000000..ea1901b --- /dev/null +++ b/config/app.config.json @@ -0,0 +1,9 @@ +{ + "features": { + "indexedDbSync": true, + "analysisPage": true + }, + "api": { + "ocrProvider": "stub" + } +} \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..381be86 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,53 @@ +// ESLint flat config (ES module) +import js from '@eslint/js'; +import reactPlugin from 'eslint-plugin-react'; +import tsParser from '@typescript-eslint/parser'; +import tsPlugin from '@typescript-eslint/eslint-plugin'; + +export default [ + { + ignores: [ + 'node_modules/**', + '.meteor/**', + 'types/**', + 'package-lock.json', + ], + }, + js.configs.recommended, + { + files: ['**/*.{js,jsx,ts,tsx}'], + languageOptions: { + ecmaVersion: 2021, + sourceType: 'module', + parser: tsParser, + globals: { + document: 'readonly', + console: 'readonly', + window: 'readonly', + localStorage: 'readonly', + navigator: 'readonly', + Image: 'readonly', + setTimeout: 'readonly', + Blaze: 'readonly', + }, + }, + plugins: { react: reactPlugin, '@typescript-eslint': tsPlugin }, + settings: { react: { version: 'detect' } }, + rules: { + 'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], + 'no-undef': 'error', + 'no-console': ['warn', { allow: ['error'] }], + 'eqeqeq': ['error', 'always'], + 'curly': ['error', 'all'], + 'no-var': 'error', + 'indent': ['error', 'tab', { SwitchCase: 1 }], + 'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'], + 'eol-last': ['error', 'always'], + 'no-trailing-spaces': 'error', + 'object-curly-spacing': ['error', 'always'], + 'array-bracket-spacing': ['error', 'never'], + 'comma-dangle': ['error', 'always-multiline'], + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }], + }, + }, +]; diff --git a/imports/api/bills.ts b/imports/api/bills.ts new file mode 100644 index 0000000..7ef8e05 --- /dev/null +++ b/imports/api/bills.ts @@ -0,0 +1,559 @@ +import { Mongo } from 'meteor/mongo'; +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import type { BillDoc, UserProfile, Item } from './models'; +// Using loose any collection due to temporary ambient types; can be replaced with proper generics once types fixed. +export const Bills = new (Mongo as any).Collection('bills'); + +Meteor.methods({ + async 'bills.insert'(bill: BillDoc) { + check(bill, Object); + if (!Array.isArray(bill.users)) { bill.users = []; } + if (!Array.isArray(bill.items)) { bill.items = []; } + bill.createdAt = bill.createdAt || new Date(); + bill.updatedAt = bill.createdAt; + return await Bills.insertAsync(bill); + }, + async 'bills.addUser'(billId: string, user: UserProfile) { + check(billId, String); check(user, Object); + if (!user.name?.trim()) { throw new Meteor.Error('invalid-user', 'User name required'); } + const existing = await Bills.findOneAsync(billId); + if (!existing) { throw new Meteor.Error('not-found', 'Bill not found'); } + if (existing.users.some((u: UserProfile) => u.name === user.name)) { throw new Meteor.Error('duplicate-user', 'User name already exists'); } + await Bills.updateAsync(billId, { $push: { users: user }, $set: { updatedAt: new Date() } }); + }, + async 'bills.removeUser'(billId: string, userId: string) { + check(billId, String); check(userId, String); + const existing = await Bills.findOneAsync(billId); + if (!existing) { throw new Meteor.Error('not-found', 'Bill not found'); } + await Bills.updateAsync(billId, { $set: { users: existing.users.filter((u: UserProfile) => u.id !== userId), items: existing.items.map((i: Item) => ({ ...i, userIds: i.userIds.filter((id: string) => id !== userId) })), updatedAt: new Date() } }); + }, + async 'bills.addItem'(billId: string, item: Item) { + check(billId, String); check(item, Object); + if (!item.name?.trim()) { throw new Meteor.Error('invalid-item', 'Item name required'); } + if (typeof item.price !== 'number' || item.price <= 0) { throw new Meteor.Error('invalid-price', 'Item price must be > 0'); } + const existing = await Bills.findOneAsync(billId); + if (!existing) { throw new Meteor.Error('not-found', 'Bill not found'); } + await Bills.updateAsync(billId, { $push: { items: item }, $set: { updatedAt: new Date() } }); + }, + async 'bills.removeItem'(billId: string, itemId: string) { + check(billId, String); check(itemId, String); + const existing = await Bills.findOneAsync(billId); + if (!existing) { throw new Meteor.Error('not-found', 'Bill not found'); } + await Bills.updateAsync(billId, { $set: { items: existing.items.filter((i: Item) => i.id !== itemId), updatedAt: new Date() } }); + }, + async 'bills.remove'(billId: string) { + check(billId, String); + const existing = await Bills.findOneAsync(billId); + if (!existing) { throw new Meteor.Error('not-found', 'Bill not found'); } + await Bills.removeAsync(billId); + }, + async 'bills.updateItems'(billId: string, items: Item[]) { + check(billId, String); + check(items, Array); + const existing = await Bills.findOneAsync(billId); + if (!existing) { throw new Meteor.Error('not-found', 'Bill not found'); } + await Bills.updateAsync(billId, { $set: { items, updatedAt: new Date() } }); + }, + async 'bills.updateTax'(billId: string, taxAmount: number) { + check(billId, String); + check(taxAmount, Number); + if (taxAmount < 0) { throw new Meteor.Error('invalid-tax', 'Tax amount cannot be negative'); } + const existing = await Bills.findOneAsync(billId); + if (!existing) { throw new Meteor.Error('not-found', 'Bill not found'); } + await Bills.updateAsync(billId, { $set: { taxAmount: Number(taxAmount.toFixed(2)), updatedAt: new Date() } }); + }, + async 'bills.toggleUserOnItem'(billId: string, itemId: string, userId: string) { + check(billId, String); + check(itemId, String); + check(userId, String); + const existing = await Bills.findOneAsync(billId); + if (!existing) { throw new Meteor.Error('not-found', 'Bill not found'); } + const item = existing.items.find((i: Item) => i.id === itemId); + if (!item) { throw new Meteor.Error('not-found', 'Item not found'); } + + // Toggle user: add if not present, remove if present + const userIds = item.userIds || []; + const index = userIds.indexOf(userId); + const newUserIds = index >= 0 + ? userIds.filter((id: string) => id !== userId) + : [...userIds, userId]; + + // Update the items array + const updatedItems = existing.items.map((i: Item) => + i.id === itemId ? { ...i, userIds: newUserIds } : i, + ); + + await Bills.updateAsync(billId, { $set: { items: updatedItems, updatedAt: new Date() } }); + } + , async 'bills.syncUserName'(userId: string, newName: string) { + check(userId, String); check(newName, String); + // Update all bills containing this user id + const cursor = Bills.find({ 'users.id': userId }); + const bills = await cursor.fetchAsync(); + for (const bill of bills) { + const newUsers = bill.users.map((u: any) => u.id === userId ? { ...u, name: newName } : u); + await Bills.updateAsync(bill._id, { $set: { users: newUsers, updatedAt: new Date() } }); + } + return true; + }, +}); + +// OCR text parser: extract items and prices from receipt text with intelligent filtering +Meteor.methods({ + async 'ocr.extract'(billId: string, text: string) { + check(billId, String); check(text, String); + console.log('ocr.extract called with billId:', billId); + console.log('ocr.extract text length:', text.length); + const existing = await Bills.findOneAsync(billId); + if (!existing) { throw new Meteor.Error('not-found', 'Bill not found'); } + const storeName = detectStoreName(text); + console.log('Detected store name:', storeName); + const { items, receiptTotal, taxAmount, totalAmount } = parseReceiptText(text, existing.users.map((u: any) => u.id)); + console.log('parseReceiptText returned items count:', items.length); + if (!items.length) { return 0; } + await persistParsedReceipt(billId, items, receiptTotal, taxAmount, totalAmount, storeName); + return items.length; + }, +}); + +// --- OCR Parsing Helpers (modularized) --- +function detectStoreName(text: string): string { + const upperText = text.toUpperCase(); + + // Check for common store patterns (prioritize specific matches first) + const storePatterns = [ + { pattern: /COSTCO\s*WHOLESALE/i, name: 'Costco' }, + { pattern: /WAL[\*\s]?MART/i, name: 'Walmart' }, + { pattern: /TARGET/i, name: 'Target' }, + { pattern: /KROGER/i, name: 'Kroger' }, + { pattern: /WHOLE\s*FOODS/i, name: 'Whole Foods' }, + { pattern: /TRADER\s*JOE/i, name: 'Trader Joe\'s' }, + { pattern: /ALDI/i, name: 'Aldi' }, + { pattern: /FRESH\s*THYME/i, name: 'Fresh Thyme' }, + { pattern: /HOBBY\s*LOBBY/i, name: 'Hobby Lobby' }, + { pattern: /HOME\s*DEPOT/i, name: 'Home Depot' }, + { pattern: /LOWE'?S/i, name: 'Lowe\'s' }, + { pattern: /CVS/i, name: 'CVS' }, + { pattern: /WALGREENS/i, name: 'Walgreens' }, + { pattern: /DOLLAR\s*GENERAL/i, name: 'Dollar General' }, + { pattern: /DOLLAR\s*TREE/i, name: 'Dollar Tree' }, + { pattern: /SAFEWAY/i, name: 'Safeway' }, + { pattern: /PUBLIX/i, name: 'Publix' }, + { pattern: /MEIJER/i, name: 'Meijer' }, + { pattern: /HALAL\s*MARKET/i, name: 'Halal Market' }, + ]; + + for (const { pattern, name } of storePatterns) { + if (pattern.test(upperText)) { + return name; + } + } + + return 'Receipt'; // Default name if no store detected +} + +function parseReceiptText(text: string, userIds: string[]) { + console.log('parseReceiptText called, userIds:', userIds.length); + const lines = text.split(/\n/).map(l => l.trim()).filter(Boolean); + console.log('parseReceiptText lines:', lines.length); + const items: Item[] = []; + let receiptTotal: number | null = null; + let taxAmount = 0; + let totalAmount: number | null = null; + + // First pass: Extract subtotal, tax, and total - more aggressive pattern matching + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Match SUBTOTAL with very loose patterns to handle OCR errors + // Handles: SUB TOTAL, SUBTOTAL, Sub Total, se dil (garbled), etc. + if (!receiptTotal) { + const subtotalMatch = line.match(/(?:SUB|se|Sub|TOTA?L?)[\s\w]*?[:\s]+.*?[$§]?(\d+[.,]\d{2})/i); + if (subtotalMatch && line.match(/sub|total|se.*?dil/i)) { + receiptTotal = parseFloat(subtotalMatch[1].replace(',', '.')); + console.log('Found subtotal:', receiptTotal, 'from line:', line); + } + } + + // Match TAX with various formats + // Handles: TAX, TAXES, Taxes:, etc. + const taxMatch = line.match(/TAX(?:ES)?[:\s]+.*?[$§]?(\d+[.,]\d{2})/i); + if (taxMatch) { + taxAmount += parseFloat(taxMatch[1].replace(',', '.')); + console.log('Found tax:', taxMatch[1], 'from line:', line); + } + + // Match TOTAL (but not SUBTOTAL) + // Handles: TOTAL, Total, TOTA, etc. + if (!totalAmount) { + const totalMatch = line.match(/^(?!.*SUB)(?!.*se).*?(?:TOTA?L?|Total)[:\s]+.*?[$§]?(\d+[.,]\d{2})/i); + if (totalMatch && !line.match(/sub|se.*?dil/i)) { + totalAmount = parseFloat(totalMatch[1].replace(',', '.')); + console.log('Found total:', totalAmount, 'from line:', line); + } + } + + // Also look for patterns like "Debit $4.47" or "Change Due $0.00" + if (!totalAmount && line.match(/debit|paid|amount|cash/i)) { + const amountMatch = line.match(/[$§]?(\d+[.,]\d{2})/); + if (amountMatch) { + const amount = parseFloat(amountMatch[1].replace(',', '.')); + if (amount > 0) { + totalAmount = amount; + console.log('Found total from payment line:', totalAmount, 'from line:', line); + } + } + } + } + + // If we didn't find subtotal/total, calculate from items after extraction + const shouldCalculateTotals = !receiptTotal && !totalAmount; + + // Second pass: Extract items - use flexible pattern matching + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + console.log(`Line ${i}: "${line}"`); + + // Skip header/footer lines + if (skipLine(line)) { + console.log(' -> Skipped (header/footer)'); + continue; + } + + // Stop at summary section + if (line.match(/^(SUB\s*TOTA?L?|TAX(?:ES)?|TOTA?L|BALANCE|CHANGE|VISA|CREDIT|DEBIT|CASH|CARD|APPROVED|THANK|se.*?dil)/i)) { + console.log(' -> Stopped at summary section'); + break; + } + + // Try all extraction patterns + const extracted = tryExtractItem(line, lines, i, userIds); + if (extracted) { + items.push(extracted); + console.log(' -> Extracted:', extracted.name, extracted.price); + } + } + + // Calculate totals from items if not found in receipt + if (shouldCalculateTotals && items.length > 0) { + receiptTotal = parseFloat(items.reduce((sum, item) => sum + item.price, 0).toFixed(2)); + totalAmount = receiptTotal + taxAmount; + console.log('Calculated totals from items - subtotal:', receiptTotal, 'total:', totalAmount); + } + + console.log('parseReceiptText finished, total items found:', items.length); + return { items, receiptTotal, taxAmount, totalAmount }; +} + +function skipLine(line: string) { + return !!( + line.match(/^(ST#|OP#|TE#|TR#|TC#|EAN|UPC|STORE|CASHIER|CUSTOMER|REG|INVOICE|RRN|SALE|SELF-CHECKOUT|CV Member|Seq|App#|Tran ID|PID|AID|TVR|TSI)/i) || + line.match(/^\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/) || // dates + line.match(/^\d{2}:\d{2}/) || // times + line.match(/^[\d\s:]+$/) || // only digits and spaces + line.match(/^[\|\-\*=_]+$/) || // separators + line.match(/^[A-Z]{1,3}:\s*$/) || // single letters with colon + line.match(/^\d{10,}$/) || // long number strings (barcodes, IDs) + line.match(/^(DISCOVER|AID|TVR|TSI|APPROVED|Entry Method|FTFM|Resp:|AMOUNT|CHANGE|Visa)/i) || // payment info + line.match(/^[XE]\s*$/) || // Single letter lines + line.length < 3 + ); +} + +function tryExtractItem(line: string, lines: string[], index: number, userIds: string[]): Item | null { + // Universal price extraction - handles multiple formats: + // Format 1: NAME 4.99 N F (Freshthyme) + // Format 2: NAME $1.48 N (Fort Wayne Halal) + // Format 3: NAME 7.84 (Walmart simple) + // Format 4: NAME @ price/unit final_price (Walmart weight) + // Format 5: 0 NAME 4.99 N F (with leading zero/count) + // Format 6: NAME 1 $1.48 N (with quantity) + // Format 7: 1892398 SMOOTHIES 16.99 (Costco - code + name + price) + // Format 8: 2 @ 3.99 5.89 (quantity @ unit price = total) + + // Extract all numbers that could be prices from the line + const pricePatterns = [ + // Pattern 1: Dollar sign prices with space or decimal: $1 48, $0.33, §2 + /[$§](\d+)[.\s](\d{2})/g, + // Pattern 2: Simple decimal prices: 4.99, 50.35, 151.12 + /\b(\d{1,4})\.(\d{2})\b/g, + // Pattern 3: Prices at end with letter flags: 4.99 N F + /(\d{1,4})\.(\d{2})\s+[A-Z]\s*[A-Z]?$/g, + ]; + + let foundPrice: number | null = null; + let priceMatch: RegExpMatchArray | null = null; + let priceIndex = 0; + + // Try each pattern and find the LAST (rightmost) price on the line + // This handles lines like "2 @ 3.99 5.89" where 5.89 is the actual price + for (const pattern of pricePatterns) { + const matches = Array.from(line.matchAll(pattern)); + for (const match of matches) { + let price: number; + if (match[0].includes('$') || match[0].includes('§')) { + // Dollar format + price = parseInt(match[1]) + parseInt(match[2]) / 100; + } else { + // Decimal format + price = parseFloat(match[1] + '.' + match[2]); + } + + // Validate price range + if (price > 0.10 && price < 2000) { + const matchIndex = match.index || 0; + // Prefer rightmost price + if (!foundPrice || matchIndex > priceIndex) { + foundPrice = price; + priceMatch = match; + priceIndex = matchIndex; + } + } + } + } + + if (!foundPrice || !priceMatch) { + return null; + } + + // Extract item name (everything before the price) + const pricePosition = priceIndex; + let name = line.substring(0, pricePosition).trim(); + + // Clean up the name + name = name + .replace(/^E\s+/, '') // Remove leading 'E' (Costco item marker) + .replace(/^[0-9\/\s]+/, '') // Remove leading numbers/codes/quantity + .replace(/\s+@.*$/, '') // Remove @ price per unit + .replace(/\s{2,}/g, ' ') // Normalize spaces + .replace(/^[A-Z]$/, '') // Remove single letter + .trim(); + + // Validate name + if (name.length < 3) { + // Try to get name from previous line (multi-line items) + if (index > 0) { + const prevLine = lines[index - 1]; + if (prevLine && prevLine.length >= 3 && !prevLine.match(/\d+\.\d{2}/)) { + name = prevLine.trim(); + } + } + } + + // Final name validation + if (name.length < 3 || name.length > 60) { + return null; + } + + // Additional filtering: skip if name looks like a header or total line + if (name.match(/^(PRODUCT|QTY|AMT|ITEM|PRICE|RODUCT|DAIRY|FROZEN|POULTRY|PRODUCE|SEAFOOD|SUBTOTAL|TAX|TOTAL)$/i)) { + return null; + } + + return { + id: `ocr${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + name: cleanName(name), + price: foundPrice, + userIds, + splitType: 'equal' + }; +} + +function pushItem(items: Item[], name: string, price: number, userIds: string[]) { + console.log('pushItem called:', { name, price, valid: (price > 0 && price < 10000) }); + if (price > 0 && price < 10000) { + items.push({ id: `ocr${Date.now()}_${items.length}`, name, price, userIds, splitType: 'equal' }); + console.log('Item pushed successfully'); + } +} + +function tryMultiLineItem(lines: string[], i: number, items: Item[], userIds: string[]) { + const line = lines[i]; + const isItemName = line.match(/^[A-Z][A-Z\s\d&\-']{2,}$/); + if (!isItemName) { return false; } + const next1 = lines[i + 1] || ''; + const next2 = lines[i + 2] || ''; + const next3 = lines[i + 3] || ''; + const barcodeMatch = next1.match(/^(\d{12,})\s*([A-Z])?$/); + const priceMatch = next2.match(/^(\d+\.?\d{1,2})$/); + if (barcodeMatch && priceMatch) { + pushItem(items, cleanName(line), parseFloat(priceMatch[1]), userIds); + return true; + } + const priceMatch2 = next3.match(/^(\d+\.?\d{1,2})$/); + if (barcodeMatch && priceMatch2) { + pushItem(items, cleanName(line), parseFloat(priceMatch2[1]), userIds); + return true; + } + const direct = next1.match(/^(\d+\.?\d{1,2})$/); + if (direct && !barcodeMatch) { + pushItem(items, cleanName(line), parseFloat(direct[1]), userIds); + return true; + } + return false; +} + +function tryPatternSingleLine(line: string, items: Item[], userIds: string[]) { + let match = line.match(/^([A-Z][A-Z\s\d&]{2,40}?)\s+(\d{12,})\s+[A-Z]\s+(\d+\.?\d{1,2})$/); + if (match) { pushItem(items, cleanName(match[1]), parseFloat(match[3]), userIds); return true; } + match = line.match(/^([A-Z][A-Z\s\d&]{2,40}?)\s+(\d{12,})\s+(\d+\.?\d{1,2})$/); + if (match) { pushItem(items, cleanName(match[1]), parseFloat(match[3]), userIds); return true; } + return false; +} + +function tryWeightLine(line: string, lines: string[], i: number, items: Item[], userIds: string[]) { + const match = line.match(/^(\d+\.\d+\s+l?1?b\s+@.+?)\s+(\d+\.?\d{1,2})$/i); + if (!match) { return false; } + const price = parseFloat(match[2]); + const prev = i > 0 ? lines[i - 1] : ''; + const prevMatch = prev.match(/^([A-Z][A-Z\s&]+)/); + const name = prevMatch ? prevMatch[1].trim() : match[1].trim(); + pushItem(items, name, price, userIds); + return true; +} + +function trySimpleFormat(line: string, items: Item[], userIds: string[]) { + const match = line.match(/^([A-Z][A-Z\s&\-']{2,40}?)\s{2,}(\d+\.?\d{1,2})$/); + if (!match) { return false; } + pushItem(items, match[1].trim(), parseFloat(match[2]), userIds); + return true; +} + +function tryPriceAtEnd(line: string, items: Item[], userIds: string[]) { + const match = line.match(/^([A-Z][A-Z\s&\-']{2,40}?)\s+(\d+\.?\d{1,2})$/); + if (!match) { return false; } + const name = match[1].trim(); + const price = parseFloat(match[2]); + const hasValidName = name.length >= 4 && name.split(' ').length >= 2; + const hasValidPrice = price >= 0.10 && price < 10000; + if (hasValidName && hasValidPrice) { pushItem(items, name, price, userIds); } + return true; +} + +function tryDollarSignPrice(line: string, items: Item[], userIds: string[]) { + // Match lines like "RUITS & VEGE $1 48 N" or "LINTRO ! $0.33 N" or "AMOSA 2 §2 N" + // Handle both $X.XX and $X XX (space instead of decimal) and §X variants + const match = line.match(/^([A-Z][A-Z\s&\-'!]{2,40}?)\s+[$§](\d+)[.\s](\d{2})\s+[A-Z]?$/i); + if (match) { + const name = match[1].trim(); + const dollars = parseInt(match[2]); + const cents = parseInt(match[3]); + const price = dollars + (cents / 100); + console.log('tryDollarSignPrice matched:', { name, price }); + pushItem(items, name, price, userIds); + return true; + } + // Also try simpler pattern: "NAME $X.XX" or "NAME $X XX" + const match2 = line.match(/^([A-Z][A-Z\s&\-'!]{2,40}?)\s+[$§](\d+)[.\s](\d{2})$/i); + if (match2) { + const name = match2[1].trim(); + const dollars = parseInt(match2[2]); + const cents = parseInt(match2[3]); + const price = dollars + (cents / 100); + console.log('tryDollarSignPrice matched (pattern 2):', { name, price }); + pushItem(items, name, price, userIds); + return true; + } + return false; +} + +function cleanName(raw: string) { + let name = raw.trim(); + // Remove common OCR artifacts and formatting + name = name.replace(/\s+\d{1,3}CT$/i, ''); // Remove "CT" count suffix + name = name.replace(/\s+[A-Z]\d+$/i, ''); // Remove code suffixes + name = name.replace(/\s+[|/].*$/, ''); // Remove | or / and everything after + name = name.replace(/\s+[$§]\s*$/, ''); // Remove trailing $ or § + name = name.replace(/\s{2,}/g, ' '); // Normalize spaces + name = name.replace(/\s+[A-Z]$/, ''); // Remove single letter at end (like "N") + + // Fix common OCR errors in item names + name = name.replace(/^ruts/i, 'Fruits'); // "ruts" -> "Fruits" + name = name.replace(/^1intro/i, 'Intro'); // "1intro" -> "Intro" + name = name.replace(/whosa/i, 'Samosa'); // "whosa" -> "Samosa" + + return name.trim(); +} + +async function persistParsedReceipt(billId: string, items: Item[], receiptTotal: number | null, taxAmount: number, totalAmount: number | null, storeName?: string) { + const calculatedTotal = Number(items.reduce((s, it) => s + it.price, 0).toFixed(2)); + const totalMismatch = receiptTotal && Math.abs(calculatedTotal - receiptTotal) > 0.05; + const calculatedWithTax = Number((calculatedTotal + taxAmount).toFixed(2)); + const totalWithTaxMismatch = totalAmount && Math.abs(calculatedWithTax - totalAmount) > 0.05; + await Bills.updateAsync(billId, { + $push: { items: { $each: items } }, + $set: { + updatedAt: new Date(), + storeName: storeName || 'Receipt', + receiptTotal: receiptTotal ? Number(receiptTotal.toFixed(2)) : null, + calculatedTotal: Number(calculatedTotal.toFixed(2)), + totalMismatch, + taxAmount: Number(taxAmount.toFixed(2)), + totalAmount: totalAmount ? Number(totalAmount.toFixed(2)) : null, + calculatedWithTax: Number(calculatedWithTax.toFixed(2)), + totalWithTaxMismatch, + }, + }); +} + +// OCR file stub: accept file metadata and generate sample items based on file characteristics +Meteor.methods({ + async 'ocr.extractFromFile'(billId: string, fileInfo: { name: string; size: number; type?: string }) { + check(billId, String); check(fileInfo, Object); + const existing = await Bills.findOneAsync(billId); + if (!existing) { + throw new Meteor.Error('not-found', 'Bill not found'); + } + + // Generate sample items based on file size (simulating OCR extraction) + // In production, this would use actual OCR to extract text and parse items + const sampleItems = [ + { name: 'Main Course', price: 18.99 }, + { name: 'Side Dish', price: 6.50 }, + { name: 'Beverage', price: 3.99 }, + { name: 'Dessert', price: 7.50 }, + { name: 'Appetizer', price: 8.99 }, + ]; + + // Generate 2-4 items based on file size + const numItems = Math.min(Math.max(2, Math.floor(fileInfo.size / 50000)), 4); + const items: Item[] = []; + + for (let i = 0; i < numItems; i++) { + const sample = sampleItems[i % sampleItems.length]; + // Add some randomness to prices + const randomPrice = (sample.price + (Math.random() * 5 - 2.5)).toFixed(2); + items.push({ + id: `ocr${Date.now()}_${i}`, + name: sample.name, + price: parseFloat(randomPrice), + userIds: existing.users.map((u: any) => u.id), + splitType: 'equal', + }); + } + + await Bills.updateAsync(billId, { $push: { items: { $each: items } }, $set: { updatedAt: new Date() } }); + return { itemCount: items.length, totalAmount: items.reduce((sum, item) => sum + item.price, 0).toFixed(2) }; + }, +}); + +// Clear all data method +Meteor.methods({ + async 'clearAllData'() { + // Only allow in development or with proper authentication + // In production, you might want to add user authentication check here + + // Import GlobalUsers to clear it too + const { GlobalUsers } = await import('./users'); + + // Remove all bills + await Bills.removeAsync({}); + + // Remove all global users + await GlobalUsers.removeAsync({}); + + console.log('All data cleared from database (bills and users)'); + return { success: true }; + }, +}); diff --git a/imports/api/models.ts b/imports/api/models.ts new file mode 100644 index 0000000..480cd42 --- /dev/null +++ b/imports/api/models.ts @@ -0,0 +1,68 @@ +export interface UserProfile { + id: string; + name: string; + contact?: string; + preferences?: Record; +} +export type SplitType = 'equal' | 'percent' | 'fixed'; +export interface ItemSplitSharePercent { userId: string; type: 'percent'; value: number; } +export interface ItemSplitShareFixed { userId: string; type: 'fixed'; value: number; } +export type ItemSplitShare = ItemSplitSharePercent | ItemSplitShareFixed; +export interface Item { + id: string; + name: string; + price: number; + userIds: string[]; // for equal split + splitType?: SplitType; + shares?: ItemSplitShare[]; // for percent/fixed +} +export interface BillDoc { + _id?: string; + createdAt: Date; + updatedAt?: Date; + date?: string; + users: UserProfile[]; + items: Item[]; + currency?: string; +} +export interface ExpenseSummaryEntry { userId: string; amount: number; } +export interface ExpenseSummary { billId: string; grandTotal: number; perUser: ExpenseSummaryEntry[]; } + +export function computeExpenseSummary(bill: BillDoc): ExpenseSummary { + const perUserMap = new Map(); + let grandTotal = 0; + bill.items.forEach(item => { + grandTotal += item.price; + if (item.splitType === 'percent' && item.shares) { + const percentShares = item.shares.filter(s => s.type === 'percent'); + const sumPercent = percentShares.reduce((a, b) => a + b.value, 0) || 100; + const scale = 100 / sumPercent; + percentShares.forEach(s => { + const amt = item.price * (s.value * scale / 100); + perUserMap.set(s.userId, (perUserMap.get(s.userId) || 0) + amt); + }); + } else if (item.splitType === 'fixed' && item.shares) { + const fixedShares = item.shares.filter(s => s.type === 'fixed'); + let allocated = 0; + fixedShares.forEach(s => { + allocated += s.value; + perUserMap.set(s.userId, (perUserMap.get(s.userId) || 0) + s.value); + }); + const remainder = Math.max(0, item.price - allocated); + if (remainder > 0 && fixedShares.length) { + const extra = remainder / fixedShares.length; + fixedShares.forEach(s => { + perUserMap.set(s.userId, (perUserMap.get(s.userId) || 0) + extra); + }); + } + } else if (item.userIds.length) { + const share = item.price / item.userIds.length; + item.userIds.forEach(uid => perUserMap.set(uid, (perUserMap.get(uid) || 0) + share)); + } + }); + return { + billId: bill._id || 'local', + grandTotal: Number(grandTotal.toFixed(2)), + perUser: Array.from(perUserMap.entries()).map(([userId, amount]) => ({ userId, amount: Number(amount.toFixed(2)) })), + }; +} diff --git a/imports/api/publications.ts b/imports/api/publications.ts new file mode 100644 index 0000000..6001b37 --- /dev/null +++ b/imports/api/publications.ts @@ -0,0 +1,11 @@ +import { Meteor } from 'meteor/meteor'; +import { Bills } from './bills'; +import { GlobalUsers } from './users'; + +Meteor.publish('bills.all', function() { + return Bills.find({}); +}); + +Meteor.publish('globalUsers.all', function() { + return GlobalUsers.find({}); +}); diff --git a/imports/api/users.ts b/imports/api/users.ts new file mode 100644 index 0000000..f7c87fc --- /dev/null +++ b/imports/api/users.ts @@ -0,0 +1,52 @@ +import { Mongo } from 'meteor/mongo'; +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; + +export interface GlobalUser { + _id?: string; + name: string; + email?: string; + createdAt: Date; + updatedAt?: Date; +} + +export const GlobalUsers = new (Mongo as any).Collection('globalUsers'); + +Meteor.methods({ + async 'globalUsers.insert'(user: Omit) { + check(user, Object); + if (!user.name?.trim()) { throw new Meteor.Error('invalid-name', 'User name required'); } + const existing = await GlobalUsers.findOneAsync({ name: user.name.trim() }); + if (existing) { throw new Meteor.Error('duplicate-name', 'User name already exists'); } + const doc: GlobalUser = { + name: user.name.trim(), + email: user.email?.trim(), + createdAt: new Date(), + updatedAt: new Date(), + }; + return await GlobalUsers.insertAsync(doc); + }, + async 'globalUsers.update'(userId: string, updates: Partial) { + check(userId, String); + check(updates, Object); + const existing = await GlobalUsers.findOneAsync(userId); + if (!existing) { throw new Meteor.Error('not-found', 'User not found'); } + if (updates.name && updates.name !== existing.name) { + const duplicate = await GlobalUsers.findOneAsync({ name: updates.name.trim(), _id: { $ne: userId } }); + if (duplicate) { throw new Meteor.Error('duplicate-name', 'User name already exists'); } + } + await GlobalUsers.updateAsync(userId, { + $set: { + ...(updates.name && { name: updates.name.trim() }), + ...(updates.email !== undefined && { email: updates.email?.trim() }), + updatedAt: new Date(), + }, + }); + }, + async 'globalUsers.remove'(userId: string) { + check(userId, String); + const existing = await GlobalUsers.findOneAsync(userId); + if (!existing) { throw new Meteor.Error('not-found', 'User not found'); } + await GlobalUsers.removeAsync(userId); + }, +}); diff --git a/imports/api/utils.js b/imports/api/utils.js new file mode 100644 index 0000000..971b3a4 --- /dev/null +++ b/imports/api/utils.js @@ -0,0 +1,33 @@ +// Formatting & tax helpers. + +export function formatMoney(value) { + if (value === null || value === undefined || value === '') { return '0.00'; } + const num = Number(parseFloat(value).toFixed(2)); + return num.toFixed(2); +} + +export function taxPerUser(bill) { + if (!bill || !bill.taxAmount) { return 0; } + const count = (bill.users || []).length; + if (!count) { return 0; } + return parseFloat(bill.taxAmount) / count; +} + +export function totalPerUserWithTax(bill, userSubtotal) { + if (!bill) { return 0; } + const share = taxPerUser(bill); + const base = parseFloat(userSubtotal || 0); + const total = base + share; // round after addition + return Number(total.toFixed(2)); +} + +export function percentageInclusive(amount, bill, grandTotal) { + if (!bill) { return '0.00'; } + const baseItems = parseFloat(grandTotal || 0); + if (!baseItems) { return '0.00'; } + if (!bill.taxAmount) { return ((parseFloat(amount || 0) / baseItems) * 100).toFixed(2); } + const share = taxPerUser(bill); + const totalWithTaxAll = baseItems + parseFloat(bill.taxAmount); + if (!totalWithTaxAll) { return '0.00'; } + return (((parseFloat(amount || 0) + share) / totalWithTaxAll) * 100).toFixed(2); +} diff --git a/imports/infra/indexedDb.ts b/imports/infra/indexedDb.ts new file mode 100644 index 0000000..a380126 --- /dev/null +++ b/imports/infra/indexedDb.ts @@ -0,0 +1,28 @@ +import { openDB } from 'idb'; +import type { BillDoc } from '../api/models'; + +const DB_NAME = 'splitly_local'; +const STORE = 'bills'; + +async function getDB() { + return openDB(DB_NAME, 1, { + upgrade(db) { + if (!db.objectStoreNames.contains(STORE)) { + db.createObjectStore(STORE, { keyPath: '_id' }); + } + }, + }); +} + +export async function cacheBills(bills: BillDoc[]) { + const db = await getDB(); + const tx = db.transaction(STORE, 'readwrite'); + const store = tx.store; + await Promise.all(bills.map(b => store.put(b))); + await tx.done; +} + +export async function loadCachedBills(): Promise { + const db = await getDB(); + return await db.getAll(STORE); +} diff --git a/imports/startup/client/routes.js b/imports/startup/client/routes.js new file mode 100644 index 0000000..0ef1794 --- /dev/null +++ b/imports/startup/client/routes.js @@ -0,0 +1,34 @@ +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; +import { Blaze } from 'meteor/blaze'; +import { Template } from 'meteor/templating'; +import { Meteor } from 'meteor/meteor'; +import '/imports/ui/blaze/layout'; +import '/imports/ui/blaze/components/userModal'; +import '/imports/ui/blaze/pages/dashboard'; +import '/imports/ui/blaze/pages/splitPage'; +import '/imports/ui/blaze/pages/history'; +import '/imports/ui/blaze/pages/analysis'; +import '/imports/ui/blaze/pages/billDetail'; +import '/imports/ui/blaze/pages/settings'; + +function render(templateName, data = {}) { + Meteor.defer(() => { + const host = document.getElementById('app'); + if (!host || !Template[templateName] || !Template.MainLayout) {return;} + + if (host._blazeView) { + Blaze.remove(host._blazeView); + host._blazeView = null; + } + const view = Blaze.renderWithData(Template.MainLayout, { yield: templateName, ...data }, host); + host._blazeView = view; + }); +} + +FlowRouter.route('/', { name: 'dashboard', action() { render('Dashboard'); } }); +FlowRouter.route('/split/:id', { name: 'splitPage', action(params) { render('SplitPage', { billId: params.id }); } }); +FlowRouter.route('/history', { name: 'history', action() { render('History'); } }); +FlowRouter.route('/analysis', { name: 'analysis', action() { render('Analysis'); } }); +FlowRouter.route('/bill/:id', { name: 'billDetail', action(params) { render('BillDetail', { billId: params.id }); } }); +FlowRouter.route('/settings', { name: 'settings', action() { render('Settings'); } }); + diff --git a/imports/ui/blaze/components/userModal.html b/imports/ui/blaze/components/userModal.html new file mode 100644 index 0000000..ab06e0d --- /dev/null +++ b/imports/ui/blaze/components/userModal.html @@ -0,0 +1,53 @@ + diff --git a/imports/ui/blaze/components/userModal.js b/imports/ui/blaze/components/userModal.js new file mode 100644 index 0000000..3f66119 --- /dev/null +++ b/imports/ui/blaze/components/userModal.js @@ -0,0 +1,171 @@ +/* eslint-env browser */ +import { Template } from 'meteor/templating'; +import './userModal.html'; // ensure template is registered +import { ReactiveVar } from 'meteor/reactive-var'; +import { Meteor } from 'meteor/meteor'; +import { GlobalUsers } from '/imports/api/users'; +import { pushAlert } from '/imports/ui/blaze/layout'; + +Template.UserModal.onCreated(function () { + this.subscribe('globalUsers.all'); + this.editingUserId = new ReactiveVar(null); + this.clickLock = false; // guard rapid double submissions +}); + +Template.UserModal.helpers({ + users() { + return GlobalUsers.find({}, { sort: { createdAt: 1 } }).fetch(); + }, + hasUsers() { + return GlobalUsers.find().count() > 0; + }, + isEditing(userId) { + return Template.instance().editingUserId.get() === userId; + }, +}); + +Template.UserModal.events({ + async 'click #addUserModalBtn'(e, tpl) { + if (tpl.clickLock) { + return; + } + const input = tpl.find('#userNameModalInput'); + const name = input.value.trim(); + if (!name) { + pushAlert('error', 'Please enter a name'); + return; + } + tpl.clickLock = true; + try { + await Meteor.callAsync('globalUsers.insert', { name }); + pushAlert('success', 'Person added'); + input.value = ''; + } catch (err) { + pushAlert('error', err.reason || 'Could not add person'); + } finally { + tpl.clickLock = false; + } + }, + async 'click .remove-user'(e) { + const userId = e.currentTarget.getAttribute('data-id'); + try { + await Meteor.callAsync('globalUsers.remove', userId); + pushAlert('success', 'Person removed'); + } catch (err) { + pushAlert('error', err.reason || 'Could not remove person'); + } + }, + 'keypress #userNameModalInput'(e, tpl) { + if (e.which === 13) { + e.preventDefault(); + tpl.find('#addUserModalBtn').click(); + } + }, + 'click .editable-name'(e, tpl) { + const userId = e.currentTarget.getAttribute('data-id'); + tpl.editingUserId.set(userId); + Meteor.setTimeout(() => { + const input = tpl.find(`.edit-user-input[data-id="${userId}"]`); + if (input) { + input.focus(); + input.select(); + } + }, 100); + }, + async 'click .save-edit-user'(e, tpl) { + if (tpl.clickLock) { + return; + } + const userId = e.currentTarget.getAttribute('data-id'); + const input = tpl.find(`.edit-user-input[data-id="${userId}"]`); + const newName = input.value.trim(); + if (!newName) { + pushAlert('error', 'Name cannot be empty'); + return; + } + tpl.clickLock = true; + try { + await Meteor.callAsync('globalUsers.update', userId, { name: newName }); + pushAlert('success', 'Name updated'); + tpl.editingUserId.set(null); + } catch (err) { + pushAlert('error', err.reason || 'Could not update name'); + } finally { + tpl.clickLock = false; + } + }, + 'click .cancel-edit-user'(e, tpl) { + tpl.editingUserId.set(null); + }, + 'keypress .edit-user-input'(e, tpl) { + if (e.which === 13) { + e.preventDefault(); + const userId = e.currentTarget.getAttribute('data-id'); + tpl.find(`.save-edit-user[data-id="${userId}"]`).click(); + } else if (e.which === 27) { + e.preventDefault(); + tpl.editingUserId.set(null); + } + }, + async 'click #saveUsersBtn'(e) { + const tpl = Template.instance(); + const editingId = tpl.editingUserId.get(); + if (editingId) { + const input = tpl.find(`.edit-user-input[data-id="${editingId}"]`); + if (input) { + const newName = input.value.trim(); + if (!newName) { + pushAlert('error', 'Name cannot be empty'); + return; + } + try { + await Meteor.callAsync('globalUsers.update', editingId, { name: newName }); + tpl.editingUserId.set(null); + } catch (err) { + pushAlert('error', err.reason || 'Update failed'); + return; + } + } + } + const users = GlobalUsers.find().fetch(); + for (const u of users) { + await Meteor.callAsync('bills.syncUserName', u._id, u.name); + } + pushAlert('success', 'Users saved & synced!'); + e.currentTarget.blur(); + const modalEl = document.getElementById('userModal'); + if (modalEl && window.bootstrap) { + (window.bootstrap.Modal.getInstance(modalEl) || new window.bootstrap.Modal(modalEl)).hide(); + } + }, + 'click #cancelUsersBtn'(e) { + e.currentTarget.blur(); + const modalEl = document.getElementById('userModal'); + if (modalEl && window.bootstrap) { + (window.bootstrap.Modal.getInstance(modalEl) || new window.bootstrap.Modal(modalEl)).hide(); + } + }, + 'click #closeUsersBtn'(e) { + e.currentTarget.blur(); + const modalEl = document.getElementById('userModal'); + if (modalEl && window.bootstrap) { + (window.bootstrap.Modal.getInstance(modalEl) || new window.bootstrap.Modal(modalEl)).hide(); + } + }, +}); + +if (typeof window !== 'undefined') { + document.addEventListener('hidden.bs.modal', (ev) => { + if (ev.target && ev.target.id === 'userModal') { + try { + const view = Blaze.getView(document.getElementById('userModal')); + const instance = view?.templateInstance?.(); + if (instance?.editingUserId) { + instance.editingUserId.set(null); + } + } catch { + /* noop */ + } + } + }); +} diff --git a/imports/ui/blaze/layout.html b/imports/ui/blaze/layout.html new file mode 100644 index 0000000..c8b3a9e --- /dev/null +++ b/imports/ui/blaze/layout.html @@ -0,0 +1,59 @@ + + + + + \ No newline at end of file diff --git a/imports/ui/blaze/layout.js b/imports/ui/blaze/layout.js new file mode 100644 index 0000000..34b2ba7 --- /dev/null +++ b/imports/ui/blaze/layout.js @@ -0,0 +1,139 @@ +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; +import { Bills } from '/imports/api/bills'; +import { cacheBills, loadCachedBills } from '/imports/infra/indexedDb'; +import config from '/config/app.config.json'; +import './layout.html'; +import './components/userModal'; + +const alertsVar = new ReactiveVar([]); + +function mapType(type) { + switch (type) { + case 'error': return 'danger'; + case 'warn': + case 'warning': return 'warning'; + case 'info': return 'info'; + case 'success': return 'success'; + default: return 'secondary'; + } +} + +function pushAlert(type, msg) { + const list = alertsVar.get(); + list.push({ id: Math.random().toString(36).slice(2), type, msg, typeClass: mapType(type) }); + alertsVar.set(list); + setTimeout(() => { + const cur = alertsVar.get(); + cur.shift(); + alertsVar.set(cur); + }, 6000); +} + +let confirmResolver = null; +export function showConfirm(message, options = {}) { + return new Promise(resolve => { + const modalEl = document.getElementById('confirmModal'); + const hasBootstrap = typeof window !== 'undefined' && window.bootstrap?.Modal; + if (!modalEl || !hasBootstrap) { + resolve(window.confirm(message)); + return; + } + confirmResolver = resolve; + const msgEl = document.getElementById('confirmModalMessage'); + if (msgEl) { + msgEl.textContent = message; + } + const okBtn = document.getElementById('confirmOkBtn'); + if (okBtn) { + okBtn.textContent = options.okText || 'OK'; + } + const cancelBtn = document.getElementById('confirmCancelBtn'); + if (cancelBtn) { + cancelBtn.textContent = options.cancelText || 'Cancel'; + } + wireConfirmHandlers(); + new window.bootstrap.Modal(modalEl, { backdrop: 'static', keyboard: false }).show(); + }); +} + +function wireConfirmHandlers() { + const okBtn = document.getElementById('confirmOkBtn'); + const cancelBtn = document.getElementById('confirmCancelBtn'); + const modalEl = document.getElementById('confirmModal'); + if (!okBtn || !cancelBtn || !modalEl) { + return; + } + okBtn.onclick = () => { + const modal = window.bootstrap ? window.bootstrap.Modal.getInstance(modalEl) : null; + modal?.hide(); + confirmResolver?.(true); + confirmResolver = null; + }; + cancelBtn.onclick = () => { + const modal = window.bootstrap ? window.bootstrap.Modal.getInstance(modalEl) : null; + modal?.hide(); + confirmResolver?.(false); + confirmResolver = null; + }; + modalEl.addEventListener('hidden.bs.modal', () => { + confirmResolver?.(false); + confirmResolver = null; + }); +} + +Template.MainLayout.onRendered(function () { + wireConfirmHandlers(); + // Populate IndexedDB cache when bills are ready (if enabled) + this.autorun(() => { + const flag = localStorage.getItem('flag_indexedDbSync'); + const enabled = flag === null ? config.features?.indexedDbSync : flag === 'true'; + if (this.subHandle.ready() && enabled) { + const allBills = Bills.find({}).fetch(); + if (allBills.length > 0) { + cacheBills(allBills).catch(err => console.error('IndexedDB cache error:', err)); + } + } + }); +}); +Template.MainLayout.onCreated(function () { + this.subHandle = this.subscribe('bills.all'); + // Try loading cached bills if offline/slow (if enabled) + const flag = localStorage.getItem('flag_indexedDbSync'); + const enabled = flag === null ? config.features?.indexedDbSync : flag === 'true'; + if (enabled) { + loadCachedBills() + .then(cached => { + if (cached.length > 0 && !this.subHandle.ready()) { + // Bills loaded from IndexedDB cache + // Bills collection will be populated from subscription when ready + } + }) + .catch(err => console.error('IndexedDB load error:', err)); + } +}); + +Template.MainLayout.helpers({ + notReady() { + return !Template.instance().subHandle.ready(); + }, + isActive(path) { + return FlowRouter.current().path === path ? 'active' : ''; + }, + showAnalysis() { + return !!config.features?.analysisPage; + }, + alerts() { + return alertsVar.get(); + }, +}); + +Template.Alerts.events({ + 'click .btn-close'(e) { + const id = e.currentTarget.getAttribute('data-id'); + alertsVar.set(alertsVar.get().filter(a => a.id !== id)); + }, +}); + +export { pushAlert }; diff --git a/imports/ui/blaze/pages/analysis.html b/imports/ui/blaze/pages/analysis.html new file mode 100644 index 0000000..9aa21c6 --- /dev/null +++ b/imports/ui/blaze/pages/analysis.html @@ -0,0 +1,82 @@ + \ No newline at end of file diff --git a/imports/ui/blaze/pages/analysis.js b/imports/ui/blaze/pages/analysis.js new file mode 100644 index 0000000..6c90cc7 --- /dev/null +++ b/imports/ui/blaze/pages/analysis.js @@ -0,0 +1,122 @@ +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; +import './analysis.html'; +import { Bills } from '/imports/api/bills'; +import { computeExpenseSummary } from '/imports/api/models'; + +// Aggregate spending analytics across all bills +function computeGlobalAnalytics(bills) { + const perUserMap = new Map(); + const userNameMap = new Map(); + let itemTotal = 0; + let taxTotal = 0; + const monthMap = new Map(); + const itemMap = new Map(); + bills.forEach(bill => { + if (!bill?.items) { + return; + } + const billSummary = computeExpenseSummary(bill); + const thisTax = Number(bill.taxAmount || 0); + taxTotal += thisTax; + const taxPerUser = bill.users?.length ? thisTax / bill.users.length : 0; + billSummary.perUser.forEach(entry => { + perUserMap.set(entry.userId, (perUserMap.get(entry.userId) || 0) + entry.amount + taxPerUser); + }); + (bill.users || []).forEach(u => { + if (!userNameMap.has(u.id)) { + userNameMap.set(u.id, u.name); + } + }); + itemTotal += billSummary.grandTotal; + const receiptDate = bill.createdAt instanceof Date ? bill.createdAt : new Date(); + const key = `${receiptDate.getFullYear()}-${String(receiptDate.getMonth() + 1).padStart(2, '0')}`; + monthMap.set(key, (monthMap.get(key) || 0) + billSummary.grandTotal + thisTax); + bill.items.forEach(it => { + const rec = itemMap.get(it.name) || { total: 0, count: 0 }; + rec.total += it.price; + rec.count += 1; + itemMap.set(it.name, rec); + }); + }); + const totalSpent = itemTotal + taxTotal; + const perUser = Array.from(perUserMap.entries()).map(([userId, amount]) => ({ userId, name: userNameMap.get(userId) || userId, amount: Number(amount.toFixed(2)) })); + const grand = totalSpent || 0.00001; + perUser.sort((a, b) => b.amount - a.amount); + perUser.forEach(p => { + p.percent = Number(((p.amount / grand) * 100).toFixed(2)); + }); + const monthly = Array.from(monthMap.entries()).map(([month, total]) => ({ month, total: Number(total.toFixed(2)) })).sort((a, b) => a.month.localeCompare(b.month)); + const topItems = Array.from(itemMap.entries()).map(([name, rec]) => ({ name, total: Number(rec.total.toFixed(2)), count: rec.count })).sort((a, b) => b.total - a.total).slice(0, 10); + const receiptCount = bills.length; + const avgPerReceipt = receiptCount ? Number((totalSpent / receiptCount).toFixed(2)) : 0; + return { totalSpent: Number(totalSpent.toFixed(2)), totalItemSpent: Number(itemTotal.toFixed(2)), taxTotal: Number(taxTotal.toFixed(2)), perUser, monthly, topItems, receiptCount, avgPerReceipt }; +} + +Template.Analysis.onCreated(function () { + this.filter = new ReactiveVar([]); + this.global = new ReactiveVar(null); + this.sub = this.subscribe('bills.all'); + this.autorun(() => { + if (!this.sub.ready()) { + return; + } + const bills = Bills.find({}, { sort: { createdAt: -1 } }).fetch(); + this.global.set(computeGlobalAnalytics(bills)); + }); +}); + +Template.Analysis.helpers({ + hasData() { + const inst = Template.instance(); + if (!inst.sub.ready()) { + return false; + } + const g = inst.global.get(); + return !!(g && g.receiptCount > 0); + }, + global() { + return Template.instance().global.get(); + }, + perUserGlobal() { + const inst = Template.instance(); + const g = inst.global.get(); + if (!g) { + return []; + } + const filter = inst.filter.get(); + const grand = g.totalSpent || 1; + return g.perUser.filter(p => !filter.length || filter.includes(p.userId)).map(p => ({ ...p, barWidth: (p.amount / grand) * 100 })); + }, + filterUsers() { + const g = Template.instance().global.get(); + return g ? g.perUser : []; + }, + hasFilter() { + return Template.instance().filter.get().length > 0; + }, + activeClass(userId) { + return Template.instance().filter.get().includes(userId) ? 'btn-primary' : 'btn-outline-secondary'; + }, + monthlyBreakdown() { + const g = Template.instance().global.get(); + return g ? g.monthly : []; + }, + topItems() { + const g = Template.instance().global.get(); + return g ? g.topItems : []; + }, +}); + +Template.Analysis.events({ + 'click .filter-btn'(e, t) { + const id = e.currentTarget.getAttribute('data-id'); + const cur = t.filter.get(); + t.filter.set(cur.includes(id) ? cur.filter(x => x !== id) : [...cur, id]); + }, + 'click #clearFilter'(e, t) { + t.filter.set([]); + }, +}); + +export { computeGlobalAnalytics }; diff --git a/imports/ui/blaze/pages/billDetail.html b/imports/ui/blaze/pages/billDetail.html new file mode 100644 index 0000000..786511f --- /dev/null +++ b/imports/ui/blaze/pages/billDetail.html @@ -0,0 +1,116 @@ + \ No newline at end of file diff --git a/imports/ui/blaze/pages/billDetail.js b/imports/ui/blaze/pages/billDetail.js new file mode 100644 index 0000000..a8989dc --- /dev/null +++ b/imports/ui/blaze/pages/billDetail.js @@ -0,0 +1,78 @@ +import './billDetail.html'; +import { Template } from 'meteor/templating'; +import { Bills } from '/imports/api/bills'; +import { computeExpenseSummary } from '/imports/api/models'; +import { showConfirm, pushAlert } from '../layout'; +import { Meteor } from 'meteor/meteor'; +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; +Template.BillDetail.helpers({ + bill() { + const id = FlowRouter.getParam('id'); + return Bills.findOne(id); + }, + updatedAt() { + const id = FlowRouter.getParam('id'); + const b = Bills.findOne(id); + return b?.updatedAt ? new Date(b.updatedAt).toLocaleString() : b?.createdAt?.toLocaleString(); + }, + perUser() { + const id = FlowRouter.getParam('id'); + const b = Bills.findOne(id); + if (!b) { + return []; + } + const summary = computeExpenseSummary(b); + return summary.perUser.map(p => ({ name: (b.users.find(u => u.id === p.userId) || { name: p.userId }).name, amount: p.amount.toFixed(2) })); + }, + grandTotal() { + const id = FlowRouter.getParam('id'); + const b = Bills.findOne(id); + if (!b) { + return '0.00'; + } + const summary = computeExpenseSummary(b); + return summary.grandTotal.toFixed(2); + }, + itemCount() { + const id = FlowRouter.getParam('id'); + const b = Bills.findOne(id); + return b?.items?.length || 0; + }, + userCount() { + const id = FlowRouter.getParam('id'); + const b = Bills.findOne(id); + return b?.users?.length || 0; + }, +}); +Template.BillDetail.events({ + 'click #exportJson'(_e) { + const id = FlowRouter.getParam('id'); + const b = Bills.findOne(id); + if (!b) { + return; + } + const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(b, null, 2)); + const a = document.createElement('a'); + a.href = dataStr; + a.download = `bill_${b._id}.json`; + a.click(); + }, + 'click #editReceiptBtn'() { + const id = FlowRouter.getParam('id'); + FlowRouter.go(`/split/${id}`); + }, + async 'click #deleteReceiptBtn'(_e) { + const ok = await showConfirm('Delete this receipt? This cannot be undone.', { okText: 'Delete', cancelText: 'Cancel' }); + if (!ok) { + return; + } + const id = FlowRouter.getParam('id'); + Meteor.call('bills.remove', id, (err) => { + if (err) { + pushAlert('error', 'Could not delete receipt: ' + (err.reason || err.message)); + } else { + FlowRouter.go('/history'); + } + }); + }, +}); diff --git a/imports/ui/blaze/pages/dashboard.html b/imports/ui/blaze/pages/dashboard.html new file mode 100644 index 0000000..70b83c5 --- /dev/null +++ b/imports/ui/blaze/pages/dashboard.html @@ -0,0 +1,105 @@ + \ No newline at end of file diff --git a/imports/ui/blaze/pages/dashboard.js b/imports/ui/blaze/pages/dashboard.js new file mode 100644 index 0000000..bcf7b4a --- /dev/null +++ b/imports/ui/blaze/pages/dashboard.js @@ -0,0 +1,496 @@ +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { Tracker } from 'meteor/tracker'; +import { Meteor } from 'meteor/meteor'; +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; +import { Bills } from '/imports/api/bills'; +import { GlobalUsers } from '/imports/api/users'; +import { computeExpenseSummary } from '/imports/api/models'; +import './dashboard.html'; +import { pushAlert } from '../layout'; +import Tesseract from 'tesseract.js'; + +Template.Dashboard.onCreated(function () { + this.subscribe('globalUsers.all'); + this.ocrProcessing = new ReactiveVar(false); + this.cameraActive = new ReactiveVar(false); + this.capturedImage = new ReactiveVar(null); + this.ocrRunning = new ReactiveVar(false); + this.ocrProgress = new ReactiveVar(0); + this.captureBillId = new ReactiveVar(null); + this.mediaStream = null; + const savedPref = localStorage.getItem('splitly_showHelp'); + this.showHelpInfo = new ReactiveVar(savedPref === null ? true : savedPref === 'true'); + this.actionLock = false; + this.autorun(() => { + const routeName = FlowRouter.current()?.route?.name; + if (routeName !== 'dashboard' && this.mediaStream) { + try { + this.mediaStream.getTracks().forEach(t => t.stop()); + } catch { + /* ignore */ + } + this.mediaStream = null; + this.cameraActive.set(false); + } + }); +}); + +Template.Dashboard.onRendered(function () { + const tpl = this; + // Handle disabled state for buttons based on reactive state + this.autorun(() => { + const isProcessing = tpl.ocrProcessing.get(); + Tracker.afterFlush(() => { + const scanBtn = document.getElementById('scanBillBtn'); + const uploadBtn = document.getElementById('uploadReceiptBtn'); + if (scanBtn) { + scanBtn.disabled = isProcessing; + } + if (uploadBtn) { + uploadBtn.disabled = isProcessing; + } + }); + }); +}); + +Template.Dashboard.helpers({ + showHelpInfo() { + return Template.instance().showHelpInfo.get(); + }, + hasRecentBills() { + return Bills.find().count() > 0; + }, + recentBills() { + return Bills.find({}, { sort: { createdAt: -1 }, limit: 3 }).fetch().map(b => ({ _id: b._id, createdAt: b.createdAt.toLocaleString(), total: computeExpenseSummary(b).grandTotal.toFixed(2), itemCount: b.items?.length || 0, userCount: b.users?.length || 0 })); + }, + ocrProcessing() { + return Template.instance().ocrProcessing.get(); + }, + cameraActive() { + return Template.instance().cameraActive.get(); + }, + capturedImage() { + return Template.instance().capturedImage.get(); + }, + ocrRunning() { + return Template.instance().ocrRunning.get(); + }, + ocrProgress() { + return Template.instance().ocrProgress.get(); + }, +}); + +Template.Dashboard.events({ + 'click #addPeopleBtn'(e, _tpl) { + e.preventDefault(); + const modalEl = document.getElementById('userModal'); + if (!modalEl) { + console.error('UserModal element not found'); + pushAlert('error', 'Modal not available'); + return; + } + if (!window.bootstrap || !window.bootstrap.Modal) { + console.error('Bootstrap Modal not available'); + pushAlert('error', 'Bootstrap not loaded'); + return; + } + try { + const modal = new window.bootstrap.Modal(modalEl); + modal.show(); + } catch (error) { + console.error('Error opening modal:', error); + pushAlert('error', 'Failed to open modal'); + } + }, + 'click #showHelpBtn'(e, tpl) { + tpl.showHelpInfo.set(true); + localStorage.setItem('splitly_showHelp', 'true'); + }, + 'click #hideHelpBtn'(e, tpl) { + tpl.showHelpInfo.set(false); + localStorage.setItem('splitly_showHelp', 'false'); + }, + 'click #uploadReceiptBtn'(e, tpl) { + if (tpl.ocrProcessing.get() || tpl.actionLock) { + return; + } + if (!GlobalUsers.find().count()) { + pushAlert('error', 'Please add people first'); + return; + } + tpl.find('#receiptFileInput').click(); + }, + async 'change #receiptFileInput'(e, tpl) { + const file = e.target.files?.[0]; + if (!file) { + return; + } + if (tpl.ocrProcessing.get()) { + e.target.value = ''; + return; + } + const fileName = file.name.toLowerCase(); + const isHEIC = file.type === 'image/heic' || file.type === 'image/heif' || fileName.endsWith('.heic') || fileName.endsWith('.heif'); + if (isHEIC) { + pushAlert('warning', 'HEIC format detected. Please convert to JPEG first.'); + e.target.value = ''; + return; + } + if (!file.type.startsWith('image/')) { + pushAlert('error', 'Please upload an image file'); + e.target.value = ''; + return; + } + tpl.ocrProcessing.set(true); + tpl.actionLock = true; + pushAlert('info', 'Reading receipt with OCR...'); + try { + const result = await Tesseract.recognize(file, 'eng'); + const text = result.data.text; + try { + const billId = await Meteor.callAsync('bills.insert', { createdAt: new Date(), users: GlobalUsers.find().fetch().map(u => ({ id: u._id, name: u.name })), items: [] }); + try { + const count = await Meteor.callAsync('ocr.extract', billId, text); + tpl.ocrProcessing.set(false); + tpl.actionLock = false; + if (count > 0) { + pushAlert('success', `Extracted ${count} item${count > 1 ? 's' : ''}!`); + } else { + pushAlert('warning', 'No items found. Please add items manually.'); + } + FlowRouter.go(`/split/${billId}`); + } catch (err2) { + tpl.ocrProcessing.set(false); + tpl.actionLock = false; + pushAlert('error', err2.reason || 'Could not parse items from receipt'); + } + } catch (err) { + pushAlert('error', err.reason || 'Could not create receipt'); + tpl.ocrProcessing.set(false); + tpl.actionLock = false; + } + } catch (error) { + console.error('OCR Error:', error); + pushAlert('error', 'Failed to read receipt.'); + tpl.ocrProcessing.set(false); + tpl.actionLock = false; + } + e.target.value = ''; + }, + async 'click #scanBillBtn'(e, tpl) { + if (tpl.cameraActive.get() || tpl.actionLock) { + return; + } + if (!GlobalUsers.find().count()) { + pushAlert('error', 'Please add people first'); + return; + } + tpl.actionLock = true; + try { + const billId = await Meteor.callAsync('bills.insert', { createdAt: new Date(), users: GlobalUsers.find().fetch().map(u => ({ id: u._id, name: u.name })), items: [] }); + tpl.captureBillId.set(billId); + tpl.cameraActive.set(true); + tpl.capturedImage.set(null); + tpl.ocrRunning.set(false); + tpl.ocrProgress.set(0); + navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }).then(stream => { + tpl.mediaStream = stream; + const videoEl = tpl.find('#receiptVideo'); + if (videoEl) { + videoEl.srcObject = stream; + } + tpl.actionLock = false; + }).catch(async () => { + pushAlert('error', 'Camera access denied'); + tpl.cameraActive.set(false); + await Meteor.callAsync('bills.remove', billId); + tpl.actionLock = false; + }); + } catch (err) { + pushAlert('error', err.reason || 'Could not create receipt'); + tpl.actionLock = false; + } + }, + 'click #cancelCaptureBtn'(e, tpl) { + tpl.actionLock = false; + const billId = tpl.captureBillId.get(); + if (tpl.mediaStream) { + tpl.mediaStream.getTracks().forEach(t => t.stop()); + tpl.mediaStream = null; + } + if (!tpl.ocrRunning.get() && billId) { + Meteor.call('bills.remove', billId); + } + tpl.cameraActive.set(false); + tpl.capturedImage.set(null); + tpl.ocrRunning.set(false); + tpl.ocrProgress.set(0); + tpl.captureBillId.set(null); + }, + 'click #captureFrameBtn'(e, tpl) { + if (tpl.actionLock) { + return; + } + tpl.actionLock = true; + const videoEl = tpl.find('#receiptVideo'); + if (!videoEl) { + pushAlert('error', 'Video element missing'); + tpl.actionLock = false; + return; + } + try { + if (!videoEl.videoWidth || !videoEl.videoHeight) { + pushAlert('info', 'Initializing camera...'); + setTimeout(() => { + if (!videoEl.videoWidth) { + pushAlert('error', 'Camera not ready'); + tpl.actionLock = false; + return; + } + const canvas = document.createElement('canvas'); + canvas.width = videoEl.videoWidth; + canvas.height = videoEl.videoHeight; + const ctx = canvas.getContext('2d'); + ctx.drawImage(videoEl, 0, 0); + if (tpl.mediaStream) { + tpl.mediaStream.getTracks().forEach(t => t.stop()); + tpl.mediaStream = null; + } + tpl.capturedImage.set(canvas.toDataURL('image/jpeg', 0.88)); + tpl.actionLock = false; + }, 800); + return; + } + const canvas = document.createElement('canvas'); + canvas.width = videoEl.videoWidth; + canvas.height = videoEl.videoHeight; + const ctx = canvas.getContext('2d'); + ctx.drawImage(videoEl, 0, 0); + if (tpl.mediaStream) { + tpl.mediaStream.getTracks().forEach(t => t.stop()); + tpl.mediaStream = null; + } + tpl.capturedImage.set(canvas.toDataURL('image/jpeg', 0.92)); + tpl.actionLock = false; + } catch (ex) { + pushAlert('error', ex.message || 'Capture failed'); + tpl.actionLock = false; + } + }, + 'click #retakeBtn'(e, tpl) { + if (tpl.actionLock) { + return; + } + tpl.actionLock = true; + tpl.capturedImage.set(null); + tpl.ocrRunning.set(false); + tpl.ocrProgress.set(0); + navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }).then(stream => { + tpl.mediaStream = stream; + const videoEl = tpl.find('#receiptVideo'); + if (videoEl) { + videoEl.srcObject = stream; + } + tpl.actionLock = false; + }).catch(() => { + pushAlert('error', 'Camera access denied'); + tpl.cameraActive.set(false); + tpl.actionLock = false; + }); + }, + 'click #continueOcrBtn'(e, tpl) { + if (tpl.actionLock) { + return; + } + tpl.actionLock = true; + const imgData = tpl.capturedImage.get(); + const billId = tpl.captureBillId.get(); + if (!imgData || !billId) { + pushAlert('error', 'Nothing captured'); + tpl.actionLock = false; + return; + } + tpl.ocrRunning.set(true); + tpl.ocrProgress.set(0); + const prepCanvas = document.createElement('canvas'); + const imgEl = new Image(); + imgEl.onload = async () => { + try { + // Stage 1: Image preprocessing (0-20%) + console.log('OCR Progress: 10% - Starting preprocessing'); + tpl.ocrProgress.set(10); + await new Promise(resolve => setTimeout(resolve, 100)); // Small delay to see progress + + const scale = 2.0; + prepCanvas.width = Math.floor(imgEl.width * scale); + prepCanvas.height = Math.floor(imgEl.height * scale); + const pctx = prepCanvas.getContext('2d'); + pctx.drawImage(imgEl, 0, 0, prepCanvas.width, prepCanvas.height); + + console.log('OCR Progress: 15% - Scaling image'); + tpl.ocrProgress.set(15); + await new Promise(resolve => setTimeout(resolve, 100)); + + const imageData = pctx.getImageData(0, 0, prepCanvas.width, prepCanvas.height); + const d = imageData.data; + + // Convert to grayscale and enhance contrast + for (let i = 0; i < d.length; i += 4) { + const gray = 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2]; + // Increase contrast + const contrast = 1.5; + const factor = (259 * (contrast + 1)) / (259 - contrast); + const enhanced = factor * (gray - 128) + 128; + const clamped = Math.max(0, Math.min(255, enhanced)); + d[i] = d[i + 1] = d[i + 2] = clamped; + } + pctx.putImageData(imageData, 0, 0); + console.log('OCR Progress: 20% - Preprocessing complete'); + tpl.ocrProgress.set(20); + await new Promise(resolve => setTimeout(resolve, 100)); + + // Stage 2: OCR text recognition (20-70%) + console.log('OCR Progress: 20% - Starting text recognition'); + const worker = await Tesseract.createWorker('eng', 1, { + logger: m => { + console.log('Tesseract logger:', m); + if (m.status === 'recognizing text') { + // Map Tesseract progress (0-1) to our range (20-70%) + const progress = 20 + Math.floor(m.progress * 50); + console.log('OCR Progress:', progress + '% - Recognizing text'); + tpl.ocrProgress.set(progress); + } + } + }); + + const { data } = await worker.recognize(prepCanvas); + console.log('OCR Progress: 70% - Text recognition complete'); + tpl.ocrProgress.set(70); + await new Promise(resolve => setTimeout(resolve, 100)); + + let text = (data.text || '').toUpperCase(); + console.log('OCR Text extracted:', text); + console.log('OCR Text length:', text.length); + + // Stage 3: Parsing items (70-90%) + console.log('OCR Progress: 75% - Parsing items'); + tpl.ocrProgress.set(75); + await new Promise(resolve => setTimeout(resolve, 100)); + + try { + const count = await Meteor.callAsync('ocr.extract', billId, text); + console.log('OCR extract returned count:', count); + console.log('OCR Progress: 90% - Items extracted'); + tpl.ocrProgress.set(90); + await new Promise(resolve => setTimeout(resolve, 100)); + + // Stage 4: Complete (90-100%) + console.log('OCR Progress: 100% - Complete!'); + tpl.ocrProgress.set(100); + await new Promise(resolve => setTimeout(resolve, 500)); // Longer pause to show 100% + + tpl.ocrRunning.set(false); + tpl.actionLock = false; + if (count > 0) { + pushAlert('success', `Extracted ${count} item${count > 1 ? 's' : ''}`); + tpl.cameraActive.set(false); + tpl.capturedImage.set(null); + tpl.captureBillId.set(null); + FlowRouter.go(`/split/${billId}`); + } else { + // Try fallback parser with universal price extraction + console.log('No items extracted by OCR, trying fallback parser'); + const fallbackLines = text.split(/\n/).map(l => l.trim()).filter(Boolean); + console.log('Fallback lines:', fallbackLines.length); + const simpleItems = []; + + for (const line of fallbackLines) { + // Skip header/footer lines + if (line.match(/^(SUBTOTAL|TAX|TOTAL|BALANCE|CHANGE|VISA|CREDIT|DEBIT|CASH|STORE|CASHIER|DATE|TIME|SELF-CHECKOUT)/i)) { + continue; + } + + // Universal price patterns + const patterns = [ + /[$§](\d+)[.\s](\d{2})/, // $1.48 or $1 48 + /\b(\d{1,4})\.(\d{2})\s*[A-Z]?\s*[A-Z]?$/, // 4.99 N F + /\b(\d{1,4})\.(\d{2})\s*$/ // 4.99 + ]; + + let matched = false; + for (const pattern of patterns) { + const matches = Array.from(line.matchAll(new RegExp(pattern.source, 'g'))); + // Get the last (rightmost) price match + const m = matches.length > 0 ? matches[matches.length - 1] : null; + if (m) { + let price; + if (m[0].includes('$') || m[0].includes('§')) { + price = parseInt(m[1]) + parseInt(m[2]) / 100; + } else { + price = parseFloat(m[1] + '.' + m[2]); + } + + if (price > 0.10 && price < 2000) { + const pricePos = m.index || 0; + let name = line.substring(0, pricePos).trim(); + name = name.replace(/^[E0-9\/\s]+/, '').replace(/\s{2,}/g, ' ').trim(); + + if (name.length >= 3 && name.length <= 60) { + console.log('Fallback matched:', { line, name, price }); + simpleItems.push({ id: `fb${Date.now()}_${simpleItems.length}`, name, price }); + matched = true; + break; + } + } + } + } + } + console.log('Fallback found items:', simpleItems.length); + if (simpleItems.length) { + const existing = Bills.findOne(billId); + const users = existing?.users || []; + const mapped = simpleItems.map(i => ({ id: i.id, name: i.name, price: i.price, userIds: users.map(u => u.id), splitType: 'equal' })); + await Meteor.callAsync('bills.updateItems', billId, [...(existing?.items || []), ...mapped]); + pushAlert('warning', `OCR fallback added ${mapped.length} item${mapped.length > 1 ? 's' : ''}`); + tpl.cameraActive.set(false); + tpl.capturedImage.set(null); + tpl.captureBillId.set(null); + FlowRouter.go(`/split/${billId}`); + } else { + pushAlert('warning', 'No items found. Please add manually.'); + tpl.cameraActive.set(false); + tpl.capturedImage.set(null); + tpl.captureBillId.set(null); + FlowRouter.go(`/split/${billId}`); + } + } + } catch (err2) { + tpl.ocrRunning.set(false); + tpl.ocrProgress.set(0); + tpl.actionLock = false; + pushAlert('error', err2.reason || 'Could not parse items'); + } + } catch (err) { + tpl.ocrRunning.set(false); + tpl.ocrProgress.set(0); + console.error('OCR worker error', err); + pushAlert('error', 'OCR failed'); + tpl.actionLock = false; + } finally { + try { + await worker.terminate(); + } catch { + /* ignore */ + } + } + }; + imgEl.onerror = () => { + tpl.ocrRunning.set(false); + tpl.ocrProgress.set(0); + pushAlert('error', 'Could not load captured image'); + tpl.actionLock = false; + }; + imgEl.src = imgData; + }, +}); diff --git a/imports/ui/blaze/pages/history.html b/imports/ui/blaze/pages/history.html new file mode 100644 index 0000000..393a607 --- /dev/null +++ b/imports/ui/blaze/pages/history.html @@ -0,0 +1,86 @@ + \ No newline at end of file diff --git a/imports/ui/blaze/pages/history.js b/imports/ui/blaze/pages/history.js new file mode 100644 index 0000000..c35bca4 --- /dev/null +++ b/imports/ui/blaze/pages/history.js @@ -0,0 +1,124 @@ +/* eslint-env browser */ +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { Meteor } from 'meteor/meteor'; +import { Bills } from '/imports/api/bills'; +import { computeExpenseSummary } from '/imports/api/models'; +import './history.html'; +import { pushAlert, showConfirm } from '../layout'; + +Template.History.onCreated(function () { + this.selectedReceipts = new ReactiveVar([]); + const savedPref = localStorage.getItem('splitly_showHelp'); + this.showHelpInfo = new ReactiveVar(savedPref === null ? true : savedPref === 'true'); +}); + +Template.History.helpers({ + bills() { + return Bills.find({}, { sort: { createdAt: -1 } }).fetch().map(b => ({ _id: b._id, createdAt: b.createdAt.toLocaleString(), total: computeExpenseSummary(b).grandTotal.toFixed(2), itemCount: b.items?.length || 0, users: b.users.map(u => u.name).join(', ') })); + }, + hasBills() { + return Bills.find().count() > 0; + }, + showHelpInfo() { + return Template.instance().showHelpInfo.get(); + }, + hasSelection() { + return Template.instance().selectedReceipts.get().length > 0; + }, + selectedCount() { + return Template.instance().selectedReceipts.get().length; + }, + isSelected(id) { + return Template.instance().selectedReceipts.get().includes(id); + }, + checkedIfSelected(id) { + return Template.instance().selectedReceipts.get().includes(id) ? 'checked' : ''; + }, + checkedIfAllSelected() { + const allBills = Bills.find().fetch(); + const selected = Template.instance().selectedReceipts.get(); + return allBills.length > 0 && selected.length === allBills.length ? 'checked' : ''; + }, +}); + +Template.History.onRendered(function () { + const tpl = this; + // Handle checkbox states with autorun to avoid Blaze attribute issues + this.autorun(() => { + const selected = tpl.selectedReceipts.get(); + const allBills = Bills.find().fetch(); + + // Update individual checkboxes + document.querySelectorAll('.receipt-checkbox').forEach(cb => { + const id = cb.getAttribute('data-id'); + cb.checked = selected.includes(id); + }); + + // Update select all checkbox + const selectAllCb = document.getElementById('selectAllCheckbox'); + if (selectAllCb) { + selectAllCb.checked = allBills.length > 0 && selected.length === allBills.length; + } + }); +}); + +Template.History.events({ + 'click #hideHelpBtn'(e, tpl) { + tpl.showHelpInfo.set(false); + localStorage.setItem('splitly_showHelp', 'false'); + }, + 'change #selectAllCheckbox'(e, tpl) { + if (e.target.checked) { + tpl.selectedReceipts.set(Bills.find().fetch().map(b => b._id)); + } else { + tpl.selectedReceipts.set([]); + } + }, + 'change .receipt-checkbox'(e, tpl) { + const id = e.target.getAttribute('data-id'); + const selected = tpl.selectedReceipts.get(); + tpl.selectedReceipts.set(e.target.checked ? [...selected, id] : selected.filter(s => s !== id)); + }, + async 'click #deleteSelectedBtn'(e, tpl) { + const selected = tpl.selectedReceipts.get(); + const count = selected.length; + const ok = await showConfirm(`Delete ${count} receipt${count > 1 ? 's' : ''}? This cannot be undone.`, { okText: 'Delete', cancelText: 'Cancel' }); + if (!ok) { + return; + } + let completed = 0; + let errors = 0; + selected.forEach(id => { + Meteor.call('bills.remove', id, err => { + completed++; + if (err) { + errors++; + } + if (completed === selected.length) { + tpl.selectedReceipts.set([]); + if (!errors) { + pushAlert('success', `Deleted ${count} receipt${count > 1 ? 's' : ''}`); + } else { + pushAlert('error', `Failed to delete ${errors} receipt${errors > 1 ? 's' : ''}`); + } + } + }); + }); + }, + async 'click .delete-single-btn'(e, tpl) { + const id = e.currentTarget.getAttribute('data-id'); + const ok = await showConfirm('Delete this receipt? This cannot be undone.', { okText: 'Delete', cancelText: 'Cancel' }); + if (!ok) { + return; + } + Meteor.call('bills.remove', id, err => { + if (err) { + pushAlert('error', err.reason || 'Failed to delete receipt'); + } else { + pushAlert('success', 'Receipt deleted'); + tpl.selectedReceipts.set(tpl.selectedReceipts.get().filter(s => s !== id)); + } + }); + }, +}); diff --git a/imports/ui/blaze/pages/settings.html b/imports/ui/blaze/pages/settings.html new file mode 100644 index 0000000..87deb85 --- /dev/null +++ b/imports/ui/blaze/pages/settings.html @@ -0,0 +1,61 @@ + \ No newline at end of file diff --git a/imports/ui/blaze/pages/settings.js b/imports/ui/blaze/pages/settings.js new file mode 100644 index 0000000..70719db --- /dev/null +++ b/imports/ui/blaze/pages/settings.js @@ -0,0 +1,82 @@ +/* eslint-env browser */ +import './settings.html'; +import { Template } from 'meteor/templating'; +import { Meteor } from 'meteor/meteor'; +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; +import { pushAlert, showConfirm } from '../layout'; +import config from '/config/app.config.json'; + +function getFlag(name, def) { + const stored = localStorage.getItem('flag_' + name); + if (stored === 'true') { + return true; + } + if (stored === 'false') { + return false; + } + return (config.features || {})[name] ?? def; +} + +Template.Settings.helpers({ + showHelpEnabled() { + const saved = localStorage.getItem('splitly_showHelp'); + return saved === null ? true : saved === 'true'; + }, + analysisEnabled() { + return getFlag('analysisPage', true); + }, + indexedEnabled() { + return getFlag('indexedDbSync', true); + }, +}); + +Template.Settings.events({ + 'change #showHelpSwitch'(e) { + localStorage.setItem('splitly_showHelp', e.currentTarget.checked); + }, + 'change #analysisPageSwitch'(e) { + localStorage.setItem('flag_analysisPage', e.currentTarget.checked); + }, + 'change #indexedSwitch'(e) { + localStorage.setItem('flag_indexedDbSync', e.currentTarget.checked); + }, + async 'click #clearCacheBtn'() { + const confirmed = await showConfirm( + 'Are you sure you want to clear all data?\n\nThis will permanently delete:\n\n• All receipts and bills\n• All items\n• All people\n• All analytics data\n• IndexedDB cache\n\nThis action CANNOT be undone!', + { + okText: 'Yes, Delete Everything', + cancelText: 'Cancel' + } + ); + + if (!confirmed) { + return; + } + + try { + // Clear all data from the server + await Meteor.callAsync('clearAllData'); + + // Clear IndexedDB cache + if (window.indexedDB) { + try { + await indexedDB.deleteDatabase('splitly-bills'); + } catch (err) { + console.error('Error clearing IndexedDB:', err); + } + } + + // Clear localStorage preferences (optional - keeping settings intact) + // localStorage.clear(); + + pushAlert('success', 'All data has been cleared successfully'); + + // Redirect to home page + setTimeout(() => { + FlowRouter.go('/'); + }, 1000); + } catch (err) { + pushAlert('error', err.reason || 'Failed to clear data'); + } + }, +}); diff --git a/imports/ui/blaze/pages/splitPage.html b/imports/ui/blaze/pages/splitPage.html new file mode 100644 index 0000000..ee0bebe --- /dev/null +++ b/imports/ui/blaze/pages/splitPage.html @@ -0,0 +1,202 @@ + + diff --git a/imports/ui/blaze/pages/splitPage.js b/imports/ui/blaze/pages/splitPage.js new file mode 100644 index 0000000..67749de --- /dev/null +++ b/imports/ui/blaze/pages/splitPage.js @@ -0,0 +1,252 @@ +/* eslint-env browser */ +import './splitPage.html'; +import '/client/styles/splitPage.css'; +import { Template } from 'meteor/templating'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { Meteor } from 'meteor/meteor'; +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; +import { Bills } from '/imports/api/bills'; +import { GlobalUsers } from '/imports/api/users'; +import { computeExpenseSummary } from '/imports/api/models'; +import { formatMoney } from '/imports/api/utils'; +import { pushAlert, showConfirm } from '../layout'; + +Template.SplitPage.onCreated(function () { + this.billId = FlowRouter.getParam('id'); + this.showAddForm = new ReactiveVar(false); + this.showDeleteButtons = new ReactiveVar(false); + const savedPref = localStorage.getItem('splitly_showHelp'); + this.showHelpInfo = new ReactiveVar(savedPref === null ? true : savedPref === 'true'); + this.autorun(() => { + this.subscribe('bills.all'); + this.subscribe('globalUsers.all'); + }); +}); + +Template.SplitPage.helpers({ + bill() { + return Bills.findOne(Template.instance().billId); + }, + showHelpInfo() { + return Template.instance().showHelpInfo.get(); + }, + showDeleteButtons() { + return Template.instance().showDeleteButtons.get(); + }, + isEditMode() { + return Template.instance().showDeleteButtons.get(); + }, + hasItems() { + const b = Bills.findOne(Template.instance().billId); + return !!(b?.items?.length); + }, + hasPeople() { + const b = Bills.findOne(Template.instance().billId); + return !!(b?.users?.length); + }, + items() { + const bill = Bills.findOne(Template.instance().billId); + return bill?.items || []; + }, + itemIndex(index) { + return index + 1; + }, + totalDifference() { + const b = Bills.findOne(Template.instance().billId); + if (!b?.totalAmount || !b?.calculatedWithTax) { + return null; + } + const diff = Math.abs(b.totalAmount - b.calculatedWithTax); + return diff > 0.01 ? Number(diff.toFixed(2)).toFixed(2) : null; + }, + formatMoney(value) { + return formatMoney(value); + }, + billUsers() { + const bill = Bills.findOne(Template.instance().billId); + return bill?.users || []; + }, + isUserSelected(userId, itemId) { + const bill = Bills.findOne(Template.instance().billId); + const item = bill?.items?.find(i => i.id === itemId); + return item?.userIds?.includes(userId) || false; + }, + finalPerUserRows() { + const b = Bills.findOne(Template.instance().billId); + if (!b) { + return []; + } + const summary = computeExpenseSummary(b); + const users = b.users || []; + const tax = Number(b.taxAmount || 0); + const billTotal = typeof b.calculatedWithTax === 'number' ? b.calculatedWithTax : Number((summary.grandTotal + tax).toFixed(2)); + if (!users.length) { + return []; + } + const taxShare = users.length ? tax / users.length : 0; + const perUserMap = new Map(summary.perUser.map(p => [p.userId, p.amount])); + const rows = users.map(u => { + const itemsSubtotal = Number((perUserMap.get(u.id) || 0).toFixed(2)); + return { userId: u.id, name: u.name, itemsSubtotal, taxShareRaw: taxShare, totalRaw: itemsSubtotal + taxShare }; + }); + rows.forEach(r => { + r.taxShare = Number(r.taxShareRaw.toFixed(2)); + if (tax > 0 && Math.abs(r.totalRaw - r.itemsSubtotal) < 0.0001) { + r.totalRaw = r.itemsSubtotal + taxShare; + } + r.totalRounded = Number(r.totalRaw.toFixed(2)); + }); + const sumRounded = Number(rows.reduce((s, r) => s + r.totalRounded, 0).toFixed(2)); + const diff = Number((billTotal - sumRounded).toFixed(2)); + if (Math.abs(diff) >= 0.01) { + const sorted = [...rows].sort((a, b) => (b.totalRaw - Math.floor(b.totalRaw)) - (a.totalRaw - Math.floor(a.totalRaw))); + sorted[0].totalRounded = Number((sorted[0].totalRounded + diff).toFixed(2)); + } + rows.forEach(r => { + r._exactShare = billTotal > 0 ? (r.totalRounded / billTotal) * 100 : 0; + }); + if (billTotal > 0 && rows.length && rows.every(r => r._exactShare === 0)) { + rows.forEach(r => { + r._exactShare = (r.itemsSubtotal / (summary.grandTotal || 1)) * 100; + }); + } + const floorPercents = rows.map(r => Math.floor(r._exactShare)); + let allocated = floorPercents.reduce((s, v) => s + v, 0); + let remaining = 100 - allocated; + const remainders = rows.map((r, i) => ({ i, remainder: r._exactShare - floorPercents[i] })).sort((a, b) => b.remainder - a.remainder); + for (let k = 0; k < remainders.length && remaining > 0; k++) { + floorPercents[remainders[k].i] += 1; + remaining--; + } + rows.forEach((r, i) => { + r.sharePercentageInt = floorPercents[i]; + }); + return rows; + }, +}); + +Template.SplitPage.events({ + 'click #hideHelpBtn'(e, tpl) { + tpl.showHelpInfo.set(false); + localStorage.setItem('splitly_showHelp', 'false'); + }, + 'click #showAddFormBtn'(e, tpl) { + tpl.showAddForm.set(true); + }, + 'click #hideAddFormBtn'(e, tpl) { + tpl.showAddForm.set(false); + }, + 'click #toggleDeleteBtn'(e, tpl) { + tpl.showDeleteButtons.set(!tpl.showDeleteButtons.get()); + }, + async 'click .user-chip'(e, tpl) { + e.preventDefault(); + const btn = e.currentTarget; + const itemId = btn.getAttribute('data-item-id'); + const userId = btn.getAttribute('data-user-id'); + const billId = tpl.billId; + + try { + await Meteor.callAsync('bills.toggleUserOnItem', billId, itemId, userId); + } catch (err) { + pushAlert('error', err.reason || 'Could not update item'); + } + }, + async 'click #addItemBtn'(e, tpl) { + const name = tpl.find('#newItemName').value.trim(); + const price = parseFloat(tpl.find('#newItemPrice').value); + if (!name) { + pushAlert('error', 'Please enter an item name'); + return; + } + if (!price || price <= 0) { + pushAlert('error', 'Please enter a valid price'); + return; + } + const billId = Template.instance().billId; + const bill = Bills.findOne(billId); + const item = { id: `item${Date.now()}`, name, price, userIds: bill.users.map(u => u.id), splitType: 'equal', shares: [] }; + try { + await Meteor.callAsync('bills.addItem', billId, item); + pushAlert('success', 'Item added'); + tpl.find('#newItemName').value = ''; + tpl.find('#newItemPrice').value = ''; + tpl.showAddForm.set(false); + } catch (err) { + pushAlert('error', err.reason || 'Could not add item'); + } + }, + 'keypress #newItemName, #newItemPrice'(e, tpl) { + if (e.which === 13) { + e.preventDefault(); + tpl.find('#addItemBtn').click(); + } + }, + async 'click #addPeopleFromSplit'(e, tpl) { + e.preventDefault(); + const globalUsers = GlobalUsers.find().fetch(); + if (!globalUsers.length) { + pushAlert('error', 'Please add people in Settings first'); + return; + } + const billId = tpl.billId; + try { + for (const user of globalUsers) { + await Meteor.callAsync('bills.addUser', billId, { id: user._id, name: user.name }); + } + pushAlert('success', 'People added to bill'); + } catch (err) { + pushAlert('error', err.reason || 'Could not add people'); + } + }, + async 'click [id^="removeItem-"]'(e) { + const itemId = e.currentTarget.getAttribute('data-id'); + const b = Bills.findOne(Template.instance().billId); + const itemNameEl = e.currentTarget.closest('.item-card')?.querySelector('.item-name'); + const itemName = itemNameEl ? itemNameEl.textContent.trim() : 'item'; + const ok = await showConfirm(`Remove "${itemName}"? This cannot be undone.`, { okText: 'Delete', cancelText: 'Keep' }); + if (!ok) { + return; + } + try { + await Meteor.callAsync('bills.removeItem', b._id, itemId); + pushAlert('success', 'Item removed'); + } catch (err) { + pushAlert('error', err.reason || 'Could not remove item'); + } + }, + 'click #saveReceiptBtn'() { + const b = Bills.findOne(Template.instance().billId); + for (const item of b.items) { + if (item.splitType === 'percent' && item.shares?.length) { + const total = item.shares.reduce((sum, s) => sum + s.value, 0); + if (total < 99 || total > 101) { + pushAlert('error', `Item "${item.name}" percentages must total ~100% (currently ${total}%)`); + return; + } + } + } + pushAlert('success', 'Receipt saved!'); + FlowRouter.go('/history'); + }, + async 'click #cancelBtn'(e, tpl) { + const ok = await showConfirm('Discard this receipt? All unsaved changes will be lost.', { okText: 'Discard', cancelText: 'Cancel' }); + if (!ok) { + return; + } + const billId = tpl.billId; + if (!billId) { + FlowRouter.go('/'); + return; + } + try { + await Meteor.callAsync('bills.remove', billId); + FlowRouter.go('/'); + } catch (err) { + pushAlert('error', err.reason || 'Could not remove bill'); + } + }, + 'click #backBtn'() { + FlowRouter.go('/'); + }, +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e39e10d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4465 @@ +{ + "name": "splitly-meteor", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "splitly-meteor", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@popperjs/core": "^2.11.8", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.13.1", + "idb": "^7.1.1", + "jquery": "^3.7.1", + "meteor-node-stubs": "^1.2.3", + "tesseract.js": "^6.0.1" + }, + "devDependencies": { + "@eslint/js": "^8.57.0", + "@typescript-eslint/eslint-plugin": "^8.46.1", + "@typescript-eslint/parser": "^8.46.1", + "eslint": "^9.38.0", + "eslint-plugin-react": "^7.37.5", + "typescript": "^5.6.3" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "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, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "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, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@meteorjs/browserify-sign": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@meteorjs/browserify-sign/-/browserify-sign-4.2.6.tgz", + "integrity": "sha512-xnQRbIrjHxaVbOEbzbcdav4QDRTnfRAVHi21SPosnGNiIHTdTeGQGmTF/f7GwntxqynabSifdBHeGA7W8lIKSQ==", + "dependencies": { + "bn.js": "^5.2.1", + "brorand": "^1.1.0", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "hash-base": "~3.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1", + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@meteorjs/browserify-sign/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/@meteorjs/browserify-sign/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/@meteorjs/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/@meteorjs/browserify-sign/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/@meteorjs/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/@meteorjs/create-ecdh": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@meteorjs/create-ecdh/-/create-ecdh-4.0.5.tgz", + "integrity": "sha512-dhn3AICsDlIZ5qY/Qu+QOL+ZGKaHcGss4PQ3CfmAF3f+o5fPJ2aDJcxd5f2au2k6sxyNqvCsLAFYFHXxHoH9yQ==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/@meteorjs/create-ecdh/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==" + }, + "node_modules/@meteorjs/crypto-browserify": { + "version": "3.12.4", + "resolved": "https://registry.npmjs.org/@meteorjs/crypto-browserify/-/crypto-browserify-3.12.4.tgz", + "integrity": "sha512-K5Sgvxef93Zrw5T9cJxKuNVgpl1C2W8cfcicN6HKy98G6RoIrx6hikwWnq8FlagvOzdIQEC2s+SMn7UFNSK0eA==", + "dependencies": { + "@meteorjs/browserify-sign": "^4.2.3", + "@meteorjs/create-ecdh": "^4.0.4", + "browserify-cipher": "^1.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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/@typescript-eslint/eslint-plugin": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", + "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/type-utils": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", + "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", + "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", + "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", + "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", + "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", + "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", + "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.1", + "@typescript-eslint/tsconfig-utils": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "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/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", + "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", + "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "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-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==" + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==" + }, + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==" + }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", + "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ] + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==" + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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==", + "dev": true, + "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==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "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, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "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 + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/domain-browser": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.23.0.tgz", + "integrity": "sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "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==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "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==", + "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==", + "dev": true, + "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/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "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/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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==", + "dev": true, + "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, + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "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/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "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-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "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 + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==" + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, + "node_modules/idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "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, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" + }, + "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==", + "dev": true + }, + "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 + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "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, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "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 + }, + "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==", + "dev": true, + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/meteor-node-stubs": { + "version": "1.2.24", + "resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-1.2.24.tgz", + "integrity": "sha512-tw9QzDFVOI5A5CcEw4tTD6CjF+Lk14uzhy2gWH5ImoH4mx4pyPVcha9MmyVur+rEVgpzk+aMG6rs3RxAF9SwiA==", + "bundleDependencies": [ + "@meteorjs/crypto-browserify", + "assert", + "browserify-zlib", + "buffer", + "console-browserify", + "constants-browserify", + "domain-browser", + "events", + "https-browserify", + "os-browserify", + "path-browserify", + "process", + "punycode", + "querystring-es3", + "readable-stream", + "stream-browserify", + "stream-http", + "string_decoder", + "timers-browserify", + "tty-browserify", + "url", + "util", + "vm-browserify" + ], + "dependencies": { + "@meteorjs/crypto-browserify": "^3.12.1", + "assert": "^2.1.0", + "browserify-zlib": "^0.2.0", + "buffer": "^5.7.1", + "console-browserify": "^1.2.0", + "constants-browserify": "^1.0.0", + "domain-browser": "^4.23.0", + "events": "^3.3.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "process": "^0.11.10", + "punycode": "^1.4.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^3.6.2", + "sha.js": "^2.4.12", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "string_decoder": "^1.3.0", + "timers-browserify": "^2.0.12", + "tty-browserify": "0.0.1", + "url": "^0.11.4", + "util": "^0.12.5", + "vm-browserify": "^1.1.2" + } + }, + "node_modules/meteor-node-stubs/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "inBundle": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==" + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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==", + "dev": true + }, + "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 + }, + "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==", + "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-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "bin": { + "opencollective-postinstall": "index.js" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "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/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==" + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "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, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "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/parse-asn1": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/pbkdf2": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "engines": { + "node": ">= 0.4" + } + }, + "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, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==" + }, + "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/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "engines": { + "node": ">=0.4.x" + } + }, + "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==", + "dev": true, + "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/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/ripemd160/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/ripemd160/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/ripemd160/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ripemd160/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "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==", + "dev": true, + "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/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tesseract.js": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-6.0.1.tgz", + "integrity": "sha512-/sPvMvrCtgxnNRCjbTYbr7BRu0yfWDsMZQ2a/T5aN/L1t8wUQN6tTWv6p6FwzpoEBA0jrN2UD2SX4QQFRdoDbA==", + "hasInstallScript": true, + "dependencies": { + "bmp-js": "^0.1.0", + "idb-keyval": "^6.2.0", + "is-url": "^1.2.4", + "node-fetch": "^2.6.9", + "opencollective-postinstall": "^2.0.3", + "regenerator-runtime": "^0.13.3", + "tesseract.js-core": "^6.0.0", + "wasm-feature-detect": "^1.2.11", + "zlibjs": "^0.3.1" + } + }, + "node_modules/tesseract.js-core": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz", + "integrity": "sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==" + }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "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==" + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==" + }, + "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, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" + }, + "node_modules/wasm-feature-detect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", + "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==" + }, + "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==" + }, + "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==", + "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, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zlibjs": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz", + "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==", + "engines": { + "node": "*" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..36b166a --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "splitly-meteor", + "version": "1.0.0", + "description": "Offline-friendly expense splitting app built with Meteor + Blaze + Bootstrap", + "private": true, + "scripts": { + "start": "meteor run", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "build": "meteor build ../output --directory", + "deploy": "meteor deploy splitly.meteorapp.com" + }, + "dependencies": { + "@babel/runtime": "^7.28.4", + "@popperjs/core": "^2.11.8", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.13.1", + "idb": "^7.1.1", + "jquery": "^3.7.1", + "meteor-node-stubs": "^1.2.3", + "tesseract.js": "^6.0.1" + }, + "devDependencies": { + "@eslint/js": "^8.57.0", + "@typescript-eslint/eslint-plugin": "^8.46.1", + "@typescript-eslint/parser": "^8.46.1", + "eslint": "^9.38.0", + "eslint-plugin-react": "^7.37.5", + "typescript": "^5.6.3" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..f569b10 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/server/main.ts b/server/main.ts new file mode 100644 index 0000000..7667a55 --- /dev/null +++ b/server/main.ts @@ -0,0 +1,8 @@ +import { Meteor } from 'meteor/meteor'; +import '/imports/api/bills'; +import '/imports/api/users'; +import '/imports/api/publications'; + +Meteor.startup(() => { + // Any server init logic here +}); diff --git a/types/meteor.d.ts b/types/meteor.d.ts new file mode 100644 index 0000000..36b48b3 --- /dev/null +++ b/types/meteor.d.ts @@ -0,0 +1,52 @@ +// Ambient Meteor + Project types +// Minimal augmentation to reduce implicit any usage in JS/TS parts +// These declarations assist mixed JS/TS without forcing full type coverage. + +// Declare minimal Mongo namespace to satisfy Collection generic reference when types not available. +declare namespace Mongo { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + class Collection { + find(selector?: any, options?: any): any; + findOne(idOrSelector: any, options?: any): T | undefined; + insert(doc: T): string; + update(idOrSelector: any, modifier: any, options?: any): number; + remove(idOrSelector: any): number; + forEach?(): void; // placeholder + } +} + +interface GlobalUserDoc { + _id?: string; + name: string; + email?: string; + createdAt: Date; + updatedAt?: Date; +} + +interface BillUserProfile { id: string; name: string; } +interface BillItemSharePercent { userId: string; type: 'percent'; value: number; } +interface BillItemShareFixed { userId: string; type: 'fixed'; value: number; } +interface BillItem { + id: string; + name: string; + price: number; + userIds: string[]; + splitType?: 'equal' | 'percent' | 'fixed'; + shares?: (BillItemSharePercent | BillItemShareFixed)[]; +} +interface BillDoc { + _id?: string; + createdAt: Date; + updatedAt?: Date; + users: BillUserProfile[]; + items: BillItem[]; + receiptTotal?: number | null; + calculatedTotal?: number | null; + calculatedWithTax?: number | null; + taxAmount?: number | null; + totalAmount?: number | null; + currency?: string; +} + +declare const Bills: Mongo.Collection; +declare const GlobalUsers: Mongo.Collection;