diff --git a/.env.ios b/.env.ios new file mode 100644 index 0000000..7a2b25a --- /dev/null +++ b/.env.ios @@ -0,0 +1,8 @@ +# iOS / Capacitor build environment +# Used by: VITE_IOS_BUILD=true npm run build +# or: vite build --mode ios (requires .env.ios to be named .env.ios) +# +# Supabase vars are intentionally absent — iOS build is local-only storage. +# No user data leaves the device, keeping App Store privacy declarations clean. + +VITE_IOS_BUILD=true diff --git a/CHANGELOG.md b/CHANGELOG.md index ea420bd..db42f02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Reverted Xcode project filename from `TimeTrackerPro.xcodeproj` back to `App.xcodeproj` + — `ios/App/App.xcodeproj/` (Capacitor CLI hardcodes `App.xcodeproj`; renaming it broke `npm run sync:ios` with ENOENT on `project.pbxproj`; the Xcode target/product name remains "TimeTrackerPro") +- Fixed `open:ios` npm script pointing to the old renamed project path + — `package.json` (updated from `TimeTrackerPro.xcodeproj/project.xcworkspace` to `App.xcodeproj/project.xcworkspace`) +- Fixed `vite-plugin-pwa` not disabling PWA during iOS builds due to wrong option name + — `vite.config.ts` (changed `disabled: isIosBuild` → `disable: isIosBuild`; plugin uses `disable`, not `disabled`, so `sw.js`, `workbox-*.js`, and `manifest.webmanifest` were being generated and synced into the iOS bundle on every build) +- Fixed outdated device capability declaration in iOS Info.plist + — `ios/App/App/Info.plist` (changed `UIRequiredDeviceCapabilities` from `armv7` to `arm64`; Apple dropped 32-bit support in iOS 11 and this stale Capacitor template value can fail App Store validation) +- Removed stale `-DCOCOAPODS` Swift compiler flag from Xcode Debug build settings + — `ios/App/App.xcodeproj/project.pbxproj` (project uses SPM, not CocoaPods; flag was a leftover from Capacitor's original CocoaPods template with no runtime effect) + +### Added +- Capacitor iOS native app scaffolding (Phase 2) + — `capacitor.config.ts`, `ios/` Xcode project, `package.json` (appId `com.adamjolicoeur.timetrackerpro`, iOS 15+ minimum via SPM, `sync:ios` script combines `build:ios` + `cap sync ios`; `ios/App/App/public` gitignored and regenerated on every sync) +- Renamed Xcode target and product from "App" to "TimeTrackerPro" + — `ios/App/App.xcodeproj/project.pbxproj` (updated target name, productName, product path, and configuration list comments so Xcode shows "TimeTrackerPro"; project file itself remains `App.xcodeproj` per Capacitor's requirement) +- Capacitor iOS integration prep (Phase 1) + — `src/App.tsx`, `src/components/Navigation.tsx`, `src/pages/Settings.tsx`, `vite.config.ts`, `.env.ios`, `index.html`, `package.json` (BrowserRouter → HashRouter for filesystem loading; `VITE_IOS_BUILD` flag disables PWA SW and hides auth/sync UI in native builds; CSP updated with `capacitor://localhost`; `build:ios` npm script added) +- `PageLayout` shared layout component for consistent page chrome + — `src/components/PageLayout.tsx`, `src/components/PageLayout.test.tsx` (standardizes title + optional actions slot across all six pages; all page components migrated to use it) + +### Fixed +- Carry over incomplete GFM checklist items as todo tasks when a day is archived + — `src/contexts/TimeTrackingContext.tsx`, `src/contexts/TimeTracking.test.tsx` (unchecked `- [ ]` items from task descriptions are now extracted and appended as new todo tasks on archive; unique IDs and safe functional setState ensure no data loss on rollback) + ### Accessibility - Added `aria-label` to all icon-only buttons whose visible text label is hidden on mobile viewports: Restore and Edit in `ArchiveItem`, Restore/Delete/Edit in `ArchiveEditDialog` header, per-task Edit/Delete in `ArchiveEditDialog` task table, and Edit/Delete in `ProjectManagement` - Replaced `focus:outline-none` with `focus-visible:outline-none` + `focus-visible:ring-2 focus-visible:ring-ring` on Radix `TabsTrigger` elements in `ArchiveItem` — the browser focus ring was previously stripped for all input methods; it is now suppressed only for pointer clicks while remaining fully visible for keyboard navigation diff --git a/CLAUDE.md b/CLAUDE.md index 735556d..b3bf5d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,9 +1,9 @@ # CLAUDE.md - AI Assistant Codebase Guide -**Last Updated:** 2026-03-09 -**Version:** 2.0.0 +**Last Updated:** 2026-04-24 +**Version:** 2.1.0 -TimeTracker Pro is a React 18 + TypeScript time tracking PWA for freelancers and consultants, with dual storage (localStorage guest mode and optional Supabase cloud sync). +TimeTracker Pro is a React 18 + TypeScript time tracking PWA for freelancers and consultants, with dual storage (localStorage guest mode and optional Supabase cloud sync). A native iOS app is also available via Capacitor. --- @@ -19,6 +19,7 @@ TimeTracker Pro is a React 18 + TypeScript time tracking PWA for freelancers and | Forms | React Hook Form + Zod | | Backend | Supabase (optional) or localStorage | | PWA | Vite PWA Plugin + Workbox | +| Native iOS | Capacitor 8 (@capacitor/core + @capacitor/ios) | | Testing | Vitest + React Testing Library + Playwright | --- @@ -57,6 +58,42 @@ export const MyComponent = () => { | `src/lib/supabase.ts` | Supabase client configuration and caching | | `src/config/categories.ts` | Default category definitions | | `src/config/projects.ts` | Default project definitions | +| `src/components/PageLayout.tsx` | Shared page chrome (title + optional actions slot) | +| `capacitor.config.ts` | Capacitor iOS configuration | +| `.env.ios` | iOS build env (VITE_IOS_BUILD=true, no Supabase) | + +--- + +## Capacitor iOS Build + +The app ships as both a PWA and a native iOS app via Capacitor 8. + +**Key differences in iOS builds:** + +- `VITE_IOS_BUILD=true` disables the Vite PWA service worker and hides auth/sync UI (UserMenu, AuthDialog, SyncStatus, InstallPrompt, UpdateNotification) +- Routing uses `HashRouter` (required — Capacitor loads from filesystem, not a server) +- CSP includes `capacitor://localhost` for WKWebView asset loading +- Data storage is localStorage-only (no Supabase keys in `.env.ios`) + +**iOS npm scripts:** + +```bash +npm run build:ios # vite build --mode ios (outputs to dist/) +npm run sync:ios # build:ios + npx cap sync ios (copies dist/ into ios/ project) +``` + +**Working with the Xcode project:** + +- `ios/App/App/public` is gitignored — it is regenerated by `cap sync ios` +- Open `ios/App/App.xcodeproj/project.xcworkspace` in Xcode (or run `npm run open:ios`) +- Bundle ID: `com.adamjolicoeur.timetrackerpro` +- Minimum iOS version: 26 (enforced via Package.swift SPM) + +**When adding new features:** + +- Gate any web-only UI (PWA install, auth, sync) behind `import.meta.env.VITE_IOS_BUILD !== "true"` +- Avoid `window.location.reload()` in iOS paths — use `window.location.replace()` to avoid interrupting the Capacitor JS bridge +- Test localStorage-only flow (no Supabase) before marking iOS features complete --- diff --git a/README.md b/README.md index 9c9ca3b..9914ee8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A modern, feature-rich Progressive Web App (PWA) for time tracking built with React, TypeScript, and Tailwind CSS. Installable on desktop and mobile devices with full offline support. Perfect for freelancers, consultants, and professionals who need to track time, manage projects, and generate invoices. -![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) ![Vite](https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white) ![TailwindCSS](https://img.shields.io/badge/tailwindcss-%2338B2AC.svg?style=for-the-badge&logo=tailwind-css&logoColor=white) ![Supabase](https://img.shields.io/badge/Supabase-3ECF8E?style=for-the-badge&logo=supabase&logoColor=white) ![PWA](https://img.shields.io/badge/PWA-Enabled-5A0FC8?style=for-the-badge&logo=pwa&logoColor=white) +![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) ![Vite](https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white) ![TailwindCSS](https://img.shields.io/badge/tailwindcss-%2338B2AC.svg?style=for-the-badge&logo=tailwind-css&logoColor=white) ![Supabase](https://img.shields.io/badge/Supabase-3ECF8E?style=for-the-badge&logo=supabase&logoColor=white) ![PWA](https://img.shields.io/badge/PWA-Enabled-5A0FC8?style=for-the-badge&logo=pwa&logoColor=white) ![Capacitor](https://img.shields.io/badge/Capacitor-119EFF?style=for-the-badge&logo=capacitor&logoColor=white) ## 📑 Table of Contents @@ -89,6 +89,7 @@ TimeTracker Pro is a professional time tracking application that helps you monit - **Mobile Optimized** - Touch-friendly with bottom navigation - **Auto-Updates** - New versions install seamlessly - **Cross-Platform** - Works on Windows, Mac, Linux, iOS, and Android +- **Native iOS App** - Distributed as a native iOS app via Capacitor (App Store / sideload) --- @@ -150,9 +151,7 @@ That's it! No complex configuration required. 1. Click "Start Day" button to begin tracking 2. The timer starts automatically -**Throughout the Day:** -3. Click "New Task" to create a task -4. Fill in task details: +**Throughout the Day:** 3. Click "New Task" to create a task 4. Fill in task details: - **Title** (required) - Brief description of the work - **Description** (optional) - Detailed notes with markdown support @@ -162,10 +161,7 @@ That's it! No complex configuration required. 1. Task duration calculates automatically 2. Create new tasks as you switch between different work items -**Evening:** -7. Click "End Day" when you're finished working -8. Review your day summary (total time, revenue, task breakdown) -9. Click "Post Time to Archive" to save permanently +**Evening:** 7. Click "End Day" when you're finished working 8. Review your day summary (total time, revenue, task breakdown) 9. Click "Post Time to Archive" to save permanently **Ongoing:** @@ -241,7 +237,7 @@ Task descriptions support **GitHub Flavored Markdown (GFM)** for rich formatting **Supported Features:** -- **Bold** and *italic* text +- **Bold** and _italic_ text - Lists (bulleted and numbered) - Task lists with checkboxes - Tables @@ -259,11 +255,13 @@ Task descriptions support **GitHub Flavored Markdown (GFM)** for rich formatting **Attendees:** John, Sarah, Mike **Topics Discussed:** + 1. Q1 project timeline 2. Budget approval 3. Design mockups **Action Items:** + - [ ] Send meeting minutes - [ ] Update project roadmap - [ ] Schedule follow-up @@ -293,7 +291,7 @@ This renders beautifully in the task view with proper formatting and styling. 2. Tap the Share button (□↑) 3. Scroll and tap "Add to Home Screen" 4. Tap "Add" to confirm - - Toggle "Open as Web App" to "On" + - Toggle "Open as Web App" to "On" 5. Find the app icon on your home screen **Android (Chrome):** @@ -409,6 +407,10 @@ npm run screenshots:install # Install Playwright browsers (first time) npm run screenshots # Capture PWA screenshots (headless) npm run screenshots:headed # Capture screenshots with visible browser +# iOS / Capacitor +npm run build:ios # Build for iOS (vite --mode ios, no PWA/auth UI) +npm run sync:ios # build:ios + cap sync ios (copies dist into Xcode project) + # CSV Import Testing npm run test-csv-import # Test standard CSV import npm run test-full-import # Test full CSV import functionality @@ -444,6 +446,11 @@ npm run test-error-handling # Test CSV error handling - **Local Storage** - Browser storage for offline data persistence - **Supabase** (optional) - PostgreSQL database and authentication +**Native Mobile:** + +- **Capacitor 8** - Native iOS app wrapper (appId: `com.adamjolicoeur.timetrackerpro`, iOS 15+ minimum) +- **iOS-only build mode** - `VITE_IOS_BUILD` flag disables PWA/auth UI for native distribution + **PWA & Performance:** - **Vite PWA Plugin** - Service worker and manifest generation @@ -476,15 +483,15 @@ Data persistence is abstracted through a service interface with two implementati ```typescript interface DataService { - loadCurrentDay(): Promise - saveCurrentDay(data: DayData): Promise - loadArchivedDays(): Promise - saveArchivedDays(days: ArchivedDay[]): Promise + loadCurrentDay(): Promise; + saveCurrentDay(data: DayData): Promise; + loadArchivedDays(): Promise; + saveArchivedDays(days: ArchivedDay[]): Promise; // ... other methods } // Factory pattern selects implementation -const service = createDataService(isAuthenticated) +const service = createDataService(isAuthenticated); // Returns: LocalStorageService OR SupabaseService ``` @@ -958,14 +965,14 @@ export default { color: '#333', a: { color: '#3182ce', - '&:hover': { color: '#2c5282' }, - }, - }, - }, - }, - }, - }, -} + '&:hover': { color: '#2c5282' } + } + } + } + } + } + } +}; ``` **3. Component-Specific Overrides:** @@ -978,7 +985,7 @@ export default { {children} - ), + ) }} > {content} @@ -995,7 +1002,7 @@ export default { // 3. Use shadcn/ui components // 4. Import in parent component -import { MyFeature } from "@/components/MyFeature"; +import { MyFeature } from '@/components/MyFeature'; ``` **Adding a New Page:** @@ -1069,25 +1076,26 @@ const MyPage = lazy(() => import("./pages/MyPage")); For a detailed list of changes, new features, and bug fixes, see [CHANGELOG.md](CHANGELOG.md). **Recent Updates:** + +- Native iOS app via Capacitor 8 — Xcode project scaffolded, iOS-specific build mode (`build:ios` / `sync:ios`) with auth/PWA UI disabled for native distribution +- `PageLayout` shared component standardizes page chrome (title + actions) across all pages +- Incomplete checklist items in task descriptions are now carried over as todo tasks when a day is archived - Improved Weekly Report error messages to distinguish Gemini API failure modes (rate limit, quota, overload, key issues) - Fixed Weekly Report for authenticated users (data now sourced from Supabase, not localStorage only) - Native HTML5 time inputs for intuitive, accessible time selection -- Consistent UX with date inputs across all dialogs -- Mobile-optimized with browser-native time pickers -- Full keyboard navigation and screen reader support --- ## 📱 iOS Screenshots -| View | Image | -| -- | -- | -| Dashboard | | +| View | Image | +| --------------------- | ------------------------------------------------------------------ | +| Dashboard | | | Time Entry - Markdown | | -| Time Entry - Preview | | -| Active Tasks | | -| Day Ended | | -| Archive | | +| Time Entry - Preview | | +| Active Tasks | | +| Day Ended | | +| Archive | | --- diff --git a/capacitor.config.ts b/capacitor.config.ts new file mode 100644 index 0000000..cf1da34 --- /dev/null +++ b/capacitor.config.ts @@ -0,0 +1,33 @@ +import type { CapacitorConfig } from "@capacitor/cli"; + +const config: CapacitorConfig = { + appId: "com.adamjolicoeur.timetrackerpro", + appName: "TimeTracker Pro", + webDir: "dist", + + ios: { + // Respect safe-area insets (notch, home indicator) automatically + contentInset: "always", + // Match the app's background so there is no flash during launch + backgroundColor: "#ffffff", + // Prevent the WKWebView itself from scrolling — the app manages scroll internally + scrollEnabled: false, + // Restricts navigation to the app bundle; blocks accidental external loads + limitsNavigationsToAppBoundDomains: true, + }, + + experimental: { + ios: { + spm: { + // iOS 26 requires PackageDescription 6.2; without this cap sync defaults to 5.9 + swiftToolsVersion: "6.2", + }, + }, + }, + + plugins: { + // Placeholder — native plugin config goes here in Phase 4 (widget bridge) + }, +}; + +export default config; diff --git a/index.html b/index.html index 2a9b559..6d0d17c 100644 --- a/index.html +++ b/index.html @@ -36,15 +36,16 @@ + "; }; + 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; + 504EC3041FED79650016851F /* TimeTrackerPro.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TimeTrackerPro.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; + 958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 504EC3011FED79650016851F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + files = ( + 4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */, + ); + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 504EC2FB1FED79650016851F = { + isa = PBXGroup; + children = ( + 958DCC722DB07C7200EA8C5F /* debug.xcconfig */, + 504EC3061FED79650016851F /* App */, + 504EC3051FED79650016851F /* Products */, + ); + sourceTree = ""; + }; + 504EC3051FED79650016851F /* Products */ = { + isa = PBXGroup; + children = ( + 504EC3041FED79650016851F /* TimeTrackerPro.app */, + ); + name = Products; + sourceTree = ""; + }; + 504EC3061FED79650016851F /* App */ = { + isa = PBXGroup; + children = ( + 50379B222058CBB4000EE86E /* capacitor.config.json */, + 504EC3071FED79650016851F /* AppDelegate.swift */, + 504EC30B1FED79650016851F /* Main.storyboard */, + 504EC30E1FED79650016851F /* Assets.xcassets */, + 504EC3101FED79650016851F /* LaunchScreen.storyboard */, + 504EC3131FED79650016851F /* Info.plist */, + 2FAD9762203C412B000D30F8 /* config.xml */, + 50B271D01FEDC1A000F3C39B /* public */, + ); + path = App; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 504EC3031FED79650016851F /* TimeTrackerPro */ = { + isa = PBXNativeTarget; + buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "TimeTrackerPro" */; + buildPhases = ( + 504EC3001FED79650016851F /* Sources */, + 504EC3011FED79650016851F /* Frameworks */, + 504EC3021FED79650016851F /* Resources */, + ); + buildRules = ( + ); + name = TimeTrackerPro; + packageProductDependencies = ( + 4D22ABE82AF431CB00220026 /* CapApp-SPM */, + ); + productName = TimeTrackerPro; + productReference = 504EC3041FED79650016851F /* TimeTrackerPro.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 504EC2FC1FED79650016851F /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 2630; + LastUpgradeCheck = 2630; + TargetAttributes = { + 504EC3031FED79650016851F = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "TimeTrackerPro" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 504EC2FB1FED79650016851F; + packageReferences = ( + D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */, + ); + preferredProjectObjectVersion = 100; + productRefGroup = 504EC3051FED79650016851F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 504EC3031FED79650016851F /* TimeTrackerPro */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 504EC3021FED79650016851F /* Resources */ = { + isa = PBXResourcesBuildPhase; + files = ( + 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */, + 50B271D11FEDC1A000F3C39B /* public in Resources */, + 504EC30F1FED79650016851F /* Assets.xcassets in Resources */, + 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */, + 504EC30D1FED79650016851F /* Main.storyboard in Resources */, + 2FAD9763203C412B000D30F8 /* config.xml in Resources */, + ); + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 504EC3001FED79650016851F /* Sources */ = { + isa = PBXSourcesBuildPhase; + files = ( + 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, + ); + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 504EC30B1FED79650016851F /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC30C1FED79650016851F /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 504EC3101FED79650016851F /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC3111FED79650016851F /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 504EC3141FED79650016851F /* Debug configuration for PBXProject "TimeTrackerPro" */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 504EC3151FED79650016851F /* Release configuration for PBXProject "TimeTrackerPro" */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 504EC3171FED79650016851F /* Debug configuration for PBXNativeTarget "TimeTrackerPro" */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = J7D3ZTJ7SH; + INFOPLIST_FILE = App/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "TimeTracker Pro"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_SWIFT_FLAGS = "$(inherited) \"-DDEBUG\""; + PRODUCT_BUNDLE_IDENTIFIER = com.adamjolicoeur.timetrackerpro; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 504EC3181FED79650016851F /* Release configuration for PBXNativeTarget "TimeTrackerPro" */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = J7D3ZTJ7SH; + INFOPLIST_FILE = App/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "TimeTracker Pro"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.adamjolicoeur.timetrackerpro; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "TimeTrackerPro" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3141FED79650016851F /* Debug configuration for PBXProject "TimeTrackerPro" */, + 504EC3151FED79650016851F /* Release configuration for PBXProject "TimeTrackerPro" */, + ); + defaultConfigurationName = Release; + }; + 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "TimeTrackerPro" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3171FED79650016851F /* Debug configuration for PBXNativeTarget "TimeTrackerPro" */, + 504EC3181FED79650016851F /* Release configuration for PBXNativeTarget "TimeTrackerPro" */, + ); + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "CapApp-SPM"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 4D22ABE82AF431CB00220026 /* CapApp-SPM */ = { + isa = XCSwiftPackageProductDependency; + package = D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */; + productName = "CapApp-SPM"; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 504EC2FC1FED79650016851F /* Project object */; +} diff --git a/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..6e461b7 --- /dev/null +++ b/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "b7d16c968312d279d4245bcf6153959e4c21bf85ca1557fa871c62e5b8fabfd0", + "pins" : [ + { + "identity" : "capacitor-swift-pm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ionic-team/capacitor-swift-pm.git", + "state" : { + "revision" : "f1a8fadf1437c23b825c818fb6509c9dbbae2f61", + "version" : "8.3.1" + } + } + ], + "version" : 3 +} diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift new file mode 100644 index 0000000..f9ef495 --- /dev/null +++ b/ios/App/App/AppDelegate.swift @@ -0,0 +1,26 @@ +import UIKit +import Capacitor + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // Called when the app was launched with a url. Feel free to add additional processing here, + // but if you want the App API to support tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(app, open: url, options: options) + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + // Called when the app was launched with an activity, including Universal Links. + // Feel free to add additional processing here, but if you want the App API to support + // tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) + } + +} diff --git a/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..e9c0651 --- /dev/null +++ b/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "favicon.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/App/App/Assets.xcassets/AppIcon.appiconset/favicon.png b/ios/App/App/Assets.xcassets/AppIcon.appiconset/favicon.png new file mode 100644 index 0000000..b15d433 Binary files /dev/null and b/ios/App/App/Assets.xcassets/AppIcon.appiconset/favicon.png differ diff --git a/ios/App/App/Assets.xcassets/Contents.json b/ios/App/App/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/ios/App/App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json b/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json new file mode 100644 index 0000000..09837de --- /dev/null +++ b/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "splash-screen.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "splash-screen 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "splash-screen 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/App/App/Assets.xcassets/Splash.imageset/splash-screen 1.png b/ios/App/App/Assets.xcassets/Splash.imageset/splash-screen 1.png new file mode 100644 index 0000000..a8ae1aa Binary files /dev/null and b/ios/App/App/Assets.xcassets/Splash.imageset/splash-screen 1.png differ diff --git a/ios/App/App/Assets.xcassets/Splash.imageset/splash-screen 2.png b/ios/App/App/Assets.xcassets/Splash.imageset/splash-screen 2.png new file mode 100644 index 0000000..a8ae1aa Binary files /dev/null and b/ios/App/App/Assets.xcassets/Splash.imageset/splash-screen 2.png differ diff --git a/ios/App/App/Assets.xcassets/Splash.imageset/splash-screen.png b/ios/App/App/Assets.xcassets/Splash.imageset/splash-screen.png new file mode 100644 index 0000000..a8ae1aa Binary files /dev/null and b/ios/App/App/Assets.xcassets/Splash.imageset/splash-screen.png differ diff --git a/ios/App/App/Base.lproj/LaunchScreen.storyboard b/ios/App/App/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..2aa21bb --- /dev/null +++ b/ios/App/App/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/App/App/Base.lproj/Main.storyboard b/ios/App/App/Base.lproj/Main.storyboard new file mode 100644 index 0000000..b44df7b --- /dev/null +++ b/ios/App/App/Base.lproj/Main.storyboard @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist new file mode 100644 index 0000000..6329b07 --- /dev/null +++ b/ios/App/App/Info.plist @@ -0,0 +1,51 @@ + + + + + CAPACITOR_DEBUG + $(CAPACITOR_DEBUG) + CFBundleDevelopmentRegion + en + CFBundleDisplayName + TimeTracker Pro + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/App/CapApp-SPM/.gitignore b/ios/App/CapApp-SPM/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/ios/App/CapApp-SPM/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/ios/App/CapApp-SPM/Package.swift b/ios/App/CapApp-SPM/Package.swift new file mode 100644 index 0000000..1b80b91 --- /dev/null +++ b/ios/App/CapApp-SPM/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 6.2 +import PackageDescription + +// DO NOT MODIFY THIS FILE - managed by Capacitor CLI commands +let package = Package( + name: "CapApp-SPM", + platforms: [.iOS(.v26)], + products: [ + .library( + name: "CapApp-SPM", + targets: ["CapApp-SPM"]) + ], + dependencies: [ + .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.3.1") + ], + targets: [ + .target( + name: "CapApp-SPM", + dependencies: [ + .product(name: "Capacitor", package: "capacitor-swift-pm"), + .product(name: "Cordova", package: "capacitor-swift-pm") + ] + ) + ] +) diff --git a/ios/App/CapApp-SPM/README.md b/ios/App/CapApp-SPM/README.md new file mode 100644 index 0000000..03964db --- /dev/null +++ b/ios/App/CapApp-SPM/README.md @@ -0,0 +1,5 @@ +# CapApp-SPM + +This package is used to host SPM dependencies for your Capacitor project + +Do not modify the contents of it or there may be unintended consequences. diff --git a/ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift b/ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift new file mode 100644 index 0000000..945afec --- /dev/null +++ b/ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift @@ -0,0 +1 @@ +public let isCapacitorApp = true diff --git a/ios/debug.xcconfig b/ios/debug.xcconfig new file mode 100644 index 0000000..53ce18d --- /dev/null +++ b/ios/debug.xcconfig @@ -0,0 +1 @@ +CAPACITOR_DEBUG = true diff --git a/package-lock.json b/package-lock.json index 6573ce4..09024ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "time-tracker", "version": "0.34.0", "dependencies": { + "@capacitor/core": "^8.3.1", + "@capacitor/ios": "^8.3.1", "@hookform/resolvers": "^3.9.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", @@ -62,6 +64,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@capacitor/cli": "^8.3.1", "@eslint/js": "^9.9.0", "@playwright/test": "^1.56.1", "@tailwindcss/typography": "^0.5.15", @@ -1691,6 +1694,92 @@ "node": ">=6.9.0" } }, + "node_modules/@capacitor/cli": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.3.1.tgz", + "integrity": "sha512-1sPGW4THTDfR6YjXwZ0jM7oAfAtciPOHN00qs/3sNAQx1kKrrEYSfDPwCm1/xlAgi0OeL69SiRfw314Ans+1sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/cli-framework-output": "^2.2.8", + "@ionic/utils-subprocess": "^3.0.1", + "@ionic/utils-terminal": "^2.3.5", + "commander": "^12.1.0", + "debug": "^4.4.0", + "env-paths": "^2.2.0", + "fs-extra": "^11.2.0", + "kleur": "^4.1.5", + "native-run": "^2.0.3", + "open": "^8.4.0", + "plist": "^3.1.0", + "prompts": "^2.4.2", + "rimraf": "^6.0.1", + "semver": "^7.6.3", + "tar": "^7.5.3", + "tslib": "^2.8.1", + "xml2js": "^0.6.2" + }, + "bin": { + "cap": "bin/capacitor", + "capacitor": "bin/capacitor" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@capacitor/cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@capacitor/cli/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@capacitor/cli/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@capacitor/core": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.1.tgz", + "integrity": "sha512-UF8ItlHguU1Z6GXfPTeT2gakf+ctNI8pAS1kwSBQlsJMlfD4OPoto/SmKnOxKCQvnF4WRcdWeg6C0zREUNaAQg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@capacitor/ios": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.3.1.tgz", + "integrity": "sha512-BEhLyYYHWJLib4mpaPMaaylbC8meqgxbNYwQJH2svsSLW7yo/hFie+Zoo66a44XnqcMd2tvmAuzimWunXZi/xA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^8.3.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -2359,6 +2448,215 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@ionic/cli-framework-output": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz", + "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-array": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", + "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-fs": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz", + "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^8.0.0", + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-object": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz", + "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz", + "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-object": "2.1.6", + "@ionic/utils-terminal": "2.3.5", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-process/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@ionic/utils-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz", + "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-subprocess": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz", + "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-array": "2.1.6", + "@ionic/utils-fs": "3.1.7", + "@ionic/utils-process": "2.1.12", + "@ionic/utils-stream": "3.1.7", + "@ionic/utils-terminal": "2.3.5", + "cross-spawn": "^7.0.3", + "debug": "^4.0.0", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz", + "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/slice-ansi": "^4.0.0", + "debug": "^4.0.0", + "signal-exit": "^3.0.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "^2.0.1", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ionic/utils-terminal/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@ionic/utils-terminal/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ionic/utils-terminal/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2376,6 +2674,19 @@ "node": ">=12" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -4611,7 +4922,7 @@ "version": "1.13.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -4653,7 +4964,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4670,7 +4980,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4687,7 +4996,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4704,7 +5012,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4721,7 +5028,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4738,7 +5044,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4755,7 +5060,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4772,7 +5076,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4789,7 +5092,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4806,7 +5108,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4820,14 +5121,14 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@swc/types": { "version": "0.1.25", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" @@ -5057,6 +5358,16 @@ "@types/estree": "*" } }, + "node_modules/@types/fs-extra": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -5136,6 +5447,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -5595,6 +5913,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -5799,6 +6127,16 @@ "node": "*" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -5955,6 +6293,27 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "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==", + "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/baseline-browser-mapping": { "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", @@ -5965,6 +6324,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5977,6 +6346,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -6034,6 +6416,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6276,6 +6668,16 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -6845,6 +7247,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -7018,6 +7430,19 @@ "dev": true, "license": "ISC" }, + "node_modules/elementtree": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "sax": "1.1.4" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/embla-carousel": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", @@ -7065,6 +7490,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -7602,6 +8037,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -8333,6 +8778,23 @@ "node": ">=8" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", @@ -8560,6 +9022,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8892,6 +9370,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -9143,6 +9634,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -10301,14 +10802,27 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mlly": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", @@ -10364,6 +10878,32 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/native-run": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz", + "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ionic/utils-fs": "^3.1.7", + "@ionic/utils-terminal": "^2.3.4", + "bplist-parser": "^0.3.2", + "debug": "^4.3.4", + "elementtree": "^0.1.7", + "ini": "^4.1.1", + "plist": "^3.1.0", + "split2": "^4.2.0", + "through2": "^4.0.2", + "tslib": "^2.6.2", + "yauzl": "^2.10.0" + }, + "bin": { + "native-run": "bin/native-run" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -10538,6 +11078,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -10721,6 +11279,13 @@ "node": "*" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10823,6 +11388,21 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, "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", @@ -11058,6 +11638,30 @@ "dev": true, "license": "MIT" }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prompts/node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -11451,6 +12055,21 @@ "pify": "^2.3.0" } }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -11749,6 +12368,110 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "4.60.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", @@ -11844,6 +12567,27 @@ "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==", + "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/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -11886,6 +12630,13 @@ "dev": true, "license": "MIT" }, + "node_modules/sax": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "dev": true, + "license": "ISC" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -12096,6 +12847,31 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/smob": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", @@ -12204,6 +12980,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -12232,6 +13018,16 @@ "node": ">= 0.4" } }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -12654,6 +13450,33 @@ "node": ">=4" } }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -12743,6 +13566,16 @@ "node": ">=0.8" } }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -12858,6 +13691,16 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -13292,6 +14135,16 @@ "node": ">= 4.0.0" } }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", @@ -14429,6 +15282,40 @@ "node": ">=12" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -14455,6 +15342,17 @@ "node": ">= 14.6" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 609c5ec..5083d18 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "scripts": { "dev": "vite", "build": "vite build", + "build:ios": "vite build --mode ios", + "sync:ios": "npm run build:ios && npx cap sync ios", + "open:ios": "open ios/App/App.xcodeproj/project.xcworkspace", "build:dev": "vite build --mode development", "lint": "eslint .", "test": "vitest", @@ -18,6 +21,8 @@ "screenshots:headed": "playwright test screenshots.spec.ts --headed" }, "dependencies": { + "@capacitor/core": "^8.3.1", + "@capacitor/ios": "^8.3.1", "@hookform/resolvers": "^3.9.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.1.1", @@ -83,6 +88,7 @@ "@testing-library/react": "^14.0.0", "@types/node": "^22.5.5", "@types/react": "^18.3.3", + "@capacitor/cli": "^8.3.1", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.5.0", "autoprefixer": "^10.4.20", diff --git a/public/splash-screen.png b/public/splash-screen.png new file mode 100644 index 0000000..a8ae1aa Binary files /dev/null and b/public/splash-screen.png differ diff --git a/src/App.tsx b/src/App.tsx index b56debe..9b19179 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { Toaster } from "@/components/ui/toaster"; import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; -import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { HashRouter, Routes, Route, Navigate } from "react-router-dom"; import { AuthProvider } from "@/contexts/AuthContext"; import { OfflineProvider } from "@/contexts/OfflineContext"; import { TimeTrackingProvider } from "@/contexts/TimeTrackingContext"; @@ -9,6 +9,8 @@ import { useAuth } from "@/hooks/useAuth"; import { Suspense, lazy } from "react"; import { InstallPrompt } from "@/components/InstallPrompt"; import { UpdateNotification } from "@/components/UpdateNotification"; + +const isIosBuild = import.meta.env.VITE_IOS_BUILD === "true"; import { MobileNav } from "@/components/MobileNav"; // Lazy load pages for code splitting @@ -42,7 +44,7 @@ const App = () => ( - + }> } /> @@ -56,9 +58,9 @@ const App = () => ( - - - + + {!isIosBuild && } + {!isIosBuild && } diff --git a/src/components/ArchiveItem.tsx b/src/components/ArchiveItem.tsx index 1d357e2..165318e 100644 --- a/src/components/ArchiveItem.tsx +++ b/src/components/ArchiveItem.tsx @@ -30,12 +30,7 @@ import { import { MarkdownDisplay } from "@/components/MarkdownDisplay"; import { DayRecord } from "@/contexts/TimeTrackingContext"; import { useTimeTracking } from "@/hooks/useTimeTracking"; -import { - getHoursWorkedForDay as calcHoursWorked, - getBillableHoursForDay as calcBillableHours, - getNonBillableHoursForDay as calcNonBillableHours, - getRevenueForDay as calcRevenue, -} from "@/utils/calculationUtils"; +import { getDayStats } from "@/utils/calculationUtils"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@radix-ui/react-tabs"; interface ArchiveItemProps { @@ -53,18 +48,16 @@ export const ArchiveItem: React.FC = ({ day, onEdit }) => { const [showRestoreDialog, setShowRestoreDialog] = useState(false); - // Memoize per-day stats so they only recompute when the day data, - // project rates, or category billing settings change. - const dayStats = useMemo(() => ({ - hoursWorked: calcHoursWorked(day), - billableHours: calcBillableHours(day, projects, categories), - nonBillableHours: calcNonBillableHours(day, projects, categories), - revenue: calcRevenue(day, projects, categories), - }), [day, projects, categories]); - - // Build lookup maps once so the task table doesn't do O(n) searches per row. + // Build lookup maps once — shared by dayStats and the task table. const projectMap = useMemo(() => new Map(projects.map(p => [p.name, p])), [projects]); - const categoryMap = useMemo(() => new Map(categories.map(c => [c.name, c])), [categories]); + const categoryMap = useMemo(() => new Map(categories.map(c => [c.id, c])), [categories]); + + // Memoize per-day stats using the pre-built maps so they only recompute + // when the day data, project rates, or category billing settings change. + const dayStats = useMemo( + () => getDayStats(day, projectMap, categoryMap), + [day, projectMap, categoryMap] + ); // Generate daily summary only when task descriptions change. const dailySummary = useMemo(() => { diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index a9ede19..0d2f85b 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -18,6 +18,8 @@ import { formatDuration } from '@/utils/timeUtil'; import { SyncStatus } from '@/components/SyncStatus'; import { useAuth } from '@/hooks/useAuth'; +const isIosBuild = import.meta.env.VITE_IOS_BUILD === "true"; + const SiteNavigationMenu = () => { const [showExportDialog, setShowExportDialog] = useState(false); const [showProjectManagement, setShowProjectManagement] = useState(false); @@ -67,16 +69,18 @@ const SiteNavigationMenu = () => { )}
- - - - {isAuthenticated && ( + {!isIosBuild && ( + + + + )} + {!isIosBuild && isAuthenticated && ( `transition-all duration-200 flex items-center space-x-2 px-4 rounded-md h-10 bg-white border border-gray-200 hover:bg-accent hover:accent-foreground hover:border-input ... ${isActive ? 'bg-blue-200 hover:bg-accent hover:text-accent-foreground' : 'bg-white'}` @@ -104,11 +108,13 @@ const SiteNavigationMenu = () => { Settings - -
- setShowAuthDialog(true)} /> -
-
+ {!isIosBuild && ( + +
+ setShowAuthDialog(true)} /> +
+
+ )}
@@ -117,10 +123,12 @@ const SiteNavigationMenu = () => { - setShowAuthDialog(false)} - /> + {!isIosBuild && ( + setShowAuthDialog(false)} + /> + )} item.completed); // Gather checklist items from current-day task descriptions - const taskChecklists = isDayStarted - ? tasks - .map((task) => ({ - task, - entries: parseTaskChecklist(task.description ?? "") - })) - .filter(({ entries }) => entries.length > 0) - : []; + const taskChecklists = useMemo(() => { + if (!isDayStarted) return []; + return tasks + .map((task) => ({ task, entries: parseTaskChecklist(task.description ?? "") })) + .filter(({ entries }) => entries.length > 0); + }, [isDayStarted, tasks]); function handleAdd() { const trimmed = inputValue.trim(); diff --git a/src/contexts/TimeTrackingContext.tsx b/src/contexts/TimeTrackingContext.tsx index 759f92c..d40edc7 100644 --- a/src/contexts/TimeTrackingContext.tsx +++ b/src/contexts/TimeTrackingContext.tsx @@ -234,11 +234,18 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ const lastSavedStateRef = useRef(''); // Track last saved state to prevent duplicate saves const currentAuthStateRef = useRef(false); // Track current auth state without triggering re-renders + // Ref-based access to dataService for todo save effect (avoids adding dataService + // to todo callback deps, which would invalidate them on every auth change). + const dataServiceRef = useRef(null); + // Guards against saving todos during the initial data load. + const todoLoadedRef = useRef(false); + // Initialize data service when auth state changes useEffect(() => { if (!authLoading) { const service = createDataService(isAuthenticated); setDataService(service); + dataServiceRef.current = service; currentAuthStateRef.current = isAuthenticated; } }, [isAuthenticated, authLoading]); @@ -327,6 +334,9 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ if (currentAuthStateRef.current && dataService) { await dataService.migrateFromLocalStorage(); } + + // Allow todo save effect to fire for user-initiated changes going forward + todoLoadedRef.current = true; } catch (error) { console.error('Error loading data:', error); } finally { @@ -917,7 +927,16 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ ): InvoiceData => utilGenerateInvoiceData(archivedDays, projects, categories, clientName, startDate, endDate); - const addTodoItem = useCallback(async (text: string) => { + // Persist todos whenever todoItems changes due to a user action. + // Uses refs so this effect doesn't recreate todo callbacks on every change. + useEffect(() => { + if (!todoLoadedRef.current || !dataServiceRef.current) return; + dataServiceRef.current.saveTodos(todoItems); + }, [todoItems]); + + // Stable callbacks — no todoItems in deps. Functional updates ensure each + // callback always operates on the latest state without closing over it. + const addTodoItem = useCallback((text: string) => { const trimmed = text.trim(); if (!trimmed) return; const newItem: TodoItem = { @@ -926,36 +945,24 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({ completed: false, createdAt: new Date().toISOString() }; - const updated = [...todoItems, newItem]; - setTodoItems(updated); - if (dataService) await dataService.saveTodos(updated); - }, [todoItems, dataService]); + setTodoItems(prev => [...prev, newItem]); + }, []); - const toggleTodoItem = useCallback(async (id: string) => { - const updated = todoItems.map((item) => { + const toggleTodoItem = useCallback((id: string) => { + setTodoItems(prev => prev.map(item => { if (item.id !== id) return item; const nowCompleted = !item.completed; - return { - ...item, - completed: nowCompleted, - completedAt: nowCompleted ? new Date().toISOString() : undefined - }; - }); - setTodoItems(updated); - if (dataService) await dataService.saveTodos(updated); - }, [todoItems, dataService]); - - const deleteTodoItem = useCallback(async (id: string) => { - const updated = todoItems.filter((item) => item.id !== id); - setTodoItems(updated); - if (dataService) await dataService.saveTodos(updated); - }, [todoItems, dataService]); - - const clearCompletedTodos = useCallback(async () => { - const updated = todoItems.filter((item) => !item.completed); - setTodoItems(updated); - if (dataService) await dataService.saveTodos(updated); - }, [todoItems, dataService]); + return { ...item, completed: nowCompleted, completedAt: nowCompleted ? new Date().toISOString() : undefined }; + })); + }, []); + + const deleteTodoItem = useCallback((id: string) => { + setTodoItems(prev => prev.filter(item => item.id !== id)); + }, []); + + const clearCompletedTodos = useCallback(() => { + setTodoItems(prev => prev.filter(item => !item.completed)); + }, []); const importFromCSV = async ( csvContent: string diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index d176133..3f02eac 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -72,16 +72,12 @@ let lastProjectsCheck: Date | null = null; let lastCategoriesCheck: Date | null = null; const DATA_CACHE_DURATION = 5 * 60 * 1000; // 5 minutes -export const getCachedProjects = async (): Promise => { - const now = new Date(); - - // Check if cache is still valid +export const getCachedProjects = (): Project[] | null => { if (cachedProjects && lastProjectsCheck && - (now.getTime() - lastProjectsCheck.getTime()) < DATA_CACHE_DURATION) { + (Date.now() - lastProjectsCheck.getTime()) < DATA_CACHE_DURATION) { return cachedProjects; } - - return null; // Cache miss + return null; }; export const setCachedProjects = (projects: Project[]) => { @@ -89,16 +85,12 @@ export const setCachedProjects = (projects: Project[]) => { lastProjectsCheck = new Date(); }; -export const getCachedCategories = async (): Promise => { - const now = new Date(); - - // Check if cache is still valid +export const getCachedCategories = (): TaskCategory[] | null => { if (cachedCategories && lastCategoriesCheck && - (now.getTime() - lastCategoriesCheck.getTime()) < DATA_CACHE_DURATION) { + (Date.now() - lastCategoriesCheck.getTime()) < DATA_CACHE_DURATION) { return cachedCategories; } - - return null; // Cache miss + return null; }; export const setCachedCategories = (categories: TaskCategory[]) => { @@ -128,7 +120,7 @@ export const trackDbCall = (operation: string, table?: string, source?: string) timestamp, operation, table, - source: source || new Error().stack?.split('\n')[2]?.trim() // Capture call stack for debugging + source: source || (ENABLE_DB_LOGGING ? new Error().stack?.split('\n')[2]?.trim() : undefined) }; dbCallLog.push(logEntry); @@ -146,7 +138,7 @@ export const trackAuthCall = (operation: string, source?: string) => { const logEntry = { timestamp, operation, - source: source || new Error().stack?.split('\n')[2]?.trim() + source: source || (ENABLE_DB_LOGGING ? new Error().stack?.split('\n')[2]?.trim() : undefined) }; dbCallLog.push(logEntry); if (dbCallLog.length > 100) { diff --git a/src/pages/Archive.tsx b/src/pages/Archive.tsx index 594b41e..b39506b 100644 --- a/src/pages/Archive.tsx +++ b/src/pages/Archive.tsx @@ -4,12 +4,7 @@ import { DayRecord } from '@/contexts/TimeTrackingContext'; import { useTimeTracking } from '@/hooks/useTimeTracking'; -import { - getHoursWorkedForDay as calcHoursWorked, - getBillableHoursForDay as calcBillableHours, - getNonBillableHoursForDay as calcNonBillableHours, - getRevenueForDay as calcRevenue -} from '@/utils/calculationUtils'; +import { getDayStats } from '@/utils/calculationUtils'; import { ArchiveItem } from '@/components/ArchiveItem'; import { ArchiveEditDialog } from '@/components/ArchiveEditDialog'; import { ExportDialog } from '@/components/ExportDialog'; @@ -85,14 +80,23 @@ const ArchiveContent: React.FC = () => { ); }, [archivedDays, filters]); - // Calculate summary stats based on filtered days — memoized so they only - // recompute when filteredDays, projects, or categories actually change. - const { totalHoursWorked, totalBillableHours, totalNonBillableHours, totalRevenue } = useMemo(() => ({ - totalHoursWorked: filteredDays.reduce((sum, day) => sum + calcHoursWorked(day), 0), - totalBillableHours: filteredDays.reduce((sum, day) => sum + calcBillableHours(day, projects, categories), 0), - totalNonBillableHours: filteredDays.reduce((sum, day) => sum + calcNonBillableHours(day, projects, categories), 0), - totalRevenue: filteredDays.reduce((sum, day) => sum + calcRevenue(day, projects, categories), 0) - }), [filteredDays, projects, categories]); + // Calculate summary stats in a single pass — builds maps once and iterates filteredDays once. + const { totalHoursWorked, totalBillableHours, totalNonBillableHours, totalRevenue } = useMemo(() => { + const projectMap = new Map(projects.map(p => [p.name, p])); + const categoryMap = new Map(categories.map(c => [c.id, c])); + return filteredDays.reduce( + (acc, day) => { + const stats = getDayStats(day, projectMap, categoryMap); + return { + totalHoursWorked: acc.totalHoursWorked + stats.hoursWorked, + totalBillableHours: acc.totalBillableHours + stats.billableHours, + totalNonBillableHours: acc.totalNonBillableHours + stats.nonBillableHours, + totalRevenue: acc.totalRevenue + stats.revenue + }; + }, + { totalHoursWorked: 0, totalBillableHours: 0, totalNonBillableHours: 0, totalRevenue: 0 } + ); + }, [filteredDays, projects, categories]); const handleEdit = (day: DayRecord) => { setEditingDay(day); diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index d06b21d..d468f00 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -10,7 +10,10 @@ import { CirclePlay, CircleStop, Archive as Play } from 'lucide-react'; import { DashboardIcon } from '@radix-ui/react-icons'; import { PageLayout } from "@/components/PageLayout"; import { TaskTrackingPanel } from '@/components/TaskTrackingPanel'; -import { useState } from 'react'; +import { useState, useMemo } from 'react'; + +// Stable epoch constant — avoids creating new Date(0) on every render +const EPOCH = new Date(0); const TimeTrackerContent = () => { const { @@ -18,13 +21,15 @@ const TimeTrackerContent = () => { dayStartTime, currentTask, tasks, + archivedDays, startDay, endDay, startNewTask, deleteTask, postDay, getTotalDayDuration, - getCurrentTaskDuration + getCurrentTaskDuration, + getTotalHoursForPeriod } = useTimeTracking(); const [showStartDayDialog, setShowStartDayDialog] = useState(false); @@ -60,13 +65,14 @@ const TimeTrackerContent = () => { const handlePostDay = () => { postDay(); }; - const { archivedDays, getTotalHoursForPeriod } = useTimeTracking(); - const totalHours = - archivedDays.length > 0 - ? getTotalHoursForPeriod(new Date(0), new Date()) - : 0; - const sortedDays = [...archivedDays].sort( - (a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime() + + const totalHours = useMemo( + () => archivedDays.length > 0 ? getTotalHoursForPeriod(EPOCH, new Date()) : 0, + [archivedDays, getTotalHoursForPeriod] + ); + const sortedDays = useMemo( + () => [...archivedDays].sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()), + [archivedDays] ); // Calculate running timer for navigation diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 5c62887..481a0bf 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -31,7 +31,8 @@ const SettingsContent: React.FC = () => { const handleClearAllData = () => { localStorage.clear(); - window.location.reload(); + // Navigate to root instead of reload() so Capacitor's JS bridge is not interrupted + window.location.replace(window.location.pathname); }; return ( diff --git a/src/services/supabaseService.ts b/src/services/supabaseService.ts index 5814a14..120ab23 100644 --- a/src/services/supabaseService.ts +++ b/src/services/supabaseService.ts @@ -94,8 +94,10 @@ export class SupabaseService implements DataService { const user = await this.requireUser(); - const categories = (await getCachedCategories()) || []; - const projects = (await getCachedProjects()) || []; + const categories = getCachedCategories() ?? []; + const projects = getCachedProjects() ?? []; + const categoryMap = new Map(categories.map(c => [c.id, c])); + const projectMap = new Map(projects.map(p => [p.name, p])); try { // 1. Save current day state @@ -158,8 +160,8 @@ export class SupabaseService implements DataService { // 4. Upsert current tasks (single batch operation) const tasksToUpsert = data.tasks.map((task) => { - const category = categories.find(c => c.id === task.category); - const project = projects.find(p => p.name === task.project); + const category = categoryMap.get(task.category ?? ""); + const project = projectMap.get(task.project ?? ""); return { id: task.id, @@ -262,8 +264,10 @@ export class SupabaseService implements DataService { const user = await this.requireUser(); - const categories = (await getCachedCategories()) || []; - const projects = (await getCachedProjects()) || []; + const categories = getCachedCategories() ?? []; + const projects = getCachedProjects() ?? []; + const categoryMap = new Map(categories.map(c => [c.id, c])); + const projectMap = new Map(projects.map(p => [p.name, p])); if (days.length === 0) { await supabase.from("tasks").delete().eq("user_id", user.id).eq("is_current", false); @@ -283,8 +287,8 @@ export class SupabaseService implements DataService { const allTasks = days.flatMap((day) => day.tasks.map((task) => { - const category = categories.find(c => c.id === task.category); - const project = projects.find(p => p.name === task.project); + const category = categoryMap.get(task.category ?? ""); + const project = projectMap.get(task.project ?? ""); return { id: task.id, @@ -514,8 +518,10 @@ export class SupabaseService implements DataService { async updateArchivedDay(dayId: string, updates: Partial): Promise { const user = await this.requireUser(); - const categories = await getCachedCategories(); - const projects = await getCachedProjects(); + const categories = getCachedCategories() ?? []; + const projects = getCachedProjects() ?? []; + const categoryMap = new Map(categories.map(c => [c.id, c])); + const projectMap = new Map(projects.map(p => [p.name, p])); const updateData: Record = {}; @@ -562,8 +568,8 @@ export class SupabaseService implements DataService { if (updates.tasks.length > 0) { const tasksToUpsert = updates.tasks.map((task) => { - const category = categories.find(c => c.id === task.category); - const project = projects.find(p => p.name === task.project); + const category = categoryMap.get(task.category ?? ""); + const project = projectMap.get(task.project ?? ""); return { id: task.id, diff --git a/src/utils/calculationUtils.ts b/src/utils/calculationUtils.ts index 57f4570..9f89911 100644 --- a/src/utils/calculationUtils.ts +++ b/src/utils/calculationUtils.ts @@ -17,6 +17,52 @@ function isTaskBillable( return projectIsBillable && categoryIsBillable; } +export interface DayStats { + hoursWorked: number; + billableHours: number; + nonBillableHours: number; + revenue: number; +} + +/** + * Computes all four day stats in a single pass with pre-built maps. + * Prefer this over calling the individual functions separately when + * you need multiple stats for the same day. + */ +export function getDayStats( + day: DayRecord, + projectMap: Map, + categoryMap: Map +): DayStats { + let totalMs = 0; + let billableMs = 0; + let nonBillableMs = 0; + let revenue = 0; + + day.tasks.forEach(task => { + if (!task.duration) return; + totalMs += task.duration; + if (task.project && task.category) { + if (isTaskBillable(task, projectMap, categoryMap)) { + billableMs += task.duration; + const project = projectMap.get(task.project); + if (project?.hourlyRate) { + revenue += (task.duration / 3600000) * project.hourlyRate; + } + } else { + nonBillableMs += task.duration; + } + } + }); + + return { + hoursWorked: Math.round((totalMs / 3600000) * 100) / 100, + billableHours: Math.round((billableMs / 3600000) * 100) / 100, + nonBillableHours: Math.round((nonBillableMs / 3600000) * 100) / 100, + revenue: Math.round(revenue * 100) / 100 + }; +} + export function getHoursWorkedForDay(day: DayRecord): number { let totalTaskDuration = 0; day.tasks.forEach(task => { diff --git a/src/utils/checklistUtils.ts b/src/utils/checklistUtils.ts index 0eb18be..398421f 100644 --- a/src/utils/checklistUtils.ts +++ b/src/utils/checklistUtils.ts @@ -16,14 +16,11 @@ export function parseTaskChecklist(description: string): ChecklistEntry[] { if (!description) return []; return description.split("\n").flatMap((line, lineIndex) => { + if (!line.includes("- [")) return []; // fast path: skip non-checklist lines const unchecked = line.match(/^(\s*)-\s\[ \]\s(.+)/); + if (unchecked) return [{ text: unchecked[2].trim(), completed: false, lineIndex }]; const checked = line.match(/^(\s*)-\s\[x\]\s(.+)/i); - if (unchecked) { - return [{ text: unchecked[2].trim(), completed: false, lineIndex }]; - } - if (checked) { - return [{ text: checked[2].trim(), completed: true, lineIndex }]; - } + if (checked) return [{ text: checked[2].trim(), completed: true, lineIndex }]; return []; }); } diff --git a/src/utils/exportUtils.ts b/src/utils/exportUtils.ts index 12a8c1b..c41750d 100644 --- a/src/utils/exportUtils.ts +++ b/src/utils/exportUtils.ts @@ -41,14 +41,17 @@ export function exportToCSV( ]; const rows = [headers.join(",")]; + const projectMap = new Map(projects.map(p => [p.name, p])); + const categoryMap = new Map(categories.map(c => [c.id, c])); + filteredDays.forEach(day => { const dayDescriptions = day.tasks.filter(t => t.description).map(t => t.description!); const dailySummary = generateDailySummary(dayDescriptions); day.tasks.forEach(task => { if (task.duration) { - const project = projects.find(p => p.name === task.project); - const category = categories.find(c => c.id === task.category); + const project = projectMap.get(task.project ?? ""); + const category = categoryMap.get(task.category ?? ""); const startTimeISO = task.startTime.toISOString(); const endTimeISO = task.endTime?.toISOString() || ""; @@ -261,6 +264,9 @@ export function parseCSVImport( }; } + // Build lookup map once before the row loop + const categoryByNameMap = new Map(categories.map(c => [c.name, c])); + const tasksByDay: { [dayId: string]: { tasks: Task[]; dayRecord: Partial }; } = {}; @@ -305,7 +311,7 @@ export function parseCSVImport( continue; } - const categoryByName = categories.find(c => c.name === taskData.category_name); + const categoryByName = categoryByNameMap.get(taskData.category_name); const categoryId = categoryByName?.id || taskData.category_id || undefined; const task: Task = { diff --git a/src/utils/reportUtils.ts b/src/utils/reportUtils.ts index 1bbd437..cd8a730 100644 --- a/src/utils/reportUtils.ts +++ b/src/utils/reportUtils.ts @@ -108,6 +108,9 @@ export function weekKey(weekStart: Date): string { export function groupByCalendarWeek(days: ArchivedDay[]): WeekGroup[] { const map = new Map(); + // Track unique projects per week with a Set to avoid O(n²) Array.includes + const projectSets = new Map>(); + for (const day of days) { const dayDate = new Date(day.date); if (isNaN(dayDate.getTime())) continue; @@ -125,20 +128,24 @@ export function groupByCalendarWeek(days: ArchivedDay[]): WeekGroup[] { totalDuration: 0, projects: [] }); + projectSets.set(key, new Set()); } const group = map.get(key)!; + const projectSet = projectSets.get(key)!; group.days.push(day); group.totalDuration += day.totalDuration; - // Collect unique non-empty project names for (const task of day.tasks) { - if (task.project && !group.projects.includes(task.project)) { - group.projects.push(task.project); - } + if (task.project) projectSet.add(task.project); } } + // Materialise unique project lists from sets + for (const [key, group] of map) { + group.projects = Array.from(projectSets.get(key) ?? []); + } + // Sort days within each week chronologically for (const group of map.values()) { group.days.sort( @@ -173,18 +180,18 @@ export function groupByDateRange( (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() ); - const projects: string[] = []; + const projectSet = new Set(); let totalDuration = 0; for (const day of filtered) { totalDuration += day.totalDuration; for (const task of day.tasks) { - if (task.project && !projects.includes(task.project)) { - projects.push(task.project); - } + if (task.project) projectSet.add(task.project); } } + const projects = Array.from(projectSet); + return { weekStart: new Date(from), weekEnd: new Date(to), diff --git a/src/utils/timeUtil.ts b/src/utils/timeUtil.ts index 6f28cae..031430c 100644 --- a/src/utils/timeUtil.ts +++ b/src/utils/timeUtil.ts @@ -25,30 +25,17 @@ export const formatDurationLong = (milliseconds: number): string => { } }; -export const formatTime = (date: Date): string => { - return date.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - hour12: true - }); -}; +// Pre-constructed formatters — Intl.DateTimeFormat is expensive to construct +// but cheap to reuse. Module-level constants are created once per page load. +const _timeFormatter = new Intl.DateTimeFormat('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); +const _dateFormatter = new Intl.DateTimeFormat('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); +const _dateShortFormatter = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); -export const formatDate = (date: Date): string => { - return date.toLocaleDateString('en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' - }); -}; +export const formatTime = (date: Date): string => _timeFormatter.format(date); -export const formatDateShort = (date: Date): string => { - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }); -}; +export const formatDate = (date: Date): string => _dateFormatter.format(date); + +export const formatDateShort = (date: Date): string => _dateShortFormatter.format(date); export const formatHoursDecimal = (milliseconds: number): number => { return Math.round((milliseconds / (1000 * 60 * 60)) * 100) / 100; diff --git a/tsconfig.app.json b/tsconfig.app.json index 0b0e43e..3dc94cc 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -6,7 +6,6 @@ "module": "ESNext", "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, @@ -14,7 +13,6 @@ "noEmit": true, "jsx": "react-jsx", - /* Linting */ "strict": false, "noUnusedLocals": false, "noUnusedParameters": false, diff --git a/tsconfig.node.json b/tsconfig.node.json index 3133162..752e594 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -5,14 +5,12 @@ "module": "ESNext", "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", "noEmit": true, - /* Linting */ "strict": true, "noUnusedLocals": false, "noUnusedParameters": false, diff --git a/vite.config.ts b/vite.config.ts index 1f01e5e..d44cb50 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,14 @@ -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react-swc"; import path from "path"; import { VitePWA } from "vite-plugin-pwa"; // https://vitejs.dev/config/ -export default defineConfig(({ mode }) => ({ +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd()); + const isIosBuild = env.VITE_IOS_BUILD === "true"; + + return { server: { host: "::", port: 8080 @@ -12,6 +16,7 @@ export default defineConfig(({ mode }) => ({ plugins: [ react(), VitePWA({ + disable: isIosBuild, registerType: "autoUpdate", includeAssets: [ "favicon.svg", @@ -106,4 +111,5 @@ export default defineConfig(({ mode }) => ({ ], passWithNoTests: true // Don't fail when no test files are found } -})); + }; +});